browser-debug-mcp-bridge 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- const { spawn } = require('node:child_process');
3
- const { existsSync } = require('node:fs');
2
+ const { spawn, spawnSync } = require('node:child_process');
3
+ const { existsSync, mkdirSync, openSync, writeFileSync, closeSync, readFileSync, unlinkSync } = require('node:fs');
4
4
  const { dirname, join, resolve } = require('node:path');
5
5
  const { createRequire } = require('node:module');
6
6
  const net = require('node:net');
7
+ const http = require('node:http');
7
8
 
8
9
  const repoRoot = resolve(__dirname, '..');
9
10
  const packageJson = join(repoRoot, 'package.json');
@@ -16,10 +17,14 @@ const useTsx = args.includes('--mode=tsx');
16
17
  const useDist = args.includes('--mode=dist');
17
18
  const dryRun = args.includes('--dry-run');
18
19
  const standalone = args.includes('--standalone');
20
+ const stopRequested = args.includes('--stop');
19
21
  const localRequire = createRequire(join(repoRoot, 'package.json'));
20
22
  const supportsColor = Boolean(process.stderr.isTTY) && !process.env.NO_COLOR;
21
23
  const greenBackground = '\x1b[42m\x1b[30m';
22
24
  const ansiReset = '\x1b[0m';
25
+ const launchLockPath = join(process.env.DATA_DIR ? resolve(process.env.DATA_DIR) : join(repoRoot, 'data'), '.mcp-start.lock');
26
+
27
+ let launchLockHeld = false;
23
28
 
24
29
  function resolveRuntimePath(specifier) {
25
30
  try {
@@ -69,12 +74,409 @@ function isPortInUse(port, host = '127.0.0.1') {
69
74
  });
70
75
  }
71
76
 
77
+ function delay(ms) {
78
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
79
+ }
80
+
81
+ function isProcessAlive(pid) {
82
+ if (!Number.isInteger(pid) || pid <= 0) {
83
+ return false;
84
+ }
85
+
86
+ try {
87
+ process.kill(pid, 0);
88
+ return true;
89
+ } catch (error) {
90
+ return Boolean(error && error.code === 'EPERM');
91
+ }
92
+ }
93
+
94
+ function parseLockPayload(raw) {
95
+ try {
96
+ const parsed = JSON.parse(raw);
97
+ return parsed && typeof parsed === 'object' ? parsed : null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ function readLaunchLock(lockPath) {
104
+ try {
105
+ const raw = readFileSync(lockPath, 'utf8');
106
+ return parseLockPayload(raw);
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+
112
+ function acquireLaunchLock(lockPath) {
113
+ mkdirSync(dirname(lockPath), { recursive: true });
114
+ const payload = JSON.stringify({
115
+ pid: process.pid,
116
+ createdAt: new Date().toISOString(),
117
+ command: process.argv.join(' '),
118
+ });
119
+
120
+ for (let attempt = 0; attempt < 2; attempt++) {
121
+ try {
122
+ const fd = openSync(lockPath, 'wx');
123
+ try {
124
+ writeFileSync(fd, payload, 'utf8');
125
+ } finally {
126
+ closeSync(fd);
127
+ }
128
+ launchLockHeld = true;
129
+ return;
130
+ } catch (error) {
131
+ if (!error || error.code !== 'EEXIST') {
132
+ throw error;
133
+ }
134
+
135
+ const existing = readLaunchLock(lockPath);
136
+ const lockPid = Number(existing && existing.pid);
137
+ if (Number.isInteger(lockPid) && lockPid > 0 && lockPid !== process.pid && isProcessAlive(lockPid)) {
138
+ process.stderr.write(
139
+ `[mcp-start] MCP_STARTUP_LOCKED: another launcher instance (pid ${lockPid}) is already running.\n`,
140
+ );
141
+ process.stderr.write('[mcp-start] If startup appears stuck, run: node scripts/mcp-start.cjs --stop\n');
142
+ process.exit(1);
143
+ }
144
+
145
+ try {
146
+ unlinkSync(lockPath);
147
+ } catch (unlinkError) {
148
+ if (!unlinkError || unlinkError.code !== 'ENOENT') {
149
+ process.stderr.write(
150
+ `[mcp-start] MCP_STARTUP_LOCK_FAILED: unable to clear stale lock at ${lockPath}.\n`,
151
+ );
152
+ process.exit(1);
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ process.stderr.write(`[mcp-start] MCP_STARTUP_LOCK_FAILED: unable to acquire startup lock at ${lockPath}.\n`);
159
+ process.exit(1);
160
+ }
161
+
162
+ function releaseLaunchLock(lockPath) {
163
+ if (!launchLockHeld) {
164
+ return;
165
+ }
166
+
167
+ try {
168
+ const existing = readLaunchLock(lockPath);
169
+ const lockPid = Number(existing && existing.pid);
170
+ if (Number.isInteger(lockPid) && lockPid > 0 && lockPid !== process.pid) {
171
+ launchLockHeld = false;
172
+ return;
173
+ }
174
+ unlinkSync(lockPath);
175
+ } catch (error) {
176
+ if (error && error.code !== 'ENOENT') {
177
+ process.stderr.write(`[mcp-start] Warning: failed to release startup lock ${lockPath}.\n`);
178
+ }
179
+ } finally {
180
+ launchLockHeld = false;
181
+ }
182
+ }
183
+
184
+ function getStartupTimeoutMs() {
185
+ const timeoutMs = Number(process.env.MCP_STARTUP_TIMEOUT_MS || '15000');
186
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 1000) {
187
+ return 15000;
188
+ }
189
+ return Math.floor(timeoutMs);
190
+ }
191
+
192
+ function fetchJson(pathname, port, timeoutMs = 1000) {
193
+ return new Promise((resolveJson) => {
194
+ const request = http.request(
195
+ {
196
+ host: '127.0.0.1',
197
+ port,
198
+ path: pathname,
199
+ method: 'GET',
200
+ timeout: timeoutMs,
201
+ },
202
+ (response) => {
203
+ const chunks = [];
204
+ response.on('data', (chunk) => chunks.push(chunk));
205
+ response.on('end', () => {
206
+ try {
207
+ const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
208
+ resolveJson(payload);
209
+ } catch {
210
+ resolveJson(null);
211
+ }
212
+ });
213
+ },
214
+ );
215
+ request.on('error', () => resolveJson(null));
216
+ request.on('timeout', () => {
217
+ request.destroy();
218
+ resolveJson(null);
219
+ });
220
+ request.end();
221
+ });
222
+ }
223
+
224
+ async function isBridgeHttpEndpoint(port) {
225
+ const root = await fetchJson('/', port);
226
+ if (root && typeof root === 'object' && typeof root.name === 'string' && root.name.includes('Browser Debug MCP Bridge')) {
227
+ return true;
228
+ }
229
+
230
+ const health = await fetchJson('/health', port);
231
+ return Boolean(health && typeof health === 'object' && health.status === 'ok' && health.websocket);
232
+ }
233
+
234
+ function getWindowsListeningPids(port) {
235
+ const result = spawnSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf8' });
236
+ if (result.status !== 0) {
237
+ return [];
238
+ }
239
+
240
+ const targetSuffix = `:${port}`;
241
+ const pids = new Set();
242
+ const lines = String(result.stdout || '').split(/\r?\n/u);
243
+
244
+ for (const line of lines) {
245
+ const parts = line.trim().split(/\s+/u);
246
+ if (parts.length < 5) {
247
+ continue;
248
+ }
249
+
250
+ const proto = String(parts[0] || '').toUpperCase();
251
+ const localAddress = String(parts[1] || '');
252
+ const state = String(parts[3] || '').toUpperCase();
253
+ const pid = Number(parts[4]);
254
+
255
+ if (proto === 'TCP' && state === 'LISTENING' && localAddress.endsWith(targetSuffix) && Number.isInteger(pid) && pid > 0) {
256
+ pids.add(pid);
257
+ }
258
+ }
259
+
260
+ return Array.from(pids);
261
+ }
262
+
263
+ function getWindowsProcessCommandLine(pid) {
264
+ const result = spawnSync(
265
+ 'powershell.exe',
266
+ ['-NoProfile', '-Command', `(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`],
267
+ { encoding: 'utf8' },
268
+ );
269
+
270
+ if (result.status !== 0) {
271
+ return '';
272
+ }
273
+
274
+ return String(result.stdout || '').trim();
275
+ }
276
+
277
+ function isLikelyBridgeCommandLine(commandLine) {
278
+ const normalized = String(commandLine || '').toLowerCase();
279
+ return normalized.includes('scripts\\mcp-start.cjs')
280
+ || normalized.includes('scripts/mcp-start.cjs')
281
+ || normalized.includes('mcp-bridge.js')
282
+ || normalized.includes('mcp-bridge.ts')
283
+ || normalized.includes('mcp-server:serve-mcp')
284
+ || normalized.includes('browser-debug-mcp-bridge.cmd');
285
+ }
286
+
287
+ function killWindowsProcess(pid) {
288
+ const result = spawnSync('taskkill', ['/PID', String(pid), '/T', '/F'], { encoding: 'utf8' });
289
+ return result.status === 0;
290
+ }
291
+
292
+ function getPosixListeningPids(port) {
293
+ const result = spawnSync('lsof', ['-nP', `-iTCP:${port}`, '-sTCP:LISTEN', '-t'], { encoding: 'utf8' });
294
+ if (result.error || result.status !== 0) {
295
+ return [];
296
+ }
297
+
298
+ return String(result.stdout || '')
299
+ .split(/\r?\n/u)
300
+ .map((value) => Number(value.trim()))
301
+ .filter((pid) => Number.isInteger(pid) && pid > 0);
302
+ }
303
+
304
+ function getListeningPids(port) {
305
+ return process.platform === 'win32' ? getWindowsListeningPids(port) : getPosixListeningPids(port);
306
+ }
307
+
308
+ function getPosixProcessCommandLine(pid) {
309
+ const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], { encoding: 'utf8' });
310
+ if (result.error || result.status !== 0) {
311
+ return '';
312
+ }
313
+ return String(result.stdout || '').trim();
314
+ }
315
+
316
+ function getProcessCommandLine(pid) {
317
+ return process.platform === 'win32' ? getWindowsProcessCommandLine(pid) : getPosixProcessCommandLine(pid);
318
+ }
319
+
320
+ function terminateProcess(pid) {
321
+ if (process.platform === 'win32') {
322
+ return killWindowsProcess(pid);
323
+ }
324
+
325
+ try {
326
+ process.kill(pid, 'SIGTERM');
327
+ return true;
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+
333
+ async function tryRecoverStaleBridgeOnWindowsPort(port) {
334
+ if (process.platform !== 'win32') {
335
+ return false;
336
+ }
337
+
338
+ const endpointLooksLikeBridge = await isBridgeHttpEndpoint(port);
339
+ const listenerPids = getWindowsListeningPids(port).filter((pid) => pid !== process.pid);
340
+ if (listenerPids.length === 0) {
341
+ return false;
342
+ }
343
+
344
+ let attemptedRestart = false;
345
+ for (const pid of listenerPids) {
346
+ const commandLine = getWindowsProcessCommandLine(pid);
347
+ const looksLikeBridge = endpointLooksLikeBridge || isLikelyBridgeCommandLine(commandLine);
348
+ if (!looksLikeBridge) {
349
+ continue;
350
+ }
351
+
352
+ attemptedRestart = true;
353
+ process.stderr.write(
354
+ `[mcp-start] Port ${port} is occupied by stale bridge process (pid ${pid}). Restarting automatically.\n`,
355
+ );
356
+
357
+ if (!killWindowsProcess(pid)) {
358
+ process.stderr.write(`[mcp-start] Failed to terminate stale process ${pid}.\n`);
359
+ }
360
+ }
361
+
362
+ if (!attemptedRestart) {
363
+ return false;
364
+ }
365
+
366
+ for (let attempt = 0; attempt < 12; attempt++) {
367
+ await delay(200);
368
+ const stillInUse = await isPortInUse(port);
369
+ if (!stillInUse) {
370
+ process.stderr.write(`[mcp-start] Recovered port ${port} from stale bridge instance.\n`);
371
+ return true;
372
+ }
373
+ }
374
+
375
+ return false;
376
+ }
377
+
378
+ async function waitForBridgeReady(port, child) {
379
+ const timeoutMs = getStartupTimeoutMs();
380
+ const deadline = Date.now() + timeoutMs;
381
+
382
+ while (Date.now() < deadline) {
383
+ if (child.exitCode !== null) {
384
+ return {
385
+ ok: false,
386
+ reason: `MCP bridge exited during startup with code ${child.exitCode}.`,
387
+ };
388
+ }
389
+
390
+ const health = await fetchJson('/health', port, 1000);
391
+ if (health && typeof health === 'object' && health.status === 'ok' && health.websocket) {
392
+ return { ok: true };
393
+ }
394
+
395
+ await delay(200);
396
+ }
397
+
398
+ return {
399
+ ok: false,
400
+ reason: `Timed out after ${timeoutMs}ms waiting for /health on 127.0.0.1:${port}.`,
401
+ };
402
+ }
403
+
404
+ async function stopBridgeOnPort(port) {
405
+ const listenerPids = getListeningPids(port).filter((pid) => pid !== process.pid);
406
+ if (listenerPids.length === 0) {
407
+ const inUse = await isPortInUse(port);
408
+ if (!inUse) {
409
+ process.stderr.write(`[mcp-start] MCP_STOP_NO_ACTIVE_PROCESS: no listener found on port ${port}.\n`);
410
+ process.exit(0);
411
+ }
412
+
413
+ process.stderr.write(
414
+ `[mcp-start] MCP_STOP_FAILED: port ${port} is in use but listener process could not be resolved on ${process.platform}.\n`,
415
+ );
416
+ process.exit(1);
417
+ }
418
+
419
+ const endpointLooksLikeBridge = await isBridgeHttpEndpoint(port);
420
+ const bridgePids = [];
421
+ const nonBridgePids = [];
422
+
423
+ for (const pid of listenerPids) {
424
+ const commandLine = getProcessCommandLine(pid);
425
+ const looksLikeBridge = endpointLooksLikeBridge || isLikelyBridgeCommandLine(commandLine);
426
+ if (looksLikeBridge) {
427
+ bridgePids.push({ pid, commandLine });
428
+ continue;
429
+ }
430
+ nonBridgePids.push({ pid, commandLine });
431
+ }
432
+
433
+ if (bridgePids.length === 0) {
434
+ process.stderr.write(
435
+ `[mcp-start] MCP_STOP_PORT_OCCUPIED_BY_OTHER_APP: port ${port} is not owned by Browser Debug MCP Bridge.\n`,
436
+ );
437
+ for (const proc of nonBridgePids) {
438
+ process.stderr.write(
439
+ `[mcp-start] Occupant pid=${proc.pid}${proc.commandLine ? ` cmd=${proc.commandLine}` : ''}\n`,
440
+ );
441
+ }
442
+ process.exit(1);
443
+ }
444
+
445
+ let stopFailed = false;
446
+ for (const proc of bridgePids) {
447
+ process.stderr.write(`[mcp-start] Stopping Browser Debug MCP Bridge process ${proc.pid} on port ${port}.\n`);
448
+ if (!terminateProcess(proc.pid)) {
449
+ process.stderr.write(`[mcp-start] Failed to terminate process ${proc.pid}.\n`);
450
+ stopFailed = true;
451
+ }
452
+ }
453
+
454
+ if (stopFailed) {
455
+ process.stderr.write('[mcp-start] MCP_STOP_FAILED: one or more processes could not be terminated.\n');
456
+ process.exit(1);
457
+ }
458
+
459
+ for (let attempt = 0; attempt < 15; attempt++) {
460
+ await delay(200);
461
+ const remainingListeners = getListeningPids(port).filter((pid) => pid !== process.pid);
462
+ if (remainingListeners.length === 0) {
463
+ process.stderr.write(`[mcp-start] MCP_STOP_SUCCESS: Browser Debug MCP Bridge stopped on port ${port}.\n`);
464
+ process.exit(0);
465
+ }
466
+ }
467
+
468
+ process.stderr.write(
469
+ `[mcp-start] MCP_STOP_FAILED: process termination requested but port ${port} is still in use.\n`,
470
+ );
471
+ process.exit(1);
472
+ }
473
+
72
474
  if (!existsSync(packageJson)) {
73
475
  process.stderr.write(`[mcp-start] Invalid repository root: ${repoRoot}\n`);
74
476
  process.exit(1);
75
477
  }
76
478
 
77
- function spawnRuntime(runtime) {
479
+ async function spawnRuntime(runtime, port) {
78
480
  const nxTarget = standalone ? 'mcp-server:serve' : 'mcp-server:serve-mcp';
79
481
  const entryScript =
80
482
  runtime === 'dist'
@@ -97,17 +499,6 @@ function spawnRuntime(runtime) {
97
499
  process.exit(0);
98
500
  }
99
501
 
100
- const startedMessage = standalone
101
- ? `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: standalone). Keep this terminal open.`
102
- : `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: mcp-stdio).`;
103
- process.stderr.write(`${supportsColor ? `${greenBackground}${startedMessage}${ansiReset}` : startedMessage}\n`);
104
- if (!standalone && process.stdin.isTTY) {
105
- process.stderr.write(
106
- '[mcp-start] Running from interactive terminal without MCP host. ' +
107
- 'Use --standalone for manual keep-alive testing.\n',
108
- );
109
- }
110
-
111
502
  const child =
112
503
  runtime === 'nx'
113
504
  ? spawn(
@@ -135,11 +526,57 @@ function spawnRuntime(runtime) {
135
526
  },
136
527
  );
137
528
 
529
+ let startupFinished = false;
530
+
531
+ const forwardSignalToChild = (signal) => {
532
+ if (child.exitCode !== null || child.killed) {
533
+ return;
534
+ }
535
+
536
+ try {
537
+ child.kill(signal);
538
+ } catch {
539
+ // Ignore signal forwarding failures when child is already terminating.
540
+ }
541
+ };
542
+
543
+ process.on('SIGINT', () => forwardSignalToChild('SIGINT'));
544
+ process.on('SIGTERM', () => forwardSignalToChild('SIGTERM'));
545
+ if (process.platform !== 'win32') {
546
+ process.on('SIGHUP', () => forwardSignalToChild('SIGHUP'));
547
+ }
548
+
549
+ process.on('exit', () => {
550
+ if (child.exitCode === null && !child.killed) {
551
+ try {
552
+ child.kill('SIGTERM');
553
+ } catch {
554
+ // Ignore failures during process exit.
555
+ }
556
+ }
557
+ });
558
+
559
+ if (!standalone) {
560
+ const shutdownFromHostDisconnect = () => {
561
+ process.stderr.write('[mcp-start] MCP host disconnected; stopping MCP bridge child process.\n');
562
+ forwardSignalToChild('SIGTERM');
563
+ };
564
+
565
+ process.stdin.on('end', shutdownFromHostDisconnect);
566
+ process.stdin.on('close', shutdownFromHostDisconnect);
567
+ process.on('disconnect', shutdownFromHostDisconnect);
568
+ }
569
+
138
570
  child.on('exit', (code, signal) => {
139
571
  if (signal) {
140
572
  process.kill(process.pid, signal);
141
573
  return;
142
574
  }
575
+ if (!startupFinished) {
576
+ process.stderr.write(
577
+ `[mcp-start] MCP_STARTUP_FAILED: MCP bridge exited before readiness check completed (code ${code ?? 0}).\n`,
578
+ );
579
+ }
143
580
  if (!standalone && process.stdin.isTTY && (code ?? 0) === 0) {
144
581
  process.stderr.write(
145
582
  '[mcp-start] MCP stdio process exited (no MCP host attached). ' +
@@ -156,27 +593,68 @@ function spawnRuntime(runtime) {
156
593
  );
157
594
  process.exit(1);
158
595
  });
596
+
597
+ const readiness = await waitForBridgeReady(port, child);
598
+ if (!readiness.ok) {
599
+ process.stderr.write(`[mcp-start] MCP_STARTUP_FAILED: ${readiness.reason}\n`);
600
+ forwardSignalToChild('SIGTERM');
601
+ process.exit(1);
602
+ }
603
+
604
+ startupFinished = true;
605
+ const startedMessage = standalone
606
+ ? `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: standalone). Keep this terminal open.`
607
+ : `[mcp-start] Started Browser Debug MCP Bridge (runtime: ${runtime}, mode: mcp-stdio).`;
608
+ process.stderr.write(`${supportsColor ? `${greenBackground}${startedMessage}${ansiReset}` : startedMessage}\n`);
609
+ if (!standalone && process.stdin.isTTY) {
610
+ process.stderr.write(
611
+ '[mcp-start] Running from interactive terminal without MCP host. ' +
612
+ 'Use --standalone for manual keep-alive testing.\n',
613
+ );
614
+ }
159
615
  }
160
616
 
161
617
  async function main() {
162
618
  const port = Number(process.env.PORT || '8065');
619
+ if (!Number.isFinite(port) || port <= 0) {
620
+ process.stderr.write(`[mcp-start] Invalid PORT value: ${String(process.env.PORT || '')}\n`);
621
+ process.exit(1);
622
+ }
623
+
624
+ if (stopRequested) {
625
+ await stopBridgeOnPort(port);
626
+ return;
627
+ }
628
+
629
+ if (!dryRun) {
630
+ acquireLaunchLock(launchLockPath);
631
+ process.on('exit', () => releaseLaunchLock(launchLockPath));
632
+ }
633
+
163
634
  if (Number.isFinite(port)) {
164
- const inUse = await isPortInUse(port);
635
+ let inUse = await isPortInUse(port);
165
636
  if (inUse) {
637
+ const recovered = await tryRecoverStaleBridgeOnWindowsPort(port);
638
+ if (recovered) {
639
+ inUse = await isPortInUse(port);
640
+ }
641
+ }
642
+
643
+ if (inUse) {
644
+ process.stderr.write(
645
+ `[mcp-start] MCP_STARTUP_PORT_IN_USE: required MCP port ${port} is already in use.\n`,
646
+ );
166
647
  process.stderr.write(
167
- `[mcp-start] Port ${port} is already in use. Another Browser Debug MCP Bridge instance is likely running.\n`,
648
+ `[mcp-start] Reserve port ${port} for Browser Debug MCP Bridge: stop the process currently using it, then start the bridge again.\n`,
168
649
  );
169
650
  process.stderr.write(
170
- '[mcp-start] Stop the existing process, or run this instance on a different port.\n',
651
+ '[mcp-start] The bridge cannot start until the configured MCP port is free.\n',
171
652
  );
172
653
  if (process.platform === 'win32') {
173
654
  process.stderr.write(
174
- '[mcp-start] Windows help: netstat -ano | findstr :8065\n',
655
+ `[mcp-start] Windows help: netstat -ano | findstr :${port}\n`,
175
656
  );
176
657
  }
177
- process.stderr.write(
178
- '[mcp-start] Example with different port: PORT=8070 browser-debug-mcp-bridge --standalone\n',
179
- );
180
658
  process.exit(1);
181
659
  }
182
660
  }
@@ -188,7 +666,7 @@ async function main() {
188
666
  );
189
667
  process.exit(1);
190
668
  }
191
- spawnRuntime('dist');
669
+ await spawnRuntime('dist', port);
192
670
  return;
193
671
  }
194
672
 
@@ -197,17 +675,17 @@ async function main() {
197
675
  process.stderr.write('[mcp-start] Missing tsx runtime. Run npm install/pnpm install first.\n');
198
676
  process.exit(1);
199
677
  }
200
- spawnRuntime('tsx');
678
+ await spawnRuntime('tsx', port);
201
679
  return;
202
680
  }
203
681
 
204
682
  if (existsSync(mcpBridgeDistEntry) && existsSync(mainServerDistEntry)) {
205
- spawnRuntime('dist');
683
+ await spawnRuntime('dist', port);
206
684
  return;
207
685
  }
208
686
 
209
687
  if (existsSync(nxBin)) {
210
- spawnRuntime('nx');
688
+ await spawnRuntime('nx', port);
211
689
  return;
212
690
  }
213
691
 
@@ -220,7 +698,7 @@ async function main() {
220
698
  }
221
699
 
222
700
  process.stderr.write('[mcp-start] nx runtime not found, using tsx fallback runtime.\n');
223
- spawnRuntime('tsx');
701
+ await spawnRuntime('tsx', port);
224
702
  }
225
703
 
226
704
  main().catch((error) => {