From 62952974126b82ce2947afc33206923ea9f2f3e6 Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:55:27 +0000 Subject: [PATCH 1/2] Preserve legacy capability reports --- README.md | 2 +- src/lib/mcp/analytics.test.ts | 50 ++++++++++++- src/lib/mcp/analytics.ts | 12 +++ src/lib/mcp/tools/missing-capability.test.ts | 44 +++++++++++ src/lib/mcp/tools/missing-capability.ts | 77 ++++++++++++++++---- 5 files changed, 166 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 734b371..263c8de 100644 --- a/README.md +++ b/README.md @@ -334,7 +334,7 @@ See [Vault payments](docs/vault-payments.md) for both provider flows, safety rul - `webmcp` - List native page tools across every tab and frame in a browser, then synchronously invoke an exact opaque `tool_ref` with structured input. - `exec_command` - Run shell commands inside a browser VM. Returns decoded stdout/stderr. - `search_docs` - Search Kernel platform documentation and guides. -- `get_more_tools` - Report a structured KERNEL capability or external-integration gap after checking the available tools. Existing-tool failures, transient capacity errors, and client permission restrictions are rejected from capability-demand analytics. Accepted requests emit `mcp_capability_requested`; historical unstructured requests remain under `$mcp_missing_capability`. +- `get_more_tools` - Report a structured KERNEL capability or external-integration gap after checking the available tools. Existing-tool failures, transient capacity errors, and client permission restrictions are rejected from capability-demand analytics. Accepted requests emit `mcp_capability_requested`; clients using the previous context-only schema receive a non-recording refresh response instead of a tool error. - `submit_feedback` - Send product, bot-detection, config-registry, MCP, or documentation feedback directly to the KERNEL team without interrupting the current task. Reports include a normalized task outcome; MCP reports identify one KERNEL-owned tool. Config-registry reports connect exactly one observed outcome to the browser session, recommendation metadata and evidence, and unchanged browser and proxy settings. - `open_auth_login` - Open a secure interactive Managed Auth MCP App after user consent. Registered only for clients that declare MCP Apps support; credentials and MFA never enter MCP/model traffic. diff --git a/src/lib/mcp/analytics.test.ts b/src/lib/mcp/analytics.test.ts index da77156..421a594 100644 --- a/src/lib/mcp/analytics.test.ts +++ b/src/lib/mcp/analytics.test.ts @@ -343,6 +343,33 @@ describe("sanitizeMcpAnalyticsEvent", () => { expect(result?.properties[PostHogMCPAnalyticsProperty.IsError]).toBe(false); }); + test("classifies get_more_tools schema rejections as validation errors", async () => { + const event = toolCallEvent({ + [PostHogMCPAnalyticsProperty.ToolName]: "get_more_tools", + [PostHogMCPAnalyticsProperty.IsError]: true, + [PostHogMCPAnalyticsProperty.ErrorType]: "Error", + [PostHogMCPAnalyticsProperty.ErrorMessage]: + "Input validation error: Invalid arguments for tool get_more_tools", + }); + + const result = await sanitizeMcpAnalyticsEvent(event); + + expect(result?.properties[PostHogMCPAnalyticsProperty.ErrorType]).toBe( + "validation", + ); + + const runtimeError = toolCallEvent({ + [PostHogMCPAnalyticsProperty.ToolName]: "get_more_tools", + [PostHogMCPAnalyticsProperty.IsError]: true, + [PostHogMCPAnalyticsProperty.ErrorType]: "Error", + [PostHogMCPAnalyticsProperty.ErrorMessage]: "handler failed", + }); + const runtimeResult = await sanitizeMcpAnalyticsEvent(runtimeError); + expect( + runtimeResult?.properties[PostHogMCPAnalyticsProperty.ErrorType], + ).toBe("Error"); + }); + test("drops vault specs, aliases, provider actions, and error bodies", async () => { const event = toolCallEvent({ [PostHogMCPAnalyticsProperty.ToolName]: "manage_vault_cards", @@ -1013,14 +1040,18 @@ describe("instrumentMcpAnalytics (SDK integration)", () => { expect(missingCapabilityTool?.description).toContain( "client-side permission restriction", ); - expect(missingCapabilityTool?.inputSchema.required).toEqual([ - "context", + expect(missingCapabilityTool?.inputSchema.required).toEqual(["context"]); + const capabilityProperties = missingCapabilityTool?.inputSchema + .properties as Record; + for (const field of [ "gap_reason", "capability_area", "capability", "requested_action", "task_outcome", - ]); + ]) { + expect(capabilityProperties[field]).toBeDefined(); + } const disabledMissingCapabilityTool = ( await disabled.client.listTools() ).tools.find(({ name }) => name === "get_more_tools"); @@ -1028,6 +1059,19 @@ describe("instrumentMcpAnalytics (SDK integration)", () => { missingCapabilityTool?.inputSchema, ); + const legacyRequest = await enabled.client.callTool({ + name: "get_more_tools", + arguments: { + context: + "Reporting a capability through the previous contract while refreshing the available tool definitions.", + }, + }); + expect(legacyRequest.isError).not.toBe(true); + expect(toolResultJSON(legacyRequest)).toMatchObject({ + recorded: false, + status: "legacy_schema_refresh_required", + }); + const unavailableRequest = await disabled.client.callTool({ name: "get_more_tools", arguments: { diff --git a/src/lib/mcp/analytics.ts b/src/lib/mcp/analytics.ts index 2859acf..c1fb762 100644 --- a/src/lib/mcp/analytics.ts +++ b/src/lib/mcp/analytics.ts @@ -19,6 +19,7 @@ import { registerFeedbackTool, } from "@/lib/mcp/tools/feedback"; import { + KERNEL_MISSING_CAPABILITY_TOOL_NAME, type MissingCapabilityReport, registerMissingCapabilityTool, } from "@/lib/mcp/tools/missing-capability"; @@ -360,6 +361,17 @@ export const sanitizeMcpAnalyticsEvent: BeforeSendFn = (event) => { enrichMcpAnalyticsEvent(event); if (event.event === PostHogMCPAnalyticsEvent.ToolCall) { annotateProjectParamUsage(properties); + const errorMessage = properties[PostHogMCPAnalyticsProperty.ErrorMessage]; + if ( + properties[PostHogMCPAnalyticsProperty.ToolName] === + KERNEL_MISSING_CAPABILITY_TOOL_NAME && + properties[PostHogMCPAnalyticsProperty.IsError] === true && + properties[PostHogMCPAnalyticsProperty.ErrorType] === "Error" && + typeof errorMessage === "string" && + errorMessage.includes("Input validation error") + ) { + properties[PostHogMCPAnalyticsProperty.ErrorType] = "validation"; + } } for (const key of Object.keys(properties)) { diff --git a/src/lib/mcp/tools/missing-capability.test.ts b/src/lib/mcp/tools/missing-capability.test.ts index c162f84..f140bbe 100644 --- a/src/lib/mcp/tools/missing-capability.test.ts +++ b/src/lib/mcp/tools/missing-capability.test.ts @@ -76,6 +76,50 @@ describe("get_more_tools", () => { } }); + test("accepts the previous context-only contract without recording demand", async () => { + const captured: MissingCapabilityReport[] = []; + const { client, close } = await connectTestMcp( + (server) => + registerMissingCapabilityTool(server, (report) => { + captured.push(report); + }), + {}, + ); + + try { + const legacy = await client.callTool({ + name: KERNEL_MISSING_CAPABILITY_TOOL_NAME, + arguments: { + context: + "Reporting a missing capability through the previous context-only contract while the client refreshes its tools.", + }, + }); + expect(legacy.isError).not.toBe(true); + expect(toolResultJSON(legacy)).toMatchObject({ + recorded: false, + status: "legacy_schema_refresh_required", + }); + expect(captured).toHaveLength(0); + + const partial = await client.callTool({ + name: KERNEL_MISSING_CAPABILITY_TOOL_NAME, + arguments: { + context: + "Reporting a partially structured capability request that must not fall back to the compatibility contract.", + gap_reason: "kernel_capability_missing", + }, + }); + expect(partial.isError).not.toBe(true); + expect(toolResultJSON(partial)).toMatchObject({ + recorded: false, + status: "incomplete_structured_report", + }); + expect(captured).toHaveLength(0); + } finally { + await close(); + } + }); + test("reports capture failures without interrupting the task", async () => { const { client, close } = await connectTestMcp( (server) => diff --git a/src/lib/mcp/tools/missing-capability.ts b/src/lib/mcp/tools/missing-capability.ts index 3082254..cefdae5 100644 --- a/src/lib/mcp/tools/missing-capability.ts +++ b/src/lib/mcp/tools/missing-capability.ts @@ -80,10 +80,10 @@ const missingCapabilityFields = { "The missing capability and the user's goal, in 15-25 words and third person. Never include credentials, URLs, domains, account names, file contents, paths, or personal data.", ), gap_reason: gapReasonSchema.describe( - "Why the task could not proceed. Only kernel_capability_missing and external_integration_unavailable are recorded as demand. For an existing tool failure, use submit_feedback instead; transient failures and client restrictions are not capability gaps.", + "Required for current reports. Why the task could not proceed. Only kernel_capability_missing and external_integration_unavailable are recorded as demand. For an existing tool failure, use submit_feedback instead; transient failures and client restrictions are not capability gaps.", ), capability_area: capabilityAreaSchema.describe( - "The single KERNEL product area that would own the capability, or external_integration/client_environment when Kernel does not own it.", + "Required for current reports. The single KERNEL product area that would own the capability, or external_integration/client_environment when Kernel does not own it.", ), capability: z .string() @@ -91,13 +91,13 @@ const missingCapabilityFields = { .min(1) .max(100) .describe( - 'A short generic capability name, such as "browser filesystem upload". Do not include a site, customer, account, domain, path, or payload.', + 'Required for current reports. A short generic capability name, such as "browser filesystem upload". Do not include a site, customer, account, domain, path, or payload.', ), requested_action: requestedActionSchema.describe( - "The primary operation the missing capability needed to perform.", + "Required for current reports. The primary operation the missing capability needed to perform.", ), task_outcome: taskOutcomeSchema.describe( - "Whether the task was completed, completed through a workaround, partially completed, or blocked.", + "Required for current reports. Whether the task was completed, completed through a workaround, partially completed, or blocked.", ), tools_checked: z .array(checkedKernelToolSchema) @@ -108,30 +108,77 @@ const missingCapabilityFields = { ), }; +const structuredMissingCapabilitySchema = z.object(missingCapabilityFields); +const missingCapabilityInputSchema = z.object({ + context: missingCapabilityFields.context, + gap_reason: missingCapabilityFields.gap_reason.optional(), + capability_area: missingCapabilityFields.capability_area.optional(), + capability: missingCapabilityFields.capability.optional(), + requested_action: missingCapabilityFields.requested_action.optional(), + task_outcome: missingCapabilityFields.task_outcome.optional(), + tools_checked: missingCapabilityFields.tools_checked, +}); + export type MissingCapabilityReport = z.infer< - z.ZodObject + typeof structuredMissingCapabilitySchema >; +type MissingCapabilityInput = z.infer; export type MissingCapabilityCapture = ( report: MissingCapabilityReport, extra: unknown, ) => void | Promise; +const STRUCTURED_REPORT_FIELDS = [ + "gap_reason", + "capability_area", + "capability", + "requested_action", + "task_outcome", +] as const; + +function hasAnyStructuredReportField(report: MissingCapabilityInput) { + return STRUCTURED_REPORT_FIELDS.some((field) => report[field] !== undefined); +} + +function isStructuredMissingCapabilityReport( + report: MissingCapabilityInput, +): report is MissingCapabilityReport { + return STRUCTURED_REPORT_FIELDS.every((field) => report[field] !== undefined); +} + export function registerMissingCapabilityTool( server: McpServer, capture?: MissingCapabilityCapture, ) { - server.tool( + server.registerTool( KERNEL_MISSING_CAPABILITY_TOOL_NAME, - "Report a capability that no available KERNEL tool can provide after checking the tool list. Classify disconnected third-party services as external integrations. Do not use this for an existing tool that failed, a transient or capacity failure, or a client-side permission restriction; use submit_feedback for an existing KERNEL tool failure. Reports never replace the original task, so continue with any available workaround.", - missingCapabilityFields, { - title: "Get more tools", - readOnlyHint: false, - destructiveHint: false, - idempotentHint: false, - openWorldHint: true, + description: + "Report a capability that no available KERNEL tool can provide after checking the tool list. Supply every structured field shown; context-only calls from the previous schema are accepted only to request a tool refresh and are not recorded. Classify disconnected third-party services as external integrations. Do not use this for an existing tool that failed, a transient or capacity failure, or a client-side permission restriction; use submit_feedback for an existing KERNEL tool failure. Reports never replace the original task, so continue with any available workaround.", + inputSchema: missingCapabilityInputSchema, + annotations: { + title: "Get more tools", + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, }, - async (report, extra) => { + async (input, extra) => { + if (!isStructuredMissingCapabilityReport(input)) { + const legacy = !hasAnyStructuredReportField(input); + return jsonResponse({ + recorded: false, + status: legacy + ? "legacy_schema_refresh_required" + : "incomplete_structured_report", + message: legacy + ? "This client used the previous get_more_tools schema. Refresh the available tool definitions, retry with the structured fields, and continue the original task with any available workaround." + : "The structured capability report is incomplete. Supply every structured field, retry, and continue the original task with any available workaround.", + }); + } + + const report = input; const externalIntegration = report.gap_reason === "external_integration_unavailable"; if ( From 115f0fd15edbd090c36505c7f0c9161039f94fec Mon Sep 17 00:00:00 2001 From: masnwilliams <43387599+masnwilliams@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:29:19 +0000 Subject: [PATCH 2/2] Address capability compatibility review --- src/lib/mcp/analytics.test.ts | 67 +++++++------- src/lib/mcp/analytics.ts | 11 ++- src/lib/mcp/register.test.ts | 5 + src/lib/mcp/tools/missing-capability.test.ts | 6 +- src/lib/mcp/tools/missing-capability.ts | 96 +++++++++++--------- 5 files changed, 97 insertions(+), 88 deletions(-) diff --git a/src/lib/mcp/analytics.test.ts b/src/lib/mcp/analytics.test.ts index 421a594..fd019e5 100644 --- a/src/lib/mcp/analytics.test.ts +++ b/src/lib/mcp/analytics.test.ts @@ -343,33 +343,6 @@ describe("sanitizeMcpAnalyticsEvent", () => { expect(result?.properties[PostHogMCPAnalyticsProperty.IsError]).toBe(false); }); - test("classifies get_more_tools schema rejections as validation errors", async () => { - const event = toolCallEvent({ - [PostHogMCPAnalyticsProperty.ToolName]: "get_more_tools", - [PostHogMCPAnalyticsProperty.IsError]: true, - [PostHogMCPAnalyticsProperty.ErrorType]: "Error", - [PostHogMCPAnalyticsProperty.ErrorMessage]: - "Input validation error: Invalid arguments for tool get_more_tools", - }); - - const result = await sanitizeMcpAnalyticsEvent(event); - - expect(result?.properties[PostHogMCPAnalyticsProperty.ErrorType]).toBe( - "validation", - ); - - const runtimeError = toolCallEvent({ - [PostHogMCPAnalyticsProperty.ToolName]: "get_more_tools", - [PostHogMCPAnalyticsProperty.IsError]: true, - [PostHogMCPAnalyticsProperty.ErrorType]: "Error", - [PostHogMCPAnalyticsProperty.ErrorMessage]: "handler failed", - }); - const runtimeResult = await sanitizeMcpAnalyticsEvent(runtimeError); - expect( - runtimeResult?.properties[PostHogMCPAnalyticsProperty.ErrorType], - ).toBe("Error"); - }); - test("drops vault specs, aliases, provider actions, and error bodies", async () => { const event = toolCallEvent({ [PostHogMCPAnalyticsProperty.ToolName]: "manage_vault_cards", @@ -1040,18 +1013,14 @@ describe("instrumentMcpAnalytics (SDK integration)", () => { expect(missingCapabilityTool?.description).toContain( "client-side permission restriction", ); - expect(missingCapabilityTool?.inputSchema.required).toEqual(["context"]); - const capabilityProperties = missingCapabilityTool?.inputSchema - .properties as Record; - for (const field of [ + expect(missingCapabilityTool?.inputSchema.required).toEqual([ + "context", "gap_reason", "capability_area", "capability", "requested_action", "task_outcome", - ]) { - expect(capabilityProperties[field]).toBeDefined(); - } + ]); const disabledMissingCapabilityTool = ( await disabled.client.listTools() ).tools.find(({ name }) => name === "get_more_tools"); @@ -1229,6 +1198,36 @@ describe("instrumentMcpAnalytics (SDK integration)", () => { expect(byEvent.has("$identify")).toBe(false); }); + test("classifies rejected capability input through instrumentation", async () => { + const captured: { event?: string }[] = []; + + const result = (await simulateRequest(captured, "tools/call", { + name: "get_more_tools", + arguments: { + context: + "Reporting a malformed structured capability request to verify validation telemetry.", + gap_reason: "not_a_gap_reason", + capability_area: "browser_files", + capability: "browser filesystem upload", + requested_action: "transfer", + task_outcome: "blocked", + }, + })) as { isError?: boolean }; + + expect(result.isError).toBe(true); + const toolCall = captured.find( + ({ event }) => event === PostHogMCPAnalyticsEvent.ToolCall, + ) as { properties: Record }; + expect(toolCall.properties).toMatchObject({ + [PostHogMCPAnalyticsProperty.ToolName]: "get_more_tools", + [PostHogMCPAnalyticsProperty.IsError]: true, + [PostHogMCPAnalyticsProperty.ErrorType]: "validation", + }); + expect( + toolCall.properties[PostHogMCPAnalyticsProperty.ErrorMessage], + ).toBeUndefined(); + }); + test("captures only structured capability demand", async () => { const captured: { event?: string }[] = []; diff --git a/src/lib/mcp/analytics.ts b/src/lib/mcp/analytics.ts index c1fb762..3a366bd 100644 --- a/src/lib/mcp/analytics.ts +++ b/src/lib/mcp/analytics.ts @@ -775,7 +775,13 @@ export function instrumentMcpAnalytics( return; } - const analytics = instrument(server, client, { + // Register first so analytics wraps the legacy dispatch shim and records those calls. + let analytics: McpAnalytics; + registerMissingCapabilityTool(server, (report, extra) => + captureMissingCapabilityReport(report, extra, analytics), + ); + + analytics = instrument(server, client, { // The first-class get_more_tools handler validates and captures structured demand itself. // Point the SDK's name-based interception at an unadvertised name so calls to the real // tool reach its registered schema and callback even while reportMissing is disabled. @@ -824,9 +830,6 @@ export function instrumentMcpAnalytics( beforeSend: sanitizeMcpAnalyticsEvent, }); - registerMissingCapabilityTool(server, (report, extra) => - captureMissingCapabilityReport(report, extra, analytics), - ); registerFeedbackTool(server, (feedback, extra) => captureMcpFeedback(feedback, extra, analytics), ); diff --git a/src/lib/mcp/register.test.ts b/src/lib/mcp/register.test.ts index 41e49d5..e9cea2e 100644 --- a/src/lib/mcp/register.test.ts +++ b/src/lib/mcp/register.test.ts @@ -35,6 +35,11 @@ function captureRegistration( const resources: string[] = []; const schemas = new Map>(); const server = { + server: { + _requestHandlers: new Map([ + ["tools/call", async () => ({ content: [] })], + ]), + }, prompt() {}, resource() {}, tool(name: string, _description: string, inputSchema: object) { diff --git a/src/lib/mcp/tools/missing-capability.test.ts b/src/lib/mcp/tools/missing-capability.test.ts index f140bbe..4e4abb7 100644 --- a/src/lib/mcp/tools/missing-capability.test.ts +++ b/src/lib/mcp/tools/missing-capability.test.ts @@ -109,11 +109,7 @@ describe("get_more_tools", () => { gap_reason: "kernel_capability_missing", }, }); - expect(partial.isError).not.toBe(true); - expect(toolResultJSON(partial)).toMatchObject({ - recorded: false, - status: "incomplete_structured_report", - }); + expect(partial.isError).toBe(true); expect(captured).toHaveLength(0); } finally { await close(); diff --git a/src/lib/mcp/tools/missing-capability.ts b/src/lib/mcp/tools/missing-capability.ts index cefdae5..b1a84c3 100644 --- a/src/lib/mcp/tools/missing-capability.ts +++ b/src/lib/mcp/tools/missing-capability.ts @@ -80,10 +80,10 @@ const missingCapabilityFields = { "The missing capability and the user's goal, in 15-25 words and third person. Never include credentials, URLs, domains, account names, file contents, paths, or personal data.", ), gap_reason: gapReasonSchema.describe( - "Required for current reports. Why the task could not proceed. Only kernel_capability_missing and external_integration_unavailable are recorded as demand. For an existing tool failure, use submit_feedback instead; transient failures and client restrictions are not capability gaps.", + "Why the task could not proceed. Only kernel_capability_missing and external_integration_unavailable are recorded as demand. For an existing tool failure, use submit_feedback instead; transient failures and client restrictions are not capability gaps.", ), capability_area: capabilityAreaSchema.describe( - "Required for current reports. The single KERNEL product area that would own the capability, or external_integration/client_environment when Kernel does not own it.", + "The single KERNEL product area that would own the capability, or external_integration/client_environment when Kernel does not own it.", ), capability: z .string() @@ -91,13 +91,13 @@ const missingCapabilityFields = { .min(1) .max(100) .describe( - 'Required for current reports. A short generic capability name, such as "browser filesystem upload". Do not include a site, customer, account, domain, path, or payload.', + 'A short generic capability name, such as "browser filesystem upload". Do not include a site, customer, account, domain, path, or payload.', ), requested_action: requestedActionSchema.describe( - "Required for current reports. The primary operation the missing capability needed to perform.", + "The primary operation the missing capability needed to perform.", ), task_outcome: taskOutcomeSchema.describe( - "Required for current reports. Whether the task was completed, completed through a workaround, partially completed, or blocked.", + "Whether the task was completed, completed through a workaround, partially completed, or blocked.", ), tools_checked: z .array(checkedKernelToolSchema) @@ -109,41 +109,60 @@ const missingCapabilityFields = { }; const structuredMissingCapabilitySchema = z.object(missingCapabilityFields); -const missingCapabilityInputSchema = z.object({ - context: missingCapabilityFields.context, - gap_reason: missingCapabilityFields.gap_reason.optional(), - capability_area: missingCapabilityFields.capability_area.optional(), - capability: missingCapabilityFields.capability.optional(), - requested_action: missingCapabilityFields.requested_action.optional(), - task_outcome: missingCapabilityFields.task_outcome.optional(), - tools_checked: missingCapabilityFields.tools_checked, -}); export type MissingCapabilityReport = z.infer< typeof structuredMissingCapabilitySchema >; -type MissingCapabilityInput = z.infer; export type MissingCapabilityCapture = ( report: MissingCapabilityReport, extra: unknown, ) => void | Promise; -const STRUCTURED_REPORT_FIELDS = [ - "gap_reason", - "capability_area", - "capability", - "requested_action", - "task_outcome", -] as const; +type ToolCallRequest = { + params?: { name?: unknown; arguments?: unknown }; +}; +type ToolCallHandler = ( + request: ToolCallRequest, + extra: unknown, +) => Promise; + +function legacySchemaResponse() { + return jsonResponse({ + recorded: false, + status: "legacy_schema_refresh_required", + message: + "This client used the previous get_more_tools schema. Refresh the available tool definitions, retry with the structured fields, and continue the original task with any available workaround.", + }); +} -function hasAnyStructuredReportField(report: MissingCapabilityInput) { - return STRUCTURED_REPORT_FIELDS.some((field) => report[field] !== undefined); +function isLegacyContextOnlyCall(request: ToolCallRequest) { + if (request.params?.name !== KERNEL_MISSING_CAPABILITY_TOOL_NAME) + return false; + const args = request.params.arguments; + if (!args || typeof args !== "object" || Array.isArray(args)) return false; + const entries = Object.entries(args); + return ( + entries.length === 1 && + entries[0]?.[0] === "context" && + typeof entries[0][1] === "string" + ); } -function isStructuredMissingCapabilityReport( - report: MissingCapabilityInput, -): report is MissingCapabilityReport { - return STRUCTURED_REPORT_FIELDS.every((field) => report[field] !== undefined); +function acceptLegacyContextOnlyCalls(server: McpServer) { + // The SDK validates before invoking the tool callback, so handle only the exact old + // payload here while leaving the advertised structured schema unchanged. + const handlers = ( + server.server as unknown as { + _requestHandlers: Map; + } + )._requestHandlers; + const handler = handlers.get("tools/call"); + if (!handler) throw new Error("tools/call handler is not registered"); + + handlers.set("tools/call", async (request, extra) => { + if (isLegacyContextOnlyCall(request)) return legacySchemaResponse(); + return handler(request, extra); + }); } export function registerMissingCapabilityTool( @@ -154,8 +173,8 @@ export function registerMissingCapabilityTool( KERNEL_MISSING_CAPABILITY_TOOL_NAME, { description: - "Report a capability that no available KERNEL tool can provide after checking the tool list. Supply every structured field shown; context-only calls from the previous schema are accepted only to request a tool refresh and are not recorded. Classify disconnected third-party services as external integrations. Do not use this for an existing tool that failed, a transient or capacity failure, or a client-side permission restriction; use submit_feedback for an existing KERNEL tool failure. Reports never replace the original task, so continue with any available workaround.", - inputSchema: missingCapabilityInputSchema, + "Report a capability that no available KERNEL tool can provide after checking the tool list. Classify disconnected third-party services as external integrations. Do not use this for an existing tool that failed, a transient or capacity failure, or a client-side permission restriction; use submit_feedback for an existing KERNEL tool failure. Reports never replace the original task, so continue with any available workaround.", + inputSchema: structuredMissingCapabilitySchema, annotations: { title: "Get more tools", readOnlyHint: false, @@ -164,21 +183,7 @@ export function registerMissingCapabilityTool( openWorldHint: true, }, }, - async (input, extra) => { - if (!isStructuredMissingCapabilityReport(input)) { - const legacy = !hasAnyStructuredReportField(input); - return jsonResponse({ - recorded: false, - status: legacy - ? "legacy_schema_refresh_required" - : "incomplete_structured_report", - message: legacy - ? "This client used the previous get_more_tools schema. Refresh the available tool definitions, retry with the structured fields, and continue the original task with any available workaround." - : "The structured capability report is incomplete. Supply every structured field, retry, and continue the original task with any available workaround.", - }); - } - - const report = input; + async (report, extra) => { const externalIntegration = report.gap_reason === "external_integration_unavailable"; if ( @@ -235,4 +240,5 @@ export function registerMissingCapabilityTool( }); }, ); + acceptLegacyContextOnlyCalls(server); }