63 lines
3.3 KiB
TypeScript
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();
|
|
}
|
|
});
|