Browse Source

fix(mcp): preserve metadata across tool pages (#35439)

Aiden Cline 2 months ago
parent
commit
2b34df94fa

+ 61 - 1
packages/opencode/test/mcp/catalog.test.ts

@@ -1,6 +1,10 @@
 import { describe, expect, test } from "bun:test"
-import type { Client } from "@modelcontextprotocol/sdk/client/index.js"
+import { Client } from "@modelcontextprotocol/sdk/client/index.js"
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
+import { Server } from "@modelcontextprotocol/sdk/server/index.js"
+import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
 import { McpCatalog } from "@/mcp/catalog"
+import { Effect } from "effect"
 
 const options = { toolCallId: "call_mcp", abortSignal: new AbortController().signal } as any
 
@@ -45,3 +49,59 @@ describe("McpCatalog.convertTool", () => {
     })
   })
 })
+
+test("preserves output schema validation across paginated tool discovery", async () => {
+  const server = new Server({ name: "pagination", version: "1.0.0" }, { capabilities: { tools: {} } })
+  server.setRequestHandler(ListToolsRequestSchema, ({ params }) =>
+    Promise.resolve(
+      params?.cursor === "page-2"
+        ? {
+            tools: [
+              {
+                name: "second",
+                inputSchema: { type: "object" },
+                outputSchema: {
+                  type: "object",
+                  properties: { value: { type: "number" } },
+                  required: ["value"],
+                },
+              },
+            ],
+          }
+        : {
+            tools: [
+              {
+                name: "first",
+                inputSchema: { type: "object" },
+                outputSchema: {
+                  type: "object",
+                  properties: { value: { type: "string" } },
+                  required: ["value"],
+                },
+              },
+            ],
+            nextCursor: "page-2",
+          },
+    ),
+  )
+  server.setRequestHandler(CallToolRequestSchema, ({ params }) =>
+    Promise.resolve({
+      content: [],
+      structuredContent: { value: params.name === "first" ? 42 : 1 },
+    }),
+  )
+
+  const client = new Client({ name: "pagination-test", version: "1.0.0" })
+  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
+  await Promise.all([client.connect(clientTransport), server.connect(serverTransport)])
+
+  try {
+    const tools = await Effect.runPromise(McpCatalog.defs(client))
+    expect(tools?.map((tool) => tool.name)).toEqual(["first", "second"])
+    await expect(client.callTool({ name: "first", arguments: {} })).rejects.toThrow(
+      "Structured content does not match the tool's output schema",
+    )
+  } finally {
+    await Promise.all([client.close(), server.close()])
+  }
+})

+ 50 - 0
patches/@modelcontextprotocol%2Fsdk@1.29.0.patch

@@ -112,6 +112,31 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1
      /**
       * After initialization has completed, this will be populated with the server's reported capabilities.
       */
+@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol {
+      * Called after listTools() to pre-compile validators for better performance.
+      */
+-    cacheToolMetadata(tools) {
+-        this._cachedToolOutputValidators.clear();
+-        this._cachedKnownTaskTools.clear();
+-        this._cachedRequiredTaskTools.clear();
++    cacheToolMetadata(tools, reset = true) {
++        if (reset) {
++            this._cachedToolOutputValidators.clear();
++            this._cachedKnownTaskTools.clear();
++            this._cachedRequiredTaskTools.clear();
++        }
+         for (const tool of tools) {
+             // If the tool has an outputSchema, create and cache the validator
+             if (tool.outputSchema) {
+@@ -569,7 +577,7 @@ class Client extends protocol_js_1.Protocol {
+     async listTools(params, options) {
+         const result = await this.request({ method: 'tools/list', params }, types_js_1.ListToolsResultSchema, options);
+         // Cache the tools and their output schemas for future validation
+-        this.cacheToolMetadata(result.tools);
++        this.cacheToolMetadata(result.tools, params?.cursor === undefined);
+         return result;
+     }
+     /**
 diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
 index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
 --- a/dist/cjs/client/streamableHttp.js
@@ -461,6 +486,31 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e
      /**
       * After initialization has completed, this will be populated with the server's reported capabilities.
       */
+@@ -537,9 +543,11 @@ export class Client extends Protocol {
+      * Called after listTools() to pre-compile validators for better performance.
+      */
+-    cacheToolMetadata(tools) {
+-        this._cachedToolOutputValidators.clear();
+-        this._cachedKnownTaskTools.clear();
+-        this._cachedRequiredTaskTools.clear();
++    cacheToolMetadata(tools, reset = true) {
++        if (reset) {
++            this._cachedToolOutputValidators.clear();
++            this._cachedKnownTaskTools.clear();
++            this._cachedRequiredTaskTools.clear();
++        }
+         for (const tool of tools) {
+             // If the tool has an outputSchema, create and cache the validator
+             if (tool.outputSchema) {
+@@ -565,7 +573,7 @@ export class Client extends Protocol {
+     async listTools(params, options) {
+         const result = await this.request({ method: 'tools/list', params }, ListToolsResultSchema, options);
+         // Cache the tools and their output schemas for future validation
+-        this.cacheToolMetadata(result.tools);
++        this.cacheToolMetadata(result.tools, params?.cursor === undefined);
+         return result;
+     }
+     /**
 diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
 index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644
 --- a/dist/esm/client/streamableHttp.js