diff --git a/packages/mcp-core/src/server.all-tools.test.ts b/packages/mcp-core/src/server.all-tools.test.ts index 4902ffbdd..d7d2d5f2a 100644 --- a/packages/mcp-core/src/server.all-tools.test.ts +++ b/packages/mcp-core/src/server.all-tools.test.ts @@ -35,8 +35,9 @@ function getContext(overrides: Partial = {}): ServerContext { async function withClient( context: ServerContext, run: (client: Client) => Promise, + experimentalMode = false, ): Promise { - const server = buildServer({ context }); + const server = buildServer({ context, experimentalMode }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client({ name: "all-tools-test", version: "1.0.0" }); @@ -56,6 +57,43 @@ async function listToolNames(context: ServerContext): Promise { ); } +/** + * Paths of `type: "array"` nodes whose `items` is not a schema object. + * + * VS Code Copilot rejects a tool at registration when any array lacks a + * truthy `items`, and GPT models need an item type. `z.tuple` emits + * `prefixItems` with `items: false`, which trips this. + */ +function findArraysWithoutItemSchema( + schema: unknown, + path: string[] = [], +): string[] { + if (!schema || typeof schema !== "object") { + return []; + } + if (Array.isArray(schema)) { + return schema.flatMap((item, index) => + findArraysWithoutItemSchema(item, [...path, String(index)]), + ); + } + + const record = schema as Record; + const types = Array.isArray(record.type) ? record.type : [record.type]; + const items = record.items; + const violations = + types.includes("array") && + (!items || typeof items !== "object" || Array.isArray(items)) + ? [path.join(".") || "(root)"] + : []; + + return [ + ...violations, + ...Object.entries(record).flatMap(([key, value]) => + findArraysWithoutItemSchema(value, [...path, key]), + ), + ]; +} + // server.test.ts covers the gateway surface used when the flag is off. describe.skipIf(!EXPOSE_ALL_TOOLS)("buildServer with EXPOSE_ALL_TOOLS", () => { it("lists every skill-granted tool and drops the catalog gateway", async () => { @@ -97,6 +135,25 @@ describe.skipIf(!EXPOSE_ALL_TOOLS)("buildServer with EXPOSE_ALL_TOOLS", () => { } }); + it.each([false, true])( + "gives every array in tools/list an item schema (experimentalMode=%s)", + async (experimentalMode) => { + const listed = await withClient( + getContext(), + async (client) => (await client.listTools()).tools, + experimentalMode, + ); + + expect(listed.length).toBeGreaterThan(0); + const violations = listed.flatMap((tool) => + findArraysWithoutItemSchema(tool.inputSchema).map( + (path) => `${tool.name}: ${path}`, + ), + ); + expect(violations).toEqual([]); + }, + ); + it("lists the replacements of deprecated tools instead of the tools", async () => { const toolNames = await listToolNames(getContext()); const deprecatedTools = Object.values(tools).filter( diff --git a/packages/mcp-core/src/toolDefinitions.json b/packages/mcp-core/src/toolDefinitions.json index 399398b30..8876e1d16 100644 --- a/packages/mcp-core/src/toolDefinitions.json +++ b/packages/mcp-core/src/toolDefinitions.json @@ -1414,8 +1414,9 @@ }, "url": { "type": "string", + "maxLength": 255, "format": "uri", - "description": "URL to check." + "description": "URL to check (max 255 characters)." }, "intervalSeconds": { "type": "integer", @@ -1438,18 +1439,12 @@ "headers": { "type": "array", "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "additionalItems": false, "minItems": 2, - "maxItems": 2 + "maxItems": 2, + "type": "array", + "items": { + "type": "string" + } }, "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Accept\",\"application/json\"]]." }, @@ -15174,6 +15169,7 @@ }, "url": { "type": "string", + "maxLength": 255, "format": "uri" }, "intervalSeconds": { @@ -15196,18 +15192,12 @@ "headers": { "type": "array", "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "type": "string" - } - ], - "additionalItems": false, "minItems": 2, - "maxItems": 2 + "maxItems": 2, + "type": "array", + "items": { + "type": "string" + } }, "description": "Optional HTTP headers as an array of [name, value] pairs, e.g. [[\"Accept\",\"application/json\"]]." }, diff --git a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts index 9c59ab45e..604f4ac8f 100644 --- a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts +++ b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.test.ts @@ -63,6 +63,7 @@ describe("create_uptime_monitor", () => { intervalSeconds: 300, timeoutMs: 8000, method: "GET", + headers: [["Accept", "application/json"]], environment: "production", }, context, @@ -74,6 +75,7 @@ describe("create_uptime_monitor", () => { intervalSeconds: 300, timeoutMs: 8000, method: "GET", + headers: [["Accept", "application/json"]], environment: "production", }); expect(requestBody).not.toHaveProperty("interval_seconds"); @@ -108,6 +110,15 @@ describe("create_uptime_monitor", () => { `); }); + it("accepts only [name, value] header pairs in the input schema", () => { + const { headers } = createUptimeMonitor.inputSchema; + expect(headers.safeParse([["Accept", "application/json"]]).success).toBe( + true, + ); + expect(headers.safeParse([["Accept"]]).success).toBe(false); + expect(headers.safeParse([["Accept", "a", "b"]]).success).toBe(false); + }); + it("rejects empty owner in the input schema", () => { const result = createUptimeMonitor.inputSchema.owner.safeParse(" "); expect(result.success).toBe(false); diff --git a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts index 85f431434..a156974e4 100644 --- a/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts +++ b/packages/mcp-core/src/tools/catalog/create-uptime-monitor.ts @@ -17,6 +17,7 @@ import { ParamUptimeMonitorStatus, ParamUptimeOwner, ParamUptimeTimeoutMs, + toUptimeHeaderPairs, toUptimeMonitorSummary, uptimeMonitorSummarySchema, } from "./support/uptime-monitors"; @@ -52,7 +53,11 @@ export default defineTool({ regionUrl: ParamRegionUrl.nullable().default(null), projectSlug: ParamProjectSlug, name: z.string().trim().min(1).max(128).describe("Monitor display name."), - url: z.string().url().describe("URL to check."), + url: z + .string() + .url() + .max(255) + .describe("URL to check (max 255 characters)."), intervalSeconds: ParamUptimeIntervalSeconds, timeoutMs: ParamUptimeTimeoutMs.default(5000), method: ParamUptimeHttpMethod.optional(), @@ -118,7 +123,7 @@ export default defineTool({ intervalSeconds: params.intervalSeconds, timeoutMs: params.timeoutMs, method: params.method, - headers: params.headers, + headers: params.headers && toUptimeHeaderPairs(params.headers), body: params.body, status: params.status, owner: params.owner, diff --git a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts index dbb973a18..7c8cf5570 100644 --- a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts +++ b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts @@ -51,12 +51,25 @@ export const ParamUptimeTimeoutMs = z .max(60_000) .describe("Request timeout in milliseconds (1000-60000)."); +/** + * Header pairs, matching HEADERS_LIST_SCHEMA in getsentry/sentry + * (src/sentry/uptime/endpoints/validators.py). + * + * Not `z.tuple`: it emits `items: false` plus `prefixItems`, which VS Code + * Copilot rejects at registration ("array type must have items"). + */ export const ParamUptimeHeaders = z - .array(z.tuple([z.string(), z.string()])) + .array(z.array(z.string()).length(2)) .describe( 'Optional HTTP headers as an array of [name, value] pairs, e.g. [["Accept","application/json"]].', ); +export function toUptimeHeaderPairs( + headers: string[][], +): Array<[string, string]> { + return headers.map(([name, value]) => [name, value]); +} + /** Owner actor for create/update. Empty/whitespace rejected. */ export const ParamUptimeOwner = z .string() diff --git a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts index d29125084..5be848f6a 100644 --- a/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts +++ b/packages/mcp-core/src/tools/catalog/update-uptime-monitor.ts @@ -18,6 +18,7 @@ import { ParamUptimeMonitorStatus, ParamUptimeOwner, ParamUptimeTimeoutMs, + toUptimeHeaderPairs, toUptimeMonitorSummary, uptimeMonitorSummarySchema, } from "./support/uptime-monitors"; @@ -65,7 +66,7 @@ export default defineTool({ .min(1) .describe("Uptime monitor ID (detector id)."), name: z.string().trim().min(1).max(128).optional(), - url: z.string().url().optional(), + url: z.string().url().max(255).optional(), intervalSeconds: ParamUptimeIntervalSeconds.optional(), timeoutMs: ParamUptimeTimeoutMs.optional(), method: ParamUptimeHttpMethod.optional(), @@ -146,7 +147,7 @@ export default defineTool({ intervalSeconds: params.intervalSeconds, timeoutMs: params.timeoutMs, method: params.method, - headers: params.headers, + headers: params.headers && toUptimeHeaderPairs(params.headers), body: params.body, status: params.status, owner: params.owner,