mcpbrowser 0.3.18 → 0.3.19

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/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  > ⚠️ **Security Notice:** MCPBrowser extracts webpage content and provides it to your AI agent (e.g., GitHub Copilot, Claude), which then sends it to the LLM provider it uses (e.g., Anthropic, OpenAI, GitHub) for processing. Make sure you trust both your agent and the LLM provider — especially when accessing pages with sensitive or private data.
9
9
 
10
- **MCPBrowser is an MCP browser server that gives AI assistants the ability to browse web pages using a real Chrome or Edge browser.** This browser-based MCP server fetches any web page — especially those protected by authentication, CAPTCHAs, anti-bot protection, or requiring JavaScript rendering. Uses your real Chrome/Edge browser for web automation so you can log in normally, then automatically extracts content. Works with corporate SSO, login forms, Cloudflare, and JavaScript-heavy sites (SPAs, dashboards).
10
+ **MCPBrowser is an MCP browser server that gives AI assistants the ability to browse web pages using a real Chrome, Edge, or Brave browser.** This browser-based MCP server fetches any web page — especially those protected by authentication, CAPTCHAs, anti-bot protection, or requiring JavaScript rendering. Uses your real browser for web automation so you can log in normally, then automatically extracts content. Works with corporate SSO, login forms, Cloudflare, and JavaScript-heavy sites (SPAs, dashboards).
11
11
 
12
12
  This is an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server using [stdio transport](https://modelcontextprotocol.io/docs/concepts/transports#stdio). Your AI assistant uses this web browser MCP server when standard HTTP requests fail — pages requiring authentication, CAPTCHA protection, or heavy JavaScript (SPAs). Once connected, the browser MCP server can navigate through websites, interact with elements, and send HTML back to the AI assistant. This gives your AI the ability to browse the web just like you do.
13
13
 
@@ -15,10 +15,9 @@ Example workflow for AI assistant to use MCPBrowser
15
15
 
16
16
  ```
17
17
  1. fetch_webpage → Load the login page
18
- 2. type_text → Enter username
19
- 3. type_text Enter password
20
- 4. click_element Click "Sign In"
21
- 5. get_current_html → Extract the content after login
18
+ 2. type_text → Enter username and password (multiple fields at once)
19
+ 3. click_element Click "Sign In"
20
+ 4. get_current_html Extract the content after login
22
21
  ```
23
22
 
24
23
 
@@ -35,6 +34,7 @@ Example workflow for AI assistant to use MCPBrowser
35
34
  - [click_element](#click_element)
36
35
  - [type_text](#type_text)
37
36
  - [get_current_html](#get_current_html)
37
+ - [scroll_page](#scroll_page)
38
38
  - [take_screenshot](#take_screenshot)
39
39
  - [close_tab](#close_tab)
40
40
  - [Configuration](#configuration-optional)
@@ -43,7 +43,7 @@ Example workflow for AI assistant to use MCPBrowser
43
43
 
44
44
  ## Requirements
45
45
 
46
- - Chrome or Edge browser
46
+ - Chrome, Edge, or Brave browser
47
47
  - [Node.js 18+](https://nodejs.org/) (includes npm)
48
48
 
49
49
  > **Note:** Node.js must be installed on your system. The VS Code extension and npm package both require Node.js to run the MCP server. Download from [nodejs.org](https://nodejs.org/) if not already installed.
@@ -54,8 +54,9 @@ Example workflow for AI assistant to use MCPBrowser
54
54
  |---|----------|------------|
55
55
  | 1 | [VS Code Extension](#option-1-vs-code-extension) | One Click |
56
56
  | 2 | [Claude Code](#option-2-claude-code) | One Command |
57
- | 3 | [Claude Desktop](#option-3-claude-desktop) | Manual |
58
- | 4 | [npm Package](#option-4-npm-package) | Manual |
57
+ | 3 | [OpenClaw](#option-3-openclaw) | One Command |
58
+ | 4 | [Claude Desktop](#option-4-claude-desktop) | Manual |
59
+ | 5 | [npm Package](#option-5-npm-package) | Manual |
59
60
 
60
61
  ### Option 1: VS Code Extension
61
62
 
@@ -85,7 +86,22 @@ mcpbrowser: npx -y mcpbrowser@latest - ✓ Connected
85
86
  That's it! Ask Claude to fetch any protected page:
86
87
  > "Fetch https://portal.azure.com using mcpbrowser"
87
88
 
88
- ### Option 3: Claude Desktop
89
+ ### Option 3: OpenClaw
90
+
91
+ [OpenClaw](https://openclaw.ai/) is a personal AI assistant that runs on your devices. Add MCPBrowser to give it browser automation capabilities:
92
+
93
+ ```bash
94
+ openclaw mcp add mcpbrowser -- npx -y mcpbrowser@latest
95
+ ```
96
+
97
+ Verify it's working:
98
+ ```bash
99
+ openclaw mcp list
100
+ ```
101
+
102
+ Now OpenClaw can browse authenticated pages, fill forms, and interact with web apps using your existing browser sessions.
103
+
104
+ ### Option 4: Claude Desktop
89
105
 
90
106
  Add to your config file:
91
107
 
@@ -105,7 +121,7 @@ Add to your config file:
105
121
 
106
122
  Restart Claude Desktop after saving.
107
123
 
108
- ### Option 4: npm Package
124
+ ### Option 5: npm Package
109
125
 
110
126
  For VS Code (GitHub Copilot) manual setup, add to your `mcp.json`:
111
127
 
@@ -183,36 +199,55 @@ Clicks on any clickable element (buttons, links, divs with onclick handlers, etc
183
199
 
184
200
  ### `type_text`
185
201
 
186
- Types text into an input field, textarea, or other editable element. Simulates human-like typing with configurable delay between keystrokes. Automatically clears existing text by default.
202
+ Types text into one or more input fields in a single call. Supports filling entire forms at once for efficient automation. Automatically clears existing text by default.
187
203
 
188
204
  **⚠️ Note:** Page must be already loaded via `fetch_webpage` first.
189
205
 
190
206
  **Parameters:**
191
207
  - `url` (string, required) - The URL of the page (must match a previously fetched page)
192
- - `selector` (string, required) - CSS selector for the input element (e.g., `#username`, `input[name="email"]`)
193
- - `text` (string, required) - Text to type into the field
194
- - `clear` (boolean, optional, default: `true`) - Whether to clear existing text first
195
- - `typeDelay` (number, optional, default: `50`) - Delay between keystrokes in milliseconds (simulates human typing)
208
+ - `fields` (array, required) - Array of fields to fill. Each field object contains:
209
+ - `selector` (string, required) - CSS selector for the input element (e.g., `#username`, `input[name="email"]`)
210
+ - `text` (string, required) - Text to type into the field
211
+ - `clear` (boolean, optional, default: `true`) - Whether to clear existing text first
212
+ - `waitForElementTimeout` (number, optional, default: `5000`) - Maximum time to wait for element in milliseconds
196
213
  - `returnHtml` (boolean, optional, default: `true`) - Whether to wait for stability and return HTML after typing
197
214
  - `removeUnnecessaryHTML` (boolean, optional, default: `true`) - Remove unnecessary HTML for size reduction. Only used when `returnHtml` is `true`
198
215
  - `postTypeWait` (number, optional, default: `1000`) - Milliseconds to wait after typing for SPAs to render dynamic content
199
- - `waitForElementTimeout` (number, optional, default: `5000`) - Maximum time to wait for element in milliseconds
200
216
 
201
217
  **Examples:**
202
218
  ```javascript
203
- // Basic text input
204
- { url: "https://example.com", selector: "#email", text: "user@example.com" }
205
-
206
- // Append text without clearing
207
- { url: "https://example.com", selector: "#search", text: " advanced", clear: false }
219
+ // Fill multiple fields at once (login form)
220
+ {
221
+ url: "https://example.com/login",
222
+ fields: [
223
+ { selector: "#username", text: "john@example.com" },
224
+ { selector: "#password", text: "secretpass123" }
225
+ ]
226
+ }
208
227
 
209
- // Fast typing without human simulation
210
- { url: "https://example.com", selector: "#username", text: "john", typeDelay: 0 }
228
+ // Single field input
229
+ { url: "https://example.com", fields: [{ selector: "#search", text: "query" }] }
211
230
 
212
- // Type without waiting for HTML return (faster)
213
- { url: "https://example.com", selector: "#field", text: "value", returnHtml: false }
231
+ // Append text without clearing
232
+ { url: "https://example.com", fields: [{ selector: "#notes", text: " additional text", clear: false }] }
233
+
234
+ // Fast form fill without HTML return
235
+ {
236
+ url: "https://example.com/signup",
237
+ fields: [
238
+ { selector: "#firstName", text: "John" },
239
+ { selector: "#lastName", text: "Doe" },
240
+ { selector: "#email", text: "john@example.com" }
241
+ ],
242
+ returnHtml: false
243
+ }
214
244
  ```
215
245
 
246
+ **Error handling:** If a field fails, the response indicates:
247
+ - Which field number failed (e.g., "Failed on field 2 of 3")
248
+ - Which fields were successfully filled
249
+ - Clear guidance to NOT re-type already filled fields
250
+
216
251
  ---
217
252
 
218
253
  ### `get_current_html`
@@ -240,6 +275,46 @@ Gets the current HTML from an already-loaded page **WITHOUT** navigating or relo
240
275
 
241
276
  ---
242
277
 
278
+ ### `scroll_page`
279
+
280
+ Scrolls within an already-loaded page. Use before `take_screenshot` to capture different parts of the page, or to bring elements into view before interaction. Supports multiple scroll modes:
281
+
282
+ - **By direction**: Scroll up/down/left/right by pixel amount
283
+ - **To element**: Scroll until a specific element is visible
284
+ - **To position**: Scroll to absolute coordinates
285
+
286
+ **⚠️ Note:** Page must be already loaded via `fetch_webpage` first.
287
+
288
+ **Parameters:**
289
+ - `url` (string, required) - The URL of the page (must match a previously fetched page)
290
+ - `direction` (string, optional) - Direction to scroll: `up`, `down`, `left`, `right`. Use with `amount`.
291
+ - `amount` (number, optional, default: `500`) - Pixels to scroll in the specified direction (~half a viewport)
292
+ - `selector` (string, optional) - CSS selector of element to scroll into view. Ignores direction/amount.
293
+ - `x` (number, optional) - Absolute horizontal scroll position. Use with `y`.
294
+ - `y` (number, optional) - Absolute vertical scroll position. Use with `x`.
295
+
296
+ **Examples:**
297
+ ```javascript
298
+ // Scroll down by 500px (default)
299
+ { url: "https://example.com", direction: "down" }
300
+
301
+ // Scroll down by 1000px
302
+ { url: "https://example.com", direction: "down", amount: 1000 }
303
+
304
+ // Scroll an element into view
305
+ { url: "https://example.com", selector: "#footer" }
306
+
307
+ // Scroll to specific position
308
+ { url: "https://example.com", x: 0, y: 2000 }
309
+
310
+ // Scroll to top of page
311
+ { url: "https://example.com", x: 0, y: 0 }
312
+ ```
313
+
314
+ **Returns:** Current scroll position, page dimensions, and viewport size — useful for understanding where you are on the page.
315
+
316
+ ---
317
+
243
318
  ### `take_screenshot`
244
319
 
245
320
  Takes a screenshot of an already-loaded page for visual analysis. **Useful when HTML parsing is insufficient** — for example, pages with charts, images, complex layouts, popups, or visual content that's hard to understand from HTML alone. Returns a PNG image.
@@ -328,7 +403,7 @@ Logs go to `stderr` so they don't interfere with MCP protocol on `stdout`.
328
403
  ## Troubleshooting
329
404
 
330
405
  **Browser doesn't open?**
331
- - Make sure Chrome or Edge is installed
406
+ - Make sure Chrome, Edge, or Brave is installed
332
407
  - Try setting `CHROME_PATH` explicitly
333
408
 
334
409
  **Can't connect to browser?**
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "mcpbrowser",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "mcpName": "io.github.cherchyk/mcpbrowser",
5
5
  "type": "module",
6
- "description": "MCP browser server - fetch web pages using real Chrome/Edge browser. Handles authentication, SSO, CAPTCHAs, and anti-bot protection. Browser automation for AI assistants.",
6
+ "description": "MCP browser server - fetch web pages using real Chrome/Edge/Brave browser. Handles authentication, SSO, CAPTCHAs, and anti-bot protection. Browser automation for AI assistants.",
7
7
  "main": "src/mcp-browser.js",
8
8
  "bin": {
9
9
  "mcpbrowser": "src/mcp-browser.js"
@@ -178,9 +178,9 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
178
178
  logger.error(`click_element: Failed to connect to browser: ${err.message}`);
179
179
  return new InformationalResponse(
180
180
  `Browser connection failed: ${err.message}`,
181
- 'Could not connect to Chrome or Edge browser. The browser must be running with remote debugging enabled.',
181
+ 'The browser must be running with remote debugging enabled.',
182
182
  [
183
- 'Ensure Chrome or Edge browser is installed and running',
183
+ 'Ensure the browser is installed and running',
184
184
  'Check that remote debugging is enabled (--remote-debugging-port)',
185
185
  'Try restarting the MCP server'
186
186
  ]
@@ -6,7 +6,7 @@
6
6
  import { getBrowser, domainPages } from '../core/browser.js';
7
7
  import { getOrCreatePage, queueRequest, navigateToUrl, waitForPageReady, extractAndProcessHtml, waitForPageStability } from '../core/page.js';
8
8
  import { detectRedirectType, waitForAutoAuth, waitForManualAuth } from '../core/auth.js';
9
- import { MCPResponse, ErrorResponse, HttpStatusResponse } from '../core/responses.js';
9
+ import { MCPResponse, ErrorResponse, HttpStatusResponse, InformationalResponse } from '../core/responses.js';
10
10
  import logger from '../core/logger.js';
11
11
 
12
12
  /**
@@ -156,7 +156,22 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
156
156
  const authCompletionTimeout = 600000;
157
157
  const reuseLastKeptPage = true;
158
158
 
159
- const browserInstance = await getBrowser(browser);
159
+ // Ensure browser connection
160
+ let browserInstance;
161
+ try {
162
+ browserInstance = await getBrowser(browser);
163
+ } catch (err) {
164
+ logger.error(`fetch_webpage: Failed to connect to browser: ${err.message}`);
165
+ return new InformationalResponse(
166
+ `Browser connection failed: ${err.message}`,
167
+ 'The browser must be running with remote debugging enabled.',
168
+ [
169
+ 'Ensure the browser is installed and running',
170
+ 'Check that remote debugging is enabled (--remote-debugging-port)',
171
+ 'Try restarting the MCP server'
172
+ ]
173
+ );
174
+ }
160
175
 
161
176
  try {
162
177
  // Get or create page for this domain (simple - no locks needed)
@@ -120,9 +120,9 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
120
120
  logger.error(`get_current_html: Failed to connect to browser: ${err.message}`);
121
121
  return new InformationalResponse(
122
122
  `Browser connection failed: ${err.message}`,
123
- 'Could not connect to Chrome or Edge browser. The browser must be running with remote debugging enabled.',
123
+ 'The browser must be running with remote debugging enabled.',
124
124
  [
125
- 'Ensure Chrome or Edge browser is installed and running',
125
+ 'Ensure the browser is installed and running',
126
126
  'Check that remote debugging is enabled (--remote-debugging-port)',
127
127
  'Try restarting the MCP server'
128
128
  ]
@@ -186,9 +186,9 @@ export async function scrollPage({ url, direction, amount = 500, selector, x, y
186
186
  logger.error(`scroll_page: Failed to connect to browser: ${err.message}`);
187
187
  return new InformationalResponse(
188
188
  `Browser connection failed: ${err.message}`,
189
- 'Could not connect to Chrome or Edge browser. The browser must be running with remote debugging enabled.',
189
+ 'The browser must be running with remote debugging enabled.',
190
190
  [
191
- 'Ensure Chrome or Edge browser is installed and running',
191
+ 'Ensure the browser is installed and running',
192
192
  'Check that remote debugging is enabled (--remote-debugging-port)',
193
193
  'Try restarting the MCP server'
194
194
  ]
@@ -149,9 +149,9 @@ export async function takeScreenshot({ url, fullPage = false }) {
149
149
  logger.error(`take_screenshot: Failed to connect to browser: ${err.message}`);
150
150
  return new InformationalResponse(
151
151
  `Browser connection failed: ${err.message}`,
152
- 'Could not connect to Chrome or Edge browser. The browser must be running with remote debugging enabled.',
152
+ 'The browser must be running with remote debugging enabled.',
153
153
  [
154
- 'Ensure Chrome or Edge browser is installed and running',
154
+ 'Ensure the browser is installed and running',
155
155
  'Check that remote debugging is enabled (--remote-debugging-port)',
156
156
  'Try restarting the MCP server'
157
157
  ]
@@ -66,21 +66,32 @@ export class TypeTextSuccessResponse extends MCPResponse {
66
66
  export const TYPE_TEXT_TOOL = {
67
67
  name: "type_text",
68
68
  title: "Type Text",
69
- description: "**BROWSER INTERACTION** - Types text into input fields on browser-loaded pages. Use this for filling forms, entering search queries, or any text input on the page.\n\nWorks with input fields, textareas, and other editable elements.\n\n**PREREQUISITE**: Page MUST be loaded with fetch_webpage first. This tool operates on an already-loaded page in the browser.",
69
+ description: "**BROWSER INTERACTION** - Types text into multiple input fields on browser-loaded pages in a single call. Use this for filling forms, entering search queries, or any text input on the page.\n\nWorks with input fields, textareas, and other editable elements. Supports filling multiple fields at once for efficient form filling.\n\n**PREREQUISITE**: Page MUST be loaded with fetch_webpage first. This tool operates on an already-loaded page in the browser.",
70
70
  inputSchema: {
71
71
  type: "object",
72
72
  properties: {
73
73
  url: { type: "string", description: "The URL of the page (must match a previously fetched page)" },
74
- selector: { type: "string", description: "CSS selector for the input element (e.g., '#username', 'input[name=\"email\"]')" },
75
- text: { type: "string", description: "Text to type into the field" },
76
- clear: { type: "boolean", description: "Whether to clear existing text first", default: true },
77
- typeDelay: { type: "number", description: "Delay between keystrokes in milliseconds (simulates human typing)", default: 50 },
78
- waitForElementTimeout: { type: "number", description: "Maximum time to wait for element in milliseconds", default: 5000 },
74
+ fields: {
75
+ type: "array",
76
+ description: "Array of fields to fill. Each field specifies a selector and text to type.",
77
+ items: {
78
+ type: "object",
79
+ properties: {
80
+ selector: { type: "string", description: "CSS selector for the input element (e.g., '#username', 'input[name=\"email\"]')" },
81
+ text: { type: "string", description: "Text to type into the field" },
82
+ clear: { type: "boolean", description: "Whether to clear existing text first", default: true },
83
+ waitForElementTimeout: { type: "number", description: "Maximum time to wait for element in milliseconds", default: 5000 }
84
+ },
85
+ required: ["selector", "text"],
86
+ additionalProperties: false
87
+ },
88
+ minItems: 1
89
+ },
79
90
  returnHtml: { type: "boolean", description: "Whether to wait for stability and return HTML after typing.", default: true },
80
91
  removeUnnecessaryHTML: { type: "boolean", description: "Remove Unnecessary HTML for size reduction by 90%. Only used when returnHtml is true.", default: true },
81
92
  postTypeWait: { type: "number", description: "Milliseconds to wait after typing for SPAs to render dynamic content.", default: 1000 }
82
93
  },
83
- required: ["url", "selector", "text"],
94
+ required: ["url", "fields"],
84
95
  additionalProperties: false
85
96
  },
86
97
  outputSchema: {
@@ -103,38 +114,48 @@ export const TYPE_TEXT_TOOL = {
103
114
  }
104
115
  };
105
116
 
117
+ // ============================================================================
118
+ // CONSTANTS
119
+ // ============================================================================
120
+
121
+ /** Hardcoded delay between keystrokes in milliseconds */
122
+ const TYPE_DELAY_MS = 10;
123
+
106
124
  // ============================================================================
107
125
  // ACTION FUNCTION
108
126
  // ============================================================================
109
127
 
110
128
  /**
111
- * Type text into an input field
129
+ * Type text into multiple input fields
112
130
  * @param {Object} params - Type parameters
113
131
  * @param {string} params.url - The URL of the page to interact with
114
- * @param {string} params.selector - CSS selector for the input element
115
- * @param {string} params.text - Text to type
116
- * @param {boolean} [params.clear=true] - Whether to clear existing text first
117
- * @param {number} [params.typeDelay=50] - Delay between keystrokes in milliseconds
118
- * @param {number} [params.waitForElementTimeout=30000] - Maximum time to wait for element
132
+ * @param {Array<{selector: string, text: string, clear?: boolean, waitForElementTimeout?: number}>} params.fields - Array of fields to fill
119
133
  * @param {boolean} [params.returnHtml=true] - Whether to wait for stability and return HTML
120
134
  * @param {boolean} [params.removeUnnecessaryHTML=true] - Whether to clean HTML (only if returnHtml is true)
121
135
  * @param {number} [params.postTypeWait=1000] - Milliseconds to wait after typing for SPAs to render dynamic content
122
136
  * @returns {Promise<Object>} Result object with success status and details
123
137
  */
124
- export async function typeText({ url, selector, text, clear = true, typeDelay = 50, waitForElementTimeout = 30000, returnHtml = true, removeUnnecessaryHTML = true, postTypeWait = 1000 }) {
138
+ export async function typeText({ url, fields, returnHtml = true, removeUnnecessaryHTML = true, postTypeWait = 1000 }) {
125
139
  const startTime = Date.now();
126
- logger.info(`type_text called: selector=${selector}, url=${url}`);
140
+ logger.info(`type_text called: ${fields?.length || 0} fields, url=${url}`);
127
141
 
128
142
  if (!url) {
129
143
  throw new Error("url parameter is required");
130
144
  }
131
145
 
132
- if (!selector) {
133
- throw new Error("selector parameter is required");
146
+ if (!fields || !Array.isArray(fields) || fields.length === 0) {
147
+ throw new Error("fields parameter is required and must be a non-empty array");
134
148
  }
135
149
 
136
- if (text === undefined || text === null) {
137
- throw new Error("text parameter is required");
150
+ // Validate each field
151
+ for (let i = 0; i < fields.length; i++) {
152
+ const field = fields[i];
153
+ if (!field.selector) {
154
+ throw new Error(`fields[${i}].selector is required`);
155
+ }
156
+ if (field.text === undefined || field.text === null) {
157
+ throw new Error(`fields[${i}].text is required`);
158
+ }
138
159
  }
139
160
 
140
161
  let hostname;
@@ -151,9 +172,9 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
151
172
  logger.error(`type_text: Failed to connect to browser: ${err.message}`);
152
173
  return new InformationalResponse(
153
174
  `Browser connection failed: ${err.message}`,
154
- 'Could not connect to Chrome or Edge browser. The browser must be running with remote debugging enabled.',
175
+ 'The browser must be running with remote debugging enabled.',
155
176
  [
156
- 'Ensure Chrome or Edge browser is installed and running',
177
+ 'Ensure the browser is installed and running',
157
178
  'Check that remote debugging is enabled (--remote-debugging-port)',
158
179
  'Try restarting the MCP server'
159
180
  ]
@@ -178,16 +199,32 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
178
199
  );
179
200
  }
180
201
 
202
+ const filledSelectors = [];
203
+ let currentFieldIndex = 0;
204
+ let currentSelector = null;
205
+
181
206
  try {
182
- await page.waitForSelector(selector, { timeout: waitForElementTimeout, visible: true });
183
-
184
- if (clear) {
185
- await page.click(selector, { clickCount: 3 }); // Select all text
186
- await page.keyboard.press('Backspace');
207
+ // Type into each field sequentially
208
+ for (const field of fields) {
209
+ const { selector, text, clear = true, waitForElementTimeout = 5000 } = field;
210
+ currentSelector = selector;
211
+
212
+ await page.waitForSelector(selector, { timeout: waitForElementTimeout, visible: true });
213
+
214
+ if (clear) {
215
+ await page.click(selector, { clickCount: 3 }); // Select all text
216
+ await page.keyboard.press('Backspace');
217
+ }
218
+
219
+ logger.info(`Typing into: ${selector}`);
220
+ await page.type(selector, String(text), { delay: TYPE_DELAY_MS });
221
+ filledSelectors.push(selector);
222
+ currentFieldIndex++;
187
223
  }
188
224
 
189
- logger.info(`Typing into: ${selector}`);
190
- await page.type(selector, String(text), { delay: typeDelay });
225
+ const fieldsSummary = filledSelectors.length === 1
226
+ ? filledSelectors[0]
227
+ : `${filledSelectors.length} fields (${filledSelectors.join(', ')})`;
191
228
 
192
229
  if (returnHtml) {
193
230
  // Wait for page to stabilize (handles form validation, autocomplete, etc.)
@@ -202,11 +239,11 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
202
239
  const currentUrl = page.url();
203
240
  const html = await extractAndProcessHtml(page, removeUnnecessaryHTML);
204
241
 
205
- logger.info(`type_text completed: typed into ${selector}`);
242
+ logger.info(`type_text completed: typed into ${fieldsSummary}`);
206
243
 
207
244
  return new TypeTextSuccessResponse(
208
245
  currentUrl,
209
- `Typed text into: ${selector}`,
246
+ `Typed text into: ${fieldsSummary}`,
210
247
  html,
211
248
  [
212
249
  "Use MCPBrowser's type_text to fill additional fields",
@@ -228,11 +265,11 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
228
265
 
229
266
  const currentUrl = page.url();
230
267
 
231
- logger.info(`type_text completed: typed into ${selector} (no HTML)`);
268
+ logger.info(`type_text completed: typed into ${fieldsSummary} (no HTML)`);
232
269
 
233
270
  return new TypeTextSuccessResponse(
234
271
  currentUrl,
235
- `Typed text into: ${selector}`,
272
+ `Typed text into: ${fieldsSummary}`,
236
273
  null,
237
274
  [
238
275
  "Use MCPBrowser's get_current_html to see updated page state",
@@ -243,16 +280,63 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
243
280
  );
244
281
  }
245
282
  } catch (err) {
246
- logger.error(`type_text failed: ${err.message}`);
247
- return new InformationalResponse(
248
- `Failed to type text: ${err.message}`,
249
- 'The input field was found but text could not be entered. It may be disabled, read-only, or covered by another element.',
250
- [
283
+ // Build informative error message for agent
284
+ const totalFields = fields.length;
285
+ const failedFieldNum = currentFieldIndex + 1;
286
+ const errorMsg = err.message;
287
+
288
+ // Determine error type for better guidance
289
+ const isNotFound = errorMsg.includes('Waiting for selector') || errorMsg.includes('failed: Waiting failed');
290
+ const isNotVisible = errorMsg.includes('not visible') || errorMsg.includes('hidden');
291
+ const isDetached = errorMsg.includes('detached') || errorMsg.includes('Node is detached');
292
+
293
+ let reason;
294
+ let nextSteps;
295
+
296
+ if (isNotFound) {
297
+ reason = `Selector not found: "${currentSelector}". The element may not exist on the page or have a different selector.`;
298
+ nextSteps = [
299
+ "Use MCPBrowser's get_current_html to find the correct selector",
300
+ "Use MCPBrowser's take_screenshot to visually inspect the form",
301
+ "Check for typos in the selector or try a simpler selector (e.g., 'input[type=\"text\"]')",
302
+ "The element may load dynamically - try increasing waitForElementTimeout"
303
+ ];
304
+ } else if (isNotVisible) {
305
+ reason = `Element "${currentSelector}" exists but is not visible. It may be hidden, collapsed, or off-screen.`;
306
+ nextSteps = [
307
+ "Use MCPBrowser's take_screenshot to see the page state",
308
+ "Use MCPBrowser's click_element to expand/show the form section first",
309
+ "Use MCPBrowser's scroll_page to bring the element into view"
310
+ ];
311
+ } else if (isDetached) {
312
+ reason = `Element "${currentSelector}" was removed from the page during interaction. The page may have reloaded or updated.`;
313
+ nextSteps = [
314
+ "Use MCPBrowser's get_current_html to check current page state",
315
+ "Retry the type_text call - the page may have stabilized"
316
+ ];
317
+ } else {
318
+ reason = `Failed to interact with "${currentSelector}": ${errorMsg}`;
319
+ nextSteps = [
251
320
  "Use MCPBrowser's get_current_html to verify page state",
252
321
  "Use MCPBrowser's take_screenshot to see what's on the page visually",
253
- "Check if the selector is correct",
254
- "Verify the input field is visible and enabled"
255
- ]
322
+ "The element may be disabled or read-only"
323
+ ];
324
+ }
325
+
326
+ // Build progress summary
327
+ let progressInfo;
328
+ if (filledSelectors.length === 0) {
329
+ progressInfo = `Failed on field 1 of ${totalFields}. No fields were filled.`;
330
+ } else {
331
+ progressInfo = `Failed on field ${failedFieldNum} of ${totalFields}. Successfully filled ${filledSelectors.length} field(s): ${filledSelectors.join(', ')}. Do NOT re-type these fields.`;
332
+ }
333
+
334
+ logger.error(`type_text failed on field ${failedFieldNum}/${totalFields} (${currentSelector}): ${errorMsg}`);
335
+
336
+ return new InformationalResponse(
337
+ `${progressInfo}`,
338
+ reason,
339
+ nextSteps
256
340
  );
257
341
  }
258
342
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Brave browser implementation for MCPBrowser
3
+ * Brave is Chromium-based and uses the same CDP protocol as Chrome
4
+ */
5
+
6
+ import { ChromiumBrowser } from './ChromiumBrowser.js';
7
+ import os from "os";
8
+
9
+ /**
10
+ * Get platform-specific default paths where Brave browser is typically installed.
11
+ * @returns {string[]} Array of possible Brave executable paths for the current platform
12
+ */
13
+ function getDefaultBravePaths() {
14
+ const platform = os.platform();
15
+
16
+ if (platform === "win32") {
17
+ return [
18
+ "C:/Program Files/BraveSoftware/Brave-Browser/Application/brave.exe",
19
+ "C:/Program Files (x86)/BraveSoftware/Brave-Browser/Application/brave.exe",
20
+ `${os.homedir()}/AppData/Local/BraveSoftware/Brave-Browser/Application/brave.exe`,
21
+ ];
22
+ } else if (platform === "darwin") {
23
+ return [
24
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
25
+ ];
26
+ } else {
27
+ return [
28
+ "/usr/bin/brave",
29
+ "/usr/bin/brave-browser",
30
+ "/usr/bin/brave-browser-stable",
31
+ "/opt/brave.com/brave/brave-browser",
32
+ "/snap/bin/brave",
33
+ ];
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Brave browser class implementation
39
+ * Extends ChromiumBrowser with Brave-specific configuration
40
+ */
41
+ export class BraveBrowser extends ChromiumBrowser {
42
+ constructor() {
43
+ const config = {
44
+ name: 'Brave',
45
+ host: process.env.BRAVE_REMOTE_DEBUG_HOST || "127.0.0.1",
46
+ port: Number(process.env.BRAVE_REMOTE_DEBUG_PORT || 9224),
47
+ wsEndpoint: process.env.BRAVE_WS_ENDPOINT,
48
+ executablePath: process.env.BRAVE_PATH,
49
+ defaultPaths: getDefaultBravePaths(),
50
+ userDataDirName: 'BraveDebug'
51
+ };
52
+ super(config);
53
+ }
54
+ }
55
+
56
+ // Legacy exports for backward compatibility
57
+ export async function connectBrave() {
58
+ const brave = new BraveBrowser();
59
+ return await brave.connect();
60
+ }
61
+
62
+ export async function disconnectBrave(browser) {
63
+ if (browser && browser.isConnected()) {
64
+ await browser.disconnect();
65
+ }
66
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { ChromeBrowser } from '../browsers/chrome.js';
7
7
  import { EdgeBrowser } from '../browsers/edge.js';
8
+ import { BraveBrowser } from '../browsers/brave.js';
8
9
  import os from 'os';
9
10
  import logger from './logger.js';
10
11
 
@@ -17,15 +18,16 @@ const browserInstances = new Map();
17
18
 
18
19
  /**
19
20
  * Detect the default browser on the system
20
- * @returns {Promise<string>} Browser type (chrome, edge)
21
+ * @returns {Promise<string>} Browser type (chrome, edge, brave)
21
22
  */
22
23
  async function detectDefaultBrowser() {
23
24
  const platform = os.platform();
24
25
 
25
- // Priority order: Chrome > Edge
26
+ // Priority order: Chrome > Edge > Brave
26
27
  const browsers = [
27
28
  new ChromeBrowser(),
28
- new EdgeBrowser()
29
+ new EdgeBrowser(),
30
+ new BraveBrowser()
29
31
  ];
30
32
 
31
33
  for (const browser of browsers) {
@@ -67,10 +69,13 @@ export async function GetBrowser(type = '') {
67
69
  case 'edge':
68
70
  browser = new EdgeBrowser();
69
71
  break;
72
+ case 'brave':
73
+ browser = new BraveBrowser();
74
+ break;
70
75
  default:
71
76
  throw new Error(
72
77
  `Unsupported browser type: ${type}. ` +
73
- `Supported: chrome, edge. ` +
78
+ `Supported: chrome, edge, brave. ` +
74
79
  `Leave empty for auto-detection.`
75
80
  );
76
81
  }
@@ -106,7 +106,7 @@ async function main() {
106
106
  // Return a proper error response instead of throwing
107
107
  return new ErrorResponse(
108
108
  `${name} failed: ${error.message}`,
109
- ['Check browser is installed', 'Try specifying browser parameter explicitly (chrome or edge)', 'Check MCP server logs for details']
109
+ ['Check browser is installed', 'Try specifying browser parameter explicitly (chrome, edge, or brave)', 'Check MCP server logs for details']
110
110
  ).toMcpFormat();
111
111
  }
112
112