Skip to content

Commit 36b9e73

Browse files
Feelw00claude
andcommitted
test(mcp): cover plugin tool cancellation propagation
Regression test for SOL-0010 fix. Uses a real MCP SDK Client + Server linked by InMemoryTransport (no SDK mocks) so the production wiring (setRequestHandler callback + callTool + tool.execute) is exercised end-to-end. Asserts that the in-flight tool receives the host's AbortSignal and that an abort listener fires when the client cancels. AI-assisted: drafted with claude code (claude-opus-4-7). Co-Authored-By: Claude Opus 4.7 <[email protected]>
1 parent aeaa6e9 commit 36b9e73

1 file changed

Lines changed: 73 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3+
import { describe, expect, it } from "vitest";
4+
import type { AnyAgentTool } from "../agents/tools/common.js";
5+
import { createToolsMcpServer } from "./tools-stdio-server.js";
6+
7+
describe("plugin-tools handler forwards host cancellation to tool.execute", () => {
8+
it("aborts in-flight tool.execute when client cancels the request", async () => {
9+
let observedSignal: AbortSignal | undefined;
10+
let abortFired = false;
11+
12+
const probeTool = {
13+
name: "probe-cancel",
14+
description: "regression probe for SOL-0010 signal propagation",
15+
parameters: {},
16+
ownerOnly: false,
17+
execute: async (_toolCallId: string, _params: unknown, signal?: AbortSignal) => {
18+
observedSignal = signal;
19+
await new Promise<void>((resolve, reject) => {
20+
if (!signal) {
21+
// Without the fix the signal is undefined; surface that explicitly
22+
// so the test fails fast instead of hanging.
23+
reject(new Error("tool.execute did not receive AbortSignal"));
24+
return;
25+
}
26+
if (signal.aborted) {
27+
abortFired = true;
28+
resolve();
29+
return;
30+
}
31+
signal.addEventListener(
32+
"abort",
33+
() => {
34+
abortFired = true;
35+
resolve();
36+
},
37+
{ once: true },
38+
);
39+
});
40+
return { content: [{ type: "text", text: "done" }] };
41+
},
42+
} as unknown as AnyAgentTool;
43+
44+
const server = createToolsMcpServer({ name: "test", tools: [probeTool] });
45+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
46+
const client = new Client({ name: "test-client", version: "0.0.0" }, { capabilities: {} });
47+
48+
await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]);
49+
50+
try {
51+
const controller = new AbortController();
52+
const callPromise = client.callTool({ name: "probe-cancel", arguments: {} }, undefined, {
53+
signal: controller.signal,
54+
});
55+
56+
// Let the server handler invoke tool.execute and register the abort
57+
// listener before we trip the controller.
58+
await new Promise((r) => setTimeout(r, 20));
59+
expect(observedSignal, "handler should receive AbortSignal via extra.signal").toBeInstanceOf(
60+
AbortSignal,
61+
);
62+
expect(observedSignal?.aborted).toBe(false);
63+
64+
controller.abort();
65+
66+
await expect(callPromise).rejects.toBeDefined();
67+
expect(abortFired, "tool.execute observed signal.abort event").toBe(true);
68+
} finally {
69+
await client.close();
70+
await server.close();
71+
}
72+
});
73+
});

0 commit comments

Comments
 (0)