@vscode/component-explorer-cli 0.1.1-6 → 0.1.1-8
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 +4 -5
- package/dist/commands/compareCommand.d.ts +0 -3
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +18 -210
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/mcpCommand.d.ts +2 -0
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +35 -6
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/screenshotCommand.d.ts.map +1 -1
- package/dist/commands/screenshotCommand.js +21 -75
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +3 -0
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +52 -3
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/comparison.d.ts +60 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +250 -0
- package/dist/comparison.js.map +1 -0
- package/dist/component-explorer-config.schema.json +183 -0
- package/dist/componentExplorer.d.ts +13 -0
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +65 -2
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonService.d.ts +55 -4
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +139 -8
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/lifecycle.js +3 -3
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/pipeClient.js +3 -3
- package/dist/daemon/pipeClient.js.map +1 -1
- package/dist/daemon/pipeServer.js +2 -2
- package/dist/daemon/pipeServer.js.map +1 -1
- package/dist/dependencyInstaller.js +1 -1
- package/dist/dependencyInstaller.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/disposables.js +24 -1
- package/dist/external/vscode-observables/observables/dist/disposables.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js +1 -4
- package/dist/external/vscode-observables/observables/dist/observableInternal/commonFacade/deps.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/index.js +2 -5
- package/dist/external/vscode-observables/observables/dist/observableInternal/index.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js +30 -6
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/consoleObservableLogger.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/baseObservable.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derived.js +12 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/observables/derived.js.map +1 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js +55 -0
- package/dist/external/vscode-observables/observables/dist/observableInternal/utils/utilsCancellation.js.map +1 -0
- package/dist/git/gitUtils.js +1 -1
- package/dist/git/gitUtils.js.map +1 -1
- package/dist/mcp/McpServer.d.ts +30 -4
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +435 -80
- package/dist/mcp/McpServer.js.map +1 -1
- package/dist/packages/simple-api/dist/{chunk-A5PE72HI.js → chunk-Q24JOMNK.js} +7 -1
- package/dist/packages/simple-api/dist/chunk-Q24JOMNK.js.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +50 -0
- package/dist/utils.js.map +1 -0
- package/dist/watchConfig.d.ts +36 -0
- package/dist/watchConfig.d.ts.map +1 -1
- package/dist/watchConfig.js +32 -22
- package/dist/watchConfig.js.map +1 -1
- package/package.json +6 -4
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/debuggerRpc.js +0 -72
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/debuggerRpc.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/devToolsLogger.js +0 -447
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/devToolsLogger.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/rpc.js +0 -64
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/rpc.js.map +0 -1
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/utils.js +0 -52
- package/dist/external/vscode-observables/observables/dist/observableInternal/logging/debugger/utils.js.map +0 -1
- package/dist/packages/simple-api/dist/chunk-A5PE72HI.js.map +0 -1
package/dist/mcp/McpServer.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import '../external/vscode-observables/observables/dist/observableInternal/index.js';
|
|
5
|
+
import { Disposable } from '../external/vscode-observables/observables/dist/disposables.js';
|
|
6
|
+
import '../external/vscode-observables/observables/dist/observableInternal/debugLocation.js';
|
|
7
|
+
import { autorun } from '../external/vscode-observables/observables/dist/observableInternal/reactions/autorun.js';
|
|
8
|
+
import '../external/vscode-observables/observables/dist/observableInternal/observables/derived.js';
|
|
9
|
+
import '../external/vscode-observables/observables/dist/observableInternal/utils/utils.js';
|
|
10
|
+
import '../external/vscode-observables/observables/dist/observableInternal/observables/observableFromEvent.js';
|
|
11
|
+
import { buildExplorerUrl } from '../utils.js';
|
|
4
12
|
|
|
5
13
|
// ---------------------------------------------------------------------------
|
|
6
14
|
// Client-local state
|
|
@@ -40,43 +48,141 @@ class WatchList {
|
|
|
40
48
|
};
|
|
41
49
|
}
|
|
42
50
|
}
|
|
51
|
+
function noDaemonError(hint) {
|
|
52
|
+
let text = 'Error: No daemon is currently running.';
|
|
53
|
+
if (hint) {
|
|
54
|
+
text += ` ${hint}`;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
text += ' Please start the Component Explorer daemon first by running:\n\n' +
|
|
58
|
+
' component-explorer serve --project <config.json>\n\n' +
|
|
59
|
+
'Or start it in the background:\n\n' +
|
|
60
|
+
' component-explorer serve --project <config.json> --background\n\n' +
|
|
61
|
+
'The daemon manages dev servers and enables fixture screenshots.';
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: 'text', text }],
|
|
65
|
+
isError: true,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// DaemonConnection - wrapper to avoid Proxy issues with observables
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
class DaemonConnection {
|
|
72
|
+
client;
|
|
73
|
+
constructor(client) {
|
|
74
|
+
this.client = client;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
43
77
|
// ---------------------------------------------------------------------------
|
|
44
78
|
// ComponentExplorerMcpServer
|
|
45
79
|
// ---------------------------------------------------------------------------
|
|
46
|
-
class ComponentExplorerMcpServer {
|
|
47
|
-
|
|
80
|
+
class ComponentExplorerMcpServer extends Disposable {
|
|
81
|
+
_daemonConnection;
|
|
82
|
+
static async create(daemon, options) {
|
|
83
|
+
const server = new ComponentExplorerMcpServer(daemon, options ?? {});
|
|
84
|
+
const transport = new StdioServerTransport();
|
|
85
|
+
await server._mcp.connect(transport);
|
|
86
|
+
return server;
|
|
87
|
+
}
|
|
48
88
|
_mcp;
|
|
49
89
|
_watchList = new WatchList();
|
|
90
|
+
_pollFn;
|
|
91
|
+
_noAutostartHint;
|
|
92
|
+
_multiSessionTools = [];
|
|
50
93
|
_sessions = [];
|
|
51
|
-
|
|
52
|
-
|
|
94
|
+
_eventStreamAbortController;
|
|
95
|
+
constructor(_daemonConnection, options) {
|
|
96
|
+
super();
|
|
97
|
+
this._daemonConnection = _daemonConnection;
|
|
98
|
+
this._pollFn = options.pollFn;
|
|
99
|
+
this._noAutostartHint = options.noAutostartHint;
|
|
53
100
|
this._mcp = new McpServer({
|
|
54
101
|
name: 'component-explorer',
|
|
55
102
|
version: '0.1.0',
|
|
56
103
|
});
|
|
57
104
|
this._registerTools();
|
|
105
|
+
this._store.add(autorun(async (reader) => {
|
|
106
|
+
const conn = this._daemonConnection.read(reader);
|
|
107
|
+
await this._onDaemonChanged(conn?.client);
|
|
108
|
+
}));
|
|
58
109
|
}
|
|
59
|
-
async
|
|
60
|
-
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
async _onDaemonChanged(daemon) {
|
|
111
|
+
// Cancel any existing event stream
|
|
112
|
+
this._eventStreamAbortController?.abort();
|
|
113
|
+
this._eventStreamAbortController = undefined;
|
|
114
|
+
if (!daemon) {
|
|
115
|
+
this._sessions = [];
|
|
116
|
+
this._log('info', { type: 'daemon-disconnected' });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Fetch sessions and start event listener
|
|
120
|
+
try {
|
|
121
|
+
this._sessions = await daemon.methods.sessions();
|
|
122
|
+
this._log('info', { type: 'daemon-connected', sessions: this._sessions.length });
|
|
123
|
+
this._updateMultiSessionToolVisibility();
|
|
124
|
+
this._startEventListener(daemon);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
this._log('info', { type: 'daemon-error', error: String(e) });
|
|
128
|
+
this._sessions = [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
_getDaemon() {
|
|
132
|
+
return this._daemonConnection.get()?.client;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* If no daemon is connected and we have a pollFn, poll immediately and wait
|
|
136
|
+
* up to 3 seconds for the daemon to become available.
|
|
137
|
+
*/
|
|
138
|
+
async _waitForDaemon() {
|
|
139
|
+
let daemon = this._getDaemon();
|
|
140
|
+
if (daemon || !this._pollFn) {
|
|
141
|
+
return daemon;
|
|
142
|
+
}
|
|
143
|
+
// Poll immediately and wait up to 3 seconds
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
const timeout = 3000;
|
|
146
|
+
while (Date.now() - startTime < timeout) {
|
|
147
|
+
await this._pollFn();
|
|
148
|
+
daemon = this._getDaemon();
|
|
149
|
+
if (daemon) {
|
|
150
|
+
return daemon;
|
|
151
|
+
}
|
|
152
|
+
// Wait 200ms before next poll
|
|
153
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
_noDaemonError() {
|
|
158
|
+
return noDaemonError(this._noAutostartHint);
|
|
64
159
|
}
|
|
65
160
|
_log(level, data) {
|
|
66
161
|
this._mcp.sendLoggingMessage({ level, logger: 'daemon', data }).catch(() => { });
|
|
67
162
|
}
|
|
68
|
-
|
|
69
|
-
const
|
|
163
|
+
_startEventListener(daemon) {
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
this._eventStreamAbortController = controller;
|
|
70
166
|
(async () => {
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
167
|
+
try {
|
|
168
|
+
const stream = await daemon.methods.events();
|
|
169
|
+
for await (const raw of stream) {
|
|
170
|
+
if (controller.signal.aborted)
|
|
171
|
+
break;
|
|
172
|
+
const event = raw;
|
|
173
|
+
if (event.type === 'source-change' && event.sessionName && event.sourceTreeId) {
|
|
174
|
+
this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
|
|
175
|
+
}
|
|
176
|
+
if (event.type === 'ref-change') {
|
|
177
|
+
await this._refreshSessions();
|
|
178
|
+
}
|
|
179
|
+
this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
|
|
75
180
|
}
|
|
76
|
-
|
|
77
|
-
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
if (!controller.signal.aborted) {
|
|
184
|
+
this._log('info', { type: 'event-stream-error', error: String(e) });
|
|
78
185
|
}
|
|
79
|
-
this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
|
|
80
186
|
}
|
|
81
187
|
})();
|
|
82
188
|
}
|
|
@@ -94,7 +200,7 @@ class ComponentExplorerMcpServer {
|
|
|
94
200
|
}
|
|
95
201
|
_sourceTreeId(sessionName) {
|
|
96
202
|
const s = this._sessions.find(s => s.name === sessionName);
|
|
97
|
-
return s
|
|
203
|
+
return s && !s.isLoading ? s.sourceTreeId : '';
|
|
98
204
|
}
|
|
99
205
|
_updateSessionSourceTreeId(sessionName, sourceTreeId) {
|
|
100
206
|
const s = this._sessions.find(s => s.name === sessionName);
|
|
@@ -103,7 +209,25 @@ class ComponentExplorerMcpServer {
|
|
|
103
209
|
}
|
|
104
210
|
}
|
|
105
211
|
async _refreshSessions() {
|
|
106
|
-
|
|
212
|
+
const daemon = this._getDaemon();
|
|
213
|
+
if (daemon) {
|
|
214
|
+
const prevCount = this._sessions.length;
|
|
215
|
+
this._sessions = await daemon.methods.sessions();
|
|
216
|
+
if (this._sessions.length !== prevCount) {
|
|
217
|
+
this._updateMultiSessionToolVisibility();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
_updateMultiSessionToolVisibility() {
|
|
222
|
+
const isMultiSession = this._sessions.length > 1;
|
|
223
|
+
for (const tool of this._multiSessionTools) {
|
|
224
|
+
if (isMultiSession) {
|
|
225
|
+
tool.enable();
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
tool.disable();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
107
231
|
}
|
|
108
232
|
async _withSourceTreeRetry(fn) {
|
|
109
233
|
try {
|
|
@@ -124,12 +248,15 @@ class ComponentExplorerMcpServer {
|
|
|
124
248
|
this._registerScreenshot();
|
|
125
249
|
this._registerCompareScreenshot();
|
|
126
250
|
this._registerApproveDiff();
|
|
251
|
+
this._registerEvaluateJs();
|
|
252
|
+
this._registerDebugReloadPage();
|
|
127
253
|
this._registerWatchAdd();
|
|
128
254
|
this._registerWatchRemove();
|
|
129
255
|
this._registerWatchSet();
|
|
130
256
|
this._registerWatchCompare();
|
|
131
257
|
this._registerWaitForUpdate();
|
|
132
258
|
this._registerSessions();
|
|
259
|
+
this._registerGetUrl();
|
|
133
260
|
}
|
|
134
261
|
_registerListFixtures() {
|
|
135
262
|
this._mcp.registerTool('list_fixtures', {
|
|
@@ -138,11 +265,16 @@ class ComponentExplorerMcpServer {
|
|
|
138
265
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
139
266
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
140
267
|
},
|
|
268
|
+
annotations: { readOnlyHint: true },
|
|
141
269
|
}, async (args) => {
|
|
270
|
+
const daemon = await this._waitForDaemon();
|
|
271
|
+
if (!daemon) {
|
|
272
|
+
return this._noDaemonError();
|
|
273
|
+
}
|
|
142
274
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
143
275
|
return this._withSourceTreeRetry(async () => {
|
|
144
276
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
145
|
-
const fixtures = await
|
|
277
|
+
const fixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
146
278
|
return {
|
|
147
279
|
content: [{ type: 'text', text: JSON.stringify(fixtures, null, 2) }],
|
|
148
280
|
};
|
|
@@ -151,21 +283,29 @@ class ComponentExplorerMcpServer {
|
|
|
151
283
|
}
|
|
152
284
|
_registerScreenshot() {
|
|
153
285
|
this._mcp.registerTool('screenshot', {
|
|
154
|
-
description: 'Take a screenshot of a single fixture'
|
|
286
|
+
description: 'Take a screenshot of a single fixture. ' +
|
|
287
|
+
'When stabilityCheck is true, the fixture is unmounted and re-mounted, then three screenshots are taken. ',
|
|
155
288
|
inputSchema: {
|
|
156
289
|
fixtureId: z.string().describe('The fixture ID'),
|
|
157
290
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
158
291
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
292
|
+
stabilityCheck: z.boolean().optional().describe('If true, takes three screenshots (at render time, ~500ms, ~3000ms) to check visual stability. Expensive (~3s extra) — only use it when you need to verify rendering stability..'),
|
|
159
293
|
},
|
|
294
|
+
annotations: { readOnlyHint: true },
|
|
160
295
|
}, async (args) => {
|
|
296
|
+
const daemon = await this._waitForDaemon();
|
|
297
|
+
if (!daemon) {
|
|
298
|
+
return this._noDaemonError();
|
|
299
|
+
}
|
|
161
300
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
162
301
|
return this._withSourceTreeRetry(async () => {
|
|
163
302
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
164
|
-
const result = await
|
|
303
|
+
const result = await daemon.methods.screenshots.take({
|
|
165
304
|
fixtureId: args.fixtureId,
|
|
166
305
|
sessionName,
|
|
167
306
|
sourceTreeId,
|
|
168
307
|
includeImage: true,
|
|
308
|
+
stabilityCheck: args.stabilityCheck,
|
|
169
309
|
});
|
|
170
310
|
const r = result;
|
|
171
311
|
this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
|
|
@@ -176,18 +316,34 @@ class ComponentExplorerMcpServer {
|
|
|
176
316
|
if (r.errors && r.errors.length > 0) {
|
|
177
317
|
info.errors = r.errors;
|
|
178
318
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
319
|
+
if (r.isStable !== undefined) {
|
|
320
|
+
info.isStable = r.isStable;
|
|
321
|
+
}
|
|
322
|
+
const content = [];
|
|
323
|
+
if (r.isStable === false && r.stabilityScreenshots) {
|
|
324
|
+
// Not stable: return all distinct screenshots
|
|
325
|
+
const seenHashes = new Set();
|
|
326
|
+
const screenshotDetails = [];
|
|
327
|
+
for (const s of r.stabilityScreenshots) {
|
|
328
|
+
screenshotDetails.push({ hash: s.hash, delayMs: s.delayMs });
|
|
329
|
+
if (!seenHashes.has(s.hash) && s.image) {
|
|
330
|
+
seenHashes.add(s.hash);
|
|
331
|
+
content.push({ type: 'image', data: s.image, mimeType: 'image/png' });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
info.stabilityScreenshots = screenshotDetails;
|
|
335
|
+
}
|
|
336
|
+
else if (r.image) {
|
|
337
|
+
// Stable or no stability check: return single image
|
|
183
338
|
content.push({ type: 'image', data: r.image, mimeType: 'image/png' });
|
|
184
339
|
}
|
|
340
|
+
content.unshift({ type: 'text', text: JSON.stringify(info, null, 2) });
|
|
185
341
|
return { content };
|
|
186
342
|
});
|
|
187
343
|
});
|
|
188
344
|
}
|
|
189
345
|
_registerCompareScreenshot() {
|
|
190
|
-
this._mcp.registerTool('compare_screenshot', {
|
|
346
|
+
const tool = this._mcp.registerTool('compare_screenshot', {
|
|
191
347
|
description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
|
|
192
348
|
inputSchema: {
|
|
193
349
|
fixtureId: z.string().describe('The fixture ID'),
|
|
@@ -196,13 +352,18 @@ class ComponentExplorerMcpServer {
|
|
|
196
352
|
baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
|
|
197
353
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
198
354
|
},
|
|
355
|
+
annotations: { readOnlyHint: true },
|
|
199
356
|
}, async (args) => {
|
|
357
|
+
const daemon = await this._waitForDaemon();
|
|
358
|
+
if (!daemon) {
|
|
359
|
+
return this._noDaemonError();
|
|
360
|
+
}
|
|
200
361
|
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
201
362
|
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
202
363
|
return this._withSourceTreeRetry(async () => {
|
|
203
364
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
204
365
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
205
|
-
const result = await
|
|
366
|
+
const result = await daemon.methods.screenshots.compare({
|
|
206
367
|
fixtureId: args.fixtureId,
|
|
207
368
|
baselineSessionName,
|
|
208
369
|
baselineSourceTreeId,
|
|
@@ -237,9 +398,11 @@ class ComponentExplorerMcpServer {
|
|
|
237
398
|
return { content };
|
|
238
399
|
});
|
|
239
400
|
});
|
|
401
|
+
tool.disable();
|
|
402
|
+
this._multiSessionTools.push(tool);
|
|
240
403
|
}
|
|
241
404
|
_registerApproveDiff() {
|
|
242
|
-
this._mcp.registerTool('approve_diff', {
|
|
405
|
+
const tool = this._mcp.registerTool('approve_diff', {
|
|
243
406
|
description: 'Approve a visual diff so it won\'t require re-inspection next time',
|
|
244
407
|
inputSchema: {
|
|
245
408
|
fixtureId: z.string(),
|
|
@@ -248,7 +411,11 @@ class ComponentExplorerMcpServer {
|
|
|
248
411
|
comment: z.string().describe('Reason for approving this diff'),
|
|
249
412
|
},
|
|
250
413
|
}, async (args) => {
|
|
251
|
-
await this.
|
|
414
|
+
const daemon = await this._waitForDaemon();
|
|
415
|
+
if (!daemon) {
|
|
416
|
+
return this._noDaemonError();
|
|
417
|
+
}
|
|
418
|
+
await daemon.methods.approvals.approve(args);
|
|
252
419
|
return {
|
|
253
420
|
content: [{
|
|
254
421
|
type: 'text',
|
|
@@ -256,6 +423,74 @@ class ComponentExplorerMcpServer {
|
|
|
256
423
|
}],
|
|
257
424
|
};
|
|
258
425
|
});
|
|
426
|
+
tool.disable();
|
|
427
|
+
this._multiSessionTools.push(tool);
|
|
428
|
+
}
|
|
429
|
+
_registerEvaluateJs() {
|
|
430
|
+
this._mcp.registerTool('evaluate_js', {
|
|
431
|
+
description: 'Evaluate a JavaScript expression in the browser page where fixtures are rendered, for debugging purposes. ' +
|
|
432
|
+
'Returns the expression result as JSON. The expression can return a Promise (it will be awaited). ' +
|
|
433
|
+
'Use this to inspect DOM state, computed styles, element dimensions, or component output. ' +
|
|
434
|
+
'Do NOT use this to modify the DOM — this tool is for read-only inspection and debugging only.',
|
|
435
|
+
inputSchema: {
|
|
436
|
+
expression: z.string().describe('JavaScript expression to evaluate. Can return a Promise. The result must be JSON-serializable.'),
|
|
437
|
+
fixtureId: z.string().optional().describe('If provided, renders this fixture before evaluating the expression'),
|
|
438
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
439
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
440
|
+
},
|
|
441
|
+
}, async (args) => {
|
|
442
|
+
const daemon = await this._waitForDaemon();
|
|
443
|
+
if (!daemon) {
|
|
444
|
+
return this._noDaemonError();
|
|
445
|
+
}
|
|
446
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
447
|
+
return this._withSourceTreeRetry(async () => {
|
|
448
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
449
|
+
const result = await daemon.methods.evaluate({
|
|
450
|
+
sessionName,
|
|
451
|
+
sourceTreeId,
|
|
452
|
+
expression: args.expression,
|
|
453
|
+
fixtureId: args.fixtureId,
|
|
454
|
+
});
|
|
455
|
+
const r = result;
|
|
456
|
+
let text;
|
|
457
|
+
try {
|
|
458
|
+
text = JSON.stringify(r.result, null, 2) ?? 'undefined';
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
text = String(r.result);
|
|
462
|
+
}
|
|
463
|
+
return {
|
|
464
|
+
content: [{ type: 'text', text }],
|
|
465
|
+
};
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
_registerDebugReloadPage() {
|
|
470
|
+
this._mcp.registerTool('debug_reload_page', {
|
|
471
|
+
description: 'Force-reload the browser page used for rendering fixtures. ' +
|
|
472
|
+
'Only use this as a last resort if screenshots or evaluate_js return stale/broken results ' +
|
|
473
|
+
'that persist after source changes. Normal HMR updates should handle most cases automatically.',
|
|
474
|
+
inputSchema: {
|
|
475
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
476
|
+
},
|
|
477
|
+
annotations: { destructiveHint: true },
|
|
478
|
+
}, async (args) => {
|
|
479
|
+
const daemon = await this._waitForDaemon();
|
|
480
|
+
if (!daemon) {
|
|
481
|
+
return this._noDaemonError();
|
|
482
|
+
}
|
|
483
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
484
|
+
const sourceTreeId = this._sourceTreeId(sessionName);
|
|
485
|
+
await daemon.methods.evaluate({
|
|
486
|
+
sessionName,
|
|
487
|
+
sourceTreeId,
|
|
488
|
+
expression: 'location.reload()',
|
|
489
|
+
});
|
|
490
|
+
return {
|
|
491
|
+
content: [{ type: 'text', text: `Reloaded page for session '${sessionName}'.` }],
|
|
492
|
+
};
|
|
493
|
+
});
|
|
259
494
|
}
|
|
260
495
|
_registerWatchAdd() {
|
|
261
496
|
this._mcp.registerTool('watch_add', {
|
|
@@ -306,7 +541,7 @@ class ComponentExplorerMcpServer {
|
|
|
306
541
|
});
|
|
307
542
|
}
|
|
308
543
|
_registerWatchCompare() {
|
|
309
|
-
this._mcp.registerTool('watch_compare', {
|
|
544
|
+
const tool = this._mcp.registerTool('watch_compare', {
|
|
310
545
|
description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
|
|
311
546
|
inputSchema: {
|
|
312
547
|
baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
|
|
@@ -314,7 +549,12 @@ class ComponentExplorerMcpServer {
|
|
|
314
549
|
baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
|
|
315
550
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
316
551
|
},
|
|
552
|
+
annotations: { readOnlyHint: true },
|
|
317
553
|
}, async (args) => {
|
|
554
|
+
const daemon = await this._waitForDaemon();
|
|
555
|
+
if (!daemon) {
|
|
556
|
+
return this._noDaemonError();
|
|
557
|
+
}
|
|
318
558
|
const ids = [...this._watchList.fixtureIds];
|
|
319
559
|
if (ids.length === 0) {
|
|
320
560
|
return { content: [{ type: 'text', text: 'Watch list is empty. Use watch_add or watch_set first.' }] };
|
|
@@ -325,12 +565,12 @@ class ComponentExplorerMcpServer {
|
|
|
325
565
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
326
566
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
327
567
|
const [baselineResult, currentResult] = await Promise.all([
|
|
328
|
-
|
|
568
|
+
daemon.methods.screenshots.takeBatch({
|
|
329
569
|
fixtureIds: ids,
|
|
330
570
|
sessionName: baselineSessionName,
|
|
331
571
|
sourceTreeId: baselineSourceTreeId,
|
|
332
572
|
}),
|
|
333
|
-
|
|
573
|
+
daemon.methods.screenshots.takeBatch({
|
|
334
574
|
fixtureIds: ids,
|
|
335
575
|
sessionName: currentSessionName,
|
|
336
576
|
sourceTreeId: currentSourceTreeId,
|
|
@@ -355,7 +595,7 @@ class ComponentExplorerMcpServer {
|
|
|
355
595
|
for (const entry of entries) {
|
|
356
596
|
let approval = undefined;
|
|
357
597
|
if (!entry.match && entry.baselineHash && entry.currentHash) {
|
|
358
|
-
approval = await
|
|
598
|
+
approval = await daemon.methods.approvals.lookup({
|
|
359
599
|
fixtureId: entry.fixtureId,
|
|
360
600
|
originalHash: entry.baselineHash,
|
|
361
601
|
modifiedHash: entry.currentHash,
|
|
@@ -368,72 +608,187 @@ class ComponentExplorerMcpServer {
|
|
|
368
608
|
};
|
|
369
609
|
});
|
|
370
610
|
});
|
|
611
|
+
tool.disable();
|
|
612
|
+
this._multiSessionTools.push(tool);
|
|
371
613
|
}
|
|
372
614
|
_registerWaitForUpdate() {
|
|
373
615
|
this._mcp.registerTool('wait_for_update', {
|
|
374
|
-
description: 'Block until the
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
616
|
+
description: 'Block until the source tree changes from the given sourceTreeId. ' +
|
|
617
|
+
'Pass the sourceTreeId you already observed — resolves immediately if it already differs, ' +
|
|
618
|
+
'otherwise waits for a source-change or ref-change event. ' +
|
|
619
|
+
'If fixtures are on the watch list, automatically re-screenshots them and reports which changed.',
|
|
620
|
+
inputSchema: {
|
|
621
|
+
sourceTreeId: z.string().describe('The sourceTreeId the client currently knows about. The call resolves once the source tree differs from this value.'),
|
|
622
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
623
|
+
},
|
|
624
|
+
annotations: { readOnlyHint: true },
|
|
625
|
+
}, async (args) => {
|
|
626
|
+
const daemon = await this._waitForDaemon();
|
|
627
|
+
if (!daemon) {
|
|
628
|
+
return this._noDaemonError();
|
|
388
629
|
}
|
|
389
|
-
|
|
390
|
-
|
|
630
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
631
|
+
const knownSourceTreeId = args.sourceTreeId;
|
|
632
|
+
// Check if already changed
|
|
633
|
+
const currentSourceTreeId = this._sourceTreeId(sessionName);
|
|
634
|
+
if (currentSourceTreeId && currentSourceTreeId !== knownSourceTreeId) {
|
|
635
|
+
return this._waitForUpdateResult(daemon, sessionName, currentSourceTreeId);
|
|
391
636
|
}
|
|
392
|
-
//
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
637
|
+
// Wait for an event that changes the source tree (max 5s)
|
|
638
|
+
const events = await daemon.methods.events();
|
|
639
|
+
const iterator = events[Symbol.asyncIterator]();
|
|
640
|
+
try {
|
|
641
|
+
const timeout = new Promise(resolve => setTimeout(() => resolve('timeout'), 5000));
|
|
642
|
+
while (true) {
|
|
643
|
+
const next = iterator.next();
|
|
644
|
+
const result = await Promise.race([next, timeout]);
|
|
645
|
+
if (result === 'timeout') {
|
|
646
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
647
|
+
}
|
|
648
|
+
const { value: event, done } = result;
|
|
649
|
+
if (done || !event) {
|
|
650
|
+
return { content: [{ type: 'text', text: 'Event stream ended.' }] };
|
|
651
|
+
}
|
|
652
|
+
const ev = event;
|
|
653
|
+
if (ev.type === 'source-change' && ev.sourceTreeId) {
|
|
654
|
+
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
655
|
+
}
|
|
656
|
+
if (ev.type === 'ref-change') {
|
|
657
|
+
await this._refreshSessions();
|
|
658
|
+
}
|
|
659
|
+
const newSourceTreeId = this._sourceTreeId(sessionName);
|
|
660
|
+
if (newSourceTreeId && newSourceTreeId !== knownSourceTreeId) {
|
|
661
|
+
return this._waitForUpdateResult(daemon, sessionName, newSourceTreeId);
|
|
408
662
|
}
|
|
409
663
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
664
|
+
}
|
|
665
|
+
finally {
|
|
666
|
+
await iterator.return?.();
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
async _waitForUpdateResult(daemon, sessionName, sourceTreeId) {
|
|
671
|
+
const watchedIds = [...this._watchList.fixtureIds];
|
|
672
|
+
if (watchedIds.length > 0) {
|
|
673
|
+
const batchResult = await daemon.methods.screenshots.takeBatch({
|
|
674
|
+
fixtureIds: watchedIds,
|
|
675
|
+
sessionName,
|
|
676
|
+
sourceTreeId,
|
|
677
|
+
});
|
|
678
|
+
const br = batchResult;
|
|
679
|
+
const changes = [];
|
|
680
|
+
for (const s of br.screenshots) {
|
|
681
|
+
const prevHash = this._watchList.getHash(s.fixtureId);
|
|
682
|
+
const changed = prevHash !== undefined && prevHash !== s.hash;
|
|
683
|
+
this._watchList.setHash(s.fixtureId, s.hash);
|
|
684
|
+
if (changed) {
|
|
685
|
+
changes.push({ fixtureId: s.fixtureId, previousHash: prevHash, hash: s.hash });
|
|
686
|
+
}
|
|
420
687
|
}
|
|
421
688
|
return {
|
|
422
|
-
content: [{
|
|
689
|
+
content: [{
|
|
690
|
+
type: 'text',
|
|
691
|
+
text: JSON.stringify({
|
|
692
|
+
sourceTreeId,
|
|
693
|
+
sessionName,
|
|
694
|
+
watchedFixtures: br.screenshots.length,
|
|
695
|
+
changed: changes,
|
|
696
|
+
}, null, 2),
|
|
697
|
+
}],
|
|
423
698
|
};
|
|
424
|
-
}
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
content: [{
|
|
702
|
+
type: 'text',
|
|
703
|
+
text: JSON.stringify({ sourceTreeId, sessionName }, null, 2),
|
|
704
|
+
}],
|
|
705
|
+
};
|
|
425
706
|
}
|
|
426
707
|
_registerSessions() {
|
|
427
708
|
this._mcp.registerTool('sessions', {
|
|
428
709
|
description: 'List active sessions with their names, URLs, and current sourceTreeIds',
|
|
710
|
+
annotations: { readOnlyHint: true },
|
|
429
711
|
}, async () => {
|
|
712
|
+
const daemon = await this._waitForDaemon();
|
|
713
|
+
if (!daemon) {
|
|
714
|
+
return this._noDaemonError();
|
|
715
|
+
}
|
|
430
716
|
await this._refreshSessions();
|
|
431
717
|
return {
|
|
432
718
|
content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
|
|
433
719
|
};
|
|
434
720
|
});
|
|
435
721
|
}
|
|
722
|
+
_registerGetUrl() {
|
|
723
|
+
this._mcp.registerTool('get_url', {
|
|
724
|
+
description: 'Get URL(s) for viewing fixtures. Returns the Component Explorer UI URL by default, ' +
|
|
725
|
+
'or the raw render URL for embedding/screenshots when useRawDirectRenderingWithoutExplorerUi is true.',
|
|
726
|
+
inputSchema: {
|
|
727
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
728
|
+
fixtureId: z.string().optional().describe('Specific fixture ID. If omitted, returns URL for the explorer root or all fixtures.'),
|
|
729
|
+
useRawDirectRenderingWithoutExplorerUi: z.boolean().optional().describe('If true, returns the raw rendering URL (for embedding or screenshots) instead of the Explorer UI URL. ' +
|
|
730
|
+
'The raw URL renders only the fixture without the explorer chrome. Default: false.'),
|
|
731
|
+
},
|
|
732
|
+
annotations: { readOnlyHint: true },
|
|
733
|
+
}, async (args) => {
|
|
734
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
735
|
+
const session = this._sessions.find(s => s.name === sessionName);
|
|
736
|
+
if (!session) {
|
|
737
|
+
// Try to refresh sessions if we don't have the requested session
|
|
738
|
+
const daemon = await this._waitForDaemon();
|
|
739
|
+
if (daemon) {
|
|
740
|
+
await this._refreshSessions();
|
|
741
|
+
}
|
|
742
|
+
const refreshedSession = this._sessions.find(s => s.name === sessionName);
|
|
743
|
+
if (!refreshedSession) {
|
|
744
|
+
return {
|
|
745
|
+
content: [{
|
|
746
|
+
type: 'text',
|
|
747
|
+
text: `Error: Session '${sessionName}' not found. Available sessions: ${this._sessions.map(s => s.name).join(', ') || '(none)'}`,
|
|
748
|
+
}],
|
|
749
|
+
isError: true,
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const resolved = session ?? this._sessions.find(s => s.name === sessionName);
|
|
754
|
+
const baseUrl = resolved && !resolved.isLoading ? resolved.serverUrl : undefined;
|
|
755
|
+
if (!baseUrl) {
|
|
756
|
+
return {
|
|
757
|
+
content: [{
|
|
758
|
+
type: 'text',
|
|
759
|
+
text: `Error: Session '${sessionName}' is still loading.`,
|
|
760
|
+
}],
|
|
761
|
+
isError: true,
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
const useRaw = args.useRawDirectRenderingWithoutExplorerUi ?? false;
|
|
765
|
+
const url = buildExplorerUrl({
|
|
766
|
+
baseUrl,
|
|
767
|
+
rawRender: useRaw,
|
|
768
|
+
fixtureId: args.fixtureId,
|
|
769
|
+
});
|
|
770
|
+
const result = {
|
|
771
|
+
url,
|
|
772
|
+
sessionName,
|
|
773
|
+
};
|
|
774
|
+
if (args.fixtureId) {
|
|
775
|
+
result.fixtureId = args.fixtureId;
|
|
776
|
+
}
|
|
777
|
+
if (useRaw) {
|
|
778
|
+
result.mode = 'raw-render';
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
result.mode = 'explorer';
|
|
782
|
+
if (args.fixtureId) {
|
|
783
|
+
result.note = 'Fixture selection in the Explorer UI is not URL-based. Navigate to the fixture manually in the tree view.';
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return {
|
|
787
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
788
|
+
};
|
|
789
|
+
});
|
|
790
|
+
}
|
|
436
791
|
}
|
|
437
792
|
|
|
438
|
-
export { ComponentExplorerMcpServer };
|
|
793
|
+
export { ComponentExplorerMcpServer, DaemonConnection };
|
|
439
794
|
//# sourceMappingURL=McpServer.js.map
|