what-devtools-mcp 0.11.7 → 0.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "what-devtools-mcp",
3
- "version": "0.11.7",
3
+ "version": "0.12.0",
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/index.js CHANGED
@@ -9,6 +9,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
10
  import { createBridge } from './bridge.js';
11
11
  import { registerTools } from './tools.js';
12
+ import { instrumentServer } from './tool-registry.js';
12
13
 
13
14
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'));
14
15
 
@@ -52,6 +53,10 @@ const server = new McpServer({
52
53
  version: pkg.version,
53
54
  });
54
55
 
56
+ // Record every registration so what_connection_status can report the real
57
+ // catalogue instead of a hand-maintained literal. Must run before any
58
+ // register* call.
59
+ instrumentServer(server);
55
60
  registerTools(server, bridge);
56
61
 
57
62
  // --- MCP Resources: static docs for agent context ---
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The single source of truth for what tools this MCP server actually exposes.
3
+ *
4
+ * `what_connection_status` is the tool CLAUDE.md tells every agent to call FIRST
5
+ * to orient, and it used to answer with a hand-maintained literal array of 17
6
+ * entries while 29 tools were registered. The product under-reported its own
7
+ * differentiator by 41% at the moment of first contact, and the same catalogue
8
+ * was hand-copied into CLAUDE.md, AGENTS.md and the docs, each free to drift.
9
+ *
10
+ * A framework whose thesis is machine-readable truth cannot hand-maintain its own
11
+ * machine-readable index, so the catalogue is now derived from registration by
12
+ * wrapping `server.tool` once before the three register* functions run.
13
+ */
14
+
15
+ const registered = [];
16
+
17
+ // Tools that answer with no browser attached. Everything else needs a live page
18
+ // on the bridge, which is a stronger requirement than "the dev server is up" and
19
+ // is the single most common reason an agent's first call comes back empty.
20
+ const OFFLINE_TOOLS = new Set([
21
+ 'what_connection_status',
22
+ 'what_lint',
23
+ 'what_validate',
24
+ 'what_scaffold',
25
+ 'what_fix',
26
+ ]);
27
+
28
+ /**
29
+ * Wrap `server.tool` so every registration is recorded. Call once, before any
30
+ * register* function runs. Idempotent.
31
+ */
32
+ export function instrumentServer(server) {
33
+ if (server.__whatToolRegistryInstalled) return server;
34
+ const original = server.tool.bind(server);
35
+ server.tool = (name, description, ...rest) => {
36
+ registered.push({ name, desc: description });
37
+ return original(name, description, ...rest);
38
+ };
39
+ server.__whatToolRegistryInstalled = true;
40
+ return server;
41
+ }
42
+
43
+ /** Every tool registered so far, in registration order. */
44
+ export function getRegisteredTools() {
45
+ return registered.map((t) => ({ ...t }));
46
+ }
47
+
48
+ /**
49
+ * Split the catalogue by what can actually answer right now.
50
+ *
51
+ * No competitor's MCP server distinguishes "this tool exists" from "this tool can
52
+ * answer in this session", which is the distinction that actually costs an agent
53
+ * turns: it discovers the difference by calling and failing.
54
+ */
55
+ export function describeToolAvailability(connected) {
56
+ const all = getRegisteredTools();
57
+ const offline = all.filter((t) => OFFLINE_TOOLS.has(t.name));
58
+ const needsBrowser = all.filter((t) => !OFFLINE_TOOLS.has(t.name));
59
+ return {
60
+ total: all.length,
61
+ available: connected ? all : offline,
62
+ requiresBrowser: connected ? [] : needsBrowser,
63
+ };
64
+ }
65
+
66
+ /** Test hook: drop the recorded registrations. @internal */
67
+ export function __resetToolRegistry() {
68
+ registered.length = 0;
69
+ }
@@ -119,6 +119,34 @@ effect(() => {
119
119
 
120
120
  // Good — stable key:
121
121
  <For each={items()}>{item => <li key={item.id}>{item.name}</li>}</For>`,
122
+ },
123
+ ERR_UNSAFE_REDIRECT: {
124
+ code: 'ERR_UNSAFE_REDIRECT',
125
+ severity: 'error',
126
+ diagnosis: 'redirect() was given a target that can leave your origin: a protocol-relative ("//host"), backslash-smuggled ("/\\host") or javascript:/data: URL. Reaching it from user input is an open redirect.',
127
+ suggestedFix: 'redirect() accepts same-origin paths and http:, https:, mailto: or tel: URLs only. Check a user-supplied target against an allowlist before passing it.',
128
+ codeExample: `// Bad - a user-controlled target can leave your origin:
129
+ redirect(query.next);
130
+
131
+ // Fix - allowlist the target first:
132
+ redirect(ALLOWED.has(query.next) ? query.next : '/');`,
133
+ },
134
+ ERR_REDIRECT_NOT_CAUGHT: {
135
+ code: 'ERR_REDIRECT_NOT_CAUGHT',
136
+ severity: 'error',
137
+ diagnosis: 'A redirect() navigation signal surfaced as an uncaught error, so nothing performed the navigation. redirect() is caught in two places only: route middleware, and a component body. An event handler, a promise callback or a timer runs long after both, and a try/catch around the call swallows the signal.',
138
+ suggestedFix: 'From an event handler, a promise callback or a timer, call navigate(to) instead. If the call is inside a try/catch, rethrow anything whose name is RouterRedirect.',
139
+ codeExample: `// Bad - an event handler runs after the render the Router caught:
140
+ <button onclick={() => redirect('/login')}>Sign in</button>
141
+
142
+ // Fix - navigate() from a handler:
143
+ <button onclick={() => navigate('/login')}>Sign in</button>
144
+
145
+ // Fix - redirect() from a component body, which the Router catches:
146
+ function Private() {
147
+ if (!user()) redirect('/login');
148
+ return <Secret />;
149
+ }`,
122
150
  },
123
151
  HINT_PREFER_COMPUTED: {
124
152
  code: 'HINT_PREFER_COMPUTED',
@@ -1063,9 +1091,25 @@ export function registerAgentTools(server, bridge) {
1063
1091
  'what_fix',
1064
1092
  'Given a What Framework error code, get diagnosis, suggested fix, and code example. Works offline — no browser needed.',
1065
1093
  {
1066
- error: z.string().describe('Error code (e.g., "ERR_INFINITE_EFFECT") or error message text'),
1094
+ error: z.string().optional().describe('Error code (e.g., "ERR_INFINITE_EFFECT") or error message text'),
1095
+ // `errorCode` is an accepted alias, not a second parameter. Every CLAUDE.md
1096
+ // this project has ever scaffolded documents `what_fix {errorCode}` while
1097
+ // the schema only accepted `error`, so the tool the guide calls a "hidden
1098
+ // gem" and tells agents to reach for FIRST returned a hard validation
1099
+ // error. The docs are fixed, but those CLAUDE.md files are already sitting
1100
+ // in users' repos, so the alias stays permanently.
1101
+ errorCode: z.string().optional().describe('Alias for `error`, accepted for compatibility with older scaffolded CLAUDE.md files'),
1067
1102
  },
1068
- async ({ error: errorInput }) => {
1103
+ async ({ error, errorCode }) => {
1104
+ const errorInput = error ?? errorCode;
1105
+ if (!errorInput) {
1106
+ return {
1107
+ content: [{
1108
+ type: 'text',
1109
+ text: 'what_fix needs an error code or message. Example: what_fix({ error: "ERR_INFINITE_EFFECT" }).',
1110
+ }],
1111
+ };
1112
+ }
1069
1113
  // Try exact code match first
1070
1114
  let entry = ERROR_DATABASE[errorInput];
1071
1115
 
package/src/tools.js CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { z } from 'zod';
7
+ import { describeToolAvailability } from './tool-registry.js';
7
8
 
8
9
  export function registerTools(server, bridge) {
9
10
  // --- Helpers ---
@@ -112,26 +113,14 @@ export function registerTools(server, bridge) {
112
113
  'Make sure your app is running with the what-devtools-mcp Vite plugin',
113
114
  'Or manually call connectDevToolsMCP() in your browser console',
114
115
  ],
115
- // Tool catalog so agents know what's available
116
- tools: [
117
- { name: 'what_components', desc: 'List mounted components with IDs' },
118
- { name: 'what_signals', desc: 'List signals with values (use filter!)' },
119
- { name: 'what_effects', desc: 'List effects with deps and run counts' },
120
- { name: 'what_explain', desc: 'Everything about one component (signals + effects + DOM + errors)' },
121
- { name: 'what_look', desc: 'Visual info without image: styles, layout, dimensions' },
122
- { name: 'what_screenshot', desc: 'Cropped component screenshot (5-20KB)' },
123
- { name: 'what_page_map', desc: 'Full page layout skeleton' },
124
- { name: 'what_diagnose', desc: 'One-call health check (errors + perf + reactivity)' },
125
- { name: 'what_errors', desc: 'Runtime errors with fix suggestions' },
126
- { name: 'what_signal_trace', desc: 'Why did a signal change? Causal chain.' },
127
- { name: 'what_dependency_graph', desc: 'Reactive dependency graph' },
128
- { name: 'what_watch', desc: 'Observe events over a time window' },
129
- { name: 'what_record_window', desc: 'Rank effects that re-ran during a recording window — what fired for this action?' },
130
- { name: 'what_set_signal', desc: 'Change a signal value in the live app' },
131
- { name: 'what_lint', desc: 'Static analysis for code (no browser needed)' },
132
- { name: 'what_scaffold', desc: 'Generate boilerplate (no browser needed)' },
133
- { name: 'what_fix', desc: 'Error diagnosis with code examples (no browser needed)' },
134
- ],
116
+ // Tool catalogue, DERIVED from registration rather than hand-maintained.
117
+ // `tools` is every registered tool; `available` is the subset that can
118
+ // answer right now, which is the distinction that actually costs an
119
+ // agent turns when no browser is attached.
120
+ ...(() => {
121
+ const a = describeToolAvailability(connected);
122
+ return { toolCount: a.total, tools: a.available, requiresBrowser: a.requiresBrowser.map((t) => t.name) };
123
+ })(),
135
124
  };
136
125
 
137
126
  return {