From bfe727150c9479953da29f7166370e849bde3e5f Mon Sep 17 00:00:00 2001 From: zm2231 <25645999+zm2231@users.noreply.github.com> Date: Wed, 13 May 2026 20:32:33 -0400 Subject: [PATCH] fix: cover serve daemon edge cases --- src/cli/serve-command.ts | 3 ++ tests/cli-serve-command.test.ts | 1 + tests/serve.test.ts | 79 +++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) diff --git a/src/cli/serve-command.ts b/src/cli/serve-command.ts index 4014dce..a028ccb 100644 --- a/src/cli/serve-command.ts +++ b/src/cli/serve-command.ts @@ -175,6 +175,9 @@ export function parseServeArgs(args: string[]): ParsedServeArgs { } function parsePort(value: string): number { + if (value.trim().length === 0) { + throw new Error("Flag '--http' requires a port."); + } const port = Number(value); if (!Number.isInteger(port) || port < 0 || port > 65_535) { throw new Error(`Invalid HTTP port '${value}'.`); diff --git a/tests/cli-serve-command.test.ts b/tests/cli-serve-command.test.ts index 030cc8a..4db2d2f 100644 --- a/tests/cli-serve-command.test.ts +++ b/tests/cli-serve-command.test.ts @@ -31,6 +31,7 @@ describe('serve command arguments', () => { it('rejects invalid ports', () => { expect(() => parseServeArgs(['--http', 'nope'])).toThrow("Invalid HTTP port 'nope'"); + expect(() => parseServeArgs(['--http='])).toThrow("Flag '--http' requires a port."); }); it('rejects conflicting stdio and HTTP modes', () => { diff --git a/tests/serve.test.ts b/tests/serve.test.ts index c90513e..0a2d881 100644 --- a/tests/serve.test.ts +++ b/tests/serve.test.ts @@ -3,6 +3,8 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'; import { describe, expect, it, vi } from 'vitest'; import type { ServerDefinition } from '../src/config.js'; +import { createKeepAliveRuntime } from '../src/daemon/runtime-wrapper.js'; +import type { Runtime } from '../src/runtime.js'; import { createBridgeServer, decodeToolName, encodeToolName, selectServedServers, serveHttp } from '../src/serve.js'; const definitions: ServerDefinition[] = [ @@ -93,6 +95,83 @@ describe('mcporter serve bridge', () => { await bridge.close(); }); + it('routes bridged keep-alive tool traffic through the daemon runtime wrapper', async () => { + const baseRuntime = { + listServers: () => definitions.map((definition) => definition.name), + getDefinitions: () => definitions, + getDefinition: (server: string) => { + const definition = definitions.find((entry) => entry.name === server); + if (!definition) { + throw new Error(`Unknown server ${server}`); + } + return definition; + }, + registerDefinition: vi.fn(), + listTools: vi.fn().mockResolvedValue([{ name: 'local-tool' }]), + callTool: vi.fn().mockResolvedValue({ content: [{ type: 'text', text: 'local' }] }), + listResources: vi.fn(), + readResource: vi.fn(), + connect: vi.fn(), + close: vi.fn().mockResolvedValue(undefined), + } satisfies Runtime; + const daemon = { + listTools: vi.fn().mockResolvedValue([ + { + name: 'ping', + description: 'daemon ping', + inputSchema: { type: 'object' }, + }, + ]), + callTool: vi + .fn() + .mockRejectedValueOnce(new Error('daemon transport died')) + .mockResolvedValueOnce({ + content: [{ type: 'text', text: 'daemon pong' }], + }), + listResources: vi.fn(), + readResource: vi.fn(), + closeServer: vi.fn().mockResolvedValue(undefined), + }; + const runtime = createKeepAliveRuntime(baseRuntime, { + daemonClient: daemon as never, + keepAliveServers: new Set(['alpha']), + }); + const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const bridge = createBridgeServer({ + runtime, + definitions, + servers: ['alpha'], + }); + const client = new Client({ name: 'daemon-wrapper-client', version: '1.0.0' }); + const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + + try { + await Promise.all([bridge.connect(serverTransport), client.connect(clientTransport)]); + + await expect(client.listTools()).resolves.toMatchObject({ + tools: [{ name: 'alpha__ping', description: '[alpha] daemon ping' }], + }); + expect(daemon.listTools).toHaveBeenCalledWith({ + server: 'alpha', + includeSchema: true, + autoAuthorize: true, + }); + expect(baseRuntime.listTools).not.toHaveBeenCalled(); + + await expect(client.callTool({ name: 'alpha__ping', arguments: {} })).resolves.toEqual({ + content: [{ type: 'text', text: 'daemon pong' }], + }); + expect(daemon.callTool).toHaveBeenCalledTimes(2); + expect(daemon.closeServer).toHaveBeenCalledWith({ server: 'alpha' }); + expect(baseRuntime.callTool).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining("Restarting 'alpha'")); + } finally { + errorSpy.mockRestore(); + await client.close().catch(() => {}); + await bridge.close().catch(() => {}); + } + }); + it('serves the bridge over Streamable HTTP', async () => { const runtime = { listTools: vi.fn().mockResolvedValue([