@vscode/component-explorer-cli 0.1.1-1 → 0.1.1-11
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 +177 -0
- package/dist/commands/acceptCommand.d.ts +2 -1
- package/dist/commands/acceptCommand.d.ts.map +1 -1
- package/dist/commands/acceptCommand.js +12 -6
- package/dist/commands/acceptCommand.js.map +1 -1
- package/dist/commands/compareCommand.d.ts +5 -1
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +43 -71
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/mcpCommand.d.ts +10 -0
- package/dist/commands/mcpCommand.d.ts.map +1 -0
- package/dist/commands/mcpCommand.js +58 -0
- package/dist/commands/mcpCommand.js.map +1 -0
- package/dist/commands/screenshotCommand.d.ts +7 -2
- package/dist/commands/screenshotCommand.d.ts.map +1 -1
- package/dist/commands/screenshotCommand.js +61 -14
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +19 -0
- package/dist/commands/serveCommand.d.ts.map +1 -0
- package/dist/commands/serveCommand.js +194 -0
- package/dist/commands/serveCommand.js.map +1 -0
- package/dist/commands/watchCommand.d.ts +1 -2
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +23 -19
- package/dist/commands/watchCommand.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 +44 -2
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +98 -16
- 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 +188 -0
- package/dist/daemon/DaemonService.d.ts.map +1 -0
- package/dist/daemon/DaemonService.js +610 -0
- package/dist/daemon/DaemonService.js.map +1 -0
- package/dist/daemon/approvalStore.d.ts +51 -0
- package/dist/daemon/approvalStore.d.ts.map +1 -0
- package/dist/daemon/approvalStore.js +58 -0
- package/dist/daemon/approvalStore.js.map +1 -0
- package/dist/daemon/lifecycle.d.ts +13 -0
- package/dist/daemon/lifecycle.d.ts.map +1 -0
- package/dist/daemon/lifecycle.js +68 -0
- package/dist/daemon/lifecycle.js.map +1 -0
- package/dist/daemon/pipeClient.d.ts +9 -0
- package/dist/daemon/pipeClient.d.ts.map +1 -0
- package/dist/daemon/pipeClient.js +111 -0
- package/dist/daemon/pipeClient.js.map +1 -0
- package/dist/daemon/pipeName.d.ts +2 -0
- package/dist/daemon/pipeName.d.ts.map +1 -0
- package/dist/daemon/pipeName.js +14 -0
- package/dist/daemon/pipeName.js.map +1 -0
- package/dist/daemon/pipeServer.d.ts +4 -0
- package/dist/daemon/pipeServer.d.ts.map +1 -0
- package/dist/daemon/pipeServer.js +25 -0
- package/dist/daemon/pipeServer.js.map +1 -0
- package/dist/daemon/types.d.ts +12 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/dependencyInstaller.js +1 -1
- package/dist/dependencyInstaller.js.map +1 -1
- package/dist/explorerSession.d.ts +5 -3
- package/dist/explorerSession.d.ts.map +1 -1
- package/dist/explorerSession.js +6 -3
- package/dist/explorerSession.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/debugLocation.js +3 -0
- package/dist/external/vscode-observables/observables/dist/observableInternal/debugLocation.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.d.ts +8 -7
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +59 -12
- package/dist/httpServer.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/McpServer.d.ts +58 -0
- package/dist/mcp/McpServer.d.ts.map +1 -0
- package/dist/mcp/McpServer.js +820 -0
- package/dist/mcp/McpServer.js.map +1 -0
- package/dist/packages/simple-api/dist/chunk-3R7GHWBM.js +137 -0
- package/dist/packages/simple-api/dist/chunk-3R7GHWBM.js.map +1 -0
- package/dist/packages/simple-api/dist/chunk-Q24JOMNK.js +27 -0
- package/dist/packages/simple-api/dist/chunk-Q24JOMNK.js.map +1 -0
- package/dist/packages/simple-api/dist/chunk-SGBCNXYH.js +24 -0
- package/dist/packages/simple-api/dist/chunk-SGBCNXYH.js.map +1 -0
- package/dist/packages/simple-api/dist/express.js +104 -0
- package/dist/packages/simple-api/dist/express.js.map +1 -0
- package/dist/resolveProject.d.ts +21 -0
- package/dist/resolveProject.d.ts.map +1 -0
- package/dist/resolveProject.js +39 -0
- package/dist/resolveProject.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 +41 -0
- package/dist/watchConfig.d.ts.map +1 -1
- package/dist/watchConfig.js +41 -19
- package/dist/watchConfig.js.map +1 -1
- package/package.json +16 -6
- 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
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
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';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Client-local state
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
class WatchList {
|
|
17
|
+
_fixtureIds = new Set();
|
|
18
|
+
_hashes = new Map();
|
|
19
|
+
get fixtureIds() { return this._fixtureIds; }
|
|
20
|
+
add(ids) {
|
|
21
|
+
for (const id of ids) {
|
|
22
|
+
this._fixtureIds.add(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
remove(ids) {
|
|
26
|
+
for (const id of ids) {
|
|
27
|
+
this._fixtureIds.delete(id);
|
|
28
|
+
this._hashes.delete(id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
set(ids) {
|
|
32
|
+
this._fixtureIds.clear();
|
|
33
|
+
this._hashes.clear();
|
|
34
|
+
for (const id of ids) {
|
|
35
|
+
this._fixtureIds.add(id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
getHash(fixtureId) {
|
|
39
|
+
return this._hashes.get(fixtureId);
|
|
40
|
+
}
|
|
41
|
+
setHash(fixtureId, hash) {
|
|
42
|
+
this._hashes.set(fixtureId, hash);
|
|
43
|
+
}
|
|
44
|
+
toJSON() {
|
|
45
|
+
return {
|
|
46
|
+
fixtureIds: [...this._fixtureIds],
|
|
47
|
+
hashes: Object.fromEntries(this._hashes),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
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
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// ComponentExplorerMcpServer
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
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
|
+
}
|
|
101
|
+
_mcp;
|
|
102
|
+
_watchList = new WatchList();
|
|
103
|
+
_pollFn;
|
|
104
|
+
_noAutostartHint;
|
|
105
|
+
_multiSessionTools = [];
|
|
106
|
+
_sessions = [];
|
|
107
|
+
_eventStreamAbortController;
|
|
108
|
+
constructor(_daemonConnection, options) {
|
|
109
|
+
super();
|
|
110
|
+
this._daemonConnection = _daemonConnection;
|
|
111
|
+
this._pollFn = options.pollFn;
|
|
112
|
+
this._noAutostartHint = options.noAutostartHint;
|
|
113
|
+
this._mcp = new McpServer({
|
|
114
|
+
name: 'component-explorer',
|
|
115
|
+
version: '0.1.0',
|
|
116
|
+
});
|
|
117
|
+
this._registerTools();
|
|
118
|
+
this._store.add(autorun(async (reader) => {
|
|
119
|
+
const conn = this._daemonConnection.read(reader);
|
|
120
|
+
await this._onDaemonChanged(conn?.client);
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
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
|
+
}
|
|
197
|
+
}
|
|
198
|
+
_log(level, data) {
|
|
199
|
+
const mcpLevel = level === 'trace' ? 'debug' : level;
|
|
200
|
+
this._mcp.sendLoggingMessage({ level: mcpLevel, logger: 'daemon', data }).catch(() => { });
|
|
201
|
+
}
|
|
202
|
+
_startEventListener(daemon) {
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
this._eventStreamAbortController = controller;
|
|
205
|
+
(async () => {
|
|
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);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
if (!controller.signal.aborted) {
|
|
223
|
+
this._log('info', { type: 'event-stream-error', error: String(e) });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
})();
|
|
227
|
+
}
|
|
228
|
+
// -- Helpers --------------------------------------------------------------
|
|
229
|
+
_defaultSessionName() {
|
|
230
|
+
return this._sessions[0]?.name ?? 'current';
|
|
231
|
+
}
|
|
232
|
+
_defaultBaselineSessionName() {
|
|
233
|
+
const worktree = this._sessions.find(s => s.sourceKind === 'worktree');
|
|
234
|
+
return worktree?.name ?? this._sessions[1]?.name ?? this._defaultSessionName();
|
|
235
|
+
}
|
|
236
|
+
_defaultCurrentSessionName() {
|
|
237
|
+
const current = this._sessions.find(s => s.sourceKind === 'current');
|
|
238
|
+
return current?.name ?? this._defaultSessionName();
|
|
239
|
+
}
|
|
240
|
+
_sourceTreeId(sessionName) {
|
|
241
|
+
const s = this._sessions.find(s => s.name === sessionName);
|
|
242
|
+
return s && !s.isLoading ? s.sourceTreeId : '';
|
|
243
|
+
}
|
|
244
|
+
_updateSessionSourceTreeId(sessionName, sourceTreeId) {
|
|
245
|
+
const s = this._sessions.find(s => s.name === sessionName);
|
|
246
|
+
if (s) {
|
|
247
|
+
s.sourceTreeId = sourceTreeId;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async _refreshSessions() {
|
|
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
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async _withSourceTreeRetry(fn) {
|
|
282
|
+
try {
|
|
283
|
+
return await fn();
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
287
|
+
if (msg.includes('Source tree changed')) {
|
|
288
|
+
await this._refreshSessions();
|
|
289
|
+
return await fn();
|
|
290
|
+
}
|
|
291
|
+
throw e;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// -- Tool registration ---------------------------------------------------
|
|
295
|
+
_registerTools() {
|
|
296
|
+
this._registerListFixtures();
|
|
297
|
+
this._registerScreenshot();
|
|
298
|
+
this._registerCompareScreenshot();
|
|
299
|
+
this._registerApproveDiff();
|
|
300
|
+
this._registerEvaluateJs();
|
|
301
|
+
this._registerDebugReloadPage();
|
|
302
|
+
this._registerWatchAdd();
|
|
303
|
+
this._registerWatchRemove();
|
|
304
|
+
this._registerWatchSet();
|
|
305
|
+
this._registerWatchCompare();
|
|
306
|
+
this._registerWaitForUpdate();
|
|
307
|
+
this._registerSessions();
|
|
308
|
+
this._registerGetUrl();
|
|
309
|
+
}
|
|
310
|
+
_registerListFixtures() {
|
|
311
|
+
this._mcp.registerTool('list_fixtures', {
|
|
312
|
+
description: 'List all fixtures from a session',
|
|
313
|
+
inputSchema: {
|
|
314
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
315
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
316
|
+
},
|
|
317
|
+
annotations: { readOnlyHint: true },
|
|
318
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
319
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
320
|
+
this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
|
|
321
|
+
return this._withSourceTreeRetry(async () => {
|
|
322
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
323
|
+
const fixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
324
|
+
return {
|
|
325
|
+
content: [{ type: 'text', text: JSON.stringify(fixtures, null, 2) }],
|
|
326
|
+
};
|
|
327
|
+
});
|
|
328
|
+
}));
|
|
329
|
+
}
|
|
330
|
+
_registerScreenshot() {
|
|
331
|
+
this._mcp.registerTool('screenshot', {
|
|
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. ',
|
|
334
|
+
inputSchema: {
|
|
335
|
+
fixtureId: z.string().describe('The fixture ID'),
|
|
336
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
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..'),
|
|
339
|
+
},
|
|
340
|
+
annotations: { readOnlyHint: true },
|
|
341
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
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 });
|
|
345
|
+
return this._withSourceTreeRetry(async () => {
|
|
346
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
347
|
+
const result = await daemon.methods.screenshots.take({
|
|
348
|
+
fixtureId: args.fixtureId,
|
|
349
|
+
sessionName,
|
|
350
|
+
sourceTreeId,
|
|
351
|
+
includeImage: true,
|
|
352
|
+
stabilityCheck: args.stabilityCheck,
|
|
353
|
+
});
|
|
354
|
+
const r = result;
|
|
355
|
+
this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
|
|
356
|
+
const info = {
|
|
357
|
+
hash: r.hash,
|
|
358
|
+
sourceTreeId: r.sourceTreeId,
|
|
359
|
+
};
|
|
360
|
+
if (r.errors && r.errors.length > 0) {
|
|
361
|
+
info.errors = r.errors;
|
|
362
|
+
}
|
|
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
|
|
382
|
+
content.push({ type: 'image', data: r.image, mimeType: 'image/png' });
|
|
383
|
+
}
|
|
384
|
+
content.unshift({ type: 'text', text: JSON.stringify(info, null, 2) });
|
|
385
|
+
return { content };
|
|
386
|
+
});
|
|
387
|
+
}));
|
|
388
|
+
}
|
|
389
|
+
_registerCompareScreenshot() {
|
|
390
|
+
const tool = this._mcp.registerTool('compare_screenshot', {
|
|
391
|
+
description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
|
|
392
|
+
inputSchema: {
|
|
393
|
+
fixtureId: z.string().describe('The fixture ID'),
|
|
394
|
+
baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
|
|
395
|
+
currentSessionName: z.string().optional().describe('Current session name (defaults to current session)'),
|
|
396
|
+
baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
|
|
397
|
+
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
398
|
+
},
|
|
399
|
+
annotations: { readOnlyHint: true },
|
|
400
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
401
|
+
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
402
|
+
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
403
|
+
this._log('debug', { type: 'tool-call', tool: 'compare_screenshot', fixtureId: args.fixtureId, baselineSessionName, currentSessionName });
|
|
404
|
+
return this._withSourceTreeRetry(async () => {
|
|
405
|
+
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
406
|
+
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
407
|
+
const result = await daemon.methods.screenshots.compare({
|
|
408
|
+
fixtureId: args.fixtureId,
|
|
409
|
+
baselineSessionName,
|
|
410
|
+
baselineSourceTreeId,
|
|
411
|
+
currentSessionName,
|
|
412
|
+
currentSourceTreeId,
|
|
413
|
+
includeImages: true,
|
|
414
|
+
});
|
|
415
|
+
const r = result;
|
|
416
|
+
const info = {
|
|
417
|
+
match: r.match,
|
|
418
|
+
baselineHash: r.baselineHash,
|
|
419
|
+
currentHash: r.currentHash,
|
|
420
|
+
};
|
|
421
|
+
if (r.baselineErrors && r.baselineErrors.length > 0) {
|
|
422
|
+
info.baselineErrors = r.baselineErrors;
|
|
423
|
+
}
|
|
424
|
+
if (r.currentErrors && r.currentErrors.length > 0) {
|
|
425
|
+
info.currentErrors = r.currentErrors;
|
|
426
|
+
}
|
|
427
|
+
if (r.approval) {
|
|
428
|
+
info.approval = r.approval;
|
|
429
|
+
}
|
|
430
|
+
const content = [
|
|
431
|
+
{ type: 'text', text: JSON.stringify(info, null, 2) },
|
|
432
|
+
];
|
|
433
|
+
if (r.baselineImage) {
|
|
434
|
+
content.push({ type: 'image', data: r.baselineImage, mimeType: 'image/png' });
|
|
435
|
+
}
|
|
436
|
+
if (r.currentImage) {
|
|
437
|
+
content.push({ type: 'image', data: r.currentImage, mimeType: 'image/png' });
|
|
438
|
+
}
|
|
439
|
+
return { content };
|
|
440
|
+
});
|
|
441
|
+
}));
|
|
442
|
+
tool.disable();
|
|
443
|
+
this._multiSessionTools.push(tool);
|
|
444
|
+
}
|
|
445
|
+
_registerApproveDiff() {
|
|
446
|
+
const tool = this._mcp.registerTool('approve_diff', {
|
|
447
|
+
description: 'Approve a visual diff so it won\'t require re-inspection next time',
|
|
448
|
+
inputSchema: {
|
|
449
|
+
fixtureId: z.string(),
|
|
450
|
+
originalHash: z.string(),
|
|
451
|
+
modifiedHash: z.string(),
|
|
452
|
+
comment: z.string().describe('Reason for approving this diff'),
|
|
453
|
+
},
|
|
454
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
455
|
+
await daemon.methods.approvals.approve(args);
|
|
456
|
+
return {
|
|
457
|
+
content: [{
|
|
458
|
+
type: 'text',
|
|
459
|
+
text: `Approved diff for ${args.fixtureId}: ${args.originalHash} → ${args.modifiedHash}`,
|
|
460
|
+
}],
|
|
461
|
+
};
|
|
462
|
+
}));
|
|
463
|
+
tool.disable();
|
|
464
|
+
this._multiSessionTools.push(tool);
|
|
465
|
+
}
|
|
466
|
+
_registerEvaluateJs() {
|
|
467
|
+
this._mcp.registerTool('evaluate_js', {
|
|
468
|
+
description: 'Evaluate a JavaScript expression in the browser page where fixtures are rendered, for debugging purposes. ' +
|
|
469
|
+
'Returns the expression result as JSON. The expression can return a Promise (it will be awaited). ' +
|
|
470
|
+
'Use this to inspect DOM state, computed styles, element dimensions, or component output. ' +
|
|
471
|
+
'Do NOT use this to modify the DOM — this tool is for read-only inspection and debugging only.',
|
|
472
|
+
inputSchema: {
|
|
473
|
+
expression: z.string().describe('JavaScript expression to evaluate. Can return a Promise. The result must be JSON-serializable.'),
|
|
474
|
+
fixtureId: z.string().optional().describe('If provided, renders this fixture before evaluating the expression'),
|
|
475
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
476
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
477
|
+
},
|
|
478
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
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 });
|
|
482
|
+
return this._withSourceTreeRetry(async () => {
|
|
483
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
484
|
+
const result = await daemon.methods.evaluate({
|
|
485
|
+
sessionName,
|
|
486
|
+
sourceTreeId,
|
|
487
|
+
expression: args.expression,
|
|
488
|
+
fixtureId: args.fixtureId,
|
|
489
|
+
});
|
|
490
|
+
const r = result;
|
|
491
|
+
let text;
|
|
492
|
+
try {
|
|
493
|
+
text = JSON.stringify(r.result, null, 2) ?? 'undefined';
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
text = String(r.result);
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
content: [{ type: 'text', text }],
|
|
500
|
+
};
|
|
501
|
+
});
|
|
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
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
_registerWatchAdd() {
|
|
527
|
+
this._mcp.registerTool('watch_add', {
|
|
528
|
+
description: 'Add fixtures to the watch list. Watched fixtures are automatically re-screenshotted when source changes.',
|
|
529
|
+
inputSchema: {
|
|
530
|
+
fixtureIds: z.array(z.string()).describe('Fixture IDs to add'),
|
|
531
|
+
},
|
|
532
|
+
}, async (args) => {
|
|
533
|
+
this._watchList.add(args.fixtureIds);
|
|
534
|
+
return {
|
|
535
|
+
content: [{
|
|
536
|
+
type: 'text',
|
|
537
|
+
text: `Watch list: ${[...this._watchList.fixtureIds].join(', ') || '(empty)'}`,
|
|
538
|
+
}],
|
|
539
|
+
};
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
_registerWatchRemove() {
|
|
543
|
+
this._mcp.registerTool('watch_remove', {
|
|
544
|
+
description: 'Remove fixtures from the watch list',
|
|
545
|
+
inputSchema: {
|
|
546
|
+
fixtureIds: z.array(z.string()).describe('Fixture IDs to remove'),
|
|
547
|
+
},
|
|
548
|
+
}, async (args) => {
|
|
549
|
+
this._watchList.remove(args.fixtureIds);
|
|
550
|
+
return {
|
|
551
|
+
content: [{
|
|
552
|
+
type: 'text',
|
|
553
|
+
text: `Watch list: ${[...this._watchList.fixtureIds].join(', ') || '(empty)'}`,
|
|
554
|
+
}],
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
_registerWatchSet() {
|
|
559
|
+
this._mcp.registerTool('watch_set', {
|
|
560
|
+
description: 'Replace the watch list entirely',
|
|
561
|
+
inputSchema: {
|
|
562
|
+
fixtureIds: z.array(z.string()).describe('Fixture IDs to watch'),
|
|
563
|
+
},
|
|
564
|
+
}, async (args) => {
|
|
565
|
+
this._watchList.set(args.fixtureIds);
|
|
566
|
+
return {
|
|
567
|
+
content: [{
|
|
568
|
+
type: 'text',
|
|
569
|
+
text: `Watch list: ${[...this._watchList.fixtureIds].join(', ') || '(empty)'}`,
|
|
570
|
+
}],
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
_registerWatchCompare() {
|
|
575
|
+
const tool = this._mcp.registerTool('watch_compare', {
|
|
576
|
+
description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
|
|
577
|
+
inputSchema: {
|
|
578
|
+
baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
|
|
579
|
+
currentSessionName: z.string().optional().describe('Current session name (defaults to current session)'),
|
|
580
|
+
baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
|
|
581
|
+
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
582
|
+
},
|
|
583
|
+
annotations: { readOnlyHint: true },
|
|
584
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
585
|
+
const ids = [...this._watchList.fixtureIds];
|
|
586
|
+
if (ids.length === 0) {
|
|
587
|
+
return { content: [{ type: 'text', text: 'Watch list is empty. Use watch_add or watch_set first.' }] };
|
|
588
|
+
}
|
|
589
|
+
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
590
|
+
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
591
|
+
return this._withSourceTreeRetry(async () => {
|
|
592
|
+
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
593
|
+
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
594
|
+
const [baselineResult, currentResult] = await Promise.all([
|
|
595
|
+
daemon.methods.screenshots.takeBatch({
|
|
596
|
+
fixtureIds: ids,
|
|
597
|
+
sessionName: baselineSessionName,
|
|
598
|
+
sourceTreeId: baselineSourceTreeId,
|
|
599
|
+
}),
|
|
600
|
+
daemon.methods.screenshots.takeBatch({
|
|
601
|
+
fixtureIds: ids,
|
|
602
|
+
sessionName: currentSessionName,
|
|
603
|
+
sourceTreeId: currentSourceTreeId,
|
|
604
|
+
}),
|
|
605
|
+
]);
|
|
606
|
+
const br = baselineResult;
|
|
607
|
+
const cr = currentResult;
|
|
608
|
+
const baselineMap = new Map(br.screenshots.map(s => [s.fixtureId, s.hash]));
|
|
609
|
+
const currentMap = new Map(cr.screenshots.map(s => [s.fixtureId, s.hash]));
|
|
610
|
+
const entries = ids.map(id => {
|
|
611
|
+
const bHash = baselineMap.get(id) ?? '';
|
|
612
|
+
const cHash = currentMap.get(id) ?? '';
|
|
613
|
+
return {
|
|
614
|
+
fixtureId: id,
|
|
615
|
+
match: bHash === cHash,
|
|
616
|
+
baselineHash: bHash,
|
|
617
|
+
currentHash: cHash,
|
|
618
|
+
};
|
|
619
|
+
});
|
|
620
|
+
// Look up approvals for changed fixtures
|
|
621
|
+
const results = [];
|
|
622
|
+
for (const entry of entries) {
|
|
623
|
+
let approval = undefined;
|
|
624
|
+
if (!entry.match && entry.baselineHash && entry.currentHash) {
|
|
625
|
+
approval = await daemon.methods.approvals.lookup({
|
|
626
|
+
fixtureId: entry.fixtureId,
|
|
627
|
+
originalHash: entry.baselineHash,
|
|
628
|
+
modifiedHash: entry.currentHash,
|
|
629
|
+
}) ?? undefined;
|
|
630
|
+
}
|
|
631
|
+
results.push({ ...entry, approval });
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
635
|
+
};
|
|
636
|
+
});
|
|
637
|
+
}));
|
|
638
|
+
tool.disable();
|
|
639
|
+
this._multiSessionTools.push(tool);
|
|
640
|
+
}
|
|
641
|
+
_registerWaitForUpdate() {
|
|
642
|
+
this._mcp.registerTool('wait_for_update', {
|
|
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
|
+
},
|
|
651
|
+
annotations: { readOnlyHint: true },
|
|
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);
|
|
658
|
+
}
|
|
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
|
+
}
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
await iterator.return?.();
|
|
689
|
+
}
|
|
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 });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
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
|
+
}],
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
content: [{
|
|
724
|
+
type: 'text',
|
|
725
|
+
text: JSON.stringify({ sourceTreeId, sessionName }, null, 2),
|
|
726
|
+
}],
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
_registerSessions() {
|
|
730
|
+
this._mcp.registerTool('sessions', {
|
|
731
|
+
description: 'List active sessions with their names, URLs, and current sourceTreeIds',
|
|
732
|
+
annotations: { readOnlyHint: true },
|
|
733
|
+
}, async () => this._withDaemon(async (_daemon) => {
|
|
734
|
+
await this._refreshSessions();
|
|
735
|
+
return {
|
|
736
|
+
content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
|
|
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
|
+
};
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export { ComponentExplorerMcpServer, DaemonConnection };
|
|
820
|
+
//# sourceMappingURL=McpServer.js.map
|