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 +12 -0
- package/package.json +1 -1
- package/src/client.js +53 -10
- package/src/index.js +35 -1
- package/src/vite-plugin.js +36 -1
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
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.
|
|
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
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
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:
|
|
52
|
+
version: pkg.version,
|
|
19
53
|
});
|
|
20
54
|
|
|
21
55
|
registerTools(server, bridge);
|
package/src/vite-plugin.js
CHANGED
|
@@ -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
|
|