Skip to content

Commit d53f991

Browse files
committed
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.
1 parent 367bec1 commit d53f991

3 files changed

Lines changed: 27 additions & 11 deletions

File tree

src/cli.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ async function main(): Promise<void> {
153153

154154
main()
155155
.then(async () => {
156+
if (findTopLevelCommand(process.argv.slice(2)) === 'mcp') {
157+
return;
158+
}
159+
156160
await flushAndCloseSentry(2000);
157161
})
158162
.catch(async (err) => {

src/server/start-mcp-server.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,15 +79,28 @@ export async function startMcpServer(): Promise<void> {
7979
enrichSentryContext();
8080
});
8181

82+
type ShutdownReason = NodeJS.Signals | 'stdin-end' | 'stdin-close';
83+
8284
let shuttingDown = false;
83-
const shutdown = async (signal: NodeJS.Signals): Promise<void> => {
85+
const shutdown = async (reason: ShutdownReason): Promise<void> => {
8486
if (shuttingDown) return;
8587
shuttingDown = true;
8688

87-
log('info', `Received ${signal}; shutting down MCP server`);
89+
if (reason === 'stdin-end') {
90+
log('info', 'MCP stdin ended; shutting down MCP server');
91+
} else if (reason === 'stdin-close') {
92+
log('info', 'MCP stdin closed; shutting down MCP server');
93+
} else {
94+
log('info', `Received ${reason}; shutting down MCP server`);
95+
}
8896

8997
let exitCode = 0;
9098

99+
if (reason === 'stdin-end' || reason === 'stdin-close') {
100+
// Allow span completion/export to settle after the client closes stdin.
101+
await new Promise((resolve) => setTimeout(resolve, 250));
102+
}
103+
91104
try {
92105
await shutdownXcodeToolsBridge();
93106
} catch (error) {
@@ -121,6 +134,14 @@ export async function startMcpServer(): Promise<void> {
121134
void shutdown('SIGINT');
122135
});
123136

137+
process.stdin.once('end', () => {
138+
void shutdown('stdin-end');
139+
});
140+
141+
process.stdin.once('close', () => {
142+
void shutdown('stdin-close');
143+
});
144+
124145
log('info', `XcodeBuildMCP server (version ${version}) started successfully`);
125146
} catch (error) {
126147
log('error', `Fatal error in startMcpServer(): ${String(error)}`, { sentry: true });

src/utils/sentry.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
* This file initializes Sentry when explicitly called to avoid side effects
55
* during module import.
66
*/
7-
87
import * as Sentry from '@sentry/node';
98
import { version } from '../version.ts';
10-
119
const USER_HOME_PATH_PATTERN = /\/Users\/[^/\s]+/g;
1210
const XCODE_VERSION_PATTERN = /^Xcode\s+(.+)$/m;
1311
const XCODE_BUILD_PATTERN = /^Build version\s+(.+)$/m;
@@ -43,11 +41,9 @@ export interface SentryRuntimeContext {
4341
function redactPathLikeData(value: string): string {
4442
return value.replace(USER_HOME_PATH_PATTERN, '/Users/<redacted>');
4543
}
46-
4744
function isRecord(value: unknown): value is Record<string, unknown> {
4845
return typeof value === 'object' && value !== null && !Array.isArray(value);
4946
}
50-
5147
function redactUnknown(value: unknown): unknown {
5248
if (typeof value === 'string') {
5349
return redactPathLikeData(value);
@@ -153,7 +149,6 @@ let initialized = false;
153149
let enriched = false;
154150
let selfTestEmitted = false;
155151
let pendingRuntimeContext: SentryRuntimeContext | null = null;
156-
157152
function isSentryDisabled(): boolean {
158153
return (
159154
process.env.XCODEBUILDMCP_SENTRY_DISABLED === 'true' || process.env.SENTRY_DISABLED === 'true'
@@ -168,7 +163,6 @@ function isSentrySelfTestEnabled(): boolean {
168163
const raw = process.env[SENTRY_SELF_TEST_ENV_VAR]?.trim().toLowerCase();
169164
return raw === '1' || raw === 'true' || raw === 'yes';
170165
}
171-
172166
function emitSentrySelfTest(mode: SentryRuntimeMode | undefined): void {
173167
if (!isSentrySelfTestEnabled() || selfTestEmitted) {
174168
return;
@@ -202,7 +196,6 @@ function boolToTag(value: boolean | undefined): string | undefined {
202196
}
203197
return String(value);
204198
}
205-
206199
function setTagIfDefined(key: string, value: string | undefined): void {
207200
if (!value) {
208201
return;
@@ -387,7 +380,6 @@ interface InternalErrorMetric {
387380
}
388381

389382
type DaemonGaugeMetricName = 'inflight_requests' | 'active_sessions' | 'idle_timeout_ms';
390-
391383
function sanitizeTagValue(value: string): string {
392384
const trimmed = value.trim().toLowerCase();
393385
if (!trimmed) {
@@ -399,7 +391,6 @@ function sanitizeTagValue(value: string): string {
399391
function shouldEmitMetrics(): boolean {
400392
return initialized && !isSentryDisabled() && !isTestEnv();
401393
}
402-
403394
export function recordToolInvocationMetric(metric: ToolInvocationMetric): void {
404395
if (!shouldEmitMetrics()) {
405396
return;

0 commit comments

Comments
 (0)