@vscode/component-explorer-cli 0.1.1-7 → 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/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/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/component-explorer-config.schema.json +183 -0
- package/dist/componentExplorer.d.ts +10 -0
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +48 -2
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonService.d.ts +26 -4
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +39 -3
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/lifecycle.js +2 -2
- 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 +29 -4
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +393 -81
- 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 +20 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +22 -1
- package/dist/utils.js.map +1 -1
- 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 {
|
|
@@ -125,12 +249,14 @@ class ComponentExplorerMcpServer {
|
|
|
125
249
|
this._registerCompareScreenshot();
|
|
126
250
|
this._registerApproveDiff();
|
|
127
251
|
this._registerEvaluateJs();
|
|
252
|
+
this._registerDebugReloadPage();
|
|
128
253
|
this._registerWatchAdd();
|
|
129
254
|
this._registerWatchRemove();
|
|
130
255
|
this._registerWatchSet();
|
|
131
256
|
this._registerWatchCompare();
|
|
132
257
|
this._registerWaitForUpdate();
|
|
133
258
|
this._registerSessions();
|
|
259
|
+
this._registerGetUrl();
|
|
134
260
|
}
|
|
135
261
|
_registerListFixtures() {
|
|
136
262
|
this._mcp.registerTool('list_fixtures', {
|
|
@@ -141,10 +267,14 @@ class ComponentExplorerMcpServer {
|
|
|
141
267
|
},
|
|
142
268
|
annotations: { readOnlyHint: true },
|
|
143
269
|
}, async (args) => {
|
|
270
|
+
const daemon = await this._waitForDaemon();
|
|
271
|
+
if (!daemon) {
|
|
272
|
+
return this._noDaemonError();
|
|
273
|
+
}
|
|
144
274
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
145
275
|
return this._withSourceTreeRetry(async () => {
|
|
146
276
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
147
|
-
const fixtures = await
|
|
277
|
+
const fixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
148
278
|
return {
|
|
149
279
|
content: [{ type: 'text', text: JSON.stringify(fixtures, null, 2) }],
|
|
150
280
|
};
|
|
@@ -153,22 +283,29 @@ class ComponentExplorerMcpServer {
|
|
|
153
283
|
}
|
|
154
284
|
_registerScreenshot() {
|
|
155
285
|
this._mcp.registerTool('screenshot', {
|
|
156
|
-
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. ',
|
|
157
288
|
inputSchema: {
|
|
158
289
|
fixtureId: z.string().describe('The fixture ID'),
|
|
159
290
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
160
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..'),
|
|
161
293
|
},
|
|
162
294
|
annotations: { readOnlyHint: true },
|
|
163
295
|
}, async (args) => {
|
|
296
|
+
const daemon = await this._waitForDaemon();
|
|
297
|
+
if (!daemon) {
|
|
298
|
+
return this._noDaemonError();
|
|
299
|
+
}
|
|
164
300
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
165
301
|
return this._withSourceTreeRetry(async () => {
|
|
166
302
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
167
|
-
const result = await
|
|
303
|
+
const result = await daemon.methods.screenshots.take({
|
|
168
304
|
fixtureId: args.fixtureId,
|
|
169
305
|
sessionName,
|
|
170
306
|
sourceTreeId,
|
|
171
307
|
includeImage: true,
|
|
308
|
+
stabilityCheck: args.stabilityCheck,
|
|
172
309
|
});
|
|
173
310
|
const r = result;
|
|
174
311
|
this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
|
|
@@ -179,18 +316,34 @@ class ComponentExplorerMcpServer {
|
|
|
179
316
|
if (r.errors && r.errors.length > 0) {
|
|
180
317
|
info.errors = r.errors;
|
|
181
318
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
186
338
|
content.push({ type: 'image', data: r.image, mimeType: 'image/png' });
|
|
187
339
|
}
|
|
340
|
+
content.unshift({ type: 'text', text: JSON.stringify(info, null, 2) });
|
|
188
341
|
return { content };
|
|
189
342
|
});
|
|
190
343
|
});
|
|
191
344
|
}
|
|
192
345
|
_registerCompareScreenshot() {
|
|
193
|
-
this._mcp.registerTool('compare_screenshot', {
|
|
346
|
+
const tool = this._mcp.registerTool('compare_screenshot', {
|
|
194
347
|
description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
|
|
195
348
|
inputSchema: {
|
|
196
349
|
fixtureId: z.string().describe('The fixture ID'),
|
|
@@ -201,12 +354,16 @@ class ComponentExplorerMcpServer {
|
|
|
201
354
|
},
|
|
202
355
|
annotations: { readOnlyHint: true },
|
|
203
356
|
}, async (args) => {
|
|
357
|
+
const daemon = await this._waitForDaemon();
|
|
358
|
+
if (!daemon) {
|
|
359
|
+
return this._noDaemonError();
|
|
360
|
+
}
|
|
204
361
|
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
205
362
|
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
206
363
|
return this._withSourceTreeRetry(async () => {
|
|
207
364
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
208
365
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
209
|
-
const result = await
|
|
366
|
+
const result = await daemon.methods.screenshots.compare({
|
|
210
367
|
fixtureId: args.fixtureId,
|
|
211
368
|
baselineSessionName,
|
|
212
369
|
baselineSourceTreeId,
|
|
@@ -241,9 +398,11 @@ class ComponentExplorerMcpServer {
|
|
|
241
398
|
return { content };
|
|
242
399
|
});
|
|
243
400
|
});
|
|
401
|
+
tool.disable();
|
|
402
|
+
this._multiSessionTools.push(tool);
|
|
244
403
|
}
|
|
245
404
|
_registerApproveDiff() {
|
|
246
|
-
this._mcp.registerTool('approve_diff', {
|
|
405
|
+
const tool = this._mcp.registerTool('approve_diff', {
|
|
247
406
|
description: 'Approve a visual diff so it won\'t require re-inspection next time',
|
|
248
407
|
inputSchema: {
|
|
249
408
|
fixtureId: z.string(),
|
|
@@ -252,7 +411,11 @@ class ComponentExplorerMcpServer {
|
|
|
252
411
|
comment: z.string().describe('Reason for approving this diff'),
|
|
253
412
|
},
|
|
254
413
|
}, async (args) => {
|
|
255
|
-
await this.
|
|
414
|
+
const daemon = await this._waitForDaemon();
|
|
415
|
+
if (!daemon) {
|
|
416
|
+
return this._noDaemonError();
|
|
417
|
+
}
|
|
418
|
+
await daemon.methods.approvals.approve(args);
|
|
256
419
|
return {
|
|
257
420
|
content: [{
|
|
258
421
|
type: 'text',
|
|
@@ -260,6 +423,8 @@ class ComponentExplorerMcpServer {
|
|
|
260
423
|
}],
|
|
261
424
|
};
|
|
262
425
|
});
|
|
426
|
+
tool.disable();
|
|
427
|
+
this._multiSessionTools.push(tool);
|
|
263
428
|
}
|
|
264
429
|
_registerEvaluateJs() {
|
|
265
430
|
this._mcp.registerTool('evaluate_js', {
|
|
@@ -274,10 +439,14 @@ class ComponentExplorerMcpServer {
|
|
|
274
439
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
275
440
|
},
|
|
276
441
|
}, async (args) => {
|
|
442
|
+
const daemon = await this._waitForDaemon();
|
|
443
|
+
if (!daemon) {
|
|
444
|
+
return this._noDaemonError();
|
|
445
|
+
}
|
|
277
446
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
278
447
|
return this._withSourceTreeRetry(async () => {
|
|
279
448
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
280
|
-
const result = await
|
|
449
|
+
const result = await daemon.methods.evaluate({
|
|
281
450
|
sessionName,
|
|
282
451
|
sourceTreeId,
|
|
283
452
|
expression: args.expression,
|
|
@@ -297,6 +466,32 @@ class ComponentExplorerMcpServer {
|
|
|
297
466
|
});
|
|
298
467
|
});
|
|
299
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
|
+
});
|
|
494
|
+
}
|
|
300
495
|
_registerWatchAdd() {
|
|
301
496
|
this._mcp.registerTool('watch_add', {
|
|
302
497
|
description: 'Add fixtures to the watch list. Watched fixtures are automatically re-screenshotted when source changes.',
|
|
@@ -346,7 +541,7 @@ class ComponentExplorerMcpServer {
|
|
|
346
541
|
});
|
|
347
542
|
}
|
|
348
543
|
_registerWatchCompare() {
|
|
349
|
-
this._mcp.registerTool('watch_compare', {
|
|
544
|
+
const tool = this._mcp.registerTool('watch_compare', {
|
|
350
545
|
description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
|
|
351
546
|
inputSchema: {
|
|
352
547
|
baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
|
|
@@ -356,6 +551,10 @@ class ComponentExplorerMcpServer {
|
|
|
356
551
|
},
|
|
357
552
|
annotations: { readOnlyHint: true },
|
|
358
553
|
}, async (args) => {
|
|
554
|
+
const daemon = await this._waitForDaemon();
|
|
555
|
+
if (!daemon) {
|
|
556
|
+
return this._noDaemonError();
|
|
557
|
+
}
|
|
359
558
|
const ids = [...this._watchList.fixtureIds];
|
|
360
559
|
if (ids.length === 0) {
|
|
361
560
|
return { content: [{ type: 'text', text: 'Watch list is empty. Use watch_add or watch_set first.' }] };
|
|
@@ -366,12 +565,12 @@ class ComponentExplorerMcpServer {
|
|
|
366
565
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
367
566
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
368
567
|
const [baselineResult, currentResult] = await Promise.all([
|
|
369
|
-
|
|
568
|
+
daemon.methods.screenshots.takeBatch({
|
|
370
569
|
fixtureIds: ids,
|
|
371
570
|
sessionName: baselineSessionName,
|
|
372
571
|
sourceTreeId: baselineSourceTreeId,
|
|
373
572
|
}),
|
|
374
|
-
|
|
573
|
+
daemon.methods.screenshots.takeBatch({
|
|
375
574
|
fixtureIds: ids,
|
|
376
575
|
sessionName: currentSessionName,
|
|
377
576
|
sourceTreeId: currentSourceTreeId,
|
|
@@ -396,7 +595,7 @@ class ComponentExplorerMcpServer {
|
|
|
396
595
|
for (const entry of entries) {
|
|
397
596
|
let approval = undefined;
|
|
398
597
|
if (!entry.match && entry.baselineHash && entry.currentHash) {
|
|
399
|
-
approval = await
|
|
598
|
+
approval = await daemon.methods.approvals.lookup({
|
|
400
599
|
fixtureId: entry.fixtureId,
|
|
401
600
|
originalHash: entry.baselineHash,
|
|
402
601
|
modifiedHash: entry.currentHash,
|
|
@@ -409,74 +608,187 @@ class ComponentExplorerMcpServer {
|
|
|
409
608
|
};
|
|
410
609
|
});
|
|
411
610
|
});
|
|
611
|
+
tool.disable();
|
|
612
|
+
this._multiSessionTools.push(tool);
|
|
412
613
|
}
|
|
413
614
|
_registerWaitForUpdate() {
|
|
414
615
|
this._mcp.registerTool('wait_for_update', {
|
|
415
|
-
description: 'Block until the
|
|
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
|
+
},
|
|
416
624
|
annotations: { readOnlyHint: true },
|
|
417
|
-
}, async () => {
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
// Close the stream after consuming one event
|
|
422
|
-
await iterator.return?.();
|
|
423
|
-
if (done || !event) {
|
|
424
|
-
return { content: [{ type: 'text', text: 'Event stream ended.' }] };
|
|
425
|
-
}
|
|
426
|
-
const ev = event;
|
|
427
|
-
// Update cached session info
|
|
428
|
-
if (ev.type === 'source-change' && ev.sourceTreeId) {
|
|
429
|
-
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
625
|
+
}, async (args) => {
|
|
626
|
+
const daemon = await this._waitForDaemon();
|
|
627
|
+
if (!daemon) {
|
|
628
|
+
return this._noDaemonError();
|
|
430
629
|
}
|
|
431
|
-
|
|
432
|
-
|
|
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);
|
|
433
636
|
}
|
|
434
|
-
//
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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);
|
|
450
662
|
}
|
|
451
663
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
}
|
|
462
687
|
}
|
|
463
688
|
return {
|
|
464
|
-
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
|
+
}],
|
|
465
698
|
};
|
|
466
|
-
}
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
content: [{
|
|
702
|
+
type: 'text',
|
|
703
|
+
text: JSON.stringify({ sourceTreeId, sessionName }, null, 2),
|
|
704
|
+
}],
|
|
705
|
+
};
|
|
467
706
|
}
|
|
468
707
|
_registerSessions() {
|
|
469
708
|
this._mcp.registerTool('sessions', {
|
|
470
709
|
description: 'List active sessions with their names, URLs, and current sourceTreeIds',
|
|
471
710
|
annotations: { readOnlyHint: true },
|
|
472
711
|
}, async () => {
|
|
712
|
+
const daemon = await this._waitForDaemon();
|
|
713
|
+
if (!daemon) {
|
|
714
|
+
return this._noDaemonError();
|
|
715
|
+
}
|
|
473
716
|
await this._refreshSessions();
|
|
474
717
|
return {
|
|
475
718
|
content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
|
|
476
719
|
};
|
|
477
720
|
});
|
|
478
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
|
+
}
|
|
479
791
|
}
|
|
480
792
|
|
|
481
|
-
export { ComponentExplorerMcpServer };
|
|
793
|
+
export { ComponentExplorerMcpServer, DaemonConnection };
|
|
482
794
|
//# sourceMappingURL=McpServer.js.map
|