From bda7febf19a09c0c0e88257c0de3c5c5b2233d98 Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch Date: Thu, 9 Jul 2026 20:58:41 -0400 Subject: [PATCH 1/3] fix(mcp): do not await startup lifecycle snapshot on critical path getSnapshot() enumerates simulator os_log sessions and peer processes. Awaiting it after stdio transport start delayed the first tools/list by ~10-17s, so short health-probe clients reported tools fetch failed. Fire-and-forget the snapshot with the same metrics/anomaly logging; deferred bootstrap already uses this pattern. Fixes #461 --- src/server/start-mcp-server.ts | 64 ++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/src/server/start-mcp-server.ts b/src/server/start-mcp-server.ts index 795a5000..aad86b31 100644 --- a/src/server/start-mcp-server.ts +++ b/src/server/start-mcp-server.ts @@ -169,30 +169,48 @@ export async function startMcpServer(): Promise { lifecycle.markPhase('running'); idleShutdown.start(); - const startupSnapshot = await lifecycle.getSnapshot(); - log('info', `[mcp-lifecycle] start ${JSON.stringify(startupSnapshot)}`); - recordMcpLifecycleMetric({ - event: 'start', - phase: startupSnapshot.phase, - uptimeMs: startupSnapshot.uptimeMs, - rssBytes: startupSnapshot.rssBytes, - matchingMcpProcessCount: startupSnapshot.matchingMcpProcessCount, - activeOperationCount: startupSnapshot.activeOperationCount, - watcherRunning: startupSnapshot.watcherRunning, - }); - for (const anomaly of startupSnapshot.anomalies) { - recordMcpLifecycleAnomalyMetric({ - kind: anomaly, - phase: startupSnapshot.phase, + + // Startup snapshot (simulator os_log sessions, peer process sample, etc.) is + // telemetry-only. Awaiting it blocked the first tools/list by ~10–17s on cold + // start, which makes short health-probe clients report "tools fetch failed" + // even though the transport is already up. Keep the same logging/metrics, but + // do not block readiness on the snapshot. See #461. + void lifecycle + .getSnapshot() + .then((startupSnapshot) => { + if (lifecycle.isShutdownRequested()) { + return; + } + log('info', `[mcp-lifecycle] start ${JSON.stringify(startupSnapshot)}`); + recordMcpLifecycleMetric({ + event: 'start', + phase: startupSnapshot.phase, + uptimeMs: startupSnapshot.uptimeMs, + rssBytes: startupSnapshot.rssBytes, + matchingMcpProcessCount: startupSnapshot.matchingMcpProcessCount, + activeOperationCount: startupSnapshot.activeOperationCount, + watcherRunning: startupSnapshot.watcherRunning, + }); + for (const anomaly of startupSnapshot.anomalies) { + recordMcpLifecycleAnomalyMetric({ + kind: anomaly, + phase: startupSnapshot.phase, + }); + } + if (startupSnapshot.anomalies.length > 0) { + log( + 'warn', + `[mcp-lifecycle] startup anomalies observed: ${startupSnapshot.anomalies.join(', ')}`, + { sentry: true }, + ); + } + }) + .catch((error) => { + log( + 'warn', + `Startup lifecycle snapshot failed: ${error instanceof Error ? error.message : String(error)}`, + ); }); - } - if (startupSnapshot.anomalies.length > 0) { - log( - 'warn', - `[mcp-lifecycle] startup anomalies observed: ${startupSnapshot.anomalies.join(', ')}`, - { sentry: true }, - ); - } lifecycle.markPhase('deferred-initialization'); void bootstrap From 1e136a3e15baa2e95464a8cd84df5d1daa700196 Mon Sep 17 00:00:00 2001 From: Pitchfork-and-Torch Date: Thu, 9 Jul 2026 21:11:39 -0400 Subject: [PATCH 2/3] fix(mcp): pin startup snapshot metrics to schedule-time phase When getSnapshot is fire-and-forget, markPhase may advance to deferred-initialization before the snapshot resolves. Capture the running phase at schedule time for start metrics/anomaly attribution. Addresses review feedback on #468 / #461. --- src/server/start-mcp-server.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/server/start-mcp-server.ts b/src/server/start-mcp-server.ts index aad86b31..97839a10 100644 --- a/src/server/start-mcp-server.ts +++ b/src/server/start-mcp-server.ts @@ -170,6 +170,11 @@ export async function startMcpServer(): Promise { lifecycle.markPhase('running'); idleShutdown.start(); + // Capture phase at schedule time so deferred snapshot metrics are not + // attributed to a later phase (e.g. deferred-initialization) if markPhase + // advances before getSnapshot resolves. See #461 / review note. + const startupMetricPhase = 'running' as const; + // Startup snapshot (simulator os_log sessions, peer process sample, etc.) is // telemetry-only. Awaiting it blocked the first tools/list by ~10–17s on cold // start, which makes short health-probe clients report "tools fetch failed" @@ -181,10 +186,13 @@ export async function startMcpServer(): Promise { if (lifecycle.isShutdownRequested()) { return; } - log('info', `[mcp-lifecycle] start ${JSON.stringify(startupSnapshot)}`); + // Prefer the phase at snapshot-schedule time for metrics; still log the + // full snapshot object as returned for operational debugging. + const metricPhase = startupMetricPhase; + log('info', `[mcp-lifecycle] start ${JSON.stringify({ ...startupSnapshot, phase: metricPhase })}`); recordMcpLifecycleMetric({ event: 'start', - phase: startupSnapshot.phase, + phase: metricPhase, uptimeMs: startupSnapshot.uptimeMs, rssBytes: startupSnapshot.rssBytes, matchingMcpProcessCount: startupSnapshot.matchingMcpProcessCount, @@ -194,7 +202,7 @@ export async function startMcpServer(): Promise { for (const anomaly of startupSnapshot.anomalies) { recordMcpLifecycleAnomalyMetric({ kind: anomaly, - phase: startupSnapshot.phase, + phase: metricPhase, }); } if (startupSnapshot.anomalies.length > 0) { From 7fe6b20601e5444fbf1c675c4dac170facc0497b Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sun, 12 Jul 2026 19:15:48 +0100 Subject: [PATCH 3/3] style: Format MCP lifecycle log call --- src/server/start-mcp-server.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/server/start-mcp-server.ts b/src/server/start-mcp-server.ts index 97839a10..a925c246 100644 --- a/src/server/start-mcp-server.ts +++ b/src/server/start-mcp-server.ts @@ -189,7 +189,10 @@ export async function startMcpServer(): Promise { // Prefer the phase at snapshot-schedule time for metrics; still log the // full snapshot object as returned for operational debugging. const metricPhase = startupMetricPhase; - log('info', `[mcp-lifecycle] start ${JSON.stringify({ ...startupSnapshot, phase: metricPhase })}`); + log( + 'info', + `[mcp-lifecycle] start ${JSON.stringify({ ...startupSnapshot, phase: metricPhase })}`, + ); recordMcpLifecycleMetric({ event: 'start', phase: metricPhase,