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' }); }); });