@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,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 };