@ricardodeazambuja/browser-mcp-server 1.0.3 → 1.4.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/CHANGELOG-v1.3.0.md +42 -0
- package/CHANGELOG-v1.4.0.md +8 -0
- package/README.md +271 -45
- package/package.json +11 -10
- package/plugins/.gitkeep +0 -0
- package/src/.gitkeep +0 -0
- package/src/browser.js +152 -0
- package/src/cdp.js +58 -0
- package/src/index.js +126 -0
- package/src/tools/.gitkeep +0 -0
- package/src/tools/console.js +139 -0
- package/src/tools/docs.js +1611 -0
- package/src/tools/index.js +60 -0
- package/src/tools/info.js +139 -0
- package/src/tools/interaction.js +126 -0
- package/src/tools/keyboard.js +27 -0
- package/src/tools/media.js +264 -0
- package/src/tools/mouse.js +104 -0
- package/src/tools/navigation.js +72 -0
- package/src/tools/network.js +552 -0
- package/src/tools/pages.js +149 -0
- package/src/tools/performance.js +517 -0
- package/src/tools/security.js +470 -0
- package/src/tools/storage.js +467 -0
- package/src/tools/system.js +196 -0
- package/src/utils.js +131 -0
- package/tests/.gitkeep +0 -0
- package/tests/fixtures/.gitkeep +0 -0
- package/tests/fixtures/test-media.html +35 -0
- package/tests/fixtures/test-network.html +48 -0
- package/tests/fixtures/test-performance.html +61 -0
- package/tests/fixtures/test-security.html +33 -0
- package/tests/fixtures/test-storage.html +76 -0
- package/tests/run-all.js +50 -0
- package/{test-browser-automation.js → tests/test-browser-automation.js} +44 -5
- package/{test-mcp.js → tests/test-mcp.js} +9 -4
- package/tests/test-media-tools.js +168 -0
- package/tests/test-network.js +212 -0
- package/tests/test-performance.js +254 -0
- package/tests/test-security.js +203 -0
- package/tests/test-storage.js +192 -0
- package/CHANGELOG-v1.0.2.md +0 -126
- package/browser-mcp-server-playwright.js +0 -792
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network Analysis Tools (CDP-based)
|
|
3
|
+
* Request monitoring, HAR export, WebSocket inspection, throttling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { connectToBrowser } = require('../browser');
|
|
7
|
+
const { getCDPSession } = require('../cdp');
|
|
8
|
+
const { debugLog, version } = require('../utils');
|
|
9
|
+
|
|
10
|
+
// Local state for network tools
|
|
11
|
+
let networkRequests = [];
|
|
12
|
+
let monitoringActive = false;
|
|
13
|
+
let webSocketFrames = new Map();
|
|
14
|
+
const MAX_REQUESTS = 500; // Limit to prevent memory issues
|
|
15
|
+
|
|
16
|
+
const definitions = [
|
|
17
|
+
{
|
|
18
|
+
name: 'browser_net_start_monitoring',
|
|
19
|
+
description: 'Start monitoring network requests with detailed timing (see browser_docs)',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
patterns: {
|
|
24
|
+
type: 'array',
|
|
25
|
+
description: 'URL patterns to monitor (default: all)',
|
|
26
|
+
items: { type: 'string' }
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
additionalProperties: false,
|
|
30
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'browser_net_get_requests',
|
|
35
|
+
description: 'Get captured network requests with timing breakdown (see browser_docs)',
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
properties: {
|
|
39
|
+
filter: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Filter by URL substring'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'browser_net_stop_monitoring',
|
|
50
|
+
description: 'Stop network monitoring and clear request log (see browser_docs)',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {},
|
|
54
|
+
additionalProperties: false,
|
|
55
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'browser_net_export_har',
|
|
60
|
+
description: 'Export full network activity log in HAR format (see browser_docs)',
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: 'object',
|
|
63
|
+
properties: {
|
|
64
|
+
includeContent: {
|
|
65
|
+
type: 'boolean',
|
|
66
|
+
description: 'Include response bodies (default: false)'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'browser_net_get_websocket_frames',
|
|
75
|
+
description: 'Get WebSocket frames for inspecting real-time communication (see browser_docs)',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
requestId: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Request ID from network monitoring'
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
required: ['requestId'],
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'browser_net_set_request_blocking',
|
|
91
|
+
description: 'Block requests matching URL patterns (see browser_docs)',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
patterns: {
|
|
96
|
+
type: 'array',
|
|
97
|
+
description: 'URL patterns to block (e.g., ["*.jpg", "*analytics*"])',
|
|
98
|
+
items: { type: 'string' }
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
required: ['patterns'],
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'browser_net_emulate_conditions',
|
|
108
|
+
description: 'Emulate network conditions (throttling) (see browser_docs)',
|
|
109
|
+
inputSchema: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
offline: {
|
|
113
|
+
type: 'boolean',
|
|
114
|
+
description: 'Emulate offline mode'
|
|
115
|
+
},
|
|
116
|
+
latency: {
|
|
117
|
+
type: 'number',
|
|
118
|
+
description: 'Round-trip latency in ms'
|
|
119
|
+
},
|
|
120
|
+
downloadThroughput: {
|
|
121
|
+
type: 'number',
|
|
122
|
+
description: 'Download speed in bytes/second (-1 for unlimited)'
|
|
123
|
+
},
|
|
124
|
+
uploadThroughput: {
|
|
125
|
+
type: 'number',
|
|
126
|
+
description: 'Upload speed in bytes/second (-1 for unlimited)'
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
required: ['offline', 'latency', 'downloadThroughput', 'uploadThroughput'],
|
|
130
|
+
additionalProperties: false,
|
|
131
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const handlers = {
|
|
137
|
+
browser_net_start_monitoring: async (args) => {
|
|
138
|
+
try {
|
|
139
|
+
if (monitoringActive) {
|
|
140
|
+
return {
|
|
141
|
+
content: [{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: '⚠️ Network monitoring is already active.\n\nUse browser_net_stop_monitoring to stop first, or browser_net_get_requests to view captured data.'
|
|
144
|
+
}]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const cdp = await getCDPSession();
|
|
149
|
+
const patterns = args.patterns || [];
|
|
150
|
+
|
|
151
|
+
// Clear previous requests
|
|
152
|
+
networkRequests = [];
|
|
153
|
+
webSocketFrames.clear();
|
|
154
|
+
|
|
155
|
+
// Enable network tracking
|
|
156
|
+
await cdp.send('Network.enable');
|
|
157
|
+
|
|
158
|
+
// Set up event listeners
|
|
159
|
+
cdp.on('Network.requestWillBeSent', (params) => {
|
|
160
|
+
if (networkRequests.length >= MAX_REQUESTS) {
|
|
161
|
+
networkRequests.shift(); // Remove oldest to maintain limit
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
networkRequests.push({
|
|
165
|
+
requestId: params.requestId,
|
|
166
|
+
url: params.request.url,
|
|
167
|
+
method: params.request.method,
|
|
168
|
+
headers: params.request.headers,
|
|
169
|
+
timestamp: params.timestamp,
|
|
170
|
+
initiator: params.initiator.type,
|
|
171
|
+
type: params.type
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
cdp.on('Network.responseReceived', (params) => {
|
|
176
|
+
const req = networkRequests.find(r => r.requestId === params.requestId);
|
|
177
|
+
if (req) {
|
|
178
|
+
req.status = params.response.status;
|
|
179
|
+
req.statusText = params.response.statusText;
|
|
180
|
+
req.mimeType = params.response.mimeType;
|
|
181
|
+
req.responseHeaders = params.response.headers;
|
|
182
|
+
req.timing = params.response.timing;
|
|
183
|
+
req.fromCache = params.response.fromDiskCache || params.response.fromServiceWorker;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
cdp.on('Network.loadingFinished', (params) => {
|
|
188
|
+
const req = networkRequests.find(r => r.requestId === params.requestId);
|
|
189
|
+
if (req) {
|
|
190
|
+
req.encodedDataLength = params.encodedDataLength;
|
|
191
|
+
req.finished = true;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
cdp.on('Network.loadingFailed', (params) => {
|
|
196
|
+
const req = networkRequests.find(r => r.requestId === params.requestId);
|
|
197
|
+
if (req) {
|
|
198
|
+
req.failed = true;
|
|
199
|
+
req.errorText = params.errorText;
|
|
200
|
+
req.canceled = params.canceled;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// WebSocket frame tracking
|
|
205
|
+
cdp.on('Network.webSocketFrameSent', (params) => {
|
|
206
|
+
if (!webSocketFrames.has(params.requestId)) {
|
|
207
|
+
webSocketFrames.set(params.requestId, []);
|
|
208
|
+
}
|
|
209
|
+
webSocketFrames.get(params.requestId).push({
|
|
210
|
+
direction: 'sent',
|
|
211
|
+
opcode: params.response.opcode,
|
|
212
|
+
mask: params.response.mask,
|
|
213
|
+
payloadData: params.response.payloadData,
|
|
214
|
+
timestamp: params.timestamp
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
cdp.on('Network.webSocketFrameReceived', (params) => {
|
|
219
|
+
if (!webSocketFrames.has(params.requestId)) {
|
|
220
|
+
webSocketFrames.set(params.requestId, []);
|
|
221
|
+
}
|
|
222
|
+
webSocketFrames.get(params.requestId).push({
|
|
223
|
+
direction: 'received',
|
|
224
|
+
opcode: params.response.opcode,
|
|
225
|
+
mask: params.response.mask,
|
|
226
|
+
payloadData: params.response.payloadData,
|
|
227
|
+
timestamp: params.timestamp
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
monitoringActive = true;
|
|
232
|
+
|
|
233
|
+
debugLog(`Started network monitoring${patterns.length > 0 ? ' with filters' : ''}`);
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
content: [{
|
|
237
|
+
type: 'text',
|
|
238
|
+
text: `✅ Network monitoring started\n\n${patterns.length > 0 ? `Patterns: ${patterns.join(', ')}\n` : ''}Capturing all network requests...\n\nUse browser_net_get_requests to view captured requests.\nLimit: ${MAX_REQUESTS} requests max`
|
|
239
|
+
}]
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
debugLog(`CDP error in browser_net_start_monitoring: ${error.message}`);
|
|
243
|
+
return {
|
|
244
|
+
content: [{
|
|
245
|
+
type: 'text',
|
|
246
|
+
text: `❌ CDP Error: ${error.message}\n\nPossible causes:\n- Browser doesn't support network monitoring\n- CDP session disconnected`
|
|
247
|
+
}],
|
|
248
|
+
isError: true
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
browser_net_get_requests: async (args) => {
|
|
254
|
+
try {
|
|
255
|
+
if (!monitoringActive) {
|
|
256
|
+
return {
|
|
257
|
+
content: [{
|
|
258
|
+
type: 'text',
|
|
259
|
+
text: '⚠️ Network monitoring is not active.\n\nUse browser_net_start_monitoring to start capturing requests first.'
|
|
260
|
+
}]
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const filter = args.filter || '';
|
|
265
|
+
const filtered = filter
|
|
266
|
+
? networkRequests.filter(r => r.url.includes(filter))
|
|
267
|
+
: networkRequests;
|
|
268
|
+
|
|
269
|
+
if (filtered.length === 0) {
|
|
270
|
+
return {
|
|
271
|
+
content: [{
|
|
272
|
+
type: 'text',
|
|
273
|
+
text: `No network requests captured yet.\n\n${filter ? `Filter: "${filter}"\n` : ''}Monitoring is active - requests will appear as they occur.`
|
|
274
|
+
}]
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Summarize requests
|
|
279
|
+
const summary = {
|
|
280
|
+
totalCaptured: networkRequests.length,
|
|
281
|
+
filtered: filtered.length,
|
|
282
|
+
requests: filtered.slice(0, 50).map(r => ({
|
|
283
|
+
method: r.method,
|
|
284
|
+
url: r.url.length > 100 ? r.url.substring(0, 97) + '...' : r.url,
|
|
285
|
+
status: r.status || 'pending',
|
|
286
|
+
type: r.type,
|
|
287
|
+
size: r.encodedDataLength ? `${(r.encodedDataLength / 1024).toFixed(2)}KB` : 'unknown',
|
|
288
|
+
timing: r.timing ? `${(r.timing.receiveHeadersEnd - r.timing.sendStart).toFixed(2)}ms` : 'N/A',
|
|
289
|
+
failed: r.failed || false,
|
|
290
|
+
fromCache: r.fromCache || false
|
|
291
|
+
}))
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: 'text',
|
|
297
|
+
text: `📊 Network Requests (showing ${Math.min(50, filtered.length)} of ${filtered.length}):\n\n${JSON.stringify(summary, null, 2)}\n\nNote: Limited to 50 requests for readability. Use filter parameter to narrow results.`
|
|
298
|
+
}]
|
|
299
|
+
};
|
|
300
|
+
} catch (error) {
|
|
301
|
+
debugLog(`Error in browser_net_get_requests: ${error.message}`);
|
|
302
|
+
return {
|
|
303
|
+
content: [{
|
|
304
|
+
type: 'text',
|
|
305
|
+
text: `❌ Error: ${error.message}`
|
|
306
|
+
}],
|
|
307
|
+
isError: true
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
|
|
312
|
+
browser_net_stop_monitoring: async (args) => {
|
|
313
|
+
try {
|
|
314
|
+
if (!monitoringActive) {
|
|
315
|
+
return {
|
|
316
|
+
content: [{
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: '⚠️ Network monitoring is not active.'
|
|
319
|
+
}]
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const cdp = await getCDPSession();
|
|
324
|
+
await cdp.send('Network.disable');
|
|
325
|
+
|
|
326
|
+
// Remove all listeners
|
|
327
|
+
cdp.removeAllListeners('Network.requestWillBeSent');
|
|
328
|
+
cdp.removeAllListeners('Network.responseReceived');
|
|
329
|
+
cdp.removeAllListeners('Network.loadingFinished');
|
|
330
|
+
cdp.removeAllListeners('Network.loadingFailed');
|
|
331
|
+
cdp.removeAllListeners('Network.webSocketFrameSent');
|
|
332
|
+
cdp.removeAllListeners('Network.webSocketFrameReceived');
|
|
333
|
+
|
|
334
|
+
const count = networkRequests.length;
|
|
335
|
+
const wsCount = webSocketFrames.size;
|
|
336
|
+
|
|
337
|
+
networkRequests = [];
|
|
338
|
+
webSocketFrames.clear();
|
|
339
|
+
monitoringActive = false;
|
|
340
|
+
|
|
341
|
+
debugLog('Stopped network monitoring');
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
content: [{
|
|
345
|
+
type: 'text',
|
|
346
|
+
text: `✅ Network monitoring stopped\n\nCaptured ${count} requests and ${wsCount} WebSocket connections.\nData has been cleared.`
|
|
347
|
+
}]
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
monitoringActive = false;
|
|
351
|
+
debugLog(`CDP error in browser_net_stop_monitoring: ${error.message}`);
|
|
352
|
+
return {
|
|
353
|
+
content: [{
|
|
354
|
+
type: 'text',
|
|
355
|
+
text: `❌ CDP Error: ${error.message}\n\nMonitoring has been stopped.`
|
|
356
|
+
}],
|
|
357
|
+
isError: true
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
browser_net_export_har: async (args) => {
|
|
363
|
+
try {
|
|
364
|
+
if (!monitoringActive || networkRequests.length === 0) {
|
|
365
|
+
return {
|
|
366
|
+
content: [{
|
|
367
|
+
type: 'text',
|
|
368
|
+
text: '⚠️ No network data to export.\n\nStart monitoring with browser_net_start_monitoring and navigate to capture requests first.'
|
|
369
|
+
}]
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const includeContent = args.includeContent || false;
|
|
374
|
+
|
|
375
|
+
// Build HAR format
|
|
376
|
+
const har = {
|
|
377
|
+
log: {
|
|
378
|
+
version: '1.2',
|
|
379
|
+
creator: {
|
|
380
|
+
name: 'Browser MCP Server',
|
|
381
|
+
version: version
|
|
382
|
+
},
|
|
383
|
+
pages: [],
|
|
384
|
+
entries: networkRequests.map(r => ({
|
|
385
|
+
startedDateTime: new Date(r.timestamp * 1000).toISOString(),
|
|
386
|
+
time: r.timing ? (r.timing.receiveHeadersEnd - r.timing.sendStart) : 0,
|
|
387
|
+
request: {
|
|
388
|
+
method: r.method,
|
|
389
|
+
url: r.url,
|
|
390
|
+
httpVersion: 'HTTP/1.1',
|
|
391
|
+
headers: Object.entries(r.headers || {}).map(([name, value]) => ({ name, value })),
|
|
392
|
+
queryString: [],
|
|
393
|
+
headersSize: -1,
|
|
394
|
+
bodySize: -1
|
|
395
|
+
},
|
|
396
|
+
response: {
|
|
397
|
+
status: r.status || 0,
|
|
398
|
+
statusText: r.statusText || '',
|
|
399
|
+
httpVersion: 'HTTP/1.1',
|
|
400
|
+
headers: Object.entries(r.responseHeaders || {}).map(([name, value]) => ({ name, value })),
|
|
401
|
+
content: {
|
|
402
|
+
size: r.encodedDataLength || 0,
|
|
403
|
+
mimeType: r.mimeType || 'application/octet-stream'
|
|
404
|
+
},
|
|
405
|
+
redirectURL: '',
|
|
406
|
+
headersSize: -1,
|
|
407
|
+
bodySize: r.encodedDataLength || 0
|
|
408
|
+
},
|
|
409
|
+
cache: {
|
|
410
|
+
beforeRequest: null,
|
|
411
|
+
afterRequest: r.fromCache ? {} : null
|
|
412
|
+
},
|
|
413
|
+
timings: r.timing ? {
|
|
414
|
+
send: r.timing.sendEnd - r.timing.sendStart,
|
|
415
|
+
wait: r.timing.receiveHeadersEnd - r.timing.sendEnd,
|
|
416
|
+
receive: 0
|
|
417
|
+
} : {
|
|
418
|
+
send: 0,
|
|
419
|
+
wait: 0,
|
|
420
|
+
receive: 0
|
|
421
|
+
}
|
|
422
|
+
}))
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
debugLog(`Exported HAR with ${har.log.entries.length} entries`);
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
content: [{
|
|
430
|
+
type: 'text',
|
|
431
|
+
text: `✅ HAR Export:\n\n${JSON.stringify(har, null, 2).substring(0, 10000)}...\n\nNote: Truncated for display. Full HAR contains ${har.log.entries.length} entries.`
|
|
432
|
+
}]
|
|
433
|
+
};
|
|
434
|
+
} catch (error) {
|
|
435
|
+
debugLog(`Error in browser_net_export_har: ${error.message}`);
|
|
436
|
+
return {
|
|
437
|
+
content: [{
|
|
438
|
+
type: 'text',
|
|
439
|
+
text: `❌ Error: ${error.message}`
|
|
440
|
+
}],
|
|
441
|
+
isError: true
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
browser_net_get_websocket_frames: async (args) => {
|
|
447
|
+
try {
|
|
448
|
+
const requestId = args.requestId;
|
|
449
|
+
|
|
450
|
+
if (!webSocketFrames.has(requestId)) {
|
|
451
|
+
return {
|
|
452
|
+
content: [{
|
|
453
|
+
type: 'text',
|
|
454
|
+
text: `⚠️ No WebSocket frames found for request ID: ${requestId}\n\nMake sure:\n1. Network monitoring is active\n2. The request ID is correct\n3. WebSocket connection has exchanged frames`
|
|
455
|
+
}]
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const frames = webSocketFrames.get(requestId);
|
|
460
|
+
const summary = frames.slice(0, 20).map(f => ({
|
|
461
|
+
direction: f.direction,
|
|
462
|
+
opcode: f.opcode,
|
|
463
|
+
payloadLength: f.payloadData ? f.payloadData.length : 0,
|
|
464
|
+
payload: f.payloadData ? f.payloadData.substring(0, 100) : '',
|
|
465
|
+
timestamp: new Date(f.timestamp * 1000).toISOString()
|
|
466
|
+
}));
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
content: [{
|
|
470
|
+
type: 'text',
|
|
471
|
+
text: `📊 WebSocket Frames (showing ${Math.min(20, frames.length)} of ${frames.length}):\n\n${JSON.stringify(summary, null, 2)}`
|
|
472
|
+
}]
|
|
473
|
+
};
|
|
474
|
+
} catch (error) {
|
|
475
|
+
debugLog(`Error in browser_net_get_websocket_frames: ${error.message}`);
|
|
476
|
+
return {
|
|
477
|
+
content: [{
|
|
478
|
+
type: 'text',
|
|
479
|
+
text: `❌ Error: ${error.message}`
|
|
480
|
+
}],
|
|
481
|
+
isError: true
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
browser_net_set_request_blocking: async (args) => {
|
|
487
|
+
try {
|
|
488
|
+
const cdp = await getCDPSession();
|
|
489
|
+
const patterns = args.patterns || [];
|
|
490
|
+
|
|
491
|
+
await cdp.send('Network.setBlockedURLs', { urls: patterns });
|
|
492
|
+
|
|
493
|
+
debugLog(`Set request blocking for ${patterns.length} patterns`);
|
|
494
|
+
|
|
495
|
+
return {
|
|
496
|
+
content: [{
|
|
497
|
+
type: 'text',
|
|
498
|
+
text: `✅ Request blocking enabled\n\nBlocked patterns:\n${patterns.map(p => ` • ${p}`).join('\n')}\n\nRequests matching these patterns will be blocked.`
|
|
499
|
+
}]
|
|
500
|
+
};
|
|
501
|
+
} catch (error) {
|
|
502
|
+
debugLog(`CDP error in browser_net_set_request_blocking: ${error.message}`);
|
|
503
|
+
return {
|
|
504
|
+
content: [{
|
|
505
|
+
type: 'text',
|
|
506
|
+
text: `❌ CDP Error: ${error.message}`
|
|
507
|
+
}],
|
|
508
|
+
isError: true
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
browser_net_emulate_conditions: async (args) => {
|
|
514
|
+
try {
|
|
515
|
+
const cdp = await getCDPSession();
|
|
516
|
+
|
|
517
|
+
await cdp.send('Network.emulateNetworkConditions', {
|
|
518
|
+
offline: args.offline,
|
|
519
|
+
latency: args.latency,
|
|
520
|
+
downloadThroughput: args.downloadThroughput,
|
|
521
|
+
uploadThroughput: args.uploadThroughput
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
debugLog(`Network conditions set: offline=${args.offline}, latency=${args.latency}ms`);
|
|
525
|
+
|
|
526
|
+
const conditions = {
|
|
527
|
+
offline: args.offline,
|
|
528
|
+
latency: `${args.latency}ms`,
|
|
529
|
+
download: args.downloadThroughput === -1 ? 'unlimited' : `${(args.downloadThroughput / 1024).toFixed(2)} KB/s`,
|
|
530
|
+
upload: args.uploadThroughput === -1 ? 'unlimited' : `${(args.uploadThroughput / 1024).toFixed(2)} KB/s`
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
content: [{
|
|
535
|
+
type: 'text',
|
|
536
|
+
text: `✅ Network conditions applied:\n\n${JSON.stringify(conditions, null, 2)}`
|
|
537
|
+
}]
|
|
538
|
+
};
|
|
539
|
+
} catch (error) {
|
|
540
|
+
debugLog(`CDP error in browser_net_emulate_conditions: ${error.message}`);
|
|
541
|
+
return {
|
|
542
|
+
content: [{
|
|
543
|
+
type: 'text',
|
|
544
|
+
text: `❌ CDP Error: ${error.message}`
|
|
545
|
+
}],
|
|
546
|
+
isError: true
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
module.exports = { definitions, handlers };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const { connectToBrowser, setActivePageIndex, getBrowserState } = require('../browser');
|
|
2
|
+
|
|
3
|
+
const definitions = [
|
|
4
|
+
{
|
|
5
|
+
name: 'browser_list_pages',
|
|
6
|
+
description: 'List all open browser pages (tabs) (see browser_docs)',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {},
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'browser_new_page',
|
|
16
|
+
description: 'Open a new browser page (tab) (see browser_docs)',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
url: { type: 'string', description: 'Optional URL to navigate to' }
|
|
21
|
+
},
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'browser_switch_page',
|
|
28
|
+
description: 'Switch to a different browser page (tab) (see browser_docs)',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
index: { type: 'number', description: 'The index of the page to switch to' }
|
|
33
|
+
},
|
|
34
|
+
required: ['index'],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'browser_close_page',
|
|
41
|
+
description: 'Close a browser page (tab) (see browser_docs)',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
index: { type: 'number', description: 'The index of the page to close. If not provided, closes current page.' }
|
|
46
|
+
},
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const handlers = {
|
|
54
|
+
browser_list_pages: async (args) => {
|
|
55
|
+
const { context, activePageIndex } = getBrowserState();
|
|
56
|
+
// Ensure we have a context, connect if needed
|
|
57
|
+
if (!context) await connectToBrowser();
|
|
58
|
+
|
|
59
|
+
// We need fresh state after connect
|
|
60
|
+
const state = getBrowserState();
|
|
61
|
+
const pages = state.context.pages();
|
|
62
|
+
|
|
63
|
+
const pageList = pages.map((p, i) => ({
|
|
64
|
+
index: i,
|
|
65
|
+
title: 'Unknown', // Will update below
|
|
66
|
+
url: p.url(),
|
|
67
|
+
isActive: i === state.activePageIndex
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Try to get titles (async)
|
|
71
|
+
await Promise.all(pages.map(async (p, i) => {
|
|
72
|
+
try {
|
|
73
|
+
pageList[i].title = await p.title();
|
|
74
|
+
} catch (e) { }
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
content: [{
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: JSON.stringify(pageList, null, 2)
|
|
81
|
+
}]
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
browser_new_page: async (args) => {
|
|
86
|
+
const { context } = await connectToBrowser();
|
|
87
|
+
const newPage = await context.newPage();
|
|
88
|
+
const pages = context.pages();
|
|
89
|
+
const newIndex = pages.length - 1;
|
|
90
|
+
setActivePageIndex(newIndex);
|
|
91
|
+
|
|
92
|
+
if (args.url) {
|
|
93
|
+
await newPage.goto(args.url, { waitUntil: 'domcontentloaded' });
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
content: [{
|
|
97
|
+
type: 'text',
|
|
98
|
+
text: `Opened new page at index ${newIndex}${args.url ? ` and navigated to ${args.url}` : ''}`
|
|
99
|
+
}]
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
browser_switch_page: async (args) => {
|
|
104
|
+
const { context } = await connectToBrowser();
|
|
105
|
+
const allPages = context.pages();
|
|
106
|
+
if (args.index < 0 || args.index >= allPages.length) {
|
|
107
|
+
throw new Error(`Invalid page index: ${args.index}. Total pages: ${allPages.length}`);
|
|
108
|
+
}
|
|
109
|
+
setActivePageIndex(args.index);
|
|
110
|
+
|
|
111
|
+
// Just for visual effect in non-headless mode, brought to front
|
|
112
|
+
try {
|
|
113
|
+
await allPages[args.index].bringToFront();
|
|
114
|
+
} catch (e) { }
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
content: [{
|
|
118
|
+
type: 'text',
|
|
119
|
+
text: `Switched to page index ${args.index}`
|
|
120
|
+
}]
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
browser_close_page: async (args) => {
|
|
125
|
+
const { context, activePageIndex } = await connectToBrowser(); // connects and gets state
|
|
126
|
+
const targetPages = context.pages();
|
|
127
|
+
const closeIdx = args.index !== undefined ? args.index : activePageIndex;
|
|
128
|
+
|
|
129
|
+
if (closeIdx < 0 || closeIdx >= targetPages.length) {
|
|
130
|
+
throw new Error(`Invalid page index: ${closeIdx}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await targetPages[closeIdx].close();
|
|
134
|
+
|
|
135
|
+
// Adjust active index if needed
|
|
136
|
+
if (activePageIndex >= context.pages().length) {
|
|
137
|
+
setActivePageIndex(Math.max(0, context.pages().length - 1));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
content: [{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: `Closed page ${closeIdx}. Active page is now ${getBrowserState().activePageIndex}.`
|
|
144
|
+
}]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
module.exports = { definitions, handlers };
|