perf(daemon): skip warm status preflight

This commit is contained in:
Peter Steinberger 2026-05-09 08:19:31 +01:00
parent 45b881d4ea
commit 761c11cb3b
No known key found for this signature in database
4 changed files with 67 additions and 10 deletions

View File

@ -5,6 +5,7 @@
### CLI
- Increase the default OAuth browser wait from 60 seconds to 5 minutes so hosted MCP sign-ins have enough time for account and permission review.
- Skip the redundant daemon `status` preflight for warm keep-alive access, cutting one socket round-trip from each routed list/call/resource request while preserving stale-config and dead-daemon recovery.
### Config

View File

@ -41,6 +41,8 @@ interface DaemonMetadata {
readonly logPath?: string | null;
}
type DaemonConfigState = 'missing' | 'fresh' | 'stale';
export function resolveDaemonPaths(configPath: string): DaemonPaths {
const key = deriveConfigKey(configPath);
return {
@ -117,13 +119,13 @@ export class DaemonClient {
}
private async ensureDaemon(): Promise<void> {
if (await this.isConfigStale()) {
const configState = await this.checkConfigState();
if (configState === 'stale') {
await this.stop().catch(() => {});
await this.restartDaemon();
return;
}
const available = await this.isResponsive();
if (available) {
if (configState === 'fresh') {
return;
}
await this.startDaemon();
@ -179,26 +181,26 @@ export class DaemonClient {
}
}
private async isConfigStale(): Promise<boolean> {
private async checkConfigState(): Promise<DaemonConfigState> {
const metadata = await readDaemonMetadata(this.metadataPath);
if (!metadata) {
return false;
return 'missing';
}
const currentLayers = normalizeLayers(await collectConfigLayers(this.options));
const metadataLayers = normalizeLayers(
metadata.configLayers ?? [{ path: metadata.configPath, mtimeMs: metadata.configMtimeMs ?? null }]
);
if (currentLayers.length !== metadataLayers.length) {
return true;
return 'stale';
}
for (let i = 0; i < currentLayers.length; i += 1) {
const current = currentLayers[i];
const previous = metadataLayers[i];
if (!current || !previous || current.path !== previous.path || current.mtimeMs !== previous.mtimeMs) {
return true;
return 'stale';
}
}
return false;
return 'fresh';
}
private async sendRequest<T>(method: DaemonRequestMethod, params: unknown, timeoutOverrideMs?: number): Promise<T> {

View File

@ -214,8 +214,7 @@ describe('DaemonClient config freshness', () => {
const client = new DaemonClient({ configPath, configExplicit: true, rootDir: tmpDir });
await client.listTools({ server: 'playwright' });
expect(sentMethods[0]).toBe('status');
expect(sentMethods).toContain('listTools');
expect(sentMethods).toEqual(['listTools']);
expect(sentMethods).not.toContain('stop');
expect(launchDaemonDetached).not.toHaveBeenCalled();
});

View File

@ -89,4 +89,59 @@ describe('daemon client', () => {
}
}
});
it('skips status preflight when daemon metadata is fresh', async () => {
const tmpDir = await makeShortTempDir('mcpd-fresh');
const originalDir = process.env.MCPORTER_DAEMON_DIR;
process.env.MCPORTER_DAEMON_DIR = tmpDir;
const configPath = path.join(tmpDir, 'config.json');
await fs.writeFile(
configPath,
JSON.stringify({ mcpServers: { warm: { command: 'node', args: ['server.js'], lifecycle: 'keep-alive' } } })
);
const { socketPath, metadataPath } = resolveDaemonPaths(configPath);
await fs.mkdir(path.dirname(socketPath), { recursive: true });
const configStats = await fs.stat(configPath);
await fs.writeFile(
metadataPath,
JSON.stringify({
pid: process.pid,
socketPath,
configPath,
configLayers: [{ path: configPath, mtimeMs: configStats.mtimeMs }],
startedAt: Date.now(),
})
);
const methods: string[] = [];
const server = net.createServer((socket) => {
let buffer = '';
socket.setEncoding('utf8');
socket.on('data', (chunk) => {
buffer += chunk;
const request = JSON.parse(buffer) as { id: string; method: string };
methods.push(request.method);
socket.end(JSON.stringify({ id: request.id, ok: true, result: { tools: [] } }));
});
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(socketPath, () => {
server.off('error', reject);
resolve();
});
});
try {
const client = new DaemonClient({ configPath, configExplicit: true });
await client.listTools({ server: 'warm' });
expect(methods).toEqual(['listTools']);
} finally {
await new Promise<void>((resolve) => server.close(() => resolve()));
await fs.unlink(socketPath).catch(() => {});
if (originalDir) {
process.env.MCPORTER_DAEMON_DIR = originalDir;
} else {
delete process.env.MCPORTER_DAEMON_DIR;
}
}
});
});