From 6afba7e2e12ec50378b0a25a2cee46de96062b15 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Mon, 26 Oct 2026 23:00:48 -0700 Subject: [PATCH] fix(server): MCP toolkit tools advertise the compact input schema Co-Authored-By: Claude Opus 5.5 --- apps/server/src/mcp/McpHttpServer.test.ts | 14 ++++++++ apps/server/src/mcp/McpHttpServer.ts | 33 ++++++++++++++++++- .../src/provider/agentContextBudget.json | 2 +- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/apps/server/src/mcp/McpHttpServer.test.ts b/apps/server/src/mcp/McpHttpServer.test.ts index 6c502c1ef1305..8d7fb85a9e9b8 100644 --- a/apps/server/src/mcp/McpHttpServer.test.ts +++ b/apps/server/src/mcp/McpHttpServer.test.ts @@ -21,6 +21,7 @@ import { HttpBody, HttpClient, HttpClientRequest, HttpRouter } from "effect/http import * as ProjectService from "../project/ProjectService.ts"; import * as PullRequestReviewGuideService from "../pullRequest/PullRequestReviewGuideService.ts"; import * as ServerConfig from "../config.ts"; +import { advertisedJsonSchema } from "./advertisedJsonSchema.ts"; import * as McpHttpServer from "./McpHttpServer.ts"; import * as McpToolAccess from "./McpToolAccess.ts"; import * as McpToolAccessTestkit from "./McpToolAccess.testkit.ts"; @@ -29,6 +30,7 @@ import * as McpSessionRegistry from "./McpSessionRegistry.ts"; import * as OrchestratorMcpService from "./OrchestratorMcpService.ts"; import * as PreviewAutomationBroker from "./PreviewAutomationBroker.ts"; import { OrchestratorToolkit } from "./toolkits/orchestrator/tools.ts"; +import { PreviewToolkit } from "./toolkits/preview/tools.ts"; import { ProjectToolkit } from "./toolkits/project/tools.ts"; const environmentId = EnvironmentId.make("environment-mcp-test"); @@ -356,6 +358,18 @@ it.effect("refuses preview tools to a client outside a thread before they run", }).pipe(Effect.provide(layerTest)), ); +it.effect("advertises the compact input schema for every registered tool", () => + Effect.gen(function* () { + const server = yield* McpServer.McpServer; + const tools: Record = PreviewToolkit.tools; + + expect(server.tools.length).toBe(Object.keys(tools).length); + for (const { tool } of server.tools) { + expect(tool.inputSchema, tool.name).toEqual(advertisedJsonSchema(tools[tool.name]!)); + } + }).pipe(Effect.provide(layerTest)), +); + it.effect("checks each preview_act action against that action's own input", () => Effect.scoped( Effect.gen(function* () { diff --git a/apps/server/src/mcp/McpHttpServer.ts b/apps/server/src/mcp/McpHttpServer.ts index 044a43baeab1e..0d8837f0ab875 100644 --- a/apps/server/src/mcp/McpHttpServer.ts +++ b/apps/server/src/mcp/McpHttpServer.ts @@ -948,6 +948,35 @@ const withToolAnalytics = return yield* registration.pipe(Effect.provideService(McpServer.McpServer, recordingServer)); }); +/** `McpServer.registerToolkit` advertises the full codec schema; agents get the compact one. */ +const withAdvertisedSchemas = Effect.fn("McpHttpServer.withAdvertisedSchemas")(function* < + Tools extends Record, + A, + E, + R, +>(toolkit: Toolkit.Toolkit, registration: Effect.Effect) { + const server = yield* McpServer.McpServer; + const tools: Record = toolkit.tools; + const advertisingServer = McpServer.McpServer.of({ + ...server, + addTool: Effect.fn("McpServer.addTool")(function (options) { + const tool = tools[options.tool.name]; + return server.addTool( + tool === undefined + ? options + : { + ...options, + tool: new McpSchema.Tool({ + ...options.tool, + inputSchema: advertisedJsonSchema(tool), + }), + }, + ); + }), + }); + return yield* registration.pipe(Effect.provideService(McpServer.McpServer, advertisingServer)); +}); + /** * `McpServer.toolkit` asks for every service its tools declare when it * registers them, but the auth middleware provides `McpInvocationContext` to @@ -959,7 +988,9 @@ export const toolkitRegistration = , EX, handlers: McpToolAccess.HandlersLayer, ) => { const registration = Layer.effectDiscard( - withToolAnalytics(McpServer.registerToolkit(decodeAsAdvertised(toolkit))), + withToolAnalytics( + withAdvertisedSchemas(toolkit, McpServer.registerToolkit(decodeAsAdvertised(toolkit))), + ), ).pipe(Layer.provide(McpServer.McpServer.layer)); // @effect-diagnostics-next-line unsafeEffectTypeAssertion:off - the auth middleware provides it per request. const registered = registration as Layer.Layer< diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index e3272bfdf8021..a54335979eb4c 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { "claudeSystemAppendBytes": 4857, "codexAdditionalContextBytes": 5409, - "mcpToolListJsonBytes": 54964 + "mcpToolListJsonBytes": 42328 }