@ricardodeazambuja/browser-mcp-server 1.3.0 → 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.
@@ -0,0 +1,467 @@
1
+ /**
2
+ * Storage & Service Workers Tools (CDP-based)
3
+ * IndexedDB, Cache Storage, Service Worker inspection and control
4
+ */
5
+
6
+ const { connectToBrowser } = require('../browser');
7
+ const { getCDPSession } = require('../cdp');
8
+ const { debugLog } = require('../utils');
9
+
10
+ const definitions = [
11
+ {
12
+ name: 'browser_storage_get_indexeddb',
13
+ description: 'Inspect IndexedDB databases and their data (see browser_docs)',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ databaseName: {
18
+ type: 'string',
19
+ description: 'Specific database to inspect (optional)'
20
+ },
21
+ objectStoreName: {
22
+ type: 'string',
23
+ description: 'Specific object store to query (optional, requires databaseName)'
24
+ }
25
+ },
26
+ additionalProperties: false,
27
+ $schema: 'http://json-schema.org/draft-07/schema#'
28
+ }
29
+ },
30
+ {
31
+ name: 'browser_storage_get_cache_storage',
32
+ description: 'List Cache Storage API caches and their entries (see browser_docs)',
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ cacheName: {
37
+ type: 'string',
38
+ description: 'Specific cache to inspect (optional)'
39
+ }
40
+ },
41
+ additionalProperties: false,
42
+ $schema: 'http://json-schema.org/draft-07/schema#'
43
+ }
44
+ },
45
+ {
46
+ name: 'browser_storage_delete_cache',
47
+ description: 'Delete a specific cache from Cache Storage (see browser_docs)',
48
+ inputSchema: {
49
+ type: 'object',
50
+ properties: {
51
+ cacheName: {
52
+ type: 'string',
53
+ description: 'Cache name to delete'
54
+ }
55
+ },
56
+ required: ['cacheName'],
57
+ additionalProperties: false,
58
+ $schema: 'http://json-schema.org/draft-07/schema#'
59
+ }
60
+ },
61
+ {
62
+ name: 'browser_storage_get_service_workers',
63
+ description: 'Get service worker registrations and their state (see browser_docs)',
64
+ inputSchema: {
65
+ type: 'object',
66
+ properties: {},
67
+ additionalProperties: false,
68
+ $schema: 'http://json-schema.org/draft-07/schema#'
69
+ }
70
+ },
71
+ {
72
+ name: 'browser_storage_unregister_service_worker',
73
+ description: 'Unregister a service worker (see browser_docs)',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ scopeURL: {
78
+ type: 'string',
79
+ description: 'Scope URL of service worker to unregister'
80
+ }
81
+ },
82
+ required: ['scopeURL'],
83
+ additionalProperties: false,
84
+ $schema: 'http://json-schema.org/draft-07/schema#'
85
+ }
86
+ }
87
+ ];
88
+
89
+ const handlers = {
90
+ browser_storage_get_indexeddb: async (args) => {
91
+ try {
92
+ const { page } = await connectToBrowser();
93
+ const cdp = await getCDPSession();
94
+
95
+ // Get security origin for current page
96
+ const origin = await page.evaluate(() => window.location.origin);
97
+
98
+ // Request database names
99
+ const { databaseNames } = await cdp.send('IndexedDB.requestDatabaseNames', {
100
+ securityOrigin: origin
101
+ });
102
+
103
+ if (databaseNames.length === 0) {
104
+ return {
105
+ content: [{
106
+ type: 'text',
107
+ text: 'No IndexedDB databases found for this origin.\n\nThe page may not be using IndexedDB.'
108
+ }]
109
+ };
110
+ }
111
+
112
+ // If no specific database requested, list all databases
113
+ if (!args.databaseName) {
114
+ return {
115
+ content: [{
116
+ type: 'text',
117
+ text: `📊 IndexedDB Databases:\n\n${JSON.stringify({ origin, databases: databaseNames }, null, 2)}\n\nUse databaseName parameter to inspect a specific database.`
118
+ }]
119
+ };
120
+ }
121
+
122
+ // Get database structure
123
+ const dbInfo = await cdp.send('IndexedDB.requestDatabase', {
124
+ securityOrigin: origin,
125
+ databaseName: args.databaseName
126
+ });
127
+
128
+ if (!args.objectStoreName) {
129
+ // Return database structure
130
+ const structure = {
131
+ name: dbInfo.databaseWithObjectStores.name,
132
+ version: dbInfo.databaseWithObjectStores.version,
133
+ objectStores: dbInfo.databaseWithObjectStores.objectStores.map(store => ({
134
+ name: store.name,
135
+ keyPath: store.keyPath,
136
+ autoIncrement: store.autoIncrement,
137
+ indexes: store.indexes.map(idx => ({
138
+ name: idx.name,
139
+ keyPath: idx.keyPath,
140
+ unique: idx.unique,
141
+ multiEntry: idx.multiEntry
142
+ }))
143
+ }))
144
+ };
145
+
146
+ return {
147
+ content: [{
148
+ type: 'text',
149
+ text: `📊 IndexedDB Database Structure:\n\n${JSON.stringify(structure, null, 2)}\n\nUse objectStoreName parameter to query data from a specific object store.`
150
+ }]
151
+ };
152
+ }
153
+
154
+ // Get object store data
155
+ const { objectStoreDataEntries, hasMore } = await cdp.send('IndexedDB.requestData', {
156
+ securityOrigin: origin,
157
+ databaseName: args.databaseName,
158
+ objectStoreName: args.objectStoreName,
159
+ indexName: '',
160
+ skipCount: 0,
161
+ pageSize: 100
162
+ });
163
+
164
+ const data = {
165
+ objectStore: args.objectStoreName,
166
+ entries: objectStoreDataEntries.length,
167
+ hasMore: hasMore,
168
+ data: objectStoreDataEntries.map(entry => ({
169
+ key: entry.key,
170
+ primaryKey: entry.primaryKey,
171
+ value: entry.value
172
+ }))
173
+ };
174
+
175
+ return {
176
+ content: [{
177
+ type: 'text',
178
+ text: `📊 IndexedDB Data:\n\n${JSON.stringify(data, null, 2)}\n\nNote: Limited to 100 entries.${hasMore ? ' More entries available.' : ''}`
179
+ }]
180
+ };
181
+ } catch (error) {
182
+ debugLog(`CDP error in browser_storage_get_indexeddb: ${error.message}`);
183
+ return {
184
+ content: [{
185
+ type: 'text',
186
+ text: `❌ CDP Error: ${error.message}\n\nPossible causes:\n- IndexedDB not accessible\n- Invalid database or object store name\n- CDP session disconnected`
187
+ }],
188
+ isError: true
189
+ };
190
+ }
191
+ },
192
+
193
+ browser_storage_get_cache_storage: async (args) => {
194
+ try {
195
+ const { page } = await connectToBrowser();
196
+ const cdp = await getCDPSession();
197
+
198
+ // Get security origin
199
+ const origin = await page.evaluate(() => window.location.origin);
200
+
201
+ // Request cache names
202
+ const { caches } = await cdp.send('CacheStorage.requestCacheNames', {
203
+ securityOrigin: origin
204
+ });
205
+
206
+ if (caches.length === 0) {
207
+ return {
208
+ content: [{
209
+ type: 'text',
210
+ text: 'No Cache Storage caches found for this origin.\n\nThe page may not be using Cache Storage API.'
211
+ }]
212
+ };
213
+ }
214
+
215
+ // If no specific cache requested, list all caches
216
+ if (!args.cacheName) {
217
+ return {
218
+ content: [{
219
+ type: 'text',
220
+ text: `📊 Cache Storage Caches:\n\n${JSON.stringify({ origin, caches: caches.map(c => c.cacheName) }, null, 2)}\n\nUse cacheName parameter to inspect entries in a specific cache.`
221
+ }]
222
+ };
223
+ }
224
+
225
+ // Find the cache ID
226
+ const cache = caches.find(c => c.cacheName === args.cacheName);
227
+ if (!cache) {
228
+ return {
229
+ content: [{
230
+ type: 'text',
231
+ text: `⚠️ Cache "${args.cacheName}" not found.\n\nAvailable caches:\n${caches.map(c => ` • ${c.cacheName}`).join('\n')}`
232
+ }]
233
+ };
234
+ }
235
+
236
+ // Get cache entries
237
+ const { cacheDataEntries, returnCount } = await cdp.send('CacheStorage.requestEntries', {
238
+ cacheId: cache.cacheId,
239
+ skipCount: 0,
240
+ pageSize: 50
241
+ });
242
+
243
+ const entries = {
244
+ cacheName: args.cacheName,
245
+ entryCount: returnCount,
246
+ entries: cacheDataEntries.map(entry => ({
247
+ requestURL: entry.requestURL,
248
+ requestMethod: entry.requestMethod,
249
+ responseStatus: entry.responseStatus,
250
+ responseStatusText: entry.responseStatusText,
251
+ responseType: entry.responseType
252
+ }))
253
+ };
254
+
255
+ return {
256
+ content: [{
257
+ type: 'text',
258
+ text: `📊 Cache Storage Entries:\n\n${JSON.stringify(entries, null, 2)}\n\nNote: Limited to 50 entries.`
259
+ }]
260
+ };
261
+ } catch (error) {
262
+ debugLog(`CDP error in browser_storage_get_cache_storage: ${error.message}`);
263
+ return {
264
+ content: [{
265
+ type: 'text',
266
+ text: `❌ CDP Error: ${error.message}\n\nPossible causes:\n- Cache Storage not accessible\n- Invalid cache name\n- CDP session disconnected`
267
+ }],
268
+ isError: true
269
+ };
270
+ }
271
+ },
272
+
273
+ browser_storage_delete_cache: async (args) => {
274
+ try {
275
+ const { page } = await connectToBrowser();
276
+ const cdp = await getCDPSession();
277
+
278
+ // Get security origin
279
+ const origin = await page.evaluate(() => window.location.origin);
280
+
281
+ // Request cache names to find the cache ID
282
+ const { caches } = await cdp.send('CacheStorage.requestCacheNames', {
283
+ securityOrigin: origin
284
+ });
285
+
286
+ const cache = caches.find(c => c.cacheName === args.cacheName);
287
+ if (!cache) {
288
+ return {
289
+ content: [{
290
+ type: 'text',
291
+ text: `⚠️ Cache "${args.cacheName}" not found.\n\nAvailable caches:\n${caches.map(c => ` • ${c.cacheName}`).join('\n')}`
292
+ }]
293
+ };
294
+ }
295
+
296
+ // Delete the cache
297
+ await cdp.send('CacheStorage.deleteCache', {
298
+ cacheId: cache.cacheId
299
+ });
300
+
301
+ debugLog(`Deleted cache: ${args.cacheName}`);
302
+
303
+ return {
304
+ content: [{
305
+ type: 'text',
306
+ text: `✅ Cache deleted successfully: ${args.cacheName}`
307
+ }]
308
+ };
309
+ } catch (error) {
310
+ debugLog(`CDP error in browser_storage_delete_cache: ${error.message}`);
311
+ return {
312
+ content: [{
313
+ type: 'text',
314
+ text: `❌ CDP Error: ${error.message}\n\nFailed to delete cache.`
315
+ }],
316
+ isError: true
317
+ };
318
+ }
319
+ },
320
+
321
+ browser_storage_get_service_workers: async (args) => {
322
+ try {
323
+ const { page } = await connectToBrowser();
324
+ const cdp = await getCDPSession();
325
+
326
+ // Enable ServiceWorker domain
327
+ await cdp.send('ServiceWorker.enable');
328
+
329
+ // Get service worker registrations
330
+ const { registrations } = await cdp.send('ServiceWorker.getServiceWorker', {});
331
+
332
+ if (registrations.length === 0) {
333
+ await cdp.send('ServiceWorker.disable');
334
+ return {
335
+ content: [{
336
+ type: 'text',
337
+ text: 'No service workers found.\n\nThe page may not have registered any service workers.'
338
+ }]
339
+ };
340
+ }
341
+
342
+ const workers = registrations.map(reg => ({
343
+ registrationId: reg.registrationId,
344
+ scopeURL: reg.scopeURL,
345
+ isDeleted: reg.isDeleted
346
+ }));
347
+
348
+ await cdp.send('ServiceWorker.disable');
349
+
350
+ return {
351
+ content: [{
352
+ type: 'text',
353
+ text: `📊 Service Workers:\n\n${JSON.stringify(workers, null, 2)}`
354
+ }]
355
+ };
356
+ } catch (error) {
357
+ debugLog(`CDP error in browser_storage_get_service_workers: ${error.message}`);
358
+
359
+ // Try alternative approach using page.evaluate
360
+ try {
361
+ const { page } = await connectToBrowser();
362
+ const swInfo = await page.evaluate(async () => {
363
+ if (!('serviceWorker' in navigator)) {
364
+ return { supported: false };
365
+ }
366
+
367
+ const registrations = await navigator.serviceWorker.getRegistrations();
368
+ return {
369
+ supported: true,
370
+ registrations: registrations.map(reg => ({
371
+ scope: reg.scope,
372
+ active: reg.active ? {
373
+ scriptURL: reg.active.scriptURL,
374
+ state: reg.active.state
375
+ } : null,
376
+ installing: reg.installing ? {
377
+ scriptURL: reg.installing.scriptURL,
378
+ state: reg.installing.state
379
+ } : null,
380
+ waiting: reg.waiting ? {
381
+ scriptURL: reg.waiting.scriptURL,
382
+ state: reg.waiting.state
383
+ } : null
384
+ }))
385
+ };
386
+ });
387
+
388
+ if (!swInfo.supported) {
389
+ return {
390
+ content: [{
391
+ type: 'text',
392
+ text: '⚠️ Service Workers not supported in this browser/context.'
393
+ }]
394
+ };
395
+ }
396
+
397
+ return {
398
+ content: [{
399
+ type: 'text',
400
+ text: `📊 Service Workers:\n\n${JSON.stringify(swInfo.registrations, null, 2)}`
401
+ }]
402
+ };
403
+ } catch (fallbackError) {
404
+ debugLog(`Fallback error in browser_storage_get_service_workers: ${fallbackError.message}`);
405
+ return {
406
+ content: [{
407
+ type: 'text',
408
+ text: `❌ Error: ${error.message}\n\nCould not retrieve service worker information.`
409
+ }],
410
+ isError: true
411
+ };
412
+ }
413
+ }
414
+ },
415
+
416
+ browser_storage_unregister_service_worker: async (args) => {
417
+ try {
418
+ const { page } = await connectToBrowser();
419
+
420
+ // Use page.evaluate to unregister via JavaScript API
421
+ const result = await page.evaluate(async (scopeURL) => {
422
+ if (!('serviceWorker' in navigator)) {
423
+ return { success: false, error: 'Service Workers not supported' };
424
+ }
425
+
426
+ const registrations = await navigator.serviceWorker.getRegistrations();
427
+ const registration = registrations.find(reg => reg.scope === scopeURL);
428
+
429
+ if (!registration) {
430
+ return { success: false, error: 'Service worker not found for scope: ' + scopeURL };
431
+ }
432
+
433
+ const unregistered = await registration.unregister();
434
+ return { success: unregistered };
435
+ }, args.scopeURL);
436
+
437
+ if (!result.success) {
438
+ return {
439
+ content: [{
440
+ type: 'text',
441
+ text: `⚠️ ${result.error}`
442
+ }]
443
+ };
444
+ }
445
+
446
+ debugLog(`Unregistered service worker: ${args.scopeURL}`);
447
+
448
+ return {
449
+ content: [{
450
+ type: 'text',
451
+ text: `✅ Service worker unregistered successfully\n\nScope: ${args.scopeURL}`
452
+ }]
453
+ };
454
+ } catch (error) {
455
+ debugLog(`Error in browser_storage_unregister_service_worker: ${error.message}`);
456
+ return {
457
+ content: [{
458
+ type: 'text',
459
+ text: `❌ Error: ${error.message}\n\nFailed to unregister service worker.`
460
+ }],
461
+ isError: true
462
+ };
463
+ }
464
+ }
465
+ };
466
+
467
+ module.exports = { definitions, handlers };
@@ -104,6 +104,10 @@ const handlers = {
104
104
  browserProfile = process.env.MCP_BROWSER_PROFILE || `${os.tmpdir()}/chrome-mcp-profile`;
105
105
  }
106
106
 
107
+ // Get dynamic tool count (loaded at runtime to avoid circular dependency)
108
+ const { tools } = require('./index');
109
+ const toolCount = tools.length;
110
+
107
111
  return {
108
112
  content: [{
109
113
  type: 'text',
@@ -113,7 +117,7 @@ const handlers = {
113
117
  `✅ Chrome: Port 9222\n` +
114
118
  `✅ Profile: ${browserProfile}\n` +
115
119
  `✅ Current page: ${url}\n\n` +
116
- `All 37 browser tools are ready to use!`
120
+ `All ${toolCount} browser tools are ready to use!`
117
121
  }]
118
122
  };
119
123
  },
package/src/utils.js CHANGED
@@ -4,6 +4,16 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const os = require('os');
7
+ const path = require('path');
8
+
9
+ // Get version from package.json
10
+ let version = 'unknown';
11
+ try {
12
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
13
+ version = pkg.version;
14
+ } catch (error) {
15
+ // Fallback if package.json cannot be read
16
+ }
7
17
 
8
18
  // Log file location
9
19
  const logFile = `${os.tmpdir()}/mcp-browser-server.log`;
@@ -116,5 +126,6 @@ module.exports = {
116
126
  loadPlaywright,
117
127
  getPlaywrightPath,
118
128
  findChromeExecutable,
119
- logFile
129
+ logFile,
130
+ version
120
131
  };
@@ -0,0 +1,48 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Network Test Page</title>
7
+ </head>
8
+ <body>
9
+ <h1>Network Test Page</h1>
10
+ <p>This page makes multiple network requests for testing.</p>
11
+ <div id="status"></div>
12
+
13
+ <script>
14
+ async function makeNetworkRequests() {
15
+ const status = document.getElementById('status');
16
+ status.innerHTML = 'Making network requests...<br>';
17
+
18
+ try {
19
+ // Make multiple fetch requests
20
+ const urls = [
21
+ 'https://jsonplaceholder.typicode.com/posts/1',
22
+ 'https://jsonplaceholder.typicode.com/posts/2',
23
+ 'https://jsonplaceholder.typicode.com/users/1'
24
+ ];
25
+
26
+ for (const url of urls) {
27
+ try {
28
+ const response = await fetch(url);
29
+ const data = await response.json();
30
+ status.innerHTML += `✓ Fetched: ${url}<br>`;
31
+ } catch (e) {
32
+ status.innerHTML += `✗ Failed: ${url} - ${e.message}<br>`;
33
+ }
34
+ }
35
+
36
+ status.innerHTML += 'Network requests completed!';
37
+ } catch (error) {
38
+ status.innerHTML += `Error: ${error.message}`;
39
+ }
40
+ }
41
+
42
+ // Auto-run after page load
43
+ window.addEventListener('load', () => {
44
+ setTimeout(makeNetworkRequests, 500);
45
+ });
46
+ </script>
47
+ </body>
48
+ </html>
@@ -0,0 +1,61 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Performance Test Page</title>
7
+ <style>
8
+ body {
9
+ font-family: Arial, sans-serif;
10
+ padding: 20px;
11
+ }
12
+ .container {
13
+ max-width: 800px;
14
+ margin: 0 auto;
15
+ }
16
+ </style>
17
+ </head>
18
+ <body>
19
+ <div class="container">
20
+ <h1>Performance Test Page</h1>
21
+ <p>This page is used for testing CPU profiling and performance metrics.</p>
22
+ <button id="heavy-compute">Run Heavy Computation</button>
23
+ <div id="output"></div>
24
+ </div>
25
+
26
+ <script>
27
+ // Heavy computation for CPU profiling test
28
+ function fibonacci(n) {
29
+ if (n <= 1) return n;
30
+ return fibonacci(n - 1) + fibonacci(n - 2);
31
+ }
32
+
33
+ function performHeavyComputation() {
34
+ const start = Date.now();
35
+ const result = fibonacci(35);
36
+ const duration = Date.now() - start;
37
+ document.getElementById('output').innerHTML =
38
+ `Computed fibonacci(35) = ${result} in ${duration}ms`;
39
+ }
40
+
41
+ // Auto-run computation for testing
42
+ document.getElementById('heavy-compute').addEventListener('click', performHeavyComputation);
43
+
44
+ // Run automatically after a short delay
45
+ setTimeout(performHeavyComputation, 100);
46
+
47
+ // Create some DOM nodes for metrics testing
48
+ for (let i = 0; i < 100; i++) {
49
+ const div = document.createElement('div');
50
+ div.textContent = `Node ${i}`;
51
+ div.style.display = 'none';
52
+ document.body.appendChild(div);
53
+ }
54
+
55
+ // Add event listeners for metrics testing
56
+ for (let i = 0; i < 50; i++) {
57
+ document.body.addEventListener('click', () => {});
58
+ }
59
+ </script>
60
+ </body>
61
+ </html>
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline'">
7
+ <title>Security Test Page</title>
8
+ </head>
9
+ <body>
10
+ <h1>Security Test Page</h1>
11
+ <p>This page is used for testing security features.</p>
12
+ <div id="status"></div>
13
+
14
+ <script>
15
+ // This inline script is allowed by CSP
16
+ document.getElementById('status').innerHTML = 'Page loaded successfully';
17
+
18
+ // Attempt to trigger CSP violation by trying to load external script
19
+ // (This will be blocked and create a CSP violation)
20
+ try {
21
+ const script = document.createElement('script');
22
+ script.src = 'https://example.com/external.js';
23
+ document.head.appendChild(script);
24
+ } catch (e) {
25
+ console.log('CSP blocked external script');
26
+ }
27
+
28
+ // Log security info
29
+ console.log('Page protocol:', window.location.protocol);
30
+ console.log('Page origin:', window.location.origin);
31
+ </script>
32
+ </body>
33
+ </html>