Files
mcpctl/src/mcplocal/tests/discovery.test.ts
Michal 3149ea3ae7
Some checks failed
CI/CD / lint (push) Successful in 52s
CI/CD / typecheck (push) Successful in 1m51s
CI/CD / test (push) Successful in 1m1s
CI/CD / smoke (push) Failing after 3m21s
CI/CD / build (push) Successful in 4m9s
CI/CD / publish (push) Has been skipped
fix: MCP proxy resilience — discovery cache, default liveness probes
Adds a per-server tools/list cache in McpRouter (positive + negative TTL)
so a slow or dead upstream only stalls the first discovery call, not every
subsequent client request. Invalidated on upstream add/remove.

Health probes now apply a default liveness spec (tools/list via the real
production path) to any RUNNING instance without an explicit healthCheck,
so synthetic and real failures converge on the same signal.

Includes supporting updates in mcpd-client, discovery, upstream/mcpd,
seeder, and fulldeploy/release scripts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 00:48:57 +01:00

72 lines
2.4 KiB
TypeScript

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 }>) {
const client = {
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(),
withTimeout: vi.fn(() => client),
withHeaders: vi.fn(() => client),
};
return client;
}
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);
});
});