@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.
Files changed (43) hide show
  1. package/CHANGELOG-v1.3.0.md +42 -0
  2. package/CHANGELOG-v1.4.0.md +8 -0
  3. package/README.md +271 -45
  4. package/package.json +11 -10
  5. package/plugins/.gitkeep +0 -0
  6. package/src/.gitkeep +0 -0
  7. package/src/browser.js +152 -0
  8. package/src/cdp.js +58 -0
  9. package/src/index.js +126 -0
  10. package/src/tools/.gitkeep +0 -0
  11. package/src/tools/console.js +139 -0
  12. package/src/tools/docs.js +1611 -0
  13. package/src/tools/index.js +60 -0
  14. package/src/tools/info.js +139 -0
  15. package/src/tools/interaction.js +126 -0
  16. package/src/tools/keyboard.js +27 -0
  17. package/src/tools/media.js +264 -0
  18. package/src/tools/mouse.js +104 -0
  19. package/src/tools/navigation.js +72 -0
  20. package/src/tools/network.js +552 -0
  21. package/src/tools/pages.js +149 -0
  22. package/src/tools/performance.js +517 -0
  23. package/src/tools/security.js +470 -0
  24. package/src/tools/storage.js +467 -0
  25. package/src/tools/system.js +196 -0
  26. package/src/utils.js +131 -0
  27. package/tests/.gitkeep +0 -0
  28. package/tests/fixtures/.gitkeep +0 -0
  29. package/tests/fixtures/test-media.html +35 -0
  30. package/tests/fixtures/test-network.html +48 -0
  31. package/tests/fixtures/test-performance.html +61 -0
  32. package/tests/fixtures/test-security.html +33 -0
  33. package/tests/fixtures/test-storage.html +76 -0
  34. package/tests/run-all.js +50 -0
  35. package/{test-browser-automation.js → tests/test-browser-automation.js} +44 -5
  36. package/{test-mcp.js → tests/test-mcp.js} +9 -4
  37. package/tests/test-media-tools.js +168 -0
  38. package/tests/test-network.js +212 -0
  39. package/tests/test-performance.js +254 -0
  40. package/tests/test-security.js +203 -0
  41. package/tests/test-storage.js +192 -0
  42. package/CHANGELOG-v1.0.2.md +0 -126
  43. 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 };