From d73e7b8447bdde0232f4adc8066d7ac6a44eb49f Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sat, 7 Mar 2026 11:27:29 +0000 Subject: [PATCH 1/2] Bump AXe version to 1.5.2 --- .axe-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.axe-version b/.axe-version index 88c5fb89..4cda8f19 100644 --- a/.axe-version +++ b/.axe-version @@ -1 +1 @@ -1.4.0 +1.5.2 From 708297c3bc57f06e6e1bff7dbb653ba52caa6639 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sat, 7 Mar 2026 20:48:04 +0000 Subject: [PATCH 2/2] fix(mcp): Preserve Sentry telemetry until server shutdown Skip the generic CLI Sentry flush when running in MCP mode so the\nserver does not close telemetry immediately after startup.\n\nHandle stdin end and close during MCP shutdown so the server can\nfinish exporting spans before exiting. --- src/cli.ts | 4 ++++ src/server/start-mcp-server.ts | 25 +++++++++++++++++++++++-- src/utils/sentry.ts | 9 --------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index bf62844f..9791e87a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -153,6 +153,10 @@ async function main(): Promise { main() .then(async () => { + if (findTopLevelCommand(process.argv.slice(2)) === 'mcp') { + return; + } + await flushAndCloseSentry(2000); }) .catch(async (err) => { diff --git a/src/server/start-mcp-server.ts b/src/server/start-mcp-server.ts index dafb7af6..88ebf67f 100644 --- a/src/server/start-mcp-server.ts +++ b/src/server/start-mcp-server.ts @@ -79,15 +79,28 @@ export async function startMcpServer(): Promise { enrichSentryContext(); }); + type ShutdownReason = NodeJS.Signals | 'stdin-end' | 'stdin-close'; + let shuttingDown = false; - const shutdown = async (signal: NodeJS.Signals): Promise => { + const shutdown = async (reason: ShutdownReason): Promise => { if (shuttingDown) return; shuttingDown = true; - log('info', `Received ${signal}; shutting down MCP server`); + if (reason === 'stdin-end') { + log('info', 'MCP stdin ended; shutting down MCP server'); + } else if (reason === 'stdin-close') { + log('info', 'MCP stdin closed; shutting down MCP server'); + } else { + log('info', `Received ${reason}; shutting down MCP server`); + } let exitCode = 0; + if (reason === 'stdin-end' || reason === 'stdin-close') { + // Allow span completion/export to settle after the client closes stdin. + await new Promise((resolve) => setTimeout(resolve, 250)); + } + try { await shutdownXcodeToolsBridge(); } catch (error) { @@ -121,6 +134,14 @@ export async function startMcpServer(): Promise { void shutdown('SIGINT'); }); + process.stdin.once('end', () => { + void shutdown('stdin-end'); + }); + + process.stdin.once('close', () => { + void shutdown('stdin-close'); + }); + log('info', `XcodeBuildMCP server (version ${version}) started successfully`); } catch (error) { log('error', `Fatal error in startMcpServer(): ${String(error)}`, { sentry: true }); diff --git a/src/utils/sentry.ts b/src/utils/sentry.ts index 4a9e48c4..d8783675 100644 --- a/src/utils/sentry.ts +++ b/src/utils/sentry.ts @@ -4,10 +4,8 @@ * This file initializes Sentry when explicitly called to avoid side effects * during module import. */ - import * as Sentry from '@sentry/node'; import { version } from '../version.ts'; - const USER_HOME_PATH_PATTERN = /\/Users\/[^/\s]+/g; const XCODE_VERSION_PATTERN = /^Xcode\s+(.+)$/m; const XCODE_BUILD_PATTERN = /^Build version\s+(.+)$/m; @@ -43,11 +41,9 @@ export interface SentryRuntimeContext { function redactPathLikeData(value: string): string { return value.replace(USER_HOME_PATH_PATTERN, '/Users/'); } - function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } - function redactUnknown(value: unknown): unknown { if (typeof value === 'string') { return redactPathLikeData(value); @@ -153,7 +149,6 @@ let initialized = false; let enriched = false; let selfTestEmitted = false; let pendingRuntimeContext: SentryRuntimeContext | null = null; - function isSentryDisabled(): boolean { return ( process.env.XCODEBUILDMCP_SENTRY_DISABLED === 'true' || process.env.SENTRY_DISABLED === 'true' @@ -168,7 +163,6 @@ function isSentrySelfTestEnabled(): boolean { const raw = process.env[SENTRY_SELF_TEST_ENV_VAR]?.trim().toLowerCase(); return raw === '1' || raw === 'true' || raw === 'yes'; } - function emitSentrySelfTest(mode: SentryRuntimeMode | undefined): void { if (!isSentrySelfTestEnabled() || selfTestEmitted) { return; @@ -202,7 +196,6 @@ function boolToTag(value: boolean | undefined): string | undefined { } return String(value); } - function setTagIfDefined(key: string, value: string | undefined): void { if (!value) { return; @@ -387,7 +380,6 @@ interface InternalErrorMetric { } type DaemonGaugeMetricName = 'inflight_requests' | 'active_sessions' | 'idle_timeout_ms'; - function sanitizeTagValue(value: string): string { const trimmed = value.trim().toLowerCase(); if (!trimmed) { @@ -399,7 +391,6 @@ function sanitizeTagValue(value: string): string { function shouldEmitMetrics(): boolean { return initialized && !isSentryDisabled() && !isTestEnv(); } - export function recordToolInvocationMetric(metric: ToolInvocationMetric): void { if (!shouldEmitMetrics()) { return;