@eddym06/custom-chrome-mcp 1.0.8 → 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.
- package/CONDITIONAL_DESCRIPTIONS.md +174 -0
- package/FUTURE_FEATURES.txt +1503 -0
- package/README.md +300 -3
- package/TEST_WORKFLOW.md +311 -0
- package/USAGE_GUIDE.md +393 -0
- package/demo_features.ts +115 -0
- package/dist/chrome-connector.d.ts +10 -0
- package/dist/chrome-connector.d.ts.map +1 -1
- package/dist/chrome-connector.js +44 -0
- package/dist/chrome-connector.js.map +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/tests/execute-script-tests.d.ts +62 -0
- package/dist/tests/execute-script-tests.d.ts.map +1 -0
- package/dist/tests/execute-script-tests.js +280 -0
- package/dist/tests/execute-script-tests.js.map +1 -0
- package/dist/tests/run-execute-tests.d.ts +7 -0
- package/dist/tests/run-execute-tests.d.ts.map +1 -0
- package/dist/tests/run-execute-tests.js +88 -0
- package/dist/tests/run-execute-tests.js.map +1 -0
- package/dist/tools/advanced-network.backup.d.ts +245 -0
- package/dist/tools/advanced-network.backup.d.ts.map +1 -0
- package/dist/tools/advanced-network.backup.js +996 -0
- package/dist/tools/advanced-network.backup.js.map +1 -0
- package/dist/tools/advanced-network.d.ts +580 -0
- package/dist/tools/advanced-network.d.ts.map +1 -0
- package/dist/tools/advanced-network.js +1325 -0
- package/dist/tools/advanced-network.js.map +1 -0
- package/dist/tools/anti-detection.js +5 -5
- package/dist/tools/anti-detection.js.map +1 -1
- package/dist/tools/capture.d.ts +7 -1
- package/dist/tools/capture.d.ts.map +1 -1
- package/dist/tools/capture.js +8 -4
- package/dist/tools/capture.js.map +1 -1
- package/dist/tools/interaction.d.ts +82 -8
- package/dist/tools/interaction.d.ts.map +1 -1
- package/dist/tools/interaction.js +75 -28
- package/dist/tools/interaction.js.map +1 -1
- package/dist/tools/navigation.js +7 -7
- package/dist/tools/navigation.js.map +1 -1
- package/dist/tools/network-accessibility.d.ts +67 -0
- package/dist/tools/network-accessibility.d.ts.map +1 -0
- package/dist/tools/network-accessibility.js +367 -0
- package/dist/tools/network-accessibility.js.map +1 -0
- package/dist/tools/playwright-launcher.js +4 -4
- package/dist/tools/playwright-launcher.js.map +1 -1
- package/dist/tools/service-worker.js +10 -10
- package/dist/tools/service-worker.js.map +1 -1
- package/dist/tools/session.js +9 -9
- package/dist/tools/session.js.map +1 -1
- package/dist/tools/system.js +3 -3
- package/dist/tools/system.js.map +1 -1
- package/dist/utils/truncate.d.ts +29 -0
- package/dist/utils/truncate.d.ts.map +1 -0
- package/dist/utils/truncate.js +46 -0
- package/dist/utils/truncate.js.map +1 -0
- package/dist/verify-tools.d.ts +7 -0
- package/dist/verify-tools.d.ts.map +1 -0
- package/dist/verify-tools.js +137 -0
- package/dist/verify-tools.js.map +1 -0
- package/package.json +2 -2
- package/recordings/demo_recording.har +3036 -0
|
@@ -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
|