diff --git a/src/flows/doctor-health-contributions.test.ts b/src/flows/doctor-health-contributions.test.ts index 279c20ae1f8..ce9000cd764 100644 --- a/src/flows/doctor-health-contributions.test.ts +++ b/src/flows/doctor-health-contributions.test.ts @@ -299,7 +299,9 @@ describe("doctor health contributions", () => { await contribution.run({ cfg: {}, + cfgForPersistence: {}, configResult: { cfg: {} }, + configPath: "/tmp/fake-openclaw.json", sourceConfigValid: true, prompter: buildDoctorPrompter(true), runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() }, @@ -319,7 +321,9 @@ describe("doctor health contributions", () => { await contribution.run({ cfg: {}, + cfgForPersistence: {}, configResult: { cfg: {} }, + configPath: "/tmp/fake-openclaw.json", sourceConfigValid: true, prompter: buildDoctorPrompter(true), runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() }, diff --git a/src/mcp/plugin-tools-handlers.cancel.test.ts b/src/mcp/plugin-tools-handlers.cancel.test.ts new file mode 100644 index 00000000000..adc8f0740a3 --- /dev/null +++ b/src/mcp/plugin-tools-handlers.cancel.test.ts @@ -0,0 +1,69 @@ +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { describe, expect, it } from "vitest"; +import type { AnyAgentTool } from "../agents/tools/common.js"; +import { createToolsMcpServer } from "./tools-stdio-server.js"; + +describe("plugin tools MCP cancellation", () => { + it("forwards host cancellation to tool.execute", async () => { + let resolveObservedSignal: (signal: AbortSignal | undefined) => void; + const observedSignal = new Promise((resolve) => { + resolveObservedSignal = resolve; + }); + let abortObserved = false; + + const tool = { + name: "probe_cancel", + description: "Probe cancellation forwarding", + parameters: { type: "object", properties: {} }, + execute: async (_toolCallId: string, _params: unknown, signal?: AbortSignal) => { + resolveObservedSignal(signal); + await new Promise((resolve, reject) => { + if (!signal) { + reject(new Error("tool.execute did not receive AbortSignal")); + return; + } + if (signal.aborted) { + abortObserved = true; + resolve(); + return; + } + signal.addEventListener( + "abort", + () => { + abortObserved = true; + resolve(); + }, + { once: true }, + ); + }); + return { content: [{ type: "text", text: "done" }] }; + }, + } as unknown as AnyAgentTool; + + const server = createToolsMcpServer({ name: "test", tools: [tool] }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const client = new Client({ name: "test-client", version: "0.0.0" }, { capabilities: {} }); + + await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); + + try { + const controller = new AbortController(); + const callPromise = client.callTool({ name: "probe_cancel", arguments: {} }, undefined, { + signal: controller.signal, + }); + const signal = await observedSignal; + + expect(signal).toBeInstanceOf(AbortSignal); + expect(signal?.aborted).toBe(false); + + controller.abort(); + + await expect(callPromise).rejects.toBeDefined(); + expect(abortObserved).toBe(true); + } finally { + await client.close(); + await server.close(); + } + }); +}); diff --git a/src/mcp/plugin-tools-handlers.ts b/src/mcp/plugin-tools-handlers.ts index 5cf7a3e4670..abd7998f339 100644 --- a/src/mcp/plugin-tools-handlers.ts +++ b/src/mcp/plugin-tools-handlers.ts @@ -42,7 +42,7 @@ export function createPluginToolsMcpHandlers(tools: AnyAgentTool[]) { inputSchema: resolveJsonSchemaForTool(tool), })), }), - callTool: async (params: CallPluginToolParams) => { + callTool: async (params: CallPluginToolParams, signal?: AbortSignal) => { const tool = toolMap.get(params.name); if (!tool) { return { @@ -51,7 +51,7 @@ export function createPluginToolsMcpHandlers(tools: AnyAgentTool[]) { }; } try { - const result = await tool.execute(`mcp-${Date.now()}`, params.arguments ?? {}); + const result = await tool.execute(`mcp-${Date.now()}`, params.arguments ?? {}, signal); const rawContent = result && typeof result === "object" && "content" in result ? (result as { content?: unknown }).content diff --git a/src/mcp/tools-stdio-server.ts b/src/mcp/tools-stdio-server.ts index 7ad96cd4e77..7713e40b607 100644 --- a/src/mcp/tools-stdio-server.ts +++ b/src/mcp/tools-stdio-server.ts @@ -14,8 +14,8 @@ export function createToolsMcpServer(params: { name: string; tools: AnyAgentTool ); server.setRequestHandler(ListToolsRequestSchema, handlers.listTools); - server.setRequestHandler(CallToolRequestSchema, async (request) => { - return await handlers.callTool(request.params); + server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { + return await handlers.callTool(request.params, extra.signal); }); return server;