what-devtools-mcp 0.11.0 → 0.11.2

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.11.0",
3
+ "version": "0.11.2",
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/client.js CHANGED
@@ -21,7 +21,7 @@ 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
27
  return { disconnect() {}, reconnect() {}, isConnected: false, eventCount: 0 };
@@ -57,15 +57,43 @@ export function connectDevToolsMCP({ port = 9229, token = '' } = {}) {
57
57
  // WebSocket connection always prints an unsuppressible red error in the
58
58
  // browser console; a failed fetch wrapped in catch() prints at most a single
59
59
  // muted "Failed to load resource" network line — the quietest probe the
60
- // platform allows. The bridge serves GET http://localhost:{port+1}/__what_mcp_token
61
- // (loopback-origin gated), so a 200 here means the bridge is up AND gives us
62
- // the token + actual WS port in one round-trip.
60
+ // platform allows.
63
61
  //
64
- // Trade-off: if the discovery HTTP port (port+1) is occupied by another
65
- // process while the WS port is free, we won't connect even with an explicit
66
- // token. That edge case is rarer and cheaper than spamming every fresh app
67
- // (the default state: no bridge running) with red WS errors.
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.
68
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
+ }
69
97
  try {
70
98
  const res = await fetch(`http://localhost:${port + 1}/__what_mcp_token`, { cache: 'no-store' });
71
99
  if (res.ok) {
@@ -351,7 +379,8 @@ export function connectDevToolsMCP({ port = 9229, token = '' } = {}) {
351
379
 
352
380
  // Manual retry escape hatch — resets the back-off and probes immediately.
353
381
  // Useful when the dev just started the bridge and doesn't want to wait
354
- // out the back-off (or reload the page).
382
+ // out the back-off (or reload the page). Also the opt-in for clients that
383
+ // start dormant (see pollEnabled below).
355
384
  function reconnect() {
356
385
  if (stopped || connected) return;
357
386
  if (probeTimer) {
@@ -365,8 +394,22 @@ export function connectDevToolsMCP({ port = 9229, token = '' } = {}) {
365
394
  window.__WHAT_MCP_RECONNECT__ = reconnect;
366
395
  }
367
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
+
368
411
  // Initial attempt — quiet probe first; WebSocket only if the bridge answers.
369
- tryConnect();
412
+ if (pollEnabled) tryConnect();
370
413
 
371
414
  return {
372
415
  disconnect,
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);
@@ -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