/** * MCPServer wire-side edge-case coverage — exercises branches in the * dispatcher-mounted JSON-RPC handlers and helpers: `tools/call` with no * arguments / no `_meta`, signal forwarding to `createSamplingMessage` / * `listClientRoots`, `emitLog` without a logger, authorizer accept-branch, * invokeTool progress + abort listeners, Zod root-level rejection. */ 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 wire edge cases", () => { it("tools/call without arguments uses empty-object schema input", { timeout: 5000, }, async () => { const z = await import("zod"); const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "no-arg", inputSchema: {}, paramsSchema: z.default.object({}).optional(), async handler() { return { content: [{ type: "text", text: "ok" }] }; }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); const peer = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "X", }); const out = (await peer.request("tools/call", { name: "no-arg", })) as { content: Array<{ text: string }> }; expect(out.content[0]?.text).toBe("ok"); await peer.close(); await server.stop(); }); it("tools/call without _meta has undefined progressToken", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "no-meta", inputSchema: {}, async handler(_args, ctx) { ctx.progress(1, 2); return { content: [{ type: "text", text: "done" }] }; }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); const peer = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "X", }); const out = (await peer.request("tools/call", { name: "no-meta", arguments: {}, })) as { content: Array<{ text: string }> }; expect(out.content[0]?.text).toBe("done"); await peer.close(); await server.stop(); }); it("createSamplingMessage and listClientRoots forward signal", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); const client = new MCPClient({ info: { name: "c", version: "0.1.0" }, }); client.setSamplingProvider(async () => ({ role: "assistant" as const, content: { type: "text" as const, text: "ok" }, model: "demo", })); client.setRootsProvider(async () => [{ uri: "file:///x" }]); const [a, b] = InMemoryTransport.pair(); await server.start(a); await client.connect(b); const ac = new AbortController(); const r1 = await server.createSamplingMessage( { messages: [ { role: "user", content: { type: "text", text: "hi" }, }, ], maxTokens: 10, }, { signal: ac.signal } ); expect(r1.role).toBe("assistant"); const r2 = await server.listClientRoots({ signal: ac.signal }); expect(r2.length).toBe(1); await client.disconnect(); await server.stop(); }); it("emitLog without logger argument omits the logger field", { 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 observed: Array<{ logger?: string }> = []; peer.onNotification("notifications/message", (params) => observed.push(params as { logger?: string }) ); server.emitLog("info", { x: 1 }); await new Promise((r) => setTimeout(r, 10)); expect(observed[0]?.logger).toBeUndefined(); await peer.close(); await server.stop(); }); it("authorize hook accepting the call passes the throw branch", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.setAuthorizer(async () => true); server.registerTool({ name: "ok", inputSchema: {}, async handler() { return { content: [{ type: "text", text: "yes" }] }; }, }); const [a, b] = InMemoryTransport.pair(); await server.start(a); const peer = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "X", }); const result = (await peer.request("tools/call", { name: "ok", arguments: {}, })) as { content: Array<{ text: string }> }; expect(result.content[0]?.text).toBe("yes"); await peer.close(); await server.stop(); }); it("invokeTool handler progress and abort-listener fire", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "p", inputSchema: {}, async handler(_args, ctx) { ctx.progress(1, 2); ctx.progress(2, 2); return { content: [{ type: "text", text: "done" }] }; }, }); const ac = new AbortController(); const result = await server.invokeTool("p", {}, { signal: ac.signal }); expect(result.content[0]).toMatchObject({ text: "done" }); ac.abort(); }); it("server readResource and getPromptMessages with aborting signal", { timeout: 5000, }, async () => { const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerResource({ uri: "mem://r", name: "r", async handler() { return { uri: "mem://r", text: "" }; }, }); server.registerPrompt({ name: "p", async handler() { return []; }, }); const ac = new AbortController(); await server.readResource("mem://r", { signal: ac.signal }); await server.getPromptMessages("p", {}, { signal: ac.signal }); ac.abort(); }); it("Zod root-level rejection formats as `(root)` in error message", { timeout: 5000, }, async () => { const z = await import("zod"); const server = new MCPServer({ info: { name: "s", version: "0.1.0" }, }); server.registerTool({ name: "needs-string", inputSchema: {}, paramsSchema: z.default.string(), async handler() { return { content: [] }; }, }); await expect( server.invokeTool("needs-string", { not: "a-string" }) ).rejects.toMatchObject({ code: MCP_CODES.INVALID_PARAMS }); const [a, b] = InMemoryTransport.pair(); const left = new JsonRpcDispatcher({ side: "server", transport: a, dispatcherId: "L", paramSchemas: { strictly: z.default.string(), }, }); const right = new JsonRpcDispatcher({ side: "client", transport: b, dispatcherId: "R", }); left.registerHandler("strictly", async () => ({})); await expect( right.request("strictly", { not: "a-string" }) ).rejects.toMatchObject({ code: MCP_CODES.INVALID_PARAMS }); await left.close(); await right.close(); }); });