what-devtools-mcp 0.10.0 → 0.11.1

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.
package/README.md CHANGED
@@ -174,6 +174,18 @@ installDevTools(core);
174
174
  connectDevToolsMCP({ port: 9229 });
175
175
  ```
176
176
 
177
+ Without the Vite plugin the client starts **dormant** — it does not poll for a
178
+ bridge, so apps with no bridge running produce zero console/network noise.
179
+ To activate it, do one of:
180
+
181
+ - pass an explicit `token` (e.g. from `WHAT_MCP_TOKEN`): `connectDevToolsMCP({ token })`
182
+ - set `window.__WHAT_DEVTOOLS_DEBUG__ = true` before calling it
183
+ - call `window.__WHAT_MCP_RECONNECT__()` from the browser console after starting the bridge
184
+
185
+ The Vite plugin doesn't need any of this: it serves a same-origin
186
+ `/__what_mcp_discovery` endpoint and the client polls that quietly, so the
187
+ bridge is discovered automatically whenever it's running.
188
+
177
189
  ## Configuration
178
190
 
179
191
  | Option | Default | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-devtools-mcp",
3
- "version": "0.10.0",
3
+ "version": "0.11.1",
4
4
  "description": "MCP server bridging AI agents to live What Framework app state via WebSocket",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bridge.js CHANGED
@@ -13,7 +13,7 @@ import { join } from 'path';
13
13
  const MAX_EVENT_LOG = 1000;
14
14
  const MAX_ERROR_LOG = 100;
15
15
 
16
- export function createBridge({ port = 9229, host = '127.0.0.1' } = {}) {
16
+ export function createBridge({ port = 9229, host = '127.0.0.1', onError } = {}) {
17
17
  let latestSnapshot = null;
18
18
  const eventLog = [];
19
19
  const errorLog = [];
@@ -60,6 +60,30 @@ export function createBridge({ port = 9229, host = '127.0.0.1' } = {}) {
60
60
  }
61
61
  }});
62
62
 
63
+ // The WebSocketServer binds the port immediately on construction. Without an
64
+ // 'error' listener, an EADDRINUSE (very likely — 9229 is also Node's default
65
+ // --inspect port) is emitted as an unhandled 'error' event, which an
66
+ // EventEmitter rethrows and hard-crashes the whole process. The bridge IS the
67
+ // server, so unlike the optional HTTP discovery port we cannot silently carry
68
+ // on with a dead WS server. Strategy: gracefully exit(0) with an actionable
69
+ // message so the launcher (e.g. `what dev`) sees a clean shutdown rather than
70
+ // an uncaught-exception stack trace. Callers (tests) can pass `onError` to
71
+ // intercept instead of exiting.
72
+ wss.on('error', (err) => {
73
+ if (err && err.code === 'EADDRINUSE') {
74
+ const inspectNote = port === 9229 ? ' (note: 9229 is also Node\'s default --inspect port)' : '';
75
+ console.error(`[what-devtools-mcp] port ${port} in use — set WHAT_MCP_PORT to a free port or stop the process using it${inspectNote}.`);
76
+ } else {
77
+ console.error(`[what-devtools-mcp] bridge WebSocket server error: ${err && err.message ? err.message : err}`);
78
+ }
79
+ if (typeof onError === 'function') {
80
+ onError(err);
81
+ } else {
82
+ // Graceful exit: the bridge cannot function without its WS port.
83
+ process.exit(0);
84
+ }
85
+ });
86
+
63
87
  // --- Token Discovery ---
64
88
  // Two mechanisms so the browser client can find the token automatically:
65
89
  //
@@ -138,9 +138,16 @@ export async function handleExtendedCommand(command, args, devtools) {
138
138
  const segments = code.split('.');
139
139
  const isSimpleIdent = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
140
140
  const PROTO_DENYLIST = new Set(['constructor', 'prototype', '__proto__']);
141
+ // Sensitive property paths: reads that exfiltrate secrets/state must
142
+ // require the explicit unsafe flag (document.cookie, window.localStorage...).
143
+ // Mirror of SENSITIVE_PATHS in tools-extended.js what_eval.
144
+ const SENSITIVE_PATHS = new Set([
145
+ 'cookie', 'localStorage', 'sessionStorage', 'indexedDB', 'credentials',
146
+ 'geolocation', 'clipboard', 'serviceWorker', 'caches', 'opener',
147
+ ]);
141
148
  const isSafeRead = segments.length >= 2 &&
142
149
  SAFE_GLOBALS.has(segments[0]) &&
143
- segments.every(s => isSimpleIdent.test(s) && !PROTO_DENYLIST.has(s));
150
+ segments.every(s => isSimpleIdent.test(s) && !PROTO_DENYLIST.has(s) && !SENSITIVE_PATHS.has(s));
144
151
 
145
152
  if (!evalEnabled && !isSafeRead) {
146
153
  return {
package/src/client.js CHANGED
@@ -21,80 +21,143 @@ function logGrouped(badge, badgeStyle, title, data) {
21
21
  console.groupEnd();
22
22
  }
23
23
 
24
- export function connectDevToolsMCP({ port = 9229, token = '' } = {}) {
24
+ export function connectDevToolsMCP({ port = 9229, token = '', discoveryUrl = '' } = {}) {
25
25
  // Never connect in production
26
26
  if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
27
- return { disconnect() {}, isConnected: false, eventCount: 0 };
27
+ return { disconnect() {}, reconnect() {}, isConnected: false, eventCount: 0 };
28
28
  }
29
29
  try {
30
30
  if (import.meta?.env?.PROD) {
31
- return { disconnect() {}, isConnected: false, eventCount: 0 };
31
+ return { disconnect() {}, reconnect() {}, isConnected: false, eventCount: 0 };
32
32
  }
33
33
  } catch {}
34
34
 
35
35
  let ws = null;
36
36
  let connected = false;
37
- let reconnectTimer = null;
38
- let reconnectDelay = 1000;
39
- const MAX_RECONNECT_DELAY = 30000;
40
- const pendingResponses = new Map();
37
+ let stopped = false;
38
+ let probeTimer = null;
39
+ // Probe back-off: first retry after 10s, then ×3 each time, capped at 5min.
40
+ // With no bridge running this yields probes at ~0s / 10s / 40s / 130s / 430s…
41
+ // — at most 3-4 quiet fetch attempts in the first two minutes, then near-silence.
42
+ const PROBE_DELAY_INITIAL = 10000;
43
+ const PROBE_DELAY_RECONNECT = 2000; // bridge restarts are usually fast
44
+ const PROBE_BACKOFF_FACTOR = 3;
45
+ const PROBE_DELAY_MAX = 300000;
46
+ let probeDelay = PROBE_DELAY_INITIAL;
41
47
  let eventCount = 0;
42
- let hasLoggedDisconnect = false;
43
- let reconnectAttempts = 0;
48
+ let hasLoggedMissingBridge = false;
49
+ let hasShownBanner = false;
44
50
  let unsubscribeFn = null;
45
51
  let flushEventBatch = null; // Hoisted — set during subscription, called by set-signal
46
52
  let discoveredToken = token;
47
53
  let discoveredPort = port;
48
54
 
49
- // Startup banner
50
- console.log(
51
- '%c⚡ What DevTools MCP %c Client v0.2.0',
52
- 'background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff;padding:4px 10px;border-radius:4px;font-weight:bold;font-size:13px',
53
- 'color:#a855f7;font-weight:bold'
54
- );
55
-
56
- // --- Token Auto-Discovery ---
57
- // If no token is provided, try to discover it from the bridge's HTTP endpoint.
58
- // The bridge serves GET http://localhost:{port+1}/__what_mcp_token
59
- async function discoverToken() {
60
- if (discoveredToken) return true; // Already have a token
61
-
62
- const discoveryPort = port + 1;
55
+ // --- Quiet bridge probe (also discovers the token) ---
56
+ // We NEVER open a WebSocket until this HTTP probe succeeds. A failed
57
+ // WebSocket connection always prints an unsuppressible red error in the
58
+ // browser console; a failed fetch wrapped in catch() prints at most a single
59
+ // muted "Failed to load resource" network line — the quietest probe the
60
+ // platform allows.
61
+ //
62
+ // Two probe modes:
63
+ //
64
+ // 1. Same-origin discovery (preferred — `discoveryUrl`, set by the Vite
65
+ // plugin): the dev server proxies the bridge probe Node-side, so a
66
+ // missing bridge produces ZERO console output the dev server itself is
67
+ // up, so there is no network-layer ERR_CONNECTION_REFUSED line either.
68
+ //
69
+ // 2. Direct probe of GET http://localhost:{port+1}/__what_mcp_token
70
+ // (loopback-origin gated; a 200 means the bridge is up AND returns the
71
+ // token + actual WS port in one round-trip). Even wrapped in catch(),
72
+ // the browser's network layer logs net::ERR_CONNECTION_REFUSED for every
73
+ // failed attempt — unsuppressible from JS — so this mode only runs when
74
+ // a bridge is actually expected (explicit token, or the
75
+ // window.__WHAT_DEVTOOLS_DEBUG__ flag). See `pollEnabled` below.
76
+ //
77
+ // Trade-off (direct mode): if the discovery HTTP port (port+1) is occupied
78
+ // by another process while the WS port is free, we won't connect even with
79
+ // an explicit token. That edge case is rarer and cheaper than spamming
80
+ // every fresh app (the default state: no bridge running) with red WS errors.
81
+ async function probeBridge() {
82
+ if (discoveryUrl) {
83
+ try {
84
+ const res = await fetch(discoveryUrl, { cache: 'no-store' });
85
+ if (res.ok) {
86
+ const data = await res.json();
87
+ if (!data.bridge) return false;
88
+ if (data.token) discoveredToken = data.token;
89
+ discoveredPort = data.wsPort || port;
90
+ return true;
91
+ }
92
+ } catch {
93
+ // Dev server unreachable (page about to die) — stay quiet.
94
+ }
95
+ return false;
96
+ }
63
97
  try {
64
- const res = await fetch(`http://localhost:${discoveryPort}/__what_mcp_token`);
98
+ const res = await fetch(`http://localhost:${port + 1}/__what_mcp_token`, { cache: 'no-store' });
65
99
  if (res.ok) {
66
100
  const data = await res.json();
67
- discoveredToken = data.token;
101
+ if (data.token) discoveredToken = data.token;
68
102
  discoveredPort = data.wsPort || port;
69
- log('MCP', BADGE, `Token discovered automatically from bridge`);
70
103
  return true;
71
104
  }
72
105
  } catch {
73
- // Discovery endpoint not availablebridge may not be running yet
106
+ // Bridge not runningstay quiet; back-off handles retries.
74
107
  }
75
108
  return false;
76
109
  }
77
110
 
78
- log('MCP', BADGE, `Connecting to bridge on ws://localhost:${port}`);
111
+ async function tryConnect() {
112
+ if (stopped) return;
113
+ const bridgeUp = await probeBridge();
114
+ if (stopped) return;
115
+ if (!bridgeUp) {
116
+ if (!hasLoggedMissingBridge) {
117
+ hasLoggedMissingBridge = true;
118
+ // Single muted line — the ONLY console-API message a fresh app sees.
119
+ console.info(
120
+ '%c[what]%c devtools bridge not detected — start what-devtools-mcp to enable agent debugging (retrying quietly in background)',
121
+ DIM, DIM
122
+ );
123
+ }
124
+ scheduleProbe();
125
+ return;
126
+ }
127
+ openSocket();
128
+ }
129
+
130
+ function scheduleProbe() {
131
+ if (probeTimer || stopped) return;
132
+ probeTimer = setTimeout(() => {
133
+ probeTimer = null;
134
+ probeDelay = Math.min(probeDelay * PROBE_BACKOFF_FACTOR, PROBE_DELAY_MAX);
135
+ tryConnect();
136
+ }, probeDelay);
137
+ }
79
138
 
80
- function connect() {
81
- reconnectAttempts++;
139
+ function openSocket() {
82
140
  try {
83
141
  const tokenParam = discoveredToken ? `?token=${encodeURIComponent(discoveredToken)}` : '';
84
142
  ws = new WebSocket(`ws://localhost:${discoveredPort}${tokenParam}`);
85
143
  } catch {
86
- if (reconnectAttempts <= 1) {
87
- log('MCP', BADGE_WARN, 'Bridge not available — retrying silently in background');
88
- }
89
- scheduleReconnect();
144
+ scheduleProbe();
90
145
  return;
91
146
  }
92
147
 
93
148
  ws.onopen = () => {
94
149
  connected = true;
95
- reconnectDelay = 1000;
96
- hasLoggedDisconnect = false;
97
- reconnectAttempts = 0;
150
+ probeDelay = PROBE_DELAY_RECONNECT;
151
+ hasLoggedMissingBridge = false;
152
+ if (!hasShownBanner) {
153
+ hasShownBanner = true;
154
+ // Banner only once we KNOW the bridge exists — silent otherwise.
155
+ console.log(
156
+ '%c⚡ What DevTools MCP %c Client v0.2.0',
157
+ 'background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff;padding:4px 10px;border-radius:4px;font-weight:bold;font-size:13px',
158
+ 'color:#a855f7;font-weight:bold'
159
+ );
160
+ }
98
161
  log('MCP', BADGE, '🟢 Connected to bridge — AI agent can now inspect this app');
99
162
 
100
163
  // Send initial snapshot
@@ -151,11 +214,12 @@ export function connectDevToolsMCP({ port = 9229, token = '' } = {}) {
151
214
  ws.onclose = () => {
152
215
  const wasConnected = connected;
153
216
  connected = false;
154
- if (wasConnected && !hasLoggedDisconnect) {
155
- hasLoggedDisconnect = true;
156
- log('MCP', BADGE_WARN, '🔴 Disconnected from bridgewill reconnect silently');
217
+ if (wasConnected) {
218
+ log('MCP', BADGE_WARN, '🔴 Disconnected from bridge — will reconnect quietly in background');
219
+ // Bridge restarts are usually quickretry fast, then back off.
220
+ probeDelay = PROBE_DELAY_RECONNECT;
157
221
  }
158
- scheduleReconnect();
222
+ scheduleProbe();
159
223
  };
160
224
 
161
225
  ws.onerror = () => {
@@ -294,47 +358,62 @@ export function connectDevToolsMCP({ port = 9229, token = '' } = {}) {
294
358
  }
295
359
  }
296
360
 
297
- function scheduleReconnect() {
298
- if (reconnectTimer) return;
299
- // After 5 failures, go very slow (30s) to avoid console spam
300
- if (reconnectAttempts === 5 && !hasLoggedDisconnect) {
301
- log('MCP', BADGE_WARN, `Bridge not available — will retry every 30s. Start the MCP server to connect.`);
302
- hasLoggedDisconnect = true;
303
- }
304
- const delay = reconnectAttempts >= 5 ? MAX_RECONNECT_DELAY : reconnectDelay;
305
- reconnectTimer = setTimeout(async () => {
306
- reconnectTimer = null;
307
- reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
308
- // Try to discover token before each reconnect attempt
309
- // (bridge may have started since last attempt)
310
- await discoverToken();
311
- connect();
312
- }, delay);
313
- }
314
-
315
361
  function disconnect() {
362
+ stopped = true;
316
363
  if (unsubscribeFn) {
317
364
  unsubscribeFn();
318
365
  unsubscribeFn = null;
319
366
  }
320
- if (reconnectTimer) {
321
- clearTimeout(reconnectTimer);
322
- reconnectTimer = null;
367
+ if (probeTimer) {
368
+ clearTimeout(probeTimer);
369
+ probeTimer = null;
323
370
  }
324
371
  if (ws) {
325
372
  ws.onclose = null; // prevent reconnect
326
373
  ws.close();
327
374
  ws = null;
328
375
  }
376
+ if (connected) log('MCP', BADGE, 'Disconnected');
329
377
  connected = false;
330
- log('MCP', BADGE, 'Disconnected');
331
378
  }
332
379
 
333
- // Initial connecttry token discovery first, then connect
334
- discoverToken().then(() => connect());
380
+ // Manual retry escape hatch resets the back-off and probes immediately.
381
+ // Useful when the dev just started the bridge and doesn't want to wait
382
+ // out the back-off (or reload the page). Also the opt-in for clients that
383
+ // start dormant (see pollEnabled below).
384
+ function reconnect() {
385
+ if (stopped || connected) return;
386
+ if (probeTimer) {
387
+ clearTimeout(probeTimer);
388
+ probeTimer = null;
389
+ }
390
+ probeDelay = PROBE_DELAY_INITIAL;
391
+ tryConnect();
392
+ }
393
+ if (typeof window !== 'undefined') {
394
+ window.__WHAT_MCP_RECONNECT__ = reconnect;
395
+ }
396
+
397
+ // Direct cross-origin polling logs net::ERR_CONNECTION_REFUSED from the
398
+ // browser's network layer on every attempt when no bridge runs — JS cannot
399
+ // suppress it, and a fresh scaffold (no bridge) saw that on every load.
400
+ // So without a same-origin discoveryUrl, only poll when a bridge is
401
+ // actually expected: an explicit token was provided, or the developer set
402
+ // window.__WHAT_DEVTOOLS_DEBUG__ = true. Otherwise stay dormant — zero
403
+ // network traffic, zero console output — until reconnect() opts in
404
+ // (window.__WHAT_MCP_RECONNECT__() from the console works too).
405
+ const pollEnabled = Boolean(
406
+ discoveryUrl
407
+ || token
408
+ || (typeof window !== 'undefined' && window.__WHAT_DEVTOOLS_DEBUG__)
409
+ );
410
+
411
+ // Initial attempt — quiet probe first; WebSocket only if the bridge answers.
412
+ if (pollEnabled) tryConnect();
335
413
 
336
414
  return {
337
415
  disconnect,
416
+ reconnect,
338
417
  get isConnected() { return connected; },
339
418
  get eventCount() { return eventCount; },
340
419
  };
package/src/index.js CHANGED
@@ -4,18 +4,52 @@
4
4
  * Creates WS bridge, registers tools + resources, connects MCP stdio transport.
5
5
  */
6
6
 
7
+ import { readFileSync } from 'node:fs';
7
8
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
10
  import { createBridge } from './bridge.js';
10
11
  import { registerTools } from './tools.js';
11
12
 
13
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
14
+
15
+ // --help / --version must print and exit BEFORE any server starts — this bin
16
+ // otherwise opens a WebSocket bridge + discovery HTTP server and blocks on
17
+ // the MCP stdio transport, which made `what-devtools-mcp --help` hang.
18
+ const cliArgs = process.argv.slice(2);
19
+ if (cliArgs.includes('--help') || cliArgs.includes('-h')) {
20
+ console.log(`what-devtools-mcp ${pkg.version} — MCP server bridging AI agents to live What Framework app state
21
+
22
+ Usage:
23
+ what-devtools-mcp [flags]
24
+
25
+ Runs an MCP server on stdio (add it to your MCP client config) plus a local
26
+ WebSocket bridge the browser devtools client connects to.
27
+
28
+ Flags:
29
+ -h, --help Show this help and exit
30
+ -v, --version Print the version and exit
31
+ --unsafe-eval Enable the what_eval tool (arbitrary JS in the browser — dev only)
32
+
33
+ Environment:
34
+ WHAT_MCP_PORT WebSocket bridge port (default: 9229; token discovery on port+1)
35
+ WHAT_MCP_TOKEN Pre-shared auth token (default: random per run)
36
+ WHAT_UNSAFE_EVAL Set to 1/true to enable what_eval (same as --unsafe-eval)
37
+
38
+ Docs: https://whatfw.com`);
39
+ process.exit(0);
40
+ }
41
+ if (cliArgs.includes('--version') || cliArgs.includes('-v')) {
42
+ console.log(pkg.version);
43
+ process.exit(0);
44
+ }
45
+
12
46
  const port = parseInt(process.env.WHAT_MCP_PORT || '9229', 10);
13
47
 
14
48
  const bridge = createBridge({ port });
15
49
 
16
50
  const server = new McpServer({
17
51
  name: 'what-devtools-mcp',
18
- version: '0.2.0',
52
+ version: pkg.version,
19
53
  });
20
54
 
21
55
  registerTools(server, bridge);
@@ -1033,7 +1067,7 @@ server.prompt(
1033
1067
  type: 'text',
1034
1068
  text: `# WhatFW MCP DevTools — Quick Reference
1035
1069
 
1036
- You have access to 28 MCP tools for inspecting and debugging a live What Framework app running in the browser.
1070
+ You have access to 29 MCP tools for inspecting and debugging a live What Framework app running in the browser.
1037
1071
 
1038
1072
  ## Connection Check
1039
1073
  Always start with: \`what_connection_status\` — confirms the browser is connected and shows signal/effect/component counts.
@@ -434,9 +434,16 @@ export function registerExtendedTools(server, bridge) {
434
434
  const segments = trimmed.split('.');
435
435
  const isSimpleIdent = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
436
436
  const PROTO_DENYLIST = new Set(['constructor', 'prototype', '__proto__']);
437
+ // Sensitive property paths: reads that exfiltrate secrets/state must
438
+ // require the explicit unsafe flag (document.cookie, window.localStorage,
439
+ // navigator.credentials...). Mirror of SENSITIVE_PATHS in client-commands.js.
440
+ const SENSITIVE_PATHS = new Set([
441
+ 'cookie', 'localStorage', 'sessionStorage', 'indexedDB', 'credentials',
442
+ 'geolocation', 'clipboard', 'serviceWorker', 'caches', 'opener',
443
+ ]);
437
444
  const isSafeRead = segments.length >= 2 &&
438
445
  SAFE_GLOBALS.has(segments[0]) &&
439
- segments.every(s => isSimpleIdent.test(s) && !PROTO_DENYLIST.has(s));
446
+ segments.every(s => isSimpleIdent.test(s) && !PROTO_DENYLIST.has(s) && !SENSITIVE_PATHS.has(s));
440
447
 
441
448
  if (!unsafeEvalEnabled && !isSafeRead) {
442
449
  return errorResponse(
@@ -41,11 +41,46 @@ const RESOLVED_BOOTSTRAP_ID = '\0' + VIRTUAL_BOOTSTRAP_ID;
41
41
  // Vite encodes `\0` as `__x00__` in `/@id/` URLs — stable since Vite 2 (2021).
42
42
  const BROWSER_BOOTSTRAP_URL = '/@id/__x00__' + VIRTUAL_BOOTSTRAP_ID;
43
43
 
44
+ // Same-origin discovery endpoint served by the dev server (see configureServer).
45
+ // The browser polls THIS instead of the bridge's cross-origin port directly:
46
+ // a failed cross-origin fetch logs net::ERR_CONNECTION_REFUSED in the console
47
+ // on every fresh scaffold (no bridge running) — unsuppressible from JS. The
48
+ // dev server itself is always up while the page is open, so polling it never
49
+ // produces console noise; the bridge probe happens Node-side instead.
50
+ export const DISCOVERY_PATH = '/__what_mcp_discovery';
51
+
44
52
  export default function whatDevToolsMCP({ port = 9229, token = '' } = {}) {
45
53
  return {
46
54
  name: 'what-devtools-mcp',
47
55
  apply: 'serve',
48
56
 
57
+ // Node-side bridge probe, exposed same-origin to the browser client.
58
+ // Responds { bridge: false } when no bridge is running (quietly), or
59
+ // { bridge: true, token, wsPort } when it is — one round-trip discovery.
60
+ configureServer(server) {
61
+ server.middlewares.use(DISCOVERY_PATH, async (req, res) => {
62
+ res.setHeader('Content-Type', 'application/json');
63
+ res.setHeader('Cache-Control', 'no-store');
64
+ try {
65
+ const probe = await fetch(`http://127.0.0.1:${port + 1}/__what_mcp_token`, {
66
+ signal: AbortSignal.timeout(1000),
67
+ });
68
+ if (probe.ok) {
69
+ const data = await probe.json();
70
+ res.end(JSON.stringify({
71
+ bridge: true,
72
+ token: data.token || '',
73
+ wsPort: data.wsPort || port,
74
+ }));
75
+ return;
76
+ }
77
+ } catch {
78
+ // Bridge not running — the normal state for a fresh scaffold.
79
+ }
80
+ res.end(JSON.stringify({ bridge: false }));
81
+ });
82
+ },
83
+
49
84
  // Resolve the virtual module so Vite knows we own it.
50
85
  resolveId(id) {
51
86
  if (id === VIRTUAL_BOOTSTRAP_ID || id === RESOLVED_BOOTSTRAP_ID) {
@@ -66,7 +101,7 @@ export default function whatDevToolsMCP({ port = 9229, token = '' } = {}) {
66
101
  `import { installDevTools } from 'what-devtools';`,
67
102
  `import { connectDevToolsMCP } from 'what-devtools-mcp/client';`,
68
103
  `installDevTools(core);`,
69
- `connectDevToolsMCP({ port: ${port}, token: ${JSON.stringify(tokenValue)} });`,
104
+ `connectDevToolsMCP({ port: ${port}, token: ${JSON.stringify(tokenValue)}, discoveryUrl: ${JSON.stringify(DISCOVERY_PATH)} });`,
70
105
  ].join('\n');
71
106
  },
72
107