@vscode/component-explorer-cli 0.1.1-2 → 0.1.1-20
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/LICENSE +21 -0
- package/README.md +177 -0
- package/SECURITY.md +14 -0
- package/dist/WorktreePool.d.ts +22 -0
- package/dist/WorktreePool.d.ts.map +1 -0
- package/dist/WorktreePool.js +58 -0
- package/dist/WorktreePool.js.map +1 -0
- package/dist/WorktreePool.test.d.ts +2 -0
- package/dist/WorktreePool.test.d.ts.map +1 -0
- package/dist/_virtual/_build-info.js +4 -0
- package/dist/_virtual/_build-info.js.map +1 -0
- package/dist/browserPage.d.ts +5 -0
- package/dist/browserPage.d.ts.map +1 -1
- package/dist/browserPage.js +28 -2
- package/dist/browserPage.js.map +1 -1
- package/dist/commands/acceptCommand.d.ts +2 -1
- package/dist/commands/acceptCommand.d.ts.map +1 -1
- package/dist/commands/acceptCommand.js +15 -8
- package/dist/commands/acceptCommand.js.map +1 -1
- package/dist/commands/checkStabilityCommand.d.ts +12 -0
- package/dist/commands/checkStabilityCommand.d.ts.map +1 -0
- package/dist/commands/checkStabilityCommand.js +84 -0
- package/dist/commands/checkStabilityCommand.js.map +1 -0
- package/dist/commands/compareCommand.d.ts +5 -1
- package/dist/commands/compareCommand.d.ts.map +1 -1
- package/dist/commands/compareCommand.js +56 -71
- package/dist/commands/compareCommand.js.map +1 -1
- package/dist/commands/mcpCommand.d.ts +4 -1
- package/dist/commands/mcpCommand.d.ts.map +1 -1
- package/dist/commands/mcpCommand.js +51 -8
- package/dist/commands/mcpCommand.js.map +1 -1
- package/dist/commands/screenshotCommand.d.ts +9 -2
- package/dist/commands/screenshotCommand.d.ts.map +1 -1
- package/dist/commands/screenshotCommand.js +73 -15
- package/dist/commands/screenshotCommand.js.map +1 -1
- package/dist/commands/serveCommand.d.ts +6 -1
- package/dist/commands/serveCommand.d.ts.map +1 -1
- package/dist/commands/serveCommand.js +104 -23
- package/dist/commands/serveCommand.js.map +1 -1
- package/dist/commands/watchCommand.d.ts +3 -2
- package/dist/commands/watchCommand.d.ts.map +1 -1
- package/dist/commands/watchCommand.js +28 -80
- package/dist/commands/watchCommand.js.map +1 -1
- package/dist/comparison.d.ts +70 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +264 -0
- package/dist/comparison.js.map +1 -0
- package/dist/component-explorer-config.schema.json +222 -0
- package/dist/componentExplorer.d.ts +40 -2
- package/dist/componentExplorer.d.ts.map +1 -1
- package/dist/componentExplorer.js +118 -24
- 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 +146 -21
- package/dist/daemon/DaemonService.d.ts.map +1 -1
- package/dist/daemon/DaemonService.js +620 -123
- package/dist/daemon/DaemonService.js.map +1 -1
- package/dist/daemon/dynamicSessions.test.d.ts +2 -0
- package/dist/daemon/dynamicSessions.test.d.ts.map +1 -0
- 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 +101 -8
- package/dist/daemon/pipeClient.js.map +1 -1
- package/dist/daemon/pipeServer.d.ts +2 -1
- package/dist/daemon/pipeServer.d.ts.map +1 -1
- package/dist/daemon/pipeServer.js +56 -6
- package/dist/daemon/pipeServer.js.map +1 -1
- package/dist/daemon/types.d.ts +12 -0
- package/dist/daemon/types.d.ts.map +1 -0
- package/dist/daemon/version.d.ts +10 -0
- package/dist/daemon/version.d.ts.map +1 -0
- package/dist/daemon/version.js +17 -0
- package/dist/daemon/version.js.map +1 -0
- package/dist/dependencyInstaller.d.ts +2 -2
- package/dist/dependencyInstaller.d.ts.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/gitIndexResolver.d.ts +25 -0
- package/dist/git/gitIndexResolver.d.ts.map +1 -0
- package/dist/git/gitIndexResolver.js +91 -0
- package/dist/git/gitIndexResolver.js.map +1 -0
- package/dist/git/gitIndexResolver.test.d.ts +2 -0
- package/dist/git/gitIndexResolver.test.d.ts.map +1 -0
- package/dist/git/gitService.d.ts +2 -0
- package/dist/git/gitService.d.ts.map +1 -1
- package/dist/git/gitService.js +6 -0
- package/dist/git/gitService.js.map +1 -1
- package/dist/git/gitUtils.js +1 -1
- package/dist/git/gitUtils.js.map +1 -1
- package/dist/git/gitWorktreeManager.d.ts +6 -0
- package/dist/git/gitWorktreeManager.d.ts.map +1 -1
- package/dist/git/gitWorktreeManager.js +42 -13
- package/dist/git/gitWorktreeManager.js.map +1 -1
- package/dist/git/gitWorktreeManager.test.d.ts +2 -0
- package/dist/git/gitWorktreeManager.test.d.ts.map +1 -0
- package/dist/git/testUtils.d.ts +13 -0
- package/dist/git/testUtils.d.ts.map +1 -0
- package/dist/httpServer.d.ts +6 -1
- package/dist/httpServer.d.ts.map +1 -1
- package/dist/httpServer.js +30 -10
- package/dist/httpServer.js.map +1 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +7 -1
- package/dist/logger.js.map +1 -1
- package/dist/mcp/McpServer.d.ts +47 -4
- package/dist/mcp/McpServer.d.ts.map +1 -1
- package/dist/mcp/McpServer.js +913 -155
- package/dist/mcp/McpServer.js.map +1 -1
- package/dist/mcp/TaskManager.d.ts +28 -0
- package/dist/mcp/TaskManager.d.ts.map +1 -0
- package/dist/mcp/TaskManager.js +54 -0
- package/dist/mcp/TaskManager.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-SGBCNXYH.js +24 -0
- package/dist/packages/simple-api/dist/chunk-SGBCNXYH.js.map +1 -0
- package/dist/packages/simple-api/dist/chunk-TAEFVNPN.js +27 -0
- package/dist/packages/simple-api/dist/chunk-TAEFVNPN.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 +31 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +49 -0
- package/dist/utils.js.map +1 -0
- package/dist/watchConfig.d.ts +52 -9
- package/dist/watchConfig.d.ts.map +1 -1
- package/dist/watchConfig.js +67 -62
- package/dist/watchConfig.js.map +1 -1
- package/package.json +31 -8
- 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/mcp/McpServer.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
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';
|
|
12
|
+
import { TaskManager } from './TaskManager.js';
|
|
4
13
|
|
|
5
14
|
// ---------------------------------------------------------------------------
|
|
6
15
|
// Client-local state
|
|
@@ -40,26 +49,197 @@ class WatchList {
|
|
|
40
49
|
};
|
|
41
50
|
}
|
|
42
51
|
}
|
|
52
|
+
function noDaemonError(hint) {
|
|
53
|
+
let text = 'Error: No daemon is currently running.';
|
|
54
|
+
if (hint) {
|
|
55
|
+
text += ` ${hint}`;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
text += ' Please start the Component Explorer daemon first by running:\n\n' +
|
|
59
|
+
' component-explorer serve --project <config.json>\n\n' +
|
|
60
|
+
'Or start it in the background:\n\n' +
|
|
61
|
+
' component-explorer serve --project <config.json> --background\n\n' +
|
|
62
|
+
'The daemon manages dev servers and enables fixture screenshots.';
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: 'text', text }],
|
|
66
|
+
isError: true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// DaemonConnection - wrapper to avoid Proxy issues with observables
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
class DaemonConnection {
|
|
73
|
+
client;
|
|
74
|
+
_stale = false;
|
|
75
|
+
constructor(client) {
|
|
76
|
+
this.client = client;
|
|
77
|
+
}
|
|
78
|
+
get isStale() { return this._stale; }
|
|
79
|
+
markStale() { this._stale = true; }
|
|
80
|
+
}
|
|
81
|
+
function isPipeConnectionError(e) {
|
|
82
|
+
if (!(e instanceof Error)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const code = e.code;
|
|
86
|
+
if (code === 'ENOENT' || code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EPIPE') {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return /connect ENOENT|ECONNREFUSED|ECONNRESET|EPIPE/.test(e.message);
|
|
90
|
+
}
|
|
43
91
|
// ---------------------------------------------------------------------------
|
|
44
92
|
// ComponentExplorerMcpServer
|
|
45
93
|
// ---------------------------------------------------------------------------
|
|
46
|
-
class ComponentExplorerMcpServer {
|
|
47
|
-
|
|
94
|
+
class ComponentExplorerMcpServer extends Disposable {
|
|
95
|
+
_daemonConnection;
|
|
96
|
+
static async create(daemon, options) {
|
|
97
|
+
const server = new ComponentExplorerMcpServer(daemon, options ?? {});
|
|
98
|
+
const transport = new StdioServerTransport();
|
|
99
|
+
await server._mcp.connect(transport);
|
|
100
|
+
return server;
|
|
101
|
+
}
|
|
48
102
|
_mcp;
|
|
49
103
|
_watchList = new WatchList();
|
|
104
|
+
_taskManager = new TaskManager();
|
|
105
|
+
_taskLastReportedIndex = new Map();
|
|
106
|
+
_pollFn;
|
|
107
|
+
_noAutostartHint;
|
|
108
|
+
_multiSessionTools = [];
|
|
50
109
|
_sessions = [];
|
|
51
|
-
|
|
52
|
-
|
|
110
|
+
_eventStreamAbortController;
|
|
111
|
+
constructor(_daemonConnection, options) {
|
|
112
|
+
super();
|
|
113
|
+
this._daemonConnection = _daemonConnection;
|
|
114
|
+
this._pollFn = options.pollFn;
|
|
115
|
+
this._noAutostartHint = options.noAutostartHint;
|
|
116
|
+
this._callTimeoutMs = options.callTimeoutMs ?? ComponentExplorerMcpServer._DEFAULT_CALL_TIMEOUT_MS;
|
|
53
117
|
this._mcp = new McpServer({
|
|
54
118
|
name: 'component-explorer',
|
|
55
119
|
version: '0.1.0',
|
|
56
120
|
});
|
|
57
121
|
this._registerTools();
|
|
122
|
+
this._store.add(autorun(async (reader) => {
|
|
123
|
+
const conn = this._daemonConnection.read(reader);
|
|
124
|
+
await this._onDaemonChanged(conn?.client);
|
|
125
|
+
}));
|
|
58
126
|
}
|
|
59
|
-
async
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
127
|
+
async _onDaemonChanged(daemon) {
|
|
128
|
+
this._eventStreamAbortController?.abort();
|
|
129
|
+
this._eventStreamAbortController = undefined;
|
|
130
|
+
if (!daemon) {
|
|
131
|
+
this._sessions = [];
|
|
132
|
+
this._log('info', { type: 'daemon-disconnected' });
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
this._sessions = await daemon.methods.sessions();
|
|
137
|
+
this._log('debug', { type: 'daemon-connected', sessions: this._sessions.length });
|
|
138
|
+
this._updateMultiSessionToolVisibility();
|
|
139
|
+
this._startEventListener(daemon);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
this._log('info', { type: 'daemon-error', error: String(e) });
|
|
143
|
+
this._sessions = [];
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
_getConnection() {
|
|
147
|
+
const conn = this._daemonConnection.get();
|
|
148
|
+
if (conn?.isStale) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return conn;
|
|
152
|
+
}
|
|
153
|
+
async _waitForDaemon() {
|
|
154
|
+
let conn = this._getConnection();
|
|
155
|
+
if (conn) {
|
|
156
|
+
return conn.client;
|
|
157
|
+
}
|
|
158
|
+
if (!this._pollFn) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
this._log('debug', { type: 'waiting-for-daemon' });
|
|
162
|
+
const startTime = Date.now();
|
|
163
|
+
const timeout = 3000;
|
|
164
|
+
while (Date.now() - startTime < timeout) {
|
|
165
|
+
await this._pollFn();
|
|
166
|
+
conn = this._getConnection();
|
|
167
|
+
if (conn) {
|
|
168
|
+
return conn.client;
|
|
169
|
+
}
|
|
170
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
171
|
+
}
|
|
172
|
+
return undefined;
|
|
173
|
+
}
|
|
174
|
+
_handleDisconnect() {
|
|
175
|
+
const conn = this._daemonConnection.get();
|
|
176
|
+
if (conn && !conn.isStale) {
|
|
177
|
+
conn.markStale();
|
|
178
|
+
this._sessions = [];
|
|
179
|
+
this._log('debug', { type: 'daemon-connection-lost' });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
_noDaemonError() {
|
|
183
|
+
return noDaemonError(this._noAutostartHint);
|
|
184
|
+
}
|
|
185
|
+
static _DEFAULT_CALL_TIMEOUT_MS = 15_000;
|
|
186
|
+
_callTimeoutMs;
|
|
187
|
+
async _withDaemon(fn, options) {
|
|
188
|
+
const daemon = await this._waitForDaemon();
|
|
189
|
+
if (!daemon) {
|
|
190
|
+
return this._noDaemonError();
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
if (options?.noTimeout) {
|
|
194
|
+
return await fn(daemon);
|
|
195
|
+
}
|
|
196
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('__mcp_timeout__')), this._callTimeoutMs));
|
|
197
|
+
return await Promise.race([fn(daemon), timeout]);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
if (e instanceof Error && e.message === '__mcp_timeout__') {
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: 'text', text: `Error: Operation timed out after ${this._callTimeoutMs / 1000}s. Retry, if the error persists, restart the involved session using the restart_session tool and retry.` }],
|
|
203
|
+
isError: true,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (isPipeConnectionError(e)) {
|
|
207
|
+
this._log('debug', { type: 'daemon-call-failed', error: String(e) });
|
|
208
|
+
this._handleDisconnect();
|
|
209
|
+
return this._noDaemonError();
|
|
210
|
+
}
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
_log(level, data) {
|
|
215
|
+
const mcpLevel = level === 'trace' ? 'debug' : level;
|
|
216
|
+
this._mcp.sendLoggingMessage({ level: mcpLevel, logger: 'daemon', data }).catch(() => { });
|
|
217
|
+
}
|
|
218
|
+
_startEventListener(daemon) {
|
|
219
|
+
const controller = new AbortController();
|
|
220
|
+
this._eventStreamAbortController = controller;
|
|
221
|
+
(async () => {
|
|
222
|
+
try {
|
|
223
|
+
const stream = await daemon.methods.events();
|
|
224
|
+
for await (const raw of stream) {
|
|
225
|
+
if (controller.signal.aborted)
|
|
226
|
+
break;
|
|
227
|
+
const event = raw;
|
|
228
|
+
if (event.type === 'source-change' && event.sessionName && event.sourceTreeId) {
|
|
229
|
+
this._updateSessionSourceTreeId(event.sessionName, event.sourceTreeId);
|
|
230
|
+
}
|
|
231
|
+
if (event.type === 'ref-change' || event.type === 'session-change') {
|
|
232
|
+
await this._refreshSessions();
|
|
233
|
+
}
|
|
234
|
+
this._log(event.type === 'log' && event.level === 'debug' ? 'debug' : 'info', event);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (e) {
|
|
238
|
+
if (!controller.signal.aborted) {
|
|
239
|
+
this._log('info', { type: 'event-stream-error', error: String(e) });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
})();
|
|
63
243
|
}
|
|
64
244
|
// -- Helpers --------------------------------------------------------------
|
|
65
245
|
_defaultSessionName() {
|
|
@@ -75,7 +255,7 @@ class ComponentExplorerMcpServer {
|
|
|
75
255
|
}
|
|
76
256
|
_sourceTreeId(sessionName) {
|
|
77
257
|
const s = this._sessions.find(s => s.name === sessionName);
|
|
78
|
-
return s
|
|
258
|
+
return s && !s.isLoading ? s.sourceTreeId : '';
|
|
79
259
|
}
|
|
80
260
|
_updateSessionSourceTreeId(sessionName, sourceTreeId) {
|
|
81
261
|
const s = this._sessions.find(s => s.name === sessionName);
|
|
@@ -84,67 +264,192 @@ class ComponentExplorerMcpServer {
|
|
|
84
264
|
}
|
|
85
265
|
}
|
|
86
266
|
async _refreshSessions() {
|
|
87
|
-
|
|
267
|
+
const conn = this._getConnection();
|
|
268
|
+
if (conn) {
|
|
269
|
+
try {
|
|
270
|
+
const prevCount = this._sessions.length;
|
|
271
|
+
this._sessions = await conn.client.methods.sessions();
|
|
272
|
+
if (this._sessions.length !== prevCount) {
|
|
273
|
+
this._updateMultiSessionToolVisibility();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
if (isPipeConnectionError(e)) {
|
|
278
|
+
this._handleDisconnect();
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
throw e;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
_updateMultiSessionToolVisibility() {
|
|
287
|
+
const isMultiSession = this._sessions.length > 1;
|
|
288
|
+
for (const tool of this._multiSessionTools) {
|
|
289
|
+
if (isMultiSession) {
|
|
290
|
+
tool.enable();
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
tool.disable();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async _withSourceTreeRetry(fn) {
|
|
298
|
+
try {
|
|
299
|
+
return await fn();
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
303
|
+
if (msg.includes('Source tree changed')) {
|
|
304
|
+
await this._refreshSessions();
|
|
305
|
+
return await fn();
|
|
306
|
+
}
|
|
307
|
+
throw e;
|
|
308
|
+
}
|
|
88
309
|
}
|
|
89
310
|
// -- Tool registration ---------------------------------------------------
|
|
311
|
+
_filterFixtures(allFixtures, fixtureIdPattern, labelPattern) {
|
|
312
|
+
let fixtureIdRegex;
|
|
313
|
+
if (fixtureIdPattern) {
|
|
314
|
+
try {
|
|
315
|
+
fixtureIdRegex = new RegExp(fixtureIdPattern);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return { error: `Error: Invalid fixtureIdPattern: ${fixtureIdPattern}` };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
let labelRegex;
|
|
322
|
+
if (labelPattern) {
|
|
323
|
+
try {
|
|
324
|
+
labelRegex = new RegExp(labelPattern);
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return { error: `Error: Invalid labelPattern: ${labelPattern}` };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
fixtures: allFixtures.filter(f => (!fixtureIdRegex || fixtureIdRegex.test(f.fixtureId)) &&
|
|
332
|
+
(!labelRegex || f.labels.some(l => labelRegex.test(l)))),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
90
335
|
_registerTools() {
|
|
91
336
|
this._registerListFixtures();
|
|
92
337
|
this._registerScreenshot();
|
|
93
338
|
this._registerCompareScreenshot();
|
|
94
339
|
this._registerApproveDiff();
|
|
340
|
+
this._registerEvaluateJs();
|
|
341
|
+
this._registerDebugReloadPage();
|
|
95
342
|
this._registerWatchAdd();
|
|
96
343
|
this._registerWatchRemove();
|
|
97
344
|
this._registerWatchSet();
|
|
98
345
|
this._registerWatchCompare();
|
|
99
346
|
this._registerWaitForUpdate();
|
|
100
347
|
this._registerSessions();
|
|
348
|
+
this._registerRestartSession();
|
|
349
|
+
this._registerOpenSession();
|
|
350
|
+
this._registerCloseSession();
|
|
351
|
+
this._registerUpdateSessionRef();
|
|
352
|
+
this._registerGetUrl();
|
|
353
|
+
this._registerCheckStability();
|
|
354
|
+
this._registerCheckTask();
|
|
355
|
+
this._registerCancelTask();
|
|
101
356
|
}
|
|
102
357
|
_registerListFixtures() {
|
|
103
358
|
this._mcp.registerTool('list_fixtures', {
|
|
104
359
|
description: 'List all fixtures from a session',
|
|
105
360
|
inputSchema: {
|
|
361
|
+
fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
|
|
362
|
+
labelPattern: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
|
|
106
363
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
107
364
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
108
365
|
},
|
|
109
|
-
|
|
366
|
+
annotations: { readOnlyHint: true },
|
|
367
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
110
368
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
369
|
+
this._log('debug', { type: 'tool-call', tool: 'list_fixtures', sessionName });
|
|
370
|
+
return this._withSourceTreeRetry(async () => {
|
|
371
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
372
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
373
|
+
const filtered = this._filterFixtures(allFixtures, args.fixtureIdPattern, args.labelPattern);
|
|
374
|
+
if ('error' in filtered) {
|
|
375
|
+
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: 'text', text: JSON.stringify(filtered.fixtures, null, 2) }],
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
}));
|
|
117
382
|
}
|
|
118
383
|
_registerScreenshot() {
|
|
119
384
|
this._mcp.registerTool('screenshot', {
|
|
120
|
-
description: 'Take a screenshot of a single fixture'
|
|
385
|
+
description: 'Take a screenshot of a single fixture. ' +
|
|
386
|
+
'When stabilityCheck is true, the fixture is unmounted and re-mounted, then three screenshots are taken. ',
|
|
121
387
|
inputSchema: {
|
|
122
388
|
fixtureId: z.string().describe('The fixture ID'),
|
|
123
389
|
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
124
390
|
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
391
|
+
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..'),
|
|
125
392
|
},
|
|
126
|
-
|
|
393
|
+
annotations: { readOnlyHint: true },
|
|
394
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
127
395
|
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
sessionName
|
|
132
|
-
|
|
133
|
-
|
|
396
|
+
this._log('debug', { type: 'tool-call', tool: 'screenshot', fixtureId: args.fixtureId, sessionName });
|
|
397
|
+
this._log('trace', { type: 'tool-args', tool: 'screenshot', args });
|
|
398
|
+
return this._withSourceTreeRetry(async () => {
|
|
399
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
400
|
+
const result = await daemon.methods.screenshots.take({
|
|
401
|
+
fixtureId: args.fixtureId,
|
|
402
|
+
sessionName,
|
|
403
|
+
sourceTreeId,
|
|
404
|
+
includeImage: true,
|
|
405
|
+
stabilityCheck: args.stabilityCheck,
|
|
406
|
+
});
|
|
407
|
+
const r = result;
|
|
408
|
+
this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
|
|
409
|
+
const info = {
|
|
410
|
+
hash: r.hash,
|
|
411
|
+
sourceTreeId: r.sourceTreeId,
|
|
412
|
+
};
|
|
413
|
+
if (r.hasError) {
|
|
414
|
+
info.hasError = true;
|
|
415
|
+
}
|
|
416
|
+
if (r.error) {
|
|
417
|
+
info.error = r.error;
|
|
418
|
+
}
|
|
419
|
+
if (r.events && r.events.length > 0) {
|
|
420
|
+
info.events = r.events;
|
|
421
|
+
}
|
|
422
|
+
if (r.resultData !== undefined) {
|
|
423
|
+
info.resultData = r.resultData;
|
|
424
|
+
}
|
|
425
|
+
if (r.isStable !== undefined) {
|
|
426
|
+
info.isStable = r.isStable;
|
|
427
|
+
}
|
|
428
|
+
const content = [];
|
|
429
|
+
if (r.isStable === false && r.stabilityScreenshots) {
|
|
430
|
+
// Not stable: return all distinct screenshots
|
|
431
|
+
const seenHashes = new Set();
|
|
432
|
+
const screenshotDetails = [];
|
|
433
|
+
for (const s of r.stabilityScreenshots) {
|
|
434
|
+
screenshotDetails.push({ hash: s.hash, delayMs: s.delayMs });
|
|
435
|
+
if (!seenHashes.has(s.hash) && s.image) {
|
|
436
|
+
seenHashes.add(s.hash);
|
|
437
|
+
content.push({ type: 'image', data: s.image, mimeType: 'image/png' });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
info.stabilityScreenshots = screenshotDetails;
|
|
441
|
+
}
|
|
442
|
+
else if (r.image) {
|
|
443
|
+
// Stable or no stability check: return single image
|
|
444
|
+
content.push({ type: 'image', data: r.image, mimeType: 'image/png' });
|
|
445
|
+
}
|
|
446
|
+
content.unshift({ type: 'text', text: JSON.stringify(info, null, 2) });
|
|
447
|
+
return { content };
|
|
134
448
|
});
|
|
135
|
-
|
|
136
|
-
this._updateSessionSourceTreeId(sessionName, r.sourceTreeId);
|
|
137
|
-
const content = [
|
|
138
|
-
{ type: 'text', text: `hash: ${r.hash}\nsourceTreeId: ${r.sourceTreeId}` },
|
|
139
|
-
];
|
|
140
|
-
if (r.image) {
|
|
141
|
-
content.push({ type: 'image', data: r.image, mimeType: 'image/png' });
|
|
142
|
-
}
|
|
143
|
-
return { content };
|
|
144
|
-
});
|
|
449
|
+
}));
|
|
145
450
|
}
|
|
146
451
|
_registerCompareScreenshot() {
|
|
147
|
-
this._mcp.registerTool('compare_screenshot', {
|
|
452
|
+
const tool = this._mcp.registerTool('compare_screenshot', {
|
|
148
453
|
description: 'Compare a fixture\'s screenshot across two sessions (e.g. baseline vs current)',
|
|
149
454
|
inputSchema: {
|
|
150
455
|
fixtureId: z.string().describe('The fixture ID'),
|
|
@@ -153,42 +458,74 @@ class ComponentExplorerMcpServer {
|
|
|
153
458
|
baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
|
|
154
459
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
155
460
|
},
|
|
156
|
-
|
|
461
|
+
annotations: { readOnlyHint: true },
|
|
462
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
157
463
|
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
158
464
|
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
465
|
+
this._log('debug', { type: 'tool-call', tool: 'compare_screenshot', fixtureId: args.fixtureId, baselineSessionName, currentSessionName });
|
|
466
|
+
return this._withSourceTreeRetry(async () => {
|
|
467
|
+
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
468
|
+
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
469
|
+
const result = await daemon.methods.screenshots.compare({
|
|
470
|
+
fixtureId: args.fixtureId,
|
|
471
|
+
baselineSessionName,
|
|
472
|
+
baselineSourceTreeId,
|
|
473
|
+
currentSessionName,
|
|
474
|
+
currentSourceTreeId,
|
|
475
|
+
includeImages: true,
|
|
476
|
+
});
|
|
477
|
+
const r = result;
|
|
478
|
+
const info = {
|
|
479
|
+
match: r.match,
|
|
480
|
+
baselineHash: r.baselineHash,
|
|
481
|
+
currentHash: r.currentHash,
|
|
482
|
+
baselineSourceTreeId,
|
|
483
|
+
currentSourceTreeId,
|
|
484
|
+
};
|
|
485
|
+
if (r.baselineHasError) {
|
|
486
|
+
info.baselineHasError = true;
|
|
487
|
+
}
|
|
488
|
+
if (r.baselineError) {
|
|
489
|
+
info.baselineError = r.baselineError;
|
|
490
|
+
}
|
|
491
|
+
if (r.baselineEvents && r.baselineEvents.length > 0) {
|
|
492
|
+
info.baselineEvents = r.baselineEvents;
|
|
493
|
+
}
|
|
494
|
+
if (r.baselineResultData !== undefined) {
|
|
495
|
+
info.baselineResultData = r.baselineResultData;
|
|
496
|
+
}
|
|
497
|
+
if (r.currentHasError) {
|
|
498
|
+
info.currentHasError = true;
|
|
499
|
+
}
|
|
500
|
+
if (r.currentError) {
|
|
501
|
+
info.currentError = r.currentError;
|
|
502
|
+
}
|
|
503
|
+
if (r.currentEvents && r.currentEvents.length > 0) {
|
|
504
|
+
info.currentEvents = r.currentEvents;
|
|
505
|
+
}
|
|
506
|
+
if (r.currentResultData !== undefined) {
|
|
507
|
+
info.currentResultData = r.currentResultData;
|
|
508
|
+
}
|
|
509
|
+
if (r.approval) {
|
|
510
|
+
info.approval = r.approval;
|
|
511
|
+
}
|
|
512
|
+
const content = [
|
|
513
|
+
{ type: 'text', text: JSON.stringify(info, null, 2) },
|
|
514
|
+
];
|
|
515
|
+
if (r.baselineImage) {
|
|
516
|
+
content.push({ type: 'image', data: r.baselineImage, mimeType: 'image/png' });
|
|
517
|
+
}
|
|
518
|
+
if (r.currentImage) {
|
|
519
|
+
content.push({ type: 'image', data: r.currentImage, mimeType: 'image/png' });
|
|
520
|
+
}
|
|
521
|
+
return { content };
|
|
168
522
|
});
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
baselineHash: r.baselineHash,
|
|
173
|
-
currentHash: r.currentHash,
|
|
174
|
-
};
|
|
175
|
-
if (r.approval) {
|
|
176
|
-
info.approval = r.approval;
|
|
177
|
-
}
|
|
178
|
-
const content = [
|
|
179
|
-
{ type: 'text', text: JSON.stringify(info, null, 2) },
|
|
180
|
-
];
|
|
181
|
-
if (r.baselineImage) {
|
|
182
|
-
content.push({ type: 'image', data: r.baselineImage, mimeType: 'image/png' });
|
|
183
|
-
}
|
|
184
|
-
if (r.currentImage) {
|
|
185
|
-
content.push({ type: 'image', data: r.currentImage, mimeType: 'image/png' });
|
|
186
|
-
}
|
|
187
|
-
return { content };
|
|
188
|
-
});
|
|
523
|
+
}));
|
|
524
|
+
tool.disable();
|
|
525
|
+
this._multiSessionTools.push(tool);
|
|
189
526
|
}
|
|
190
527
|
_registerApproveDiff() {
|
|
191
|
-
this._mcp.registerTool('approve_diff', {
|
|
528
|
+
const tool = this._mcp.registerTool('approve_diff', {
|
|
192
529
|
description: 'Approve a visual diff so it won\'t require re-inspection next time',
|
|
193
530
|
inputSchema: {
|
|
194
531
|
fixtureId: z.string(),
|
|
@@ -196,15 +533,77 @@ class ComponentExplorerMcpServer {
|
|
|
196
533
|
modifiedHash: z.string(),
|
|
197
534
|
comment: z.string().describe('Reason for approving this diff'),
|
|
198
535
|
},
|
|
199
|
-
}, async (args) => {
|
|
200
|
-
await
|
|
536
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
537
|
+
await daemon.methods.approvals.approve(args);
|
|
201
538
|
return {
|
|
202
539
|
content: [{
|
|
203
540
|
type: 'text',
|
|
204
541
|
text: `Approved diff for ${args.fixtureId}: ${args.originalHash} → ${args.modifiedHash}`,
|
|
205
542
|
}],
|
|
206
543
|
};
|
|
207
|
-
});
|
|
544
|
+
}));
|
|
545
|
+
tool.disable();
|
|
546
|
+
this._multiSessionTools.push(tool);
|
|
547
|
+
}
|
|
548
|
+
_registerEvaluateJs() {
|
|
549
|
+
this._mcp.registerTool('evaluate_js', {
|
|
550
|
+
description: 'Evaluate a JavaScript expression in the browser page where fixtures are rendered, for debugging purposes. ' +
|
|
551
|
+
'Returns the expression result as JSON. The expression can return a Promise (it will be awaited). ' +
|
|
552
|
+
'Use this to inspect DOM state, computed styles, element dimensions, or component output. ' +
|
|
553
|
+
'Do NOT use this to modify the DOM — this tool is for read-only inspection and debugging only.',
|
|
554
|
+
inputSchema: {
|
|
555
|
+
expression: z.string().describe('JavaScript expression to evaluate. Can return a Promise. The result must be JSON-serializable.'),
|
|
556
|
+
fixtureId: z.string().optional().describe('If provided, renders this fixture before evaluating the expression'),
|
|
557
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
558
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
559
|
+
},
|
|
560
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
561
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
562
|
+
this._log('debug', { type: 'tool-call', tool: 'evaluate_js', sessionName, hasFixtureId: !!args.fixtureId });
|
|
563
|
+
this._log('trace', { type: 'tool-args', tool: 'evaluate_js', expressionLength: args.expression.length, fixtureId: args.fixtureId });
|
|
564
|
+
return this._withSourceTreeRetry(async () => {
|
|
565
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
566
|
+
const result = await daemon.methods.evaluate({
|
|
567
|
+
sessionName,
|
|
568
|
+
sourceTreeId,
|
|
569
|
+
expression: args.expression,
|
|
570
|
+
fixtureId: args.fixtureId,
|
|
571
|
+
});
|
|
572
|
+
const r = result;
|
|
573
|
+
let text;
|
|
574
|
+
try {
|
|
575
|
+
text = JSON.stringify(r.result, null, 2) ?? 'undefined';
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
text = String(r.result);
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
content: [{ type: 'text', text }],
|
|
582
|
+
};
|
|
583
|
+
});
|
|
584
|
+
}));
|
|
585
|
+
}
|
|
586
|
+
_registerDebugReloadPage() {
|
|
587
|
+
this._mcp.registerTool('debug_reload_page', {
|
|
588
|
+
description: 'Force-reload the browser page used for rendering fixtures. ' +
|
|
589
|
+
'Only use this as a last resort if screenshots or evaluate_js return stale/broken results ' +
|
|
590
|
+
'that persist after source changes. Normal HMR updates should handle most cases automatically.',
|
|
591
|
+
inputSchema: {
|
|
592
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
593
|
+
},
|
|
594
|
+
annotations: { destructiveHint: true },
|
|
595
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
596
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
597
|
+
const sourceTreeId = this._sourceTreeId(sessionName);
|
|
598
|
+
await daemon.methods.evaluate({
|
|
599
|
+
sessionName,
|
|
600
|
+
sourceTreeId,
|
|
601
|
+
expression: 'location.reload()',
|
|
602
|
+
});
|
|
603
|
+
return {
|
|
604
|
+
content: [{ type: 'text', text: `Reloaded page for session '${sessionName}'.` }],
|
|
605
|
+
};
|
|
606
|
+
}));
|
|
208
607
|
}
|
|
209
608
|
_registerWatchAdd() {
|
|
210
609
|
this._mcp.registerTool('watch_add', {
|
|
@@ -255,7 +654,7 @@ class ComponentExplorerMcpServer {
|
|
|
255
654
|
});
|
|
256
655
|
}
|
|
257
656
|
_registerWatchCompare() {
|
|
258
|
-
this._mcp.registerTool('watch_compare', {
|
|
657
|
+
const tool = this._mcp.registerTool('watch_compare', {
|
|
259
658
|
description: 'Compare all watched fixtures across two sessions. Takes fresh screenshots from both sessions and reports which fixtures differ.',
|
|
260
659
|
inputSchema: {
|
|
261
660
|
baselineSessionName: z.string().optional().describe('Baseline session name (defaults to worktree session)'),
|
|
@@ -263,124 +662,483 @@ class ComponentExplorerMcpServer {
|
|
|
263
662
|
baselineSourceTreeId: z.string().optional().describe('Baseline source tree ID (defaults to latest known)'),
|
|
264
663
|
currentSourceTreeId: z.string().optional().describe('Current source tree ID (defaults to latest known)'),
|
|
265
664
|
},
|
|
266
|
-
|
|
665
|
+
annotations: { readOnlyHint: true },
|
|
666
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
267
667
|
const ids = [...this._watchList.fixtureIds];
|
|
268
668
|
if (ids.length === 0) {
|
|
269
669
|
return { content: [{ type: 'text', text: 'Watch list is empty. Use watch_add or watch_set first.' }] };
|
|
270
670
|
}
|
|
271
671
|
const baselineSessionName = args.baselineSessionName ?? this._defaultBaselineSessionName();
|
|
272
672
|
const currentSessionName = args.currentSessionName ?? this._defaultCurrentSessionName();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
673
|
+
return this._withSourceTreeRetry(async () => {
|
|
674
|
+
const baselineSourceTreeId = args.baselineSourceTreeId ?? this._sourceTreeId(baselineSessionName);
|
|
675
|
+
const currentSourceTreeId = args.currentSourceTreeId ?? this._sourceTreeId(currentSessionName);
|
|
676
|
+
const [baselineResult, currentResult] = await Promise.all([
|
|
677
|
+
daemon.methods.screenshots.takeBatch({
|
|
678
|
+
fixtureIds: ids,
|
|
679
|
+
sessionName: baselineSessionName,
|
|
680
|
+
sourceTreeId: baselineSourceTreeId,
|
|
681
|
+
}),
|
|
682
|
+
daemon.methods.screenshots.takeBatch({
|
|
683
|
+
fixtureIds: ids,
|
|
684
|
+
sessionName: currentSessionName,
|
|
685
|
+
sourceTreeId: currentSourceTreeId,
|
|
686
|
+
}),
|
|
687
|
+
]);
|
|
688
|
+
const br = baselineResult;
|
|
689
|
+
const cr = currentResult;
|
|
690
|
+
const baselineMap = new Map(br.screenshots.map(s => [s.fixtureId, s.hash]));
|
|
691
|
+
const currentMap = new Map(cr.screenshots.map(s => [s.fixtureId, s.hash]));
|
|
692
|
+
const entries = ids.map(id => {
|
|
693
|
+
const bHash = baselineMap.get(id) ?? '';
|
|
694
|
+
const cHash = currentMap.get(id) ?? '';
|
|
695
|
+
return {
|
|
696
|
+
fixtureId: id,
|
|
697
|
+
match: bHash === cHash,
|
|
698
|
+
baselineHash: bHash,
|
|
699
|
+
currentHash: cHash,
|
|
700
|
+
};
|
|
701
|
+
});
|
|
702
|
+
// Look up approvals for changed fixtures
|
|
703
|
+
const results = [];
|
|
704
|
+
for (const entry of entries) {
|
|
705
|
+
let approval = undefined;
|
|
706
|
+
if (!entry.match && entry.baselineHash && entry.currentHash) {
|
|
707
|
+
approval = await daemon.methods.approvals.lookup({
|
|
708
|
+
fixtureId: entry.fixtureId,
|
|
709
|
+
originalHash: entry.baselineHash,
|
|
710
|
+
modifiedHash: entry.currentHash,
|
|
711
|
+
}) ?? undefined;
|
|
712
|
+
}
|
|
713
|
+
results.push({ ...entry, approval });
|
|
714
|
+
}
|
|
294
715
|
return {
|
|
295
|
-
|
|
296
|
-
match: bHash === cHash,
|
|
297
|
-
baselineHash: bHash,
|
|
298
|
-
currentHash: cHash,
|
|
716
|
+
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
299
717
|
};
|
|
300
718
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
let approval = undefined;
|
|
305
|
-
if (!entry.match && entry.baselineHash && entry.currentHash) {
|
|
306
|
-
approval = await this._daemon.methods.approvals.lookup({
|
|
307
|
-
fixtureId: entry.fixtureId,
|
|
308
|
-
originalHash: entry.baselineHash,
|
|
309
|
-
modifiedHash: entry.currentHash,
|
|
310
|
-
}) ?? undefined;
|
|
311
|
-
}
|
|
312
|
-
results.push({ ...entry, approval });
|
|
313
|
-
}
|
|
314
|
-
return {
|
|
315
|
-
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
|
|
316
|
-
};
|
|
317
|
-
});
|
|
719
|
+
}));
|
|
720
|
+
tool.disable();
|
|
721
|
+
this._multiSessionTools.push(tool);
|
|
318
722
|
}
|
|
319
723
|
_registerWaitForUpdate() {
|
|
320
724
|
this._mcp.registerTool('wait_for_update', {
|
|
321
|
-
description: 'Block until the
|
|
322
|
-
|
|
323
|
-
|
|
725
|
+
description: 'Block until the source tree changes from the given sourceTreeId. ' +
|
|
726
|
+
'Pass the sourceTreeId you already observed — resolves immediately if it already differs, ' +
|
|
727
|
+
'otherwise waits for a source-change or ref-change event. ' +
|
|
728
|
+
'If fixtures are on the watch list, automatically re-screenshots them and reports which changed.',
|
|
729
|
+
inputSchema: {
|
|
730
|
+
sourceTreeId: z.string().describe('The sourceTreeId the client currently knows about. The call resolves once the source tree differs from this value.'),
|
|
731
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
732
|
+
},
|
|
733
|
+
annotations: { readOnlyHint: true },
|
|
734
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
735
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
736
|
+
const knownSourceTreeId = args.sourceTreeId;
|
|
737
|
+
const currentSourceTreeId = this._sourceTreeId(sessionName);
|
|
738
|
+
if (currentSourceTreeId && currentSourceTreeId !== knownSourceTreeId) {
|
|
739
|
+
return this._waitForUpdateResult(daemon, sessionName, currentSourceTreeId);
|
|
740
|
+
}
|
|
741
|
+
// Wait for an event that changes the source tree (max 5s)
|
|
742
|
+
const events = await daemon.methods.events();
|
|
324
743
|
const iterator = events[Symbol.asyncIterator]();
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
744
|
+
try {
|
|
745
|
+
const deadline = Date.now() + 5000;
|
|
746
|
+
while (true) {
|
|
747
|
+
const remaining = deadline - Date.now();
|
|
748
|
+
if (remaining <= 0) {
|
|
749
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
750
|
+
}
|
|
751
|
+
const timeout = new Promise(resolve => setTimeout(() => resolve('timeout'), remaining));
|
|
752
|
+
const next = iterator.next();
|
|
753
|
+
const result = await Promise.race([next, timeout]);
|
|
754
|
+
if (result === 'timeout') {
|
|
755
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
756
|
+
}
|
|
757
|
+
const { value: event, done } = result;
|
|
758
|
+
if (done || !event) {
|
|
759
|
+
return { content: [{ type: 'text', text: 'Event stream ended.' }] };
|
|
760
|
+
}
|
|
761
|
+
const ev = event;
|
|
762
|
+
if (ev.type === 'source-change' && ev.sourceTreeId) {
|
|
763
|
+
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
764
|
+
}
|
|
765
|
+
if (ev.type === 'ref-change') {
|
|
766
|
+
const refreshResult = await Promise.race([this._refreshSessions(), timeout]);
|
|
767
|
+
if (refreshResult === 'timeout') {
|
|
768
|
+
return { content: [{ type: 'text', text: JSON.stringify({ timeout: true, sessionName, sourceTreeId: knownSourceTreeId }, null, 2) }] };
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const newSourceTreeId = this._sourceTreeId(sessionName);
|
|
772
|
+
if (newSourceTreeId && newSourceTreeId !== knownSourceTreeId) {
|
|
773
|
+
return this._waitForUpdateResult(daemon, sessionName, newSourceTreeId);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
330
776
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (ev.type === 'source-change' && ev.sourceTreeId) {
|
|
334
|
-
this._updateSessionSourceTreeId(ev.sessionName, ev.sourceTreeId);
|
|
777
|
+
finally {
|
|
778
|
+
await iterator.return?.();
|
|
335
779
|
}
|
|
336
|
-
|
|
337
|
-
|
|
780
|
+
}));
|
|
781
|
+
}
|
|
782
|
+
async _waitForUpdateResult(daemon, sessionName, sourceTreeId) {
|
|
783
|
+
const watchedIds = [...this._watchList.fixtureIds];
|
|
784
|
+
if (watchedIds.length > 0) {
|
|
785
|
+
const batchResult = await daemon.methods.screenshots.takeBatch({
|
|
786
|
+
fixtureIds: watchedIds,
|
|
787
|
+
sessionName,
|
|
788
|
+
sourceTreeId,
|
|
789
|
+
});
|
|
790
|
+
const br = batchResult;
|
|
791
|
+
const changes = [];
|
|
792
|
+
for (const s of br.screenshots) {
|
|
793
|
+
const prevHash = this._watchList.getHash(s.fixtureId);
|
|
794
|
+
const changed = prevHash !== undefined && prevHash !== s.hash;
|
|
795
|
+
this._watchList.setHash(s.fixtureId, s.hash);
|
|
796
|
+
if (changed) {
|
|
797
|
+
changes.push({ fixtureId: s.fixtureId, previousHash: prevHash, hash: s.hash });
|
|
798
|
+
}
|
|
338
799
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
800
|
+
return {
|
|
801
|
+
content: [{
|
|
802
|
+
type: 'text',
|
|
803
|
+
text: JSON.stringify({
|
|
804
|
+
sourceTreeId,
|
|
805
|
+
sessionName,
|
|
806
|
+
watchedFixtures: br.screenshots.length,
|
|
807
|
+
changed: changes,
|
|
808
|
+
}, null, 2),
|
|
809
|
+
}],
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
return {
|
|
813
|
+
content: [{
|
|
814
|
+
type: 'text',
|
|
815
|
+
text: JSON.stringify({ sourceTreeId, sessionName }, null, 2),
|
|
816
|
+
}],
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
_registerSessions() {
|
|
820
|
+
this._mcp.registerTool('sessions', {
|
|
821
|
+
description: 'List active sessions with their names, URLs, and current sourceTreeIds',
|
|
822
|
+
annotations: { readOnlyHint: true },
|
|
823
|
+
}, async () => this._withDaemon(async (_daemon) => {
|
|
824
|
+
await this._refreshSessions();
|
|
825
|
+
return {
|
|
826
|
+
content: [{ type: 'text', text: JSON.stringify(this._sessions, null, 2) }],
|
|
827
|
+
};
|
|
828
|
+
}));
|
|
829
|
+
}
|
|
830
|
+
_registerRestartSession() {
|
|
831
|
+
this._mcp.registerTool('restart_session', {
|
|
832
|
+
description: 'Restart a session by disposing its browser page and dev server, then recreating them. ' +
|
|
833
|
+
'Use this when a session appears stuck (e.g. after a timeout).',
|
|
834
|
+
inputSchema: {
|
|
835
|
+
sessionName: z.string().optional().describe('Session name to restart (defaults to first session)'),
|
|
836
|
+
},
|
|
837
|
+
annotations: { destructiveHint: true },
|
|
838
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
839
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
840
|
+
this._log('info', { type: 'tool-call', tool: 'restart_session', sessionName });
|
|
841
|
+
const sessions = await daemon.methods.restartSession({ sessionName });
|
|
842
|
+
this._sessions = sessions;
|
|
843
|
+
return {
|
|
844
|
+
content: [{ type: 'text', text: `Session '${sessionName}' restarted.\n` + JSON.stringify(sessions, null, 2) }],
|
|
845
|
+
};
|
|
846
|
+
}));
|
|
847
|
+
}
|
|
848
|
+
_registerOpenSession() {
|
|
849
|
+
this._mcp.registerTool('open_session', {
|
|
850
|
+
description: 'Open a new worktree-backed session at a given git ref. ' +
|
|
851
|
+
'The ref can be a branch name, tag, commit SHA, or the special value "INDEX" to snapshot staged changes. ' +
|
|
852
|
+
'The daemon allocates a reusable worktree slot from a fixed pool (max configured in component-explorer.json). ' +
|
|
853
|
+
'Returns the updated session list on success.',
|
|
854
|
+
inputSchema: {
|
|
855
|
+
name: z.string().describe('Unique session name (e.g. "baseline", "bisect")'),
|
|
856
|
+
ref: z.string().describe('Git ref: branch, tag, commit SHA, or "INDEX" for staged changes'),
|
|
857
|
+
},
|
|
858
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
859
|
+
this._log('info', { type: 'tool-call', tool: 'open_session', name: args.name, ref: args.ref });
|
|
860
|
+
const result = await daemon.methods.openSession({ name: args.name, ref: args.ref });
|
|
861
|
+
if ('error' in result) {
|
|
862
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
863
|
+
}
|
|
864
|
+
this._sessions = result.sessions;
|
|
865
|
+
this._updateMultiSessionToolVisibility();
|
|
866
|
+
return {
|
|
867
|
+
content: [{ type: 'text', text: JSON.stringify(result.sessions, null, 2) }],
|
|
868
|
+
};
|
|
869
|
+
}, { noTimeout: true }));
|
|
870
|
+
}
|
|
871
|
+
_registerCloseSession() {
|
|
872
|
+
this._mcp.registerTool('close_session', {
|
|
873
|
+
description: 'Close a dynamic worktree session and release its worktree slot back to the pool. ' +
|
|
874
|
+
'Cannot close static sessions configured in component-explorer.json.',
|
|
875
|
+
inputSchema: {
|
|
876
|
+
name: z.string().describe('Session name to close'),
|
|
877
|
+
},
|
|
878
|
+
annotations: { destructiveHint: true },
|
|
879
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
880
|
+
this._log('info', { type: 'tool-call', tool: 'close_session', name: args.name });
|
|
881
|
+
const result = await daemon.methods.closeSession({ name: args.name });
|
|
882
|
+
if ('error' in result) {
|
|
883
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
884
|
+
}
|
|
885
|
+
this._sessions = result.sessions;
|
|
886
|
+
this._updateMultiSessionToolVisibility();
|
|
887
|
+
return {
|
|
888
|
+
content: [{ type: 'text', text: `Session '${args.name}' closed.\n` + JSON.stringify(result.sessions, null, 2) }],
|
|
889
|
+
};
|
|
890
|
+
}));
|
|
891
|
+
}
|
|
892
|
+
_registerUpdateSessionRef() {
|
|
893
|
+
this._mcp.registerTool('update_session_ref', {
|
|
894
|
+
description: 'Change the git ref of an existing dynamic session. ' +
|
|
895
|
+
'The worktree is checked out to the new ref and Vite\'s HMR handles the incremental update (no server restart). ' +
|
|
896
|
+
'Fails if the worktree has uncommitted changes — the error will list the dirty files. ' +
|
|
897
|
+
'The ref can be a branch, tag, commit SHA, or "INDEX" for staged changes.',
|
|
898
|
+
inputSchema: {
|
|
899
|
+
name: z.string().describe('Session name to update'),
|
|
900
|
+
ref: z.string().describe('New git ref: branch, tag, commit SHA, or "INDEX"'),
|
|
901
|
+
},
|
|
902
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
903
|
+
this._log('info', { type: 'tool-call', tool: 'update_session_ref', name: args.name, ref: args.ref });
|
|
904
|
+
const result = await daemon.methods.updateSessionRef({ name: args.name, ref: args.ref });
|
|
905
|
+
if ('error' in result) {
|
|
906
|
+
return { content: [{ type: 'text', text: result.error }], isError: true };
|
|
907
|
+
}
|
|
908
|
+
this._sessions = result.sessions;
|
|
909
|
+
return {
|
|
910
|
+
content: [{ type: 'text', text: JSON.stringify(result.sessions, null, 2) }],
|
|
911
|
+
};
|
|
912
|
+
}, { noTimeout: true }));
|
|
913
|
+
}
|
|
914
|
+
_registerGetUrl() {
|
|
915
|
+
this._mcp.registerTool('get_url', {
|
|
916
|
+
description: 'Get URL(s) for viewing fixtures. Returns the Component Explorer UI URL by default, ' +
|
|
917
|
+
'or the raw render URL for embedding/screenshots when useRawDirectRenderingWithoutExplorerUi is true.',
|
|
918
|
+
inputSchema: {
|
|
919
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
920
|
+
fixtureId: z.string().optional().describe('Specific fixture ID. If omitted, returns URL for the explorer root or all fixtures.'),
|
|
921
|
+
useRawDirectRenderingWithoutExplorerUi: z.boolean().optional().describe('If true, returns the raw rendering URL (for embedding or screenshots) instead of the Explorer UI URL. ' +
|
|
922
|
+
'The raw URL renders only the fixture without the explorer chrome. Default: false.'),
|
|
923
|
+
},
|
|
924
|
+
annotations: { readOnlyHint: true },
|
|
925
|
+
}, async (args) => {
|
|
926
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
927
|
+
let session = this._sessions.find(s => s.name === sessionName);
|
|
928
|
+
if (!session) {
|
|
929
|
+
const daemon = await this._waitForDaemon();
|
|
930
|
+
if (daemon) {
|
|
931
|
+
try {
|
|
932
|
+
await this._refreshSessions();
|
|
355
933
|
}
|
|
934
|
+
catch (e) {
|
|
935
|
+
if (isPipeConnectionError(e)) {
|
|
936
|
+
this._handleDisconnect();
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
throw e;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
session = this._sessions.find(s => s.name === sessionName);
|
|
944
|
+
if (!session) {
|
|
945
|
+
return {
|
|
946
|
+
content: [{
|
|
947
|
+
type: 'text',
|
|
948
|
+
text: `Error: Session '${sessionName}' not found. Available sessions: ${this._sessions.map(s => s.name).join(', ') || '(none)'}`,
|
|
949
|
+
}],
|
|
950
|
+
isError: true,
|
|
951
|
+
};
|
|
356
952
|
}
|
|
953
|
+
}
|
|
954
|
+
const baseUrl = session && !session.isLoading ? session.serverUrl : undefined;
|
|
955
|
+
if (!baseUrl) {
|
|
956
|
+
return {
|
|
957
|
+
content: [{
|
|
958
|
+
type: 'text',
|
|
959
|
+
text: `Error: Session '${sessionName}' is still loading.`,
|
|
960
|
+
}],
|
|
961
|
+
isError: true,
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
const useRaw = args.useRawDirectRenderingWithoutExplorerUi ?? false;
|
|
965
|
+
const url = buildExplorerUrl({
|
|
966
|
+
baseUrl,
|
|
967
|
+
rawRender: useRaw,
|
|
968
|
+
fixtureId: args.fixtureId,
|
|
969
|
+
});
|
|
970
|
+
const result = {
|
|
971
|
+
url,
|
|
972
|
+
sessionName,
|
|
973
|
+
};
|
|
974
|
+
if (args.fixtureId) {
|
|
975
|
+
result.fixtureId = args.fixtureId;
|
|
976
|
+
}
|
|
977
|
+
if (useRaw) {
|
|
978
|
+
result.mode = 'raw-render';
|
|
979
|
+
}
|
|
980
|
+
else {
|
|
981
|
+
result.mode = 'explorer';
|
|
982
|
+
if (args.fixtureId) {
|
|
983
|
+
result.note = 'Fixture selection in the Explorer UI is not URL-based. Navigate to the fixture manually in the tree view.';
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return {
|
|
987
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
988
|
+
};
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
_registerCheckStability() {
|
|
992
|
+
this._mcp.registerTool('check_stability', {
|
|
993
|
+
description: 'Check rendering stability of fixtures. Each fixture is unmounted, re-mounted, and screenshotted 3 times (~3s per fixture). ' +
|
|
994
|
+
'Returns results directly if finished within ~10s, otherwise returns a taskId for polling via check_task. ' +
|
|
995
|
+
'When returning a taskId, includes partial results collected so far.',
|
|
996
|
+
inputSchema: {
|
|
997
|
+
fixtureIdPattern: z.string().optional().describe('RegExp to filter fixtures by fixture ID'),
|
|
998
|
+
labelPattern: z.string().optional().describe('RegExp to filter fixtures by label (matched against inherited labels)'),
|
|
999
|
+
sessionName: z.string().optional().describe('Session name (defaults to first session)'),
|
|
1000
|
+
sourceTreeId: z.string().optional().describe('Source tree ID (defaults to latest known)'),
|
|
1001
|
+
},
|
|
1002
|
+
annotations: { readOnlyHint: true },
|
|
1003
|
+
}, async (args) => this._withDaemon(async (daemon) => {
|
|
1004
|
+
const sessionName = args.sessionName ?? this._defaultSessionName();
|
|
1005
|
+
this._log('debug', { type: 'tool-call', tool: 'check_stability', sessionName, fixtureIdPattern: args.fixtureIdPattern, labelPattern: args.labelPattern });
|
|
1006
|
+
return this._withSourceTreeRetry(async () => {
|
|
1007
|
+
const sourceTreeId = args.sourceTreeId ?? this._sourceTreeId(sessionName);
|
|
1008
|
+
const allFixtures = await daemon.methods.fixtures.list({ sessionName, sourceTreeId });
|
|
1009
|
+
const filtered = this._filterFixtures(allFixtures, args.fixtureIdPattern, args.labelPattern);
|
|
1010
|
+
if ('error' in filtered) {
|
|
1011
|
+
return { content: [{ type: 'text', text: filtered.error }], isError: true };
|
|
1012
|
+
}
|
|
1013
|
+
const fixtures = filtered.fixtures;
|
|
1014
|
+
this._log('info', { type: 'check-stability-start', total: fixtures.length, filtered: allFixtures.length - fixtures.length });
|
|
1015
|
+
const task = this._taskManager.startTask(async (report, signal) => {
|
|
1016
|
+
const results = [];
|
|
1017
|
+
report({ completed: 0, total: fixtures.length, partialResult: results });
|
|
1018
|
+
for (let i = 0; i < fixtures.length; i++) {
|
|
1019
|
+
if (signal.aborted) {
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
const fixture = fixtures[i];
|
|
1023
|
+
this._log('info', { type: 'check-stability-progress', fixtureId: fixture.fixtureId, index: i + 1, total: fixtures.length });
|
|
1024
|
+
const result = await daemon.methods.screenshots.take({
|
|
1025
|
+
fixtureId: fixture.fixtureId,
|
|
1026
|
+
sessionName,
|
|
1027
|
+
sourceTreeId,
|
|
1028
|
+
includeImage: false,
|
|
1029
|
+
stabilityCheck: true,
|
|
1030
|
+
});
|
|
1031
|
+
const r = result;
|
|
1032
|
+
results.push({
|
|
1033
|
+
fixtureId: fixture.fixtureId,
|
|
1034
|
+
isStable: r.isStable ?? true,
|
|
1035
|
+
screenshots: r.stabilityScreenshots?.map(s => ({ hash: s.hash, delayMs: s.delayMs })) ?? [],
|
|
1036
|
+
});
|
|
1037
|
+
report({ completed: i + 1, total: fixtures.length, partialResult: results });
|
|
1038
|
+
}
|
|
1039
|
+
const stable = results.filter(r => r.isStable).length;
|
|
1040
|
+
return {
|
|
1041
|
+
fixtures: results,
|
|
1042
|
+
summary: { total: results.length, stable, unstable: results.length - stable },
|
|
1043
|
+
};
|
|
1044
|
+
});
|
|
1045
|
+
const waited = await this._taskManager.waitForTask(task.id, 10_000);
|
|
1046
|
+
if (!waited) {
|
|
1047
|
+
return { content: [{ type: 'text', text: 'Error: task disappeared' }], isError: true };
|
|
1048
|
+
}
|
|
1049
|
+
if (waited.done) {
|
|
1050
|
+
return {
|
|
1051
|
+
content: [{ type: 'text', text: JSON.stringify(waited.result, null, 2) }],
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
const partial = waited.progress.partialResult;
|
|
1055
|
+
this._taskLastReportedIndex.set(task.id, partial.length);
|
|
357
1056
|
return {
|
|
358
1057
|
content: [{
|
|
359
1058
|
type: 'text',
|
|
360
1059
|
text: JSON.stringify({
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
1060
|
+
taskId: task.id,
|
|
1061
|
+
status: 'running',
|
|
1062
|
+
progress: { completed: waited.progress.completed, total: waited.progress.total },
|
|
1063
|
+
elapsedMs: waited.elapsedMs,
|
|
1064
|
+
results: partial,
|
|
364
1065
|
}, null, 2),
|
|
365
1066
|
}],
|
|
366
1067
|
};
|
|
1068
|
+
});
|
|
1069
|
+
}));
|
|
1070
|
+
}
|
|
1071
|
+
_registerCheckTask() {
|
|
1072
|
+
this._mcp.registerTool('check_task', {
|
|
1073
|
+
description: 'Check on a running task. Waits up to ~2s for completion; if still running, returns progress and new results since last check.',
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
taskId: z.string().describe('The task ID returned by a previous tool call'),
|
|
1076
|
+
},
|
|
1077
|
+
annotations: { readOnlyHint: true },
|
|
1078
|
+
}, async (args) => {
|
|
1079
|
+
const waited = await this._taskManager.waitForTask(args.taskId, 2_000);
|
|
1080
|
+
if (!waited) {
|
|
1081
|
+
this._taskLastReportedIndex.delete(args.taskId);
|
|
1082
|
+
return {
|
|
1083
|
+
content: [{ type: 'text', text: `Error: No task found with id '${args.taskId}'` }],
|
|
1084
|
+
isError: true,
|
|
1085
|
+
};
|
|
367
1086
|
}
|
|
1087
|
+
if (waited.done) {
|
|
1088
|
+
const lastIndex = this._taskLastReportedIndex.get(args.taskId) ?? 0;
|
|
1089
|
+
this._taskLastReportedIndex.delete(args.taskId);
|
|
1090
|
+
const fullResult = waited.result;
|
|
1091
|
+
const newResults = fullResult.fixtures.slice(lastIndex);
|
|
1092
|
+
return {
|
|
1093
|
+
content: [{
|
|
1094
|
+
type: 'text',
|
|
1095
|
+
text: JSON.stringify({
|
|
1096
|
+
status: 'done',
|
|
1097
|
+
newResults,
|
|
1098
|
+
summary: fullResult.summary,
|
|
1099
|
+
}, null, 2),
|
|
1100
|
+
}],
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
const partial = waited.progress.partialResult;
|
|
1104
|
+
const lastIndex = this._taskLastReportedIndex.get(args.taskId) ?? 0;
|
|
1105
|
+
const newResults = partial.slice(lastIndex);
|
|
1106
|
+
this._taskLastReportedIndex.set(args.taskId, partial.length);
|
|
368
1107
|
return {
|
|
369
|
-
content: [{
|
|
1108
|
+
content: [{
|
|
1109
|
+
type: 'text',
|
|
1110
|
+
text: JSON.stringify({
|
|
1111
|
+
taskId: args.taskId,
|
|
1112
|
+
status: 'running',
|
|
1113
|
+
progress: { completed: waited.progress.completed, total: waited.progress.total },
|
|
1114
|
+
elapsedMs: waited.elapsedMs,
|
|
1115
|
+
newResults,
|
|
1116
|
+
}, null, 2),
|
|
1117
|
+
}],
|
|
370
1118
|
};
|
|
371
1119
|
});
|
|
372
1120
|
}
|
|
373
|
-
|
|
374
|
-
this._mcp.registerTool('
|
|
375
|
-
description: '
|
|
376
|
-
|
|
377
|
-
|
|
1121
|
+
_registerCancelTask() {
|
|
1122
|
+
this._mcp.registerTool('cancel_task', {
|
|
1123
|
+
description: 'Cancel a running task',
|
|
1124
|
+
inputSchema: {
|
|
1125
|
+
taskId: z.string().describe('The task ID to cancel'),
|
|
1126
|
+
},
|
|
1127
|
+
}, async (args) => {
|
|
1128
|
+
const task = this._taskManager.getTask(args.taskId);
|
|
1129
|
+
if (!task) {
|
|
1130
|
+
return {
|
|
1131
|
+
content: [{ type: 'text', text: `Error: No task found with id '${args.taskId}'` }],
|
|
1132
|
+
isError: true,
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
this._taskManager.removeTask(args.taskId);
|
|
378
1136
|
return {
|
|
379
|
-
content: [{ type: 'text', text:
|
|
1137
|
+
content: [{ type: 'text', text: `Task '${args.taskId}' cancelled.` }],
|
|
380
1138
|
};
|
|
381
1139
|
});
|
|
382
1140
|
}
|
|
383
1141
|
}
|
|
384
1142
|
|
|
385
|
-
export { ComponentExplorerMcpServer };
|
|
1143
|
+
export { ComponentExplorerMcpServer, DaemonConnection };
|
|
386
1144
|
//# sourceMappingURL=McpServer.js.map
|