@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,1325 @@
|
|
|
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 { truncateArray } from '../utils/truncate.js';
|
|
8
|
+
import * as fs from 'fs/promises';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
// Storage for intercepted responses
|
|
11
|
+
const interceptedResponses = new Map();
|
|
12
|
+
// Storage for mock endpoints
|
|
13
|
+
const mockEndpoints = new Map();
|
|
14
|
+
// Storage for WebSocket connections
|
|
15
|
+
const websocketConnections = new Map();
|
|
16
|
+
const websocketMessages = new Map();
|
|
17
|
+
// Storage for HAR recording
|
|
18
|
+
const harRecordings = new Map();
|
|
19
|
+
// Storage for injected scripts
|
|
20
|
+
const injectedScripts = new Map();
|
|
21
|
+
/**
|
|
22
|
+
* Helper function to wrap tool handlers with error handling
|
|
23
|
+
*/
|
|
24
|
+
async function safeHandler(operation, errorMessage = 'Operation failed') {
|
|
25
|
+
try {
|
|
26
|
+
return await operation();
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.error(`[Advanced Network Tools] ${errorMessage}:`, error);
|
|
30
|
+
return {
|
|
31
|
+
success: false,
|
|
32
|
+
error: error.message || errorMessage,
|
|
33
|
+
details: error.stack,
|
|
34
|
+
suggestion: 'Check Chrome connection and ensure the page is loaded'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createAdvancedNetworkTools(connector) {
|
|
39
|
+
return [
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
41
|
+
// 1. NETWORK RESPONSE INTERCEPTION
|
|
42
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
43
|
+
{
|
|
44
|
+
name: 'enable_response_interception',
|
|
45
|
+
description: '🔴 START HERE for traffic interception. Enables network traffic capture - intercepts ALL responses (API calls, HTTP requests). COMPLETE WORKFLOW: 1️⃣ enable_response_interception → 2️⃣ navigate or click (trigger traffic) → 3️⃣ list_intercepted_responses (see what was captured) → 4️⃣ modify_intercepted_response (optional: change response) → 5️⃣ disable_response_interception. Use when user says "intercept traffic", "capture requests", "monitor API calls". WARNING: Cannot work with create_mock_endpoint simultaneously.',
|
|
46
|
+
inputSchema: z.object({
|
|
47
|
+
patterns: z.array(z.string()).default(['*']).describe('URL patterns to intercept'),
|
|
48
|
+
resourceTypes: z.array(z.string()).optional().describe('Resource types to intercept (Document, Script, XHR, Fetch, etc.)'),
|
|
49
|
+
timeoutMs: z.number().default(10000).optional().describe('Operation timeout in milliseconds (default: 10000)'),
|
|
50
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
51
|
+
}),
|
|
52
|
+
handler: async ({ patterns = ['*'], resourceTypes, timeoutMs = 10000, tabId }) => {
|
|
53
|
+
try {
|
|
54
|
+
const tabKey = tabId || 'default';
|
|
55
|
+
// Check for conflicts with mocks
|
|
56
|
+
if (mockEndpoints.has(tabKey) && mockEndpoints.get(tabKey).length > 0) {
|
|
57
|
+
console.error('⚠️ WARNING: Mock endpoints are already active. This may cause conflicts.');
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: 'Conflict detected: Mock endpoints are already active',
|
|
61
|
+
suggestion: 'Call clear_all_mocks first, then enable response interception',
|
|
62
|
+
activeMocks: mockEndpoints.get(tabKey).length
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
await withTimeout(connector.verifyConnection(), Math.min(timeoutMs, 5000), 'Connection verification timeout');
|
|
66
|
+
// Use PERSISTENT client so listeners stay active
|
|
67
|
+
const client = await withTimeout(connector.getPersistentClient(tabId), Math.min(timeoutMs, 5000), 'Failed to get persistent tab client');
|
|
68
|
+
const { Network, Fetch } = client;
|
|
69
|
+
if (!Network || !Fetch) {
|
|
70
|
+
throw new Error('Network or Fetch domain not available. CDP connection may be unstable.');
|
|
71
|
+
}
|
|
72
|
+
await withTimeout(Network.enable(), timeoutMs, 'Network.enable timeout');
|
|
73
|
+
const requestPatterns = patterns.map((pattern) => {
|
|
74
|
+
const p = {
|
|
75
|
+
urlPattern: pattern,
|
|
76
|
+
requestStage: 'Response'
|
|
77
|
+
};
|
|
78
|
+
if (resourceTypes && resourceTypes.length > 0) {
|
|
79
|
+
p.resourceType = resourceTypes[0];
|
|
80
|
+
}
|
|
81
|
+
return p;
|
|
82
|
+
});
|
|
83
|
+
await withTimeout(Fetch.enable({ patterns: requestPatterns }), timeoutMs, 'Fetch.enable timeout');
|
|
84
|
+
const effectiveTabId = tabId || 'default';
|
|
85
|
+
if (!interceptedResponses.has(effectiveTabId)) {
|
|
86
|
+
interceptedResponses.set(effectiveTabId, new Map());
|
|
87
|
+
}
|
|
88
|
+
// Register listener on persistent client - stays active!
|
|
89
|
+
Fetch.requestPaused((params) => {
|
|
90
|
+
try {
|
|
91
|
+
const responses = interceptedResponses.get(effectiveTabId);
|
|
92
|
+
if (responses) {
|
|
93
|
+
responses.set(params.requestId, params);
|
|
94
|
+
console.error(`[Response Interceptor] Captured: ${params.request?.url}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.error('[Response Interception] Error storing intercepted response:', e);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
console.error(`✅ Response interceptor ACTIVE and listening for patterns: ${patterns.join(', ')}`);
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
message: `Response interception enabled and LISTENING for patterns: ${patterns.join(', ')}`,
|
|
105
|
+
patterns,
|
|
106
|
+
stage: 'Response',
|
|
107
|
+
note: 'Interceptor is now ACTIVE and will continue capturing responses until disabled'
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: error.message || 'Unknown error',
|
|
114
|
+
details: error.stack,
|
|
115
|
+
suggestion: 'Ensure Chrome is running with debugging port and page is loaded'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'list_intercepted_responses',
|
|
122
|
+
description: '� MANDATORY STEP after enable_response_interception! Lists captured network traffic. USE THIS WHEN: 1️⃣ After clicking button/link, expected content doesn\'t appear in HTML/page. 2️⃣ After form submission, no visible response on page. 3️⃣ Suspecting AJAX/XHR/Fetch requests (background API calls). 4️⃣ Page "loads" but data is missing/incomplete. WHY CRITICAL: Modern websites load data via background requests (APIs) that DON\'T show in HTML/DOM. get_html only shows static markup, NOT dynamic API responses. This tool reveals the "invisible" network traffic. COMMON MISTAKE: Assuming get_html shows everything - it doesn\'t! API responses are SEPARATE from DOM. Shows: URLs, methods, status codes, headers, requestIds.',
|
|
123
|
+
inputSchema: z.object({
|
|
124
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
125
|
+
}),
|
|
126
|
+
handler: async ({ tabId }) => {
|
|
127
|
+
try {
|
|
128
|
+
await withTimeout(connector.verifyConnection(), 3000, 'Connection verification timeout');
|
|
129
|
+
const effectiveTabId = tabId || 'default';
|
|
130
|
+
const responses = interceptedResponses.get(effectiveTabId);
|
|
131
|
+
if (!responses || responses.size === 0) {
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
interceptedResponses: [],
|
|
135
|
+
count: 0,
|
|
136
|
+
message: 'No responses intercepted'
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const responseList = Array.from(responses.values()).map((resp) => {
|
|
140
|
+
try {
|
|
141
|
+
return {
|
|
142
|
+
requestId: resp.requestId,
|
|
143
|
+
url: resp.request?.url || 'unknown',
|
|
144
|
+
method: resp.request?.method || 'unknown',
|
|
145
|
+
responseStatusCode: resp.responseStatusCode,
|
|
146
|
+
responseHeaders: resp.responseHeaders || []
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
return {
|
|
151
|
+
requestId: resp.requestId,
|
|
152
|
+
url: 'error parsing response',
|
|
153
|
+
method: 'unknown',
|
|
154
|
+
responseStatusCode: 0,
|
|
155
|
+
responseHeaders: []
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
// Truncate if too many responses
|
|
160
|
+
const truncatedList = truncateArray(responseList, 100, 'Use more specific URL patterns in enable_response_interception to reduce captured traffic.');
|
|
161
|
+
return {
|
|
162
|
+
success: true,
|
|
163
|
+
interceptedResponses: truncatedList.items,
|
|
164
|
+
count: responseList.length,
|
|
165
|
+
...truncatedList
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
success: false,
|
|
171
|
+
error: error.message || 'Failed to list intercepted responses',
|
|
172
|
+
interceptedResponses: [],
|
|
173
|
+
count: 0
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'modify_intercepted_response',
|
|
180
|
+
description: '✏️ STEP 3 (optional) of interception workflow. Modifies captured response BEFORE browser receives it. Change: response body (JSON/HTML), headers, status code. Then sends modified packet to page. WORKFLOW: 1️⃣ enable_response_interception → 2️⃣ list_intercepted_responses (get requestId) → 3️⃣ modify_intercepted_response (change data) → 4️⃣ browser receives modified response. Use when user says "modify response", "change API data", "edit packet", "send modified data".',
|
|
181
|
+
inputSchema: z.object({
|
|
182
|
+
requestId: z.string().describe('Request ID from list_intercepted_responses'),
|
|
183
|
+
modifiedBody: z.string().optional().describe('New response body (base64 if binary)'),
|
|
184
|
+
modifiedHeaders: z.record(z.string()).optional().describe('New/modified response headers'),
|
|
185
|
+
modifiedStatusCode: z.number().optional().describe('New status code (e.g., 200, 404, 500)'),
|
|
186
|
+
timeoutMs: z.number().default(15000).optional().describe('Operation timeout in milliseconds (default: 15000)'),
|
|
187
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
188
|
+
}),
|
|
189
|
+
handler: async ({ requestId, modifiedBody, modifiedHeaders, modifiedStatusCode, timeoutMs = 15000, tabId }) => {
|
|
190
|
+
try {
|
|
191
|
+
await withTimeout(connector.verifyConnection(), Math.min(timeoutMs, 5000), 'Connection verification timeout');
|
|
192
|
+
const client = await withTimeout(connector.getTabClient(tabId), Math.min(timeoutMs, 5000), 'Failed to get tab client');
|
|
193
|
+
const { Fetch } = client;
|
|
194
|
+
if (!Fetch) {
|
|
195
|
+
throw new Error('Fetch domain not available');
|
|
196
|
+
}
|
|
197
|
+
const effectiveTabId = tabId || 'default';
|
|
198
|
+
const responses = interceptedResponses.get(effectiveTabId);
|
|
199
|
+
const originalResponse = responses?.get(requestId);
|
|
200
|
+
if (!originalResponse) {
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
error: `Response ${requestId} not found`,
|
|
204
|
+
suggestion: 'Use list_intercepted_responses to get valid request IDs. The response may have timed out or already been processed.'
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const headers = [];
|
|
208
|
+
if (modifiedHeaders) {
|
|
209
|
+
Object.entries(modifiedHeaders).forEach(([name, value]) => {
|
|
210
|
+
headers.push({ name, value });
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
else if (originalResponse.responseHeaders) {
|
|
214
|
+
headers.push(...originalResponse.responseHeaders);
|
|
215
|
+
}
|
|
216
|
+
await withTimeout(Fetch.fulfillRequest({
|
|
217
|
+
requestId,
|
|
218
|
+
responseCode: modifiedStatusCode || originalResponse.responseStatusCode || 200,
|
|
219
|
+
responseHeaders: headers.length > 0 ? headers : undefined,
|
|
220
|
+
body: modifiedBody ? Buffer.from(modifiedBody).toString('base64') : undefined
|
|
221
|
+
}), timeoutMs, 'Fetch.fulfillRequest timeout');
|
|
222
|
+
responses?.delete(requestId);
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
message: `Response ${requestId} modified`,
|
|
226
|
+
url: originalResponse.request?.url || 'unknown'
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
return {
|
|
231
|
+
success: false,
|
|
232
|
+
error: error.message || 'Failed to modify response',
|
|
233
|
+
details: error.stack,
|
|
234
|
+
suggestion: 'Check if interception is enabled and request ID is valid'
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: 'disable_response_interception',
|
|
241
|
+
description: '🛑 Stops network interception and cleans up. USE THIS WHEN: 1️⃣ Finished analyzing traffic (cleanup). 2️⃣ Want to enable mocks (interception conflicts with mocks). 3️⃣ Done testing, restoring normal behavior. IMPORTANT: Always disable when done to prevent memory leaks and conflicts.',
|
|
242
|
+
inputSchema: z.object({
|
|
243
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
244
|
+
}),
|
|
245
|
+
handler: async ({ tabId }) => {
|
|
246
|
+
try {
|
|
247
|
+
await withTimeout(connector.verifyConnection(), 3000, 'Connection timeout');
|
|
248
|
+
// Get persistent client
|
|
249
|
+
const client = await connector.getPersistentClient(tabId);
|
|
250
|
+
const { Fetch } = client;
|
|
251
|
+
if (Fetch) {
|
|
252
|
+
await withTimeout(Fetch.disable(), 3000, 'Fetch.disable timeout');
|
|
253
|
+
}
|
|
254
|
+
// Close persistent client to clean up listeners
|
|
255
|
+
await connector.closePersistentClient(tabId);
|
|
256
|
+
const effectiveTabId = tabId || 'default';
|
|
257
|
+
interceptedResponses.delete(effectiveTabId);
|
|
258
|
+
console.error(`✅ Response interceptor STOPPED for tab: ${effectiveTabId}`);
|
|
259
|
+
return {
|
|
260
|
+
success: true,
|
|
261
|
+
message: 'Response interception disabled and listener closed'
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
// Even if disable fails, clean up local state
|
|
266
|
+
const effectiveTabId = tabId || 'default';
|
|
267
|
+
interceptedResponses.delete(effectiveTabId);
|
|
268
|
+
try {
|
|
269
|
+
await connector.closePersistentClient(tabId);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
// ignore
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
message: 'Response interception disabled (with errors)',
|
|
277
|
+
warning: error.message
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
283
|
+
// 2. REQUEST/RESPONSE MOCKING
|
|
284
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
285
|
+
{
|
|
286
|
+
name: 'create_mock_endpoint',
|
|
287
|
+
description: '🎭 Creates fake API endpoint - intercepts URL and returns fake data. USE THIS WHEN: 1️⃣ Testing frontend without backend (API not ready). 2️⃣ Simulating error responses (test error handling). 3️⃣ Replacing slow APIs (instant fake data). 4️⃣ Creating demo/prototype (fake data looks real). WORKFLOW: create_mock_endpoint → navigate/click → page gets fake response instead of real API. ⚠️ CONFLICT: Cannot run with enable_response_interception simultaneously. Disable interception first. PARAMS: urlPattern supports wildcards (*api.com/users*), latency simulates slow network.',
|
|
288
|
+
inputSchema: z.object({
|
|
289
|
+
urlPattern: z.string().describe('URL pattern to mock (supports * wildcards)'),
|
|
290
|
+
responseBody: z.string().describe('Response body (JSON string, HTML, etc.)'),
|
|
291
|
+
statusCode: z.number().default(200).describe('HTTP status code'),
|
|
292
|
+
headers: z.record(z.string()).optional().describe('Response headers'),
|
|
293
|
+
latency: z.number().default(0).describe('Simulated latency in milliseconds'),
|
|
294
|
+
method: z.string().optional().describe('HTTP method to match (GET, POST, etc.)'),
|
|
295
|
+
timeoutMs: z.number().default(15000).optional().describe('Operation timeout in milliseconds (default: 15000)'),
|
|
296
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
297
|
+
}),
|
|
298
|
+
handler: async ({ urlPattern, responseBody, statusCode = 200, headers = {}, latency = 0, method, timeoutMs = 15000, tabId }) => {
|
|
299
|
+
try {
|
|
300
|
+
const tabKey = tabId || 'default';
|
|
301
|
+
// Check for conflicts with response interception
|
|
302
|
+
if (interceptedResponses.has(tabKey) && interceptedResponses.get(tabKey).size > 0) {
|
|
303
|
+
console.error('⚠️ WARNING: Response interception is already active. This may cause conflicts.');
|
|
304
|
+
return {
|
|
305
|
+
success: false,
|
|
306
|
+
error: 'Conflict detected: Response interception is already active',
|
|
307
|
+
suggestion: 'Call disable_response_interception first, then create mock endpoints',
|
|
308
|
+
interceptedCount: interceptedResponses.get(tabKey).size
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// Validate inputs
|
|
312
|
+
if (!urlPattern || urlPattern.trim() === '') {
|
|
313
|
+
return {
|
|
314
|
+
success: false,
|
|
315
|
+
error: 'urlPattern is required and cannot be empty'
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
if (latency < 0 || latency > 60000) {
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
error: 'latency must be between 0 and 60000ms'
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
await withTimeout(connector.verifyConnection(), 5000, 'Connection timeout');
|
|
325
|
+
// Use PERSISTENT client
|
|
326
|
+
const client = await withTimeout(connector.getPersistentClient(tabId), 5000, 'Get persistent client timeout');
|
|
327
|
+
const { Network, Fetch } = client;
|
|
328
|
+
if (!Network || !Fetch) {
|
|
329
|
+
throw new Error('Network or Fetch domain not available');
|
|
330
|
+
}
|
|
331
|
+
await withTimeout(Network.enable(), timeoutMs, 'Network.enable timeout');
|
|
332
|
+
await withTimeout(Fetch.enable({
|
|
333
|
+
patterns: [{ urlPattern, requestStage: 'Request' }]
|
|
334
|
+
}), timeoutMs, 'Fetch.enable timeout');
|
|
335
|
+
const effectiveTabId = tabId || 'default';
|
|
336
|
+
if (!mockEndpoints.has(effectiveTabId)) {
|
|
337
|
+
mockEndpoints.set(effectiveTabId, []);
|
|
338
|
+
}
|
|
339
|
+
const mock = {
|
|
340
|
+
urlPattern,
|
|
341
|
+
responseBody,
|
|
342
|
+
statusCode,
|
|
343
|
+
headers: headers || {},
|
|
344
|
+
latency,
|
|
345
|
+
method,
|
|
346
|
+
callCount: 0
|
|
347
|
+
};
|
|
348
|
+
mockEndpoints.get(effectiveTabId).push(mock);
|
|
349
|
+
// Register listener on persistent client
|
|
350
|
+
Fetch.requestPaused(async (params) => {
|
|
351
|
+
try {
|
|
352
|
+
const url = params.request.url;
|
|
353
|
+
const requestMethod = params.request.method;
|
|
354
|
+
const matchingMock = mockEndpoints.get(effectiveTabId)?.find((m) => {
|
|
355
|
+
try {
|
|
356
|
+
// Better pattern matching
|
|
357
|
+
let urlMatch = false;
|
|
358
|
+
// Convert glob pattern to regex
|
|
359
|
+
const pattern = m.urlPattern
|
|
360
|
+
.replace(/\./g, '\\.') // Escape dots
|
|
361
|
+
.replace(/\*/g, '.*') // * becomes .*
|
|
362
|
+
.replace(/\?/g, '.'); // ? becomes .
|
|
363
|
+
const regex = new RegExp(`^${pattern}$`, 'i');
|
|
364
|
+
urlMatch = regex.test(url);
|
|
365
|
+
// Fallback: simple contains check
|
|
366
|
+
if (!urlMatch && m.urlPattern.includes('*')) {
|
|
367
|
+
const plainPart = m.urlPattern.replace(/\*/g, '');
|
|
368
|
+
urlMatch = url.includes(plainPart);
|
|
369
|
+
}
|
|
370
|
+
const methodMatch = !m.method || m.method.toUpperCase() === requestMethod.toUpperCase();
|
|
371
|
+
if (urlMatch && methodMatch) {
|
|
372
|
+
console.error(`[Mock Matcher] ✅ Pattern "${m.urlPattern}" matched URL: ${url}`);
|
|
373
|
+
}
|
|
374
|
+
return urlMatch && methodMatch;
|
|
375
|
+
}
|
|
376
|
+
catch (e) {
|
|
377
|
+
console.error('[Mock Matcher] ❌ Pattern matching error:', e);
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
if (matchingMock) {
|
|
382
|
+
matchingMock.callCount++;
|
|
383
|
+
console.error(`[Mock Endpoint] 🎯 Intercepted ${requestMethod} ${url} -> Responding with mock data`);
|
|
384
|
+
if (matchingMock.latency > 0) {
|
|
385
|
+
await new Promise(resolve => setTimeout(resolve, matchingMock.latency));
|
|
386
|
+
}
|
|
387
|
+
const responseHeaders = [
|
|
388
|
+
{ name: 'Content-Type', value: 'application/json' },
|
|
389
|
+
...Object.entries(matchingMock.headers || {}).map(([name, value]) => ({ name, value }))
|
|
390
|
+
];
|
|
391
|
+
await withTimeout(Fetch.fulfillRequest({
|
|
392
|
+
requestId: params.requestId,
|
|
393
|
+
responseCode: matchingMock.statusCode,
|
|
394
|
+
responseHeaders,
|
|
395
|
+
body: Buffer.from(matchingMock.responseBody).toString('base64')
|
|
396
|
+
}), timeoutMs, 'fulfillRequest timeout');
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
await withTimeout(Fetch.continueRequest({ requestId: params.requestId }), Math.min(timeoutMs, 5000), 'continueRequest timeout');
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
catch (e) {
|
|
403
|
+
console.error('[Mock Endpoint] Error processing request:', e);
|
|
404
|
+
try {
|
|
405
|
+
await Fetch.continueRequest({ requestId: params.requestId });
|
|
406
|
+
}
|
|
407
|
+
catch (continueError) {
|
|
408
|
+
console.error('[Mock Endpoint] Failed to continue request:', continueError);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
console.error(`✅ Mock endpoint ACTIVE for pattern: ${urlPattern}`);
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
message: `Mock endpoint created and LISTENING for ${urlPattern}`,
|
|
416
|
+
mock: {
|
|
417
|
+
urlPattern,
|
|
418
|
+
statusCode,
|
|
419
|
+
latency,
|
|
420
|
+
method: method || 'any'
|
|
421
|
+
},
|
|
422
|
+
note: 'Mock is now ACTIVE and will intercept matching requests until cleared'
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
return {
|
|
427
|
+
success: false,
|
|
428
|
+
error: error.message || 'Failed to create mock endpoint',
|
|
429
|
+
details: error.stack,
|
|
430
|
+
suggestion: 'Ensure Chrome is running and page is loaded'
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'list_mock_endpoints',
|
|
437
|
+
description: 'List all active mock endpoints',
|
|
438
|
+
inputSchema: z.object({
|
|
439
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
440
|
+
}),
|
|
441
|
+
handler: async ({ tabId }) => {
|
|
442
|
+
await connector.verifyConnection();
|
|
443
|
+
const effectiveTabId = tabId || 'default';
|
|
444
|
+
const mocks = mockEndpoints.get(effectiveTabId) || [];
|
|
445
|
+
return {
|
|
446
|
+
success: true,
|
|
447
|
+
mocks: mocks.map((m) => ({
|
|
448
|
+
urlPattern: m.urlPattern,
|
|
449
|
+
statusCode: m.statusCode,
|
|
450
|
+
method: m.method || 'any',
|
|
451
|
+
latency: m.latency,
|
|
452
|
+
callCount: m.callCount
|
|
453
|
+
})),
|
|
454
|
+
count: mocks.length
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: 'delete_mock_endpoint',
|
|
460
|
+
description: 'Delete a specific mock endpoint',
|
|
461
|
+
inputSchema: z.object({
|
|
462
|
+
urlPattern: z.string().describe('URL pattern of mock to delete'),
|
|
463
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
464
|
+
}),
|
|
465
|
+
handler: async ({ urlPattern, tabId }) => {
|
|
466
|
+
await connector.verifyConnection();
|
|
467
|
+
const effectiveTabId = tabId || 'default';
|
|
468
|
+
const mocks = mockEndpoints.get(effectiveTabId);
|
|
469
|
+
if (!mocks) {
|
|
470
|
+
return { success: false, message: 'No mocks found' };
|
|
471
|
+
}
|
|
472
|
+
const initialLength = mocks.length;
|
|
473
|
+
const filtered = mocks.filter((m) => m.urlPattern !== urlPattern);
|
|
474
|
+
mockEndpoints.set(effectiveTabId, filtered);
|
|
475
|
+
return {
|
|
476
|
+
success: true,
|
|
477
|
+
message: `Deleted ${initialLength - filtered.length} mock(s)`,
|
|
478
|
+
remaining: filtered.length
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
name: 'clear_all_mocks',
|
|
484
|
+
description: 'Clear all mock endpoints and close persistent listeners',
|
|
485
|
+
inputSchema: z.object({
|
|
486
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
487
|
+
}),
|
|
488
|
+
handler: async ({ tabId }) => {
|
|
489
|
+
await connector.verifyConnection();
|
|
490
|
+
const effectiveTabId = tabId || 'default';
|
|
491
|
+
const count = mockEndpoints.get(effectiveTabId)?.length || 0;
|
|
492
|
+
mockEndpoints.delete(effectiveTabId);
|
|
493
|
+
// Close persistent client
|
|
494
|
+
try {
|
|
495
|
+
await connector.closePersistentClient(tabId);
|
|
496
|
+
console.error(`✅ Mock endpoints CLEARED and listener closed`);
|
|
497
|
+
}
|
|
498
|
+
catch (e) {
|
|
499
|
+
console.error('⚠️ Error closing persistent client:', e);
|
|
500
|
+
}
|
|
501
|
+
return {
|
|
502
|
+
success: true,
|
|
503
|
+
message: `Cleared ${count} mock endpoint(s) and closed listener`
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
508
|
+
// 3. WEBSOCKET INTERCEPTION
|
|
509
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
510
|
+
{
|
|
511
|
+
name: 'enable_websocket_interception',
|
|
512
|
+
description: '📡 Intercepts WebSocket traffic (real-time bidirectional messages). USE THIS WHEN: 1️⃣ Debugging chat applications (see messages sent/received). 2️⃣ Analyzing game state updates (real-time data). 3️⃣ Monitoring live notifications/updates. 4️⃣ Inspecting streaming data. WHY: WebSockets are hidden from regular network tools (not HTTP requests). WORKFLOW: enable_websocket_interception → interact with app → list_websocket_messages → see real-time traffic. Common for: chat apps, collaborative tools, live dashboards, multiplayer games.',
|
|
513
|
+
inputSchema: z.object({
|
|
514
|
+
urlPattern: z.string().optional().describe('URL pattern to intercept (optional, default all)'),
|
|
515
|
+
timeoutMs: z.number().default(10000).optional().describe('Operation timeout in milliseconds (default: 10000)'),
|
|
516
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
517
|
+
}),
|
|
518
|
+
handler: async ({ urlPattern, timeoutMs = 10000, tabId }) => {
|
|
519
|
+
try {
|
|
520
|
+
await withTimeout(connector.verifyConnection(), Math.min(timeoutMs, 5000), 'Connection timeout');
|
|
521
|
+
// Use PERSISTENT client
|
|
522
|
+
const client = await withTimeout(connector.getPersistentClient(tabId), Math.min(timeoutMs, 5000), 'Get persistent client timeout');
|
|
523
|
+
const { Network } = client;
|
|
524
|
+
if (!Network) {
|
|
525
|
+
throw new Error('Network domain not available');
|
|
526
|
+
}
|
|
527
|
+
await withTimeout(Network.enable(), timeoutMs, 'Network.enable timeout');
|
|
528
|
+
const effectiveTabId = tabId || 'default';
|
|
529
|
+
if (!websocketConnections.has(effectiveTabId)) {
|
|
530
|
+
websocketConnections.set(effectiveTabId, []);
|
|
531
|
+
}
|
|
532
|
+
if (!websocketMessages.has(effectiveTabId)) {
|
|
533
|
+
websocketMessages.set(effectiveTabId, []);
|
|
534
|
+
}
|
|
535
|
+
// Register listeners on persistent client
|
|
536
|
+
Network.webSocketCreated((params) => {
|
|
537
|
+
try {
|
|
538
|
+
const conns = websocketConnections.get(effectiveTabId);
|
|
539
|
+
if (conns) {
|
|
540
|
+
conns.push({
|
|
541
|
+
requestId: params.requestId,
|
|
542
|
+
url: params.url,
|
|
543
|
+
initiator: params.initiator,
|
|
544
|
+
timestamp: Date.now()
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
console.error('[WebSocket] Error storing connection:', e);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
Network.webSocketFrameSent((params) => {
|
|
553
|
+
try {
|
|
554
|
+
const messages = websocketMessages.get(effectiveTabId);
|
|
555
|
+
if (messages) {
|
|
556
|
+
messages.push({
|
|
557
|
+
requestId: params.requestId,
|
|
558
|
+
timestamp: params.timestamp,
|
|
559
|
+
direction: 'sent',
|
|
560
|
+
opcode: params.response?.opcode,
|
|
561
|
+
mask: params.response?.mask,
|
|
562
|
+
payloadData: params.response?.payloadData
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
catch (e) {
|
|
567
|
+
console.error('[WebSocket] Error storing sent message:', e);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
Network.webSocketFrameReceived((params) => {
|
|
571
|
+
try {
|
|
572
|
+
const messages = websocketMessages.get(effectiveTabId);
|
|
573
|
+
if (messages) {
|
|
574
|
+
messages.push({
|
|
575
|
+
requestId: params.requestId,
|
|
576
|
+
timestamp: params.timestamp,
|
|
577
|
+
direction: 'received',
|
|
578
|
+
opcode: params.response?.opcode,
|
|
579
|
+
mask: params.response?.mask,
|
|
580
|
+
payloadData: params.response?.payloadData
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
catch (e) {
|
|
585
|
+
console.error('[WebSocket] Error storing received message:', e);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
Network.webSocketClosed((params) => {
|
|
589
|
+
try {
|
|
590
|
+
const conns = websocketConnections.get(effectiveTabId);
|
|
591
|
+
if (conns) {
|
|
592
|
+
const conn = conns.find((c) => c.requestId === params.requestId);
|
|
593
|
+
if (conn) {
|
|
594
|
+
conn.closed = true;
|
|
595
|
+
conn.closedAt = Date.now();
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
catch (e) {
|
|
600
|
+
console.error('[WebSocket] Error marking connection closed:', e);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
console.error(`✅ WebSocket interceptor ACTIVE and listening`);
|
|
604
|
+
return {
|
|
605
|
+
success: true,
|
|
606
|
+
message: 'WebSocket interception enabled and LISTENING',
|
|
607
|
+
pattern: urlPattern || 'all',
|
|
608
|
+
note: 'WebSocket interceptor is now ACTIVE and will capture messages until disabled'
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
return {
|
|
613
|
+
success: false,
|
|
614
|
+
error: error.message || 'Failed to enable WebSocket interception',
|
|
615
|
+
details: error.stack,
|
|
616
|
+
suggestion: 'Ensure Chrome is running and page is loaded'
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: 'list_websocket_connections',
|
|
623
|
+
description: 'List all WebSocket connections',
|
|
624
|
+
inputSchema: z.object({
|
|
625
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
626
|
+
}),
|
|
627
|
+
handler: async ({ tabId }) => {
|
|
628
|
+
await connector.verifyConnection();
|
|
629
|
+
const effectiveTabId = tabId || 'default';
|
|
630
|
+
const conns = websocketConnections.get(effectiveTabId) || [];
|
|
631
|
+
return {
|
|
632
|
+
success: true,
|
|
633
|
+
connections: conns.map((c) => ({
|
|
634
|
+
requestId: c.requestId,
|
|
635
|
+
url: c.url,
|
|
636
|
+
timestamp: c.timestamp,
|
|
637
|
+
closed: c.closed || false
|
|
638
|
+
})),
|
|
639
|
+
count: conns.length
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
name: 'list_websocket_messages',
|
|
645
|
+
description: 'List all WebSocket messages (sent and received)',
|
|
646
|
+
inputSchema: z.object({
|
|
647
|
+
requestId: z.string().optional().describe('Filter by specific WebSocket connection'),
|
|
648
|
+
direction: z.enum(['sent', 'received', 'all']).default('all').describe('Filter by direction'),
|
|
649
|
+
limit: z.number().default(100).describe('Max messages to return'),
|
|
650
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
651
|
+
}),
|
|
652
|
+
handler: async ({ requestId, direction = 'all', limit = 100, tabId }) => {
|
|
653
|
+
await connector.verifyConnection();
|
|
654
|
+
const effectiveTabId = tabId || 'default';
|
|
655
|
+
let messages = websocketMessages.get(effectiveTabId) || [];
|
|
656
|
+
if (requestId) {
|
|
657
|
+
messages = messages.filter((m) => m.requestId === requestId);
|
|
658
|
+
}
|
|
659
|
+
if (direction !== 'all') {
|
|
660
|
+
messages = messages.filter((m) => m.direction === direction);
|
|
661
|
+
}
|
|
662
|
+
messages = messages.slice(-limit);
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
messages: messages.map((m) => ({
|
|
666
|
+
requestId: m.requestId,
|
|
667
|
+
timestamp: m.timestamp,
|
|
668
|
+
direction: m.direction,
|
|
669
|
+
payloadData: m.payloadData
|
|
670
|
+
})),
|
|
671
|
+
count: messages.length
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
name: 'send_websocket_message',
|
|
677
|
+
description: 'Send a fake WebSocket message (inject into the stream)',
|
|
678
|
+
inputSchema: z.object({
|
|
679
|
+
requestId: z.string().describe('WebSocket connection ID'),
|
|
680
|
+
message: z.string().describe('Message to send'),
|
|
681
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
682
|
+
}),
|
|
683
|
+
handler: async ({ requestId, message, tabId }) => {
|
|
684
|
+
try {
|
|
685
|
+
if (!message || message.trim() === '') {
|
|
686
|
+
return {
|
|
687
|
+
success: false,
|
|
688
|
+
error: 'message cannot be empty'
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
await withTimeout(connector.verifyConnection(), 3000, 'Connection timeout');
|
|
692
|
+
const client = await withTimeout(connector.getTabClient(tabId), 3000, 'Get client timeout');
|
|
693
|
+
const { Runtime } = client;
|
|
694
|
+
if (!Runtime) {
|
|
695
|
+
throw new Error('Runtime domain not available');
|
|
696
|
+
}
|
|
697
|
+
await withTimeout(Runtime.enable(), 3000, 'Runtime.enable timeout');
|
|
698
|
+
// Escape message for JavaScript injection
|
|
699
|
+
const escapedMessage = message.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n');
|
|
700
|
+
const script = `
|
|
701
|
+
(function() {
|
|
702
|
+
// Find the WebSocket by inspecting global WebSocket instances
|
|
703
|
+
// This is a workaround since CDP doesn't expose WS instances
|
|
704
|
+
const originalSend = WebSocket.prototype.send;
|
|
705
|
+
let foundWS = null;
|
|
706
|
+
|
|
707
|
+
WebSocket.prototype.send = function(...args) {
|
|
708
|
+
foundWS = this;
|
|
709
|
+
return originalSend.apply(this, args);
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// Trigger to get reference
|
|
713
|
+
setTimeout(() => {
|
|
714
|
+
if (foundWS && foundWS.readyState === WebSocket.OPEN) {
|
|
715
|
+
try {
|
|
716
|
+
foundWS.send('${escapedMessage}');
|
|
717
|
+
return 'success';
|
|
718
|
+
} catch (e) {
|
|
719
|
+
return 'error: ' + e.message;
|
|
720
|
+
}
|
|
721
|
+
} else {
|
|
722
|
+
return 'error: WebSocket not found or not open';
|
|
723
|
+
}
|
|
724
|
+
}, 100);
|
|
725
|
+
|
|
726
|
+
return 'Message injection attempted';
|
|
727
|
+
})();
|
|
728
|
+
`;
|
|
729
|
+
const result = await withTimeout(Runtime.evaluate({ expression: script }), 5000, 'Runtime.evaluate timeout');
|
|
730
|
+
return {
|
|
731
|
+
success: true,
|
|
732
|
+
message: 'WebSocket message injection attempted',
|
|
733
|
+
note: 'CDP limitation: Direct WS injection requires JavaScript workaround',
|
|
734
|
+
result: result.result?.value
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
catch (error) {
|
|
738
|
+
return {
|
|
739
|
+
success: false,
|
|
740
|
+
error: error.message || 'Failed to send WebSocket message',
|
|
741
|
+
details: error.stack,
|
|
742
|
+
suggestion: 'Ensure WebSocket connection is active and page has a WebSocket instance'
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
{
|
|
748
|
+
name: 'disable_websocket_interception',
|
|
749
|
+
description: 'Stops WebSocket message capturing - ends monitoring of WebSocket connections and message flow. Use when done analyzing WebSocket traffic, to clean up listeners, or to stop real-time message capture.',
|
|
750
|
+
inputSchema: z.object({
|
|
751
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
752
|
+
}),
|
|
753
|
+
handler: async ({ tabId }) => {
|
|
754
|
+
await connector.verifyConnection();
|
|
755
|
+
const effectiveTabId = tabId || 'default';
|
|
756
|
+
websocketConnections.delete(effectiveTabId);
|
|
757
|
+
websocketMessages.delete(effectiveTabId);
|
|
758
|
+
return {
|
|
759
|
+
success: true,
|
|
760
|
+
message: 'WebSocket interception disabled'
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
765
|
+
// 4. HAR FILE GENERATION & REPLAY
|
|
766
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
767
|
+
{
|
|
768
|
+
name: 'start_har_recording',
|
|
769
|
+
description: '🎬 Starts recording ALL network traffic in HAR format. USE THIS WHEN: 1️⃣ Performance analysis (find slow requests). 2️⃣ Debugging network issues (see all requests/responses). 3️⃣ Creating test fixtures (replay captured traffic later). 4️⃣ Documenting API behavior (save all API calls). 5️⃣ Security analysis (inspect headers/cookies). WORKFLOW: start_har_recording → perform actions → stop_har_recording → export_har_file. HAR files can be opened in: Chrome DevTools, HAR Viewer, performance tools.',
|
|
770
|
+
inputSchema: z.object({
|
|
771
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
772
|
+
}),
|
|
773
|
+
handler: async ({ tabId }) => {
|
|
774
|
+
await connector.verifyConnection();
|
|
775
|
+
const client = await connector.getTabClient(tabId);
|
|
776
|
+
const { Network, Page } = client;
|
|
777
|
+
await Network.enable();
|
|
778
|
+
await Page.enable();
|
|
779
|
+
const effectiveTabId = tabId || 'default';
|
|
780
|
+
const recording = {
|
|
781
|
+
startTime: Date.now(),
|
|
782
|
+
entries: [],
|
|
783
|
+
pages: []
|
|
784
|
+
};
|
|
785
|
+
harRecordings.set(effectiveTabId, recording);
|
|
786
|
+
Network.requestWillBeSent((params) => {
|
|
787
|
+
const entry = {
|
|
788
|
+
requestId: params.requestId,
|
|
789
|
+
startedDateTime: new Date(params.timestamp * 1000).toISOString(),
|
|
790
|
+
time: 0,
|
|
791
|
+
request: {
|
|
792
|
+
method: params.request.method,
|
|
793
|
+
url: params.request.url,
|
|
794
|
+
httpVersion: 'HTTP/1.1',
|
|
795
|
+
headers: Object.entries(params.request.headers || {}).map(([name, value]) => ({ name, value })),
|
|
796
|
+
queryString: [],
|
|
797
|
+
cookies: [],
|
|
798
|
+
headersSize: -1,
|
|
799
|
+
bodySize: params.request.postData ? params.request.postData.length : 0
|
|
800
|
+
},
|
|
801
|
+
response: {},
|
|
802
|
+
cache: {},
|
|
803
|
+
timings: {
|
|
804
|
+
blocked: -1,
|
|
805
|
+
dns: -1,
|
|
806
|
+
connect: -1,
|
|
807
|
+
send: 0,
|
|
808
|
+
wait: 0,
|
|
809
|
+
receive: 0,
|
|
810
|
+
ssl: -1
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
recording.entries.push(entry);
|
|
814
|
+
});
|
|
815
|
+
Network.responseReceived((params) => {
|
|
816
|
+
const entry = recording.entries.find((e) => e.requestId === params.requestId);
|
|
817
|
+
if (entry) {
|
|
818
|
+
entry.response = {
|
|
819
|
+
status: params.response.status,
|
|
820
|
+
statusText: params.response.statusText,
|
|
821
|
+
httpVersion: params.response.protocol || 'HTTP/1.1',
|
|
822
|
+
headers: Object.entries(params.response.headers || {}).map(([name, value]) => ({ name, value })),
|
|
823
|
+
cookies: [],
|
|
824
|
+
content: {
|
|
825
|
+
size: 0,
|
|
826
|
+
mimeType: params.response.mimeType || 'application/octet-stream'
|
|
827
|
+
},
|
|
828
|
+
redirectURL: '',
|
|
829
|
+
headersSize: -1,
|
|
830
|
+
bodySize: -1
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
Network.loadingFinished((params) => {
|
|
835
|
+
const entry = recording.entries.find((e) => e.requestId === params.requestId);
|
|
836
|
+
if (entry) {
|
|
837
|
+
entry.time = (params.timestamp * 1000) - new Date(entry.startedDateTime).getTime();
|
|
838
|
+
}
|
|
839
|
+
});
|
|
840
|
+
return {
|
|
841
|
+
success: true,
|
|
842
|
+
message: 'HAR recording started',
|
|
843
|
+
startTime: recording.startTime
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
name: 'stop_har_recording',
|
|
849
|
+
description: '⏹️ Stops HAR recording and returns captured data. USE THIS WHEN: 1️⃣ Done testing/reproducing issue (captured enough traffic). 2️⃣ Ready to analyze requests (stop before reviewing). 3️⃣ Want to get HAR JSON (preview before exporting). PREREQUISITE: Must call start_har_recording first. WORKFLOW: start_har_recording → perform actions → stop_har_recording → export_har_file (to save to disk). Returns: Full HAR JSON with all captured requests/responses/timings.',
|
|
850
|
+
inputSchema: z.object({
|
|
851
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
852
|
+
}),
|
|
853
|
+
handler: async ({ tabId }) => {
|
|
854
|
+
await connector.verifyConnection();
|
|
855
|
+
const effectiveTabId = tabId || 'default';
|
|
856
|
+
const recording = harRecordings.get(effectiveTabId);
|
|
857
|
+
if (!recording) {
|
|
858
|
+
throw new Error('No active HAR recording');
|
|
859
|
+
}
|
|
860
|
+
const har = {
|
|
861
|
+
log: {
|
|
862
|
+
version: '1.2',
|
|
863
|
+
creator: {
|
|
864
|
+
name: 'Custom Chrome MCP',
|
|
865
|
+
version: '1.0.9'
|
|
866
|
+
},
|
|
867
|
+
pages: recording.pages,
|
|
868
|
+
entries: recording.entries
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
harRecordings.delete(effectiveTabId);
|
|
872
|
+
return {
|
|
873
|
+
success: true,
|
|
874
|
+
har,
|
|
875
|
+
entriesCount: recording.entries.length,
|
|
876
|
+
duration: Date.now() - recording.startTime
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: 'export_har_file',
|
|
882
|
+
description: '💾 Saves HAR recording to disk as .har file. USE THIS WHEN: 1️⃣ Sharing network logs (send to team/support). 2️⃣ Archiving test runs (keep record of network behavior). 3️⃣ Performance analysis (load in tools: Chrome DevTools, WebPageTest). 4️⃣ Creating test fixtures (replay traffic for testing). PREREQUISITE: Must call stop_har_recording first. FILE FORMAT: JSON file viewable in: Chrome DevTools Network tab, HAR Viewer online, performance tools. WORKFLOW: start_har_recording → actions → stop_har_recording → export_har_file.',
|
|
883
|
+
inputSchema: z.object({
|
|
884
|
+
filename: z.string().describe('Filename to save HAR (e.g., recording.har)'),
|
|
885
|
+
outputDir: z.string().optional().describe('Output directory (default: current directory)'),
|
|
886
|
+
timeoutMs: z.number().default(60000).optional().describe('File write timeout in milliseconds (default: 60000)'),
|
|
887
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
888
|
+
}),
|
|
889
|
+
handler: async ({ filename, outputDir = '.', timeoutMs = 60000, tabId }) => {
|
|
890
|
+
try {
|
|
891
|
+
// Validate filename
|
|
892
|
+
if (!filename || filename.trim() === '') {
|
|
893
|
+
return {
|
|
894
|
+
success: false,
|
|
895
|
+
error: 'filename is required'
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
if (!filename.endsWith('.har')) {
|
|
899
|
+
filename += '.har';
|
|
900
|
+
}
|
|
901
|
+
await withTimeout(connector.verifyConnection(), 3000, 'Connection timeout');
|
|
902
|
+
const effectiveTabId = tabId || 'default';
|
|
903
|
+
const recording = harRecordings.get(effectiveTabId);
|
|
904
|
+
if (!recording) {
|
|
905
|
+
return {
|
|
906
|
+
success: false,
|
|
907
|
+
error: 'No active HAR recording to export',
|
|
908
|
+
suggestion: 'Use start_har_recording first before exporting'
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const har = {
|
|
912
|
+
log: {
|
|
913
|
+
version: '1.2',
|
|
914
|
+
creator: {
|
|
915
|
+
name: 'Custom Chrome MCP',
|
|
916
|
+
version: '1.0.10'
|
|
917
|
+
},
|
|
918
|
+
pages: recording.pages || [],
|
|
919
|
+
entries: recording.entries || []
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
// Ensure directory exists
|
|
923
|
+
try {
|
|
924
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
925
|
+
}
|
|
926
|
+
catch (mkdirError) {
|
|
927
|
+
if (mkdirError.code !== 'EEXIST') {
|
|
928
|
+
throw new Error(`Failed to create directory: ${mkdirError.message}`);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
const filepath = path.join(outputDir, filename);
|
|
932
|
+
await withTimeout(fs.writeFile(filepath, JSON.stringify(har, null, 2), 'utf-8'), timeoutMs, 'File write timeout');
|
|
933
|
+
return {
|
|
934
|
+
success: true,
|
|
935
|
+
message: `HAR file exported to ${filepath}`,
|
|
936
|
+
filepath,
|
|
937
|
+
entriesCount: recording.entries?.length || 0
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
return {
|
|
942
|
+
success: false,
|
|
943
|
+
error: error.message || 'Failed to export HAR file',
|
|
944
|
+
details: error.stack,
|
|
945
|
+
suggestion: 'Check directory permissions and disk space'
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
951
|
+
// 5. ADVANCED REQUEST PATTERNS
|
|
952
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
953
|
+
{
|
|
954
|
+
name: 'add_advanced_interception_pattern',
|
|
955
|
+
description: '🎯 Advanced request filtering (filter by status, size, duration, content-type). USE THIS WHEN: 1️⃣ Finding slow requests (minDuration: 1000 = requests > 1s). 2️⃣ Large file analysis (minSize/maxSize for images/videos). 3️⃣ Error tracking (statusCodeMin: 400 = errors only). 4️⃣ Content type filtering (contentType: "application/json" = API calls). ACTIONS: "log" (track matches), "block" (prevent request), "delay" (throttle speed). ADVANCED: Combine multiple filters for precision (urlPattern + method + statusCodeMin).',
|
|
956
|
+
inputSchema: z.object({
|
|
957
|
+
name: z.string().describe('Pattern name for reference'),
|
|
958
|
+
urlPattern: z.string().optional().describe('URL pattern (glob)'),
|
|
959
|
+
method: z.string().optional().describe('HTTP method'),
|
|
960
|
+
resourceType: z.string().optional().describe('Resource type'),
|
|
961
|
+
statusCodeMin: z.number().optional().describe('Min status code'),
|
|
962
|
+
statusCodeMax: z.number().optional().describe('Max status code'),
|
|
963
|
+
minSize: z.number().optional().describe('Min response size in bytes'),
|
|
964
|
+
maxSize: z.number().optional().describe('Max response size in bytes'),
|
|
965
|
+
minDuration: z.number().optional().describe('Min request duration in ms'),
|
|
966
|
+
contentType: z.string().optional().describe('Content-Type to match'),
|
|
967
|
+
action: z.enum(['log', 'block', 'delay']).default('log').describe('Action to take'),
|
|
968
|
+
delayMs: z.number().optional().describe('Delay in ms (if action=delay)'),
|
|
969
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
970
|
+
}),
|
|
971
|
+
handler: async ({ name, urlPattern, method, resourceType, statusCodeMin, statusCodeMax, minSize, maxSize, minDuration, contentType, action = 'log', delayMs, tabId }) => {
|
|
972
|
+
await connector.verifyConnection();
|
|
973
|
+
const client = await connector.getTabClient(tabId);
|
|
974
|
+
const { Network, Fetch } = client;
|
|
975
|
+
await Network.enable();
|
|
976
|
+
const pattern = {
|
|
977
|
+
name,
|
|
978
|
+
urlPattern,
|
|
979
|
+
method,
|
|
980
|
+
resourceType,
|
|
981
|
+
statusCodeMin,
|
|
982
|
+
statusCodeMax,
|
|
983
|
+
minSize,
|
|
984
|
+
maxSize,
|
|
985
|
+
minDuration,
|
|
986
|
+
contentType,
|
|
987
|
+
action,
|
|
988
|
+
delayMs,
|
|
989
|
+
matchCount: 0
|
|
990
|
+
};
|
|
991
|
+
// Store request start times for duration calculation
|
|
992
|
+
const requestTimes = new Map();
|
|
993
|
+
// Enable Network domain for advanced monitoring
|
|
994
|
+
await Network.enable();
|
|
995
|
+
// Track request start times
|
|
996
|
+
Network.requestWillBeSent((params) => {
|
|
997
|
+
requestTimes.set(params.requestId, params.timestamp);
|
|
998
|
+
});
|
|
999
|
+
// Monitor responses for advanced filtering
|
|
1000
|
+
Network.responseReceived((params) => {
|
|
1001
|
+
const url = params.response.url;
|
|
1002
|
+
const status = params.response.status;
|
|
1003
|
+
const mimeType = params.response.mimeType;
|
|
1004
|
+
const startTime = requestTimes.get(params.requestId);
|
|
1005
|
+
const duration = startTime ? (params.timestamp - startTime) * 1000 : 0;
|
|
1006
|
+
let matches = true;
|
|
1007
|
+
// URL pattern matching
|
|
1008
|
+
if (urlPattern) {
|
|
1009
|
+
const regex = new RegExp(urlPattern.replace(/\*/g, '.*'));
|
|
1010
|
+
if (!regex.test(url))
|
|
1011
|
+
matches = false;
|
|
1012
|
+
}
|
|
1013
|
+
// Status code filtering
|
|
1014
|
+
if (statusCodeMin && status < statusCodeMin)
|
|
1015
|
+
matches = false;
|
|
1016
|
+
if (statusCodeMax && status > statusCodeMax)
|
|
1017
|
+
matches = false;
|
|
1018
|
+
// Content-Type filtering
|
|
1019
|
+
if (contentType && mimeType && !mimeType.includes(contentType))
|
|
1020
|
+
matches = false;
|
|
1021
|
+
// Duration filtering
|
|
1022
|
+
if (minDuration && duration < minDuration)
|
|
1023
|
+
matches = false;
|
|
1024
|
+
// Size filtering (will be checked on loadingFinished)
|
|
1025
|
+
if (matches) {
|
|
1026
|
+
// For 'log' action, just increment counter
|
|
1027
|
+
if (action === 'log') {
|
|
1028
|
+
pattern.matchCount++;
|
|
1029
|
+
console.log(`[Pattern: ${name}] Matched request:`, {
|
|
1030
|
+
url,
|
|
1031
|
+
status,
|
|
1032
|
+
mimeType,
|
|
1033
|
+
duration: `${duration.toFixed(0)}ms`
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
requestTimes.delete(params.requestId);
|
|
1038
|
+
});
|
|
1039
|
+
// Enable Fetch for blocking/delaying (basic filtering)
|
|
1040
|
+
if (action === 'block' || action === 'delay') {
|
|
1041
|
+
if (urlPattern) {
|
|
1042
|
+
await Fetch.enable({
|
|
1043
|
+
patterns: [{
|
|
1044
|
+
urlPattern,
|
|
1045
|
+
requestStage: 'Request'
|
|
1046
|
+
}]
|
|
1047
|
+
});
|
|
1048
|
+
Fetch.requestPaused(async (params) => {
|
|
1049
|
+
let matches = true;
|
|
1050
|
+
if (method && params.request.method !== method)
|
|
1051
|
+
matches = false;
|
|
1052
|
+
if (resourceType && params.resourceType !== resourceType)
|
|
1053
|
+
matches = false;
|
|
1054
|
+
if (matches && action === 'block') {
|
|
1055
|
+
await Fetch.failRequest({
|
|
1056
|
+
requestId: params.requestId,
|
|
1057
|
+
errorReason: 'BlockedByClient'
|
|
1058
|
+
});
|
|
1059
|
+
pattern.matchCount++;
|
|
1060
|
+
}
|
|
1061
|
+
else if (matches && action === 'delay') {
|
|
1062
|
+
if (delayMs) {
|
|
1063
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
1064
|
+
}
|
|
1065
|
+
await Fetch.continueRequest({ requestId: params.requestId });
|
|
1066
|
+
pattern.matchCount++;
|
|
1067
|
+
}
|
|
1068
|
+
else {
|
|
1069
|
+
await Fetch.continueRequest({ requestId: params.requestId });
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
success: true,
|
|
1076
|
+
message: `Advanced pattern '${name}' added`,
|
|
1077
|
+
pattern: {
|
|
1078
|
+
name,
|
|
1079
|
+
action,
|
|
1080
|
+
filters: {
|
|
1081
|
+
urlPattern,
|
|
1082
|
+
method,
|
|
1083
|
+
resourceType,
|
|
1084
|
+
statusCode: statusCodeMin && statusCodeMax ? `${statusCodeMin}-${statusCodeMax}` : undefined,
|
|
1085
|
+
size: minSize && maxSize ? `${minSize}-${maxSize}` : undefined,
|
|
1086
|
+
duration: minDuration ? `>${minDuration}ms` : undefined,
|
|
1087
|
+
contentType
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
note: action === 'log'
|
|
1091
|
+
? 'Pattern will log matching requests to console'
|
|
1092
|
+
: `Pattern will ${action} matching requests`
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1097
|
+
// 6. CSS/JS INJECTION PIPELINE
|
|
1098
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
1099
|
+
{
|
|
1100
|
+
name: 'inject_css_global',
|
|
1101
|
+
description: '🎨 Injects persistent CSS into all pages (survives navigation). USE THIS WHEN: 1️⃣ Hiding elements (e.g., .ad { display: none !important; }). 2️⃣ Custom styling (dark mode, font changes, colors). 3️⃣ Fixing UI bugs (z-index issues, layout breaks). 4️⃣ Accessibility (increase contrast, font size). 5️⃣ Testing responsive design (force mobile/desktop views). PERSISTENT: CSS auto-applies to new pages. REMOVAL: Use clear_all_injections or remove_injection. TIP: Use !important for specificity.',
|
|
1102
|
+
inputSchema: z.object({
|
|
1103
|
+
css: z.string().describe('CSS code to inject'),
|
|
1104
|
+
name: z.string().optional().describe('Name for this injection (for reference)'),
|
|
1105
|
+
timeoutMs: z.number().default(10000).optional().describe('Operation timeout in milliseconds (default: 10000)'),
|
|
1106
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
1107
|
+
}),
|
|
1108
|
+
handler: async ({ css, name, timeoutMs = 10000, tabId }) => {
|
|
1109
|
+
try {
|
|
1110
|
+
if (!css || css.trim() === '') {
|
|
1111
|
+
return {
|
|
1112
|
+
success: false,
|
|
1113
|
+
error: 'css cannot be empty'
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
await withTimeout(connector.verifyConnection(), 5000, 'Connection timeout');
|
|
1117
|
+
const client = await withTimeout(connector.getTabClient(tabId), 5000, 'Get client timeout');
|
|
1118
|
+
const { Page } = client;
|
|
1119
|
+
if (!Page) {
|
|
1120
|
+
throw new Error('Page domain not available');
|
|
1121
|
+
}
|
|
1122
|
+
await withTimeout(Page.enable(), 5000, 'Page.enable timeout');
|
|
1123
|
+
// Escape CSS safely
|
|
1124
|
+
const escapedCSS = css.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
1125
|
+
const escapedName = (name || 'unnamed').replace(/'/g, "\\'");
|
|
1126
|
+
const script = `
|
|
1127
|
+
(function() {
|
|
1128
|
+
try {
|
|
1129
|
+
const style = document.createElement('style');
|
|
1130
|
+
style.textContent = \`${escapedCSS}\`;
|
|
1131
|
+
style.setAttribute('data-mcp-injection', '${escapedName}');
|
|
1132
|
+
if (document.head) {
|
|
1133
|
+
document.head.appendChild(style);
|
|
1134
|
+
} else {
|
|
1135
|
+
// Fallback if head doesn't exist yet
|
|
1136
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1137
|
+
document.head.appendChild(style);
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
return 'success';
|
|
1141
|
+
} catch (e) {
|
|
1142
|
+
return 'error: ' + e.message;
|
|
1143
|
+
}
|
|
1144
|
+
})();
|
|
1145
|
+
`;
|
|
1146
|
+
const result = await withTimeout(Page.addScriptToEvaluateOnNewDocument({ source: script }), timeoutMs, 'addScriptToEvaluateOnNewDocument timeout');
|
|
1147
|
+
const effectiveTabId = tabId || 'default';
|
|
1148
|
+
if (!injectedScripts.has(effectiveTabId)) {
|
|
1149
|
+
injectedScripts.set(effectiveTabId, []);
|
|
1150
|
+
}
|
|
1151
|
+
injectedScripts.get(effectiveTabId).push(result.identifier);
|
|
1152
|
+
// Also inject in current page
|
|
1153
|
+
const { Runtime } = client;
|
|
1154
|
+
if (Runtime) {
|
|
1155
|
+
await withTimeout(Runtime.enable(), Math.min(timeoutMs, 3000), 'Runtime.enable timeout');
|
|
1156
|
+
await withTimeout(Runtime.evaluate({ expression: script }), timeoutMs, 'Runtime.evaluate timeout');
|
|
1157
|
+
}
|
|
1158
|
+
return {
|
|
1159
|
+
success: true,
|
|
1160
|
+
message: 'CSS injected globally',
|
|
1161
|
+
identifier: result.identifier,
|
|
1162
|
+
name: name || 'unnamed'
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
catch (error) {
|
|
1166
|
+
return {
|
|
1167
|
+
success: false,
|
|
1168
|
+
error: error.message || 'Failed to inject CSS',
|
|
1169
|
+
details: error.stack,
|
|
1170
|
+
suggestion: 'Check CSS syntax and ensure page is loaded'
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1175
|
+
{
|
|
1176
|
+
name: 'inject_js_global',
|
|
1177
|
+
description: '⚡ Injects persistent JavaScript into all pages (runs before page loads). USE THIS WHEN: 1️⃣ Intercepting functions (e.g., window.fetch = customFetch). 2️⃣ Adding global utilities (helper functions on every page). 3️⃣ Modifying APIs (override console.log, localStorage). 4️⃣ Event monitoring (capture all clicks before page code). 5️⃣ Anti-detection bypass (modify navigator.webdriver). TIMING: Runs BEFORE page scripts (critical for interception). PERSISTENT: Auto-applies to new pages. CAUTION: Can break sites if code has errors.',
|
|
1178
|
+
inputSchema: z.object({
|
|
1179
|
+
javascript: z.string().describe('JavaScript code to inject'),
|
|
1180
|
+
name: z.string().optional().describe('Name for this injection (for reference)'),
|
|
1181
|
+
runImmediately: z.boolean().default(true).describe('Also run in current page'),
|
|
1182
|
+
timeoutMs: z.number().default(15000).optional().describe('Operation timeout in milliseconds (default: 15000)'),
|
|
1183
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
1184
|
+
}),
|
|
1185
|
+
handler: async ({ javascript, name, runImmediately = true, timeoutMs = 15000, tabId }) => {
|
|
1186
|
+
try {
|
|
1187
|
+
if (!javascript || javascript.trim() === '') {
|
|
1188
|
+
return {
|
|
1189
|
+
success: false,
|
|
1190
|
+
error: 'javascript cannot be empty'
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
await withTimeout(connector.verifyConnection(), Math.min(timeoutMs, 5000), 'Connection timeout');
|
|
1194
|
+
const client = await withTimeout(connector.getTabClient(tabId), Math.min(timeoutMs, 5000), 'Get client timeout');
|
|
1195
|
+
const { Page } = client;
|
|
1196
|
+
if (!Page) {
|
|
1197
|
+
throw new Error('Page domain not available');
|
|
1198
|
+
}
|
|
1199
|
+
await withTimeout(Page.enable(), timeoutMs, 'Page.enable timeout');
|
|
1200
|
+
// Validate JavaScript syntax
|
|
1201
|
+
try {
|
|
1202
|
+
new Function(javascript);
|
|
1203
|
+
}
|
|
1204
|
+
catch (syntaxError) {
|
|
1205
|
+
return {
|
|
1206
|
+
success: false,
|
|
1207
|
+
error: 'JavaScript syntax error',
|
|
1208
|
+
details: syntaxError.message,
|
|
1209
|
+
suggestion: 'Check your JavaScript code for syntax errors'
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
const result = await withTimeout(Page.addScriptToEvaluateOnNewDocument({ source: javascript }), timeoutMs, 'addScriptToEvaluateOnNewDocument timeout');
|
|
1213
|
+
const effectiveTabId = tabId || 'default';
|
|
1214
|
+
if (!injectedScripts.has(effectiveTabId)) {
|
|
1215
|
+
injectedScripts.set(effectiveTabId, []);
|
|
1216
|
+
}
|
|
1217
|
+
injectedScripts.get(effectiveTabId).push(result.identifier);
|
|
1218
|
+
if (runImmediately) {
|
|
1219
|
+
const { Runtime } = client;
|
|
1220
|
+
if (Runtime) {
|
|
1221
|
+
await withTimeout(Runtime.enable(), Math.min(timeoutMs, 3000), 'Runtime.enable timeout');
|
|
1222
|
+
const evalResult = await withTimeout(Runtime.evaluate({ expression: javascript, returnByValue: false }), timeoutMs, 'Runtime.evaluate timeout');
|
|
1223
|
+
if (evalResult.exceptionDetails) {
|
|
1224
|
+
return {
|
|
1225
|
+
success: true,
|
|
1226
|
+
message: 'JavaScript injected globally but execution failed in current page',
|
|
1227
|
+
identifier: result.identifier,
|
|
1228
|
+
name: name || 'unnamed',
|
|
1229
|
+
executionError: evalResult.exceptionDetails.text,
|
|
1230
|
+
runImmediately
|
|
1231
|
+
};
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
return {
|
|
1236
|
+
success: true,
|
|
1237
|
+
message: 'JavaScript injected globally',
|
|
1238
|
+
identifier: result.identifier,
|
|
1239
|
+
name: name || 'unnamed',
|
|
1240
|
+
runImmediately
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
catch (error) {
|
|
1244
|
+
return {
|
|
1245
|
+
success: false,
|
|
1246
|
+
error: error.message || 'Failed to inject JavaScript',
|
|
1247
|
+
details: error.stack,
|
|
1248
|
+
suggestion: 'Check JavaScript syntax and ensure page is loaded'
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
},
|
|
1253
|
+
{
|
|
1254
|
+
name: 'list_injected_scripts',
|
|
1255
|
+
description: '📋 Lists all currently active global CSS/JS injections. USE THIS WHEN: 1️⃣ Debugging styling issues (check what CSS is injected). 2️⃣ Page behavior is unexpected (see active scripts). 3️⃣ Before adding more injections (avoid duplicates). 4️⃣ Getting identifiers for removal (use with remove_injection). Returns: Array with identifier, name, type (CSS/JS) for each injection. MANAGEMENT: Use clear_all_injections to remove all, or remove_injection with specific identifier.',
|
|
1256
|
+
inputSchema: z.object({
|
|
1257
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
1258
|
+
}),
|
|
1259
|
+
handler: async ({ tabId }) => {
|
|
1260
|
+
await connector.verifyConnection();
|
|
1261
|
+
const effectiveTabId = tabId || 'default';
|
|
1262
|
+
const scripts = injectedScripts.get(effectiveTabId) || [];
|
|
1263
|
+
return {
|
|
1264
|
+
success: true,
|
|
1265
|
+
injections: scripts,
|
|
1266
|
+
count: scripts.length
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
name: 'remove_injection',
|
|
1272
|
+
description: '🗑️ Removes specific global CSS/JS injection by identifier. USE THIS WHEN: 1️⃣ Injection causing bugs (remove problematic script). 2️⃣ No longer needed (cleanup after testing). 3️⃣ Updating injection (remove old, add new). PREREQUISITE: Get identifier from list_injected_scripts. EFFECT: Stops applying to new pages (existing pages keep injection until refresh). TIP: Use clear_all_injections to remove all at once.',
|
|
1273
|
+
inputSchema: z.object({
|
|
1274
|
+
identifier: z.string().describe('Injection identifier from inject_css_global or inject_js_global'),
|
|
1275
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
1276
|
+
}),
|
|
1277
|
+
handler: async ({ identifier, tabId }) => {
|
|
1278
|
+
await connector.verifyConnection();
|
|
1279
|
+
const client = await connector.getTabClient(tabId);
|
|
1280
|
+
const { Page } = client;
|
|
1281
|
+
await Page.removeScriptToEvaluateOnNewDocument({
|
|
1282
|
+
identifier
|
|
1283
|
+
});
|
|
1284
|
+
const effectiveTabId = tabId || 'default';
|
|
1285
|
+
const scripts = injectedScripts.get(effectiveTabId);
|
|
1286
|
+
if (scripts) {
|
|
1287
|
+
const filtered = scripts.filter((id) => id !== identifier);
|
|
1288
|
+
injectedScripts.set(effectiveTabId, filtered);
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
success: true,
|
|
1292
|
+
message: `Injection ${identifier} removed`
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
name: 'clear_all_injections',
|
|
1298
|
+
description: '🧹 Removes ALL global CSS/JS injections at once. USE THIS WHEN: 1️⃣ Resetting page to default (remove all modifications). 2️⃣ Injections causing conflicts (start fresh). 3️⃣ Finishing testing (cleanup). 4️⃣ Too many injections to remove individually. EFFECT: Stops applying to new pages (existing pages keep injections until refresh). COUNT: Returns number of injections cleared. TIP: Use list_injected_scripts to see what was removed.',
|
|
1299
|
+
inputSchema: z.object({
|
|
1300
|
+
tabId: z.string().optional().describe('Tab ID (optional)')
|
|
1301
|
+
}),
|
|
1302
|
+
handler: async ({ tabId }) => {
|
|
1303
|
+
await connector.verifyConnection();
|
|
1304
|
+
const client = await connector.getTabClient(tabId);
|
|
1305
|
+
const { Page } = client;
|
|
1306
|
+
const effectiveTabId = tabId || 'default';
|
|
1307
|
+
const scripts = injectedScripts.get(effectiveTabId) || [];
|
|
1308
|
+
for (const identifier of scripts) {
|
|
1309
|
+
try {
|
|
1310
|
+
await Page.removeScriptToEvaluateOnNewDocument({ identifier });
|
|
1311
|
+
}
|
|
1312
|
+
catch (e) {
|
|
1313
|
+
// Ignore errors for already removed scripts
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
injectedScripts.delete(effectiveTabId);
|
|
1317
|
+
return {
|
|
1318
|
+
success: true,
|
|
1319
|
+
message: `Cleared ${scripts.length} injection(s)`
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
];
|
|
1324
|
+
}
|
|
1325
|
+
//# sourceMappingURL=advanced-network.js.map
|