fix(mcp): support draft-07 output schemas

This commit is contained in:
Aiden Cline 2026-07-28 11:08:36 -05:00
commit 87c86b33b4
2 changed files with 57 additions and 0 deletions

View file

@ -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({

View file

@ -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()])
}
})