Files
reverse-engineering-anything/src/server/RequestTrackingMcpServer.test.ts
T
Edgar Arakelyanandgensokyo-dev 60bd646478 feat(mcp): add opt-in streamable HTTP transport (#4955)
Add an explicit Streamable HTTP entry for environment/gateway hosts while keeping stdio registrations and default behavior. Preserve the shared provider/session owner across DELETE and replacement; fence and drain tool/completion work before handover or teardown.

Bound HTTP input and plain-response admission, cancel refused bodies, join whole exchanges and asynchronous response cleanup before provider hooks, gate startup readiness, and reject queued HTTP/1.1 pipelining while retaining sequential keep-alive.

Validation: 8 static/build tasks, 83 adapter tests and registry metadata pass. A real installed gh binary produces seven equivalent CLI/stdio/HTTP reports with DELETE/replacement and clean signal shutdown. Broader local native-provider/ownership failures reproduce on clean main and are documented with limitations in the PR.

Co-authored-by: Edgar Arakelyan <edgar.arakelyan@scale.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-19 23:58:51 +08:00

89 lines
2.7 KiB
TypeScript

import { Client } from "@modelcontextprotocol/client";
import { InMemoryTransport } from "@modelcontextprotocol/server";
import { expect, it } from "vitest";
import { z } from "zod";
import { RequestTrackingMcpServer } from "./RequestTrackingMcpServer.js";
it.each([false, true])(
"preserves context-only callbacks with HTTP tracking enabled=%s",
async (tracking) => {
const server = new RequestTrackingMcpServer({
name: "context-only",
version: "1",
});
if (tracking) server.enableRequestTracking();
server.registerTool("context", {}, (context) => ({
content: [{ type: "text", text: String(context.mcpReq.signal.aborted) }],
}));
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const client = new Client({ name: "context-client", version: "1" });
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
expect((await client.callTool({ name: "context" })).content).toEqual([
{ type: "text", text: "false" },
]);
expect(await server.drainRequests(0)).toBe(true);
} finally {
await client.close();
await server.close();
}
},
);
it("never starts a tool whose asynchronous input validation finishes after transport cancellation", async () => {
let release!: () => void;
let entered!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const validating = new Promise<void>((resolve) => {
entered = resolve;
});
let calls = 0;
const server = new RequestTrackingMcpServer({
name: "validation-race",
version: "1",
});
server.enableRequestTracking();
server.registerTool(
"slow_validation",
{
inputSchema: z.object({
value: z.string().refine(async () => {
entered();
await gate;
return true;
}),
}),
},
() => {
calls += 1;
return { content: [] };
},
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const client = new Client({ name: "validation-client", version: "1" });
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
void client
.callTool({ name: "slow_validation", arguments: { value: "x" } })
.catch(() => undefined);
await validating;
await server.close();
expect(await server.drainRequests(0)).toBe(true);
release();
await new Promise<void>((resolve) => setImmediate(resolve));
expect(calls).toBe(0);
expect(await server.drainRequests(0)).toBe(true);
} finally {
release();
await client.close();
await server.close();
}
});