fix(mcp): isolate schema validators per client
This commit is contained in:
parent
87c86b33b4
commit
b087a788b4
3 changed files with 50 additions and 31 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue