perf(daemon): skip warm status preflight
This commit is contained in:
parent
45b881d4ea
commit
761c11cb3b
@ -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
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user