Skip to main content
Glama

MCP TypeScript SDK

index.test.ts32 kB
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable no-constant-binary-expression */ /* eslint-disable @typescript-eslint/no-unused-expressions */ import { Client } from "./index.js"; import { z } from "zod"; import { RequestSchema, NotificationSchema, ResultSchema, LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS, InitializeRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, CreateMessageRequestSchema, ElicitRequestSchema, ListRootsRequestSchema, ErrorCode, } from "../types.js"; import { Transport } from "../shared/transport.js"; import { Server } from "../server/index.js"; import { InMemoryTransport } from "../inMemory.js"; /*** * Test: Initialize with Matching Protocol Version */ test("should initialize with matching protocol version", async () => { const clientTransport: Transport = { start: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined), send: jest.fn().mockImplementation((message) => { if (message.method === "initialize") { clientTransport.onmessage?.({ jsonrpc: "2.0", id: message.id, result: { protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: {}, serverInfo: { name: "test", version: "1.0", }, instructions: "test instructions", }, }); } return Promise.resolve(); }), }; const client = new Client( { name: "test client", version: "1.0", }, { capabilities: { sampling: {}, }, }, ); await client.connect(clientTransport); // Should have sent initialize with latest version expect(clientTransport.send).toHaveBeenCalledWith( expect.objectContaining({ method: "initialize", params: expect.objectContaining({ protocolVersion: LATEST_PROTOCOL_VERSION, }), }), expect.objectContaining({ relatedRequestId: undefined, }), ); // Should have the instructions returned expect(client.getInstructions()).toEqual("test instructions"); }); /*** * Test: Initialize with Supported Older Protocol Version */ test("should initialize with supported older protocol version", async () => { const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; const clientTransport: Transport = { start: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined), send: jest.fn().mockImplementation((message) => { if (message.method === "initialize") { clientTransport.onmessage?.({ jsonrpc: "2.0", id: message.id, result: { protocolVersion: OLD_VERSION, capabilities: {}, serverInfo: { name: "test", version: "1.0", }, }, }); } return Promise.resolve(); }), }; const client = new Client( { name: "test client", version: "1.0", }, { capabilities: { sampling: {}, }, }, ); await client.connect(clientTransport); // Connection should succeed with the older version expect(client.getServerVersion()).toEqual({ name: "test", version: "1.0", }); // Expect no instructions expect(client.getInstructions()).toBeUndefined(); }); /*** * Test: Reject Unsupported Protocol Version */ test("should reject unsupported protocol version", async () => { const clientTransport: Transport = { start: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined), send: jest.fn().mockImplementation((message) => { if (message.method === "initialize") { clientTransport.onmessage?.({ jsonrpc: "2.0", id: message.id, result: { protocolVersion: "invalid-version", capabilities: {}, serverInfo: { name: "test", version: "1.0", }, }, }); } return Promise.resolve(); }), }; const client = new Client( { name: "test client", version: "1.0", }, { capabilities: { sampling: {}, }, }, ); await expect(client.connect(clientTransport)).rejects.toThrow( "Server's protocol version is not supported: invalid-version", ); expect(clientTransport.close).toHaveBeenCalled(); }); /*** * Test: Connect New Client to Old Supported Server Version */ test("should connect new client to old, supported server version", async () => { const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; const server = new Server( { name: "test server", version: "1.0", }, { capabilities: { resources: {}, tools: {}, }, }, ); server.setRequestHandler(InitializeRequestSchema, (_request) => ({ protocolVersion: OLD_VERSION, capabilities: { resources: {}, tools: {}, }, serverInfo: { name: "old server", version: "1.0", }, })); server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [], })); server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client( { name: "new client", version: "1.0", protocolVersion: LATEST_PROTOCOL_VERSION, }, { capabilities: { sampling: {}, }, enforceStrictCapabilities: true, }, ); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); expect(client.getServerVersion()).toEqual({ name: "old server", version: "1.0", }); }); /*** * Test: Version Negotiation with Old Client and Newer Server */ test("should negotiate version when client is old, and newer server supports its version", async () => { const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; const server = new Server( { name: "new server", version: "1.0", }, { capabilities: { resources: {}, tools: {}, }, }, ); server.setRequestHandler(InitializeRequestSchema, (_request) => ({ protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: { resources: {}, tools: {}, }, serverInfo: { name: "new server", version: "1.0", }, })); server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [], })); server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client( { name: "old client", version: "1.0", protocolVersion: OLD_VERSION, }, { capabilities: { sampling: {}, }, enforceStrictCapabilities: true, }, ); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); expect(client.getServerVersion()).toEqual({ name: "new server", version: "1.0", }); }); /*** * Test: Throw when Old Client and Server Version Mismatch */ test("should throw when client is old, and server doesn't support its version", async () => { const OLD_VERSION = SUPPORTED_PROTOCOL_VERSIONS[1]; const FUTURE_VERSION = "FUTURE_VERSION"; const server = new Server( { name: "new server", version: "1.0", }, { capabilities: { resources: {}, tools: {}, }, }, ); server.setRequestHandler(InitializeRequestSchema, (_request) => ({ protocolVersion: FUTURE_VERSION, capabilities: { resources: {}, tools: {}, }, serverInfo: { name: "new server", version: "1.0", }, })); server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [], })); server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client( { name: "old client", version: "1.0", protocolVersion: OLD_VERSION, }, { capabilities: { sampling: {}, }, enforceStrictCapabilities: true, }, ); await Promise.all([ expect(client.connect(clientTransport)).rejects.toThrow( "Server's protocol version is not supported: FUTURE_VERSION" ), server.connect(serverTransport), ]); }); /*** * Test: Respect Server Capabilities */ test("should respect server capabilities", async () => { const server = new Server( { name: "test server", version: "1.0", }, { capabilities: { resources: {}, tools: {}, }, }, ); server.setRequestHandler(InitializeRequestSchema, (_request) => ({ protocolVersion: LATEST_PROTOCOL_VERSION, capabilities: { resources: {}, tools: {}, }, serverInfo: { name: "test", version: "1.0", }, })); server.setRequestHandler(ListResourcesRequestSchema, () => ({ resources: [], })); server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [], })); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client( { name: "test client", version: "1.0", }, { capabilities: { sampling: {}, }, enforceStrictCapabilities: true, }, ); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // Server supports resources and tools, but not prompts expect(client.getServerCapabilities()).toEqual({ resources: {}, tools: {}, }); // These should work await expect(client.listResources()).resolves.not.toThrow(); await expect(client.listTools()).resolves.not.toThrow(); // These should throw because prompts, logging, and completions are not supported await expect(client.listPrompts()).rejects.toThrow( "Server does not support prompts", ); await expect(client.setLoggingLevel("error")).rejects.toThrow( "Server does not support logging", ); await expect( client.complete({ ref: { type: "ref/prompt", name: "test" }, argument: { name: "test", value: "test" }, }), ).rejects.toThrow("Server does not support completions"); }); /*** * Test: Respect Client Notification Capabilities */ test("should respect client notification capabilities", async () => { const server = new Server( { name: "test server", version: "1.0", }, { capabilities: {}, }, ); const client = new Client( { name: "test client", version: "1.0", }, { capabilities: { roots: { listChanged: true, }, }, }, ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // This should work because the client has the roots.listChanged capability await expect(client.sendRootsListChanged()).resolves.not.toThrow(); // Create a new client without the roots.listChanged capability const clientWithoutCapability = new Client( { name: "test client without capability", version: "1.0", }, { capabilities: {}, enforceStrictCapabilities: true, }, ); await clientWithoutCapability.connect(clientTransport); // This should throw because the client doesn't have the roots.listChanged capability await expect(clientWithoutCapability.sendRootsListChanged()).rejects.toThrow( /^Client does not support/, ); }); /*** * Test: Respect Server Notification Capabilities */ test("should respect server notification capabilities", async () => { const server = new Server( { name: "test server", version: "1.0", }, { capabilities: { logging: {}, resources: { listChanged: true, }, }, }, ); const client = new Client( { name: "test client", version: "1.0", }, { capabilities: {}, }, ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // These should work because the server has the corresponding capabilities await expect( server.sendLoggingMessage({ level: "info", data: "Test" }), ).resolves.not.toThrow(); await expect(server.sendResourceListChanged()).resolves.not.toThrow(); // This should throw because the server doesn't have the tools capability await expect(server.sendToolListChanged()).rejects.toThrow( "Server does not support notifying of tool list changes", ); }); /*** * Test: Only Allow setRequestHandler for Declared Capabilities */ test("should only allow setRequestHandler for declared capabilities", () => { const client = new Client( { name: "test client", version: "1.0", }, { capabilities: { sampling: {}, }, }, ); // This should work because sampling is a declared capability expect(() => { client.setRequestHandler(CreateMessageRequestSchema, () => ({ model: "test-model", role: "assistant", content: { type: "text", text: "Test response", }, })); }).not.toThrow(); // This should throw because roots listing is not a declared capability expect(() => { client.setRequestHandler(ListRootsRequestSchema, () => ({})); }).toThrow("Client does not support roots capability"); }); test("should allow setRequestHandler for declared elicitation capability", () => { const client = new Client( { name: "test-client", version: "1.0.0", }, { capabilities: { elicitation: {}, }, }, ); // This should work because elicitation is a declared capability expect(() => { client.setRequestHandler(ElicitRequestSchema, () => ({ action: "accept", content: { username: "test-user", confirmed: true, }, })); }).not.toThrow(); // This should throw because sampling is not a declared capability expect(() => { client.setRequestHandler(CreateMessageRequestSchema, () => ({ model: "test-model", role: "assistant", content: { type: "text", text: "Test response", }, })); }).toThrow("Client does not support sampling capability"); }); /*** * Test: Type Checking * Test that custom request/notification/result schemas can be used with the Client class. */ test("should typecheck", () => { const GetWeatherRequestSchema = RequestSchema.extend({ method: z.literal("weather/get"), params: z.object({ city: z.string(), }), }); const GetForecastRequestSchema = RequestSchema.extend({ method: z.literal("weather/forecast"), params: z.object({ city: z.string(), days: z.number(), }), }); const WeatherForecastNotificationSchema = NotificationSchema.extend({ method: z.literal("weather/alert"), params: z.object({ severity: z.enum(["warning", "watch"]), message: z.string(), }), }); const WeatherRequestSchema = GetWeatherRequestSchema.or( GetForecastRequestSchema, ); const WeatherNotificationSchema = WeatherForecastNotificationSchema; const WeatherResultSchema = ResultSchema.extend({ temperature: z.number(), conditions: z.string(), }); type WeatherRequest = z.infer<typeof WeatherRequestSchema>; type WeatherNotification = z.infer<typeof WeatherNotificationSchema>; type WeatherResult = z.infer<typeof WeatherResultSchema>; // Create a typed Client for weather data const weatherClient = new Client< WeatherRequest, WeatherNotification, WeatherResult >( { name: "WeatherClient", version: "1.0.0", }, { capabilities: { sampling: {}, }, }, ); // Typecheck that only valid weather requests/notifications/results are allowed false && weatherClient.request( { method: "weather/get", params: { city: "Seattle", }, }, WeatherResultSchema, ); false && weatherClient.notification({ method: "weather/alert", params: { severity: "warning", message: "Storm approaching", }, }); }); /*** * Test: Handle Client Cancelling a Request */ test("should handle client cancelling a request", async () => { const server = new Server( { name: "test server", version: "1.0", }, { capabilities: { resources: {}, }, }, ); // Set up server to delay responding to listResources server.setRequestHandler( ListResourcesRequestSchema, async (request, extra) => { await new Promise((resolve) => setTimeout(resolve, 1000)); return { resources: [], }; }, ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client( { name: "test client", version: "1.0", }, { capabilities: {}, }, ); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // Set up abort controller const controller = new AbortController(); // Issue request but cancel it immediately const listResourcesPromise = client.listResources(undefined, { signal: controller.signal, }); controller.abort("Cancelled by test"); // Request should be rejected await expect(listResourcesPromise).rejects.toBe("Cancelled by test"); }); /*** * Test: Handle Request Timeout */ test("should handle request timeout", async () => { const server = new Server( { name: "test server", version: "1.0", }, { capabilities: { resources: {}, }, }, ); // Set up server with a delayed response server.setRequestHandler( ListResourcesRequestSchema, async (_request, extra) => { const timer = new Promise((resolve) => { const timeout = setTimeout(resolve, 100); extra.signal.addEventListener("abort", () => clearTimeout(timeout)); }); await timer; return { resources: [], }; }, ); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); const client = new Client( { name: "test client", version: "1.0", }, { capabilities: {}, }, ); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // Request with 0 msec timeout should fail immediately await expect( client.listResources(undefined, { timeout: 0 }), ).rejects.toMatchObject({ code: ErrorCode.RequestTimeout, }); }); describe('outputSchema validation', () => { /*** * Test: Validate structuredContent Against outputSchema */ test('should validate structuredContent against outputSchema', async () => { const server = new Server({ name: 'test-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // Set up server handlers server.setRequestHandler(InitializeRequestSchema, async (request) => ({ protocolVersion: request.params.protocolVersion, capabilities: {}, serverInfo: { name: 'test-server', version: '1.0.0', } })); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'test-tool', description: 'A test tool', inputSchema: { type: 'object', properties: {}, }, outputSchema: { type: 'object', properties: { result: { type: 'string' }, count: { type: 'number' }, }, required: ['result', 'count'], additionalProperties: false, }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'test-tool') { return { structuredContent: { result: 'success', count: 42 }, }; } throw new Error('Unknown tool'); }); const client = new Client({ name: 'test-client', version: '1.0.0', }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // List tools to cache the schemas await client.listTools(); // Call the tool - should validate successfully const result = await client.callTool({ name: 'test-tool' }); expect(result.structuredContent).toEqual({ result: 'success', count: 42 }); }); /*** * Test: Throw Error when structuredContent Does Not Match Schema */ test('should throw error when structuredContent does not match schema', async () => { const server = new Server({ name: 'test-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // Set up server handlers server.setRequestHandler(InitializeRequestSchema, async (request) => ({ protocolVersion: request.params.protocolVersion, capabilities: {}, serverInfo: { name: 'test-server', version: '1.0.0', } })); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'test-tool', description: 'A test tool', inputSchema: { type: 'object', properties: {}, }, outputSchema: { type: 'object', properties: { result: { type: 'string' }, count: { type: 'number' }, }, required: ['result', 'count'], additionalProperties: false, }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'test-tool') { // Return invalid structured content (count is string instead of number) return { structuredContent: { result: 'success', count: 'not a number' }, }; } throw new Error('Unknown tool'); }); const client = new Client({ name: 'test-client', version: '1.0.0', }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // List tools to cache the schemas await client.listTools(); // Call the tool - should throw validation error await expect(client.callTool({ name: 'test-tool' })).rejects.toThrow( /Structured content does not match the tool's output schema/ ); }); /*** * Test: Throw Error when Tool with outputSchema Returns No structuredContent */ test('should throw error when tool with outputSchema returns no structuredContent', async () => { const server = new Server({ name: 'test-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // Set up server handlers server.setRequestHandler(InitializeRequestSchema, async (request) => ({ protocolVersion: request.params.protocolVersion, capabilities: {}, serverInfo: { name: 'test-server', version: '1.0.0', } })); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'test-tool', description: 'A test tool', inputSchema: { type: 'object', properties: {}, }, outputSchema: { type: 'object', properties: { result: { type: 'string' }, }, required: ['result'], }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'test-tool') { // Return content instead of structuredContent return { content: [{ type: 'text', text: 'This should be structured content' }], }; } throw new Error('Unknown tool'); }); const client = new Client({ name: 'test-client', version: '1.0.0', }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // List tools to cache the schemas await client.listTools(); // Call the tool - should throw error await expect(client.callTool({ name: 'test-tool' })).rejects.toThrow( /Tool test-tool has an output schema but did not return structured content/ ); }); /*** * Test: Handle Tools Without outputSchema Normally */ test('should handle tools without outputSchema normally', async () => { const server = new Server({ name: 'test-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // Set up server handlers server.setRequestHandler(InitializeRequestSchema, async (request) => ({ protocolVersion: request.params.protocolVersion, capabilities: {}, serverInfo: { name: 'test-server', version: '1.0.0', } })); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'test-tool', description: 'A test tool', inputSchema: { type: 'object', properties: {}, }, // No outputSchema }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'test-tool') { // Return regular content return { content: [{ type: 'text', text: 'Normal response' }], }; } throw new Error('Unknown tool'); }); const client = new Client({ name: 'test-client', version: '1.0.0', }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // List tools to cache the schemas await client.listTools(); // Call the tool - should work normally without validation const result = await client.callTool({ name: 'test-tool' }); expect(result.content).toEqual([{ type: 'text', text: 'Normal response' }]); }); /*** * Test: Handle Complex JSON Schema Validation */ test('should handle complex JSON schema validation', async () => { const server = new Server({ name: 'test-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // Set up server handlers server.setRequestHandler(InitializeRequestSchema, async (request) => ({ protocolVersion: request.params.protocolVersion, capabilities: {}, serverInfo: { name: 'test-server', version: '1.0.0', } })); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'complex-tool', description: 'A tool with complex schema', inputSchema: { type: 'object', properties: {}, }, outputSchema: { type: 'object', properties: { name: { type: 'string', minLength: 3 }, age: { type: 'integer', minimum: 0, maximum: 120 }, active: { type: 'boolean' }, tags: { type: 'array', items: { type: 'string' }, minItems: 1, }, metadata: { type: 'object', properties: { created: { type: 'string' }, }, required: ['created'], }, }, required: ['name', 'age', 'active', 'tags', 'metadata'], additionalProperties: false, }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'complex-tool') { return { structuredContent: { name: 'John Doe', age: 30, active: true, tags: ['user', 'admin'], metadata: { created: '2023-01-01T00:00:00Z', }, }, }; } throw new Error('Unknown tool'); }); const client = new Client({ name: 'test-client', version: '1.0.0', }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // List tools to cache the schemas await client.listTools(); // Call the tool - should validate successfully const result = await client.callTool({ name: 'complex-tool' }); expect(result.structuredContent).toBeDefined(); const structuredContent = result.structuredContent as { name: string; age: number }; expect(structuredContent.name).toBe('John Doe'); expect(structuredContent.age).toBe(30); }); /*** * Test: Fail Validation with Additional Properties When Not Allowed */ test('should fail validation with additional properties when not allowed', async () => { const server = new Server({ name: 'test-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); // Set up server handlers server.setRequestHandler(InitializeRequestSchema, async (request) => ({ protocolVersion: request.params.protocolVersion, capabilities: {}, serverInfo: { name: 'test-server', version: '1.0.0', } })); server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'strict-tool', description: 'A tool with strict schema', inputSchema: { type: 'object', properties: {}, }, outputSchema: { type: 'object', properties: { name: { type: 'string' }, }, required: ['name'], additionalProperties: false, }, }, ], })); server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'strict-tool') { // Return structured content with extra property return { structuredContent: { name: 'John', extraField: 'not allowed', }, }; } throw new Error('Unknown tool'); }); const client = new Client({ name: 'test-client', version: '1.0.0', }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await Promise.all([ client.connect(clientTransport), server.connect(serverTransport), ]); // List tools to cache the schemas await client.listTools(); // Call the tool - should throw validation error due to additional property await expect(client.callTool({ name: 'strict-tool' })).rejects.toThrow( /Structured content does not match the tool's output schema/ ); }); });

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