@vscode/component-explorer-cli 0.1.1-7 → 0.1.1-9
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 +34 -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 +60 -10
- 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 +66 -14
- package/dist/componentExplorer.js.map +1 -1
- package/dist/daemon/DaemonContext.d.ts +4 -0
- package/dist/daemon/DaemonContext.d.ts.map +1 -0
- package/dist/daemon/DaemonService.d.ts +43 -20
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +56 -7
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/lifecycle.d.ts +8 -3
- package/dist/daemon/lifecycle.d.ts.map +1 -1
- package/dist/daemon/lifecycle.js +27 -10
- package/dist/daemon/lifecycle.js.map +1 -1
- package/dist/daemon/pipeClient.d.ts +6 -1
- package/dist/daemon/pipeClient.d.ts.map +1 -1
- package/dist/daemon/pipeClient.js +19 -6
- package/dist/daemon/pipeClient.js.map +1 -1
- package/dist/daemon/pipeServer.d.ts.map +1 -1
- package/dist/daemon/pipeServer.js +5 -3
- 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/formatValue.d.ts +2 -0
- package/dist/formatValue.d.ts.map +1 -0
- package/dist/formatValue.js +96 -0
- package/dist/formatValue.js.map +1 -0
- package/dist/formatValue.test.d.ts +2 -0
- package/dist/formatValue.test.d.ts.map +1 -0
- package/dist/git/gitUtils.js +1 -1
- package/dist/git/gitUtils.js.map +1 -1
- package/dist/httpServer.js +4 -3
- package/dist/httpServer.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 +433 -95
- 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,180 @@ 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
|
+
_stale = false;
|
|
74
|
+
constructor(client) {
|
|
75
|
+
this.client = client;
|
|
76
|
+
}
|
|
77
|
+
get isStale() { return this._stale; }
|
|
78
|
+
markStale() { this._stale = true; }
|
|
79
|
+
}
|
|
80
|
+
function isPipeConnectionError(e) {
|
|
81
|
+
if (!(e instanceof Error)) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
const code = e.code;
|
|
85
|
+
if (code === 'ENOENT' || code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EPIPE') {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return /connect ENOENT|ECONNREFUSED|ECONNRESET|EPIPE/.test(e.message);
|
|
89
|
+
}
|
|
43
90
|
// ---------------------------------------------------------------------------
|
|
44
91
|
// ComponentExplorerMcpServer
|
|
45
92
|
// ---------------------------------------------------------------------------
|
|
46
|
-
class ComponentExplorerMcpServer {
|
|
47
|
-
|
|
93
|
+
class ComponentExplorerMcpServer extends Disposable {
|
|
94
|
+
_daemonConnection;
|
|
95
|
+
static async create(daemon, options) {
|
|
96
|
+
const server = new ComponentExplorerMcpServer(daemon, options ?? {});
|
|
97
|
+
const transport = new StdioServerTransport();
|
|
98
|
+
await server._mcp.connect(transport);
|
|
99
|
+
return server;
|
|
100
|
+
}
|
|
48
101
|
_mcp;
|
|
49
102
|
_watchList = new WatchList();
|
|
103
|
+
_pollFn;
|
|
104
|
+
_noAutostartHint;
|
|
105
|
+
_multiSessionTools = [];
|
|
50
106
|
_sessions = [];
|
|
51
|
-
|
|
52
|
-
|
|
107
|
+
_eventStreamAbortController;
|
|
108
|
+
constructor(_daemonConnection, options) {
|
|
109
|
+
super();
|
|
110
|
+
this._daemonConnection = _daemonConnection;
|
|
111
|
+
this._pollFn = options.pollFn;
|
|
112
|
+
this._noAutostartHint = options.noAutostartHint;
|
|
53
113
|
this._mcp = new McpServer({
|
|
54
114
|
name: 'component-explorer',
|
|
55
115
|
version: '0.1.0',
|
|
56
116
|
});
|
|
57
117
|
this._registerTools();
|
|
118
|
+
this._store.add(autorun(async (reader) => {
|
|
119
|
+
const conn = this._daemonConnection.read(reader);
|
|
120
|
+
await this._onDaemonChanged(conn?.client);
|
|
121
|
+
}));
|
|
58
122
|
}
|
|
59
|
-
async
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
123
|
+
async _onDaemonChanged(daemon) {
|
|
124
|
+
this._eventStreamAbortController?.abort();
|
|
125
|
+
this._eventStreamAbortController = undefined;
|
|
126
|
+
if (!daemon) {
|
|
127
|
+
this._sessions = [];
|
|
128
|
+
this._log('info', { type: 'daemon-disconnected' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
this._sessions = await daemon.methods.sessions();
|
|
133
|
+
this._log('debug', { type: 'daemon-connected', sessions: this._sessions.length });
|
|
134
|
+
this._updateMultiSessionToolVisibility();
|
|
135
|
+
this._startEventListener(daemon);
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
this._log('info', { type: 'daemon-error', error: String(e) });
|
|
139
|
+
this._sessions = [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
_getConnection() {
|
|
143
|
+
const conn = this._daemonConnection.get();
|
|
144
|
+
if (conn?.isStale) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
return conn;
|
|
148
|
+
}
|
|
149
|
+
async _waitForDaemon() {
|
|
150
|
+
let conn = this._getConnection();
|
|
151
|
+
if (conn) {
|
|
152
|
+
return conn.client;
|
|
153
|
+
}
|
|
154
|
+
if (!this._pollFn) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
this._log('debug', { type: 'waiting-for-daemon' });
|
|
158
|
+
const startTime = Date.now();
|
|
159
|
+
const timeout = 3000;
|
|
160
|
+
while (Date.now() - startTime < timeout) {
|
|
161
|
+
await this._pollFn();
|
|
162
|
+
conn = this._getConnection();
|
|
163
|
+
if (conn) {
|
|
164
|
+
return conn.client;
|
|
165
|
+
}
|
|
166
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
_handleDisconnect() {
|
|
171
|
+
const conn = this._daemonConnection.get();
|
|
172
|
+
if (conn && !conn.isStale) {
|
|
173
|
+
conn.markStale();
|
|
174
|
+
this._sessions = [];
|
|
175
|
+
this._log('debug', { type: 'daemon-connection-lost' });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
_noDaemonError() {
|
|
179
|
+
return noDaemonError(this._noAutostartHint);
|
|
180
|
+
}
|
|
181
|
+
async _withDaemon(fn) {
|
|
182
|
+
const daemon = await this._waitForDaemon();
|
|
183
|
+
if (!daemon) {
|
|
184
|
+
return this._noDaemonError();
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return await fn(daemon);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
if (isPipeConnectionError(e)) {
|
|
191
|
+
this._log('debug', { type: 'daemon-call-failed', error: String(e) });
|
|
192
|
+
this._handleDisconnect();
|
|
193
|
+
return this._noDaemonError();
|
|
194
|
+
}
|
|
195
|
+
throw e;
|
|
196
|
+
}
|
|
64
197
|
}
|
|
65
198
|
_log(level, data) {
|
|
66
|
-
|
|
199
|
+
const mcpLevel = level === 'trace' ? 'debug' : level;
|
|
200
|
+
this._mcp.sendLoggingMessage({ level: mcpLevel, logger: 'daemon', data }).catch(() => { });
|
|
67
201
|
}
|
|
68
|
-
|
|
69
|
-
const
|
|
202
|
+
_startEventListener(daemon) {
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
this._eventStreamAbortController = controller;
|
|
70
205
|
(async () => {
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
206
|
+
try {
|
|
207
|
+
const stream = await daemon.methods.events();
|
|
208
|
+
for await (const raw of stream) {
|
|
209
|
+
if (controller.signal.aborted)
|
|
210
|
+
break;
|
|
211
|
+
const event = raw;
|
|
212
|
+
if (event.type === 'source-change' && event.sessionName && event.sourceTreeId) {
|
|
213
|
+
this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
|
|
214
|
+
}
|
|
215
|
+
if (event.type === 'ref-change') {
|
|
216
|
+
await this._refreshSessions();
|
|
217
|
+
}
|
|
218
|
+
this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
|
|
75
219
|
}
|
|
76
|
-
|
|
77
|
-
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
if (!controller.signal.aborted) {
|
|
223
|
+
this._log('info', { type: 'event-stream-error', error: String(e) });
|
|
78
224
|
}
|
|
79
|
-
this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
|
|
80
225
|
}
|
|
81
226
|
})();
|
|
82
227
|
}
|
|
@@ -94,7 +239,7 @@ class ComponentExplorerMcpServer {
|
|
|
94
239
|
}
|
|
95
240
|
_sourceTreeId(sessionName) {
|
|
96
241
|
const s = this._sessions.find(s => s.name === sessionName);
|
|
97
|
-
return s
|
|
242
|
+
return s && !s.isLoading ? s.sourceTreeId : '';
|
|
98
243
|
}
|
|
99
244
|
_updateSessionSourceTreeId(sessionName, sourceTreeId) {
|
|
100
245
|
const s = this._sessions.find(s => s.name === sessionName);
|
|
@@ -103,7 +248,35 @@ class ComponentExplorerMcpServer {
|
|
|
103
248
|
}
|
|
104
249
|
}
|
|
105
250
|
async _refreshSessions() {
|
|
106
|
-
|
|
251
|
+
const conn = this._getConnection();
|
|
252
|
+
if (conn) {
|
|
253
|
+
try {
|
|
254
|
+
const prevCount = this._sessions.length;
|
|
255
|
+
this._sessions = await conn.client.methods.sessions();
|
|
256
|
+
if (this._sessions.length !== prevCount) {
|
|
257
|
+
this._updateMultiSessionToolVisibility();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
catch (e) {
|
|
261
|
+
if (isPipeConnectionError(e)) {
|
|
262
|
+
this._handleDisconnect();
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
throw e;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
_updateMultiSessionToolVisibility() {
|
|
271
|
+
const isMultiSession = this._sessions.length > 1;
|
|
272
|
+
for (const tool of this._multiSessionTools) {
|
|
273
|
+
if (isMultiSession) {
|
|
274
|
+
tool.enable();
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
tool.disable();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
107
280
|
}
|
|
108
281
|
async _withSourceTreeRetry(fn) {
|
|
109
282
|
try {
|
|
@@ -125,12 +298,14 @@ class ComponentExplorerMcpServer {
|
|
|
125
298
|
this._registerCompareScreenshot();
|
|
126
299
|
this._registerApproveDiff();
|
|
127
300
|
this._registerEvaluateJs();
|
|
301
|
+
this._registerDebugReloadPage();
|
|
128
302
|
this._registerWatchAdd();
|
|
129
303
|
this._registerWatchRemove();
|
|
130
304
|
this._registerWatchSet();
|
|
131
305
|
this._registerWatchCompare();
|
|
132
306
|
this._registerWaitForUpdate();
|
|
133
307
|
this._registerSessions();
|
|
308
|
+
this._registerGetUrl();
|
|
134
309
|
}
|
|
135
310
|
_registerListFixtures() {
|
|
136
311
|
this._mcp.registerTool('list_fixtures', {
|
|
@@ -140,35 +315,41 @@ class ComponentExplorerMcpServer {
|
|
|
140
315
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
141
316
|
},
|
|
142
317
|
annotations: { readOnlyHint: true },
|
|
143
|
-
}, async (args) => {
|
|
318
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
144
319
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
320
|
+
this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
|
|
145
321
|
return this._withSourceTreeRetry(async () => {
|
|
146
322
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
147
|
-
const fixtures = await
|
|
323
|
+
const fixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
148
324
|
return {
|
|
149
325
|
content: [{ type: 'text', text: JSON.stringify(fixtures, null, 2) }],
|
|
150
326
|
};
|
|
151
327
|
});
|
|
152
|
-
});
|
|
328
|
+
}));
|
|
153
329
|
}
|
|
154
330
|
_registerScreenshot() {
|
|
155
331
|
this._mcp.registerTool('screenshot', {
|
|
156
|
-
description: 'Take a screenshot of a single fixture'
|
|
332
|
+
description: 'Take a screenshot of a single fixture. ' +
|
|
333
|
+
'When stabilityCheck is true, the fixture is unmounted and re-mounted, then three screenshots are taken. ',
|
|
157
334
|
inputSchema: {
|
|
158
335
|
fixtureId: z.string().describe('The fixture ID'),
|
|
159
336
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
160
337
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
338
|
+
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
339
|
},
|
|
162
340
|
annotations: { readOnlyHint: true },
|
|
163
|
-
}, async (args) => {
|
|
341
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
164
342
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
343
|
+
this._log('debug', { type: 'tool-call', tool: 'screenshot', fixtureId: args.fixtureId, sessionName });
|
|
344
|
+
this._log('trace', { type: 'tool-args', tool: 'screenshot', args });
|
|
165
345
|
return this._withSourceTreeRetry(async () => {
|
|
166
346
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
167
|
-
const result = await
|
|
347
|
+
const result = await daemon.methods.screenshots.take({
|
|
168
348
|
fixtureId: args.fixtureId,
|
|
169
349
|
sessionName,
|
|
170
350
|
sourceTreeId,
|
|
171
351
|
includeImage: true,
|
|
352
|
+
stabilityCheck: args.stabilityCheck,
|
|
172
353
|
});
|
|
173
354
|
const r = result;
|
|
174
355
|
this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
|
|
@@ -179,18 +360,34 @@ class ComponentExplorerMcpServer {
|
|
|
179
360
|
if (r.errors && r.errors.length > 0) {
|
|
180
361
|
info.errors = r.errors;
|
|
181
362
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
363
|
+
if (r.isStable !== undefined) {
|
|
364
|
+
info.isStable = r.isStable;
|
|
365
|
+
}
|
|
366
|
+
const content = [];
|
|
367
|
+
if (r.isStable === false && r.stabilityScreenshots) {
|
|
368
|
+
// Not stable: return all distinct screenshots
|
|
369
|
+
const seenHashes = new Set();
|
|
370
|
+
const screenshotDetails = [];
|
|
371
|
+
for (const s of r.stabilityScreenshots) {
|
|
372
|
+
screenshotDetails.push({ hash: s.hash, delayMs: s.delayMs });
|
|
373
|
+
if (!seenHashes.has(s.hash) && s.image) {
|
|
374
|
+
seenHashes.add(s.hash);
|
|
375
|
+
content.push({ type: 'image', data: s.image, mimeType: 'image/png' });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
info.stabilityScreenshots = screenshotDetails;
|
|
379
|
+
}
|
|
380
|
+
else if (r.image) {
|
|
381
|
+
// Stable or no stability check: return single image
|
|
186
382
|
content.push({ type: 'image', data: r.image, mimeType: 'image/png' });
|
|
187
383
|
}
|
|
384
|
+
content.unshift({ type: 'text', text: JSON.stringify(info, null, 2) });
|
|
188
385
|
return { content };
|
|
189
386
|
});
|
|
190
|
-
});
|
|
387
|
+
}));
|
|
191
388
|
}
|
|
192
389
|
_registerCompareScreenshot() {
|
|
193
|
-
this._mcp.registerTool('compare_screenshot', {
|
|
390
|
+
const tool = this._mcp.registerTool('compare_screenshot', {
|
|
194
391
|
description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
|
|
195
392
|
inputSchema: {
|
|
196
393
|
fixtureId: z.string().describe('The fixture ID'),
|
|
@@ -200,13 +397,14 @@ class ComponentExplorerMcpServer {
|
|
|
200
397
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
201
398
|
},
|
|
202
399
|
annotations: { readOnlyHint: true },
|
|
203
|
-
}, async (args) => {
|
|
400
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
204
401
|
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
205
402
|
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
403
|
+
this._log('debug', { type: 'tool-call', tool: 'compare_screenshot', fixtureId: args.fixtureId, baselineSessionName, currentSessionName });
|
|
206
404
|
return this._withSourceTreeRetry(async () => {
|
|
207
405
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
208
406
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
209
|
-
const result = await
|
|
407
|
+
const result = await daemon.methods.screenshots.compare({
|
|
210
408
|
fixtureId: args.fixtureId,
|
|
211
409
|
baselineSessionName,
|
|
212
410
|
baselineSourceTreeId,
|
|
@@ -240,10 +438,12 @@ class ComponentExplorerMcpServer {
|
|
|
240
438
|
}
|
|
241
439
|
return { content };
|
|
242
440
|
});
|
|
243
|
-
});
|
|
441
|
+
}));
|
|
442
|
+
tool.disable();
|
|
443
|
+
this._multiSessionTools.push(tool);
|
|
244
444
|
}
|
|
245
445
|
_registerApproveDiff() {
|
|
246
|
-
this._mcp.registerTool('approve_diff', {
|
|
446
|
+
const tool = this._mcp.registerTool('approve_diff', {
|
|
247
447
|
description: 'Approve a visual diff so it won\'t require re-inspection next time',
|
|
248
448
|
inputSchema: {
|
|
249
449
|
fixtureId: z.string(),
|
|
@@ -251,15 +451,17 @@ class ComponentExplorerMcpServer {
|
|
|
251
451
|
modifiedHash: z.string(),
|
|
252
452
|
comment: z.string().describe('Reason for approving this diff'),
|
|
253
453
|
},
|
|
254
|
-
}, async (args) => {
|
|
255
|
-
await
|
|
454
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
455
|
+
await daemon.methods.approvals.approve(args);
|
|
256
456
|
return {
|
|
257
457
|
content: [{
|
|
258
458
|
type: 'text',
|
|
259
459
|
text: `Approved diff for ${args.fixtureId}: ${args.originalHash} → ${args.modifiedHash}`,
|
|
260
460
|
}],
|
|
261
461
|
};
|
|
262
|
-
});
|
|
462
|
+
}));
|
|
463
|
+
tool.disable();
|
|
464
|
+
this._multiSessionTools.push(tool);
|
|
263
465
|
}
|
|
264
466
|
_registerEvaluateJs() {
|
|
265
467
|
this._mcp.registerTool('evaluate_js', {
|
|
@@ -273,11 +475,13 @@ class ComponentExplorerMcpServer {
|
|
|
273
475
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
274
476
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
275
477
|
},
|
|
276
|
-
}, async (args) => {
|
|
478
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
277
479
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
480
|
+
this._log('debug', { type: 'tool-call', tool: 'evaluate_js', sessionName, hasFixtureId: !!args.fixtureId });
|
|
481
|
+
this._log('trace', { type: 'tool-args', tool: 'evaluate_js', expressionLength: args.expression.length, fixtureId: args.fixtureId });
|
|
278
482
|
return this._withSourceTreeRetry(async () => {
|
|
279
483
|
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
280
|
-
const result = await
|
|
484
|
+
const result = await daemon.methods.evaluate({
|
|
281
485
|
sessionName,
|
|
282
486
|
sourceTreeId,
|
|
283
487
|
expression: args.expression,
|
|
@@ -295,7 +499,29 @@ class ComponentExplorerMcpServer {
|
|
|
295
499
|
content: [{ type: 'text', text }],
|
|
296
500
|
};
|
|
297
501
|
});
|
|
298
|
-
});
|
|
502
|
+
}));
|
|
503
|
+
}
|
|
504
|
+
_registerDebugReloadPage() {
|
|
505
|
+
this._mcp.registerTool('debug_reload_page', {
|
|
506
|
+
description: 'Force-reload the browser page used for rendering fixtures. ' +
|
|
507
|
+
'Only use this as a last resort if screenshots or evaluate_js return stale/broken results ' +
|
|
508
|
+
'that persist after source changes. Normal HMR updates should handle most cases automatically.',
|
|
509
|
+
inputSchema: {
|
|
510
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
511
|
+
},
|
|
512
|
+
annotations: { destructiveHint: true },
|
|
513
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
514
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
515
|
+
const sourceTreeId = this._sourceTreeId(sessionName);
|
|
516
|
+
await daemon.methods.evaluate({
|
|
517
|
+
sessionName,
|
|
518
|
+
sourceTreeId,
|
|
519
|
+
expression: 'location.reload()',
|
|
520
|
+
});
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: 'text', text: `Reloaded page for session '${sessionName}'.` }],
|
|
523
|
+
};
|
|
524
|
+
}));
|
|
299
525
|
}
|
|
300
526
|
_registerWatchAdd() {
|
|
301
527
|
this._mcp.registerTool('watch_add', {
|
|
@@ -346,7 +572,7 @@ class ComponentExplorerMcpServer {
|
|
|
346
572
|
});
|
|
347
573
|
}
|
|
348
574
|
_registerWatchCompare() {
|
|
349
|
-
this._mcp.registerTool('watch_compare', {
|
|
575
|
+
const tool = this._mcp.registerTool('watch_compare', {
|
|
350
576
|
description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
|
|
351
577
|
inputSchema: {
|
|
352
578
|
baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
|
|
@@ -355,7 +581,7 @@ class ComponentExplorerMcpServer {
|
|
|
355
581
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
356
582
|
},
|
|
357
583
|
annotations: { readOnlyHint: true },
|
|
358
|
-
}, async (args) => {
|
|
584
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
359
585
|
const ids = [...this._watchList.fixtureIds];
|
|
360
586
|
if (ids.length === 0) {
|
|
361
587
|
return { content: [{ type: 'text', text: 'Watch list is empty. Use watch_add or watch_set first.' }] };
|
|
@@ -366,12 +592,12 @@ class ComponentExplorerMcpServer {
|
|
|
366
592
|
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
367
593
|
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
368
594
|
const [baselineResult, currentResult] = await Promise.all([
|
|
369
|
-
|
|
595
|
+
daemon.methods.screenshots.takeBatch({
|
|
370
596
|
fixtureIds: ids,
|
|
371
597
|
sessionName: baselineSessionName,
|
|
372
598
|
sourceTreeId: baselineSourceTreeId,
|
|
373
599
|
}),
|
|
374
|
-
|
|
600
|
+
daemon.methods.screenshots.takeBatch({
|
|
375
601
|
fixtureIds: ids,
|
|
376
602
|
sessionName: currentSessionName,
|
|
377
603
|
sourceTreeId: currentSourceTreeId,
|
|
@@ -396,7 +622,7 @@ class ComponentExplorerMcpServer {
|
|
|
396
622
|
for (const entry of entries) {
|
|
397
623
|
let approval = undefined;
|
|
398
624
|
if (!entry.match && entry.baselineHash && entry.currentHash) {
|
|
399
|
-
approval = await
|
|
625
|
+
approval = await daemon.methods.approvals.lookup({
|
|
400
626
|
fixtureId: entry.fixtureId,
|
|
401
627
|
originalHash: entry.baselineHash,
|
|
402
628
|
modifiedHash: entry.currentHash,
|
|
@@ -408,75 +634,187 @@ class ComponentExplorerMcpServer {
|
|
|
408
634
|
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
409
635
|
};
|
|
410
636
|
});
|
|
411
|
-
});
|
|
637
|
+
}));
|
|
638
|
+
tool.disable();
|
|
639
|
+
this._multiSessionTools.push(tool);
|
|
412
640
|
}
|
|
413
641
|
_registerWaitForUpdate() {
|
|
414
642
|
this._mcp.registerTool('wait_for_update', {
|
|
415
|
-
description: 'Block until the
|
|
643
|
+
description: 'Block until the source tree changes from the given sourceTreeId. ' +
|
|
644
|
+
'Pass the sourceTreeId you already observed — resolves immediately if it already differs, ' +
|
|
645
|
+
'otherwise waits for a source-change or ref-change event. ' +
|
|
646
|
+
'If fixtures are on the watch list, automatically re-screenshots them and reports which changed.',
|
|
647
|
+
inputSchema: {
|
|
648
|
+
sourceTreeId: z.string().describe('The sourceTreeId the client currently knows about. The call resolves once the source tree differs from this value.'),
|
|
649
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
650
|
+
},
|
|
416
651
|
annotations: { readOnlyHint: true },
|
|
417
|
-
}, async () => {
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if (done || !event) {
|
|
424
|
-
return { content: [{ type: 'text', text: 'Event stream ended.' }] };
|
|
652
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
653
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
654
|
+
const knownSourceTreeId = args.sourceTreeId;
|
|
655
|
+
const currentSourceTreeId = this._sourceTreeId(sessionName);
|
|
656
|
+
if (currentSourceTreeId && currentSourceTreeId !== knownSourceTreeId) {
|
|
657
|
+
return this._waitForUpdateResult(daemon, sessionName, currentSourceTreeId);
|
|
425
658
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
659
|
+
// Wait for an event that changes the source tree (max 5s)
|
|
660
|
+
const events = await daemon.methods.events();
|
|
661
|
+
const iterator = events[Symbol.asyncIterator]();
|
|
662
|
+
try {
|
|
663
|
+
const timeout = new Promise(resolve => setTimeout(() => resolve('timeout'), 5000));
|
|
664
|
+
while (true) {
|
|
665
|
+
const next = iterator.next();
|
|
666
|
+
const result = await Promise.race([next, timeout]);
|
|
667
|
+
if (result === 'timeout') {
|
|
668
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
669
|
+
}
|
|
670
|
+
const { value: event, done } = result;
|
|
671
|
+
if (done || !event) {
|
|
672
|
+
return { content: [{ type: 'text', text: 'Event stream ended.' }] };
|
|
673
|
+
}
|
|
674
|
+
const ev = event;
|
|
675
|
+
if (ev.type === 'source-change' && ev.sourceTreeId) {
|
|
676
|
+
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
677
|
+
}
|
|
678
|
+
if (ev.type === 'ref-change') {
|
|
679
|
+
await this._refreshSessions();
|
|
680
|
+
}
|
|
681
|
+
const newSourceTreeId = this._sourceTreeId(sessionName);
|
|
682
|
+
if (newSourceTreeId && newSourceTreeId !== knownSourceTreeId) {
|
|
683
|
+
return this._waitForUpdateResult(daemon, sessionName, newSourceTreeId);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
430
686
|
}
|
|
431
|
-
|
|
432
|
-
await
|
|
687
|
+
finally {
|
|
688
|
+
await iterator.return?.();
|
|
433
689
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
async _waitForUpdateResult(daemon, sessionName, sourceTreeId) {
|
|
693
|
+
const watchedIds = [...this._watchList.fixtureIds];
|
|
694
|
+
if (watchedIds.length > 0) {
|
|
695
|
+
const batchResult = await daemon.methods.screenshots.takeBatch({
|
|
696
|
+
fixtureIds: watchedIds,
|
|
697
|
+
sessionName,
|
|
698
|
+
sourceTreeId,
|
|
699
|
+
});
|
|
700
|
+
const br = batchResult;
|
|
701
|
+
const changes = [];
|
|
702
|
+
for (const s of br.screenshots) {
|
|
703
|
+
const prevHash = this._watchList.getHash(s.fixtureId);
|
|
704
|
+
const changed = prevHash !== undefined && prevHash !== s.hash;
|
|
705
|
+
this._watchList.setHash(s.fixtureId, s.hash);
|
|
706
|
+
if (changed) {
|
|
707
|
+
changes.push({ fixtureId: s.fixtureId, previousHash: prevHash, hash: s.hash });
|
|
451
708
|
}
|
|
452
|
-
return {
|
|
453
|
-
content: [{
|
|
454
|
-
type: 'text',
|
|
455
|
-
text: JSON.stringify({
|
|
456
|
-
event: ev,
|
|
457
|
-
watchedFixtures: br.screenshots.length,
|
|
458
|
-
changed: changes,
|
|
459
|
-
}, null, 2),
|
|
460
|
-
}],
|
|
461
|
-
};
|
|
462
709
|
}
|
|
463
710
|
return {
|
|
464
|
-
content: [{
|
|
711
|
+
content: [{
|
|
712
|
+
type: 'text',
|
|
713
|
+
text: JSON.stringify({
|
|
714
|
+
sourceTreeId,
|
|
715
|
+
sessionName,
|
|
716
|
+
watchedFixtures: br.screenshots.length,
|
|
717
|
+
changed: changes,
|
|
718
|
+
}, null, 2),
|
|
719
|
+
}],
|
|
465
720
|
};
|
|
466
|
-
}
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
content: [{
|
|
724
|
+
type: 'text',
|
|
725
|
+
text: JSON.stringify({ sourceTreeId, sessionName }, null, 2),
|
|
726
|
+
}],
|
|
727
|
+
};
|
|
467
728
|
}
|
|
468
729
|
_registerSessions() {
|
|
469
730
|
this._mcp.registerTool('sessions', {
|
|
470
731
|
description: 'List active sessions with their names, URLs, and current sourceTreeIds',
|
|
471
732
|
annotations: { readOnlyHint: true },
|
|
472
|
-
}, async () => {
|
|
733
|
+
}, async () => this._withDaemon(async (_daemon) => {
|
|
473
734
|
await this._refreshSessions();
|
|
474
735
|
return {
|
|
475
736
|
content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
|
|
476
737
|
};
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
_registerGetUrl() {
|
|
741
|
+
this._mcp.registerTool('get_url', {
|
|
742
|
+
description: 'Get URL(s) for viewing fixtures. Returns the Component Explorer UI URL by default, ' +
|
|
743
|
+
'or the raw render URL for embedding/screenshots when useRawDirectRenderingWithoutExplorerUi is true.',
|
|
744
|
+
inputSchema: {
|
|
745
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
746
|
+
fixtureId: z.string().optional().describe('Specific fixture ID. If omitted, returns URL for the explorer root or all fixtures.'),
|
|
747
|
+
useRawDirectRenderingWithoutExplorerUi: z.boolean().optional().describe('If true, returns the raw rendering URL (for embedding or screenshots) instead of the Explorer UI URL. ' +
|
|
748
|
+
'The raw URL renders only the fixture without the explorer chrome. Default: false.'),
|
|
749
|
+
},
|
|
750
|
+
annotations: { readOnlyHint: true },
|
|
751
|
+
}, async (args) => {
|
|
752
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
753
|
+
let session = this._sessions.find(s => s.name === sessionName);
|
|
754
|
+
if (!session) {
|
|
755
|
+
const daemon = await this._waitForDaemon();
|
|
756
|
+
if (daemon) {
|
|
757
|
+
try {
|
|
758
|
+
await this._refreshSessions();
|
|
759
|
+
}
|
|
760
|
+
catch (e) {
|
|
761
|
+
if (isPipeConnectionError(e)) {
|
|
762
|
+
this._handleDisconnect();
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
throw e;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
session = this._sessions.find(s => s.name === sessionName);
|
|
770
|
+
if (!session) {
|
|
771
|
+
return {
|
|
772
|
+
content: [{
|
|
773
|
+
type: 'text',
|
|
774
|
+
text: `Error: Session '${sessionName}' not found. Available sessions: ${this._sessions.map(s => s.name).join(', ') || '(none)'}`,
|
|
775
|
+
}],
|
|
776
|
+
isError: true,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const baseUrl = session && !session.isLoading ? session.serverUrl : undefined;
|
|
781
|
+
if (!baseUrl) {
|
|
782
|
+
return {
|
|
783
|
+
content: [{
|
|
784
|
+
type: 'text',
|
|
785
|
+
text: `Error: Session '${sessionName}' is still loading.`,
|
|
786
|
+
}],
|
|
787
|
+
isError: true,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
const useRaw = args.useRawDirectRenderingWithoutExplorerUi ?? false;
|
|
791
|
+
const url = buildExplorerUrl({
|
|
792
|
+
baseUrl,
|
|
793
|
+
rawRender: useRaw,
|
|
794
|
+
fixtureId: args.fixtureId,
|
|
795
|
+
});
|
|
796
|
+
const result = {
|
|
797
|
+
url,
|
|
798
|
+
sessionName,
|
|
799
|
+
};
|
|
800
|
+
if (args.fixtureId) {
|
|
801
|
+
result.fixtureId = args.fixtureId;
|
|
802
|
+
}
|
|
803
|
+
if (useRaw) {
|
|
804
|
+
result.mode = 'raw-render';
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
result.mode = 'explorer';
|
|
808
|
+
if (args.fixtureId) {
|
|
809
|
+
result.note = 'Fixture selection in the Explorer UI is not URL-based. Navigate to the fixture manually in the tree view.';
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
814
|
+
};
|
|
477
815
|
});
|
|
478
816
|
}
|
|
479
817
|
}
|
|
480
818
|
|
|
481
|
-
export { ComponentExplorerMcpServer };
|
|
819
|
+
export { ComponentExplorerMcpServer, DaemonConnection };
|
|
482
820
|
//# sourceMappingURL=McpServer.js.map
|