@ricardodeazambuja/browser-mcp-server 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ricardo de Azambuja
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,544 @@
1
+ # Browser MCP Server
2
+
3
+ A universal browser automation MCP server using Playwright. Control Chrome programmatically through the Model Context Protocol.
4
+
5
+ **16 powerful browser automation tools** including navigation, clicking, typing, screenshots, console capture, and session recording.
6
+
7
+ ## Features
8
+
9
+ - ✅ **Universal**: Works with Antigravity, Claude Desktop, and any MCP client
10
+ - ✅ **Hybrid Mode**: Connects to existing Chrome OR launches its own
11
+ - ✅ **Safe**: Isolated browser profile (won't touch your personal Chrome)
12
+ - ✅ **16 Tools**: Navigate, click, type, screenshot, console logs, and more
13
+ - ✅ **Console Capture**: Debug JavaScript errors in real-time
14
+ - ✅ **Session Recording**: Playwright traces with screenshots, DOM, and network activity
15
+ - ✅ **Portable**: One codebase works everywhere
16
+
17
+ ## Quick Reference
18
+
19
+ | Installation Method | Best For | Setup Time |
20
+ |-------------------|----------|------------|
21
+ | **Clone Repository** | Development, contributing | 2 minutes |
22
+ | **Direct Download** | Quick testing, minimal setup | 1 minute |
23
+ | **NPM Package** (coming soon) | Production use, easy updates | 30 seconds |
24
+
25
+ | MCP Client | Config File Location |
26
+ |------------|---------------------|
27
+ | **Claude Desktop** | `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)<br>`%APPDATA%/Claude/claude_desktop_config.json` (Windows) |
28
+ | **Antigravity** | `~/.gemini/antigravity/mcp_config.json` |
29
+ | **Claude Code** | Use `claude mcp add` command |
30
+ | **Gemini CLI** | Use `gemini mcp add` command |
31
+
32
+ **Key Points:**
33
+ - ✅ Requires Node.js >= 16.0.0
34
+ - ✅ Must install Playwright separately
35
+ - ✅ Uses absolute paths in config files
36
+ - ✅ Isolated browser profile (won't touch personal Chrome)
37
+ - ✅ Restart MCP client after config changes
38
+
39
+ ## Quick Start
40
+
41
+ ### Installation
42
+
43
+ #### Method 1: Clone Repository (Recommended)
44
+
45
+ ```bash
46
+ # Clone the repository
47
+ git clone https://github.com/ricardodeazambuja/browser-mcp-server.git
48
+ cd browser-mcp-server
49
+
50
+ # Install Playwright (one-time setup)
51
+ npm install playwright
52
+ npx playwright install chromium
53
+ ```
54
+
55
+ #### Method 2: Direct Download (Single File)
56
+
57
+ ```bash
58
+ # Download the main file directly (no git required)
59
+ curl -o browser-mcp-server-playwright.js \
60
+ https://raw.githubusercontent.com/ricardodeazambuja/browser-mcp-server/master/browser-mcp-server-playwright.js
61
+
62
+ # Install Playwright
63
+ npm install playwright
64
+ npx playwright install chromium
65
+ ```
66
+
67
+ #### Method 3: NPM Package (Coming Soon)
68
+
69
+ Once published to npm:
70
+
71
+ ```bash
72
+ # Install globally
73
+ npm install -g @ricardodeazambuja/browser-mcp-server
74
+
75
+ # Or use directly with npx (no installation needed)
76
+ npx @ricardodeazambuja/browser-mcp-server
77
+ ```
78
+
79
+ ### Usage with Claude Desktop
80
+
81
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
82
+
83
+ **Using local installation:**
84
+ ```json
85
+ {
86
+ "mcpServers": {
87
+ "browser-tools": {
88
+ "command": "node",
89
+ "args": ["/absolute/path/to/browser-mcp-server-playwright.js"]
90
+ }
91
+ }
92
+ }
93
+ ```
94
+
95
+ **Using NPM (when published):**
96
+ ```json
97
+ {
98
+ "mcpServers": {
99
+ "browser-tools": {
100
+ "command": "npx",
101
+ "args": ["-y", "@ricardodeazambuja/browser-mcp-server"]
102
+ }
103
+ }
104
+ }
105
+ ```
106
+
107
+ **Note:** Replace `/absolute/path/to/` with the actual path where you installed the file.
108
+
109
+ ### Usage with Antigravity
110
+
111
+ Add to `~/.gemini/antigravity/mcp_config.json`:
112
+
113
+ **Using local installation:**
114
+ ```json
115
+ {
116
+ "mcpServers": {
117
+ "browser-tools": {
118
+ "command": "node",
119
+ "args": ["/home/username/.gemini/antigravity/browser-mcp-server-playwright.js"]
120
+ }
121
+ }
122
+ }
123
+ ```
124
+
125
+ **Using NPM (when published):**
126
+ ```json
127
+ {
128
+ "mcpServers": {
129
+ "browser-tools": {
130
+ "command": "npx",
131
+ "args": ["-y", "@ricardodeazambuja/browser-mcp-server"]
132
+ }
133
+ }
134
+ }
135
+ ```
136
+
137
+ Then refresh MCP servers in Antigravity.
138
+
139
+ ### Usage with Claude Code
140
+
141
+ Add the browser-mcp-server using the Claude CLI:
142
+
143
+ **Using local installation:**
144
+ ```bash
145
+ # Install the MCP server with default isolated profile
146
+ claude mcp add --transport stdio browser \
147
+ -- node /absolute/path/to/browser-mcp-server-playwright.js
148
+
149
+ # Or with custom browser profile for more control
150
+ claude mcp add --transport stdio browser \
151
+ --env MCP_BROWSER_PROFILE=/path/to/custom/profile \
152
+ -- node /absolute/path/to/browser-mcp-server-playwright.js
153
+ ```
154
+
155
+ **Using NPM (when published):**
156
+ ```bash
157
+ # Install using npx (no local installation needed)
158
+ claude mcp add --transport stdio browser \
159
+ -- npx -y @ricardodeazambuja/browser-mcp-server
160
+
161
+ # With custom browser profile
162
+ claude mcp add --transport stdio browser \
163
+ --env MCP_BROWSER_PROFILE=/path/to/custom/profile \
164
+ -- npx -y @ricardodeazambuja/browser-mcp-server
165
+ ```
166
+
167
+ **Verify installation:**
168
+ ```bash
169
+ # List all MCP servers
170
+ claude mcp list
171
+
172
+ # Check server status
173
+ claude mcp get browser
174
+ ```
175
+
176
+ **Example usage in Claude Code:**
177
+
178
+ ```bash
179
+ # Natural language commands
180
+ > Navigate to https://example.com and take a screenshot
181
+ > Click the login button and fill in the username field
182
+ > What's the text in the .main-content selector?
183
+
184
+ # Direct tool invocation via slash commands
185
+ > /mcp__browser__browser_navigate https://example.com
186
+ > /mcp__browser__browser_screenshot
187
+ ```
188
+
189
+ **Note:** The server uses an isolated browser profile at `/tmp/chrome-mcp-profile` by default, ensuring it won't access your personal Chrome cookies or data.
190
+
191
+ ### Usage with Gemini CLI
192
+
193
+ Add the browser-mcp-server using the Gemini CLI commands:
194
+
195
+ **Using local installation:**
196
+ ```bash
197
+ # Install the MCP server with default isolated profile
198
+ gemini mcp add browser node /absolute/path/to/browser-mcp-server-playwright.js
199
+
200
+ # Or with custom browser profile
201
+ gemini mcp add -e MCP_BROWSER_PROFILE=/path/to/custom/profile browser \
202
+ node /absolute/path/to/browser-mcp-server-playwright.js
203
+ ```
204
+
205
+ **Using NPM (when published):**
206
+ ```bash
207
+ # Install using npx (no local installation needed)
208
+ gemini mcp add browser npx -y @ricardodeazambuja/browser-mcp-server
209
+
210
+ # With custom browser profile
211
+ gemini mcp add -e MCP_BROWSER_PROFILE=/path/to/custom/profile browser \
212
+ npx -y @ricardodeazambuja/browser-mcp-server
213
+ ```
214
+
215
+ **Management commands:**
216
+ ```bash
217
+ # List all configured MCP servers
218
+ gemini mcp list
219
+
220
+ # Remove the server if needed
221
+ gemini mcp remove browser
222
+ ```
223
+
224
+ **Example usage in Gemini CLI:**
225
+
226
+ ```bash
227
+ # Natural language commands
228
+ > Navigate to https://github.com and take a screenshot
229
+ > Click the search button and type "MCP servers"
230
+ > Get the text from the .repository-content selector
231
+
232
+ # The CLI will use the browser automation tools automatically
233
+ ```
234
+
235
+ **Advanced options:**
236
+
237
+ ```bash
238
+ # Add with specific scope (user vs project)
239
+ gemini mcp add -s user browser node /path/to/browser-mcp-server-playwright.js
240
+
241
+ # Add with timeout configuration
242
+ gemini mcp add --timeout 30000 browser node /path/to/browser-mcp-server-playwright.js
243
+
244
+ # Skip tool confirmation prompts (use with caution)
245
+ gemini mcp add --trust browser node /path/to/browser-mcp-server-playwright.js
246
+ ```
247
+
248
+ ## Available Tools (16)
249
+
250
+ ### Navigation & Interaction
251
+ 1. **browser_navigate(url)** - Navigate to a URL
252
+ 2. **browser_click(selector)** - Click an element
253
+ 3. **browser_type(selector, text)** - Type text into an input
254
+ 4. **browser_scroll(x?, y?)** - Scroll the page
255
+
256
+ ### Information Gathering
257
+ 5. **browser_screenshot(fullPage?)** - Capture screenshot
258
+ 6. **browser_get_text(selector)** - Get text from element
259
+ 7. **browser_get_dom(selector?)** - Get DOM structure
260
+ 8. **browser_evaluate(code)** - Execute JavaScript
261
+
262
+ ### Console Debugging ⭐ NEW
263
+ 9. **browser_console_start(level?)** - Start capturing console logs
264
+ 10. **browser_console_get(filter?)** - Get captured logs
265
+ 11. **browser_console_clear()** - Clear logs and stop
266
+
267
+ ### Advanced
268
+ 12. **browser_wait_for_selector(selector, timeout?)** - Wait for element
269
+ 13. **browser_resize_window(width, height)** - Resize browser window
270
+ 14. **browser_start_video_recording(path?)** - Start recording session (Playwright traces)
271
+ 15. **browser_stop_video_recording()** - Stop and save recording
272
+ 16. **browser_health_check()** - Verify browser connection
273
+
274
+ ## Examples
275
+
276
+ ### Navigate and Screenshot
277
+ ```javascript
278
+ // Agent uses:
279
+ browser_navigate("https://example.com")
280
+ browser_screenshot(fullPage: true)
281
+ ```
282
+
283
+ ### Debug JavaScript Errors
284
+ ```javascript
285
+ // Agent uses:
286
+ browser_console_start()
287
+ browser_navigate("https://myapp.com")
288
+ browser_click("#submit-button")
289
+ browser_console_get(filter: "error")
290
+ // Shows: ❌ [ERROR] Uncaught TypeError: ...
291
+ ```
292
+
293
+ ### Automate Form Submission
294
+ ```javascript
295
+ // Agent uses:
296
+ browser_navigate("https://example.com/login")
297
+ browser_type("#username", "user@example.com")
298
+ browser_type("#password", "secret")
299
+ browser_click("#login-button")
300
+ browser_wait_for_selector(".dashboard")
301
+ ```
302
+
303
+ ## How It Works
304
+
305
+ ### Hybrid Mode (Automatic)
306
+
307
+ The server automatically detects your environment:
308
+
309
+ **Antigravity Mode:**
310
+ - Detects Chrome on port 9222
311
+ - Connects to existing browser
312
+ - Uses Antigravity's browser profile
313
+ - No new browser window
314
+
315
+ **Standalone Mode:**
316
+ - No Chrome detected on port 9222
317
+ - Launches new Chrome instance
318
+ - Uses isolated profile (`/tmp/chrome-mcp-profile`)
319
+ - New browser window appears
320
+
321
+ ### Safety Features
322
+
323
+ - **Isolated Profile**: Uses `/tmp/chrome-mcp-profile` (not your personal Chrome!)
324
+ - **No Setup Dialogs**: Silent startup with `--no-first-run` flags
325
+ - **Clean Environment**: No extensions, sync, or background updates
326
+ - **Reproducible**: Same behavior across systems
327
+
328
+ ## Security
329
+
330
+ This MCP server provides powerful browser automation capabilities. Please review these security considerations:
331
+
332
+ ### Isolated Browser Profile
333
+ - Uses `/tmp/chrome-mcp-profile` by default (configurable via `MCP_BROWSER_PROFILE`)
334
+ - **Does NOT access your personal Chrome data** (cookies, passwords, history)
335
+ - Each instance runs in a clean, isolated environment
336
+
337
+ ### Tool Safety
338
+
339
+ **browser_evaluate**: Executes arbitrary JavaScript in the browser context
340
+ - Code runs in browser sandbox (no access to your host system)
341
+ - Only executes when explicitly called by MCP client
342
+ - Requires user approval in most MCP clients
343
+ - **Recommendation**: Only use with trusted MCP clients and review code when possible
344
+
345
+ **browser_navigate**: Navigates to any URL
346
+ - Can visit any website the browser can access
347
+ - Uses isolated profile to prevent cookie/session theft
348
+ - **Recommendation**: Be cautious with URLs from untrusted sources
349
+
350
+ ### Debug Logs
351
+ - Server logs to `/tmp/mcp-browser-server.log`
352
+ - Logs may contain visited URLs and error messages
353
+ - Log file is cleared on system reboot (stored in `/tmp`)
354
+ - **Does NOT log** page content or sensitive data
355
+
356
+ ### Best Practices
357
+ - ✅ Only use with trusted MCP clients (Claude Desktop, Antigravity, etc.)
358
+ - ✅ Review automation scripts before execution when possible
359
+ - ✅ Use the default isolated profile (don't point to your personal Chrome)
360
+ - ✅ Report security issues via [GitHub Issues](https://github.com/ricardodeazambuja/browser-mcp-server/issues)
361
+
362
+ ## Configuration
363
+
364
+ ### Environment Variables
365
+
366
+ ```bash
367
+ # Custom browser profile location (optional)
368
+ export MCP_BROWSER_PROFILE="$HOME/.mcp-browser-profile"
369
+
370
+ # Then run the server
371
+ node browser-mcp-server-playwright.js
372
+ ```
373
+
374
+ ### MCP Config with Environment Variables
375
+
376
+ ```json
377
+ {
378
+ "mcpServers": {
379
+ "browser-tools": {
380
+ "command": "node",
381
+ "args": ["/path/to/browser-mcp-server-playwright.js"],
382
+ "env": {
383
+ "MCP_BROWSER_PROFILE": "/tmp/my-custom-profile"
384
+ }
385
+ }
386
+ }
387
+ }
388
+ ```
389
+
390
+ ## Troubleshooting
391
+
392
+ ### "Playwright is not installed"
393
+
394
+ ```bash
395
+ npm install playwright
396
+ npx playwright install chromium
397
+ ```
398
+
399
+ ### "Cannot connect to Chrome"
400
+
401
+ **For Antigravity:**
402
+ - Click the Chrome logo (top right) to launch browser
403
+
404
+ **For Standalone:**
405
+ - The server will auto-launch Chrome
406
+ - Ensure Playwright is installed (see above)
407
+
408
+ ### Check Server Status
409
+
410
+ Use the `browser_health_check` tool to verify:
411
+ - Connection mode (Antigravity vs Standalone)
412
+ - Playwright source
413
+ - Browser profile location
414
+ - Current page URL
415
+
416
+ ## Development
417
+
418
+ ### Project Structure
419
+
420
+ ```
421
+ browser-mcp-server/
422
+ ├── browser-mcp-server-playwright.js # Main server
423
+ ├── package.json # npm package config
424
+ ├── README.md # This file
425
+ └── LICENSE # MIT license
426
+ ```
427
+
428
+ ### Testing
429
+
430
+ ```bash
431
+ # Test server initialization
432
+ npm test
433
+
434
+ # Manual test
435
+ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' | node browser-mcp-server-playwright.js
436
+ ```
437
+
438
+ ### Debug Logging
439
+
440
+ Check `/tmp/mcp-browser-server.log` for detailed logs:
441
+ - Playwright loading attempts
442
+ - Browser connection/launch status
443
+ - Console capture events
444
+ - Tool execution
445
+
446
+ ## Technical Details
447
+
448
+ ### MCP Protocol
449
+ - Implements MCP 2024-11-05 protocol
450
+ - JSON-RPC 2.0 over stdio
451
+ - Supports `initialize`, `notifications/initialized`, `tools/list`, `tools/call`
452
+
453
+ ### Browser Control
454
+ - Uses Playwright for automation
455
+ - Connects via Chrome DevTools Protocol (CDP)
456
+ - Port 9222 for remote debugging
457
+
458
+ ### Chrome Launch Flags
459
+ ```bash
460
+ --remote-debugging-port=9222 # Enable CDP
461
+ --user-data-dir=/tmp/chrome-mcp-profile # Isolated profile
462
+ --no-first-run # Skip setup
463
+ --no-default-browser-check # No popups
464
+ --disable-fre # No first-run experience
465
+ --disable-sync # No Google sync
466
+ --disable-component-update # No auto-updates
467
+ # + more stability flags
468
+ ```
469
+
470
+ ## Compatibility
471
+
472
+ ### Tested With
473
+ - ✅ Antigravity
474
+ - ✅ Claude Desktop (macOS, Windows, Linux)
475
+ - ✅ Other MCP clients via stdio
476
+
477
+ ### Requirements
478
+ - Node.js >= 16.0.0
479
+ - Playwright (peer dependency)
480
+ - Chrome/Chromium browser
481
+
482
+ ### Platforms
483
+ - ✅ Linux
484
+ - ✅ macOS
485
+ - ✅ Windows
486
+
487
+ ## Comparison with Other Tools
488
+
489
+ ### vs. Puppeteer MCP Servers
490
+ - ✅ More tools (16 vs typical 8-10)
491
+ - ✅ Console capture built-in
492
+ - ✅ Better error messages
493
+ - ✅ Hybrid mode (connect OR launch)
494
+
495
+ ### vs. Selenium Grid
496
+ - ✅ Simpler setup (no grid needed)
497
+ - ✅ MCP protocol integration
498
+ - ✅ Built for AI agents
499
+ - ✅ Lightweight (single process)
500
+
501
+ ### vs. Browser Extensions
502
+ - ✅ Works headlessly if needed
503
+ - ✅ No extension installation
504
+ - ✅ Programmable via MCP
505
+ - ✅ Works with any MCP client
506
+
507
+ ## Contributing
508
+
509
+ Contributions welcome! Please:
510
+ 1. Fork the repository
511
+ 2. Create a feature branch
512
+ 3. Make your changes
513
+ 4. Submit a pull request
514
+
515
+ ## License
516
+
517
+ MIT License - see LICENSE file
518
+
519
+ ## Credits
520
+
521
+ - Built with [Playwright](https://playwright.dev/)
522
+ - Implements [Model Context Protocol](https://modelcontextprotocol.io/)
523
+ - Originally developed for [Antigravity](https://antigravity.google/)
524
+
525
+ ## Support
526
+
527
+ - 🐛 [Report Issues](https://github.com/ricardodeazambuja/browser-mcp-server/issues)
528
+ - 💬 [Discussions](https://github.com/ricardodeazambuja/browser-mcp-server/discussions)
529
+ - 📧 Contact: Via GitHub Issues
530
+
531
+ ## Changelog
532
+
533
+ ### v1.0.0 (2025-12-26)
534
+ - ✅ Initial release
535
+ - ✅ 16 browser automation tools
536
+ - ✅ Console capture (start/get/clear)
537
+ - ✅ Hybrid mode (connect OR launch)
538
+ - ✅ Safe Chrome launch with isolated profile
539
+ - ✅ Multi-source Playwright loading
540
+ - ✅ Universal compatibility (Antigravity + Claude Desktop + more)
541
+
542
+ ---
543
+
544
+ **Made with ❤️ for the MCP community**
@@ -0,0 +1,684 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Browser Automation MCP Server for Antigravity (Playwright Edition)
5
+ *
6
+ * This MCP server provides 13 browser automation tools by connecting to
7
+ * Antigravity's Chrome instance that runs with remote debugging on port 9222.
8
+ *
9
+ * Antigravity launches Chrome with:
10
+ * --remote-debugging-port=9222
11
+ * --user-data-dir=~/.gemini/antigravity-browser-profile
12
+ *
13
+ * This server uses the same Playwright installation as browser_subagent,
14
+ * allowing seamless integration with Antigravity's browser ecosystem.
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const os = require('os');
19
+ const logFile = `${os.tmpdir()}/mcp-browser-server.log`;
20
+
21
+ // Helper to log debug info
22
+ function debugLog(msg) {
23
+ const timestamp = new Date().toISOString();
24
+ fs.appendFileSync(logFile, `${timestamp} - ${msg}\n`);
25
+ }
26
+
27
+ debugLog('Server starting...');
28
+ debugLog(`HOME: ${process.env.HOME}`);
29
+ debugLog(`CWD: ${process.cwd()}`);
30
+
31
+ let playwright = null;
32
+ let playwrightError = null;
33
+ let playwrightPath = null;
34
+
35
+ // Try to load Playwright from multiple sources
36
+ function loadPlaywright() {
37
+ if (playwright) return playwright;
38
+ if (playwrightError) throw playwrightError;
39
+
40
+ const sources = [
41
+ // 1. Antigravity's Go-based Playwright
42
+ { path: `${process.env.HOME}/.cache/ms-playwright-go/1.50.1/package`, name: 'Antigravity Go Playwright' },
43
+ // 2. Standard npm Playwright (local)
44
+ { path: 'playwright', name: 'npm Playwright (local)' },
45
+ // 3. Global npm Playwright
46
+ { path: `${process.env.HOME}/.npm-global/lib/node_modules/playwright`, name: 'npm Playwright (global)' }
47
+ ];
48
+
49
+ for (const source of sources) {
50
+ try {
51
+ debugLog(`Trying to load Playwright from: ${source.path}`);
52
+ playwright = require(source.path);
53
+ playwrightPath = source.path;
54
+ debugLog(`✅ Playwright loaded successfully: ${source.name}`);
55
+ return playwright;
56
+ } catch (error) {
57
+ debugLog(`❌ Could not load from ${source.path}: ${error.message}`);
58
+ }
59
+ }
60
+
61
+ // None worked
62
+ playwrightError = new Error(
63
+ '❌ Playwright is not installed.\n\n' +
64
+ 'To install Playwright:\n' +
65
+ '1. In Antigravity: Click the Chrome logo (top right) to "Open Browser" - this installs Playwright automatically\n' +
66
+ '2. Standalone mode: Run:\n' +
67
+ ' npm install playwright\n' +
68
+ ' npx playwright install chromium\n\n' +
69
+ `Tried locations:\n${sources.map(s => ` - ${s.path}`).join('\n')}`
70
+ );
71
+ throw playwrightError;
72
+ }
73
+
74
+ const readline = require('readline');
75
+
76
+ const rl = readline.createInterface({
77
+ input: process.stdin,
78
+ output: process.stdout,
79
+ terminal: false
80
+ });
81
+
82
+ let browser = null;
83
+ let context = null;
84
+ let page = null;
85
+
86
+ // Console log capture
87
+ let consoleLogs = [];
88
+ let consoleListening = false;
89
+
90
+ // Connect to existing Chrome OR launch new instance (hybrid mode)
91
+ async function connectToBrowser() {
92
+ if (!browser) {
93
+ try {
94
+ // Load Playwright (will throw if not installed)
95
+ const pw = loadPlaywright();
96
+
97
+ // STRATEGY 1: Try to connect to existing Chrome (Antigravity mode)
98
+ try {
99
+ debugLog('Attempting to connect to Chrome on port 9222...');
100
+ browser = await pw.chromium.connectOverCDP('http://localhost:9222');
101
+ debugLog('✅ Connected to existing Chrome (Antigravity mode)');
102
+
103
+ const contexts = browser.contexts();
104
+ context = contexts.length > 0 ? contexts[0] : await browser.newContext();
105
+ const pages = context.pages();
106
+ page = pages.length > 0 ? pages[0] : await context.newPage();
107
+
108
+ debugLog('Successfully connected to Chrome');
109
+ return { browser, context, page };
110
+ } catch (connectError) {
111
+ debugLog(`Could not connect to existing Chrome: ${connectError.message}`);
112
+ }
113
+
114
+ // STRATEGY 2: Launch our own Chrome (Standalone mode)
115
+ debugLog('No existing Chrome found. Launching new instance...');
116
+
117
+ const profileDir = process.env.MCP_BROWSER_PROFILE ||
118
+ `${os.tmpdir()}/chrome-mcp-profile`;
119
+
120
+ debugLog(`Browser profile: ${profileDir}`);
121
+
122
+ browser = await pw.chromium.launch({
123
+ headless: false,
124
+ args: [
125
+ // CRITICAL: Remote debugging
126
+ '--remote-debugging-port=9222',
127
+
128
+ // CRITICAL: Isolated profile (don't touch user's personal Chrome!)
129
+ `--user-data-dir=${profileDir}`,
130
+
131
+ // IMPORTANT: Skip first-run experience
132
+ '--no-first-run',
133
+ '--no-default-browser-check',
134
+ '--disable-fre',
135
+
136
+ // STABILITY: Reduce popups and background activity
137
+ '--disable-features=TranslateUI,OptGuideOnDeviceModel',
138
+ '--disable-sync',
139
+ '--disable-component-update',
140
+ '--disable-background-networking',
141
+ '--disable-breakpad',
142
+ '--disable-background-timer-throttling',
143
+ '--disable-backgrounding-occluded-windows',
144
+ '--disable-renderer-backgrounding'
145
+ ]
146
+ });
147
+
148
+ context = await browser.newContext();
149
+ page = await context.newPage();
150
+
151
+ debugLog('✅ Successfully launched new Chrome instance (Standalone mode)');
152
+
153
+ } catch (error) {
154
+ debugLog(`Failed to connect/launch Chrome: ${error.message}`);
155
+ const errorMsg =
156
+ '❌ Cannot start browser.\n\n' +
157
+ 'To fix this:\n' +
158
+ '1. In Antigravity: Click the Chrome logo (top right) to "Open Browser"\n' +
159
+ '2. Standalone mode: Ensure Playwright is installed:\n' +
160
+ ' npm install playwright\n' +
161
+ ' npx playwright install chromium\n\n' +
162
+ `Error: ${error.message}`;
163
+ throw new Error(errorMsg);
164
+ }
165
+ }
166
+ return { browser, context, page };
167
+ }
168
+
169
+ // MCP Tool definitions
170
+ const tools = [
171
+ {
172
+ name: 'browser_navigate',
173
+ description: 'Navigate to a URL in the browser',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ url: { type: 'string', description: 'The URL to navigate to' }
178
+ },
179
+ required: ['url'],
180
+ additionalProperties: false,
181
+ $schema: 'http://json-schema.org/draft-07/schema#'
182
+ }
183
+ },
184
+ {
185
+ name: 'browser_click',
186
+ description: 'Click an element on the page using Playwright selector',
187
+ inputSchema: {
188
+ type: 'object',
189
+ properties: {
190
+ selector: { type: 'string', description: 'Playwright selector for the element' }
191
+ },
192
+ required: ['selector'],
193
+ additionalProperties: false,
194
+ $schema: 'http://json-schema.org/draft-07/schema#'
195
+ }
196
+ },
197
+ {
198
+ name: 'browser_screenshot',
199
+ description: 'Take a screenshot of the current page',
200
+ inputSchema: {
201
+ type: 'object',
202
+ properties: {
203
+ fullPage: { type: 'boolean', description: 'Capture full page', default: false }
204
+ },
205
+ additionalProperties: false,
206
+ $schema: 'http://json-schema.org/draft-07/schema#'
207
+ }
208
+ },
209
+ {
210
+ name: 'browser_get_text',
211
+ description: 'Get text content from an element',
212
+ inputSchema: {
213
+ type: 'object',
214
+ properties: {
215
+ selector: { type: 'string', description: 'Playwright selector for the element' }
216
+ },
217
+ required: ['selector'],
218
+ additionalProperties: false,
219
+ $schema: 'http://json-schema.org/draft-07/schema#'
220
+ }
221
+ },
222
+ {
223
+ name: 'browser_type',
224
+ description: 'Type text into an input field',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ selector: { type: 'string', description: 'Playwright selector for the input' },
229
+ text: { type: 'string', description: 'Text to type' }
230
+ },
231
+ required: ['selector', 'text'],
232
+ additionalProperties: false,
233
+ $schema: 'http://json-schema.org/draft-07/schema#'
234
+ }
235
+ },
236
+ {
237
+ name: 'browser_evaluate',
238
+ description: 'Execute JavaScript in the browser context',
239
+ inputSchema: {
240
+ type: 'object',
241
+ properties: {
242
+ code: { type: 'string', description: 'JavaScript code to execute' }
243
+ },
244
+ required: ['code'],
245
+ additionalProperties: false,
246
+ $schema: 'http://json-schema.org/draft-07/schema#'
247
+ }
248
+ },
249
+ {
250
+ name: 'browser_wait_for_selector',
251
+ description: 'Wait for an element to appear on the page',
252
+ inputSchema: {
253
+ type: 'object',
254
+ properties: {
255
+ selector: { type: 'string', description: 'Playwright selector to wait for' },
256
+ timeout: { type: 'number', description: 'Timeout in milliseconds', default: 30000 }
257
+ },
258
+ required: ['selector'],
259
+ additionalProperties: false,
260
+ $schema: 'http://json-schema.org/draft-07/schema#'
261
+ }
262
+ },
263
+ {
264
+ name: 'browser_scroll',
265
+ description: 'Scroll the page',
266
+ inputSchema: {
267
+ type: 'object',
268
+ properties: {
269
+ x: { type: 'number', description: 'Horizontal scroll position' },
270
+ y: { type: 'number', description: 'Vertical scroll position' }
271
+ },
272
+ additionalProperties: false,
273
+ $schema: 'http://json-schema.org/draft-07/schema#'
274
+ }
275
+ },
276
+ {
277
+ name: 'browser_resize_window',
278
+ description: 'Resize the browser window (useful for testing responsiveness)',
279
+ inputSchema: {
280
+ type: 'object',
281
+ properties: {
282
+ width: { type: 'number', description: 'Window width in pixels' },
283
+ height: { type: 'number', description: 'Window height in pixels' }
284
+ },
285
+ required: ['width', 'height'],
286
+ additionalProperties: false,
287
+ $schema: 'http://json-schema.org/draft-07/schema#'
288
+ }
289
+ },
290
+ {
291
+ name: 'browser_get_dom',
292
+ description: 'Get the full DOM structure or specific element data',
293
+ inputSchema: {
294
+ type: 'object',
295
+ properties: {
296
+ selector: { type: 'string', description: 'Optional selector to get DOM of specific element' }
297
+ },
298
+ additionalProperties: false,
299
+ $schema: 'http://json-schema.org/draft-07/schema#'
300
+ }
301
+ },
302
+ {
303
+ name: 'browser_start_video_recording',
304
+ description: 'Start recording browser session as video',
305
+ inputSchema: {
306
+ type: 'object',
307
+ properties: {
308
+ path: { type: 'string', description: 'Path to save the video file' }
309
+ },
310
+ additionalProperties: false,
311
+ $schema: 'http://json-schema.org/draft-07/schema#'
312
+ }
313
+ },
314
+ {
315
+ name: 'browser_stop_video_recording',
316
+ description: 'Stop video recording and save the file',
317
+ inputSchema: {
318
+ type: 'object',
319
+ properties: {},
320
+ additionalProperties: false,
321
+ $schema: 'http://json-schema.org/draft-07/schema#'
322
+ }
323
+ },
324
+ {
325
+ name: 'browser_health_check',
326
+ description: 'Check if the browser is running and accessible on port 9222',
327
+ inputSchema: {
328
+ type: 'object',
329
+ properties: {},
330
+ additionalProperties: false,
331
+ $schema: 'http://json-schema.org/draft-07/schema#'
332
+ }
333
+ },
334
+ {
335
+ name: 'browser_console_start',
336
+ description: 'Start capturing browser console logs (console.log, console.error, console.warn, etc.)',
337
+ inputSchema: {
338
+ type: 'object',
339
+ properties: {
340
+ level: {
341
+ type: 'string',
342
+ description: 'Optional filter for log level: "log", "error", "warn", "info", "debug", or "all"',
343
+ enum: ['log', 'error', 'warn', 'info', 'debug', 'all']
344
+ }
345
+ },
346
+ additionalProperties: false,
347
+ $schema: 'http://json-schema.org/draft-07/schema#'
348
+ }
349
+ },
350
+ {
351
+ name: 'browser_console_get',
352
+ description: 'Get all captured console logs since browser_console_start was called',
353
+ inputSchema: {
354
+ type: 'object',
355
+ properties: {
356
+ filter: {
357
+ type: 'string',
358
+ description: 'Optional filter by log level: "log", "error", "warn", "info", "debug", or "all"',
359
+ enum: ['log', 'error', 'warn', 'info', 'debug', 'all']
360
+ }
361
+ },
362
+ additionalProperties: false,
363
+ $schema: 'http://json-schema.org/draft-07/schema#'
364
+ }
365
+ },
366
+ {
367
+ name: 'browser_console_clear',
368
+ description: 'Clear all captured console logs and stop listening',
369
+ inputSchema: {
370
+ type: 'object',
371
+ properties: {},
372
+ additionalProperties: false,
373
+ $schema: 'http://json-schema.org/draft-07/schema#'
374
+ }
375
+ }
376
+ ];
377
+
378
+ // Tool execution handlers
379
+ async function executeTool(name, args) {
380
+ try {
381
+ const { page } = await connectToBrowser();
382
+
383
+ switch (name) {
384
+ case 'browser_navigate':
385
+ await page.goto(args.url, { waitUntil: 'domcontentloaded' });
386
+ return { content: [{ type: 'text', text: `Navigated to ${args.url}` }] };
387
+
388
+ case 'browser_click':
389
+ await page.click(args.selector);
390
+ return { content: [{ type: 'text', text: `Clicked ${args.selector}` }] };
391
+
392
+ case 'browser_screenshot':
393
+ const screenshot = await page.screenshot({
394
+ fullPage: args.fullPage || false,
395
+ type: 'png'
396
+ });
397
+ return {
398
+ content: [{
399
+ type: 'image',
400
+ data: screenshot.toString('base64'),
401
+ mimeType: 'image/png'
402
+ }]
403
+ };
404
+
405
+ case 'browser_get_text':
406
+ const text = await page.textContent(args.selector);
407
+ return { content: [{ type: 'text', text }] };
408
+
409
+ case 'browser_type':
410
+ await page.fill(args.selector, args.text);
411
+ return { content: [{ type: 'text', text: `Typed into ${args.selector}` }] };
412
+
413
+ case 'browser_evaluate':
414
+ const result = await page.evaluate(args.code);
415
+ return {
416
+ content: [{
417
+ type: 'text',
418
+ text: JSON.stringify(result, null, 2)
419
+ }]
420
+ };
421
+
422
+ case 'browser_wait_for_selector':
423
+ await page.waitForSelector(args.selector, {
424
+ timeout: args.timeout || 30000
425
+ });
426
+ return {
427
+ content: [{
428
+ type: 'text',
429
+ text: `Element ${args.selector} appeared`
430
+ }]
431
+ };
432
+
433
+ case 'browser_scroll':
434
+ await page.evaluate(({ x, y }) => {
435
+ window.scrollTo(x || 0, y || 0);
436
+ }, args);
437
+ return {
438
+ content: [{
439
+ type: 'text',
440
+ text: `Scrolled to (${args.x || 0}, ${args.y || 0})`
441
+ }]
442
+ };
443
+
444
+ case 'browser_resize_window':
445
+ await page.setViewportSize({
446
+ width: args.width,
447
+ height: args.height
448
+ });
449
+ return {
450
+ content: [{
451
+ type: 'text',
452
+ text: `Resized window to ${args.width}x${args.height}`
453
+ }]
454
+ };
455
+
456
+ case 'browser_get_dom':
457
+ const domContent = await page.evaluate((sel) => {
458
+ const element = sel ? document.querySelector(sel) : document.documentElement;
459
+ if (!element) return null;
460
+ return {
461
+ outerHTML: element.outerHTML,
462
+ textContent: element.textContent,
463
+ attributes: Array.from(element.attributes || []).map(attr => ({
464
+ name: attr.name,
465
+ value: attr.value
466
+ })),
467
+ children: element.children.length
468
+ };
469
+ }, args.selector);
470
+ return {
471
+ content: [{
472
+ type: 'text',
473
+ text: JSON.stringify(domContent, null, 2)
474
+ }]
475
+ };
476
+
477
+ case 'browser_start_video_recording':
478
+ const videoPath = args.path || `${os.tmpdir()}/browser-recording-${Date.now()}.webm`;
479
+ await context.tracing.start({
480
+ screenshots: true,
481
+ snapshots: true
482
+ });
483
+ // Start video recording using Playwright's video feature
484
+ if (!context._options.recordVideo) {
485
+ // Note: Video recording needs to be set when creating context
486
+ // For existing context, we'll use screenshots as fallback
487
+ return {
488
+ content: [{
489
+ type: 'text',
490
+ text: 'Started session tracing (screenshots). For full video, context needs recordVideo option at creation.'
491
+ }]
492
+ };
493
+ }
494
+ return {
495
+ content: [{
496
+ type: 'text',
497
+ text: `Started video recording to ${videoPath}`
498
+ }]
499
+ };
500
+
501
+ case 'browser_stop_video_recording':
502
+ const tracePath = `${os.tmpdir()}/trace-${Date.now()}.zip`;
503
+ await context.tracing.stop({ path: tracePath });
504
+ return {
505
+ content: [{
506
+ type: 'text',
507
+ text: `Stopped recording. Trace saved to ${tracePath}. Use 'playwright show-trace ${tracePath}' to view.`
508
+ }]
509
+ };
510
+
511
+ case 'browser_health_check':
512
+ // Connection already succeeded if we got here
513
+ const url = await page.url();
514
+
515
+ // Detect mode: connected to existing Chrome or launched our own
516
+ const isConnected = browser.isConnected && browser.isConnected();
517
+ const mode = isConnected ? 'Connected to existing Chrome (Antigravity)' : 'Launched standalone Chrome';
518
+
519
+ // Determine profile path based on mode
520
+ let browserProfile;
521
+ if (isConnected) {
522
+ browserProfile = `${process.env.HOME}/.gemini/antigravity-browser-profile`;
523
+ } else {
524
+ browserProfile = process.env.MCP_BROWSER_PROFILE || `${os.tmpdir()}/chrome-mcp-profile`;
525
+ }
526
+
527
+ return {
528
+ content: [{
529
+ type: 'text',
530
+ text: `✅ Browser automation is fully functional!\n\n` +
531
+ `Mode: ${mode}\n` +
532
+ `✅ Playwright: ${playwrightPath || 'loaded'}\n` +
533
+ `✅ Chrome: Port 9222\n` +
534
+ `✅ Profile: ${browserProfile}\n` +
535
+ `✅ Current page: ${url}\n\n` +
536
+ `All 16 browser tools are ready to use!`
537
+ }]
538
+ };
539
+
540
+ case 'browser_console_start':
541
+ if (!consoleListening) {
542
+ page.on('console', msg => {
543
+ const logEntry = {
544
+ type: msg.type(),
545
+ text: msg.text(),
546
+ timestamp: new Date().toISOString(),
547
+ location: msg.location()
548
+ };
549
+ consoleLogs.push(logEntry);
550
+ debugLog(`Console [${logEntry.type}]: ${logEntry.text}`);
551
+ });
552
+ consoleListening = true;
553
+ debugLog('Console logging started');
554
+ }
555
+ return {
556
+ content: [{
557
+ type: 'text',
558
+ 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.`
559
+ }]
560
+ };
561
+
562
+ case 'browser_console_get':
563
+ const filter = args.filter;
564
+ const filtered = filter && filter !== 'all'
565
+ ? consoleLogs.filter(log => log.type === filter)
566
+ : consoleLogs;
567
+
568
+ if (filtered.length === 0) {
569
+ return {
570
+ content: [{
571
+ type: 'text',
572
+ text: consoleListening
573
+ ? `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.`
574
+ : `Console logging is not active.\n\nUse browser_console_start to begin capturing logs.`
575
+ }]
576
+ };
577
+ }
578
+
579
+ const logSummary = `📋 Captured ${filtered.length} console log${filtered.length === 1 ? '' : 's'}${filter && filter !== 'all' ? ` (filtered by: ${filter})` : ''}:\n\n`;
580
+ const formattedLogs = filtered.map((log, i) => {
581
+ const icon = {
582
+ 'error': '❌',
583
+ 'warn': '⚠️',
584
+ 'log': '📝',
585
+ 'info': 'ℹ️',
586
+ 'debug': '🔍'
587
+ }[log.type] || '📄';
588
+
589
+ return `${i + 1}. ${icon} [${log.type.toUpperCase()}] ${log.timestamp}\n ${log.text}${log.location.url ? `\n Location: ${log.location.url}:${log.location.lineNumber}` : ''}`;
590
+ }).join('\n\n');
591
+
592
+ return {
593
+ content: [{
594
+ type: 'text',
595
+ text: logSummary + formattedLogs
596
+ }]
597
+ };
598
+
599
+ case 'browser_console_clear':
600
+ const count = consoleLogs.length;
601
+ consoleLogs = [];
602
+ if (consoleListening) {
603
+ page.removeAllListeners('console');
604
+ consoleListening = false;
605
+ }
606
+ debugLog(`Cleared ${count} console logs and stopped listening`);
607
+ return {
608
+ content: [{
609
+ type: 'text',
610
+ text: `✅ Cleared ${count} console log${count === 1 ? '' : 's'} and stopped listening.\n\nUse browser_console_start to resume capturing.`
611
+ }]
612
+ };
613
+
614
+ default:
615
+ throw new Error(`Unknown tool: ${name}`);
616
+ }
617
+ } catch (error) {
618
+ debugLog(`Tool execution error (${name}): ${error.message}`);
619
+ return {
620
+ content: [{
621
+ type: 'text',
622
+ text: `❌ Error executing ${name}: ${error.message}`
623
+ }],
624
+ isError: true
625
+ };
626
+ }
627
+ }
628
+
629
+ // MCP Protocol handler
630
+ rl.on('line', async (line) => {
631
+ let request;
632
+ try {
633
+ debugLog(`Received: ${line.substring(0, 200)}`);
634
+ request = JSON.parse(line);
635
+
636
+ if (request.method === 'initialize') {
637
+ debugLog(`Initialize with protocol: ${request.params.protocolVersion}`);
638
+ respond(request.id, {
639
+ protocolVersion: request.params.protocolVersion || '2024-11-05',
640
+ capabilities: { tools: {} },
641
+ serverInfo: {
642
+ name: 'browser-automation-playwright',
643
+ version: '1.0.0'
644
+ }
645
+ });
646
+ } else if (request.method === 'notifications/initialized') {
647
+ // This is a notification - no response needed
648
+ debugLog('Received initialized notification');
649
+ } else if (request.method === 'tools/list') {
650
+ debugLog('Sending tools list');
651
+ respond(request.id, { tools });
652
+ } else if (request.method === 'tools/call') {
653
+ debugLog(`Calling tool: ${request.params.name}`);
654
+ const result = await executeTool(request.params.name, request.params.arguments || {});
655
+ respond(request.id, result);
656
+ } else {
657
+ debugLog(`Unknown method: ${request.method}`);
658
+ respond(request.id, null, { code: -32601, message: 'Method not found' });
659
+ }
660
+ } catch (error) {
661
+ debugLog(`Error processing request: ${error.message}`);
662
+ console.error('Error processing request:', error.message, 'Request:', line);
663
+ const id = request?.id || null;
664
+ respond(id, null, { code: -32603, message: error.message });
665
+ }
666
+ });
667
+
668
+ function respond(id, result, error = null) {
669
+ const response = { jsonrpc: '2.0', id };
670
+ if (error) response.error = error;
671
+ else response.result = result;
672
+ console.log(JSON.stringify(response));
673
+ }
674
+
675
+ // Cleanup on exit
676
+ process.on('SIGTERM', async () => {
677
+ if (browser) await browser.close();
678
+ process.exit(0);
679
+ });
680
+
681
+ process.on('SIGINT', async () => {
682
+ if (browser) await browser.close();
683
+ process.exit(0);
684
+ });
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@ricardodeazambuja/browser-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "Universal browser automation MCP server using Playwright. Works with Antigravity, Claude Desktop, and any MCP client.",
5
+ "main": "browser-mcp-server-playwright.js",
6
+ "bin": {
7
+ "browser-mcp-server": "./browser-mcp-server-playwright.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node browser-mcp-server-playwright.js",
11
+ "install-browsers": "npx playwright install chromium",
12
+ "test": "echo 'Testing MCP server...' && echo '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"test\",\"version\":\"1.0.0\"}}}' | node browser-mcp-server-playwright.js"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "mcp-server",
17
+ "model-context-protocol",
18
+ "browser-automation",
19
+ "playwright",
20
+ "claude",
21
+ "antigravity",
22
+ "web-automation",
23
+ "testing",
24
+ "scraping"
25
+ ],
26
+ "author": "Ricardo de Azambuja (https://ricardodeazambuja.com)",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/ricardodeazambuja/browser-mcp-server.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/ricardodeazambuja/browser-mcp-server/issues"
34
+ },
35
+ "homepage": "https://github.com/ricardodeazambuja/browser-mcp-server#readme",
36
+ "engines": {
37
+ "node": ">=16.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "playwright": "^1.40.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "playwright": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "files": [
48
+ "browser-mcp-server-playwright.js",
49
+ "README.md",
50
+ "LICENSE"
51
+ ]
52
+ }