Files

63 lines
3.3 KiB
TypeScript

/**
* 覆盖 stdio 代理对 MCP 工具、资源、模板、游标与上游错误的转发行为。
*
* @packageDocumentation
*/
import assert from "node:assert/strict";
import test from "node:test";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { createProxyServer } from "../src/bridge.js";
test("stdio proxy forwards tools, resources, templates, reads, errors, and cursors", async () => {
const calls: Array<{ method: string; params: unknown }> = [];
const upstream = {
listTools: async (params?: { cursor?: string }) => {
calls.push({ method: "tools/list", params });
return { tools: [{ name: "list_spaces", description: "List spaces", inputSchema: { type: "object" } }], nextCursor: "tools-next" };
},
callTool: async (params: { name: string; arguments?: Record<string, unknown> }) => {
calls.push({ method: "tools/call", params });
if (params.name === "explode") throw new Error("upstream failed");
return { content: [{ type: "text", text: JSON.stringify(params.arguments) }] };
},
listResources: async (params?: { cursor?: string }) => {
calls.push({ method: "resources/list", params });
return { resources: [{ uri: "gamecraft://spaces", name: "Spaces" }], nextCursor: "resources-next" };
},
listResourceTemplates: async (params?: { cursor?: string }) => {
calls.push({ method: "resources/templates/list", params });
return { resourceTemplates: [{ uriTemplate: "gamecraft://space/{spaceId}", name: "Space" }] };
},
readResource: async (params: { uri: string }) => {
calls.push({ method: "resources/read", params });
return { contents: [{ uri: params.uri, text: "resource-value" }] };
},
};
const server = createProxyServer(upstream);
const client = new Client({ name: "bridge-test", version: "1.0.0" });
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
assert.equal((await client.listTools({ cursor: "tool-cursor" })).nextCursor, "tools-next");
assert.equal((await client.listResources({ cursor: "resource-cursor" })).nextCursor, "resources-next");
assert.equal((await client.listResourceTemplates({ cursor: "template-cursor" })).resourceTemplates.length, 1);
const resource = await client.readResource({ uri: "gamecraft://spaces" });
assert.equal("text" in resource.contents[0]! ? resource.contents[0].text : undefined, "resource-value");
const called = await client.callTool({ name: "list_spaces", arguments: { page: 2 } });
const content = called.content as Array<{ type: string; text?: string }>;
assert.match(String(content[0]?.type === "text" ? content[0].text : ""), /\"page\":2/);
await assert.rejects(client.callTool({ name: "explode", arguments: {} }), /upstream failed/);
assert.deepEqual(calls.slice(0, 3), [
{ method: "tools/list", params: { cursor: "tool-cursor" } },
{ method: "resources/list", params: { cursor: "resource-cursor" } },
{ method: "resources/templates/list", params: { cursor: "template-cursor" } },
]);
} finally {
await client.close();
await server.close();
}
});