@eddym06/custom-chrome-mcp 1.0.4 → 1.1.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 (72) hide show
  1. package/CONDITIONAL_DESCRIPTIONS.md +174 -0
  2. package/FUTURE_FEATURES.txt +1503 -0
  3. package/README.md +300 -3
  4. package/TEST_WORKFLOW.md +311 -0
  5. package/USAGE_GUIDE.md +393 -0
  6. package/demo_features.ts +115 -0
  7. package/dist/chrome-connector.d.ts +31 -4
  8. package/dist/chrome-connector.d.ts.map +1 -1
  9. package/dist/chrome-connector.js +402 -53
  10. package/dist/chrome-connector.js.map +1 -1
  11. package/dist/index.js +69 -12
  12. package/dist/index.js.map +1 -1
  13. package/dist/tests/execute-script-tests.d.ts +62 -0
  14. package/dist/tests/execute-script-tests.d.ts.map +1 -0
  15. package/dist/tests/execute-script-tests.js +280 -0
  16. package/dist/tests/execute-script-tests.js.map +1 -0
  17. package/dist/tests/run-execute-tests.d.ts +7 -0
  18. package/dist/tests/run-execute-tests.d.ts.map +1 -0
  19. package/dist/tests/run-execute-tests.js +88 -0
  20. package/dist/tests/run-execute-tests.js.map +1 -0
  21. package/dist/tools/advanced-network.backup.d.ts +245 -0
  22. package/dist/tools/advanced-network.backup.d.ts.map +1 -0
  23. package/dist/tools/advanced-network.backup.js +996 -0
  24. package/dist/tools/advanced-network.backup.js.map +1 -0
  25. package/dist/tools/advanced-network.d.ts +580 -0
  26. package/dist/tools/advanced-network.d.ts.map +1 -0
  27. package/dist/tools/advanced-network.js +1325 -0
  28. package/dist/tools/advanced-network.js.map +1 -0
  29. package/dist/tools/anti-detection.d.ts.map +1 -1
  30. package/dist/tools/anti-detection.js +13 -8
  31. package/dist/tools/anti-detection.js.map +1 -1
  32. package/dist/tools/capture.d.ts +15 -9
  33. package/dist/tools/capture.d.ts.map +1 -1
  34. package/dist/tools/capture.js +21 -12
  35. package/dist/tools/capture.js.map +1 -1
  36. package/dist/tools/interaction.d.ts +84 -10
  37. package/dist/tools/interaction.d.ts.map +1 -1
  38. package/dist/tools/interaction.js +88 -33
  39. package/dist/tools/interaction.js.map +1 -1
  40. package/dist/tools/navigation.d.ts.map +1 -1
  41. package/dist/tools/navigation.js +43 -21
  42. package/dist/tools/navigation.js.map +1 -1
  43. package/dist/tools/network-accessibility.d.ts +67 -0
  44. package/dist/tools/network-accessibility.d.ts.map +1 -0
  45. package/dist/tools/network-accessibility.js +367 -0
  46. package/dist/tools/network-accessibility.js.map +1 -0
  47. package/dist/tools/playwright-launcher.d.ts +1 -1
  48. package/dist/tools/playwright-launcher.js +6 -6
  49. package/dist/tools/playwright-launcher.js.map +1 -1
  50. package/dist/tools/service-worker.d.ts +2 -2
  51. package/dist/tools/service-worker.d.ts.map +1 -1
  52. package/dist/tools/service-worker.js +22 -12
  53. package/dist/tools/service-worker.js.map +1 -1
  54. package/dist/tools/session.d.ts.map +1 -1
  55. package/dist/tools/session.js +23 -14
  56. package/dist/tools/session.js.map +1 -1
  57. package/dist/tools/system.d.ts +2 -2
  58. package/dist/tools/system.d.ts.map +1 -1
  59. package/dist/tools/system.js +9 -5
  60. package/dist/tools/system.js.map +1 -1
  61. package/dist/utils/truncate.d.ts +29 -0
  62. package/dist/utils/truncate.d.ts.map +1 -0
  63. package/dist/utils/truncate.js +46 -0
  64. package/dist/utils/truncate.js.map +1 -0
  65. package/dist/verify-tools.d.ts +7 -0
  66. package/dist/verify-tools.d.ts.map +1 -0
  67. package/dist/verify-tools.js +137 -0
  68. package/dist/verify-tools.js.map +1 -0
  69. package/package.json +3 -3
  70. package/recordings/demo_recording.har +3036 -0
  71. package/.npmrc.example +0 -2
  72. package/test-playwright.js +0 -57
@@ -0,0 +1,996 @@
1
+ /**
2
+ * Advanced Network Tools
3
+ * Response interception, mocking, WebSocket, HAR, patterns, injection
4
+ */
5
+ import { z } from 'zod';
6
+ import { withTimeout } from '../utils/helpers.js';
7
+ import * as fs from 'fs/promises';
8
+ import * as path from 'path';
9
+ // Storage for intercepted responses
10
+ const interceptedResponses = new Map();
11
+ // Storage for mock endpoints
12
+ const mockEndpoints = new Map();
13
+ // Storage for WebSocket connections
14
+ const websocketConnections = new Map();
15
+ const websocketMessages = new Map();
16
+ // Storage for HAR recording
17
+ const harRecordings = new Map();
18
+ // Storage for injected scripts
19
+ const injectedScripts = new Map();
20
+ export function createAdvancedNetworkTools(connector) {
21
+ return [
22
+ // ═══════════════════════════════════════════════════════════════════
23
+ // 1. NETWORK RESPONSE INTERCEPTION
24
+ // ═══════════════════════════════════════════════════════════════════
25
+ {
26
+ name: 'enable_response_interception',
27
+ description: 'Enable interception of network RESPONSES (not just requests). Allows modifying response body, headers, and status code before they reach the browser.',
28
+ inputSchema: z.object({
29
+ patterns: z.array(z.string()).default(['*']).describe('URL patterns to intercept'),
30
+ resourceTypes: z.array(z.string()).optional().describe('Resource types to intercept (Document, Script, XHR, Fetch, etc.)'),
31
+ tabId: z.string().optional().describe('Tab ID (optional)')
32
+ }),
33
+ handler: async ({ patterns = ['*'], resourceTypes, tabId }) => {
34
+ try {
35
+ await withTimeout(connector.verifyConnection(), 5000, 'Connection verification timeout');
36
+ const client = await withTimeout(connector.getTabClient(tabId), 5000, 'Failed to get tab client');
37
+ const { Network, Fetch } = client;
38
+ if (!Network || !Fetch) {
39
+ throw new Error('Network or Fetch domain not available. CDP connection may be unstable.');
40
+ }
41
+ await withTimeout(Network.enable(), 5000, 'Network.enable timeout');
42
+ const requestPatterns = patterns.map((pattern) => {
43
+ const p = {
44
+ urlPattern: pattern,
45
+ requestStage: 'Response'
46
+ };
47
+ if (resourceTypes && resourceTypes.length > 0) {
48
+ p.resourceType = resourceTypes[0];
49
+ }
50
+ return p;
51
+ });
52
+ await withTimeout(Fetch.enable({ patterns: requestPatterns }), 5000, 'Fetch.enable timeout');
53
+ const effectiveTabId = tabId || 'default';
54
+ if (!interceptedResponses.has(effectiveTabId)) {
55
+ interceptedResponses.set(effectiveTabId, new Map());
56
+ }
57
+ Fetch.requestPaused((params) => {
58
+ try {
59
+ const responses = interceptedResponses.get(effectiveTabId);
60
+ if (responses) {
61
+ responses.set(params.requestId, params);
62
+ }
63
+ }
64
+ catch (e) {
65
+ console.error('[Response Interception] Error storing intercepted response:', e);
66
+ }
67
+ });
68
+ return {
69
+ success: true,
70
+ message: `Response interception enabled for patterns: ${patterns.join(', ')}`,
71
+ patterns,
72
+ stage: 'Response'
73
+ };
74
+ }
75
+ catch (error) {
76
+ return {
77
+ success: false,
78
+ error: error.message || 'Unknown error',
79
+ details: error.stack,
80
+ suggestion: 'Ensure Chrome is running with debugging port and page is loaded'
81
+ };
82
+ }
83
+ }
84
+ },
85
+ {
86
+ name: 'list_intercepted_responses',
87
+ description: 'List all currently intercepted responses waiting for action',
88
+ inputSchema: z.object({
89
+ tabId: z.string().optional().describe('Tab ID (optional)')
90
+ }),
91
+ handler: async ({ tabId }) => {
92
+ try {
93
+ await withTimeout(connector.verifyConnection(), 3000, 'Connection verification timeout');
94
+ const effectiveTabId = tabId || 'default';
95
+ const responses = interceptedResponses.get(effectiveTabId);
96
+ if (!responses || responses.size === 0) {
97
+ return {
98
+ success: true,
99
+ interceptedResponses: [],
100
+ count: 0,
101
+ message: 'No responses intercepted'
102
+ };
103
+ }
104
+ const responseList = Array.from(responses.values()).map((resp) => {
105
+ try {
106
+ return {
107
+ requestId: resp.requestId,
108
+ url: resp.request?.url || 'unknown',
109
+ method: resp.request?.method || 'unknown',
110
+ responseStatusCode: resp.responseStatusCode,
111
+ responseHeaders: resp.responseHeaders || []
112
+ };
113
+ }
114
+ catch (e) {
115
+ return {
116
+ requestId: resp.requestId,
117
+ url: 'error parsing response',
118
+ method: 'unknown',
119
+ responseStatusCode: 0,
120
+ responseHeaders: []
121
+ };
122
+ }
123
+ });
124
+ return {
125
+ success: true,
126
+ interceptedResponses: responseList,
127
+ count: responseList.length
128
+ };
129
+ }
130
+ catch (error) {
131
+ return {
132
+ success: false,
133
+ error: error.message || 'Failed to list intercepted responses',
134
+ interceptedResponses: [],
135
+ count: 0
136
+ };
137
+ }
138
+ }
139
+ },
140
+ {
141
+ name: 'modify_intercepted_response',
142
+ description: 'Modify an intercepted response (body, headers, status code) and continue',
143
+ inputSchema: z.object({
144
+ requestId: z.string().describe('Request ID from list_intercepted_responses'),
145
+ modifiedBody: z.string().optional().describe('New response body (base64 if binary)'),
146
+ modifiedHeaders: z.record(z.string()).optional().describe('New/modified response headers'),
147
+ modifiedStatusCode: z.number().optional().describe('New status code (e.g., 200, 404, 500)'),
148
+ tabId: z.string().optional().describe('Tab ID (optional)')
149
+ }),
150
+ handler: async ({ requestId, modifiedBody, modifiedHeaders, modifiedStatusCode, tabId }) => {
151
+ try {
152
+ await withTimeout(connector.verifyConnection(), 3000, 'Connection verification timeout');
153
+ const client = await withTimeout(connector.getTabClient(tabId), 3000, 'Failed to get tab client');
154
+ const { Fetch } = client;
155
+ if (!Fetch) {
156
+ throw new Error('Fetch domain not available');
157
+ }
158
+ const effectiveTabId = tabId || 'default';
159
+ const responses = interceptedResponses.get(effectiveTabId);
160
+ const originalResponse = responses?.get(requestId);
161
+ if (!originalResponse) {
162
+ return {
163
+ success: false,
164
+ error: `Response ${requestId} not found`,
165
+ suggestion: 'Use list_intercepted_responses to get valid request IDs. The response may have timed out or already been processed.'
166
+ };
167
+ }
168
+ const headers = [];
169
+ if (modifiedHeaders) {
170
+ Object.entries(modifiedHeaders).forEach(([name, value]) => {
171
+ headers.push({ name, value });
172
+ });
173
+ }
174
+ else if (originalResponse.responseHeaders) {
175
+ headers.push(...originalResponse.responseHeaders);
176
+ }
177
+ await withTimeout(Fetch.fulfillRequest({
178
+ requestId,
179
+ responseCode: modifiedStatusCode || originalResponse.responseStatusCode || 200,
180
+ responseHeaders: headers.length > 0 ? headers : undefined,
181
+ body: modifiedBody ? Buffer.from(modifiedBody).toString('base64') : undefined
182
+ }), 10000, 'Fetch.fulfillRequest timeout');
183
+ responses?.delete(requestId);
184
+ return {
185
+ success: true,
186
+ message: `Response ${requestId} modified`,
187
+ url: originalResponse.request?.url || 'unknown'
188
+ };
189
+ }
190
+ catch (error) {
191
+ return {
192
+ success: false,
193
+ error: error.message || 'Failed to modify response',
194
+ details: error.stack,
195
+ suggestion: 'Check if interception is enabled and request ID is valid'
196
+ };
197
+ }
198
+ }
199
+ },
200
+ {
201
+ name: 'disable_response_interception',
202
+ description: 'Disable response interception',
203
+ inputSchema: z.object({
204
+ tabId: z.string().optional().describe('Tab ID (optional)')
205
+ }),
206
+ handler: async ({ tabId }) => {
207
+ await connector.verifyConnection();
208
+ const client = await connector.getTabClient(tabId);
209
+ const { Fetch } = client;
210
+ await Fetch.disable();
211
+ const effectiveTabId = tabId || 'default';
212
+ interceptedResponses.delete(effectiveTabId);
213
+ return {
214
+ success: true,
215
+ message: 'Response interception disabled'
216
+ };
217
+ }
218
+ },
219
+ // ═══════════════════════════════════════════════════════════════════
220
+ // 2. REQUEST/RESPONSE MOCKING
221
+ // ═══════════════════════════════════════════════════════════════════
222
+ {
223
+ name: 'create_mock_endpoint',
224
+ description: 'Create a mock endpoint that intercepts requests and responds with fake data without hitting real server',
225
+ inputSchema: z.object({
226
+ urlPattern: z.string().describe('URL pattern to mock (supports * wildcards)'),
227
+ responseBody: z.string().describe('Response body (JSON string, HTML, etc.)'),
228
+ statusCode: z.number().default(200).describe('HTTP status code'),
229
+ headers: z.record(z.string()).optional().describe('Response headers'),
230
+ latency: z.number().default(0).describe('Simulated latency in milliseconds'),
231
+ method: z.string().optional().describe('HTTP method to match (GET, POST, etc.)'),
232
+ tabId: z.string().optional().describe('Tab ID (optional)')
233
+ }),
234
+ handler: async ({ urlPattern, responseBody, statusCode = 200, headers = {}, latency = 0, method, tabId }) => {
235
+ await connector.verifyConnection();
236
+ const client = await connector.getTabClient(tabId);
237
+ const { Network, Fetch } = client;
238
+ await Network.enable();
239
+ await Fetch.enable({
240
+ patterns: [{ urlPattern, requestStage: 'Request' }]
241
+ });
242
+ const effectiveTabId = tabId || 'default';
243
+ if (!mockEndpoints.has(effectiveTabId)) {
244
+ mockEndpoints.set(effectiveTabId, []);
245
+ }
246
+ const mock = {
247
+ urlPattern,
248
+ responseBody,
249
+ statusCode,
250
+ headers,
251
+ latency,
252
+ method,
253
+ callCount: 0
254
+ };
255
+ mockEndpoints.get(effectiveTabId).push(mock);
256
+ Fetch.requestPaused(async (params) => {
257
+ const url = params.request.url;
258
+ const requestMethod = params.request.method;
259
+ const matchingMock = mockEndpoints.get(effectiveTabId)?.find((m) => {
260
+ const urlMatch = url.includes(urlPattern.replace('*', '')) ||
261
+ new RegExp(urlPattern.replace(/\*/g, '.*')).test(url);
262
+ const methodMatch = !m.method || m.method === requestMethod;
263
+ return urlMatch && methodMatch;
264
+ });
265
+ if (matchingMock) {
266
+ matchingMock.callCount++;
267
+ if (matchingMock.latency > 0) {
268
+ await new Promise(resolve => setTimeout(resolve, matchingMock.latency));
269
+ }
270
+ const responseHeaders = [
271
+ { name: 'Content-Type', value: 'application/json' },
272
+ ...Object.entries(matchingMock.headers).map(([name, value]) => ({ name, value }))
273
+ ];
274
+ await Fetch.fulfillRequest({
275
+ requestId: params.requestId,
276
+ responseCode: matchingMock.statusCode,
277
+ responseHeaders,
278
+ body: Buffer.from(matchingMock.responseBody).toString('base64')
279
+ });
280
+ }
281
+ else {
282
+ await Fetch.continueRequest({ requestId: params.requestId });
283
+ }
284
+ });
285
+ return {
286
+ success: true,
287
+ message: `Mock endpoint created for ${urlPattern}`,
288
+ mock: {
289
+ urlPattern,
290
+ statusCode,
291
+ latency,
292
+ method: method || 'any'
293
+ }
294
+ };
295
+ }
296
+ },
297
+ {
298
+ name: 'list_mock_endpoints',
299
+ description: 'List all active mock endpoints',
300
+ inputSchema: z.object({
301
+ tabId: z.string().optional().describe('Tab ID (optional)')
302
+ }),
303
+ handler: async ({ tabId }) => {
304
+ await connector.verifyConnection();
305
+ const effectiveTabId = tabId || 'default';
306
+ const mocks = mockEndpoints.get(effectiveTabId) || [];
307
+ return {
308
+ success: true,
309
+ mocks: mocks.map((m) => ({
310
+ urlPattern: m.urlPattern,
311
+ statusCode: m.statusCode,
312
+ method: m.method || 'any',
313
+ latency: m.latency,
314
+ callCount: m.callCount
315
+ })),
316
+ count: mocks.length
317
+ };
318
+ }
319
+ },
320
+ {
321
+ name: 'delete_mock_endpoint',
322
+ description: 'Delete a specific mock endpoint',
323
+ inputSchema: z.object({
324
+ urlPattern: z.string().describe('URL pattern of mock to delete'),
325
+ tabId: z.string().optional().describe('Tab ID (optional)')
326
+ }),
327
+ handler: async ({ urlPattern, tabId }) => {
328
+ await connector.verifyConnection();
329
+ const effectiveTabId = tabId || 'default';
330
+ const mocks = mockEndpoints.get(effectiveTabId);
331
+ if (!mocks) {
332
+ return { success: false, message: 'No mocks found' };
333
+ }
334
+ const initialLength = mocks.length;
335
+ const filtered = mocks.filter((m) => m.urlPattern !== urlPattern);
336
+ mockEndpoints.set(effectiveTabId, filtered);
337
+ return {
338
+ success: true,
339
+ message: `Deleted ${initialLength - filtered.length} mock(s)`,
340
+ remaining: filtered.length
341
+ };
342
+ }
343
+ },
344
+ {
345
+ name: 'clear_all_mocks',
346
+ description: 'Clear all mock endpoints',
347
+ inputSchema: z.object({
348
+ tabId: z.string().optional().describe('Tab ID (optional)')
349
+ }),
350
+ handler: async ({ tabId }) => {
351
+ await connector.verifyConnection();
352
+ const effectiveTabId = tabId || 'default';
353
+ const count = mockEndpoints.get(effectiveTabId)?.length || 0;
354
+ mockEndpoints.delete(effectiveTabId);
355
+ return {
356
+ success: true,
357
+ message: `Cleared ${count} mock endpoint(s)`
358
+ };
359
+ }
360
+ },
361
+ // ═══════════════════════════════════════════════════════════════════
362
+ // 3. WEBSOCKET INTERCEPTION
363
+ // ═══════════════════════════════════════════════════════════════════
364
+ {
365
+ name: 'enable_websocket_interception',
366
+ description: 'Enable WebSocket interception to capture and modify WebSocket messages in real-time',
367
+ inputSchema: z.object({
368
+ urlPattern: z.string().optional().describe('URL pattern to intercept (optional, default all)'),
369
+ tabId: z.string().optional().describe('Tab ID (optional)')
370
+ }),
371
+ handler: async ({ urlPattern, tabId }) => {
372
+ await connector.verifyConnection();
373
+ const client = await connector.getTabClient(tabId);
374
+ const { Network } = client;
375
+ await Network.enable();
376
+ const effectiveTabId = tabId || 'default';
377
+ if (!websocketConnections.has(effectiveTabId)) {
378
+ websocketConnections.set(effectiveTabId, []);
379
+ }
380
+ if (!websocketMessages.has(effectiveTabId)) {
381
+ websocketMessages.set(effectiveTabId, []);
382
+ }
383
+ Network.webSocketCreated((params) => {
384
+ const conns = websocketConnections.get(effectiveTabId);
385
+ conns.push({
386
+ requestId: params.requestId,
387
+ url: params.url,
388
+ initiator: params.initiator,
389
+ timestamp: Date.now()
390
+ });
391
+ });
392
+ Network.webSocketFrameSent((params) => {
393
+ const messages = websocketMessages.get(effectiveTabId);
394
+ messages.push({
395
+ requestId: params.requestId,
396
+ timestamp: params.timestamp,
397
+ direction: 'sent',
398
+ opcode: params.response.opcode,
399
+ mask: params.response.mask,
400
+ payloadData: params.response.payloadData
401
+ });
402
+ });
403
+ Network.webSocketFrameReceived((params) => {
404
+ const messages = websocketMessages.get(effectiveTabId);
405
+ messages.push({
406
+ requestId: params.requestId,
407
+ timestamp: params.timestamp,
408
+ direction: 'received',
409
+ opcode: params.response.opcode,
410
+ mask: params.response.mask,
411
+ payloadData: params.response.payloadData
412
+ });
413
+ });
414
+ Network.webSocketClosed((params) => {
415
+ const conns = websocketConnections.get(effectiveTabId);
416
+ const conn = conns.find((c) => c.requestId === params.requestId);
417
+ if (conn) {
418
+ conn.closed = true;
419
+ conn.closedAt = Date.now();
420
+ }
421
+ });
422
+ return {
423
+ success: true,
424
+ message: 'WebSocket interception enabled',
425
+ pattern: urlPattern || 'all'
426
+ };
427
+ }
428
+ },
429
+ {
430
+ name: 'list_websocket_connections',
431
+ description: 'List all WebSocket connections',
432
+ inputSchema: z.object({
433
+ tabId: z.string().optional().describe('Tab ID (optional)')
434
+ }),
435
+ handler: async ({ tabId }) => {
436
+ await connector.verifyConnection();
437
+ const effectiveTabId = tabId || 'default';
438
+ const conns = websocketConnections.get(effectiveTabId) || [];
439
+ return {
440
+ success: true,
441
+ connections: conns.map((c) => ({
442
+ requestId: c.requestId,
443
+ url: c.url,
444
+ timestamp: c.timestamp,
445
+ closed: c.closed || false
446
+ })),
447
+ count: conns.length
448
+ };
449
+ }
450
+ },
451
+ {
452
+ name: 'list_websocket_messages',
453
+ description: 'List all WebSocket messages (sent and received)',
454
+ inputSchema: z.object({
455
+ requestId: z.string().optional().describe('Filter by specific WebSocket connection'),
456
+ direction: z.enum(['sent', 'received', 'all']).default('all').describe('Filter by direction'),
457
+ limit: z.number().default(100).describe('Max messages to return'),
458
+ tabId: z.string().optional().describe('Tab ID (optional)')
459
+ }),
460
+ handler: async ({ requestId, direction = 'all', limit = 100, tabId }) => {
461
+ await connector.verifyConnection();
462
+ const effectiveTabId = tabId || 'default';
463
+ let messages = websocketMessages.get(effectiveTabId) || [];
464
+ if (requestId) {
465
+ messages = messages.filter((m) => m.requestId === requestId);
466
+ }
467
+ if (direction !== 'all') {
468
+ messages = messages.filter((m) => m.direction === direction);
469
+ }
470
+ messages = messages.slice(-limit);
471
+ return {
472
+ success: true,
473
+ messages: messages.map((m) => ({
474
+ requestId: m.requestId,
475
+ timestamp: m.timestamp,
476
+ direction: m.direction,
477
+ payloadData: m.payloadData
478
+ })),
479
+ count: messages.length
480
+ };
481
+ }
482
+ },
483
+ {
484
+ name: 'send_websocket_message',
485
+ description: 'Send a fake WebSocket message (inject into the stream)',
486
+ inputSchema: z.object({
487
+ requestId: z.string().describe('WebSocket connection ID'),
488
+ message: z.string().describe('Message to send'),
489
+ tabId: z.string().optional().describe('Tab ID (optional)')
490
+ }),
491
+ handler: async ({ requestId, message, tabId }) => {
492
+ await connector.verifyConnection();
493
+ const client = await connector.getTabClient(tabId);
494
+ const { Network } = client;
495
+ // Note: CDP doesn't directly support sending WS messages,
496
+ // but we can execute JavaScript to do it
497
+ const { Runtime } = client;
498
+ await Runtime.enable();
499
+ const script = `
500
+ (function() {
501
+ // Find the WebSocket by inspecting global WebSocket instances
502
+ // This is a workaround since CDP doesn't expose WS instances
503
+ const originalSend = WebSocket.prototype.send;
504
+ let foundWS = null;
505
+
506
+ WebSocket.prototype.send = function(...args) {
507
+ foundWS = this;
508
+ return originalSend.apply(this, args);
509
+ };
510
+
511
+ // Trigger to get reference
512
+ setTimeout(() => {
513
+ if (foundWS && foundWS.readyState === WebSocket.OPEN) {
514
+ foundWS.send('${message.replace(/'/g, "\\'")}');
515
+ }
516
+ }, 100);
517
+
518
+ return 'Message injection attempted';
519
+ })();
520
+ `;
521
+ const result = await Runtime.evaluate({ expression: script });
522
+ return {
523
+ success: true,
524
+ message: 'WebSocket message injection attempted',
525
+ note: 'CDP limitation: Direct WS injection requires JavaScript workaround'
526
+ };
527
+ }
528
+ },
529
+ {
530
+ name: 'disable_websocket_interception',
531
+ description: 'Disable WebSocket interception',
532
+ inputSchema: z.object({
533
+ tabId: z.string().optional().describe('Tab ID (optional)')
534
+ }),
535
+ handler: async ({ tabId }) => {
536
+ await connector.verifyConnection();
537
+ const effectiveTabId = tabId || 'default';
538
+ websocketConnections.delete(effectiveTabId);
539
+ websocketMessages.delete(effectiveTabId);
540
+ return {
541
+ success: true,
542
+ message: 'WebSocket interception disabled'
543
+ };
544
+ }
545
+ },
546
+ // ═══════════════════════════════════════════════════════════════════
547
+ // 4. HAR FILE GENERATION & REPLAY
548
+ // ═══════════════════════════════════════════════════════════════════
549
+ {
550
+ name: 'start_har_recording',
551
+ description: 'Start recording all network traffic in HAR (HTTP Archive) format',
552
+ inputSchema: z.object({
553
+ tabId: z.string().optional().describe('Tab ID (optional)')
554
+ }),
555
+ handler: async ({ tabId }) => {
556
+ await connector.verifyConnection();
557
+ const client = await connector.getTabClient(tabId);
558
+ const { Network, Page } = client;
559
+ await Network.enable();
560
+ await Page.enable();
561
+ const effectiveTabId = tabId || 'default';
562
+ const recording = {
563
+ startTime: Date.now(),
564
+ entries: [],
565
+ pages: []
566
+ };
567
+ harRecordings.set(effectiveTabId, recording);
568
+ Network.requestWillBeSent((params) => {
569
+ const entry = {
570
+ requestId: params.requestId,
571
+ startedDateTime: new Date(params.timestamp * 1000).toISOString(),
572
+ time: 0,
573
+ request: {
574
+ method: params.request.method,
575
+ url: params.request.url,
576
+ httpVersion: 'HTTP/1.1',
577
+ headers: Object.entries(params.request.headers || {}).map(([name, value]) => ({ name, value })),
578
+ queryString: [],
579
+ cookies: [],
580
+ headersSize: -1,
581
+ bodySize: params.request.postData ? params.request.postData.length : 0
582
+ },
583
+ response: {},
584
+ cache: {},
585
+ timings: {
586
+ blocked: -1,
587
+ dns: -1,
588
+ connect: -1,
589
+ send: 0,
590
+ wait: 0,
591
+ receive: 0,
592
+ ssl: -1
593
+ }
594
+ };
595
+ recording.entries.push(entry);
596
+ });
597
+ Network.responseReceived((params) => {
598
+ const entry = recording.entries.find((e) => e.requestId === params.requestId);
599
+ if (entry) {
600
+ entry.response = {
601
+ status: params.response.status,
602
+ statusText: params.response.statusText,
603
+ httpVersion: params.response.protocol || 'HTTP/1.1',
604
+ headers: Object.entries(params.response.headers || {}).map(([name, value]) => ({ name, value })),
605
+ cookies: [],
606
+ content: {
607
+ size: 0,
608
+ mimeType: params.response.mimeType || 'application/octet-stream'
609
+ },
610
+ redirectURL: '',
611
+ headersSize: -1,
612
+ bodySize: -1
613
+ };
614
+ }
615
+ });
616
+ Network.loadingFinished((params) => {
617
+ const entry = recording.entries.find((e) => e.requestId === params.requestId);
618
+ if (entry) {
619
+ entry.time = (params.timestamp * 1000) - new Date(entry.startedDateTime).getTime();
620
+ }
621
+ });
622
+ return {
623
+ success: true,
624
+ message: 'HAR recording started',
625
+ startTime: recording.startTime
626
+ };
627
+ }
628
+ },
629
+ {
630
+ name: 'stop_har_recording',
631
+ description: 'Stop HAR recording and return the HAR data',
632
+ inputSchema: z.object({
633
+ tabId: z.string().optional().describe('Tab ID (optional)')
634
+ }),
635
+ handler: async ({ tabId }) => {
636
+ await connector.verifyConnection();
637
+ const effectiveTabId = tabId || 'default';
638
+ const recording = harRecordings.get(effectiveTabId);
639
+ if (!recording) {
640
+ throw new Error('No active HAR recording');
641
+ }
642
+ const har = {
643
+ log: {
644
+ version: '1.2',
645
+ creator: {
646
+ name: 'Custom Chrome MCP',
647
+ version: '1.0.9'
648
+ },
649
+ pages: recording.pages,
650
+ entries: recording.entries
651
+ }
652
+ };
653
+ harRecordings.delete(effectiveTabId);
654
+ return {
655
+ success: true,
656
+ har,
657
+ entriesCount: recording.entries.length,
658
+ duration: Date.now() - recording.startTime
659
+ };
660
+ }
661
+ },
662
+ {
663
+ name: 'export_har_file',
664
+ description: 'Export HAR recording to a file',
665
+ inputSchema: z.object({
666
+ filename: z.string().describe('Filename to save HAR (e.g., recording.har)'),
667
+ outputDir: z.string().optional().describe('Output directory (default: current directory)'),
668
+ tabId: z.string().optional().describe('Tab ID (optional)')
669
+ }),
670
+ handler: async ({ filename, outputDir = '.', tabId }) => {
671
+ await connector.verifyConnection();
672
+ const effectiveTabId = tabId || 'default';
673
+ const recording = harRecordings.get(effectiveTabId);
674
+ if (!recording) {
675
+ throw new Error('No active HAR recording to export');
676
+ }
677
+ const har = {
678
+ log: {
679
+ version: '1.2',
680
+ creator: {
681
+ name: 'Custom Chrome MCP',
682
+ version: '1.0.9'
683
+ },
684
+ pages: recording.pages,
685
+ entries: recording.entries
686
+ }
687
+ };
688
+ const filepath = path.join(outputDir, filename);
689
+ await fs.writeFile(filepath, JSON.stringify(har, null, 2), 'utf-8');
690
+ return {
691
+ success: true,
692
+ message: `HAR file exported to ${filepath}`,
693
+ filepath,
694
+ entriesCount: recording.entries.length
695
+ };
696
+ }
697
+ },
698
+ // ═══════════════════════════════════════════════════════════════════
699
+ // 5. ADVANCED REQUEST PATTERNS
700
+ // ═══════════════════════════════════════════════════════════════════
701
+ {
702
+ name: 'add_advanced_interception_pattern',
703
+ description: 'Add advanced interception pattern with complex filtering (status code, size, duration, content-type, etc.)',
704
+ inputSchema: z.object({
705
+ name: z.string().describe('Pattern name for reference'),
706
+ urlPattern: z.string().optional().describe('URL pattern (glob)'),
707
+ method: z.string().optional().describe('HTTP method'),
708
+ resourceType: z.string().optional().describe('Resource type'),
709
+ statusCodeMin: z.number().optional().describe('Min status code'),
710
+ statusCodeMax: z.number().optional().describe('Max status code'),
711
+ minSize: z.number().optional().describe('Min response size in bytes'),
712
+ maxSize: z.number().optional().describe('Max response size in bytes'),
713
+ minDuration: z.number().optional().describe('Min request duration in ms'),
714
+ contentType: z.string().optional().describe('Content-Type to match'),
715
+ action: z.enum(['log', 'block', 'delay']).default('log').describe('Action to take'),
716
+ delayMs: z.number().optional().describe('Delay in ms (if action=delay)'),
717
+ tabId: z.string().optional().describe('Tab ID (optional)')
718
+ }),
719
+ handler: async ({ name, urlPattern, method, resourceType, statusCodeMin, statusCodeMax, minSize, maxSize, minDuration, contentType, action = 'log', delayMs, tabId }) => {
720
+ await connector.verifyConnection();
721
+ const client = await connector.getTabClient(tabId);
722
+ const { Network, Fetch } = client;
723
+ await Network.enable();
724
+ const pattern = {
725
+ name,
726
+ urlPattern,
727
+ method,
728
+ resourceType,
729
+ statusCodeMin,
730
+ statusCodeMax,
731
+ minSize,
732
+ maxSize,
733
+ minDuration,
734
+ contentType,
735
+ action,
736
+ delayMs,
737
+ matchCount: 0
738
+ };
739
+ // Store request start times for duration calculation
740
+ const requestTimes = new Map();
741
+ // Enable Network domain for advanced monitoring
742
+ await Network.enable();
743
+ // Track request start times
744
+ Network.requestWillBeSent((params) => {
745
+ requestTimes.set(params.requestId, params.timestamp);
746
+ });
747
+ // Monitor responses for advanced filtering
748
+ Network.responseReceived((params) => {
749
+ const url = params.response.url;
750
+ const status = params.response.status;
751
+ const mimeType = params.response.mimeType;
752
+ const startTime = requestTimes.get(params.requestId);
753
+ const duration = startTime ? (params.timestamp - startTime) * 1000 : 0;
754
+ let matches = true;
755
+ // URL pattern matching
756
+ if (urlPattern) {
757
+ const regex = new RegExp(urlPattern.replace(/\*/g, '.*'));
758
+ if (!regex.test(url))
759
+ matches = false;
760
+ }
761
+ // Status code filtering
762
+ if (statusCodeMin && status < statusCodeMin)
763
+ matches = false;
764
+ if (statusCodeMax && status > statusCodeMax)
765
+ matches = false;
766
+ // Content-Type filtering
767
+ if (contentType && mimeType && !mimeType.includes(contentType))
768
+ matches = false;
769
+ // Duration filtering
770
+ if (minDuration && duration < minDuration)
771
+ matches = false;
772
+ // Size filtering (will be checked on loadingFinished)
773
+ if (matches) {
774
+ // For 'log' action, just increment counter
775
+ if (action === 'log') {
776
+ pattern.matchCount++;
777
+ console.log(`[Pattern: ${name}] Matched request:`, {
778
+ url,
779
+ status,
780
+ mimeType,
781
+ duration: `${duration.toFixed(0)}ms`
782
+ });
783
+ }
784
+ }
785
+ requestTimes.delete(params.requestId);
786
+ });
787
+ // Enable Fetch for blocking/delaying (basic filtering)
788
+ if (action === 'block' || action === 'delay') {
789
+ if (urlPattern) {
790
+ await Fetch.enable({
791
+ patterns: [{
792
+ urlPattern,
793
+ requestStage: 'Request'
794
+ }]
795
+ });
796
+ Fetch.requestPaused(async (params) => {
797
+ let matches = true;
798
+ if (method && params.request.method !== method)
799
+ matches = false;
800
+ if (resourceType && params.resourceType !== resourceType)
801
+ matches = false;
802
+ if (matches && action === 'block') {
803
+ await Fetch.failRequest({
804
+ requestId: params.requestId,
805
+ errorReason: 'BlockedByClient'
806
+ });
807
+ pattern.matchCount++;
808
+ }
809
+ else if (matches && action === 'delay') {
810
+ if (delayMs) {
811
+ await new Promise(resolve => setTimeout(resolve, delayMs));
812
+ }
813
+ await Fetch.continueRequest({ requestId: params.requestId });
814
+ pattern.matchCount++;
815
+ }
816
+ else {
817
+ await Fetch.continueRequest({ requestId: params.requestId });
818
+ }
819
+ });
820
+ }
821
+ }
822
+ return {
823
+ success: true,
824
+ message: `Advanced pattern '${name}' added`,
825
+ pattern: {
826
+ name,
827
+ action,
828
+ filters: {
829
+ urlPattern,
830
+ method,
831
+ resourceType,
832
+ statusCode: statusCodeMin && statusCodeMax ? `${statusCodeMin}-${statusCodeMax}` : undefined,
833
+ size: minSize && maxSize ? `${minSize}-${maxSize}` : undefined,
834
+ duration: minDuration ? `>${minDuration}ms` : undefined,
835
+ contentType
836
+ }
837
+ },
838
+ note: action === 'log'
839
+ ? 'Pattern will log matching requests to console'
840
+ : `Pattern will ${action} matching requests`
841
+ };
842
+ }
843
+ },
844
+ // ═══════════════════════════════════════════════════════════════════
845
+ // 6. CSS/JS INJECTION PIPELINE
846
+ // ═══════════════════════════════════════════════════════════════════
847
+ {
848
+ name: 'inject_css_global',
849
+ description: 'Inject CSS into ALL pages automatically (persists across navigation)',
850
+ inputSchema: z.object({
851
+ css: z.string().describe('CSS code to inject'),
852
+ name: z.string().optional().describe('Name for this injection (for reference)'),
853
+ tabId: z.string().optional().describe('Tab ID (optional)')
854
+ }),
855
+ handler: async ({ css, name, tabId }) => {
856
+ await connector.verifyConnection();
857
+ const client = await connector.getTabClient(tabId);
858
+ const { Page } = client;
859
+ await Page.enable();
860
+ const script = `
861
+ (function() {
862
+ const style = document.createElement('style');
863
+ style.textContent = \`${css.replace(/`/g, '\\`')}\`;
864
+ style.setAttribute('data-mcp-injection', '${name || 'unnamed'}');
865
+ document.head.appendChild(style);
866
+ })();
867
+ `;
868
+ const result = await Page.addScriptToEvaluateOnNewDocument({
869
+ source: script
870
+ });
871
+ const effectiveTabId = tabId || 'default';
872
+ if (!injectedScripts.has(effectiveTabId)) {
873
+ injectedScripts.set(effectiveTabId, []);
874
+ }
875
+ injectedScripts.get(effectiveTabId).push(result.identifier);
876
+ // Also inject in current page
877
+ const { Runtime } = client;
878
+ await Runtime.enable();
879
+ await Runtime.evaluate({ expression: script });
880
+ return {
881
+ success: true,
882
+ message: 'CSS injected globally',
883
+ identifier: result.identifier,
884
+ name: name || 'unnamed'
885
+ };
886
+ }
887
+ },
888
+ {
889
+ name: 'inject_js_global',
890
+ description: 'Inject JavaScript into ALL pages automatically (runs before any page script)',
891
+ inputSchema: z.object({
892
+ javascript: z.string().describe('JavaScript code to inject'),
893
+ name: z.string().optional().describe('Name for this injection (for reference)'),
894
+ runImmediately: z.boolean().default(true).describe('Also run in current page'),
895
+ tabId: z.string().optional().describe('Tab ID (optional)')
896
+ }),
897
+ handler: async ({ javascript, name, runImmediately = true, tabId }) => {
898
+ await connector.verifyConnection();
899
+ const client = await connector.getTabClient(tabId);
900
+ const { Page } = client;
901
+ await Page.enable();
902
+ const result = await Page.addScriptToEvaluateOnNewDocument({
903
+ source: javascript
904
+ });
905
+ const effectiveTabId = tabId || 'default';
906
+ if (!injectedScripts.has(effectiveTabId)) {
907
+ injectedScripts.set(effectiveTabId, []);
908
+ }
909
+ injectedScripts.get(effectiveTabId).push(result.identifier);
910
+ if (runImmediately) {
911
+ const { Runtime } = client;
912
+ await Runtime.enable();
913
+ await Runtime.evaluate({ expression: javascript });
914
+ }
915
+ return {
916
+ success: true,
917
+ message: 'JavaScript injected globally',
918
+ identifier: result.identifier,
919
+ name: name || 'unnamed',
920
+ runImmediately
921
+ };
922
+ }
923
+ },
924
+ {
925
+ name: 'list_injected_scripts',
926
+ description: 'List all globally injected CSS/JS',
927
+ inputSchema: z.object({
928
+ tabId: z.string().optional().describe('Tab ID (optional)')
929
+ }),
930
+ handler: async ({ tabId }) => {
931
+ await connector.verifyConnection();
932
+ const effectiveTabId = tabId || 'default';
933
+ const scripts = injectedScripts.get(effectiveTabId) || [];
934
+ return {
935
+ success: true,
936
+ injections: scripts,
937
+ count: scripts.length
938
+ };
939
+ }
940
+ },
941
+ {
942
+ name: 'remove_injection',
943
+ description: 'Remove a specific injected script (CSS or JS)',
944
+ inputSchema: z.object({
945
+ identifier: z.string().describe('Injection identifier from inject_css_global or inject_js_global'),
946
+ tabId: z.string().optional().describe('Tab ID (optional)')
947
+ }),
948
+ handler: async ({ identifier, tabId }) => {
949
+ await connector.verifyConnection();
950
+ const client = await connector.getTabClient(tabId);
951
+ const { Page } = client;
952
+ await Page.removeScriptToEvaluateOnNewDocument({
953
+ identifier
954
+ });
955
+ const effectiveTabId = tabId || 'default';
956
+ const scripts = injectedScripts.get(effectiveTabId);
957
+ if (scripts) {
958
+ const filtered = scripts.filter((id) => id !== identifier);
959
+ injectedScripts.set(effectiveTabId, filtered);
960
+ }
961
+ return {
962
+ success: true,
963
+ message: `Injection ${identifier} removed`
964
+ };
965
+ }
966
+ },
967
+ {
968
+ name: 'clear_all_injections',
969
+ description: 'Clear all injected CSS/JS scripts',
970
+ inputSchema: z.object({
971
+ tabId: z.string().optional().describe('Tab ID (optional)')
972
+ }),
973
+ handler: async ({ tabId }) => {
974
+ await connector.verifyConnection();
975
+ const client = await connector.getTabClient(tabId);
976
+ const { Page } = client;
977
+ const effectiveTabId = tabId || 'default';
978
+ const scripts = injectedScripts.get(effectiveTabId) || [];
979
+ for (const identifier of scripts) {
980
+ try {
981
+ await Page.removeScriptToEvaluateOnNewDocument({ identifier });
982
+ }
983
+ catch (e) {
984
+ // Ignore errors for already removed scripts
985
+ }
986
+ }
987
+ injectedScripts.delete(effectiveTabId);
988
+ return {
989
+ success: true,
990
+ message: `Cleared ${scripts.length} injection(s)`
991
+ };
992
+ }
993
+ }
994
+ ];
995
+ }
996
+ //# sourceMappingURL=advanced-network.backup.js.map