UNPKG

@odel/module-sdk

Version:

SDK for building Odel modules - MCP protocol over HTTP for Cloudflare Workers

176 lines 8.08 kB
/** * MCP Protocol Compliance Tests * * These tests validate that a module correctly implements the MCP protocol. * All modules should use these tests to ensure protocol compliance. */ import { describe, it, expect } from 'vitest'; /** * Standard MCP protocol compliance tests * * @param getContext - Function that returns test context with worker and environment * @param expectedTools - Array of tool names that should be registered * * @example * ```typescript * import { testMCPCompliance } from '@odel/module-sdk/testing'; * import worker from './src/index'; * * testMCPCompliance( * () => ({ * worker, * env: {}, * createExecutionContext, * waitOnExecutionContext * }), * ['add', 'subtract'] * ); * ``` */ export function testMCPCompliance(getContext, expectedTools) { describe('MCP Protocol Compliance', () => { describe('tools/list', () => { it('returns valid JSON-RPC response', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = createMCPRequest('tools/list'); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); expect(response.status).toBe(200); const result = await response.json(); expect(result.jsonrpc).toBe('2.0'); expect(result.id).toBe(1); expect(result.result).toBeDefined(); }); it('returns all expected tools', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = createMCPRequest('tools/list'); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); const result = await response.json(); expect(result.result.tools).toBeDefined(); expect(Array.isArray(result.result.tools)).toBe(true); const toolNames = result.result.tools.map((t) => t.name); expect(toolNames).toEqual(expectedTools); }); it('includes required fields for each tool', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = createMCPRequest('tools/list'); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); const result = await response.json(); for (const tool of result.result.tools) { expect(tool.name).toBeDefined(); expect(typeof tool.name).toBe('string'); expect(tool.description).toBeDefined(); expect(typeof tool.description).toBe('string'); expect(tool.inputSchema).toBeDefined(); expect(typeof tool.inputSchema).toBe('object'); } }); it('supports extended mode with outputSchema', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = createMCPRequest('tools/list', { extended: true }); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); const result = await response.json(); for (const tool of result.result.tools) { expect(tool.outputSchema).toBeDefined(); expect(typeof tool.outputSchema).toBe('object'); } }); it('omits outputSchema in standard mode', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = createMCPRequest('tools/list'); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); const result = await response.json(); for (const tool of result.result.tools) { expect(tool.outputSchema).toBeUndefined(); } }); }); describe('tools/call', () => { it('returns error for unknown tool', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = createMCPRequest('tools/call', { name: 'nonexistent_tool', arguments: {} }); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); const result = await response.json(); expect(result.error).toBeDefined(); expect(result.error.code).toBe(-32601); }); it('validates input parameters', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = createMCPRequest('tools/call', { name: expectedTools[0], arguments: { invalid: 'params' } }); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); const result = await response.json(); // Should either succeed (if invalid params are valid for this tool) // or return validation error if (result.error) { expect(result.error.code).toBe(-32602); } else { expect(result.result).toBeDefined(); } }); }); describe('Unknown methods', () => { it('returns error for unknown method', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = createMCPRequest('unknown/method'); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); const result = await response.json(); expect(result.error).toBeDefined(); expect(result.error.code).toBe(-32601); }); }); describe('HTTP methods', () => { it('rejects GET requests', async () => { const { worker, env, createExecutionContext, waitOnExecutionContext } = getContext(); const request = new Request('http://example.com', { method: 'GET' }); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); expect(response.status).toBe(405); }); }); }); } /** * Create a JSON-RPC MCP request * * @param method - MCP method name (e.g., 'tools/list', 'tools/call') * @param params - Optional parameters for the method * @param id - Request ID (defaults to 1) */ function createMCPRequest(method, params, id = 1) { return new Request('http://example.com', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id, method, ...(params && { params }) }) }); } //# sourceMappingURL=mcp-compliance.js.map