@ricardodeazambuja/browser-mcp-server 1.0.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG-v1.3.0.md +42 -0
  2. package/CHANGELOG-v1.4.0.md +8 -0
  3. package/README.md +271 -45
  4. package/package.json +11 -10
  5. package/plugins/.gitkeep +0 -0
  6. package/src/.gitkeep +0 -0
  7. package/src/browser.js +152 -0
  8. package/src/cdp.js +58 -0
  9. package/src/index.js +126 -0
  10. package/src/tools/.gitkeep +0 -0
  11. package/src/tools/console.js +139 -0
  12. package/src/tools/docs.js +1611 -0
  13. package/src/tools/index.js +60 -0
  14. package/src/tools/info.js +139 -0
  15. package/src/tools/interaction.js +126 -0
  16. package/src/tools/keyboard.js +27 -0
  17. package/src/tools/media.js +264 -0
  18. package/src/tools/mouse.js +104 -0
  19. package/src/tools/navigation.js +72 -0
  20. package/src/tools/network.js +552 -0
  21. package/src/tools/pages.js +149 -0
  22. package/src/tools/performance.js +517 -0
  23. package/src/tools/security.js +470 -0
  24. package/src/tools/storage.js +467 -0
  25. package/src/tools/system.js +196 -0
  26. package/src/utils.js +131 -0
  27. package/tests/.gitkeep +0 -0
  28. package/tests/fixtures/.gitkeep +0 -0
  29. package/tests/fixtures/test-media.html +35 -0
  30. package/tests/fixtures/test-network.html +48 -0
  31. package/tests/fixtures/test-performance.html +61 -0
  32. package/tests/fixtures/test-security.html +33 -0
  33. package/tests/fixtures/test-storage.html +76 -0
  34. package/tests/run-all.js +50 -0
  35. package/{test-browser-automation.js → tests/test-browser-automation.js} +44 -5
  36. package/{test-mcp.js → tests/test-mcp.js} +9 -4
  37. package/tests/test-media-tools.js +168 -0
  38. package/tests/test-network.js +212 -0
  39. package/tests/test-performance.js +254 -0
  40. package/tests/test-security.js +203 -0
  41. package/tests/test-storage.js +192 -0
  42. package/CHANGELOG-v1.0.2.md +0 -126
  43. package/browser-mcp-server-playwright.js +0 -792
package/src/browser.js ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Browser connection and state management
3
+ */
4
+
5
+ const os = require('os');
6
+ const { debugLog, loadPlaywright, findChromeExecutable } = require('./utils');
7
+ const { resetCDPSession } = require('./cdp');
8
+
9
+ // Browser state
10
+ let browser = null;
11
+ let context = null;
12
+ let page = null;
13
+ let activePageIndex = 0;
14
+
15
+ /**
16
+ * Connect to existing Chrome or launch new instance (hybrid mode)
17
+ * @returns {Object} { browser, context, page }
18
+ */
19
+ async function connectToBrowser() {
20
+ // Check if browser is disconnected or closed
21
+ if (browser && (!browser.isConnected || !browser.isConnected())) {
22
+ debugLog('Browser connection lost, resetting...');
23
+ browser = null;
24
+ context = null;
25
+ page = null;
26
+ }
27
+
28
+ if (!browser) {
29
+ try {
30
+ const pw = loadPlaywright();
31
+
32
+ // STRATEGY 1: Try to connect to existing Chrome (Antigravity mode)
33
+ try {
34
+ debugLog('Attempting to connect to Chrome on port 9222...');
35
+ browser = await pw.chromium.connectOverCDP('http://localhost:9222');
36
+ debugLog('✅ Connected to existing Chrome (Antigravity mode)');
37
+
38
+ const contexts = browser.contexts();
39
+ context = contexts.length > 0 ? contexts[0] : await browser.newContext();
40
+ } catch (connectError) {
41
+ debugLog(`Could not connect to existing Chrome: ${connectError.message}`);
42
+ }
43
+
44
+ // STRATEGY 2: Launch our own Chrome (Standalone mode)
45
+ if (!browser) {
46
+ debugLog('No existing Chrome found. Launching new instance...');
47
+
48
+ const profileDir = process.env.MCP_BROWSER_PROFILE ||
49
+ `${os.tmpdir()}/chrome-mcp-profile`;
50
+
51
+ debugLog(`Browser profile: ${profileDir}`);
52
+
53
+ const chromeExecutable = findChromeExecutable();
54
+ const launchOptions = {
55
+ headless: false,
56
+ args: [
57
+ '--remote-debugging-port=9222',
58
+ '--no-first-run',
59
+ '--no-default-browser-check',
60
+ '--disable-fre',
61
+ '--disable-features=TranslateUI,OptGuideOnDeviceModel',
62
+ '--disable-sync',
63
+ '--disable-component-update',
64
+ '--disable-background-networking',
65
+ '--disable-breakpad',
66
+ '--disable-background-timer-throttling',
67
+ '--disable-backgrounding-occluded-windows',
68
+ '--disable-renderer-backgrounding'
69
+ ]
70
+ };
71
+
72
+ if (chromeExecutable) {
73
+ debugLog(`Using system Chrome/Chromium: ${chromeExecutable}`);
74
+ launchOptions.executablePath = chromeExecutable;
75
+ } else {
76
+ debugLog('No system Chrome/Chromium found. Attempting to use Playwright Chromium...');
77
+ }
78
+
79
+ try {
80
+ context = await pw.chromium.launchPersistentContext(profileDir, launchOptions);
81
+ browser = context;
82
+ } catch (launchError) {
83
+ if (!chromeExecutable && launchError.message.includes("Executable doesn't exist")) {
84
+ debugLog('Playwright Chromium not installed and no system Chrome found');
85
+ throw new Error(
86
+ '❌ No Chrome/Chromium browser found!\n\n' +
87
+ 'This MCP server needs a Chrome or Chromium browser to work.\n\n' +
88
+ 'Option 1 - Install Chrome/Chromium on your system\n' +
89
+ 'Option 2 - Install Playwright\'s Chromium: npx playwright install chromium\n' +
90
+ 'Option 3 - Use with Antigravity: Open browser via Chrome logo\n'
91
+ );
92
+ }
93
+ throw launchError;
94
+ }
95
+ debugLog('✅ Successfully launched new Chrome instance (Standalone mode)');
96
+ }
97
+
98
+ } catch (error) {
99
+ debugLog(`Failed to connect/launch Chrome: ${error.message}`);
100
+ throw error;
101
+ }
102
+ }
103
+
104
+ // Ensure we have a context and page
105
+ if (!context) {
106
+ const contexts = browser.contexts();
107
+ context = contexts.length > 0 ? contexts[0] : await browser.newContext();
108
+ }
109
+
110
+ const pages = context.pages();
111
+ if (pages.length === 0) {
112
+ page = await context.newPage();
113
+ activePageIndex = 0;
114
+ } else {
115
+ if (activePageIndex >= pages.length) activePageIndex = pages.length - 1;
116
+ page = pages[activePageIndex];
117
+ }
118
+
119
+ return { browser, context, page };
120
+ }
121
+
122
+ /**
123
+ * Get browser state
124
+ */
125
+ function getBrowserState() {
126
+ return { browser, context, page, activePageIndex };
127
+ }
128
+
129
+ /**
130
+ * Set active page index
131
+ */
132
+ function setActivePageIndex(index) {
133
+ activePageIndex = index;
134
+ }
135
+
136
+ /**
137
+ * Reset browser state
138
+ */
139
+ function resetBrowserState() {
140
+ browser = null;
141
+ context = null;
142
+ page = null;
143
+ activePageIndex = 0;
144
+ resetCDPSession();
145
+ }
146
+
147
+ module.exports = {
148
+ connectToBrowser,
149
+ getBrowserState,
150
+ setActivePageIndex,
151
+ resetBrowserState
152
+ };
package/src/cdp.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * CDP (Chrome DevTools Protocol) Session Manager
3
+ * Manages CDP session lifecycle for advanced browser automation tools
4
+ */
5
+
6
+ const { debugLog } = require('./utils');
7
+
8
+ // CDP session state
9
+ let cdpSession = null;
10
+ let currentPage = null;
11
+
12
+ /**
13
+ * Get or create CDP session for current page
14
+ * Sessions are cached per page instance for efficiency
15
+ * @returns {CDPSession} Active CDP session
16
+ */
17
+ async function getCDPSession() {
18
+ // Lazy-load to avoid circular dependency with browser.js
19
+ const { connectToBrowser } = require('./browser');
20
+ const { page } = await connectToBrowser();
21
+
22
+ // If page changed or session doesn't exist, create new session
23
+ if (!cdpSession || currentPage !== page) {
24
+ // Detach old session if it exists
25
+ if (cdpSession) {
26
+ try {
27
+ await cdpSession.detach();
28
+ debugLog('Detached old CDP session');
29
+ } catch (e) {
30
+ debugLog(`Failed to detach old CDP session: ${e.message}`);
31
+ }
32
+ }
33
+
34
+ // Create new session for current page
35
+ currentPage = page;
36
+ cdpSession = await page.context().newCDPSession(page);
37
+ debugLog('Created new CDP session');
38
+ }
39
+
40
+ return cdpSession;
41
+ }
42
+
43
+ /**
44
+ * Reset CDP session state
45
+ * Called when browser state is reset (connection lost, browser restart, etc.)
46
+ */
47
+ function resetCDPSession() {
48
+ if (cdpSession) {
49
+ debugLog('Resetting CDP session state');
50
+ }
51
+ cdpSession = null;
52
+ currentPage = null;
53
+ }
54
+
55
+ module.exports = {
56
+ getCDPSession,
57
+ resetCDPSession
58
+ };
package/src/index.js ADDED
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Universal Browser Automation MCP Server (Playwright Edition)
5
+ * Main Entry Point
6
+ */
7
+
8
+ const readline = require('readline');
9
+ const { debugLog, version } = require('./utils');
10
+ const { tools, handlers } = require('./tools');
11
+ const { getBrowserState } = require('./browser');
12
+
13
+ class BrowserMCPServer {
14
+ constructor() {
15
+ this.rl = readline.createInterface({
16
+ input: process.stdin,
17
+ output: process.stdout,
18
+ terminal: false
19
+ });
20
+
21
+ this.init();
22
+ }
23
+
24
+ init() {
25
+ debugLog('Server starting via src/index.js...');
26
+
27
+ // Process stdin lines as MCP requests
28
+ this.rl.on('line', (line) => this.handleLine(line));
29
+
30
+ // Handle cleanup
31
+ process.on('SIGTERM', () => this.cleanup());
32
+ process.on('SIGINT', () => this.cleanup());
33
+ }
34
+
35
+ async handleLine(line) {
36
+ let request;
37
+ try {
38
+ debugLog(`Received: ${line.substring(0, 200)}`);
39
+ request = JSON.parse(line);
40
+
41
+ if (request.method === 'initialize') {
42
+ this.handleInitialize(request);
43
+ } else if (request.method === 'notifications/initialized') {
44
+ debugLog('Received initialized notification');
45
+ } else if (request.method === 'tools/list') {
46
+ this.handleToolsList(request);
47
+ } else if (request.method === 'tools/call') {
48
+ await this.handleToolCall(request);
49
+ } else {
50
+ debugLog(`Unknown method: ${request.method}`);
51
+ this.respond(request.id, null, { code: -32601, message: 'Method not found' });
52
+ }
53
+ } catch (error) {
54
+ debugLog(`Error processing request: ${error.message}`);
55
+ // Only log to stderr if it's not a JSON parse error (which would break MCP)
56
+ // Actually, stderr is fine, just don't write to stdout unless it's a JSON-RPC response
57
+ const id = request?.id || null;
58
+ this.respond(id, null, { code: -32603, message: error.message });
59
+ }
60
+ }
61
+
62
+ handleInitialize(request) {
63
+ debugLog(`Initialize with protocol: ${request.params.protocolVersion}`);
64
+ this.respond(request.id, {
65
+ protocolVersion: request.params.protocolVersion || '2024-11-05',
66
+ capabilities: { tools: {} },
67
+ serverInfo: {
68
+ name: 'browser-automation-playwright',
69
+ version: version
70
+ }
71
+ });
72
+ }
73
+
74
+ handleToolsList(request) {
75
+ debugLog('Sending tools list');
76
+ this.respond(request.id, { tools });
77
+ }
78
+
79
+ async handleToolCall(request) {
80
+ debugLog(`Calling tool: ${request.params.name}`);
81
+ const result = await this.executeTool(request.params.name, request.params.arguments || {});
82
+ this.respond(request.id, result);
83
+ }
84
+
85
+ async executeTool(name, args) {
86
+ try {
87
+ const handler = handlers[name];
88
+ if (!handler) {
89
+ throw new Error(`Unknown tool: ${name}`);
90
+ }
91
+ return await handler(args);
92
+ } catch (error) {
93
+ debugLog(`Tool execution error (${name}): ${error.message}`);
94
+ return {
95
+ content: [{
96
+ type: 'text',
97
+ text: `❌ Error executing ${name}: ${error.message}`
98
+ }],
99
+ isError: true
100
+ };
101
+ }
102
+ }
103
+
104
+ respond(id, result, error = null) {
105
+ const response = { jsonrpc: '2.0', id };
106
+ if (error) response.error = error;
107
+ else response.result = result;
108
+ console.log(JSON.stringify(response));
109
+ }
110
+
111
+ async cleanup() {
112
+ const { browser } = getBrowserState();
113
+ if (browser) {
114
+ debugLog('Closing browser on exit...');
115
+ await browser.close().catch(() => { });
116
+ }
117
+ process.exit(0);
118
+ }
119
+ }
120
+
121
+ // Start the server if this file is run directly
122
+ if (require.main === module) {
123
+ new BrowserMCPServer();
124
+ }
125
+
126
+ module.exports = { BrowserMCPServer };
File without changes
@@ -0,0 +1,139 @@
1
+ const { connectToBrowser } = require('../browser');
2
+ const { debugLog } = require('../utils');
3
+
4
+ // Local state for console tool
5
+ let consoleLogs = [];
6
+ let consoleListening = false;
7
+
8
+ const definitions = [
9
+ {
10
+ name: 'browser_console_start',
11
+ description: 'Start capturing browser console logs (console.log, console.error, console.warn, etc.) (see browser_docs)',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ level: {
16
+ type: 'string',
17
+ description: 'Optional filter for log level: "log", "error", "warn", "info", "debug", or "all"',
18
+ enum: ['log', 'error', 'warn', 'info', 'debug', 'all']
19
+ }
20
+ },
21
+ additionalProperties: false,
22
+ $schema: 'http://json-schema.org/draft-07/schema#'
23
+ }
24
+ },
25
+ {
26
+ name: 'browser_console_get',
27
+ description: 'Get all captured console logs since browser_console_start was called (see browser_docs)',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ filter: {
32
+ type: 'string',
33
+ description: 'Optional filter by log level: "log", "error", "warn", "info", "debug", or "all"',
34
+ enum: ['log', 'error', 'warn', 'info', 'debug', 'all']
35
+ }
36
+ },
37
+ additionalProperties: false,
38
+ $schema: 'http://json-schema.org/draft-07/schema#'
39
+ }
40
+ },
41
+ {
42
+ name: 'browser_console_clear',
43
+ description: 'Clear all captured console logs and stop listening (see browser_docs)',
44
+ inputSchema: {
45
+ type: 'object',
46
+ properties: {},
47
+ additionalProperties: false,
48
+ $schema: 'http://json-schema.org/draft-07/schema#'
49
+ }
50
+ }
51
+ ];
52
+
53
+ const handlers = {
54
+ browser_console_start: async (args) => {
55
+ const { page } = await connectToBrowser();
56
+ if (!consoleListening) {
57
+ page.on('console', msg => {
58
+ const logEntry = {
59
+ type: msg.type(),
60
+ text: msg.text(),
61
+ timestamp: new Date().toISOString(),
62
+ location: msg.location()
63
+ };
64
+ consoleLogs.push(logEntry);
65
+ debugLog(`Console [${logEntry.type}]: ${logEntry.text}`);
66
+ });
67
+ consoleListening = true;
68
+ debugLog('Console logging started');
69
+ }
70
+ return {
71
+ content: [{
72
+ type: 'text',
73
+ 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.`
74
+ }]
75
+ };
76
+ },
77
+
78
+ browser_console_get: async (args) => {
79
+ const filter = args.filter;
80
+ const filtered = filter && filter !== 'all'
81
+ ? consoleLogs.filter(log => log.type === filter)
82
+ : consoleLogs;
83
+
84
+ if (filtered.length === 0) {
85
+ return {
86
+ content: [{
87
+ type: 'text',
88
+ text: consoleListening
89
+ ? `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.`
90
+ : `Console logging is not active.\n\nUse browser_console_start to begin capturing logs.`
91
+ }]
92
+ };
93
+ }
94
+
95
+ const logSummary = `📋 Captured ${filtered.length} console log${filtered.length === 1 ? '' : 's'}${filter && filter !== 'all' ? ` (filtered by: ${filter})` : ''}:\n\n`;
96
+ const formattedLogs = filtered.map((log, i) => {
97
+ const icon = {
98
+ 'error': '❌',
99
+ 'warn': '⚠️',
100
+ 'log': '📝',
101
+ 'info': 'ℹ️',
102
+ 'debug': '🔍'
103
+ }[log.type] || '📄';
104
+
105
+ return `${i + 1}. ${icon} [${log.type.toUpperCase()}] ${log.timestamp}\n ${log.text}${log.location.url ? `\n Location: ${log.location.url}:${log.location.lineNumber}` : ''}`;
106
+ }).join('\n\n');
107
+
108
+ return {
109
+ content: [{
110
+ type: 'text',
111
+ text: logSummary + formattedLogs
112
+ }]
113
+ };
114
+ },
115
+
116
+ browser_console_clear: async (args) => {
117
+ const { page } = await connectToBrowser();
118
+ const count = consoleLogs.length;
119
+ consoleLogs = [];
120
+ if (consoleListening) {
121
+ // Removing listeners is tricky if we don't store the reference to the specific function we passed
122
+ // But page.removeAllListeners('console') is cleaner if we are the only one using it.
123
+ // In this server context, we likely are.
124
+ if (page) {
125
+ page.removeAllListeners('console');
126
+ }
127
+ consoleListening = false;
128
+ }
129
+ debugLog(`Cleared ${count} console logs and stopped listening`);
130
+ return {
131
+ content: [{
132
+ type: 'text',
133
+ text: `✅ Cleared ${count} console log${count === 1 ? '' : 's'} and stopped listening.\n\nUse browser_console_start to resume capturing.`
134
+ }]
135
+ };
136
+ }
137
+ };
138
+
139
+ module.exports = { definitions, handlers };