import { describe, it, expect, vi } from 'vitest'; import { McpdUpstream } from '../src/upstream/mcpd.js'; import type { JsonRpcRequest } from '../src/types.js'; function mockMcpdClient(responses: Map = new Map()) { return { baseUrl: 'http://test:3100', token: 'test-token', get: vi.fn(), post: vi.fn(async (_path: string, body: unknown) => { const req = body as { serverId: string; method: string }; const key = `${req.serverId}:${req.method}`; if (responses.has(key)) { return responses.get(key); } return { result: { ok: true } }; }), put: vi.fn(), delete: vi.fn(), forward: vi.fn(), }; } describe('McpdUpstream', () => { it('sends tool calls via mcpd proxy', async () => { const client = mockMcpdClient(new Map([ ['srv-1:tools/call', { result: { content: [{ type: 'text', text: 'hello' }] } }], ])); const upstream = new McpdUpstream('srv-1', 'slack', client as any); const request: JsonRpcRequest = { jsonrpc: '2.0', id: '1', method: 'tools/call', params: { name: 'search', arguments: { query: 'test' } }, }; const response = await upstream.send(request); expect(response.result).toEqual({ content: [{ type: 'text', text: 'hello' }] }); expect(client.post).toHaveBeenCalledWith('/api/v1/mcp/proxy', { serverId: 'srv-1', method: 'tools/call', params: { name: 'search', arguments: { query: 'test' } }, }); }); it('sends tools/list via mcpd proxy', async () => { const client = mockMcpdClient(new Map([ ['srv-1:tools/list', { result: { tools: [{ name: 'search', description: 'Search' }] } }], ])); const upstream = new McpdUpstream('srv-1', 'slack', client as any); const request: JsonRpcRequest = { jsonrpc: '2.0', id: '2', method: 'tools/list', }; const response = await upstream.send(request); expect(response.result).toEqual({ tools: [{ name: 'search', description: 'Search' }] }); }); it('returns error when mcpd fails', async () => { const client = mockMcpdClient(); client.post.mockRejectedValue(new Error('connection refused')); const upstream = new McpdUpstream('srv-1', 'slack', client as any); const request: JsonRpcRequest = { jsonrpc: '2.0', id: '3', method: 'tools/list' }; const response = await upstream.send(request); expect(response.error).toBeDefined(); expect(response.error!.message).toContain('mcpd proxy error'); }); it('returns error when upstream is closed', async () => { const client = mockMcpdClient(); const upstream = new McpdUpstream('srv-1', 'slack', client as any); await upstream.close(); const request: JsonRpcRequest = { jsonrpc: '2.0', id: '4', method: 'tools/list' }; const response = await upstream.send(request); expect(response.error).toBeDefined(); expect(response.error!.message).toContain('closed'); }); it('reports alive status correctly', async () => { const client = mockMcpdClient(); const upstream = new McpdUpstream('srv-1', 'slack', client as any); expect(upstream.isAlive()).toBe(true); await upstream.close(); expect(upstream.isAlive()).toBe(false); }); it('relays error responses from mcpd', async () => { const client = mockMcpdClient(new Map([ ['srv-1:tools/call', { error: { code: -32601, message: 'Tool not found' } }], ])); const upstream = new McpdUpstream('srv-1', 'slack', client as any); const request: JsonRpcRequest = { jsonrpc: '2.0', id: '5', method: 'tools/call', params: { name: 'nonexistent' }, }; const response = await upstream.send(request); expect(response.error).toEqual({ code: -32601, message: 'Tool not found' }); }); it('routes */list methods through discoveryClient when provided', async () => { const mainClient = mockMcpdClient(); const discoveryClient = mockMcpdClient(new Map([ ['srv-1:tools/list', { result: { tools: [] } }], ['srv-1:resources/list', { result: { resources: [] } }], ['srv-1:prompts/list', { result: { prompts: [] } }], ])); const upstream = new McpdUpstream('srv-1', 'slack', mainClient as any, undefined, discoveryClient as any); await upstream.send({ jsonrpc: '2.0', id: '1', method: 'tools/list' }); await upstream.send({ jsonrpc: '2.0', id: '2', method: 'resources/list' }); await upstream.send({ jsonrpc: '2.0', id: '3', method: 'prompts/list' }); expect(discoveryClient.post).toHaveBeenCalledTimes(3); expect(mainClient.post).not.toHaveBeenCalled(); }); it('routes tools/call through mainClient even when discoveryClient is set', async () => { const mainClient = mockMcpdClient(new Map([ ['srv-1:tools/call', { result: { ok: true } }], ])); const discoveryClient = mockMcpdClient(); const upstream = new McpdUpstream('srv-1', 'slack', mainClient as any, undefined, discoveryClient as any); await upstream.send({ jsonrpc: '2.0', id: '1', method: 'tools/call', params: { name: 'noop', arguments: {} }, }); expect(mainClient.post).toHaveBeenCalledTimes(1); expect(discoveryClient.post).not.toHaveBeenCalled(); }); });