diff --git a/apps/server/src/mcp/McpHttpServer.test.ts b/apps/server/src/mcp/McpHttpServer.test.ts index 395d9dfe04fe5..e50795e0bc847 100644 --- a/apps/server/src/mcp/McpHttpServer.test.ts +++ b/apps/server/src/mcp/McpHttpServer.test.ts @@ -649,16 +649,25 @@ it.effect("decodes null optional arguments the way it decodes omitted ones", () it.effect("rejects arguments a tool does not declare instead of dropping them", () => { const calls: Array = []; return Effect.gen(function* () { - const launch = yield* Effect.flip( - callTool("t3_thread_launch", { title: "Repro", scratch: true, prompt: "say hi" }), - ); - const wait = yield* Effect.flip( - callTool("t3_thread_wait", { threadId: "thread-excess-arguments", timeout: 5_000 }), - ); + const launch = yield* callTool("t3_thread_launch", { + title: "Repro", + scratch: true, + prompt: "say hi", + }); + const wait = yield* callTool("t3_thread_wait", { + threadId: "thread-excess-arguments", + timeout: 5_000, + }); expect(calls).toEqual([]); - expect(launch.message).toContain('Unknown argument "prompt"'); - expect(wait.message).toContain('Unknown argument "timeout"'); + expect(launch.isError).toBe(true); + expect(launch.content).toEqual([ + { type: "text", text: expect.stringContaining('Unknown argument "prompt"') }, + ]); + expect(wait.isError).toBe(true); + expect(wait.content).toEqual([ + { type: "text", text: expect.stringContaining('Unknown argument "timeout"') }, + ]); }).pipe(Effect.provide(layerRecordingTools(calls))); }); @@ -717,9 +726,12 @@ it.effect.each(["limit", "runLimit", "maxCharsPerItem"])( const rejected = yield* callTool("t3_thread_read", { threadId: "thread-below-minimum", [field]: 0, - }).pipe(Effect.flip); + }); - expect(String(rejected)).toContain("greater than or equal to 1"); + expect(rejected.isError).toBe(true); + expect(rejected.content).toEqual([ + { type: "text", text: expect.stringContaining("greater than or equal to 1") }, + ]); expect(calls).toEqual([]); }).pipe(Effect.provide(layerRecordingTools(calls))); }, diff --git a/apps/server/src/mcp/McpHttpServer.ts b/apps/server/src/mcp/McpHttpServer.ts index eaf54214ba07b..39a8fb4f0f7c7 100644 --- a/apps/server/src/mcp/McpHttpServer.ts +++ b/apps/server/src/mcp/McpHttpServer.ts @@ -1167,6 +1167,38 @@ const withAdvertisedSchemas = Effect.fn("McpHttpServer.withAdvertisedSchemas")(f return yield* registration.pipe(Effect.provideService(McpServer.McpServer, advertisingServer)); }); +/** + * `registerToolkit` raises `InvalidParams` only for invalid arguments. MCP before 2025-11-25 sends + * that as a JSON-RPC error, which Cursor shows models as "Tool execution error", so it goes back + * as a tool result the model can read and correct. + */ +const withReadableInvalidArguments = Effect.fn("McpHttpServer.withReadableInvalidArguments")( + function* (registration: Effect.Effect) { + const server = yield* McpServer.McpServer; + const readableServer = McpServer.McpServer.of({ + ...server, + addTool: Effect.fn("McpServer.addTool")(function (options) { + return server.addTool({ + ...options, + handle: (payload) => + options.handle(payload).pipe( + Effect.catchTags({ + InvalidParams: (error) => + Effect.succeed( + new McpSchema.CallToolResult({ + isError: true, + content: [{ type: "text", text: error.message }], + }), + ), + }), + ), + }); + }), + }); + return yield* registration.pipe(Effect.provideService(McpServer.McpServer, readableServer)); + }, +); + /** Hands each call the services its registration was built with; the request's own still win. */ const provideRegistrationServices = () => McpServer.middleware<{ provides: Services }>()( @@ -1191,7 +1223,10 @@ export const toolkitRegistration = , EX, Layer.effectDiscard( withToolAnalytics( withToolSpans( - withAdvertisedSchemas(toolkit, McpServer.registerToolkit(decodeAsAdvertised(toolkit))), + withAdvertisedSchemas( + toolkit, + withReadableInvalidArguments(McpServer.registerToolkit(decodeAsAdvertised(toolkit))), + ), ), ), ).pipe( diff --git a/apps/server/src/mcp/toolkits/core.test.ts b/apps/server/src/mcp/toolkits/core.test.ts index b2aaed70322cf..f31b5dbb36f49 100644 --- a/apps/server/src/mcp/toolkits/core.test.ts +++ b/apps/server/src/mcp/toolkits/core.test.ts @@ -575,12 +575,11 @@ it.effect("rejects an unknown organize target instead of archiving the calling t .pipe( Effect.provideService(McpInvocationContext.McpInvocationContext, scope), Effect.provideService(McpSchema.McpServerClient, client), - Effect.result, ); expect(dispatched).toEqual([]); expect(outcome).toMatchObject({ - _tag: "Failure", - failure: { message: expect.stringContaining('Unknown argument "threadIds"') }, + isError: true, + content: [{ type: "text", text: expect.stringContaining('Unknown argument "threadIds"') }], }); }).pipe( Effect.provide(