From 8f559f3b164b991312374a90d455712fb7015521 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 8 Jun 2026 22:42:07 -0500 Subject: [PATCH 1/2] fix(opencode): cancel active MCP tool calls --- packages/opencode/src/mcp/index.ts | 3 +- packages/opencode/test/mcp/lifecycle.test.ts | 30 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 3a348abb8ed3..68e21560a5ee 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -166,7 +166,7 @@ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number return dynamicTool({ description: mcpTool.description ?? "", inputSchema: jsonSchema(schema), - execute: async (args: unknown) => { + execute: async (args: unknown, options) => { return client.callTool( { name: mcpTool.name, @@ -175,6 +175,7 @@ function convertMcpTool(mcpTool: MCPToolDef, client: MCPClient, timeout?: number CallToolResultSchema, { resetTimeoutOnProgress: true, + signal: options.abortSignal, timeout, }, ) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 7bd5e4f00b4d..895a8578cbf4 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -19,6 +19,7 @@ interface MockClientState { listResourcesShouldFail: boolean prompts: Array<{ name: string; description?: string }> resources: Array<{ name: string; uri: string; description?: string }> + callToolSignal?: AbortSignal closed: boolean notificationHandlers: Map any> } @@ -173,6 +174,11 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ return { resources: this._state?.resources ?? [] } } + async callTool(_params: unknown, _schema: unknown, options?: { signal?: AbortSignal }) { + if (this._state) this._state.callToolSignal = options?.signal + return { content: [] } + } + async close() { if (this._state) this._state.closed = true } @@ -234,6 +240,30 @@ it.instance( { config: { mcp: {} } }, ) +it.instance( + "forwards tool cancellation to the MCP request", + () => + MCP.Service.use((mcp: MCPNS.Interface) => + Effect.gen(function* () { + lastCreatedClientName = "cancel-server" + const serverState = getOrCreateClientState("cancel-server") + const controller = new AbortController() + + yield* mcp.add("cancel-server", { + type: "local", + command: ["echo", "test"], + }) + + const tools = yield* mcp.tools() + yield* Effect.promise(() => + tools["cancel-server_test_tool"].execute!({}, { abortSignal: controller.signal } as never), + ) + expect(serverState.callToolSignal).toBe(controller.signal) + }), + ), + { config: { mcp: {} } }, +) + // ======================================================================== // Test: tool change notifications refresh the cache // ======================================================================== From 34a39a2b47006f4e43d57f87b88421c80beb2c66 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 8 Jun 2026 22:45:54 -0500 Subject: [PATCH 2/2] test(opencode): remove redundant MCP cancellation coverage --- packages/opencode/test/mcp/lifecycle.test.ts | 30 -------------------- 1 file changed, 30 deletions(-) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 895a8578cbf4..7bd5e4f00b4d 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -19,7 +19,6 @@ interface MockClientState { listResourcesShouldFail: boolean prompts: Array<{ name: string; description?: string }> resources: Array<{ name: string; uri: string; description?: string }> - callToolSignal?: AbortSignal closed: boolean notificationHandlers: Map any> } @@ -174,11 +173,6 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ return { resources: this._state?.resources ?? [] } } - async callTool(_params: unknown, _schema: unknown, options?: { signal?: AbortSignal }) { - if (this._state) this._state.callToolSignal = options?.signal - return { content: [] } - } - async close() { if (this._state) this._state.closed = true } @@ -240,30 +234,6 @@ it.instance( { config: { mcp: {} } }, ) -it.instance( - "forwards tool cancellation to the MCP request", - () => - MCP.Service.use((mcp: MCPNS.Interface) => - Effect.gen(function* () { - lastCreatedClientName = "cancel-server" - const serverState = getOrCreateClientState("cancel-server") - const controller = new AbortController() - - yield* mcp.add("cancel-server", { - type: "local", - command: ["echo", "test"], - }) - - const tools = yield* mcp.tools() - yield* Effect.promise(() => - tools["cancel-server_test_tool"].execute!({}, { abortSignal: controller.signal } as never), - ) - expect(serverState.callToolSignal).toBe(controller.signal) - }), - ), - { config: { mcp: {} } }, -) - // ======================================================================== // Test: tool change notifications refresh the cache // ========================================================================