mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-10 03:18:31 +00:00
fix(core): expose MCP output schemas to code mode (#35502)
This commit is contained in:
parent
50a762e7b9
commit
4e019ba5d9
5 changed files with 106 additions and 2 deletions
|
|
@ -61,6 +61,7 @@ export interface ToolDefinition {
|
|||
readonly name: string
|
||||
readonly description: string | undefined
|
||||
readonly inputSchema: unknown
|
||||
readonly outputSchema: unknown
|
||||
}
|
||||
|
||||
export interface PromptDefinition {
|
||||
|
|
@ -235,6 +236,7 @@ export const connect = Effect.fnUntraced(function* (
|
|||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
outputSchema: "outputSchema" in tool ? tool.outputSchema : undefined,
|
||||
}))
|
||||
}),
|
||||
prompts: () =>
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ export class Tool extends Schema.Class<Tool>("MCP.Tool")({
|
|||
name: Schema.String,
|
||||
description: Schema.String.pipe(Schema.optional),
|
||||
inputSchema: Schema.Unknown.pipe(Schema.optional),
|
||||
outputSchema: Schema.Unknown.pipe(Schema.optional),
|
||||
}) {}
|
||||
|
||||
export const ToolResultContent = Schema.Union([
|
||||
|
|
@ -391,7 +392,13 @@ export const layer = Layer.effect(
|
|||
} satisfies MCPClient.ElicitationHandler
|
||||
|
||||
const toTool = (server: ServerName, def: MCPClient.ToolDefinition) =>
|
||||
new Tool({ server, name: def.name, description: def.description, inputSchema: def.inputSchema })
|
||||
new Tool({
|
||||
server,
|
||||
name: def.name,
|
||||
description: def.description,
|
||||
inputSchema: def.inputSchema,
|
||||
outputSchema: def.outputSchema,
|
||||
})
|
||||
|
||||
const toPrompt = (server: ServerName, def: MCPClient.PromptDefinition) =>
|
||||
new Prompt({
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export const layer = Layer.effectDiscard(
|
|||
properties: schema.properties ?? {},
|
||||
additionalProperties: false,
|
||||
},
|
||||
outputSchema: tool.outputSchema as JsonSchema.JsonSchema | undefined,
|
||||
execute: (input, context) =>
|
||||
Effect.gen(function* () {
|
||||
yield* permission.assert({
|
||||
|
|
|
|||
40
packages/core/test/fixture/mcp-output-schema.ts
Normal file
40
packages/core/test/fixture/mcp-output-schema.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
|
||||
|
||||
const server = new Server({ name: "output-schema", version: "1.0.0" }, { capabilities: { tools: {} } })
|
||||
|
||||
server.setRequestHandler(ListToolsRequestSchema, ({ params }) =>
|
||||
Promise.resolve(
|
||||
params?.cursor === "page-2"
|
||||
? {
|
||||
tools: [
|
||||
{
|
||||
name: "second",
|
||||
inputSchema: { type: "object" },
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "number" } },
|
||||
required: ["value"],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
tools: [
|
||||
{
|
||||
name: "first",
|
||||
inputSchema: { type: "object" },
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
required: ["value"],
|
||||
},
|
||||
},
|
||||
],
|
||||
nextCursor: "page-2",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
await server.connect(new StdioServerTransport())
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import path from "node:path"
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
|
||||
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
||||
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
|
||||
import { ConfigMCP } from "@opencode-ai/core/config/mcp"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
|
|
@ -15,7 +17,7 @@ import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
|||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||
import { Deferred, Effect, Fiber, Layer, Stream } from "effect"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { settleTool, toolIdentity, waitForTool } from "./lib/tool"
|
||||
import { settleTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool"
|
||||
|
||||
let assertion: Deferred.Deferred<PermissionV2.AssertInput> | undefined
|
||||
let decision: Effect.Effect<void, PermissionV2.Error> = Effect.void
|
||||
|
|
@ -29,6 +31,11 @@ const mcp = Layer.mock(MCP.Service, {
|
|||
name: "search",
|
||||
description: "Search",
|
||||
inputSchema: { type: "object", properties: {} },
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: { ok: { type: "boolean" } },
|
||||
required: ["ok"],
|
||||
},
|
||||
}),
|
||||
]),
|
||||
callTool: (input) =>
|
||||
|
|
@ -133,6 +140,53 @@ test("preserves output schema validation across paginated tool discovery", async
|
|||
}
|
||||
})
|
||||
|
||||
test("retains output schemas across paginated MCP discovery", async () => {
|
||||
const tools = await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
const connection = yield* MCPClient.connect(
|
||||
"pagination",
|
||||
new ConfigMCP.Local({
|
||||
type: "local",
|
||||
command: [process.execPath, path.join(import.meta.dir, "fixture/mcp-output-schema.ts")],
|
||||
}),
|
||||
import.meta.dir,
|
||||
)
|
||||
return yield* connection.tools()
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
expect(tools.map((tool) => ({ name: tool.name, outputSchema: tool.outputSchema }))).toEqual([
|
||||
{
|
||||
name: "first",
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "string" } },
|
||||
required: ["value"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "second",
|
||||
outputSchema: {
|
||||
type: "object",
|
||||
properties: { value: { type: "number" } },
|
||||
required: ["value"],
|
||||
},
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it.effect("advertises MCP output schemas to Code Mode", () =>
|
||||
Effect.gen(function* () {
|
||||
const registry = yield* ToolRegistry.Service
|
||||
yield* waitForTool(registry, "execute")
|
||||
const execute = (yield* toolDefinitions(registry)).find((tool) => tool.name === "execute")
|
||||
|
||||
expect(execute?.description).toContain("tools.demo.search(input: {}): Promise<{ ok: boolean }>")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("waits for permission before calling an MCP tool", () =>
|
||||
Effect.gen(function* () {
|
||||
calls = 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue