fix: cover serve daemon edge cases

This commit is contained in:
zm2231 2026-05-13 20:32:33 -04:00
parent 6879a69f49
commit bfe727150c
3 changed files with 83 additions and 0 deletions

View File

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

View File

@ -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', () => {

View File

@ -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([