Skip to content

Commit d5028bc

Browse files
fix: Improve server status detection with health check instead of process monitoring
- Replace unreliable process exit detection with functional health check - Test /health endpoint after startup delay to confirm server is actually ready - Prevent client launch and browser auto-open when server fails to start - Provides clear error messages for different failure scenarios This fixes the issue where port conflicts and server crashes during startup would still result in browser auto-opening to a non-functional page. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 698d245 commit d5028bc

File tree

2 files changed

+138
-33
lines changed

2 files changed

+138
-33
lines changed

client/bin/start.js

Lines changed: 138 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ function delay(ms) {
1212
return new Promise((resolve) => setTimeout(resolve, ms, true));
1313
}
1414

15+
const ServerStatus = {
16+
UNKNOWN: "unknown",
17+
STARTING: "starting",
18+
RUNNING: "running",
19+
FAILED: "failed",
20+
STOPPED: "stopped",
21+
};
22+
1523
async function startDevServer(serverOptions) {
1624
const { SERVER_PORT, CLIENT_PORT, sessionToken, envVars, abort } =
1725
serverOptions;
@@ -39,19 +47,48 @@ async function startDevServer(serverOptions) {
3947

4048
const server = spawn(serverCommand, serverArgs, spawnOptions);
4149

42-
// Give server time to start
43-
const serverOk = await Promise.race([
44-
new Promise((resolve) => {
45-
server.subscribe({
46-
complete: () => resolve(false),
47-
error: () => resolve(false),
48-
next: () => {}, // We're using echoOutput
50+
// Track server status
51+
let serverStatus = ServerStatus.STARTING;
52+
53+
server.subscribe({
54+
complete: () => {
55+
// Server process ended - could be normal or abnormal
56+
if (serverStatus === ServerStatus.STARTING) {
57+
serverStatus = ServerStatus.FAILED; // Failed during startup
58+
console.log("Server failed during startup (process completed)");
59+
} else {
60+
serverStatus = ServerStatus.STOPPED; // Stopped after running
61+
}
62+
},
63+
error: () => {
64+
serverStatus = ServerStatus.FAILED;
65+
console.log("Server failed with error");
66+
},
67+
next: () => {}, // We're using echoOutput
68+
});
69+
70+
// Wait for server to start and actually test the connection
71+
await delay(3000);
72+
if (serverStatus === ServerStatus.STARTING) {
73+
// Check if server is actually listening by attempting a connection
74+
try {
75+
const response = await fetch(`http://127.0.0.1:${SERVER_PORT}/health`, {
76+
signal: AbortSignal.timeout(2000),
4977
});
50-
}),
51-
delay(3000).then(() => true),
52-
]);
78+
if (response.ok) {
79+
serverStatus = ServerStatus.RUNNING;
80+
console.log("Server confirmed healthy via /health endpoint");
81+
} else {
82+
serverStatus = ServerStatus.FAILED;
83+
console.log(`Server health check failed: ${response.status}`);
84+
}
85+
} catch (err) {
86+
serverStatus = ServerStatus.FAILED;
87+
console.log(`Server health check failed: ${err.message}`);
88+
}
89+
}
5390

54-
return { server, serverOk };
91+
return { server, getServerStatus: () => serverStatus };
5592
}
5693

5794
async function startProdServer(serverOptions) {
@@ -72,6 +109,9 @@ async function startProdServer(serverOptions) {
72109
"index.js",
73110
);
74111

112+
// Track server status
113+
let serverStatus = ServerStatus.STARTING;
114+
75115
const server = spawnPromise(
76116
"node",
77117
[
@@ -92,15 +132,56 @@ async function startProdServer(serverOptions) {
92132
},
93133
);
94134

95-
// Make sure server started before starting client
96-
const serverOk = await Promise.race([server, delay(2 * 1000)]);
135+
// Monitor for server completion
136+
server
137+
.then(() => {
138+
// Server completed normally
139+
if (serverStatus === ServerStatus.STARTING) {
140+
serverStatus = ServerStatus.FAILED; // Never started properly
141+
console.log("Server failed during startup (process completed)");
142+
} else {
143+
serverStatus = ServerStatus.STOPPED;
144+
}
145+
})
146+
.catch(() => {
147+
serverStatus = ServerStatus.FAILED;
148+
console.log("Server failed with error");
149+
});
150+
151+
// Wait for server to start - but actually test the connection
152+
await delay(2000); // Initial delay for server to attempt startup
153+
154+
if (serverStatus === ServerStatus.STARTING) {
155+
// Check if server is actually listening by attempting a connection
156+
try {
157+
const response = await fetch(`http://127.0.0.1:${SERVER_PORT}/health`, {
158+
signal: AbortSignal.timeout(2000),
159+
});
160+
if (response.ok) {
161+
serverStatus = ServerStatus.RUNNING;
162+
console.log("Server confirmed healthy via /health endpoint");
163+
} else {
164+
serverStatus = ServerStatus.FAILED;
165+
console.log(`Server health check failed: ${response.status}`);
166+
}
167+
} catch (err) {
168+
serverStatus = ServerStatus.FAILED;
169+
console.log(`Server health check failed: ${err.message}`);
170+
}
171+
}
97172

98-
return { server, serverOk };
173+
return { server, getServerStatus: () => serverStatus };
99174
}
100175

101176
async function startDevClient(clientOptions) {
102-
const { CLIENT_PORT, authDisabled, sessionToken, abort, cancelled } =
103-
clientOptions;
177+
const {
178+
CLIENT_PORT,
179+
authDisabled,
180+
sessionToken,
181+
abort,
182+
cancelled,
183+
getServerStatus,
184+
} = clientOptions;
104185
const clientCommand = "npx";
105186
const clientArgs = ["vite", "--port", CLIENT_PORT];
106187

@@ -111,23 +192,45 @@ async function startDevClient(clientOptions) {
111192
echoOutput: true,
112193
});
113194

195+
let autoOpenTimeout = null;
196+
let hasOpened = false;
197+
114198
// Auto-open browser after vite starts
115199
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
116200
const url = authDisabled
117201
? `http://127.0.0.1:${CLIENT_PORT}`
118202
: `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
119203

120204
// Give vite time to start before opening browser
121-
setTimeout(() => {
122-
open(url);
123-
console.log(`\n🔗 Opening browser at: ${url}\n`);
205+
autoOpenTimeout = setTimeout(() => {
206+
// Check if server is still running before opening
207+
if (
208+
!cancelled &&
209+
!hasOpened &&
210+
getServerStatus &&
211+
getServerStatus() === ServerStatus.RUNNING
212+
) {
213+
hasOpened = true;
214+
open(url);
215+
console.log(`\n🔗 Opening browser at: ${url}\n`);
216+
}
124217
}, 3000);
125218
}
126219

127220
await new Promise((resolve) => {
128221
client.subscribe({
129-
complete: resolve,
222+
complete: () => {
223+
// Clear timeout if process completes
224+
if (autoOpenTimeout) {
225+
clearTimeout(autoOpenTimeout);
226+
}
227+
resolve(null);
228+
},
130229
error: (err) => {
230+
// Clear timeout on error
231+
if (autoOpenTimeout) {
232+
clearTimeout(autoOpenTimeout);
233+
}
131234
if (!cancelled || process.env.DEBUG) {
132235
console.error("Client error:", err);
133236
}
@@ -139,7 +242,8 @@ async function startDevClient(clientOptions) {
139242
}
140243

141244
async function startProdClient(clientOptions) {
142-
const { CLIENT_PORT, authDisabled, sessionToken, abort } = clientOptions;
245+
const { CLIENT_PORT, authDisabled, sessionToken, abort, cancelled } =
246+
clientOptions;
143247
const inspectorClientPath = resolve(
144248
__dirname,
145249
"../..",
@@ -148,12 +252,13 @@ async function startProdClient(clientOptions) {
148252
"client.js",
149253
);
150254

151-
// Auto-open browser with token
152-
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
255+
// Only auto-open browser if not cancelled
256+
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && !cancelled) {
153257
const url = authDisabled
154258
? `http://127.0.0.1:${CLIENT_PORT}`
155259
: `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
156260
open(url);
261+
console.log(`\n🔗 Opening browser at: ${url}\n`);
157262
}
158263

159264
await spawnPromise("node", [inspectorClientPath], {
@@ -224,7 +329,8 @@ async function main() {
224329
abort.abort();
225330
});
226331

227-
let server, serverOk;
332+
let server;
333+
let getServerStatus = () => ServerStatus.UNKNOWN;
228334

229335
try {
230336
const serverOptions = {
@@ -242,17 +348,19 @@ async function main() {
242348
: await startProdServer(serverOptions);
243349

244350
server = result.server;
245-
serverOk = result.serverOk;
351+
getServerStatus = result.getServerStatus;
246352
} catch (error) {}
247353

248-
if (serverOk) {
354+
if (getServerStatus() === ServerStatus.RUNNING) {
355+
console.log("Server is confirmed running, starting client...");
249356
try {
250357
const clientOptions = {
251358
CLIENT_PORT,
252359
authDisabled,
253360
sessionToken,
254361
abort,
255362
cancelled,
363+
getServerStatus,
256364
};
257365

258366
await (isDev
@@ -261,6 +369,10 @@ async function main() {
261369
} catch (e) {
262370
if (!cancelled || process.env.DEBUG) throw e;
263371
}
372+
} else {
373+
console.log(
374+
`Server failed to start (status: ${getServerStatus()}), not starting client`,
375+
);
264376
}
265377

266378
return 0;

server/src/index.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -541,13 +541,6 @@ server.on("listening", () => {
541541
console.log(
542542
`Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth`,
543543
);
544-
545-
// Display clickable URL with pre-filled token
546-
const clientPort = process.env.CLIENT_PORT || "6274";
547-
const clientUrl = `http://localhost:${clientPort}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
548-
console.log(
549-
`\n🔗 Open inspector with token pre-filled:\n ${clientUrl}\n`,
550-
);
551544
} else {
552545
console.log(
553546
`⚠️ WARNING: Authentication is disabled. This is not recommended.`,

0 commit comments

Comments
 (0)