Compare commits

...

3 Commits

Author SHA1 Message Date
Peter Steinberger
b554ab399b
fix: dedupe keep-alive daemon restarts (#125) (thanks @zm2231) 2026-03-28 21:04:56 +00:00
zm2231
b691380914
test: cover cli managed runtime wiring 2026-03-28 21:01:04 +00:00
zm2231
d78a7954cb
fix: add managed runtime and dedupe keep-alive restarts 2026-03-28 21:01:04 +00:00
3 changed files with 53 additions and 1 deletions

View File

@ -3,6 +3,7 @@
## [Unreleased]
### CLI
- Deduplicate concurrent keep-alive daemon restarts per server so repeated fatal errors only force-close the cached daemon transport once before retrying. (PR #125, thanks @zm2231)
- Keep `mcporter call --output json` parseable by emitting valid JSON even when the command falls back to raw output. (PR #128, thanks @armanddp)
- Ignore static `Authorization` headers once OAuth is active so imported editor configs cannot override fresh OAuth tokens. (PR #123, thanks @ahonn)
- Preserve full JSON/error payloads when `data` is just one field instead of collapsing the response to `data` alone. (PR #106, thanks @AielloChan)

View File

@ -18,6 +18,8 @@ export function createKeepAliveRuntime(base: Runtime, options: KeepAliveRuntimeO
}
class KeepAliveRuntime implements Runtime {
private readonly restartPromises = new Map<string, Promise<void>>();
constructor(
private readonly base: Runtime,
private readonly daemon: DaemonClient,
@ -111,10 +113,26 @@ class KeepAliveRuntime implements Runtime {
// The daemon keeps STDIO transports warm; if a call fails due to a fatal error,
// force-close the cached server so the retry launches a fresh Chrome instance.
logDaemonRetry(server, operation, error);
await this.daemon.closeServer({ server }).catch(() => {});
await this.restartServer(server);
return action();
}
}
private async restartServer(server: string): Promise<void> {
const existing = this.restartPromises.get(server);
if (existing) {
await existing;
return;
}
const restart = this.daemon.closeServer({ server }).catch(() => {});
this.restartPromises.set(server, restart);
try {
await restart;
} finally {
this.restartPromises.delete(server);
}
}
}
const NON_FATAL_CODES = new Set([ErrorCode.InvalidRequest, ErrorCode.MethodNotFound, ErrorCode.InvalidParams]);

View File

@ -131,6 +131,39 @@ describe('createKeepAliveRuntime', () => {
logSpy.mockRestore();
});
it('deduplicates concurrent restarts for the same server', async () => {
const runtime = new FakeRuntime(definitions);
let releaseClose!: () => void;
const closePromise = new Promise<void>((resolve) => {
releaseClose = resolve;
});
const daemon = {
callTool: vi
.fn()
.mockRejectedValueOnce(new Error('transport hung up'))
.mockRejectedValueOnce(new Error('transport hung up'))
.mockResolvedValue('daemon-call'),
closeServer: vi.fn().mockImplementation(async () => {
await closePromise;
}),
listTools: vi.fn(),
listResources: vi.fn(),
};
const keepAliveRuntime = createKeepAliveRuntime(runtime as unknown as Runtime, {
daemonClient: daemon as never,
keepAliveServers: new Set(['alpha']),
});
const first = keepAliveRuntime.callTool('alpha', 'ping', {});
const second = keepAliveRuntime.callTool('alpha', 'pong', {});
await Promise.resolve();
expect(daemon.closeServer).toHaveBeenCalledTimes(1);
releaseClose();
await expect(Promise.all([first, second])).resolves.toEqual(['daemon-call', 'daemon-call']);
expect(daemon.closeServer).toHaveBeenCalledTimes(1);
});
it('does not restart daemon servers for InvalidParams errors', async () => {
const runtime = new FakeRuntime(definitions);
const error = new McpError(ErrorCode.InvalidParams, 'Tool not found');