feat: implement v2 3-tier architecture (mcpctl → mcplocal → mcpd)
- Rename local-proxy to mcplocal with HTTP server, LLM pipeline, mcpd discovery
- Add LLM pre-processing: token estimation, filter cache, metrics, Gemini CLI + DeepSeek providers
- Add mcpd auth (login/logout) and MCP proxy endpoints
- Update CLI: dual URLs (mcplocalUrl/mcpdUrl), auth commands, --direct flag
- Add tiered health monitoring, shell completions, e2e integration tests
- 57 test files, 597 tests passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 11:42:06 +00:00
|
|
|
import { describe, it, expect, vi } from 'vitest';
|
|
|
|
|
import { refreshUpstreams } from '../src/discovery.js';
|
|
|
|
|
import { McpRouter } from '../src/router.js';
|
|
|
|
|
|
|
|
|
|
function mockMcpdClient(servers: Array<{ id: string; name: string; transport: string }>) {
|
2026-04-17 00:48:57 +01:00
|
|
|
const client = {
|
feat: implement v2 3-tier architecture (mcpctl → mcplocal → mcpd)
- Rename local-proxy to mcplocal with HTTP server, LLM pipeline, mcpd discovery
- Add LLM pre-processing: token estimation, filter cache, metrics, Gemini CLI + DeepSeek providers
- Add mcpd auth (login/logout) and MCP proxy endpoints
- Update CLI: dual URLs (mcplocalUrl/mcpdUrl), auth commands, --direct flag
- Add tiered health monitoring, shell completions, e2e integration tests
- 57 test files, 597 tests passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 11:42:06 +00:00
|
|
|
baseUrl: 'http://test:3100',
|
|
|
|
|
token: 'test-token',
|
|
|
|
|
get: vi.fn(async () => servers),
|
|
|
|
|
post: vi.fn(async () => ({ result: {} })),
|
|
|
|
|
put: vi.fn(),
|
|
|
|
|
delete: vi.fn(),
|
|
|
|
|
forward: vi.fn(),
|
2026-04-17 00:48:57 +01:00
|
|
|
withTimeout: vi.fn(() => client),
|
|
|
|
|
withHeaders: vi.fn(() => client),
|
feat: implement v2 3-tier architecture (mcpctl → mcplocal → mcpd)
- Rename local-proxy to mcplocal with HTTP server, LLM pipeline, mcpd discovery
- Add LLM pre-processing: token estimation, filter cache, metrics, Gemini CLI + DeepSeek providers
- Add mcpd auth (login/logout) and MCP proxy endpoints
- Update CLI: dual URLs (mcplocalUrl/mcpdUrl), auth commands, --direct flag
- Add tiered health monitoring, shell completions, e2e integration tests
- 57 test files, 597 tests passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 11:42:06 +00:00
|
|
|
};
|
2026-04-17 00:48:57 +01:00
|
|
|
return client;
|
feat: implement v2 3-tier architecture (mcpctl → mcplocal → mcpd)
- Rename local-proxy to mcplocal with HTTP server, LLM pipeline, mcpd discovery
- Add LLM pre-processing: token estimation, filter cache, metrics, Gemini CLI + DeepSeek providers
- Add mcpd auth (login/logout) and MCP proxy endpoints
- Update CLI: dual URLs (mcplocalUrl/mcpdUrl), auth commands, --direct flag
- Add tiered health monitoring, shell completions, e2e integration tests
- 57 test files, 597 tests passing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 11:42:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe('refreshUpstreams', () => {
|
|
|
|
|
it('registers mcpd servers as upstreams', async () => {
|
|
|
|
|
const router = new McpRouter();
|
|
|
|
|
const client = mockMcpdClient([
|
|
|
|
|
{ id: 'srv-1', name: 'slack', transport: 'stdio' },
|
|
|
|
|
{ id: 'srv-2', name: 'github', transport: 'stdio' },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const registered = await refreshUpstreams(router, client as any);
|
|
|
|
|
expect(registered).toEqual(['slack', 'github']);
|
|
|
|
|
expect(router.getUpstreamNames()).toContain('slack');
|
|
|
|
|
expect(router.getUpstreamNames()).toContain('github');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('removes stale upstreams', async () => {
|
|
|
|
|
const router = new McpRouter();
|
|
|
|
|
// First refresh: 2 servers
|
|
|
|
|
const client1 = mockMcpdClient([
|
|
|
|
|
{ id: 'srv-1', name: 'slack', transport: 'stdio' },
|
|
|
|
|
{ id: 'srv-2', name: 'github', transport: 'stdio' },
|
|
|
|
|
]);
|
|
|
|
|
await refreshUpstreams(router, client1 as any);
|
|
|
|
|
expect(router.getUpstreamNames()).toHaveLength(2);
|
|
|
|
|
|
|
|
|
|
// Second refresh: only 1 server
|
|
|
|
|
const client2 = mockMcpdClient([
|
|
|
|
|
{ id: 'srv-1', name: 'slack', transport: 'stdio' },
|
|
|
|
|
]);
|
|
|
|
|
await refreshUpstreams(router, client2 as any);
|
|
|
|
|
expect(router.getUpstreamNames()).toEqual(['slack']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('does not duplicate existing upstreams', async () => {
|
|
|
|
|
const router = new McpRouter();
|
|
|
|
|
const client = mockMcpdClient([
|
|
|
|
|
{ id: 'srv-1', name: 'slack', transport: 'stdio' },
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
await refreshUpstreams(router, client as any);
|
|
|
|
|
await refreshUpstreams(router, client as any);
|
|
|
|
|
expect(router.getUpstreamNames()).toEqual(['slack']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('handles empty server list', async () => {
|
|
|
|
|
const router = new McpRouter();
|
|
|
|
|
const client = mockMcpdClient([]);
|
|
|
|
|
|
|
|
|
|
const registered = await refreshUpstreams(router, client as any);
|
|
|
|
|
expect(registered).toEqual([]);
|
|
|
|
|
expect(router.getUpstreamNames()).toHaveLength(0);
|
|
|
|
|
});
|
|
|
|
|
});
|