import { describe, expect, it } from "vitest"; import { McpGateway, McpGatewayError } from "../src/gateway.js"; import type { McpNamespace, McpToolResult } from "../src/namespace.js"; function makeNamespace(name: string, toolNames: string[], enabled = true): McpNamespace { return { name, enabled, tools: toolNames.map((toolName) => ({ name: toolName, description: `tool ${toolName}`, inputSchema: { type: "object", properties: {} }, handler: (args): McpToolResult => ({ content: [{ type: "text", text: JSON.stringify({ ns: name, tool: toolName, args }) }], }), })), resources: [], prompts: [], }; } function makeResourceNamespace(): McpNamespace { return { name: "map", enabled: true, tools: [], resources: [ { uri: "map://{mapId}", name: "map definition", read: (params) => ({ contents: [{ uri: `map://${params?.["mapId"]}`, text: `map:${params?.["mapId"]}` }] }), }, { uri: "server://status", name: "server status", read: () => ({ contents: [{ uri: "server://status", text: "status-ok" }] }), }, ], prompts: [ { name: "orient", description: "onboard", get: () => ({ messages: [{ role: "assistant", content: { type: "text", text: "orientation" } }] }), }, ], }; } async function toolText(gateway: McpGateway, name: string, args: Record = {}): Promise { const result = await gateway.callTool(name, args); return result.content[0]?.text ?? ""; } describe("McpGateway", () => { it("registers namespaces and reports status", () => { const gateway = new McpGateway(); gateway.register(makeNamespace("server", ["info"])); gateway.register(makeNamespace("map", ["list", "get"])); expect(gateway.namespaces().map((ns) => ns.name)).toEqual(["server", "map"]); expect(gateway.status()).toEqual([ { name: "server", enabled: true }, { name: "map", enabled: true }, ]); gateway.disable("map"); expect(gateway.status()).toEqual([ { name: "server", enabled: true }, { name: "map", enabled: false }, ]); gateway.enable("map"); expect(gateway.status()[1]?.enabled).toBe(true); }); it("rejects duplicate registration", () => { const gateway = new McpGateway(); gateway.register(makeNamespace("server", [])); expect(() => gateway.register(makeNamespace("server", []))).toThrow(/already registered/); }); it("lists tools across namespaces with dotted names", () => { const gateway = new McpGateway(); gateway.register(makeNamespace("server", ["info", "status"])); gateway.register(makeNamespace("map", ["list"])); expect(gateway.listTools().map((tool) => tool.name)).toEqual(["server.info", "server.status", "map.list"]); expect(gateway.listTools()[0]?.description).toBe("tool info"); }); it("routes callTool to the owning namespace and passes arguments", async () => { const gateway = new McpGateway(); gateway.register(makeNamespace("server", ["info", "status"])); gateway.register(makeNamespace("map", ["list"])); const text = await toolText(gateway, "map.list", { extra: 1 }); expect(JSON.parse(text)).toEqual({ ns: "map", tool: "list", args: { extra: 1 } }); }); it("returns an isError result for unknown tools", async () => { const gateway = new McpGateway(); gateway.register(makeNamespace("server", ["info"])); const result = await gateway.callTool("map.list", {}); expect(result.isError).toBe(true); expect(result.content[0]?.text).toMatch(/tool not found: map\.list/); }); it("validates required inputSchema arguments", async () => { const gateway = new McpGateway(); gateway.register({ name: "time", enabled: true, tools: [ { name: "advance", description: "advance", inputSchema: { type: "object", properties: { ticks: { type: "number" } }, required: ["ticks"], }, handler: (args) => ({ content: [{ type: "text", text: `advanced ${String(args["ticks"])}` }] }), }, ], resources: [], prompts: [], }); const ok = await gateway.callTool("time.advance", { ticks: 3 }); expect(ok.isError).toBeUndefined(); expect(ok.content[0]?.text).toBe("advanced 3"); const missing = await gateway.callTool("time.advance", {}); expect(missing.isError).toBe(true); expect(missing.content[0]?.text).toMatch(/missing required arguments: ticks/); }); it("excludes disabled namespaces from listings", () => { const gateway = new McpGateway(); gateway.register(makeNamespace("server", ["info"])); gateway.register(makeNamespace("map", ["list", "get"])); gateway.disable("map"); expect(gateway.listTools().map((tool) => tool.name)).toEqual(["server.info"]); }); it("rejects tool calls on a disabled namespace with a clear error", async () => { const gateway = new McpGateway(); gateway.register(makeNamespace("server", ["info"])); gateway.register(makeNamespace("map", ["list"])); gateway.disable("map"); const result = await gateway.callTool("map.list", {}); expect(result.isError).toBe(true); expect(result.content[0]?.text).toMatch(/namespace "map" is disabled/); }); it("lists resources and routes concrete and template URIs", async () => { const gateway = new McpGateway(); gateway.register(makeResourceNamespace()); expect(gateway.listResources().map((resource) => resource.uri)).toEqual(["map://{mapId}", "server://status"]); const concrete = await gateway.readResource("server://status"); expect(concrete.contents[0]?.text).toBe("status-ok"); const templated = await gateway.readResource("map://farm"); expect(templated.contents[0]).toEqual({ uri: "map://farm", text: "map:farm" }); }); it("throws for unknown resources", async () => { const gateway = new McpGateway(); gateway.register(makeResourceNamespace()); await expect(gateway.readResource("item://nope")).rejects.toThrow(McpGatewayError); await expect(gateway.readResource("item://nope")).rejects.toThrow(/resource not found/); }); it("lists and gets prompts", async () => { const gateway = new McpGateway(); gateway.register(makeResourceNamespace()); expect(gateway.listPrompts().map((prompt) => prompt.name)).toEqual(["map.orient"]); const prompt = await gateway.getPrompt("map.orient", {}); expect(prompt.messages[0]?.content.text).toBe("orientation"); }); it("throws for unknown prompts", async () => { const gateway = new McpGateway(); gateway.register(makeResourceNamespace()); await expect(gateway.getPrompt("server.nope", {})).rejects.toThrow(/prompt not found/); }); it("blocks disabled namespaces across resources and prompts", async () => { const gateway = new McpGateway(); gateway.register(makeResourceNamespace()); gateway.disable("map"); await expect(gateway.readResource("map://farm")).rejects.toThrow(/disabled/); await expect(gateway.getPrompt("map.orient", {})).rejects.toThrow(/disabled/); }); });