Преглед на файлове

fix(core): expose MCP output schemas to code mode (#35502)

Aiden Cline преди 1 месец
родител
ревизия
4e019ba5d9

+ 2 - 0
packages/core/src/mcp/client.ts

@@ -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: () =>

+ 8 - 1
packages/core/src/mcp/index.ts

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

+ 1 - 0
packages/core/src/tool/mcp.ts

@@ -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 - 0
packages/core/test/fixture/mcp-output-schema.ts

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

+ 55 - 1
packages/core/test/mcp.test.ts

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