mcpbrowser 0.3.18 → 0.3.20
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 +101 -26
- package/package.json +2 -2
- package/src/actions/accept-eula.js +188 -0
- package/src/actions/click-element.js +6 -6
- package/src/actions/fetch-page.js +22 -7
- package/src/actions/get-current-html.js +3 -3
- package/src/actions/scroll-page.js +7 -7
- package/src/actions/take-screenshot.js +3 -3
- package/src/actions/type-text.js +127 -43
- package/src/browsers/brave.js +66 -0
- package/src/core/browser.js +9 -4
- package/src/core/eula.js +120 -0
- package/src/core/logger.js +17 -1
- package/src/core/page.js +6 -6
- package/src/mcp-browser.js +18 -2
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
|
|
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.
|
|
20
|
-
4.
|
|
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
|
|
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 | [
|
|
58
|
-
| 4 | [
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
193
|
-
- `
|
|
194
|
-
- `
|
|
195
|
-
- `
|
|
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
|
-
//
|
|
204
|
-
{
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{
|
|
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
|
-
//
|
|
210
|
-
{ url: "https://example.com", selector: "#
|
|
228
|
+
// Single field input
|
|
229
|
+
{ url: "https://example.com", fields: [{ selector: "#search", text: "query" }] }
|
|
211
230
|
|
|
212
|
-
//
|
|
213
|
-
{ url: "https://example.com", selector: "#
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.3.20",
|
|
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"
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* accept-eula.js - Accept the End User License Agreement
|
|
3
|
+
* This tool must be called before any other MCPBrowser tools can be used.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MCPResponse } from '../core/responses.js';
|
|
7
|
+
import { acceptEula, isEulaAccepted, EULA_URL } from '../core/eula.js';
|
|
8
|
+
import logger from '../core/logger.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// EULA CHECK HELPER
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if EULA acceptance is required before executing a tool.
|
|
20
|
+
* @param {string} toolName - The name of the tool being called
|
|
21
|
+
* @returns {Object|null} MCP-formatted response if EULA not accepted, null if OK to proceed
|
|
22
|
+
*/
|
|
23
|
+
export function requireEulaAcceptance(toolName) {
|
|
24
|
+
if (isEulaAccepted()) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
logger.info(`Tool ${toolName} requested - EULA acceptance required`);
|
|
29
|
+
return new EulaPendingResponse(
|
|
30
|
+
EULA_URL,
|
|
31
|
+
[],
|
|
32
|
+
toolName
|
|
33
|
+
).toMcpFormat();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// RESPONSE CLASSES
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Response when EULA needs to be accepted (shown before acceptance)
|
|
42
|
+
*/
|
|
43
|
+
export class EulaPendingResponse extends MCPResponse {
|
|
44
|
+
/**
|
|
45
|
+
* @param {string} eulaUrl - URL to the EULA
|
|
46
|
+
* @param {string[]} nextSteps - Suggested next actions
|
|
47
|
+
* @param {string} [attemptedAction] - The tool the user tried to use (optional)
|
|
48
|
+
*/
|
|
49
|
+
constructor(eulaUrl, nextSteps, attemptedAction = null) {
|
|
50
|
+
super(nextSteps);
|
|
51
|
+
|
|
52
|
+
if (typeof eulaUrl !== 'string') {
|
|
53
|
+
throw new TypeError('eulaUrl must be a string');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this.eulaUrl = eulaUrl;
|
|
57
|
+
this.accepted = false;
|
|
58
|
+
this.attemptedAction = attemptedAction;
|
|
59
|
+
this.requiresUserConfirmation = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_getAdditionalFields() {
|
|
63
|
+
return {
|
|
64
|
+
accepted: this.accepted,
|
|
65
|
+
eulaUrl: this.eulaUrl,
|
|
66
|
+
requiresUserConfirmation: this.requiresUserConfirmation,
|
|
67
|
+
...(this.attemptedAction && { attemptedAction: this.attemptedAction })
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getTextSummary() {
|
|
72
|
+
return `STOP - EXPLICIT USER CONFIRMATION REQUIRED !!!
|
|
73
|
+
|
|
74
|
+
Use ask questions tool to show a Yes/No options asking the user: "Do you accept the MCPBrowser EULA at ${this.eulaUrl}?"
|
|
75
|
+
|
|
76
|
+
DO NOT call accept_eula until the user explicitly selects "Yes".
|
|
77
|
+
If the user selects "No" or does not respond, DO NOT proceed.
|
|
78
|
+
|
|
79
|
+
When calling accept_eula, you MUST set userExplicitlyConfirmed=true ONLY if user said Yes.`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Response for successful EULA acceptance
|
|
85
|
+
*/
|
|
86
|
+
export class EulaAcceptedResponse extends MCPResponse {
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} eulaUrl - URL to the EULA that was accepted
|
|
89
|
+
* @param {string[]} nextSteps - Suggested next actions
|
|
90
|
+
*/
|
|
91
|
+
constructor(eulaUrl, nextSteps) {
|
|
92
|
+
super(nextSteps);
|
|
93
|
+
|
|
94
|
+
if (typeof eulaUrl !== 'string') {
|
|
95
|
+
throw new TypeError('eulaUrl must be a string');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
this.eulaUrl = eulaUrl;
|
|
99
|
+
this.accepted = true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_getAdditionalFields() {
|
|
103
|
+
return {
|
|
104
|
+
accepted: this.accepted,
|
|
105
|
+
eulaUrl: this.eulaUrl
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getTextSummary() {
|
|
110
|
+
return `EULA accepted (${this.eulaUrl}). You now have full access to MCPBrowser tools.`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ============================================================================
|
|
115
|
+
// TOOL DEFINITION
|
|
116
|
+
// ============================================================================
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @type {Tool}
|
|
120
|
+
*/
|
|
121
|
+
export const ACCEPT_EULA_TOOL = {
|
|
122
|
+
name: "accept_eula",
|
|
123
|
+
description: "CRITICAL: You MUST ask the user 'Do you accept the MCPBrowser EULA?' and wait for their explicit 'Yes' response BEFORE calling this tool. Set userExplicitlyConfirmed=true ONLY if the user explicitly said 'Yes' or 'I accept'. If the user has not responded or said 'No', DO NOT call this tool. Calling this tool without user consent violates the EULA terms.",
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: "object",
|
|
126
|
+
properties: {
|
|
127
|
+
userExplicitlyConfirmed: {
|
|
128
|
+
type: "boolean",
|
|
129
|
+
description: "REQUIRED: Must be true. Set to true ONLY if the user explicitly said 'Yes' or 'I accept' to the EULA prompt. Never set to true without explicit user confirmation."
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
required: ["userExplicitlyConfirmed"]
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// TOOL IMPLEMENTATION
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Handle EULA acceptance
|
|
142
|
+
* @param {Object} args - Tool arguments
|
|
143
|
+
* @param {boolean} args.userExplicitlyConfirmed - Whether user explicitly confirmed acceptance
|
|
144
|
+
* @returns {Promise<MCPResponse>} Response indicating EULA status
|
|
145
|
+
*/
|
|
146
|
+
export async function handleAcceptEula(args) {
|
|
147
|
+
const { userExplicitlyConfirmed } = args;
|
|
148
|
+
|
|
149
|
+
logger.debug(`accept_eula called with userExplicitlyConfirmed: ${userExplicitlyConfirmed}`);
|
|
150
|
+
|
|
151
|
+
// If already accepted and calling again, just confirm
|
|
152
|
+
if (isEulaAccepted()) {
|
|
153
|
+
logger.info('EULA already accepted');
|
|
154
|
+
return new EulaAcceptedResponse(
|
|
155
|
+
EULA_URL,
|
|
156
|
+
[
|
|
157
|
+
'Use fetch_webpage to navigate to a URL',
|
|
158
|
+
'Use get_current_html to see the current page content'
|
|
159
|
+
]
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// CRITICAL: Validate user explicitly confirmed
|
|
164
|
+
if (userExplicitlyConfirmed !== true) {
|
|
165
|
+
logger.warn('accept_eula called without userExplicitlyConfirmed=true - rejecting');
|
|
166
|
+
return new EulaPendingResponse(
|
|
167
|
+
EULA_URL,
|
|
168
|
+
[
|
|
169
|
+
'Ask the user: "Do you accept the MCPBrowser EULA?"',
|
|
170
|
+
'Wait for explicit "Yes" response',
|
|
171
|
+
'Then call accept_eula with userExplicitlyConfirmed=true'
|
|
172
|
+
]
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Accept the EULA
|
|
177
|
+
acceptEula(EULA_URL);
|
|
178
|
+
logger.info('EULA accepted with explicit user confirmation');
|
|
179
|
+
|
|
180
|
+
return new EulaAcceptedResponse(
|
|
181
|
+
EULA_URL,
|
|
182
|
+
[
|
|
183
|
+
'Use fetch_webpage to navigate to a URL',
|
|
184
|
+
'Use click_element to interact with page elements',
|
|
185
|
+
'Use type_text to enter text into forms'
|
|
186
|
+
]
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -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
|
-
'
|
|
181
|
+
'The browser must be running with remote debugging enabled.',
|
|
182
182
|
[
|
|
183
|
-
'Ensure
|
|
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
|
]
|
|
@@ -192,7 +192,7 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
192
192
|
|
|
193
193
|
if (!page) {
|
|
194
194
|
const isConnectionLost = pageError && pageError.includes('connection');
|
|
195
|
-
logger.
|
|
195
|
+
logger.debug(`click_element: ${pageError || 'No page found for ' + hostname}`);
|
|
196
196
|
return new InformationalResponse(
|
|
197
197
|
isConnectionLost ? `Page connection lost for ${hostname}` : `No open page found for ${hostname}`,
|
|
198
198
|
isConnectionLost
|
|
@@ -259,13 +259,13 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
259
259
|
// await page.evaluate(el => el.scrollIntoView({ behavior: 'smooth', block: 'center' }), elementHandle);
|
|
260
260
|
// await new Promise(r => setTimeout(r, 300)); // Brief delay after scroll
|
|
261
261
|
|
|
262
|
-
logger.
|
|
262
|
+
logger.debug(`Clicking: ${selector || `text="${text}"`}`);
|
|
263
263
|
await elementHandle.click();
|
|
264
264
|
|
|
265
265
|
if (returnHtml) {
|
|
266
266
|
// Wait for page to stabilize (handles both navigation and SPA content updates)
|
|
267
267
|
// This ensures content is fully loaded before returning, just like fetch_webpage does
|
|
268
|
-
logger.
|
|
268
|
+
logger.debug('Waiting for page stability...');
|
|
269
269
|
await waitForPageStability(page);
|
|
270
270
|
|
|
271
271
|
// Wait for SPAs to render dynamic content after click
|
|
@@ -292,7 +292,7 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
292
292
|
);
|
|
293
293
|
} else {
|
|
294
294
|
// Wait for page to stabilize even for fast clicks (ensures JS has finished)
|
|
295
|
-
logger.
|
|
295
|
+
logger.debug('Waiting for page stability (fast mode)...');
|
|
296
296
|
await waitForPageStability(page);
|
|
297
297
|
|
|
298
298
|
// Wait for SPAs to render dynamic content after click
|
|
@@ -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
|
-
|
|
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)
|
|
@@ -175,7 +190,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
175
190
|
const redirectInfo = detectRedirectType(url, hostname, currentUrl, currentHostname);
|
|
176
191
|
|
|
177
192
|
if (redirectInfo.type === 'requested_auth') {
|
|
178
|
-
logger.
|
|
193
|
+
logger.debug('User requested auth page directly, returning content');
|
|
179
194
|
// Update domain mapping if needed
|
|
180
195
|
if (redirectInfo.currentHostname !== hostname) {
|
|
181
196
|
domainPages.delete(hostname);
|
|
@@ -183,13 +198,13 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
183
198
|
hostname = redirectInfo.currentHostname;
|
|
184
199
|
}
|
|
185
200
|
} else if (redirectInfo.type === 'permanent') {
|
|
186
|
-
logger.
|
|
201
|
+
logger.debug(`Redirect: ${hostname} → ${redirectInfo.currentHostname}`);
|
|
187
202
|
|
|
188
203
|
// Check if we already have a tab for the redirected hostname
|
|
189
204
|
// (can happen after reconnect - we mapped mail.google.com but not gmail.com)
|
|
190
205
|
const existingPage = domainPages.get(redirectInfo.currentHostname);
|
|
191
206
|
if (existingPage && existingPage !== page && !existingPage.isClosed()) {
|
|
192
|
-
logger.
|
|
207
|
+
logger.debug(`Found existing tab for ${redirectInfo.currentHostname}, reusing it`);
|
|
193
208
|
// Close the new tab we just opened, use the existing one
|
|
194
209
|
await page.close().catch(() => {});
|
|
195
210
|
domainPages.delete(hostname);
|
|
@@ -248,7 +263,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
248
263
|
|
|
249
264
|
// Additional wait if requested (for pages that need extra time)
|
|
250
265
|
if (postLoadWait > 0) {
|
|
251
|
-
logger.
|
|
266
|
+
logger.debug(`Waiting ${postLoadWait}ms (postLoadWait)...`);
|
|
252
267
|
await new Promise(resolve => setTimeout(resolve, postLoadWait));
|
|
253
268
|
}
|
|
254
269
|
|
|
@@ -259,7 +274,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
259
274
|
|
|
260
275
|
// Check for non-2xx HTTP status codes - return informational response (not red error)
|
|
261
276
|
if (statusCode && (statusCode >= 400 && statusCode < 600)) {
|
|
262
|
-
logger.
|
|
277
|
+
logger.debug(`HTTP ${statusCode} ${statusText} - returning as informational response`);
|
|
263
278
|
return new HttpStatusResponse(
|
|
264
279
|
page.url(),
|
|
265
280
|
statusCode,
|
|
@@ -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
|
-
'
|
|
123
|
+
'The browser must be running with remote debugging enabled.',
|
|
124
124
|
[
|
|
125
|
-
'Ensure
|
|
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
|
]
|
|
@@ -134,7 +134,7 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
|
|
|
134
134
|
|
|
135
135
|
if (!page) {
|
|
136
136
|
const isConnectionLost = pageError && pageError.includes('connection');
|
|
137
|
-
logger.
|
|
137
|
+
logger.debug(`get_current_html: ${pageError || 'No page found for ' + hostname}`);
|
|
138
138
|
return new InformationalResponse(
|
|
139
139
|
isConnectionLost ? `Page connection lost for ${hostname}` : `No open page found for ${hostname}`,
|
|
140
140
|
isConnectionLost
|
|
@@ -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
|
-
'
|
|
189
|
+
'The browser must be running with remote debugging enabled.',
|
|
190
190
|
[
|
|
191
|
-
'Ensure
|
|
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
|
]
|
|
@@ -200,7 +200,7 @@ export async function scrollPage({ url, direction, amount = 500, selector, x, y
|
|
|
200
200
|
|
|
201
201
|
if (!page) {
|
|
202
202
|
const isConnectionLost = pageError && pageError.includes('connection');
|
|
203
|
-
logger.
|
|
203
|
+
logger.debug(`scroll_page: ${pageError || 'No page found for ' + hostname}`);
|
|
204
204
|
return new InformationalResponse(
|
|
205
205
|
isConnectionLost ? `Page connection lost for ${hostname}` : `No open page found for ${hostname}`,
|
|
206
206
|
isConnectionLost
|
|
@@ -219,7 +219,7 @@ export async function scrollPage({ url, direction, amount = 500, selector, x, y
|
|
|
219
219
|
// Determine scroll mode and execute
|
|
220
220
|
if (selector) {
|
|
221
221
|
// Scroll to element mode
|
|
222
|
-
logger.
|
|
222
|
+
logger.debug(`scroll_page: Scrolling to element: ${selector}`);
|
|
223
223
|
|
|
224
224
|
const elementExists = await page.$(selector);
|
|
225
225
|
if (!elementExists) {
|
|
@@ -243,7 +243,7 @@ export async function scrollPage({ url, direction, amount = 500, selector, x, y
|
|
|
243
243
|
|
|
244
244
|
} else if (typeof x === 'number' && typeof y === 'number') {
|
|
245
245
|
// Absolute position mode
|
|
246
|
-
logger.
|
|
246
|
+
logger.debug(`scroll_page: Scrolling to absolute position: (${x}, ${y})`);
|
|
247
247
|
|
|
248
248
|
await page.evaluate(({ scrollX, scrollY }) => {
|
|
249
249
|
window.scrollTo(scrollX, scrollY);
|
|
@@ -251,7 +251,7 @@ export async function scrollPage({ url, direction, amount = 500, selector, x, y
|
|
|
251
251
|
|
|
252
252
|
} else if (direction) {
|
|
253
253
|
// Directional scroll mode
|
|
254
|
-
logger.
|
|
254
|
+
logger.debug(`scroll_page: Scrolling ${direction} by ${amount}px`);
|
|
255
255
|
|
|
256
256
|
const scrollDeltas = {
|
|
257
257
|
up: { x: 0, y: -amount },
|
|
@@ -271,7 +271,7 @@ export async function scrollPage({ url, direction, amount = 500, selector, x, y
|
|
|
271
271
|
|
|
272
272
|
} else {
|
|
273
273
|
// No scroll parameters provided - just return current position
|
|
274
|
-
logger.
|
|
274
|
+
logger.debug(`scroll_page: No scroll action specified, returning current position`);
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// Small delay to let scroll complete
|
|
@@ -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
|
-
'
|
|
152
|
+
'The browser must be running with remote debugging enabled.',
|
|
153
153
|
[
|
|
154
|
-
'Ensure
|
|
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
|
]
|
|
@@ -163,7 +163,7 @@ export async function takeScreenshot({ url, fullPage = false }) {
|
|
|
163
163
|
|
|
164
164
|
if (!page) {
|
|
165
165
|
const isConnectionLost = pageError && pageError.includes('connection');
|
|
166
|
-
logger.
|
|
166
|
+
logger.debug(`take_screenshot: ${pageError || 'No page found for ' + hostname}`);
|
|
167
167
|
return new InformationalResponse(
|
|
168
168
|
isConnectionLost ? `Page connection lost for ${hostname}` : `No open page found for ${hostname}`,
|
|
169
169
|
isConnectionLost
|
package/src/actions/type-text.js
CHANGED
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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", "
|
|
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
|
|
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.
|
|
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,
|
|
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:
|
|
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 (!
|
|
133
|
-
throw new Error("
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
'
|
|
175
|
+
'The browser must be running with remote debugging enabled.',
|
|
155
176
|
[
|
|
156
|
-
'Ensure
|
|
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
|
]
|
|
@@ -165,7 +186,7 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
|
|
|
165
186
|
|
|
166
187
|
if (!page) {
|
|
167
188
|
const isConnectionLost = pageError && pageError.includes('connection');
|
|
168
|
-
logger.
|
|
189
|
+
logger.debug(`type_text: ${pageError || 'No page found for ' + hostname}`);
|
|
169
190
|
return new InformationalResponse(
|
|
170
191
|
isConnectionLost ? `Page connection lost for ${hostname}` : `No open page found for ${hostname}`,
|
|
171
192
|
isConnectionLost
|
|
@@ -178,20 +199,36 @@ 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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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.debug(`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
|
-
|
|
190
|
-
|
|
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.)
|
|
194
|
-
logger.
|
|
231
|
+
logger.debug('Waiting for page stability after typing...');
|
|
195
232
|
await waitForPageStability(page);
|
|
196
233
|
|
|
197
234
|
// Wait for SPAs to render dynamic content after typing
|
|
@@ -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 ${
|
|
242
|
+
logger.info(`type_text completed: typed into ${fieldsSummary}`);
|
|
206
243
|
|
|
207
244
|
return new TypeTextSuccessResponse(
|
|
208
245
|
currentUrl,
|
|
209
|
-
`Typed text into: ${
|
|
246
|
+
`Typed text into: ${fieldsSummary}`,
|
|
210
247
|
html,
|
|
211
248
|
[
|
|
212
249
|
"Use MCPBrowser's type_text to fill additional fields",
|
|
@@ -218,7 +255,7 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
|
|
|
218
255
|
);
|
|
219
256
|
} else {
|
|
220
257
|
// Wait for page to stabilize even without returning HTML
|
|
221
|
-
logger.
|
|
258
|
+
logger.debug('Waiting for page stability after typing (fast mode)...');
|
|
222
259
|
await waitForPageStability(page);
|
|
223
260
|
|
|
224
261
|
// Wait for SPAs to render dynamic content after typing
|
|
@@ -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 ${
|
|
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: ${
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
"
|
|
254
|
-
|
|
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
|
+
}
|
package/src/core/browser.js
CHANGED
|
@@ -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
|
}
|
package/src/core/eula.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EULA (End User License Agreement) Management
|
|
3
|
+
* Tracks whether the user has accepted the EULA.
|
|
4
|
+
* EULA acceptance is persisted to disk and remembered across sessions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import logger from './logger.js';
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
|
|
12
|
+
// EULA URL - where the full license agreement can be found
|
|
13
|
+
export const EULA_URL = 'https://github.com/cherchyk/MCPBrowser/blob/main/EULA.md';
|
|
14
|
+
|
|
15
|
+
// Config directory and file paths
|
|
16
|
+
const CONFIG_DIR = join(homedir(), '.mcpbrowser');
|
|
17
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
18
|
+
|
|
19
|
+
// In-memory cache of EULA acceptance status
|
|
20
|
+
let eulaAccepted = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load config from disk
|
|
24
|
+
* @returns {Object} Config object
|
|
25
|
+
*/
|
|
26
|
+
function loadConfig() {
|
|
27
|
+
try {
|
|
28
|
+
if (existsSync(CONFIG_FILE)) {
|
|
29
|
+
const data = readFileSync(CONFIG_FILE, 'utf-8');
|
|
30
|
+
return JSON.parse(data);
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logger.warn(`Failed to load config: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Save config to disk
|
|
40
|
+
* @param {Object} config - Config object to save
|
|
41
|
+
*/
|
|
42
|
+
function saveConfig(config) {
|
|
43
|
+
try {
|
|
44
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
45
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf-8');
|
|
48
|
+
logger.info(`Config saved to ${CONFIG_FILE}`);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error(`Failed to save config: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Initialize EULA status from persisted config
|
|
56
|
+
*/
|
|
57
|
+
function initEulaStatus() {
|
|
58
|
+
if (eulaAccepted === null) {
|
|
59
|
+
const config = loadConfig();
|
|
60
|
+
eulaAccepted = config.eulaAccepted === true;
|
|
61
|
+
if (eulaAccepted) {
|
|
62
|
+
logger.info('EULA previously accepted, loaded from config');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if the EULA has been accepted
|
|
69
|
+
* @returns {boolean} True if EULA has been accepted
|
|
70
|
+
*/
|
|
71
|
+
export function isEulaAccepted() {
|
|
72
|
+
initEulaStatus();
|
|
73
|
+
return eulaAccepted;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Accept the EULA (persisted across sessions)
|
|
78
|
+
* @param {string} eulaUrl - The EULA URL being accepted
|
|
79
|
+
*/
|
|
80
|
+
export function acceptEula(eulaUrl) {
|
|
81
|
+
eulaAccepted = true;
|
|
82
|
+
const config = loadConfig();
|
|
83
|
+
config.eulaAccepted = true;
|
|
84
|
+
config.eulaAcceptedAt = new Date().toISOString();
|
|
85
|
+
config.eulaUrl = eulaUrl;
|
|
86
|
+
saveConfig(config);
|
|
87
|
+
logger.info(`EULA accepted and persisted (${eulaUrl})`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Reset EULA acceptance (clears both memory and persisted state)
|
|
92
|
+
* @param {boolean} [persistReset=true] - Whether to also clear the persisted config
|
|
93
|
+
*/
|
|
94
|
+
export function resetEula(persistReset = true) {
|
|
95
|
+
eulaAccepted = false;
|
|
96
|
+
if (persistReset) {
|
|
97
|
+
const config = loadConfig();
|
|
98
|
+
delete config.eulaAccepted;
|
|
99
|
+
delete config.eulaAcceptedAt;
|
|
100
|
+
delete config.eulaUrl;
|
|
101
|
+
saveConfig(config);
|
|
102
|
+
logger.debug('EULA acceptance reset (memory and disk)');
|
|
103
|
+
} else {
|
|
104
|
+
logger.debug('EULA acceptance reset (memory only)');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get EULA status summary
|
|
110
|
+
* @returns {{ accepted: boolean, acceptedAt: string|null, eulaUrl: string }} Current EULA status
|
|
111
|
+
*/
|
|
112
|
+
export function getEulaStatus() {
|
|
113
|
+
initEulaStatus();
|
|
114
|
+
const config = loadConfig();
|
|
115
|
+
return {
|
|
116
|
+
accepted: eulaAccepted,
|
|
117
|
+
acceptedAt: config.eulaAcceptedAt || null,
|
|
118
|
+
eulaUrl: EULA_URL
|
|
119
|
+
};
|
|
120
|
+
}
|
package/src/core/logger.js
CHANGED
|
@@ -14,6 +14,14 @@ function info(message) {
|
|
|
14
14
|
console.error(`${PREFIX} ${message}`);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Log a warning message
|
|
19
|
+
* @param {string} message - The message to log
|
|
20
|
+
*/
|
|
21
|
+
function warn(message) {
|
|
22
|
+
console.error(`${PREFIX} ⚠️ ${message}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
17
25
|
/**
|
|
18
26
|
* Log an error message
|
|
19
27
|
* @param {string} message - The message to log
|
|
@@ -22,5 +30,13 @@ function error(message) {
|
|
|
22
30
|
console.error(`${PREFIX} ❌ ${message}`);
|
|
23
31
|
}
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Log a debug message
|
|
35
|
+
* @param {string} message - The message to log
|
|
36
|
+
*/
|
|
37
|
+
function debug(message) {
|
|
38
|
+
console.error(`${PREFIX} 🔍 ${message}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const logger = { info, warn, error, debug };
|
|
26
42
|
export default logger;
|
package/src/core/page.js
CHANGED
|
@@ -43,7 +43,7 @@ async function processQueue() {
|
|
|
43
43
|
const queueLength = requestQueue.length;
|
|
44
44
|
|
|
45
45
|
if (queueLength > 0) {
|
|
46
|
-
logger.
|
|
46
|
+
logger.debug(`Queue: ${queueLength} requests waiting`);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
try {
|
|
@@ -285,7 +285,7 @@ export async function waitForPageReady(page) {
|
|
|
285
285
|
const spaCheck = await isItSPA(page);
|
|
286
286
|
|
|
287
287
|
if (spaCheck.isSPA) {
|
|
288
|
-
logger.
|
|
288
|
+
logger.debug(`SPA detected: ${spaCheck.indicators.join(', ')}`);
|
|
289
289
|
|
|
290
290
|
// Wait for SPA to render
|
|
291
291
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
@@ -296,7 +296,7 @@ export async function waitForPageReady(page) {
|
|
|
296
296
|
} catch {
|
|
297
297
|
// OK if timeout - SPA might have websockets or long-polling
|
|
298
298
|
}
|
|
299
|
-
logger.
|
|
299
|
+
logger.debug('SPA content ready');
|
|
300
300
|
} else {
|
|
301
301
|
// For non-SPAs, just wait briefly for any pending network requests
|
|
302
302
|
try {
|
|
@@ -314,17 +314,17 @@ export async function waitForPageReady(page) {
|
|
|
314
314
|
* @returns {Promise<void>}
|
|
315
315
|
*/
|
|
316
316
|
export async function waitForPageStability(page) {
|
|
317
|
-
logger.
|
|
317
|
+
logger.debug('Waiting for page stability (network idle)...');
|
|
318
318
|
|
|
319
319
|
// Give time for any triggered actions to complete
|
|
320
320
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
321
321
|
|
|
322
322
|
try {
|
|
323
323
|
await page.waitForNetworkIdle({ timeout: 5000 });
|
|
324
|
-
logger.
|
|
324
|
+
logger.debug('Page stabilized');
|
|
325
325
|
} catch {
|
|
326
326
|
// Ignore timeout - page may have long-polling or websockets
|
|
327
|
-
logger.
|
|
327
|
+
logger.debug('Network still active, continuing anyway');
|
|
328
328
|
}
|
|
329
329
|
}
|
|
330
330
|
|
package/src/mcp-browser.js
CHANGED
|
@@ -16,6 +16,9 @@ import { dirname, join } from 'path';
|
|
|
16
16
|
import { ErrorResponse } from './core/responses.js';
|
|
17
17
|
import logger from './core/logger.js';
|
|
18
18
|
|
|
19
|
+
// Import EULA functionality
|
|
20
|
+
import { handleAcceptEula, ACCEPT_EULA_TOOL, requireEulaAcceptance } from './actions/accept-eula.js';
|
|
21
|
+
|
|
19
22
|
// Import core functionality
|
|
20
23
|
import { fetchPage, FETCH_WEBPAGE_TOOL } from './actions/fetch-page.js';
|
|
21
24
|
import { clickElement, CLICK_ELEMENT_TOOL } from './actions/click-element.js';
|
|
@@ -47,7 +50,9 @@ async function main() {
|
|
|
47
50
|
const server = new Server({ name: "MCP Browser", version: packageJson.version }, { capabilities: { tools: {} } });
|
|
48
51
|
|
|
49
52
|
// Assemble tools from action imports
|
|
53
|
+
// ACCEPT_EULA_TOOL must be first - it's required before using other tools
|
|
50
54
|
const tools = [
|
|
55
|
+
ACCEPT_EULA_TOOL,
|
|
51
56
|
FETCH_WEBPAGE_TOOL,
|
|
52
57
|
CLICK_ELEMENT_TOOL,
|
|
53
58
|
TYPE_TEXT_TOOL,
|
|
@@ -66,7 +71,17 @@ async function main() {
|
|
|
66
71
|
let result;
|
|
67
72
|
|
|
68
73
|
try {
|
|
74
|
+
// EULA check - accept_eula is always allowed, other tools require EULA acceptance
|
|
75
|
+
if (name !== "accept_eula") {
|
|
76
|
+
const eulaResponse = requireEulaAcceptance(name);
|
|
77
|
+
if (eulaResponse) return eulaResponse;
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
switch (name) {
|
|
81
|
+
case "accept_eula":
|
|
82
|
+
result = await handleAcceptEula(safeArgs);
|
|
83
|
+
break;
|
|
84
|
+
|
|
70
85
|
case "fetch_webpage":
|
|
71
86
|
result = await fetchPage(safeArgs);
|
|
72
87
|
break;
|
|
@@ -106,7 +121,7 @@ async function main() {
|
|
|
106
121
|
// Return a proper error response instead of throwing
|
|
107
122
|
return new ErrorResponse(
|
|
108
123
|
`${name} failed: ${error.message}`,
|
|
109
|
-
['Check browser is installed', 'Try specifying browser parameter explicitly (chrome or
|
|
124
|
+
['Check browser is installed', 'Try specifying browser parameter explicitly (chrome, edge, or brave)', 'Check MCP server logs for details']
|
|
110
125
|
).toMcpFormat();
|
|
111
126
|
}
|
|
112
127
|
|
|
@@ -144,7 +159,8 @@ export {
|
|
|
144
159
|
closeTab,
|
|
145
160
|
getCurrentHtml,
|
|
146
161
|
takeScreenshot,
|
|
147
|
-
scrollPage
|
|
162
|
+
scrollPage,
|
|
163
|
+
handleAcceptEula
|
|
148
164
|
};
|
|
149
165
|
|
|
150
166
|
// Run the MCP server only if this is the main module (not imported for testing)
|