@ricardodeazambuja/browser-mcp-server 1.0.3

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,792 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Universal Browser Automation MCP Server (Playwright Edition)
5
+ *
6
+ * A Model Context Protocol server providing 16 browser automation tools
7
+ * for AI agents. Works with Antigravity, Claude Desktop, and any MCP client.
8
+ *
9
+ * KEY FEATURES:
10
+ * - Smart Chrome Detection: Automatically finds and uses system Chrome/Chromium
11
+ * - Three-Tier Strategy: Antigravity Chrome > System Chrome > Playwright Chromium
12
+ * - 16 Tools: Navigate, click, type, screenshot, console capture, and more
13
+ * - Isolated Profile: Uses /tmp/chrome-mcp-profile (won't touch personal Chrome)
14
+ * - Auto-Reconnect: Handles browser crashes and disconnections gracefully
15
+ *
16
+ * MODES:
17
+ * 1. Antigravity Mode: Connects to existing Chrome on port 9222
18
+ * - Detects: Chrome with --remote-debugging-port=9222
19
+ * - Profile: ~/.gemini/antigravity-browser-profile
20
+ *
21
+ * 2. Standalone Mode: Launches own Chrome instance
22
+ * - Searches: /usr/bin/google-chrome, /usr/bin/chromium, etc.
23
+ * - Falls back to: Playwright's Chromium (if installed)
24
+ * - Profile: /tmp/chrome-mcp-profile (configurable via MCP_BROWSER_PROFILE)
25
+ *
26
+ * @version 1.0.3
27
+ * @author Ricardo de Azambuja
28
+ * @license MIT
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const os = require('os');
33
+ const logFile = `${os.tmpdir()}/mcp-browser-server.log`;
34
+
35
+ // Helper to log debug info
36
+ function debugLog(msg) {
37
+ const timestamp = new Date().toISOString();
38
+ fs.appendFileSync(logFile, `${timestamp} - ${msg}\n`);
39
+ }
40
+
41
+ debugLog('Server starting...');
42
+ debugLog(`HOME: ${process.env.HOME}`);
43
+ debugLog(`CWD: ${process.cwd()}`);
44
+
45
+ let playwright = null;
46
+ let playwrightError = null;
47
+ let playwrightPath = null;
48
+
49
+ // Try to load Playwright from multiple sources
50
+ function loadPlaywright() {
51
+ if (playwright) return playwright;
52
+ if (playwrightError) throw playwrightError;
53
+
54
+ const sources = [
55
+ // 1. Standard npm Playwright (local) - prioritize for standalone mode
56
+ { path: 'playwright', name: 'npm Playwright (local)' },
57
+ // 2. Antigravity's Go-based Playwright - fallback for Antigravity mode
58
+ { path: `${process.env.HOME}/.cache/ms-playwright-go/1.50.1/package`, name: 'Antigravity Go Playwright' },
59
+ // 3. Global npm Playwright
60
+ { path: `${process.env.HOME}/.npm-global/lib/node_modules/playwright`, name: 'npm Playwright (global)' }
61
+ ];
62
+
63
+ for (const source of sources) {
64
+ try {
65
+ debugLog(`Trying to load Playwright from: ${source.path}`);
66
+ playwright = require(source.path);
67
+ playwrightPath = source.path;
68
+ debugLog(`✅ Playwright loaded successfully: ${source.name}`);
69
+ return playwright;
70
+ } catch (error) {
71
+ debugLog(`❌ Could not load from ${source.path}: ${error.message}`);
72
+ }
73
+ }
74
+
75
+ // None worked
76
+ playwrightError = new Error(
77
+ '❌ Playwright is not installed.\n\n' +
78
+ 'To install Playwright:\n' +
79
+ '1. In Antigravity: Click the Chrome logo (top right) to "Open Browser" - this installs Playwright automatically\n' +
80
+ '2. Standalone mode: Run:\n' +
81
+ ' npm install playwright\n' +
82
+ ' npx playwright install chromium\n\n' +
83
+ `Tried locations:\n${sources.map(s => ` - ${s.path}`).join('\n')}`
84
+ );
85
+ throw playwrightError;
86
+ }
87
+
88
+ const readline = require('readline');
89
+
90
+ const rl = readline.createInterface({
91
+ input: process.stdin,
92
+ output: process.stdout,
93
+ terminal: false
94
+ });
95
+
96
+ let browser = null;
97
+ let context = null;
98
+ let page = null;
99
+
100
+ // Console log capture
101
+ let consoleLogs = [];
102
+ let consoleListening = false;
103
+
104
+ // Find Chrome executable in common locations
105
+ function findChromeExecutable() {
106
+ const { execSync } = require('child_process');
107
+
108
+ const commonPaths = [
109
+ // Linux
110
+ '/usr/bin/google-chrome',
111
+ '/usr/bin/google-chrome-stable',
112
+ '/usr/bin/chromium',
113
+ '/usr/bin/chromium-browser',
114
+ '/snap/bin/chromium',
115
+ // macOS
116
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
117
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
118
+ // Windows (via WSL or similar)
119
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
120
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
121
+ ];
122
+
123
+ // First try common paths
124
+ for (const path of commonPaths) {
125
+ if (fs.existsSync(path)) {
126
+ debugLog(`Found Chrome at: ${path}`);
127
+ return path;
128
+ }
129
+ }
130
+
131
+ // Try using 'which' on Unix-like systems
132
+ if (process.platform !== 'win32') {
133
+ try {
134
+ const result = execSync('which google-chrome || which chromium || which chromium-browser', { encoding: 'utf8' }).trim();
135
+ if (result && fs.existsSync(result)) {
136
+ debugLog(`Found Chrome via 'which': ${result}`);
137
+ return result;
138
+ }
139
+ } catch (e) {
140
+ debugLog(`'which' command failed: ${e.message}`);
141
+ }
142
+ }
143
+
144
+ debugLog('No system Chrome found');
145
+ return null;
146
+ }
147
+
148
+ // Connect to existing Chrome OR launch new instance (hybrid mode)
149
+ async function connectToBrowser() {
150
+ // Check if browser is disconnected or closed
151
+ if (browser && (!browser.isConnected || !browser.isConnected())) {
152
+ debugLog('Browser connection lost, resetting...');
153
+ browser = null;
154
+ context = null;
155
+ page = null;
156
+ }
157
+
158
+ if (!browser) {
159
+ try {
160
+ // Load Playwright (will throw if not installed)
161
+ const pw = loadPlaywright();
162
+
163
+ // STRATEGY 1: Try to connect to existing Chrome (Antigravity mode)
164
+ try {
165
+ debugLog('Attempting to connect to Chrome on port 9222...');
166
+ browser = await pw.chromium.connectOverCDP('http://localhost:9222');
167
+ debugLog('✅ Connected to existing Chrome (Antigravity mode)');
168
+
169
+ const contexts = browser.contexts();
170
+ context = contexts.length > 0 ? contexts[0] : await browser.newContext();
171
+ const pages = context.pages();
172
+ page = pages.length > 0 ? pages[0] : await context.newPage();
173
+
174
+ debugLog('Successfully connected to Chrome');
175
+ return { browser, context, page };
176
+ } catch (connectError) {
177
+ debugLog(`Could not connect to existing Chrome: ${connectError.message}`);
178
+ }
179
+
180
+ // STRATEGY 2: Launch our own Chrome (Standalone mode)
181
+ debugLog('No existing Chrome found. Launching new instance...');
182
+
183
+ const profileDir = process.env.MCP_BROWSER_PROFILE ||
184
+ `${os.tmpdir()}/chrome-mcp-profile`;
185
+
186
+ debugLog(`Browser profile: ${profileDir}`);
187
+
188
+ // Try to find system Chrome first
189
+ const chromeExecutable = findChromeExecutable();
190
+ const launchOptions = {
191
+ headless: false,
192
+ args: [
193
+ // CRITICAL: Remote debugging
194
+ '--remote-debugging-port=9222',
195
+
196
+ // IMPORTANT: Skip first-run experience
197
+ '--no-first-run',
198
+ '--no-default-browser-check',
199
+ '--disable-fre',
200
+
201
+ // STABILITY: Reduce popups and background activity
202
+ '--disable-features=TranslateUI,OptGuideOnDeviceModel',
203
+ '--disable-sync',
204
+ '--disable-component-update',
205
+ '--disable-background-networking',
206
+ '--disable-breakpad',
207
+ '--disable-background-timer-throttling',
208
+ '--disable-backgrounding-occluded-windows',
209
+ '--disable-renderer-backgrounding'
210
+ ]
211
+ };
212
+
213
+ // If system Chrome found, use it; otherwise use Playwright's Chromium
214
+ if (chromeExecutable) {
215
+ debugLog(`Using system Chrome/Chromium: ${chromeExecutable}`);
216
+ launchOptions.executablePath = chromeExecutable;
217
+ } else {
218
+ debugLog('No system Chrome/Chromium found. Attempting to use Playwright Chromium...');
219
+ }
220
+
221
+ // Use launchPersistentContext to properly handle user data directory
222
+ try {
223
+ context = await pw.chromium.launchPersistentContext(profileDir, launchOptions);
224
+ } catch (launchError) {
225
+ // If launch failed and no system Chrome was found, provide helpful error
226
+ if (!chromeExecutable && launchError.message.includes('Executable doesn\'t exist')) {
227
+ debugLog('Playwright Chromium not installed and no system Chrome found');
228
+ throw new Error(
229
+ '❌ No Chrome/Chromium browser found!\n\n' +
230
+ 'This MCP server needs a Chrome or Chromium browser to work.\n\n' +
231
+ 'Option 1 - Install Chrome/Chromium on your system:\n' +
232
+ ' • Ubuntu/Debian: sudo apt install google-chrome-stable\n' +
233
+ ' • Ubuntu/Debian: sudo apt install chromium-browser\n' +
234
+ ' • Fedora: sudo dnf install google-chrome-stable\n' +
235
+ ' • macOS: brew install --cask google-chrome\n' +
236
+ ' • Or download from: https://www.google.com/chrome/\n\n' +
237
+ 'Option 2 - Install Playwright\'s Chromium:\n' +
238
+ ' npm install playwright\n' +
239
+ ' npx playwright install chromium\n\n' +
240
+ 'Option 3 - Use with Antigravity:\n' +
241
+ ' Open Antigravity and click the Chrome logo (top right) to start the browser.\n' +
242
+ ' This MCP server will automatically connect to it.\n'
243
+ );
244
+ }
245
+ throw launchError;
246
+ }
247
+
248
+ // With launchPersistentContext, browser is the context
249
+ browser = context;
250
+ const pages = context.pages();
251
+ page = pages.length > 0 ? pages[0] : await context.newPage();
252
+
253
+ debugLog('✅ Successfully launched new Chrome instance (Standalone mode)');
254
+
255
+ } catch (error) {
256
+ debugLog(`Failed to connect/launch Chrome: ${error.message}`);
257
+
258
+ // If error wasn't already formatted nicely, provide generic error
259
+ if (!error.message.startsWith('❌')) {
260
+ const errorMsg =
261
+ '❌ Cannot start browser.\n\n' +
262
+ 'To fix this:\n' +
263
+ '1. In Antigravity: Click the Chrome logo (top right) to "Open Browser"\n' +
264
+ '2. Standalone mode: Install Chrome/Chromium or Playwright\'s Chromium:\n' +
265
+ ' npm install playwright\n' +
266
+ ' npx playwright install chromium\n\n' +
267
+ `Error: ${error.message}`;
268
+ throw new Error(errorMsg);
269
+ }
270
+
271
+ throw error;
272
+ }
273
+ }
274
+ return { browser, context, page };
275
+ }
276
+
277
+ // MCP Tool definitions
278
+ const tools = [
279
+ {
280
+ name: 'browser_navigate',
281
+ description: 'Navigate to a URL in the browser',
282
+ inputSchema: {
283
+ type: 'object',
284
+ properties: {
285
+ url: { type: 'string', description: 'The URL to navigate to' }
286
+ },
287
+ required: ['url'],
288
+ additionalProperties: false,
289
+ $schema: 'http://json-schema.org/draft-07/schema#'
290
+ }
291
+ },
292
+ {
293
+ name: 'browser_click',
294
+ description: 'Click an element on the page using Playwright selector',
295
+ inputSchema: {
296
+ type: 'object',
297
+ properties: {
298
+ selector: { type: 'string', description: 'Playwright selector for the element' }
299
+ },
300
+ required: ['selector'],
301
+ additionalProperties: false,
302
+ $schema: 'http://json-schema.org/draft-07/schema#'
303
+ }
304
+ },
305
+ {
306
+ name: 'browser_screenshot',
307
+ description: 'Take a screenshot of the current page',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {
311
+ fullPage: { type: 'boolean', description: 'Capture full page', default: false }
312
+ },
313
+ additionalProperties: false,
314
+ $schema: 'http://json-schema.org/draft-07/schema#'
315
+ }
316
+ },
317
+ {
318
+ name: 'browser_get_text',
319
+ description: 'Get text content from an element',
320
+ inputSchema: {
321
+ type: 'object',
322
+ properties: {
323
+ selector: { type: 'string', description: 'Playwright selector for the element' }
324
+ },
325
+ required: ['selector'],
326
+ additionalProperties: false,
327
+ $schema: 'http://json-schema.org/draft-07/schema#'
328
+ }
329
+ },
330
+ {
331
+ name: 'browser_type',
332
+ description: 'Type text into an input field',
333
+ inputSchema: {
334
+ type: 'object',
335
+ properties: {
336
+ selector: { type: 'string', description: 'Playwright selector for the input' },
337
+ text: { type: 'string', description: 'Text to type' }
338
+ },
339
+ required: ['selector', 'text'],
340
+ additionalProperties: false,
341
+ $schema: 'http://json-schema.org/draft-07/schema#'
342
+ }
343
+ },
344
+ {
345
+ name: 'browser_evaluate',
346
+ description: 'Execute JavaScript in the browser context',
347
+ inputSchema: {
348
+ type: 'object',
349
+ properties: {
350
+ code: { type: 'string', description: 'JavaScript code to execute' }
351
+ },
352
+ required: ['code'],
353
+ additionalProperties: false,
354
+ $schema: 'http://json-schema.org/draft-07/schema#'
355
+ }
356
+ },
357
+ {
358
+ name: 'browser_wait_for_selector',
359
+ description: 'Wait for an element to appear on the page',
360
+ inputSchema: {
361
+ type: 'object',
362
+ properties: {
363
+ selector: { type: 'string', description: 'Playwright selector to wait for' },
364
+ timeout: { type: 'number', description: 'Timeout in milliseconds', default: 30000 }
365
+ },
366
+ required: ['selector'],
367
+ additionalProperties: false,
368
+ $schema: 'http://json-schema.org/draft-07/schema#'
369
+ }
370
+ },
371
+ {
372
+ name: 'browser_scroll',
373
+ description: 'Scroll the page',
374
+ inputSchema: {
375
+ type: 'object',
376
+ properties: {
377
+ x: { type: 'number', description: 'Horizontal scroll position' },
378
+ y: { type: 'number', description: 'Vertical scroll position' }
379
+ },
380
+ additionalProperties: false,
381
+ $schema: 'http://json-schema.org/draft-07/schema#'
382
+ }
383
+ },
384
+ {
385
+ name: 'browser_resize_window',
386
+ description: 'Resize the browser window (useful for testing responsiveness)',
387
+ inputSchema: {
388
+ type: 'object',
389
+ properties: {
390
+ width: { type: 'number', description: 'Window width in pixels' },
391
+ height: { type: 'number', description: 'Window height in pixels' }
392
+ },
393
+ required: ['width', 'height'],
394
+ additionalProperties: false,
395
+ $schema: 'http://json-schema.org/draft-07/schema#'
396
+ }
397
+ },
398
+ {
399
+ name: 'browser_get_dom',
400
+ description: 'Get the full DOM structure or specific element data',
401
+ inputSchema: {
402
+ type: 'object',
403
+ properties: {
404
+ selector: { type: 'string', description: 'Optional selector to get DOM of specific element' }
405
+ },
406
+ additionalProperties: false,
407
+ $schema: 'http://json-schema.org/draft-07/schema#'
408
+ }
409
+ },
410
+ {
411
+ name: 'browser_start_video_recording',
412
+ description: 'Start recording browser session as video',
413
+ inputSchema: {
414
+ type: 'object',
415
+ properties: {
416
+ path: { type: 'string', description: 'Path to save the video file' }
417
+ },
418
+ additionalProperties: false,
419
+ $schema: 'http://json-schema.org/draft-07/schema#'
420
+ }
421
+ },
422
+ {
423
+ name: 'browser_stop_video_recording',
424
+ description: 'Stop video recording and save the file',
425
+ inputSchema: {
426
+ type: 'object',
427
+ properties: {},
428
+ additionalProperties: false,
429
+ $schema: 'http://json-schema.org/draft-07/schema#'
430
+ }
431
+ },
432
+ {
433
+ name: 'browser_health_check',
434
+ description: 'Check if the browser is running and accessible on port 9222',
435
+ inputSchema: {
436
+ type: 'object',
437
+ properties: {},
438
+ additionalProperties: false,
439
+ $schema: 'http://json-schema.org/draft-07/schema#'
440
+ }
441
+ },
442
+ {
443
+ name: 'browser_console_start',
444
+ description: 'Start capturing browser console logs (console.log, console.error, console.warn, etc.)',
445
+ inputSchema: {
446
+ type: 'object',
447
+ properties: {
448
+ level: {
449
+ type: 'string',
450
+ description: 'Optional filter for log level: "log", "error", "warn", "info", "debug", or "all"',
451
+ enum: ['log', 'error', 'warn', 'info', 'debug', 'all']
452
+ }
453
+ },
454
+ additionalProperties: false,
455
+ $schema: 'http://json-schema.org/draft-07/schema#'
456
+ }
457
+ },
458
+ {
459
+ name: 'browser_console_get',
460
+ description: 'Get all captured console logs since browser_console_start was called',
461
+ inputSchema: {
462
+ type: 'object',
463
+ properties: {
464
+ filter: {
465
+ type: 'string',
466
+ description: 'Optional filter by log level: "log", "error", "warn", "info", "debug", or "all"',
467
+ enum: ['log', 'error', 'warn', 'info', 'debug', 'all']
468
+ }
469
+ },
470
+ additionalProperties: false,
471
+ $schema: 'http://json-schema.org/draft-07/schema#'
472
+ }
473
+ },
474
+ {
475
+ name: 'browser_console_clear',
476
+ description: 'Clear all captured console logs and stop listening',
477
+ inputSchema: {
478
+ type: 'object',
479
+ properties: {},
480
+ additionalProperties: false,
481
+ $schema: 'http://json-schema.org/draft-07/schema#'
482
+ }
483
+ }
484
+ ];
485
+
486
+ // Tool execution handlers
487
+ async function executeTool(name, args) {
488
+ try {
489
+ const { page } = await connectToBrowser();
490
+
491
+ switch (name) {
492
+ case 'browser_navigate':
493
+ await page.goto(args.url, { waitUntil: 'domcontentloaded' });
494
+ return { content: [{ type: 'text', text: `Navigated to ${args.url}` }] };
495
+
496
+ case 'browser_click':
497
+ await page.click(args.selector);
498
+ return { content: [{ type: 'text', text: `Clicked ${args.selector}` }] };
499
+
500
+ case 'browser_screenshot':
501
+ const screenshot = await page.screenshot({
502
+ fullPage: args.fullPage || false,
503
+ type: 'png'
504
+ });
505
+ return {
506
+ content: [{
507
+ type: 'image',
508
+ data: screenshot.toString('base64'),
509
+ mimeType: 'image/png'
510
+ }]
511
+ };
512
+
513
+ case 'browser_get_text':
514
+ const text = await page.textContent(args.selector);
515
+ return { content: [{ type: 'text', text }] };
516
+
517
+ case 'browser_type':
518
+ await page.fill(args.selector, args.text);
519
+ return { content: [{ type: 'text', text: `Typed into ${args.selector}` }] };
520
+
521
+ case 'browser_evaluate':
522
+ const result = await page.evaluate(args.code);
523
+ return {
524
+ content: [{
525
+ type: 'text',
526
+ text: JSON.stringify(result, null, 2)
527
+ }]
528
+ };
529
+
530
+ case 'browser_wait_for_selector':
531
+ await page.waitForSelector(args.selector, {
532
+ timeout: args.timeout || 30000
533
+ });
534
+ return {
535
+ content: [{
536
+ type: 'text',
537
+ text: `Element ${args.selector} appeared`
538
+ }]
539
+ };
540
+
541
+ case 'browser_scroll':
542
+ await page.evaluate(({ x, y }) => {
543
+ window.scrollTo(x || 0, y || 0);
544
+ }, args);
545
+ return {
546
+ content: [{
547
+ type: 'text',
548
+ text: `Scrolled to (${args.x || 0}, ${args.y || 0})`
549
+ }]
550
+ };
551
+
552
+ case 'browser_resize_window':
553
+ await page.setViewportSize({
554
+ width: args.width,
555
+ height: args.height
556
+ });
557
+ return {
558
+ content: [{
559
+ type: 'text',
560
+ text: `Resized window to ${args.width}x${args.height}`
561
+ }]
562
+ };
563
+
564
+ case 'browser_get_dom':
565
+ const domContent = await page.evaluate((sel) => {
566
+ const element = sel ? document.querySelector(sel) : document.documentElement;
567
+ if (!element) return null;
568
+ return {
569
+ outerHTML: element.outerHTML,
570
+ textContent: element.textContent,
571
+ attributes: Array.from(element.attributes || []).map(attr => ({
572
+ name: attr.name,
573
+ value: attr.value
574
+ })),
575
+ children: element.children.length
576
+ };
577
+ }, args.selector);
578
+ return {
579
+ content: [{
580
+ type: 'text',
581
+ text: JSON.stringify(domContent, null, 2)
582
+ }]
583
+ };
584
+
585
+ case 'browser_start_video_recording':
586
+ const videoPath = args.path || `${os.tmpdir()}/browser-recording-${Date.now()}.webm`;
587
+ await context.tracing.start({
588
+ screenshots: true,
589
+ snapshots: true
590
+ });
591
+ // Start video recording using Playwright's video feature
592
+ if (!context._options.recordVideo) {
593
+ // Note: Video recording needs to be set when creating context
594
+ // For existing context, we'll use screenshots as fallback
595
+ return {
596
+ content: [{
597
+ type: 'text',
598
+ text: 'Started session tracing (screenshots). For full video, context needs recordVideo option at creation.'
599
+ }]
600
+ };
601
+ }
602
+ return {
603
+ content: [{
604
+ type: 'text',
605
+ text: `Started video recording to ${videoPath}`
606
+ }]
607
+ };
608
+
609
+ case 'browser_stop_video_recording':
610
+ const tracePath = `${os.tmpdir()}/trace-${Date.now()}.zip`;
611
+ await context.tracing.stop({ path: tracePath });
612
+ return {
613
+ content: [{
614
+ type: 'text',
615
+ text: `Stopped recording. Trace saved to ${tracePath}. Use 'playwright show-trace ${tracePath}' to view.`
616
+ }]
617
+ };
618
+
619
+ case 'browser_health_check':
620
+ // Connection already succeeded if we got here
621
+ const url = await page.url();
622
+
623
+ // Detect mode: connected to existing Chrome or launched our own
624
+ const isConnected = browser.isConnected && browser.isConnected();
625
+ const mode = isConnected ? 'Connected to existing Chrome (Antigravity)' : 'Launched standalone Chrome';
626
+
627
+ // Determine profile path based on mode
628
+ let browserProfile;
629
+ if (isConnected) {
630
+ browserProfile = `${process.env.HOME}/.gemini/antigravity-browser-profile`;
631
+ } else {
632
+ browserProfile = process.env.MCP_BROWSER_PROFILE || `${os.tmpdir()}/chrome-mcp-profile`;
633
+ }
634
+
635
+ return {
636
+ content: [{
637
+ type: 'text',
638
+ text: `✅ Browser automation is fully functional!\n\n` +
639
+ `Mode: ${mode}\n` +
640
+ `✅ Playwright: ${playwrightPath || 'loaded'}\n` +
641
+ `✅ Chrome: Port 9222\n` +
642
+ `✅ Profile: ${browserProfile}\n` +
643
+ `✅ Current page: ${url}\n\n` +
644
+ `All 16 browser tools are ready to use!`
645
+ }]
646
+ };
647
+
648
+ case 'browser_console_start':
649
+ if (!consoleListening) {
650
+ page.on('console', msg => {
651
+ const logEntry = {
652
+ type: msg.type(),
653
+ text: msg.text(),
654
+ timestamp: new Date().toISOString(),
655
+ location: msg.location()
656
+ };
657
+ consoleLogs.push(logEntry);
658
+ debugLog(`Console [${logEntry.type}]: ${logEntry.text}`);
659
+ });
660
+ consoleListening = true;
661
+ debugLog('Console logging started');
662
+ }
663
+ return {
664
+ content: [{
665
+ type: 'text',
666
+ text: `✅ Console logging started.\n\nCapturing: console.log, console.error, console.warn, console.info, console.debug\n\nUse browser_console_get to retrieve captured logs.`
667
+ }]
668
+ };
669
+
670
+ case 'browser_console_get':
671
+ const filter = args.filter;
672
+ const filtered = filter && filter !== 'all'
673
+ ? consoleLogs.filter(log => log.type === filter)
674
+ : consoleLogs;
675
+
676
+ if (filtered.length === 0) {
677
+ return {
678
+ content: [{
679
+ type: 'text',
680
+ text: consoleListening
681
+ ? `No console logs captured yet.\n\n${filter && filter !== 'all' ? `Filter: ${filter}\n` : ''}Console logging is active - logs will appear as the page executes JavaScript.`
682
+ : `Console logging is not active.\n\nUse browser_console_start to begin capturing logs.`
683
+ }]
684
+ };
685
+ }
686
+
687
+ const logSummary = `📋 Captured ${filtered.length} console log${filtered.length === 1 ? '' : 's'}${filter && filter !== 'all' ? ` (filtered by: ${filter})` : ''}:\n\n`;
688
+ const formattedLogs = filtered.map((log, i) => {
689
+ const icon = {
690
+ 'error': '❌',
691
+ 'warn': '⚠️',
692
+ 'log': '📝',
693
+ 'info': 'ℹ️',
694
+ 'debug': '🔍'
695
+ }[log.type] || '📄';
696
+
697
+ return `${i + 1}. ${icon} [${log.type.toUpperCase()}] ${log.timestamp}\n ${log.text}${log.location.url ? `\n Location: ${log.location.url}:${log.location.lineNumber}` : ''}`;
698
+ }).join('\n\n');
699
+
700
+ return {
701
+ content: [{
702
+ type: 'text',
703
+ text: logSummary + formattedLogs
704
+ }]
705
+ };
706
+
707
+ case 'browser_console_clear':
708
+ const count = consoleLogs.length;
709
+ consoleLogs = [];
710
+ if (consoleListening) {
711
+ page.removeAllListeners('console');
712
+ consoleListening = false;
713
+ }
714
+ debugLog(`Cleared ${count} console logs and stopped listening`);
715
+ return {
716
+ content: [{
717
+ type: 'text',
718
+ text: `✅ Cleared ${count} console log${count === 1 ? '' : 's'} and stopped listening.\n\nUse browser_console_start to resume capturing.`
719
+ }]
720
+ };
721
+
722
+ default:
723
+ throw new Error(`Unknown tool: ${name}`);
724
+ }
725
+ } catch (error) {
726
+ debugLog(`Tool execution error (${name}): ${error.message}`);
727
+ return {
728
+ content: [{
729
+ type: 'text',
730
+ text: `❌ Error executing ${name}: ${error.message}`
731
+ }],
732
+ isError: true
733
+ };
734
+ }
735
+ }
736
+
737
+ // MCP Protocol handler
738
+ rl.on('line', async (line) => {
739
+ let request;
740
+ try {
741
+ debugLog(`Received: ${line.substring(0, 200)}`);
742
+ request = JSON.parse(line);
743
+
744
+ if (request.method === 'initialize') {
745
+ debugLog(`Initialize with protocol: ${request.params.protocolVersion}`);
746
+ respond(request.id, {
747
+ protocolVersion: request.params.protocolVersion || '2024-11-05',
748
+ capabilities: { tools: {} },
749
+ serverInfo: {
750
+ name: 'browser-automation-playwright',
751
+ version: '1.0.2'
752
+ }
753
+ });
754
+ } else if (request.method === 'notifications/initialized') {
755
+ // This is a notification - no response needed
756
+ debugLog('Received initialized notification');
757
+ } else if (request.method === 'tools/list') {
758
+ debugLog('Sending tools list');
759
+ respond(request.id, { tools });
760
+ } else if (request.method === 'tools/call') {
761
+ debugLog(`Calling tool: ${request.params.name}`);
762
+ const result = await executeTool(request.params.name, request.params.arguments || {});
763
+ respond(request.id, result);
764
+ } else {
765
+ debugLog(`Unknown method: ${request.method}`);
766
+ respond(request.id, null, { code: -32601, message: 'Method not found' });
767
+ }
768
+ } catch (error) {
769
+ debugLog(`Error processing request: ${error.message}`);
770
+ console.error('Error processing request:', error.message, 'Request:', line);
771
+ const id = request?.id || null;
772
+ respond(id, null, { code: -32603, message: error.message });
773
+ }
774
+ });
775
+
776
+ function respond(id, result, error = null) {
777
+ const response = { jsonrpc: '2.0', id };
778
+ if (error) response.error = error;
779
+ else response.result = result;
780
+ console.log(JSON.stringify(response));
781
+ }
782
+
783
+ // Cleanup on exit
784
+ process.on('SIGTERM', async () => {
785
+ if (browser) await browser.close();
786
+ process.exit(0);
787
+ });
788
+
789
+ process.on('SIGINT', async () => {
790
+ if (browser) await browser.close();
791
+ process.exit(0);
792
+ });