@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,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 };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const { connectToBrowser, getBrowserState } = require('../browser');
|
|
3
|
+
const { getPlaywrightPath } = require('../utils');
|
|
4
|
+
|
|
5
|
+
const definitions = [
|
|
6
|
+
{
|
|
7
|
+
name: 'browser_health_check',
|
|
8
|
+
description: 'Check if the browser is running and accessible on port 9222 (see browser_docs)',
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {},
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'browser_wait',
|
|
18
|
+
description: 'Pause execution for a duration (see browser_docs)',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
ms: { type: 'number', description: 'Milliseconds to wait' }
|
|
23
|
+
},
|
|
24
|
+
required: ['ms'],
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'browser_resize_window',
|
|
31
|
+
description: 'Resize the browser window (useful for testing responsiveness) (see browser_docs)',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
width: { type: 'number', description: 'Window width in pixels' },
|
|
36
|
+
height: { type: 'number', description: 'Window height in pixels' }
|
|
37
|
+
},
|
|
38
|
+
required: ['width', 'height'],
|
|
39
|
+
additionalProperties: false,
|
|
40
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'browser_wait_for_selector',
|
|
45
|
+
description: 'Wait for an element to appear on the page (see browser_docs)',
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
selector: { type: 'string', description: 'Playwright selector to wait for' },
|
|
50
|
+
timeout: { type: 'number', description: 'Timeout in milliseconds', default: 30000 }
|
|
51
|
+
},
|
|
52
|
+
required: ['selector'],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'browser_start_video_recording',
|
|
59
|
+
description: 'Start recording browser session as video (see browser_docs)',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
path: { type: 'string', description: 'Path to save the video file' }
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'browser_stop_video_recording',
|
|
71
|
+
description: 'Stop video recording and save the file (see browser_docs)',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {},
|
|
75
|
+
additionalProperties: false,
|
|
76
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const handlers = {
|
|
82
|
+
browser_health_check: async (args) => {
|
|
83
|
+
// connectToBrowser called inside executeTool usually, but here we check state
|
|
84
|
+
const { browser, page } = getBrowserState();
|
|
85
|
+
|
|
86
|
+
// We try to connect if not connected
|
|
87
|
+
if (!browser) {
|
|
88
|
+
// If getting here via executeTool, connectToBrowser was already called
|
|
89
|
+
// If somehow not, we call it
|
|
90
|
+
await connectToBrowser();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const state = getBrowserState();
|
|
94
|
+
const url = state.page ? await state.page.url() : 'Unknown';
|
|
95
|
+
|
|
96
|
+
const isConnected = state.browser && state.browser.isConnected && state.browser.isConnected();
|
|
97
|
+
const mode = isConnected ? 'Connected to existing Chrome (Antigravity)' : 'Launched standalone Chrome';
|
|
98
|
+
|
|
99
|
+
// Determine profile path based on mode
|
|
100
|
+
let browserProfile;
|
|
101
|
+
if (isConnected) {
|
|
102
|
+
browserProfile = `${process.env.HOME}/.gemini/antigravity-browser-profile`;
|
|
103
|
+
} else {
|
|
104
|
+
browserProfile = process.env.MCP_BROWSER_PROFILE || `${os.tmpdir()}/chrome-mcp-profile`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get dynamic tool count (loaded at runtime to avoid circular dependency)
|
|
108
|
+
const { tools } = require('./index');
|
|
109
|
+
const toolCount = tools.length;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
content: [{
|
|
113
|
+
type: 'text',
|
|
114
|
+
text: `✅ Browser automation is fully functional!\n\n` +
|
|
115
|
+
`Mode: ${mode}\n` +
|
|
116
|
+
`✅ Playwright: ${getPlaywrightPath() || 'loaded'}\n` +
|
|
117
|
+
`✅ Chrome: Port 9222\n` +
|
|
118
|
+
`✅ Profile: ${browserProfile}\n` +
|
|
119
|
+
`✅ Current page: ${url}\n\n` +
|
|
120
|
+
`All ${toolCount} browser tools are ready to use!`
|
|
121
|
+
}]
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
browser_wait: async (args) => {
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, args.ms));
|
|
127
|
+
return { content: [{ type: 'text', text: `Waited for ${args.ms}ms` }] };
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
browser_resize_window: async (args) => {
|
|
131
|
+
const { page } = await connectToBrowser();
|
|
132
|
+
await page.setViewportSize({
|
|
133
|
+
width: args.width,
|
|
134
|
+
height: args.height
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
content: [{
|
|
138
|
+
type: 'text',
|
|
139
|
+
text: `Resized window to ${args.width}x${args.height}`
|
|
140
|
+
}]
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
browser_wait_for_selector: async (args) => {
|
|
145
|
+
const { page } = await connectToBrowser();
|
|
146
|
+
await page.waitForSelector(args.selector, {
|
|
147
|
+
timeout: args.timeout || 30000
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
content: [{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text: `Element ${args.selector} appeared`
|
|
153
|
+
}]
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
browser_start_video_recording: async (args) => {
|
|
158
|
+
const { context } = await connectToBrowser();
|
|
159
|
+
const videoPath = args.path || `${os.tmpdir()}/browser-recording-${Date.now()}.webm`;
|
|
160
|
+
await context.tracing.start({
|
|
161
|
+
screenshots: true,
|
|
162
|
+
snapshots: true
|
|
163
|
+
});
|
|
164
|
+
// Start video recording using Playwright's video feature
|
|
165
|
+
if (!context._options || !context._options.recordVideo) {
|
|
166
|
+
// Note: Video recording needs to be set when creating context
|
|
167
|
+
// For existing context, we'll use screenshots as fallback
|
|
168
|
+
return {
|
|
169
|
+
content: [{
|
|
170
|
+
type: 'text',
|
|
171
|
+
text: 'Started session tracing (screenshots). For full video, context needs recordVideo option at creation.'
|
|
172
|
+
}]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
content: [{
|
|
177
|
+
type: 'text',
|
|
178
|
+
text: `Started video recording to ${videoPath}`
|
|
179
|
+
}]
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
browser_stop_video_recording: async (args) => {
|
|
184
|
+
const { context } = await connectToBrowser();
|
|
185
|
+
const tracePath = `${os.tmpdir()}/trace-${Date.now()}.zip`;
|
|
186
|
+
await context.tracing.stop({ path: tracePath });
|
|
187
|
+
return {
|
|
188
|
+
content: [{
|
|
189
|
+
type: 'text',
|
|
190
|
+
text: `Stopped recording. Trace saved to ${tracePath}. Use 'playwright show-trace ${tracePath}' to view.`
|
|
191
|
+
}]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
module.exports = { definitions, handlers };
|