/** * MCPServer edge-case coverage — exercises server-side branches not naturally * hit by happy-path tests: provider fall-backs, list-helpers with optional * fields, normaliseToolResult array path, Zod root-level error formatting, * authorize-true branch, signal forwarding to direct invocation API. */ import { describe, expect, it } from "@suaveplan/testing/runner"; import { MCPClient } from "../client/mcp-client/mcp-client.js"; import { MCP_CODES } from "../error-codes/error-codes.js"; import { JsonRpcDispatcher } from "../jsonrpc/dispatcher/dispatcher.js"; import { MCPServer } from "../server/mcp-server/mcp-server.js"; import { InMemoryTransport } from "../transport/in-memory/in-memory.js"; describe("server edge cases", () => { it("server without sampling/completion provider falls back", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); const peer = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "X", }); const completion = await peer.request("completion/complete", { ref: { type: "ref/prompt", name: "x" }, argument: { name: "a", value: "b" }, }); expect( (completion as { completion: { values: string[] } }).completion .values ).toEqual([]); const pong = await peer.request("ping", {}); expect(pong).toEqual({}); const list = await peer.request("resources/list", {}); expect((list as { resources: unknown[] }).resources).toEqual([]); const tplList = await peer.request("resources/templates/list", {}); expect( (tplList as { resourceTemplates: unknown[] }).resourceTemplates ).toEqual([]); await expect( peer.request("tools/call", { name: "nope" }) ).rejects.toMatchObject({ code: MCP_CODES.INTERNAL_ERROR }); await expect( peer.request("resources/read", { uri: "mem://nope" }) ).rejects.toMatchObject({ code: MCP_CODES.INTERNAL_ERROR }); await expect( peer.request("prompts/get", { name: "nope" }) ).rejects.toMatchObject({ code: MCP_CODES.INTERNAL_ERROR }); await peer.request("resources/subscribe", { uri: "mem://x" }); await peer.request("resources/unsubscribe", { uri: "mem://x" }); await peer.close(); await server.stop(); }); it("tool with paramsSchema validates over the dispatcher", { timeout: 5000, }, async () => { const z = await import("zod"); const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "validated", inputSchema: {}, paramsSchema: z.default.object({ n: z.default.number() }), async handler(args) { return { content: [ { type: "text", text: String((args as { n: number }).n), }, ], }; }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); const peer = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "X", }); await expect( peer.request("tools/call", { name: "validated", arguments: { n: "not-a-number" }, }) ).rejects.toMatchObject({ code: MCP_CODES.INVALID_PARAMS }); const ok = await peer.request("tools/call", { name: "validated", arguments: { n: 7 }, }); expect( (ok as { content: Array<{ text: string }> }).content[0]?.text ).toBe("7"); await peer.close(); await server.stop(); }); it("readResource and getPromptMessages accept signal", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerResource({ uri: "mem://x", name: "x", async handler({ signal }) { return { uri: "mem://x", text: signal.aborted ? "aborted" : "ok", }; }, }); server.registerPrompt({ name: "p", async handler() { return [ { role: "user", content: { type: "text", text: "hi" }, }, ]; }, }); const ac = new AbortController(); const r = await server.readResource("mem://x", { signal: ac.signal }); expect(r[0]?.text).toBe("ok"); const m = await server.getPromptMessages( "p", {}, { signal: ac.signal } ); expect(m.length).toBe(1); server.registerTool({ name: "fast", inputSchema: {}, async handler(_args, ctx) { return { content: [ { type: "text", text: ctx.signal.aborted ? "x" : "ok", }, ], }; }, }); const r2 = await server.invokeTool("fast", {}, { signal: ac.signal }); expect(r2.content[0]).toMatchObject({ text: "ok" }); }); it("server invokeTool with paramsSchema replaces params with parsed data", { timeout: 5000, }, async () => { const z = await import("zod"); const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "sum", inputSchema: {}, paramsSchema: z.default.object({ a: z.default.number(), b: z.default.number(), }), async handler(args) { const { a, b } = args as { a: number; b: number }; return { content: [{ type: "text", text: String(a + b) }], }; }, }); const result = await server.invokeTool("sum", { a: 2, b: 3 }); expect(result.content[0]).toMatchObject({ text: "5" }); }); it("server initialized getter starts false and flips true after handshake", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); expect(server.initialized).toBe(false); const client = new MCPClient({ info: { name: "c", version: "0.1.0" }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); await client.connect(b); expect(server.initialized).toBe(true); await client.disconnect(); await server.stop(); }); it("server rejects initialize for unsupported protocol version", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); const peer = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "X", }); await expect( peer.request("initialize", { protocolVersion: "9999-01-01", capabilities: {}, clientInfo: { name: "c", version: "0.1.0" }, }) ).rejects.toMatchObject({ code: MCP_CODES.INTERNAL_ERROR, }); await peer.close(); await server.stop(); }); it("tool handler returning a ContentItem array is normalised", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "arr", inputSchema: {}, async handler() { return [{ type: "text" as const, text: "x" }]; }, }); const result = await server.invokeTool("arr", {}); expect(result.content).toEqual([{ type: "text", text: "x" }]); expect(result.isError).toBeUndefined(); }); it("server resource handler returning array passes through", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerResource({ uri: "mem://multi", name: "multi", async handler() { return [ { uri: "mem://multi", text: "part1" }, { uri: "mem://multi", text: "part2" }, ]; }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); const peer = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "X", }); const out = (await peer.request("resources/read", { uri: "mem://multi", })) as { contents: unknown[] }; expect(out.contents.length).toBe(2); await peer.close(); await server.stop(); }); it("prompts/get over the wire surfaces handler description", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerPrompt({ name: "with-desc", description: "a documented prompt", async handler() { return [ { role: "user" as const, content: { type: "text" as const, text: "hi" }, }, ]; }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); const peer = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "X", }); const out = (await peer.request("prompts/get", { name: "with-desc", })) as { description?: string; messages: unknown[] }; expect(out.description).toBe("a documented prompt"); expect(out.messages.length).toBe(1); await peer.close(); await server.stop(); }); it("server unregisterTool without dispatcher does not notify", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "z", inputSchema: {}, async handler() { return { content: [] }; }, }); expect(server.unregisterTool("z")).toBe(true); expect(server.unregisterTool("z")).toBe(false); }); it("unregister returns false for unknown resource/prompt names", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); expect(server.unregisterResource("mem://missing")).toBe(false); expect(server.unregisterPrompt("missing")).toBe(false); }); it("list helpers surface optional description/mimeType/arguments", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerResource({ uri: "mem://r", name: "r", description: "rd", mimeType: "text/plain", async handler() { return { uri: "mem://r" }; }, }); server.registerPrompt({ name: "p", description: "pd", arguments: [{ name: "a" }], async handler() { return []; }, }); server.registerTool({ name: "t", description: "td", inputSchema: {}, async handler() { return { content: [] }; }, }); const resources = server.listResources(); expect(resources[0]?.description).toBe("rd"); expect(resources[0]?.mimeType).toBe("text/plain"); const prompts = server.listPrompts(); expect(prompts[0]?.description).toBe("pd"); expect(prompts[0]?.arguments).toEqual([{ name: "a" }]); const tools = server.listTools(); expect(tools[0]?.description).toBe("td"); }); it("markResourceUpdated before start is a silent no-op", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.markResourceUpdated("mem://x"); }); it("server invokeTool wraps non-Error handler throws into isError", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "throwsString", inputSchema: {}, async handler() { const v: unknown = "raw-string"; throw v; }, }); const result = await server.invokeTool("throwsString", {}); expect(result.isError).toBe(true); expect(result.content[0]).toMatchObject({ text: "Error: raw-string" }); }); it("server readResource returns array handler output unwrapped", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerResource({ uri: "mem://multi", name: "multi", async handler() { return [ { uri: "mem://multi", text: "a" }, { uri: "mem://multi", text: "b" }, ]; }, }); const out = await server.readResource("mem://multi"); expect(out.length).toBe(2); }); it("server start wires injected clock and idGenerator", { timeout: 5000, }, async () => { let n = 0; const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, clock: () => 4242, idGenerator: () => { n += 1; return `s-${n}`; }, }); const [t] = InMemoryTransport.pair(); await server.start(t); expect(server.running).toBe(true); await server.stop(); }); });