@dynamicu/chromedebug-mcp 2.2.0
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/CLAUDE.md +344 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/chrome-extension/README.md +41 -0
- package/chrome-extension/background.js +3917 -0
- package/chrome-extension/chrome-session-manager.js +706 -0
- package/chrome-extension/content.css +181 -0
- package/chrome-extension/content.js +3022 -0
- package/chrome-extension/data-buffer.js +435 -0
- package/chrome-extension/dom-tracker.js +411 -0
- package/chrome-extension/extension-config.js +78 -0
- package/chrome-extension/firebase-client.js +278 -0
- package/chrome-extension/firebase-config.js +32 -0
- package/chrome-extension/firebase-config.module.js +22 -0
- package/chrome-extension/firebase-config.module.template.js +27 -0
- package/chrome-extension/firebase-config.template.js +36 -0
- package/chrome-extension/frame-capture.js +407 -0
- package/chrome-extension/icon128.png +1 -0
- package/chrome-extension/icon16.png +1 -0
- package/chrome-extension/icon48.png +1 -0
- package/chrome-extension/license-helper.js +181 -0
- package/chrome-extension/logger.js +23 -0
- package/chrome-extension/manifest.json +73 -0
- package/chrome-extension/network-tracker.js +510 -0
- package/chrome-extension/offscreen.html +10 -0
- package/chrome-extension/options.html +203 -0
- package/chrome-extension/options.js +282 -0
- package/chrome-extension/pako.min.js +2 -0
- package/chrome-extension/performance-monitor.js +533 -0
- package/chrome-extension/pii-redactor.js +405 -0
- package/chrome-extension/popup.html +532 -0
- package/chrome-extension/popup.js +2446 -0
- package/chrome-extension/upload-manager.js +323 -0
- package/chrome-extension/web-vitals.iife.js +1 -0
- package/config/api-keys.json +11 -0
- package/config/chrome-pilot-config.json +45 -0
- package/package.json +126 -0
- package/scripts/cleanup-processes.js +109 -0
- package/scripts/config-manager.js +280 -0
- package/scripts/generate-extension-config.js +53 -0
- package/scripts/setup-security.js +64 -0
- package/src/capture/architecture.js +426 -0
- package/src/capture/error-handling-tests.md +38 -0
- package/src/capture/error-handling-types.ts +360 -0
- package/src/capture/index.js +508 -0
- package/src/capture/interfaces.js +625 -0
- package/src/capture/memory-manager.js +713 -0
- package/src/capture/types.js +342 -0
- package/src/chrome-controller.js +2658 -0
- package/src/cli.js +19 -0
- package/src/config-loader.js +303 -0
- package/src/database.js +2178 -0
- package/src/firebase-license-manager.js +462 -0
- package/src/firebase-privacy-guard.js +397 -0
- package/src/http-server.js +1516 -0
- package/src/index-direct.js +157 -0
- package/src/index-modular.js +219 -0
- package/src/index-monolithic-backup.js +2230 -0
- package/src/index.js +305 -0
- package/src/legacy/chrome-controller-old.js +1406 -0
- package/src/legacy/index-express.js +625 -0
- package/src/legacy/index-old.js +977 -0
- package/src/legacy/routes.js +260 -0
- package/src/legacy/shared-storage.js +101 -0
- package/src/logger.js +10 -0
- package/src/mcp/handlers/chrome-tool-handler.js +306 -0
- package/src/mcp/handlers/element-tool-handler.js +51 -0
- package/src/mcp/handlers/frame-tool-handler.js +957 -0
- package/src/mcp/handlers/request-handler.js +104 -0
- package/src/mcp/handlers/workflow-tool-handler.js +636 -0
- package/src/mcp/server.js +68 -0
- package/src/mcp/tools/index.js +701 -0
- package/src/middleware/auth.js +371 -0
- package/src/middleware/security.js +267 -0
- package/src/port-discovery.js +258 -0
- package/src/routes/admin.js +182 -0
- package/src/services/browser-daemon.js +494 -0
- package/src/services/chrome-service.js +375 -0
- package/src/services/failover-manager.js +412 -0
- package/src/services/git-safety-service.js +675 -0
- package/src/services/heartbeat-manager.js +200 -0
- package/src/services/http-client.js +195 -0
- package/src/services/process-manager.js +318 -0
- package/src/services/process-tracker.js +574 -0
- package/src/services/profile-manager.js +449 -0
- package/src/services/project-manager.js +415 -0
- package/src/services/session-manager.js +497 -0
- package/src/services/session-registry.js +491 -0
- package/src/services/unified-session-manager.js +678 -0
- package/src/shared-storage-old.js +267 -0
- package/src/standalone-server.js +53 -0
- package/src/utils/extension-path.js +145 -0
- package/src/utils.js +187 -0
- package/src/validation/log-transformer.js +125 -0
- package/src/validation/schemas.js +391 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Tool Handler - Handles workflow recording and restore point tools
|
|
3
|
+
* Extracted from original index.js with preserved functionality
|
|
4
|
+
* Now uses HTTP client for authenticated access to recordings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { httpClient } from '../../services/http-client.js';
|
|
8
|
+
|
|
9
|
+
export class WorkflowToolHandler {
|
|
10
|
+
constructor(chromeController) {
|
|
11
|
+
this.chromeController = chromeController;
|
|
12
|
+
this.useHttpClient = true; // Flag to use HTTP client instead of direct database access
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Handles workflow-related tool calls
|
|
17
|
+
* @param {string} name - Tool name
|
|
18
|
+
* @param {Object} args - Tool arguments
|
|
19
|
+
* @returns {Object} Tool execution result
|
|
20
|
+
*/
|
|
21
|
+
async handle(name, args) {
|
|
22
|
+
switch (name) {
|
|
23
|
+
case 'get_workflow_recording':
|
|
24
|
+
return await this.handleGetWorkflowRecording(args);
|
|
25
|
+
|
|
26
|
+
case 'list_workflow_recordings':
|
|
27
|
+
return await this.handleListWorkflowRecordings();
|
|
28
|
+
|
|
29
|
+
case 'save_restore_point':
|
|
30
|
+
return await this.handleSaveRestorePoint(args);
|
|
31
|
+
|
|
32
|
+
case 'restore_from_point':
|
|
33
|
+
return await this.handleRestoreFromPoint(args);
|
|
34
|
+
|
|
35
|
+
case 'list_restore_points':
|
|
36
|
+
return await this.handleListRestorePoints(args);
|
|
37
|
+
|
|
38
|
+
case 'play_workflow_recording':
|
|
39
|
+
return await this.handlePlayWorkflowRecording(args);
|
|
40
|
+
|
|
41
|
+
case 'play_workflow_by_name':
|
|
42
|
+
return await this.handlePlayWorkflowByName(args);
|
|
43
|
+
|
|
44
|
+
case 'get_workflow_function_traces':
|
|
45
|
+
return await this.handleGetWorkflowFunctionTraces(args);
|
|
46
|
+
|
|
47
|
+
case 'get_workflow_errors':
|
|
48
|
+
return await this.handleGetWorkflowErrors(args);
|
|
49
|
+
|
|
50
|
+
case 'get_workflow_summary':
|
|
51
|
+
return await this.handleGetWorkflowSummary(args);
|
|
52
|
+
|
|
53
|
+
case 'get_workflow_actions_filtered':
|
|
54
|
+
return await this.handleGetWorkflowActionsFiltered(args);
|
|
55
|
+
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`Unknown workflow tool: ${name}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle get workflow recording
|
|
63
|
+
* @param {Object} args - Arguments with sessionId
|
|
64
|
+
* @returns {Object} Formatted workflow recording
|
|
65
|
+
*/
|
|
66
|
+
async handleGetWorkflowRecording(args) {
|
|
67
|
+
if (!args.sessionId) {
|
|
68
|
+
throw new Error('Session ID is required');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let recording;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
if (this.useHttpClient) {
|
|
75
|
+
recording = await httpClient.getWorkflowRecording(args.sessionId);
|
|
76
|
+
} else {
|
|
77
|
+
recording = await this.chromeController.getWorkflowRecording(args.sessionId);
|
|
78
|
+
}
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.warn('[WorkflowToolHandler] HTTP client failed, falling back to direct access:', error.message);
|
|
81
|
+
recording = await this.chromeController.getWorkflowRecording(args.sessionId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (recording.error) {
|
|
85
|
+
throw new Error(recording.error);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const formattedRecording = {
|
|
89
|
+
sessionId: recording.sessionId,
|
|
90
|
+
url: recording.url,
|
|
91
|
+
title: recording.title,
|
|
92
|
+
timestamp: new Date(recording.timestamp).toISOString(),
|
|
93
|
+
totalActions: recording.totalActions,
|
|
94
|
+
actions: recording.actions || [],
|
|
95
|
+
logs: recording.logs || []
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return formattedRecording;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle list workflow recordings
|
|
103
|
+
* @returns {Object} List of workflow recordings
|
|
104
|
+
*/
|
|
105
|
+
async handleListWorkflowRecordings() {
|
|
106
|
+
let result;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (this.useHttpClient) {
|
|
110
|
+
result = await httpClient.listWorkflowRecordings();
|
|
111
|
+
} else {
|
|
112
|
+
result = await this.chromeController.listWorkflowRecordings();
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.warn('[WorkflowToolHandler] HTTP client failed, falling back to direct access:', error.message);
|
|
116
|
+
result = await this.chromeController.listWorkflowRecordings();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const recordings = result.recordings || [];
|
|
120
|
+
|
|
121
|
+
if (recordings.length === 0) {
|
|
122
|
+
return {
|
|
123
|
+
message: 'No workflow recordings found.',
|
|
124
|
+
recordings: []
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const formattedRecordings = recordings.map((recording, index) => ({
|
|
129
|
+
index: index + 1,
|
|
130
|
+
name: recording.name || recording.session_id,
|
|
131
|
+
sessionId: recording.session_id,
|
|
132
|
+
url: recording.url,
|
|
133
|
+
title: recording.title,
|
|
134
|
+
totalActions: recording.total_actions,
|
|
135
|
+
timestamp: new Date(recording.timestamp).toISOString(),
|
|
136
|
+
hasScreenshots: !!recording.screenshot_settings
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
message: `Found ${recordings.length} workflow recordings`,
|
|
141
|
+
count: recordings.length,
|
|
142
|
+
recordings: formattedRecordings
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Handle save restore point
|
|
148
|
+
* @param {Object} args - Arguments with workflowId, actionIndex, description
|
|
149
|
+
* @returns {Object} Save result
|
|
150
|
+
*/
|
|
151
|
+
async handleSaveRestorePoint(args) {
|
|
152
|
+
if (!args.workflowId) {
|
|
153
|
+
throw new Error('Workflow ID is required');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Capture current browser state
|
|
157
|
+
const page = await this.chromeController.getPage();
|
|
158
|
+
if (!page) {
|
|
159
|
+
throw new Error('No page available. Please launch Chrome and navigate to a URL first.');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Execute content script to capture state
|
|
163
|
+
const captureResult = await page.evaluate(() => {
|
|
164
|
+
const captureState = () => {
|
|
165
|
+
const state = {
|
|
166
|
+
url: window.location.href,
|
|
167
|
+
title: document.title,
|
|
168
|
+
domSnapshot: {
|
|
169
|
+
html: document.documentElement.outerHTML,
|
|
170
|
+
formData: {},
|
|
171
|
+
checkboxStates: {},
|
|
172
|
+
radioStates: {},
|
|
173
|
+
selectValues: {},
|
|
174
|
+
textareaValues: {},
|
|
175
|
+
},
|
|
176
|
+
scrollX: window.scrollX,
|
|
177
|
+
scrollY: window.scrollY,
|
|
178
|
+
localStorage: {},
|
|
179
|
+
sessionStorage: {},
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Capture form values
|
|
183
|
+
document.querySelectorAll('input').forEach(input => {
|
|
184
|
+
const id = input.id || input.name || Math.random().toString(36);
|
|
185
|
+
if (input.type === 'checkbox') {
|
|
186
|
+
state.domSnapshot.checkboxStates[id] = input.checked;
|
|
187
|
+
} else if (input.type === 'radio') {
|
|
188
|
+
state.domSnapshot.radioStates[id] = {
|
|
189
|
+
checked: input.checked,
|
|
190
|
+
value: input.value,
|
|
191
|
+
};
|
|
192
|
+
} else if (input.type !== 'file' && input.type !== 'password') {
|
|
193
|
+
state.domSnapshot.formData[id] = input.value;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Capture select values
|
|
198
|
+
document.querySelectorAll('select').forEach(select => {
|
|
199
|
+
const id = select.id || select.name || Math.random().toString(36);
|
|
200
|
+
state.domSnapshot.selectValues[id] = select.value;
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Capture textarea values
|
|
204
|
+
document.querySelectorAll('textarea').forEach(textarea => {
|
|
205
|
+
const id = textarea.id || textarea.name || Math.random().toString(36);
|
|
206
|
+
state.domSnapshot.textareaValues[id] = textarea.value;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Capture storage
|
|
210
|
+
try {
|
|
211
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
212
|
+
const key = localStorage.key(i);
|
|
213
|
+
state.localStorage[key] = localStorage.getItem(key);
|
|
214
|
+
}
|
|
215
|
+
} catch (e) {}
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
219
|
+
const key = sessionStorage.key(i);
|
|
220
|
+
state.sessionStorage[key] = sessionStorage.getItem(key);
|
|
221
|
+
}
|
|
222
|
+
} catch (e) {}
|
|
223
|
+
|
|
224
|
+
return state;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return captureState();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Get cookies
|
|
231
|
+
const cookies = await page.cookies();
|
|
232
|
+
captureResult.cookies = cookies;
|
|
233
|
+
|
|
234
|
+
// Save restore point
|
|
235
|
+
const restoreData = {
|
|
236
|
+
workflowId: args.workflowId,
|
|
237
|
+
actionIndex: args.actionIndex || 0,
|
|
238
|
+
...captureResult,
|
|
239
|
+
description: args.description,
|
|
240
|
+
timestamp: Date.now(),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const result = await this.chromeController.saveRestorePoint(restoreData);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
message: 'Restore point saved successfully!',
|
|
247
|
+
restorePointId: result.restorePointId,
|
|
248
|
+
workflowId: args.workflowId,
|
|
249
|
+
description: args.description || 'N/A'
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Handle restore from point
|
|
255
|
+
* @param {Object} args - Arguments with restorePointId
|
|
256
|
+
* @returns {Object} Restore result
|
|
257
|
+
*/
|
|
258
|
+
async handleRestoreFromPoint(args) {
|
|
259
|
+
if (!args.restorePointId) {
|
|
260
|
+
throw new Error('Restore point ID is required');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Get restore point data
|
|
264
|
+
const restoreData = await this.chromeController.getRestorePoint(args.restorePointId);
|
|
265
|
+
if (restoreData.error) {
|
|
266
|
+
throw new Error(restoreData.error);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const page = await this.chromeController.getPage();
|
|
270
|
+
if (!page) {
|
|
271
|
+
throw new Error('No page available. Please launch Chrome first.');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Navigate if needed
|
|
275
|
+
const currentUrl = await page.url();
|
|
276
|
+
if (currentUrl !== restoreData.url) {
|
|
277
|
+
await page.goto(restoreData.url, { waitUntil: 'networkidle0' });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Restore state
|
|
281
|
+
await page.evaluate((data) => {
|
|
282
|
+
// Restore storage
|
|
283
|
+
try {
|
|
284
|
+
localStorage.clear();
|
|
285
|
+
Object.entries(data.localStorage).forEach(([key, value]) => {
|
|
286
|
+
localStorage.setItem(key, value);
|
|
287
|
+
});
|
|
288
|
+
} catch (e) {}
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
sessionStorage.clear();
|
|
292
|
+
Object.entries(data.sessionStorage).forEach(([key, value]) => {
|
|
293
|
+
sessionStorage.setItem(key, value);
|
|
294
|
+
});
|
|
295
|
+
} catch (e) {}
|
|
296
|
+
|
|
297
|
+
// Restore form values
|
|
298
|
+
const snapshot = data.domSnapshot;
|
|
299
|
+
Object.entries(snapshot.formData).forEach(([id, value]) => {
|
|
300
|
+
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
301
|
+
if (element) element.value = value;
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
Object.entries(snapshot.checkboxStates).forEach(([id, checked]) => {
|
|
305
|
+
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
306
|
+
if (element) element.checked = checked;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
Object.entries(snapshot.radioStates).forEach(([id, state]) => {
|
|
310
|
+
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
311
|
+
if (element && state.checked) element.checked = true;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
Object.entries(snapshot.selectValues).forEach(([id, value]) => {
|
|
315
|
+
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
316
|
+
if (element) element.value = value;
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
Object.entries(snapshot.textareaValues).forEach(([id, value]) => {
|
|
320
|
+
const element = document.getElementById(id) || document.querySelector(`[name="${id}"]`);
|
|
321
|
+
if (element) element.value = value;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Restore scroll
|
|
325
|
+
window.scrollTo(data.scrollX, data.scrollY);
|
|
326
|
+
}, restoreData);
|
|
327
|
+
|
|
328
|
+
// Restore cookies
|
|
329
|
+
if (restoreData.cookies && restoreData.cookies.length > 0) {
|
|
330
|
+
for (const cookie of restoreData.cookies) {
|
|
331
|
+
await page.setCookie(cookie);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
message: 'Successfully restored from restore point!',
|
|
337
|
+
url: restoreData.url,
|
|
338
|
+
title: restoreData.title,
|
|
339
|
+
timestamp: new Date(restoreData.timestamp).toISOString()
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Handle list restore points
|
|
345
|
+
* @param {Object} args - Arguments with workflowId
|
|
346
|
+
* @returns {Object} List of restore points
|
|
347
|
+
*/
|
|
348
|
+
async handleListRestorePoints(args) {
|
|
349
|
+
if (!args.workflowId) {
|
|
350
|
+
throw new Error('Workflow ID is required');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const restorePoints = await this.chromeController.listRestorePoints(args.workflowId);
|
|
354
|
+
|
|
355
|
+
if (restorePoints.length === 0) {
|
|
356
|
+
return {
|
|
357
|
+
message: `No restore points found for workflow: ${args.workflowId}`,
|
|
358
|
+
workflowId: args.workflowId,
|
|
359
|
+
restorePoints: []
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const formattedPoints = restorePoints.map((rp, index) => ({
|
|
364
|
+
index: index + 1,
|
|
365
|
+
id: rp.id,
|
|
366
|
+
actionIndex: rp.actionIndex,
|
|
367
|
+
url: rp.url,
|
|
368
|
+
title: rp.title || 'N/A',
|
|
369
|
+
description: rp.description || 'N/A',
|
|
370
|
+
timestamp: new Date(rp.timestamp).toISOString()
|
|
371
|
+
}));
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
message: `Found ${restorePoints.length} restore points for workflow ${args.workflowId}`,
|
|
375
|
+
workflowId: args.workflowId,
|
|
376
|
+
count: restorePoints.length,
|
|
377
|
+
restorePoints: formattedPoints
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Handle play workflow recording
|
|
383
|
+
* @param {Object} args - Arguments with sessionId and speed
|
|
384
|
+
* @returns {Object} Playback result
|
|
385
|
+
*/
|
|
386
|
+
async handlePlayWorkflowRecording(args) {
|
|
387
|
+
if (!args.sessionId) {
|
|
388
|
+
throw new Error('Session ID is required');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const speed = args.speed || 1;
|
|
392
|
+
const result = await this.chromeController.playWorkflowRecording(args.sessionId, speed);
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
message: 'Workflow playback completed',
|
|
396
|
+
sessionId: args.sessionId,
|
|
397
|
+
speed: speed,
|
|
398
|
+
result: result
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Handle play workflow by name
|
|
404
|
+
* @param {Object} args - Arguments with name and speed
|
|
405
|
+
* @returns {Object} Playback result
|
|
406
|
+
*/
|
|
407
|
+
async handlePlayWorkflowByName(args) {
|
|
408
|
+
if (!args.name) {
|
|
409
|
+
throw new Error('Workflow name is required');
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const speed = args.speed || 1;
|
|
413
|
+
const result = await this.chromeController.playWorkflowByName(args.name, speed);
|
|
414
|
+
|
|
415
|
+
return {
|
|
416
|
+
message: 'Workflow playback completed',
|
|
417
|
+
workflowName: args.name,
|
|
418
|
+
speed: speed,
|
|
419
|
+
result: result
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Get only function execution traces from a workflow
|
|
425
|
+
* @param {Object} args - Arguments with sessionId, includeStack, limit
|
|
426
|
+
* @returns {Object} Function traces
|
|
427
|
+
*/
|
|
428
|
+
async handleGetWorkflowFunctionTraces(args) {
|
|
429
|
+
if (!args.sessionId) {
|
|
430
|
+
throw new Error('Session ID is required');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Get the full recording first
|
|
434
|
+
const recording = await this.chromeController.getWorkflowRecording(args.sessionId);
|
|
435
|
+
if (recording.error) {
|
|
436
|
+
throw new Error(recording.error);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Filter for function traces only
|
|
440
|
+
let functionTraces = recording.actions.filter(action => action.type === 'function-trace');
|
|
441
|
+
|
|
442
|
+
// Apply limit if specified
|
|
443
|
+
if (args.limit && args.limit > 0) {
|
|
444
|
+
functionTraces = functionTraces.slice(0, args.limit);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Remove stack traces if not requested
|
|
448
|
+
if (!args.includeStack) {
|
|
449
|
+
functionTraces = functionTraces.map(trace => {
|
|
450
|
+
const { stack, ...traceWithoutStack } = trace;
|
|
451
|
+
return traceWithoutStack;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
sessionId: args.sessionId,
|
|
457
|
+
totalFunctionTraces: functionTraces.length,
|
|
458
|
+
url: recording.url,
|
|
459
|
+
title: recording.title,
|
|
460
|
+
functionTraces: functionTraces
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Get only error logs and error-related actions
|
|
466
|
+
* @param {Object} args - Arguments with sessionId, includeWarnings
|
|
467
|
+
* @returns {Object} Error information
|
|
468
|
+
*/
|
|
469
|
+
async handleGetWorkflowErrors(args) {
|
|
470
|
+
if (!args.sessionId) {
|
|
471
|
+
throw new Error('Session ID is required');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const recording = await this.chromeController.getWorkflowRecording(args.sessionId);
|
|
475
|
+
if (recording.error) {
|
|
476
|
+
throw new Error(recording.error);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Filter logs for errors (and optionally warnings)
|
|
480
|
+
let errorLogs = recording.logs.filter(log => log.level === 'error');
|
|
481
|
+
if (args.includeWarnings) {
|
|
482
|
+
const warningLogs = recording.logs.filter(log => log.level === 'warn');
|
|
483
|
+
errorLogs = [...errorLogs, ...warningLogs];
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Sort by timestamp
|
|
487
|
+
errorLogs.sort((a, b) => a.timestamp - b.timestamp);
|
|
488
|
+
|
|
489
|
+
// Find actions that happened around error times (within 1 second)
|
|
490
|
+
const errorRelatedActions = [];
|
|
491
|
+
for (const errorLog of errorLogs) {
|
|
492
|
+
const nearbyActions = recording.actions.filter(action =>
|
|
493
|
+
Math.abs(action.timestamp - errorLog.timestamp) < 1000
|
|
494
|
+
);
|
|
495
|
+
if (nearbyActions.length > 0) {
|
|
496
|
+
errorRelatedActions.push({
|
|
497
|
+
error: errorLog,
|
|
498
|
+
nearbyActions: nearbyActions
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
sessionId: args.sessionId,
|
|
505
|
+
url: recording.url,
|
|
506
|
+
title: recording.title,
|
|
507
|
+
totalErrors: errorLogs.length,
|
|
508
|
+
errorLogs: errorLogs,
|
|
509
|
+
errorRelatedActions: errorRelatedActions
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Get a concise summary of a workflow recording
|
|
515
|
+
* @param {Object} args - Arguments with sessionId
|
|
516
|
+
* @returns {Object} Workflow summary
|
|
517
|
+
*/
|
|
518
|
+
async handleGetWorkflowSummary(args) {
|
|
519
|
+
if (!args.sessionId) {
|
|
520
|
+
throw new Error('Session ID is required');
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const recording = await this.chromeController.getWorkflowRecording(args.sessionId);
|
|
524
|
+
if (recording.error) {
|
|
525
|
+
throw new Error(recording.error);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Count action types
|
|
529
|
+
const actionCounts = {};
|
|
530
|
+
for (const action of recording.actions) {
|
|
531
|
+
actionCounts[action.type] = (actionCounts[action.type] || 0) + 1;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Count log levels
|
|
535
|
+
const logCounts = {};
|
|
536
|
+
for (const log of recording.logs) {
|
|
537
|
+
logCounts[log.level] = (logCounts[log.level] || 0) + 1;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Find unique components from function traces
|
|
541
|
+
const uniqueComponents = new Set();
|
|
542
|
+
const functionTraces = recording.actions.filter(a => a.type === 'function-trace');
|
|
543
|
+
functionTraces.forEach(trace => {
|
|
544
|
+
if (trace.component) {
|
|
545
|
+
uniqueComponents.add(trace.component);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Calculate duration
|
|
550
|
+
const startTime = Math.min(...recording.actions.map(a => a.timestamp));
|
|
551
|
+
const endTime = Math.max(...recording.actions.map(a => a.timestamp));
|
|
552
|
+
const durationMs = endTime - startTime;
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
sessionId: args.sessionId,
|
|
556
|
+
name: recording.name,
|
|
557
|
+
url: recording.url,
|
|
558
|
+
title: recording.title,
|
|
559
|
+
timestamp: recording.timestamp,
|
|
560
|
+
duration: {
|
|
561
|
+
ms: durationMs,
|
|
562
|
+
seconds: (durationMs / 1000).toFixed(2),
|
|
563
|
+
readable: `${Math.floor(durationMs / 1000)}s`
|
|
564
|
+
},
|
|
565
|
+
totalActions: recording.actions.length,
|
|
566
|
+
actionBreakdown: actionCounts,
|
|
567
|
+
totalLogs: recording.logs.length,
|
|
568
|
+
logBreakdown: logCounts,
|
|
569
|
+
hasErrors: (logCounts.error || 0) > 0,
|
|
570
|
+
errorCount: logCounts.error || 0,
|
|
571
|
+
warningCount: logCounts.warn || 0,
|
|
572
|
+
functionTraceCount: actionCounts['function-trace'] || 0,
|
|
573
|
+
uniqueComponentsTraced: Array.from(uniqueComponents),
|
|
574
|
+
userInteractions: {
|
|
575
|
+
clicks: actionCounts.click || 0,
|
|
576
|
+
inputs: actionCounts.input || 0,
|
|
577
|
+
keypresses: actionCounts.keypress || 0,
|
|
578
|
+
scrolls: actionCounts.scroll || 0
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Get filtered workflow actions
|
|
585
|
+
* @param {Object} args - Arguments with sessionId, actionTypes, startIndex, limit, includeDetails
|
|
586
|
+
* @returns {Object} Filtered actions
|
|
587
|
+
*/
|
|
588
|
+
async handleGetWorkflowActionsFiltered(args) {
|
|
589
|
+
if (!args.sessionId) {
|
|
590
|
+
throw new Error('Session ID is required');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const recording = await this.chromeController.getWorkflowRecording(args.sessionId);
|
|
594
|
+
if (recording.error) {
|
|
595
|
+
throw new Error(recording.error);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
let filteredActions = recording.actions;
|
|
599
|
+
|
|
600
|
+
// Filter by action types if specified
|
|
601
|
+
if (args.actionTypes && args.actionTypes.length > 0) {
|
|
602
|
+
filteredActions = filteredActions.filter(action =>
|
|
603
|
+
args.actionTypes.includes(action.type)
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Apply pagination
|
|
608
|
+
const startIndex = args.startIndex || 0;
|
|
609
|
+
const limit = args.limit || 100;
|
|
610
|
+
const paginatedActions = filteredActions.slice(startIndex, startIndex + limit);
|
|
611
|
+
|
|
612
|
+
// Simplify actions if details not requested
|
|
613
|
+
let resultActions = paginatedActions;
|
|
614
|
+
if (!args.includeDetails) {
|
|
615
|
+
resultActions = paginatedActions.map(action => ({
|
|
616
|
+
type: action.type,
|
|
617
|
+
timestamp: action.timestamp,
|
|
618
|
+
component: action.component,
|
|
619
|
+
selector: action.selector,
|
|
620
|
+
value: action.value,
|
|
621
|
+
text: action.text
|
|
622
|
+
}));
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return {
|
|
626
|
+
sessionId: args.sessionId,
|
|
627
|
+
url: recording.url,
|
|
628
|
+
title: recording.title,
|
|
629
|
+
totalMatchingActions: filteredActions.length,
|
|
630
|
+
startIndex: startIndex,
|
|
631
|
+
returnedCount: resultActions.length,
|
|
632
|
+
hasMore: startIndex + limit < filteredActions.length,
|
|
633
|
+
actions: resultActions
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import {
|
|
4
|
+
ListToolsRequestSchema,
|
|
5
|
+
CallToolRequestSchema
|
|
6
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
|
|
8
|
+
import { ToolRegistry } from './tools/index.js';
|
|
9
|
+
import { RequestHandler } from './handlers/request-handler.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* MCP Server module responsible for server initialization and configuration
|
|
13
|
+
* Handles the Model Context Protocol server setup and request routing
|
|
14
|
+
*/
|
|
15
|
+
export class MCPServer {
|
|
16
|
+
constructor(chromeController) {
|
|
17
|
+
this.chromeController = chromeController;
|
|
18
|
+
this.toolRegistry = new ToolRegistry();
|
|
19
|
+
this.requestHandler = new RequestHandler(chromeController, this.toolRegistry);
|
|
20
|
+
|
|
21
|
+
this.server = new Server(
|
|
22
|
+
{
|
|
23
|
+
name: 'chrome-pilot',
|
|
24
|
+
version: '1.0.0',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
capabilities: {
|
|
28
|
+
tools: {},
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
this.setupHandlers();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Sets up the MCP server request handlers
|
|
38
|
+
*/
|
|
39
|
+
setupHandlers() {
|
|
40
|
+
// List tools handler
|
|
41
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
42
|
+
return {
|
|
43
|
+
tools: this.toolRegistry.getAllTools(),
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Call tool handler - delegates to RequestHandler
|
|
48
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
49
|
+
return await this.requestHandler.handleToolCall(request);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Starts the MCP server with stdio transport
|
|
55
|
+
*/
|
|
56
|
+
async start() {
|
|
57
|
+
const transport = new StdioServerTransport();
|
|
58
|
+
await this.server.connect(transport);
|
|
59
|
+
console.error('Chrome Debug MCP server running on stdio');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gets the server instance for external use
|
|
64
|
+
*/
|
|
65
|
+
getServer() {
|
|
66
|
+
return this.server;
|
|
67
|
+
}
|
|
68
|
+
}
|