Explorar o código

fix(mcp): isolate schema validators per client

Aiden Cline hai 2 semanas
pai
achega
b087a788b4

+ 2 - 2
packages/opencode/src/cli/cmd/mcp.ts

@@ -5,7 +5,7 @@ import { Cause } from "effect"
 import { Client, StreamableHTTPClientTransport, UnauthorizedError } from "@modelcontextprotocol/client"
 import * as prompts from "@clack/prompts"
 import { UI } from "../ui"
-import { CLIENT_OPTIONS, MCP } from "../../mcp"
+import { clientOptions, MCP } from "../../mcp"
 import { McpAuth } from "../../mcp/auth"
 import { McpOAuthProvider } from "../../mcp/oauth-provider"
 import { Config } from "@/config/config"
@@ -751,7 +751,7 @@ export const McpDebugCommand = effectCmd({
         authProvider,
         requestInit: serverConfig.headers ? { headers: serverConfig.headers } : undefined,
       })
-      const client = new Client({ name: "opencode-debug", version: InstallationVersion }, CLIENT_OPTIONS)
+      const client = new Client({ name: "opencode-debug", version: InstallationVersion }, clientOptions())
 
       try {
         await client.connect(transport)

+ 30 - 27
packages/opencode/src/mcp/index.ts

@@ -38,33 +38,36 @@ 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
-    // sampling: {},
-    // https://github.com/anomalyco/opencode/issues/23066
-    // elicitation: {},
-    // https://github.com/anomalyco/opencode/issues/2308
-    roots: {},
-    // https://github.com/anomalyco/opencode/issues/28567
-    // tasks: {},
-  },
-  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)
+
+export function clientOptions(): ClientOptions {
+  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()
+
+  return {
+    capabilities: {
+      // https://github.com/anomalyco/opencode/issues/11948
+      // sampling: {},
+      // https://github.com/anomalyco/opencode/issues/23066
+      // elicitation: {},
+      // https://github.com/anomalyco/opencode/issues/2308
+      roots: {},
+      // https://github.com/anomalyco/opencode/issues/28567
+      // tasks: {},
     },
-  },
-} satisfies ClientOptions
+    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)
+      },
+    },
+  }
+}
 
 export const Resource = Schema.Struct({
   name: Schema.String,
@@ -93,7 +96,7 @@ function createClient(directory: string) {
   const client: MCPClient = new Client(
     { name: "opencode", version: InstallationVersion },
     {
-      ...CLIENT_OPTIONS,
+      ...clientOptions(),
       listChanged: {
         tools: { autoRefresh: false, onChanged: (error) => client.onToolsChanged?.(error) },
       },

+ 18 - 2
packages/opencode/test/mcp/catalog.test.ts

@@ -2,7 +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 { clientOptions } from "@/mcp"
 import { Effect } from "effect"
 
 const options = { toolCallId: "call_mcp", abortSignal: new AbortController().signal } as any
@@ -167,7 +167,7 @@ test("accepts and validates draft-07 tool output schemas", async () => {
     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 client = new Client({ name: "draft-07-test", version: "1.0.0" }, clientOptions())
   const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
   await Promise.all([client.connect(clientTransport), server.connect(serverTransport)])
 
@@ -183,3 +183,19 @@ test("accepts and validates draft-07 tool output schemas", async () => {
     await Promise.all([client.close(), server.close()])
   }
 })
+
+test("isolates output schema caches between MCP clients", () => {
+  const stringSchema = {
+    $schema: "http://json-schema.org/draft-07/schema#",
+    $id: "https://example.com/shared-schema",
+    type: "string",
+  }
+  const numberSchema = { ...stringSchema, type: "number" }
+  const stringValidator = clientOptions().jsonSchemaValidator!.getValidator(stringSchema)
+  const numberValidator = clientOptions().jsonSchemaValidator!.getValidator(numberSchema)
+
+  expect(stringValidator("value").valid).toBe(true)
+  expect(stringValidator(42).valid).toBe(false)
+  expect(numberValidator(42).valid).toBe(true)
+  expect(numberValidator("value").valid).toBe(false)
+})