@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.
Files changed (95) hide show
  1. package/CLAUDE.md +344 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/chrome-extension/README.md +41 -0
  5. package/chrome-extension/background.js +3917 -0
  6. package/chrome-extension/chrome-session-manager.js +706 -0
  7. package/chrome-extension/content.css +181 -0
  8. package/chrome-extension/content.js +3022 -0
  9. package/chrome-extension/data-buffer.js +435 -0
  10. package/chrome-extension/dom-tracker.js +411 -0
  11. package/chrome-extension/extension-config.js +78 -0
  12. package/chrome-extension/firebase-client.js +278 -0
  13. package/chrome-extension/firebase-config.js +32 -0
  14. package/chrome-extension/firebase-config.module.js +22 -0
  15. package/chrome-extension/firebase-config.module.template.js +27 -0
  16. package/chrome-extension/firebase-config.template.js +36 -0
  17. package/chrome-extension/frame-capture.js +407 -0
  18. package/chrome-extension/icon128.png +1 -0
  19. package/chrome-extension/icon16.png +1 -0
  20. package/chrome-extension/icon48.png +1 -0
  21. package/chrome-extension/license-helper.js +181 -0
  22. package/chrome-extension/logger.js +23 -0
  23. package/chrome-extension/manifest.json +73 -0
  24. package/chrome-extension/network-tracker.js +510 -0
  25. package/chrome-extension/offscreen.html +10 -0
  26. package/chrome-extension/options.html +203 -0
  27. package/chrome-extension/options.js +282 -0
  28. package/chrome-extension/pako.min.js +2 -0
  29. package/chrome-extension/performance-monitor.js +533 -0
  30. package/chrome-extension/pii-redactor.js +405 -0
  31. package/chrome-extension/popup.html +532 -0
  32. package/chrome-extension/popup.js +2446 -0
  33. package/chrome-extension/upload-manager.js +323 -0
  34. package/chrome-extension/web-vitals.iife.js +1 -0
  35. package/config/api-keys.json +11 -0
  36. package/config/chrome-pilot-config.json +45 -0
  37. package/package.json +126 -0
  38. package/scripts/cleanup-processes.js +109 -0
  39. package/scripts/config-manager.js +280 -0
  40. package/scripts/generate-extension-config.js +53 -0
  41. package/scripts/setup-security.js +64 -0
  42. package/src/capture/architecture.js +426 -0
  43. package/src/capture/error-handling-tests.md +38 -0
  44. package/src/capture/error-handling-types.ts +360 -0
  45. package/src/capture/index.js +508 -0
  46. package/src/capture/interfaces.js +625 -0
  47. package/src/capture/memory-manager.js +713 -0
  48. package/src/capture/types.js +342 -0
  49. package/src/chrome-controller.js +2658 -0
  50. package/src/cli.js +19 -0
  51. package/src/config-loader.js +303 -0
  52. package/src/database.js +2178 -0
  53. package/src/firebase-license-manager.js +462 -0
  54. package/src/firebase-privacy-guard.js +397 -0
  55. package/src/http-server.js +1516 -0
  56. package/src/index-direct.js +157 -0
  57. package/src/index-modular.js +219 -0
  58. package/src/index-monolithic-backup.js +2230 -0
  59. package/src/index.js +305 -0
  60. package/src/legacy/chrome-controller-old.js +1406 -0
  61. package/src/legacy/index-express.js +625 -0
  62. package/src/legacy/index-old.js +977 -0
  63. package/src/legacy/routes.js +260 -0
  64. package/src/legacy/shared-storage.js +101 -0
  65. package/src/logger.js +10 -0
  66. package/src/mcp/handlers/chrome-tool-handler.js +306 -0
  67. package/src/mcp/handlers/element-tool-handler.js +51 -0
  68. package/src/mcp/handlers/frame-tool-handler.js +957 -0
  69. package/src/mcp/handlers/request-handler.js +104 -0
  70. package/src/mcp/handlers/workflow-tool-handler.js +636 -0
  71. package/src/mcp/server.js +68 -0
  72. package/src/mcp/tools/index.js +701 -0
  73. package/src/middleware/auth.js +371 -0
  74. package/src/middleware/security.js +267 -0
  75. package/src/port-discovery.js +258 -0
  76. package/src/routes/admin.js +182 -0
  77. package/src/services/browser-daemon.js +494 -0
  78. package/src/services/chrome-service.js +375 -0
  79. package/src/services/failover-manager.js +412 -0
  80. package/src/services/git-safety-service.js +675 -0
  81. package/src/services/heartbeat-manager.js +200 -0
  82. package/src/services/http-client.js +195 -0
  83. package/src/services/process-manager.js +318 -0
  84. package/src/services/process-tracker.js +574 -0
  85. package/src/services/profile-manager.js +449 -0
  86. package/src/services/project-manager.js +415 -0
  87. package/src/services/session-manager.js +497 -0
  88. package/src/services/session-registry.js +491 -0
  89. package/src/services/unified-session-manager.js +678 -0
  90. package/src/shared-storage-old.js +267 -0
  91. package/src/standalone-server.js +53 -0
  92. package/src/utils/extension-path.js +145 -0
  93. package/src/utils.js +187 -0
  94. package/src/validation/log-transformer.js +125 -0
  95. 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
+ }