Skip to content

Commit 837ee74

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 837ee74

File tree

2 files changed

+137
-33
lines changed

2 files changed

+137
-33
lines changed

client/bin/start.js

Lines changed: 137 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,44 @@ 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() === ServerStatus.RUNNING
211+
) {
212+
hasOpened = true;
213+
open(url);
214+
console.log(`\n🔗 Opening browser at: ${url}\n`);
215+
}
124216
}, 3000);
125217
}
126218

127219
await new Promise((resolve) => {
128220
client.subscribe({
129-
complete: resolve,
221+
complete: () => {
222+
// Clear timeout if process completes
223+
if (autoOpenTimeout) {
224+
clearTimeout(autoOpenTimeout);
225+
}
226+
resolve(null);
227+
},
130228
error: (err) => {
229+
// Clear timeout on error
230+
if (autoOpenTimeout) {
231+
clearTimeout(autoOpenTimeout);
232+
}
131233
if (!cancelled || process.env.DEBUG) {
132234
console.error("Client error:", err);
133235
}
@@ -139,7 +241,8 @@ async function startDevClient(clientOptions) {
139241
}
140242

141243
async function startProdClient(clientOptions) {
142-
const { CLIENT_PORT, authDisabled, sessionToken, abort } = clientOptions;
244+
const { CLIENT_PORT, authDisabled, sessionToken, abort, cancelled } =
245+
clientOptions;
143246
const inspectorClientPath = resolve(
144247
__dirname,
145248
"../..",
@@ -148,12 +251,13 @@ async function startProdClient(clientOptions) {
148251
"client.js",
149252
);
150253

151-
// Auto-open browser with token
152-
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false") {
254+
// Only auto-open browser if not cancelled
255+
if (process.env.MCP_AUTO_OPEN_ENABLED !== "false" && !cancelled) {
153256
const url = authDisabled
154257
? `http://127.0.0.1:${CLIENT_PORT}`
155258
: `http://127.0.0.1:${CLIENT_PORT}/?MCP_PROXY_AUTH_TOKEN=${sessionToken}`;
156259
open(url);
260+
console.log(`\n🔗 Opening browser at: ${url}\n`);
157261
}
158262

159263
await spawnPromise("node", [inspectorClientPath], {
@@ -224,7 +328,8 @@ async function main() {
224328
abort.abort();
225329
});
226330

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

229334
try {
230335
const serverOptions = {
@@ -242,17 +347,19 @@ async function main() {
242347
: await startProdServer(serverOptions);
243348

244349
server = result.server;
245-
serverOk = result.serverOk;
350+
getServerStatus = result.getServerStatus;
246351
} catch (error) {}
247352

248-
if (serverOk) {
353+
if (getServerStatus() === ServerStatus.RUNNING) {
354+
console.log("Server is confirmed running, starting client...");
249355
try {
250356
const clientOptions = {
251357
CLIENT_PORT,
252358
authDisabled,
253359
sessionToken,
254360
abort,
255361
cancelled,
362+
getServerStatus,
256363
};
257364

258365
await (isDev
@@ -261,6 +368,10 @@ async function main() {
261368
} catch (e) {
262369
if (!cancelled || process.env.DEBUG) throw e;
263370
}
371+
} else {
372+
console.log(
373+
`Server failed to start (status: ${getServerStatus()}), not starting client`,
374+
);
264375
}
265376

266377
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)