Skip to main content
Glama

MCP TypeScript SDK

mcp.test.ts107 kB
import { McpServer } from "./mcp.js"; import { Client } from "../client/index.js"; import { InMemoryTransport } from "../inMemory.js"; import { z } from "zod"; import { ListToolsResultSchema, CallToolResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, ReadResourceResultSchema, ListPromptsResultSchema, GetPromptResultSchema, CompleteResultSchema, LoggingMessageNotificationSchema, Notification, TextContent, ElicitRequestSchema, } from "../types.js"; import { ResourceTemplate } from "./mcp.js"; import { completable } from "./completable.js"; import { UriTemplate } from "../shared/uriTemplate.js"; import { getDisplayName } from "../shared/metadataUtils.js"; describe("McpServer", () => { /*** * Test: Basic Server Instance */ test("should expose underlying Server instance", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); expect(mcpServer.server).toBeDefined(); }); /*** * Test: Notification Sending via Server */ test("should allow sending notifications via Server", async () => { const mcpServer = new McpServer( { name: "test server", version: "1.0", }, { capabilities: { logging: {} } }, ); const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification) } const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // This should work because we're using the underlying server await expect( mcpServer.server.sendLoggingMessage({ level: "info", data: "Test log message", }), ).resolves.not.toThrow(); expect(notifications).toMatchObject([ { "method": "notifications/message", params: { level: "info", data: "Test log message", } } ]) }); /*** * Test: Progress Notification with Message Field */ test("should send progress notifications with message field", async () => { const mcpServer = new McpServer( { name: "test server", version: "1.0", } ); // Create a tool that sends progress updates mcpServer.tool( "long-operation", "A long running operation with progress updates", { steps: z.number().min(1).describe("Number of steps to perform"), }, async ({ steps }, { sendNotification, _meta }) => { const progressToken = _meta?.progressToken; if (progressToken) { // Send progress notification for each step for (let i = 1; i <= steps; i++) { await sendNotification({ method: "notifications/progress", params: { progressToken, progress: i, total: steps, message: `Completed step ${i} of ${steps}`, }, }); } } return { content: [{ type: "text" as const, text: `Operation completed with ${steps} steps` }] }; } ); const progressUpdates: Array<{ progress: number, total?: number, message?: string }> = []; const client = new Client({ name: "test client", version: "1.0", }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Call the tool with progress tracking await client.request( { method: "tools/call", params: { name: "long-operation", arguments: { steps: 3 }, _meta: { progressToken: "progress-test-1" } } }, CallToolResultSchema, { onprogress: (progress) => { progressUpdates.push(progress); } } ); // Verify progress notifications were received with message field expect(progressUpdates).toHaveLength(3); expect(progressUpdates[0]).toMatchObject({ progress: 1, total: 3, message: "Completed step 1 of 3", }); expect(progressUpdates[1]).toMatchObject({ progress: 2, total: 3, message: "Completed step 2 of 3", }); expect(progressUpdates[2]).toMatchObject({ progress: 3, total: 3, message: "Completed step 3 of 3", }); }); }); describe("ResourceTemplate", () => { /*** * Test: ResourceTemplate Creation with String Pattern */ test("should create ResourceTemplate with string pattern", () => { const template = new ResourceTemplate("test://{category}/{id}", { list: undefined, }); expect(template.uriTemplate.toString()).toBe("test://{category}/{id}"); expect(template.listCallback).toBeUndefined(); }); /*** * Test: ResourceTemplate Creation with UriTemplate Instance */ test("should create ResourceTemplate with UriTemplate", () => { const uriTemplate = new UriTemplate("test://{category}/{id}"); const template = new ResourceTemplate(uriTemplate, { list: undefined }); expect(template.uriTemplate).toBe(uriTemplate); expect(template.listCallback).toBeUndefined(); }); /*** * Test: ResourceTemplate with List Callback */ test("should create ResourceTemplate with list callback", async () => { const list = jest.fn().mockResolvedValue({ resources: [{ name: "Test", uri: "test://example" }], }); const template = new ResourceTemplate("test://{id}", { list }); expect(template.listCallback).toBe(list); const abortController = new AbortController(); const result = await template.listCallback?.({ signal: abortController.signal, requestId: 'not-implemented', sendRequest: () => { throw new Error("Not implemented") }, sendNotification: () => { throw new Error("Not implemented") } }); expect(result?.resources).toHaveLength(1); expect(list).toHaveBeenCalled(); }); }); describe("tool()", () => { /*** * Test: Zero-Argument Tool Registration */ test("should register zero-argument tool", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification) } mcpServer.tool("test", async () => ({ content: [ { type: "text", text: "Test response", }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); const result = await client.request( { method: "tools/list", }, ListToolsResultSchema, ); expect(result.tools).toHaveLength(1); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].inputSchema).toEqual({ type: "object", }); // Adding the tool before the connection was established means no notification was sent expect(notifications).toHaveLength(0) // Adding another tool triggers the update notification mcpServer.tool("test2", async () => ({ content: [ { type: "text", text: "Test response", }, ], })); // Yield event loop to let the notification fly await new Promise(process.nextTick) expect(notifications).toMatchObject([ { method: "notifications/tools/list_changed", } ]) }); /*** * Test: Updating Existing Tool */ test("should update existing tool", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification) } // Register initial tool const tool = mcpServer.tool("test", async () => ({ content: [ { type: "text", text: "Initial response", }, ], })); // Update the tool tool.update({ callback: async () => ({ content: [ { type: "text", text: "Updated response", }, ], }) }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Call the tool and verify we get the updated response const result = await client.request( { method: "tools/call", params: { name: "test", }, }, CallToolResultSchema, ); expect(result.content).toEqual([ { type: "text", text: "Updated response", }, ]); // Update happened before transport was connected, so no notifications should be expected expect(notifications).toHaveLength(0) }); /*** * Test: Updating Tool with Schema */ test("should update tool with schema", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification) } // Register initial tool const tool = mcpServer.tool( "test", { name: z.string(), }, async ({ name }) => ({ content: [ { type: "text", text: `Initial: ${name}`, }, ], }), ); // Update the tool with a different schema tool.update({ paramsSchema: { name: z.string(), value: z.number(), }, callback: async ({ name, value }) => ({ content: [ { type: "text", text: `Updated: ${name}, ${value}`, }, ], }) }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Verify the schema was updated const listResult = await client.request( { method: "tools/list", }, ListToolsResultSchema, ); expect(listResult.tools[0].inputSchema).toMatchObject({ properties: { name: { type: "string" }, value: { type: "number" }, }, }); // Call the tool with the new schema const callResult = await client.request( { method: "tools/call", params: { name: "test", arguments: { name: "test", value: 42, }, }, }, CallToolResultSchema, ); expect(callResult.content).toEqual([ { type: "text", text: "Updated: test, 42", }, ]); // Update happened before transport was connected, so no notifications should be expected expect(notifications).toHaveLength(0) }); /*** * Test: Tool List Changed Notifications */ test("should send tool list changed notifications when connected", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = [] const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification) } // Register initial tool const tool = mcpServer.tool("test", async () => ({ content: [ { type: "text", text: "Test response", }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); expect(notifications).toHaveLength(0) // Now update the tool tool.update({ callback: async () => ({ content: [ { type: "text", text: "Updated response", }, ], }) }); // Yield event loop to let the notification fly await new Promise(process.nextTick) expect(notifications).toMatchObject([ { method: "notifications/tools/list_changed" } ]) // Now delete the tool tool.remove(); // Yield event loop to let the notification fly await new Promise(process.nextTick) expect(notifications).toMatchObject([ { method: "notifications/tools/list_changed" }, { method: "notifications/tools/list_changed" }, ]) }); /*** * Test: Tool Registration with Parameters */ test("should register tool with params", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); // old api mcpServer.tool( "test", { name: z.string(), value: z.number(), }, async ({ name, value }) => ({ content: [ { type: "text", text: `${name}: ${value}`, }, ], }), ); // new api mcpServer.registerTool( "test (new api)", { inputSchema: { name: z.string(), value: z.number() }, }, async ({ name, value }) => ({ content: [{ type: "text", text: `${name}: ${value}` }], }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/list", }, ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", properties: { name: { type: "string" }, value: { type: "number" }, }, }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema); }); /*** * Test: Tool Registration with Description */ test("should register tool with description", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); // old api mcpServer.tool("test", "Test description", async () => ({ content: [ { type: "text", text: "Test response", }, ], })); // new api mcpServer.registerTool( "test (new api)", { description: "Test description", }, async () => ({ content: [ { type: "text" as const, text: "Test response", }, ], }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/list", }, ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].description).toBe("Test description"); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].description).toBe("Test description"); }); /*** * Test: Tool Registration with Annotations */ test("should register tool with annotations", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.tool("test", { title: "Test Tool", readOnlyHint: true }, async () => ({ content: [ { type: "text", text: "Test response", }, ], })); mcpServer.registerTool( "test (new api)", { annotations: { title: "Test Tool", readOnlyHint: true }, }, async () => ({ content: [ { type: "text" as const, text: "Test response", }, ], }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/list", }, ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); }); /*** * Test: Tool Registration with Parameters and Annotations */ test("should register tool with params and annotations", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.tool( "test", { name: z.string() }, { title: "Test Tool", readOnlyHint: true }, async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }] }) ); mcpServer.registerTool( "test (new api)", { inputSchema: { name: z.string() }, annotations: { title: "Test Tool", readOnlyHint: true }, }, async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }] }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/list" }, ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", properties: { name: { type: "string" } } }); expect(result.tools[0].annotations).toEqual({ title: "Test Tool", readOnlyHint: true }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema); expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); }); /*** * Test: Tool Registration with Description, Parameters, and Annotations */ test("should register tool with description, params, and annotations", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.tool( "test", "A tool with everything", { name: z.string() }, { title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false }, async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }] }) ); mcpServer.registerTool( "test (new api)", { description: "A tool with everything", inputSchema: { name: z.string() }, annotations: { title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false }, }, async ({ name }) => ({ content: [{ type: "text", text: `Hello, ${name}!` }] }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/list" }, ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].description).toBe("A tool with everything"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", properties: { name: { type: "string" } } }); expect(result.tools[0].annotations).toEqual({ title: "Complete Test Tool", readOnlyHint: true, openWorldHint: false }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].description).toBe("A tool with everything"); expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema); expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); }); /*** * Test: Tool Registration with Description, Empty Parameters, and Annotations */ test("should register tool with description, empty params, and annotations", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.tool( "test", "A tool with everything but empty params", {}, { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false }, async () => ({ content: [{ type: "text", text: "Test response" }] }) ); mcpServer.registerTool( "test (new api)", { description: "A tool with everything but empty params", inputSchema: {}, annotations: { title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false }, }, async () => ({ content: [{ type: "text" as const, text: "Test response" }] }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/list" }, ListToolsResultSchema, ); expect(result.tools).toHaveLength(2); expect(result.tools[0].name).toBe("test"); expect(result.tools[0].description).toBe("A tool with everything but empty params"); expect(result.tools[0].inputSchema).toMatchObject({ type: "object", properties: {} }); expect(result.tools[0].annotations).toEqual({ title: "Complete Test Tool with empty params", readOnlyHint: true, openWorldHint: false }); expect(result.tools[1].name).toBe("test (new api)"); expect(result.tools[1].description).toBe("A tool with everything but empty params"); expect(result.tools[1].inputSchema).toEqual(result.tools[0].inputSchema); expect(result.tools[1].annotations).toEqual(result.tools[0].annotations); }); /*** * Test: Tool Argument Validation */ test("should validate tool args", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.tool( "test", { name: z.string(), value: z.number(), }, async ({ name, value }) => ({ content: [ { type: "text", text: `${name}: ${value}`, }, ], }), ); mcpServer.registerTool( "test (new api)", { inputSchema: { name: z.string(), value: z.number(), }, }, async ({ name, value }) => ({ content: [ { type: "text", text: `${name}: ${value}`, }, ], }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); await expect( client.request( { method: "tools/call", params: { name: "test", arguments: { name: "test", value: "not a number", }, }, }, CallToolResultSchema, ), ).rejects.toThrow(/Invalid arguments/); await expect( client.request( { method: "tools/call", params: { name: "test (new api)", arguments: { name: "test", value: "not a number", }, }, }, CallToolResultSchema, ), ).rejects.toThrow(/Invalid arguments/); }); /*** * Test: Preventing Duplicate Tool Registration */ test("should prevent duplicate tool registration", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); mcpServer.tool("test", async () => ({ content: [ { type: "text", text: "Test response", }, ], })); expect(() => { mcpServer.tool("test", async () => ({ content: [ { type: "text", text: "Test response 2", }, ], })); }).toThrow(/already registered/); }); /*** * Test: Multiple Tool Registration */ test("should allow registering multiple tools", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); // This should succeed mcpServer.tool("tool1", () => ({ content: [] })); // This should also succeed and not throw about request handlers mcpServer.tool("tool2", () => ({ content: [] })); }); /*** * Test: Tool with Output Schema and Structured Content */ test("should support tool with outputSchema and structuredContent", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); // Register a tool with outputSchema mcpServer.registerTool( "test", { description: "Test tool with structured output", inputSchema: { input: z.string(), }, outputSchema: { processedInput: z.string(), resultType: z.string(), timestamp: z.string() }, }, async ({ input }) => ({ structuredContent: { processedInput: input, resultType: "structured", timestamp: "2023-01-01T00:00:00Z" }, content: [ { type: "text", text: JSON.stringify({ processedInput: input, resultType: "structured", timestamp: "2023-01-01T00:00:00Z" }), }, ] }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Verify the tool registration includes outputSchema const listResult = await client.request( { method: "tools/list", }, ListToolsResultSchema, ); expect(listResult.tools).toHaveLength(1); expect(listResult.tools[0].outputSchema).toMatchObject({ type: "object", properties: { processedInput: { type: "string" }, resultType: { type: "string" }, timestamp: { type: "string" } }, required: ["processedInput", "resultType", "timestamp"] }); // Call the tool and verify it returns valid structuredContent const result = await client.request( { method: "tools/call", params: { name: "test", arguments: { input: "hello", }, }, }, CallToolResultSchema, ); expect(result.structuredContent).toBeDefined(); const structuredContent = result.structuredContent as { processedInput: string; resultType: string; timestamp: string; }; expect(structuredContent.processedInput).toBe("hello"); expect(structuredContent.resultType).toBe("structured"); expect(structuredContent.timestamp).toBe("2023-01-01T00:00:00Z"); // For backward compatibility, content is auto-generated from structuredContent expect(result.content).toBeDefined(); expect(result.content!).toHaveLength(1); expect(result.content![0]).toMatchObject({ type: "text" }); const textContent = result.content![0] as TextContent; expect(JSON.parse(textContent.text)).toEqual(result.structuredContent); }); /*** * Test: Tool with Output Schema Must Provide Structured Content */ test("should throw error when tool with outputSchema returns no structuredContent", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); // Register a tool with outputSchema that returns only content without structuredContent mcpServer.registerTool( "test", { description: "Test tool with output schema but missing structured content", inputSchema: { input: z.string(), }, outputSchema: { processedInput: z.string(), resultType: z.string(), }, }, async ({ input }) => ({ // Only return content without structuredContent content: [ { type: "text", text: `Processed: ${input}`, }, ], }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Call the tool and expect it to throw an error await expect( client.callTool({ name: "test", arguments: { input: "hello", }, }), ).rejects.toThrow(/Tool test has an output schema but no structured content was provided/); }); /*** * Test: Schema Validation Failure for Invalid Structured Content */ test("should fail schema validation when tool returns invalid structuredContent", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); // Register a tool with outputSchema that returns invalid data mcpServer.registerTool( "test", { description: "Test tool with invalid structured output", inputSchema: { input: z.string(), }, outputSchema: { processedInput: z.string(), resultType: z.string(), timestamp: z.string() }, }, async ({ input }) => ({ content: [ { type: "text", text: JSON.stringify({ processedInput: input, resultType: "structured", // Missing required 'timestamp' field someExtraField: "unexpected" // Extra field not in schema }), }, ], structuredContent: { processedInput: input, resultType: "structured", // Missing required 'timestamp' field someExtraField: "unexpected" // Extra field not in schema }, }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Call the tool and expect it to throw a server-side validation error await expect( client.callTool({ name: "test", arguments: { input: "hello", }, }), ).rejects.toThrow(/Invalid structured content for tool test/); }); /*** * Test: Pass Session ID to Tool Callback */ test("should pass sessionId to tool callback via RequestHandlerExtra", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); let receivedSessionId: string | undefined; mcpServer.tool("test-tool", async (extra) => { receivedSessionId = extra.sessionId; return { content: [ { type: "text", text: "Test response", }, ], }; }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); // Set a test sessionId on the server transport serverTransport.sessionId = "test-session-123"; await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); await client.request( { method: "tools/call", params: { name: "test-tool", }, }, CallToolResultSchema, ); expect(receivedSessionId).toBe("test-session-123"); }); /*** * Test: Pass Request ID to Tool Callback */ test("should pass requestId to tool callback via RequestHandlerExtra", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); let receivedRequestId: string | number | undefined; mcpServer.tool("request-id-test", async (extra) => { receivedRequestId = extra.requestId; return { content: [ { type: "text", text: `Received request ID: ${extra.requestId}`, }, ], }; }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/call", params: { name: "request-id-test", }, }, CallToolResultSchema, ); expect(receivedRequestId).toBeDefined(); expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); expect(result.content && result.content[0].text).toContain("Received request ID:"); }); /*** * Test: Send Notification within Tool Call */ test("should provide sendNotification within tool call", async () => { const mcpServer = new McpServer( { name: "test server", version: "1.0", }, { capabilities: { logging: {} } }, ); const client = new Client({ name: "test client", version: "1.0", }); let receivedLogMessage: string | undefined; const loggingMessage = "hello here is log message 1"; client.setNotificationHandler(LoggingMessageNotificationSchema, (notification) => { receivedLogMessage = notification.params.data as string; }); mcpServer.tool("test-tool", async ({ sendNotification }) => { await sendNotification({ method: "notifications/message", params: { level: "debug", data: loggingMessage } }); return { content: [ { type: "text", text: "Test response", }, ], }; }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); await client.request( { method: "tools/call", params: { name: "test-tool", }, }, CallToolResultSchema, ); expect(receivedLogMessage).toBe(loggingMessage); }); /*** * Test: Client to Server Tool Call */ test("should allow client to call server tools", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.tool( "test", "Test tool", { input: z.string(), }, async ({ input }) => ({ content: [ { type: "text", text: `Processed: ${input}`, }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/call", params: { name: "test", arguments: { input: "hello", }, }, }, CallToolResultSchema, ); expect(result.content).toEqual([ { type: "text", text: "Processed: hello", }, ]); }); /*** * Test: Graceful Tool Error Handling */ test("should handle server tool errors gracefully", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.tool("error-test", async () => { throw new Error("Tool execution failed"); }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "tools/call", params: { name: "error-test", }, }, CallToolResultSchema, ); expect(result.isError).toBe(true); expect(result.content).toEqual([ { type: "text", text: "Tool execution failed", }, ]); }); /*** * Test: McpError for Invalid Tool Name */ test("should throw McpError for invalid tool name", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.tool("test-tool", async () => ({ content: [ { type: "text", text: "Test response", }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); await expect( client.request( { method: "tools/call", params: { name: "nonexistent-tool", }, }, CallToolResultSchema, ), ).rejects.toThrow(/Tool nonexistent-tool not found/); }); }); describe("resource()", () => { /*** * Test: Resource Registration with URI and Read Callback */ test("should register resource with uri and readCallback", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource("test", "test://resource", async () => ({ contents: [ { uri: "test://resource", text: "Test content", }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "resources/list", }, ListResourcesResultSchema, ); expect(result.resources).toHaveLength(1); expect(result.resources[0].name).toBe("test"); expect(result.resources[0].uri).toBe("test://resource"); }); /*** * Test: Update Resource with URI */ test("should update resource with uri", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register initial resource const resource = mcpServer.resource("test", "test://resource", async () => ({ contents: [ { uri: "test://resource", text: "Initial content", }, ], })); // Update the resource resource.update({ callback: async () => ({ contents: [ { uri: "test://resource", text: "Updated content", }, ], }) }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Read the resource and verify we get the updated content const result = await client.request( { method: "resources/read", params: { uri: "test://resource", }, }, ReadResourceResultSchema, ); expect(result.contents).toHaveLength(1); expect(result.contents[0].text).toBe("Updated content"); // Update happened before transport was connected, so no notifications should be expected expect(notifications).toHaveLength(0); }); /*** * Test: Update Resource Template */ test("should update resource template", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register initial resource template const resourceTemplate = mcpServer.resource( "test", new ResourceTemplate("test://resource/{id}", { list: undefined }), async (uri) => ({ contents: [ { uri: uri.href, text: "Initial content", }, ], }), ); // Update the resource template resourceTemplate.update({ callback: async (uri) => ({ contents: [ { uri: uri.href, text: "Updated content", }, ], }) }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Read the resource and verify we get the updated content const result = await client.request( { method: "resources/read", params: { uri: "test://resource/123", }, }, ReadResourceResultSchema, ); expect(result.contents).toHaveLength(1); expect(result.contents[0].text).toBe("Updated content"); // Update happened before transport was connected, so no notifications should be expected expect(notifications).toHaveLength(0); }); /*** * Test: Resource List Changed Notification */ test("should send resource list changed notification when connected", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register initial resource const resource = mcpServer.resource("test", "test://resource", async () => ({ contents: [ { uri: "test://resource", text: "Test content", }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); expect(notifications).toHaveLength(0); // Now update the resource while connected resource.update({ callback: async () => ({ contents: [ { uri: "test://resource", text: "Updated content", }, ], }) }); // Yield event loop to let the notification fly await new Promise(process.nextTick); expect(notifications).toMatchObject([ { method: "notifications/resources/list_changed" } ]); }); /*** * Test: Remove Resource and Send Notification */ test("should remove resource and send notification when connected", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register initial resources const resource1 = mcpServer.resource("resource1", "test://resource1", async () => ({ contents: [{ uri: "test://resource1", text: "Resource 1 content" }], })); mcpServer.resource("resource2", "test://resource2", async () => ({ contents: [{ uri: "test://resource2", text: "Resource 2 content" }], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Verify both resources are registered let result = await client.request( { method: "resources/list" }, ListResourcesResultSchema, ); expect(result.resources).toHaveLength(2); expect(notifications).toHaveLength(0); // Remove a resource resource1.remove() // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ { method: "notifications/resources/list_changed" } ]); // Verify the resource was removed result = await client.request( { method: "resources/list" }, ListResourcesResultSchema, ); expect(result.resources).toHaveLength(1); expect(result.resources[0].uri).toBe("test://resource2"); }); /*** * Test: Remove Resource Template and Send Notification */ test("should remove resource template and send notification when connected", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register resource template const resourceTemplate = mcpServer.resource( "template", new ResourceTemplate("test://resource/{id}", { list: undefined }), async (uri) => ({ contents: [ { uri: uri.href, text: "Template content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Verify template is registered const result = await client.request( { method: "resources/templates/list" }, ListResourceTemplatesResultSchema, ); expect(result.resourceTemplates).toHaveLength(1); expect(notifications).toHaveLength(0); // Remove the template resourceTemplate.remove() // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ { method: "notifications/resources/list_changed" } ]); // Verify the template was removed const result2 = await client.request( { method: "resources/templates/list" }, ListResourceTemplatesResultSchema, ); expect(result2.resourceTemplates).toHaveLength(0); }); /*** * Test: Resource Registration with Metadata */ test("should register resource with metadata", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", "test://resource", { description: "Test resource", mimeType: "text/plain", }, async () => ({ contents: [ { uri: "test://resource", text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "resources/list", }, ListResourcesResultSchema, ); expect(result.resources).toHaveLength(1); expect(result.resources[0].description).toBe("Test resource"); expect(result.resources[0].mimeType).toBe("text/plain"); }); /*** * Test: Resource Template Registration */ test("should register resource template", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{id}", { list: undefined }), async () => ({ contents: [ { uri: "test://resource/123", text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "resources/templates/list", }, ListResourceTemplatesResultSchema, ); expect(result.resourceTemplates).toHaveLength(1); expect(result.resourceTemplates[0].name).toBe("test"); expect(result.resourceTemplates[0].uriTemplate).toBe( "test://resource/{id}", ); }); /*** * Test: Resource Template with List Callback */ test("should register resource template with listCallback", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{id}", { list: async () => ({ resources: [ { name: "Resource 1", uri: "test://resource/1", }, { name: "Resource 2", uri: "test://resource/2", }, ], }), }), async (uri) => ({ contents: [ { uri: uri.href, text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "resources/list", }, ListResourcesResultSchema, ); expect(result.resources).toHaveLength(2); expect(result.resources[0].name).toBe("Resource 1"); expect(result.resources[0].uri).toBe("test://resource/1"); expect(result.resources[1].name).toBe("Resource 2"); expect(result.resources[1].uri).toBe("test://resource/2"); }); /*** * Test: Template Variables to Read Callback */ test("should pass template variables to readCallback", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{category}/{id}", { list: undefined, }), async (uri, { category, id }) => ({ contents: [ { uri: uri.href, text: `Category: ${category}, ID: ${id}`, }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "resources/read", params: { uri: "test://resource/books/123", }, }, ReadResourceResultSchema, ); expect(result.contents[0].text).toBe("Category: books, ID: 123"); }); /*** * Test: Preventing Duplicate Resource Registration */ test("should prevent duplicate resource registration", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); mcpServer.resource("test", "test://resource", async () => ({ contents: [ { uri: "test://resource", text: "Test content", }, ], })); expect(() => { mcpServer.resource("test2", "test://resource", async () => ({ contents: [ { uri: "test://resource", text: "Test content 2", }, ], })); }).toThrow(/already registered/); }); /*** * Test: Multiple Resource Registration */ test("should allow registering multiple resources", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); // This should succeed mcpServer.resource("resource1", "test://resource1", async () => ({ contents: [ { uri: "test://resource1", text: "Test content 1", }, ], })); // This should also succeed and not throw about request handlers mcpServer.resource("resource2", "test://resource2", async () => ({ contents: [ { uri: "test://resource2", text: "Test content 2", }, ], })); }); /*** * Test: Preventing Duplicate Resource Template Registration */ test("should prevent duplicate resource template registration", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{id}", { list: undefined }), async () => ({ contents: [ { uri: "test://resource/123", text: "Test content", }, ], }), ); expect(() => { mcpServer.resource( "test", new ResourceTemplate("test://resource/{id}", { list: undefined }), async () => ({ contents: [ { uri: "test://resource/123", text: "Test content 2", }, ], }), ); }).toThrow(/already registered/); }); /*** * Test: Graceful Resource Read Error Handling */ test("should handle resource read errors gracefully", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource("error-test", "test://error", async () => { throw new Error("Resource read failed"); }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); await expect( client.request( { method: "resources/read", params: { uri: "test://error", }, }, ReadResourceResultSchema, ), ).rejects.toThrow(/Resource read failed/); }); /*** * Test: McpError for Invalid Resource URI */ test("should throw McpError for invalid resource URI", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource("test", "test://resource", async () => ({ contents: [ { uri: "test://resource", text: "Test content", }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); await expect( client.request( { method: "resources/read", params: { uri: "test://nonexistent", }, }, ReadResourceResultSchema, ), ).rejects.toThrow(/Resource test:\/\/nonexistent not found/); }); /*** * Test: Registering a resource template with a complete callback should update server capabilities to advertise support for completion */ test("should advertise support for completion when a resource template with a complete callback is defined", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{category}", { list: undefined, complete: { category: () => ["books", "movies", "music"], }, }), async () => ({ contents: [ { uri: "test://resource/test", text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) }) /*** * Test: Resource Template Parameter Completion */ test("should support completion of resource template parameters", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{category}", { list: undefined, complete: { category: () => ["books", "movies", "music"], }, }), async () => ({ contents: [ { uri: "test://resource/test", text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "completion/complete", params: { ref: { type: "ref/resource", uri: "test://resource/{category}", }, argument: { name: "category", value: "", }, }, }, CompleteResultSchema, ); expect(result.completion.values).toEqual(["books", "movies", "music"]); expect(result.completion.total).toBe(3); }); /*** * Test: Filtered Resource Template Parameter Completion */ test("should support filtered completion of resource template parameters", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{category}", { list: undefined, complete: { category: (test: string) => ["books", "movies", "music"].filter((value) => value.startsWith(test), ), }, }), async () => ({ contents: [ { uri: "test://resource/test", text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "completion/complete", params: { ref: { type: "ref/resource", uri: "test://resource/{category}", }, argument: { name: "category", value: "m", }, }, }, CompleteResultSchema, ); expect(result.completion.values).toEqual(["movies", "music"]); expect(result.completion.total).toBe(2); }); /*** * Test: Pass Request ID to Resource Callback */ test("should pass requestId to resource callback via RequestHandlerExtra", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); let receivedRequestId: string | number | undefined; mcpServer.resource("request-id-test", "test://resource", async (_uri, extra) => { receivedRequestId = extra.requestId; return { contents: [ { uri: "test://resource", text: `Received request ID: ${extra.requestId}`, }, ], }; }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "resources/read", params: { uri: "test://resource", }, }, ReadResourceResultSchema, ); expect(receivedRequestId).toBeDefined(); expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); expect(result.contents[0].text).toContain("Received request ID:"); }); }); describe("prompt()", () => { /*** * Test: Zero-Argument Prompt Registration */ test("should register zero-argument prompt", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.prompt("test", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Test response", }, }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "prompts/list", }, ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(1); expect(result.prompts[0].name).toBe("test"); expect(result.prompts[0].arguments).toBeUndefined(); }); /*** * Test: Updating Existing Prompt */ test("should update existing prompt", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register initial prompt const prompt = mcpServer.prompt("test", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Initial response", }, }, ], })); // Update the prompt prompt.update({ callback: async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Updated response", }, }, ], }) }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Call the prompt and verify we get the updated response const result = await client.request( { method: "prompts/get", params: { name: "test", }, }, GetPromptResultSchema, ); expect(result.messages).toHaveLength(1); expect(result.messages[0].content.text).toBe("Updated response"); // Update happened before transport was connected, so no notifications should be expected expect(notifications).toHaveLength(0); }); /*** * Test: Updating Prompt with Schema */ test("should update prompt with schema", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register initial prompt const prompt = mcpServer.prompt( "test", { name: z.string(), }, async ({ name }) => ({ messages: [ { role: "assistant", content: { type: "text", text: `Initial: ${name}`, }, }, ], }), ); // Update the prompt with a different schema prompt.update({ argsSchema: { name: z.string(), value: z.string(), }, callback: async ({ name, value }) => ({ messages: [ { role: "assistant", content: { type: "text", text: `Updated: ${name}, ${value}`, }, }, ], }) }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Verify the schema was updated const listResult = await client.request( { method: "prompts/list", }, ListPromptsResultSchema, ); expect(listResult.prompts[0].arguments).toHaveLength(2); expect(listResult.prompts[0].arguments?.map(a => a.name).sort()).toEqual(["name", "value"]); // Call the prompt with the new schema const getResult = await client.request( { method: "prompts/get", params: { name: "test", arguments: { name: "test", value: "value", }, }, }, GetPromptResultSchema, ); expect(getResult.messages).toHaveLength(1); expect(getResult.messages[0].content.text).toBe("Updated: test, value"); // Update happened before transport was connected, so no notifications should be expected expect(notifications).toHaveLength(0); }); /*** * Test: Prompt List Changed Notification */ test("should send prompt list changed notification when connected", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register initial prompt const prompt = mcpServer.prompt("test", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Test response", }, }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); expect(notifications).toHaveLength(0); // Now update the prompt while connected prompt.update({ callback: async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Updated response", }, }, ], }) }); // Yield event loop to let the notification fly await new Promise(process.nextTick); expect(notifications).toMatchObject([ { method: "notifications/prompts/list_changed" } ]); }); /*** * Test: Remove Prompt and Send Notification */ test("should remove prompt and send notification when connected", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const notifications: Notification[] = []; const client = new Client({ name: "test client", version: "1.0", }); client.fallbackNotificationHandler = async (notification) => { notifications.push(notification); }; // Register initial prompts const prompt1 = mcpServer.prompt("prompt1", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Prompt 1 response", }, }, ], })); mcpServer.prompt("prompt2", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Prompt 2 response", }, }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); // Verify both prompts are registered let result = await client.request( { method: "prompts/list" }, ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(2); expect(result.prompts.map(p => p.name).sort()).toEqual(["prompt1", "prompt2"]); expect(notifications).toHaveLength(0); // Remove a prompt prompt1.remove() // Yield event loop to let the notification fly await new Promise(process.nextTick); // Should have sent notification expect(notifications).toMatchObject([ { method: "notifications/prompts/list_changed" } ]); // Verify the prompt was removed result = await client.request( { method: "prompts/list" }, ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(1); expect(result.prompts[0].name).toBe("prompt2"); }); /*** * Test: Prompt Registration with Arguments Schema */ test("should register prompt with args schema", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.prompt( "test", { name: z.string(), value: z.string(), }, async ({ name, value }) => ({ messages: [ { role: "assistant", content: { type: "text", text: `${name}: ${value}`, }, }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "prompts/list", }, ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(1); expect(result.prompts[0].name).toBe("test"); expect(result.prompts[0].arguments).toEqual([ { name: "name", required: true }, { name: "value", required: true }, ]); }); /*** * Test: Prompt Registration with Description */ test("should register prompt with description", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.prompt("test", "Test description", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Test response", }, }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "prompts/list", }, ListPromptsResultSchema, ); expect(result.prompts).toHaveLength(1); expect(result.prompts[0].name).toBe("test"); expect(result.prompts[0].description).toBe("Test description"); }); /*** * Test: Prompt Argument Validation */ test("should validate prompt args", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.prompt( "test", { name: z.string(), value: z.string().min(3), }, async ({ name, value }) => ({ messages: [ { role: "assistant", content: { type: "text", text: `${name}: ${value}`, }, }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); await expect( client.request( { method: "prompts/get", params: { name: "test", arguments: { name: "test", value: "ab", // Too short }, }, }, GetPromptResultSchema, ), ).rejects.toThrow(/Invalid arguments/); }); /*** * Test: Preventing Duplicate Prompt Registration */ test("should prevent duplicate prompt registration", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); mcpServer.prompt("test", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Test response", }, }, ], })); expect(() => { mcpServer.prompt("test", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Test response 2", }, }, ], })); }).toThrow(/already registered/); }); /*** * Test: Multiple Prompt Registration */ test("should allow registering multiple prompts", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); // This should succeed mcpServer.prompt("prompt1", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Test response 1", }, }, ], })); // This should also succeed and not throw about request handlers mcpServer.prompt("prompt2", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Test response 2", }, }, ], })); }); /*** * Test: Prompt Registration with Arguments */ test("should allow registering prompts with arguments", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); // This should succeed mcpServer.prompt( "echo", { message: z.string() }, ({ message }) => ({ messages: [{ role: "user", content: { type: "text", text: `Please process this message: ${message}` } }] }) ); }); /*** * Test: Resources and Prompts with Completion Handlers */ test("should allow registering both resources and prompts with completion handlers", () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); // Register a resource with completion mcpServer.resource( "test", new ResourceTemplate("test://resource/{category}", { list: undefined, complete: { category: () => ["books", "movies", "music"], }, }), async () => ({ contents: [ { uri: "test://resource/test", text: "Test content", }, ], }), ); // Register a prompt with completion mcpServer.prompt( "echo", { message: completable(z.string(), () => ["hello", "world"]) }, ({ message }) => ({ messages: [{ role: "user", content: { type: "text", text: `Please process this message: ${message}` } }] }) ); }); /*** * Test: McpError for Invalid Prompt Name */ test("should throw McpError for invalid prompt name", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.prompt("test-prompt", async () => ({ messages: [ { role: "assistant", content: { type: "text", text: "Test response", }, }, ], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); await expect( client.request( { method: "prompts/get", params: { name: "nonexistent-prompt", }, }, GetPromptResultSchema, ), ).rejects.toThrow(/Prompt nonexistent-prompt not found/); }); /*** * Test: Registering a prompt with a completable argument should update server capabilities to advertise support for completion */ test("should advertise support for completion when a prompt with a completable argument is defined", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.prompt( "test-prompt", { name: completable(z.string(), () => ["Alice", "Bob", "Charlie"]), }, async ({ name }) => ({ messages: [ { role: "assistant", content: { type: "text", text: `Hello ${name}`, }, }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); expect(client.getServerCapabilities()).toMatchObject({ completions: {} }) }) /*** * Test: Prompt Argument Completion */ test("should support completion of prompt arguments", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.prompt( "test-prompt", { name: completable(z.string(), () => ["Alice", "Bob", "Charlie"]), }, async ({ name }) => ({ messages: [ { role: "assistant", content: { type: "text", text: `Hello ${name}`, }, }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "completion/complete", params: { ref: { type: "ref/prompt", name: "test-prompt", }, argument: { name: "name", value: "", }, }, }, CompleteResultSchema, ); expect(result.completion.values).toEqual(["Alice", "Bob", "Charlie"]); expect(result.completion.total).toBe(3); }); /*** * Test: Filtered Prompt Argument Completion */ test("should support filtered completion of prompt arguments", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.prompt( "test-prompt", { name: completable(z.string(), (test) => ["Alice", "Bob", "Charlie"].filter((value) => value.startsWith(test)), ), }, async ({ name }) => ({ messages: [ { role: "assistant", content: { type: "text", text: `Hello ${name}`, }, }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "completion/complete", params: { ref: { type: "ref/prompt", name: "test-prompt", }, argument: { name: "name", value: "A", }, }, }, CompleteResultSchema, ); expect(result.completion.values).toEqual(["Alice"]); expect(result.completion.total).toBe(1); }); /*** * Test: Pass Request ID to Prompt Callback */ test("should pass requestId to prompt callback via RequestHandlerExtra", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); let receivedRequestId: string | number | undefined; mcpServer.prompt("request-id-test", async (extra) => { receivedRequestId = extra.requestId; return { messages: [ { role: "assistant", content: { type: "text", text: `Received request ID: ${extra.requestId}`, }, }, ], }; }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "prompts/get", params: { name: "request-id-test", }, }, GetPromptResultSchema, ); expect(receivedRequestId).toBeDefined(); expect(typeof receivedRequestId === 'string' || typeof receivedRequestId === 'number').toBe(true); expect(result.messages[0].content.text).toContain("Received request ID:"); }); /*** * Test: Resource Template Metadata Priority */ test("should prioritize individual resource metadata over template metadata", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{id}", { list: async () => ({ resources: [ { name: "Resource 1", uri: "test://resource/1", description: "Individual resource description", mimeType: "text/plain", }, { name: "Resource 2", uri: "test://resource/2", // This resource has no description or mimeType }, ], }), }), { description: "Template description", mimeType: "application/json", }, async (uri) => ({ contents: [ { uri: uri.href, text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "resources/list", }, ListResourcesResultSchema, ); expect(result.resources).toHaveLength(2); // Resource 1 should have its own metadata expect(result.resources[0].name).toBe("Resource 1"); expect(result.resources[0].description).toBe("Individual resource description"); expect(result.resources[0].mimeType).toBe("text/plain"); // Resource 2 should inherit template metadata expect(result.resources[1].name).toBe("Resource 2"); expect(result.resources[1].description).toBe("Template description"); expect(result.resources[1].mimeType).toBe("application/json"); }); /*** * Test: Resource Template Metadata Overrides All Fields */ test("should allow resource to override all template metadata fields", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.resource( "test", new ResourceTemplate("test://resource/{id}", { list: async () => ({ resources: [ { name: "Overridden Name", uri: "test://resource/1", description: "Overridden description", mimeType: "text/markdown", // Add any other metadata fields if they exist }, ], }), }), { name: "Template Name", description: "Template description", mimeType: "application/json", }, async (uri) => ({ contents: [ { uri: uri.href, text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); const result = await client.request( { method: "resources/list", }, ListResourcesResultSchema, ); expect(result.resources).toHaveLength(1); // All fields should be from the individual resource, not the template expect(result.resources[0].name).toBe("Overridden Name"); expect(result.resources[0].description).toBe("Overridden description"); expect(result.resources[0].mimeType).toBe("text/markdown"); }); }); describe("Tool title precedence", () => { test("should follow correct title precedence: title → annotations.title → name", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); // Tool 1: Only name mcpServer.tool( "tool_name_only", async () => ({ content: [{ type: "text", text: "Response" }], }) ); // Tool 2: Name and annotations.title mcpServer.tool( "tool_with_annotations_title", "Tool with annotations title", { title: "Annotations Title" }, async () => ({ content: [{ type: "text", text: "Response" }], }) ); // Tool 3: Name and title (using registerTool) mcpServer.registerTool( "tool_with_title", { title: "Regular Title", description: "Tool with regular title" }, async () => ({ content: [{ type: "text", text: "Response" }], }) ); // Tool 4: All three - title should win mcpServer.registerTool( "tool_with_all_titles", { title: "Regular Title Wins", description: "Tool with all titles", annotations: { title: "Annotations Title Should Not Show" } }, async () => ({ content: [{ type: "text", text: "Response" }], }) ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.connect(serverTransport), ]); const result = await client.request( { method: "tools/list" }, ListToolsResultSchema, ); expect(result.tools).toHaveLength(4); // Tool 1: Only name - should display name const tool1 = result.tools.find(t => t.name === "tool_name_only"); expect(tool1).toBeDefined(); expect(getDisplayName(tool1!)).toBe("tool_name_only"); // Tool 2: Name and annotations.title - should display annotations.title const tool2 = result.tools.find(t => t.name === "tool_with_annotations_title"); expect(tool2).toBeDefined(); expect(tool2!.annotations?.title).toBe("Annotations Title"); expect(getDisplayName(tool2!)).toBe("Annotations Title"); // Tool 3: Name and title - should display title const tool3 = result.tools.find(t => t.name === "tool_with_title"); expect(tool3).toBeDefined(); expect(tool3!.title).toBe("Regular Title"); expect(getDisplayName(tool3!)).toBe("Regular Title"); // Tool 4: All three - title should take precedence const tool4 = result.tools.find(t => t.name === "tool_with_all_titles"); expect(tool4).toBeDefined(); expect(tool4!.title).toBe("Regular Title Wins"); expect(tool4!.annotations?.title).toBe("Annotations Title Should Not Show"); expect(getDisplayName(tool4!)).toBe("Regular Title Wins"); }); test("getDisplayName unit tests for title precedence", () => { // Test 1: Only name expect(getDisplayName({ name: "tool_name" })).toBe("tool_name"); // Test 2: Name and title - title wins expect(getDisplayName({ name: "tool_name", title: "Tool Title" })).toBe("Tool Title"); // Test 3: Name and annotations.title - annotations.title wins expect(getDisplayName({ name: "tool_name", annotations: { title: "Annotations Title" } })).toBe("Annotations Title"); // Test 4: All three - title wins (correct precedence) expect(getDisplayName({ name: "tool_name", title: "Regular Title", annotations: { title: "Annotations Title" } })).toBe("Regular Title"); // Test 5: Empty title should not be used expect(getDisplayName({ name: "tool_name", title: "", annotations: { title: "Annotations Title" } })).toBe("Annotations Title"); // Test 6: Undefined vs null handling expect(getDisplayName({ name: "tool_name", title: undefined, annotations: { title: "Annotations Title" } })).toBe("Annotations Title"); }); test("should support resource template completion with resolved context", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.registerResource( "test", new ResourceTemplate("github://repos/{owner}/{repo}", { list: undefined, complete: { repo: (value, context) => { if (context?.arguments?.["owner"] === "org1") { return ["project1", "project2", "project3"].filter(r => r.startsWith(value)); } else if (context?.arguments?.["owner"] === "org2") { return ["repo1", "repo2", "repo3"].filter(r => r.startsWith(value)); } return []; }, }, }), { title: "GitHub Repository", description: "Repository information" }, async () => ({ contents: [ { uri: "github://repos/test/test", text: "Test content", }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Test with microsoft owner const result1 = await client.request( { method: "completion/complete", params: { ref: { type: "ref/resource", uri: "github://repos/{owner}/{repo}", }, argument: { name: "repo", value: "p", }, context: { arguments: { owner: "org1", }, }, }, }, CompleteResultSchema, ); expect(result1.completion.values).toEqual(["project1", "project2", "project3"]); expect(result1.completion.total).toBe(3); // Test with facebook owner const result2 = await client.request( { method: "completion/complete", params: { ref: { type: "ref/resource", uri: "github://repos/{owner}/{repo}", }, argument: { name: "repo", value: "r", }, context: { arguments: { owner: "org2", }, }, }, }, CompleteResultSchema, ); expect(result2.completion.values).toEqual(["repo1", "repo2", "repo3"]); expect(result2.completion.total).toBe(3); // Test with no resolved context const result3 = await client.request( { method: "completion/complete", params: { ref: { type: "ref/resource", uri: "github://repos/{owner}/{repo}", }, argument: { name: "repo", value: "t", }, }, }, CompleteResultSchema, ); expect(result3.completion.values).toEqual([]); expect(result3.completion.total).toBe(0); }); test("should support prompt argument completion with resolved context", async () => { const mcpServer = new McpServer({ name: "test server", version: "1.0", }); const client = new Client({ name: "test client", version: "1.0", }); mcpServer.registerPrompt( "test-prompt", { title: "Team Greeting", description: "Generate a greeting for team members", argsSchema: { department: completable(z.string(), (value) => { return ["engineering", "sales", "marketing", "support"].filter(d => d.startsWith(value)); }), name: completable(z.string(), (value, context) => { const department = context?.arguments?.["department"]; if (department === "engineering") { return ["Alice", "Bob", "Charlie"].filter(n => n.startsWith(value)); } else if (department === "sales") { return ["David", "Eve", "Frank"].filter(n => n.startsWith(value)); } else if (department === "marketing") { return ["Grace", "Henry", "Iris"].filter(n => n.startsWith(value)); } return ["Guest"].filter(n => n.startsWith(value)); }), } }, async ({ department, name }) => ({ messages: [ { role: "assistant", content: { type: "text", text: `Hello ${name}, welcome to the ${department} team!`, }, }, ], }), ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Test with engineering department const result1 = await client.request( { method: "completion/complete", params: { ref: { type: "ref/prompt", name: "test-prompt", }, argument: { name: "name", value: "A", }, context: { arguments: { department: "engineering", }, }, }, }, CompleteResultSchema, ); expect(result1.completion.values).toEqual(["Alice"]); // Test with sales department const result2 = await client.request( { method: "completion/complete", params: { ref: { type: "ref/prompt", name: "test-prompt", }, argument: { name: "name", value: "D", }, context: { arguments: { department: "sales", }, }, }, }, CompleteResultSchema, ); expect(result2.completion.values).toEqual(["David"]); // Test with marketing department const result3 = await client.request( { method: "completion/complete", params: { ref: { type: "ref/prompt", name: "test-prompt", }, argument: { name: "name", value: "G", }, context: { arguments: { department: "marketing", }, }, }, }, CompleteResultSchema, ); expect(result3.completion.values).toEqual(["Grace"]); // Test with no resolved context const result4 = await client.request( { method: "completion/complete", params: { ref: { type: "ref/prompt", name: "test-prompt", }, argument: { name: "name", value: "G", }, }, }, CompleteResultSchema, ); expect(result4.completion.values).toEqual(["Guest"]); }); }); describe("elicitInput()", () => { const checkAvailability = jest.fn().mockResolvedValue(false); const findAlternatives = jest.fn().mockResolvedValue([]); const makeBooking = jest.fn().mockResolvedValue("BOOKING-123"); let mcpServer: McpServer; let client: Client; beforeEach(() => { jest.clearAllMocks(); // Create server with restaurant booking tool mcpServer = new McpServer({ name: "restaurant-booking-server", version: "1.0.0", }); // Register the restaurant booking tool from README example mcpServer.tool( "book-restaurant", { restaurant: z.string(), date: z.string(), partySize: z.number() }, async ({ restaurant, date, partySize }) => { // Check availability const available = await checkAvailability(restaurant, date, partySize); if (!available) { // Ask user if they want to try alternative dates const result = await mcpServer.server.elicitInput({ message: `No tables available at ${restaurant} on ${date}. Would you like to check alternative dates?`, requestedSchema: { type: "object", properties: { checkAlternatives: { type: "boolean", title: "Check alternative dates", description: "Would you like me to check other dates?" }, flexibleDates: { type: "string", title: "Date flexibility", description: "How flexible are your dates?", enum: ["next_day", "same_week", "next_week"], enumNames: ["Next day", "Same week", "Next week"] } }, required: ["checkAlternatives"] } }); if (result.action === "accept" && result.content?.checkAlternatives) { const alternatives = await findAlternatives( restaurant, date, partySize, result.content.flexibleDates as string ); return { content: [{ type: "text", text: `Found these alternatives: ${alternatives.join(", ")}` }] }; } return { content: [{ type: "text", text: "No booking made. Original date not available." }] }; } await makeBooking(restaurant, date, partySize); return { content: [{ type: "text", text: `Booked table for ${partySize} at ${restaurant} on ${date}` }] }; } ); // Create client with elicitation capability client = new Client( { name: "test-client", version: "1.0.0", }, { capabilities: { elicitation: {}, }, } ); }); test("should successfully elicit additional information", async () => { // Mock availability check to return false checkAvailability.mockResolvedValue(false); findAlternatives.mockResolvedValue(["2024-12-26", "2024-12-27", "2024-12-28"]); // Set up client to accept alternative date checking client.setRequestHandler(ElicitRequestSchema, async (request) => { expect(request.params.message).toContain("No tables available at ABC Restaurant on 2024-12-25"); return { action: "accept", content: { checkAlternatives: true, flexibleDates: "same_week" } }; }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Call the tool const result = await client.callTool({ name: "book-restaurant", arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", partySize: 2 } }); expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); expect(findAlternatives).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2, "same_week"); expect(result.content).toEqual([{ type: "text", text: "Found these alternatives: 2024-12-26, 2024-12-27, 2024-12-28" }]); }); test("should handle user declining to elicitation request", async () => { // Mock availability check to return false checkAvailability.mockResolvedValue(false); // Set up client to reject alternative date checking client.setRequestHandler(ElicitRequestSchema, async () => { return { action: "accept", content: { checkAlternatives: false } }; }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Call the tool const result = await client.callTool({ name: "book-restaurant", arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", partySize: 2 } }); expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); expect(findAlternatives).not.toHaveBeenCalled(); expect(result.content).toEqual([{ type: "text", text: "No booking made. Original date not available." }]); }); test("should handle user cancelling the elicitation", async () => { // Mock availability check to return false checkAvailability.mockResolvedValue(false); // Set up client to cancel the elicitation client.setRequestHandler(ElicitRequestSchema, async () => { return { action: "cancel" }; }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), mcpServer.server.connect(serverTransport), ]); // Call the tool const result = await client.callTool({ name: "book-restaurant", arguments: { restaurant: "ABC Restaurant", date: "2024-12-25", partySize: 2 } }); expect(checkAvailability).toHaveBeenCalledWith("ABC Restaurant", "2024-12-25", 2); expect(findAlternatives).not.toHaveBeenCalled(); expect(result.content).toEqual([{ type: "text", text: "No booking made. Original date not available." }]); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jeffwalters9597/MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server