mirror of
https://github.com/morluto/rea.git
synced 2026-10-29 19:05:45 +00:00
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>
89 lines
2.7 KiB
TypeScript
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();
|
|
}
|
|
});
|