fix(mcp): support draft-07 output schemas
This commit is contained in:
parent
017a5977d2
commit
87c86b33b4
2 changed files with 57 additions and 0 deletions
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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()])
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue