@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.
- package/CHANGELOG-v1.0.2.md +126 -0
- package/LICENSE +21 -0
- package/README.md +596 -0
- package/browser-mcp-server-playwright.js +792 -0
- package/package.json +50 -0
- package/test-browser-automation.js +189 -0
- package/test-mcp.js +150 -0
|
@@ -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
|
+
});
|