Pārlūkot izejas kodu

fix(mcp): support draft-07 output schemas

Aiden Cline 2 nedēļas atpakaļ
vecāks
revīzija
87c86b33b4

+ 15 - 0
packages/opencode/src/mcp/index.ts

@@ -15,6 +15,7 @@ import {
   type Tool as MCPToolDef,
 } from "@modelcontextprotocol/client"
 import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"
+import { Ajv, AjvJsonSchemaValidator, addFormats } from "@modelcontextprotocol/client/validators/ajv"
 import { Config } from "@/config/config"
 import { ConfigMCPV1 } from "@opencode-ai/core/v1/config/mcp"
 import { NamedError } from "@opencode-ai/core/util/error"
@@ -34,8 +35,16 @@ import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
 import { McpCatalog } from "./catalog"
 import { McpEvent } from "@opencode-ai/schema/mcp-event"
 import { McpBrowser } from "./browser"
+import { lazy } from "@/util/lazy"
 
 const DEFAULT_TIMEOUT = 30_000
+const draft7Validator = lazy(() => {
+  const ajv = new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true })
+  addFormats(ajv)
+  return new AjvJsonSchemaValidator(ajv)
+})
+const defaultValidator = new AjvJsonSchemaValidator()
+
 export const CLIENT_OPTIONS = {
   capabilities: {
     // https://github.com/anomalyco/opencode/issues/11948
@@ -49,6 +58,12 @@ export const CLIENT_OPTIONS = {
   },
   versionNegotiation: { mode: "auto" },
   listMaxPages: 1_000,
+  jsonSchemaValidator: {
+    getValidator: <T>(schema: { $schema?: string }) => {
+      if (!schema.$schema?.toLowerCase().includes("draft-07")) return defaultValidator.getValidator<T>(schema)
+      return draft7Validator().getValidator<T>(schema)
+    },
+  },
 } satisfies ClientOptions
 
 export const Resource = Schema.Struct({

+ 42 - 0
packages/opencode/test/mcp/catalog.test.ts

@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test"
 import { Client, InMemoryTransport } from "@modelcontextprotocol/client"
 import { Server } from "@modelcontextprotocol/server"
 import { McpCatalog } from "@/mcp/catalog"
+import { CLIENT_OPTIONS } from "@/mcp"
 import { Effect } from "effect"
 
 const options = { toolCallId: "call_mcp", abortSignal: new AbortController().signal } as any
@@ -141,3 +142,44 @@ test("preserves output schema validation across paginated tool discovery", async
     await Promise.all([client.close(), server.close()])
   }
 })
+
+test("accepts and validates draft-07 tool output schemas", async () => {
+  const server = new Server({ name: "draft-07", version: "1.0.0" }, { capabilities: { tools: {} } })
+  let calls = 0
+  server.setRequestHandler("tools/list", () =>
+    Promise.resolve({
+      tools: [
+        {
+          name: "draft-07-tool",
+          inputSchema: { type: "object" as const },
+          outputSchema: {
+            $schema: "http://json-schema.org/draft-07/schema#",
+            type: "object" as const,
+            properties: { value: { type: "string" } },
+            required: ["value"],
+          },
+        },
+      ],
+    }),
+  )
+  server.setRequestHandler("tools/call", () => {
+    calls++
+    return Promise.resolve({ content: [], structuredContent: { value: calls === 1 ? "valid" : 42 } })
+  })
+
+  const client = new Client({ name: "draft-07-test", version: "1.0.0" }, CLIENT_OPTIONS)
+  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(["draft-07-tool"])
+    await expect(client.callTool({ name: "draft-07-tool", arguments: {} })).resolves.toMatchObject({
+      structuredContent: { value: "valid" },
+    })
+    await expect(client.callTool({ name: "draft-07-tool", arguments: {} })).rejects.toThrow(/output schema/i)
+    expect(calls).toBe(2)
+  } finally {
+    await Promise.all([client.close(), server.close()])
+  }
+})