mcpbrowser 0.3.8 → 0.3.10
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 +23 -1
- package/package.json +1 -1
- package/src/actions/click-element.js +12 -0
- package/src/actions/close-tab.js +6 -2
- package/src/actions/fetch-page.js +17 -7
- package/src/actions/get-current-html.js +8 -0
- package/src/actions/type-text.js +13 -0
- package/src/browsers/ChromiumBrowser.js +3 -2
- package/src/core/auth.js +7 -9
- package/src/core/browser.js +7 -6
- package/src/core/logger.js +26 -0
- package/src/core/page.js +12 -12
- package/src/mcp-browser.js +3 -1
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# MCPBrowser (MCP Browser)
|
|
1
|
+
# ✅ MCPBrowser (MCP Browser)
|
|
2
2
|
|
|
3
3
|
[](https://marketplace.visualstudio.com/items?itemName=cherchyk.mcpbrowser)
|
|
4
4
|
[](https://www.npmjs.com/package/mcpbrowser)
|
|
@@ -270,6 +270,28 @@ Environment variables for advanced setup:
|
|
|
270
270
|
| `CHROME_USER_DATA_DIR` | Browser profile directory | `%LOCALAPPDATA%/ChromeAuthProfile` |
|
|
271
271
|
| `CHROME_REMOTE_DEBUG_PORT` | DevTools port | `9222` |
|
|
272
272
|
|
|
273
|
+
## Observability & Logging
|
|
274
|
+
|
|
275
|
+
MCPBrowser logs all operations to help you understand what's happening:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
[MCPBrowser] fetch_webpage called: url=https://example.com
|
|
279
|
+
[MCPBrowser] Tab created: example.com
|
|
280
|
+
[MCPBrowser] Navigating to: https://example.com
|
|
281
|
+
[MCPBrowser] Navigation complete: https://example.com (1234ms)
|
|
282
|
+
[MCPBrowser] SPA detected: React, minimal content (0 chars)
|
|
283
|
+
[MCPBrowser] SPA content ready
|
|
284
|
+
[MCPBrowser] fetch_webpage completed: https://example.com
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Error messages are marked with ❌:**
|
|
288
|
+
```
|
|
289
|
+
[MCPBrowser] ❌ fetch_webpage failed: net::ERR_NAME_NOT_RESOLVED
|
|
290
|
+
[MCPBrowser] ❌ No open page found for example.com
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Logs go to `stderr` so they don't interfere with MCP protocol on `stdout`.
|
|
294
|
+
|
|
273
295
|
## Troubleshooting
|
|
274
296
|
|
|
275
297
|
**Browser doesn't open?**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpbrowser",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"mcpName": "io.github.cherchyk/mcpbrowser",
|
|
5
5
|
"type": "module",
|
|
6
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.",
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
import { getBrowser, domainPages } from '../core/browser.js';
|
|
28
28
|
import { extractAndProcessHtml, waitForPageStability } from '../core/page.js';
|
|
29
29
|
import { MCPResponse, ErrorResponse } from '../core/responses.js';
|
|
30
|
+
import logger from '../core/logger.js';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -153,6 +154,8 @@ export const CLICK_ELEMENT_TOOL = {
|
|
|
153
154
|
* });
|
|
154
155
|
*/
|
|
155
156
|
export async function clickElement({ url, selector, text, waitForElementTimeout = 30000, returnHtml = true, removeUnnecessaryHTML = true, postClickWait = 1000 }) {
|
|
157
|
+
logger.info(`click_element called: ${selector || `text="${text}"`}`);
|
|
158
|
+
|
|
156
159
|
if (!url) {
|
|
157
160
|
throw new Error("url parameter is required");
|
|
158
161
|
}
|
|
@@ -172,6 +175,7 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
172
175
|
let page = domainPages.get(hostname);
|
|
173
176
|
|
|
174
177
|
if (!page || page.isClosed()) {
|
|
178
|
+
logger.error(`No open page found for ${hostname}`);
|
|
175
179
|
return new ErrorResponse(
|
|
176
180
|
`No open page found for ${hostname}. Please fetch the page first using fetch_webpage.`,
|
|
177
181
|
[
|
|
@@ -232,11 +236,13 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
232
236
|
// await page.evaluate(el => el.scrollIntoView({ behavior: 'smooth', block: 'center' }), elementHandle);
|
|
233
237
|
// await new Promise(r => setTimeout(r, 300)); // Brief delay after scroll
|
|
234
238
|
|
|
239
|
+
logger.info(`Clicking: ${selector || `text="${text}"`}`);
|
|
235
240
|
await elementHandle.click();
|
|
236
241
|
|
|
237
242
|
if (returnHtml) {
|
|
238
243
|
// Wait for page to stabilize (handles both navigation and SPA content updates)
|
|
239
244
|
// This ensures content is fully loaded before returning, just like fetch_webpage does
|
|
245
|
+
logger.info('Waiting for page stability...');
|
|
240
246
|
await waitForPageStability(page);
|
|
241
247
|
|
|
242
248
|
// Wait for SPAs to render dynamic content after click
|
|
@@ -247,6 +253,8 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
247
253
|
const currentUrl = page.url();
|
|
248
254
|
const html = await extractAndProcessHtml(page, removeUnnecessaryHTML);
|
|
249
255
|
|
|
256
|
+
logger.info(`click_element completed: ${selector || `text="${text}"`}`);
|
|
257
|
+
|
|
250
258
|
return new ClickElementSuccessResponse(
|
|
251
259
|
currentUrl,
|
|
252
260
|
selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`,
|
|
@@ -260,6 +268,7 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
260
268
|
);
|
|
261
269
|
} else {
|
|
262
270
|
// Wait for page to stabilize even for fast clicks (ensures JS has finished)
|
|
271
|
+
logger.info('Waiting for page stability (fast mode)...');
|
|
263
272
|
await waitForPageStability(page);
|
|
264
273
|
|
|
265
274
|
// Wait for SPAs to render dynamic content after click
|
|
@@ -269,6 +278,8 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
269
278
|
|
|
270
279
|
const currentUrl = page.url();
|
|
271
280
|
|
|
281
|
+
logger.info(`click_element completed: ${selector || `text="${text}"`}`);
|
|
282
|
+
|
|
272
283
|
return new ClickElementSuccessResponse(
|
|
273
284
|
currentUrl,
|
|
274
285
|
selector ? `Clicked element: ${selector}` : `Clicked element with text: "${text}"`,
|
|
@@ -281,6 +292,7 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
|
|
|
281
292
|
);
|
|
282
293
|
}
|
|
283
294
|
} catch (err) {
|
|
295
|
+
logger.error(`click_element failed: ${err.message}`);
|
|
284
296
|
return new ErrorResponse(
|
|
285
297
|
`Failed to click element: ${err.message}`,
|
|
286
298
|
[
|
package/src/actions/close-tab.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { domainPages } from '../core/browser.js';
|
|
6
6
|
import { MCPResponse, ErrorResponse } from '../core/responses.js';
|
|
7
|
+
import logger from '../core/logger.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -95,6 +96,9 @@ export const CLOSE_TAB_TOOL = {
|
|
|
95
96
|
* @returns {Promise<object>} Result indicating success or failure
|
|
96
97
|
*/
|
|
97
98
|
export async function closeTab({ url }) {
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
logger.info(`close_tab called: url=${url}`);
|
|
101
|
+
|
|
98
102
|
try {
|
|
99
103
|
// Validate URL
|
|
100
104
|
if (!url || typeof url !== 'string') {
|
|
@@ -171,7 +175,7 @@ export async function closeTab({ url }) {
|
|
|
171
175
|
// Remove from domain pool
|
|
172
176
|
domainPages.delete(hostname);
|
|
173
177
|
|
|
174
|
-
|
|
178
|
+
logger.info(`close_tab completed: closed tab for ${hostname}`);
|
|
175
179
|
|
|
176
180
|
return new CloseTabSuccessResponse(
|
|
177
181
|
`Successfully closed tab for ${hostname}`,
|
|
@@ -182,7 +186,7 @@ export async function closeTab({ url }) {
|
|
|
182
186
|
);
|
|
183
187
|
|
|
184
188
|
} catch (error) {
|
|
185
|
-
|
|
189
|
+
logger.error(`close_tab failed: ${error.message}`);
|
|
186
190
|
return new ErrorResponse(
|
|
187
191
|
error.message,
|
|
188
192
|
[
|
|
@@ -7,6 +7,7 @@ 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
9
|
import { MCPResponse, ErrorResponse } from '../core/responses.js';
|
|
10
|
+
import logger from '../core/logger.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -113,12 +114,15 @@ export const FETCH_WEBPAGE_TOOL = {
|
|
|
113
114
|
* @returns {Promise<Object>} Result object with success status, URL, HTML content, or error details
|
|
114
115
|
*/
|
|
115
116
|
export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = true, postLoadWait = 0 }) {
|
|
117
|
+
logger.info(`fetch_webpage called: url=${url}`);
|
|
118
|
+
|
|
116
119
|
// Handle missing URL with environment variable fallback
|
|
117
120
|
if (!url) {
|
|
118
121
|
const fallbackUrl = process.env.DEFAULT_FETCH_URL || process.env.MCP_DEFAULT_FETCH_URL;
|
|
119
122
|
if (fallbackUrl) {
|
|
120
123
|
url = fallbackUrl;
|
|
121
124
|
} else {
|
|
125
|
+
logger.error("Missing url parameter");
|
|
122
126
|
return new ErrorResponse(
|
|
123
127
|
"Missing url parameter and no DEFAULT_FETCH_URL/MCP_DEFAULT_FETCH_URL configured",
|
|
124
128
|
["Set DEFAULT_FETCH_URL or MCP_DEFAULT_FETCH_URL environment variable", "Provide url parameter in the request"]
|
|
@@ -131,6 +135,7 @@ export async function fetchPage({ url, browser = '', removeUnnecessaryHTML = tru
|
|
|
131
135
|
try {
|
|
132
136
|
hostname = new URL(url).hostname;
|
|
133
137
|
} catch {
|
|
138
|
+
logger.error(`Invalid URL: ${url}`);
|
|
134
139
|
return new ErrorResponse(`Invalid URL: ${url}`, []);
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -170,7 +175,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
170
175
|
const redirectInfo = detectRedirectType(url, hostname, currentUrl, currentHostname);
|
|
171
176
|
|
|
172
177
|
if (redirectInfo.type === 'requested_auth') {
|
|
173
|
-
|
|
178
|
+
logger.info('User requested auth page directly, returning content');
|
|
174
179
|
// Update domain mapping if needed
|
|
175
180
|
if (redirectInfo.currentHostname !== hostname) {
|
|
176
181
|
domainPages.delete(hostname);
|
|
@@ -178,13 +183,13 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
178
183
|
hostname = redirectInfo.currentHostname;
|
|
179
184
|
}
|
|
180
185
|
} else if (redirectInfo.type === 'permanent') {
|
|
181
|
-
|
|
186
|
+
logger.info(`Redirect: ${hostname} → ${redirectInfo.currentHostname}`);
|
|
182
187
|
|
|
183
188
|
// Check if we already have a tab for the redirected hostname
|
|
184
189
|
// (can happen after reconnect - we mapped mail.google.com but not gmail.com)
|
|
185
190
|
const existingPage = domainPages.get(redirectInfo.currentHostname);
|
|
186
191
|
if (existingPage && existingPage !== page && !existingPage.isClosed()) {
|
|
187
|
-
|
|
192
|
+
logger.info(`Found existing tab for ${redirectInfo.currentHostname}, reusing it`);
|
|
188
193
|
// Close the new tab we just opened, use the existing one
|
|
189
194
|
await page.close().catch(() => {});
|
|
190
195
|
domainPages.delete(hostname);
|
|
@@ -192,20 +197,20 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
192
197
|
// Map original hostname to existing page
|
|
193
198
|
domainPages.set(hostname, existingPage);
|
|
194
199
|
} else {
|
|
195
|
-
console.error(`[MCPBrowser] Mapping both hostnames to same tab for future reuse`);
|
|
196
200
|
// Map both original and final hostname to the same page
|
|
197
201
|
domainPages.set(hostname, page);
|
|
198
202
|
domainPages.set(redirectInfo.currentHostname, page);
|
|
199
203
|
}
|
|
200
204
|
hostname = redirectInfo.currentHostname;
|
|
201
205
|
} else if (redirectInfo.type === 'auth') {
|
|
202
|
-
|
|
203
|
-
|
|
206
|
+
logger.info(`Authentication required: ${redirectInfo.flowType}`);
|
|
207
|
+
logger.info(`Auth URL: ${redirectInfo.currentUrl}`);
|
|
204
208
|
|
|
205
209
|
// Try auto-auth first (check if existing session works)
|
|
206
210
|
const autoAuthResult = await waitForAutoAuth(page);
|
|
207
211
|
|
|
208
212
|
if (autoAuthResult.success) {
|
|
213
|
+
logger.info(`Auto-auth successful, now at: ${page.url()}`);
|
|
209
214
|
// Update hostname to where we landed
|
|
210
215
|
if (autoAuthResult.hostname !== hostname) {
|
|
211
216
|
domainPages.delete(hostname);
|
|
@@ -217,6 +222,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
217
222
|
const manualAuthResult = await waitForManualAuth(page, authCompletionTimeout);
|
|
218
223
|
|
|
219
224
|
if (!manualAuthResult.success) {
|
|
225
|
+
logger.error(`Authentication failed: ${manualAuthResult.error}`);
|
|
220
226
|
return new ErrorResponse(
|
|
221
227
|
manualAuthResult.error,
|
|
222
228
|
[
|
|
@@ -233,6 +239,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
233
239
|
domainPages.set(manualAuthResult.hostname, page);
|
|
234
240
|
hostname = manualAuthResult.hostname;
|
|
235
241
|
}
|
|
242
|
+
logger.info(`Authentication successful, now at: ${page.url()}`);
|
|
236
243
|
}
|
|
237
244
|
|
|
238
245
|
// Wait for page stability after auth
|
|
@@ -241,13 +248,15 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
241
248
|
|
|
242
249
|
// Additional wait if requested (for pages that need extra time)
|
|
243
250
|
if (postLoadWait > 0) {
|
|
244
|
-
|
|
251
|
+
logger.info(`Waiting ${postLoadWait}ms (postLoadWait)...`);
|
|
245
252
|
await new Promise(resolve => setTimeout(resolve, postLoadWait));
|
|
246
253
|
}
|
|
247
254
|
|
|
248
255
|
// Extract and process HTML
|
|
249
256
|
const processedHtml = await extractAndProcessHtml(page, removeUnnecessaryHTML);
|
|
250
257
|
|
|
258
|
+
logger.info(`fetch_webpage completed: ${page.url()}`);
|
|
259
|
+
|
|
251
260
|
return new FetchPageSuccessResponse(
|
|
252
261
|
page.url(),
|
|
253
262
|
processedHtml,
|
|
@@ -259,6 +268,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
|
|
|
259
268
|
]
|
|
260
269
|
);
|
|
261
270
|
} catch (err) {
|
|
271
|
+
logger.error(`fetch_webpage failed: ${err.message || String(err)}`);
|
|
262
272
|
return new ErrorResponse(
|
|
263
273
|
err.message || String(err),
|
|
264
274
|
[
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { getBrowser, domainPages } from '../core/browser.js';
|
|
6
6
|
import { extractAndProcessHtml } from '../core/page.js';
|
|
7
7
|
import { MCPResponse, ErrorResponse } from '../core/responses.js';
|
|
8
|
+
import logger from '../core/logger.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -98,6 +99,9 @@ export const GET_CURRENT_HTML_TOOL = {
|
|
|
98
99
|
* @returns {Promise<Object>} Result object with current HTML
|
|
99
100
|
*/
|
|
100
101
|
export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
|
|
102
|
+
const startTime = Date.now();
|
|
103
|
+
logger.info(`get_current_html called: url=${url}`);
|
|
104
|
+
|
|
101
105
|
if (!url) {
|
|
102
106
|
throw new Error("url parameter is required");
|
|
103
107
|
}
|
|
@@ -113,6 +117,7 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
|
|
|
113
117
|
let page = domainPages.get(hostname);
|
|
114
118
|
|
|
115
119
|
if (!page || page.isClosed()) {
|
|
120
|
+
logger.error(`get_current_html: No open page found for ${hostname}`);
|
|
116
121
|
return new ErrorResponse(
|
|
117
122
|
`No open page found for ${hostname}. Please fetch the page first using fetch_webpage.`,
|
|
118
123
|
[
|
|
@@ -125,6 +130,8 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
|
|
|
125
130
|
const currentUrl = page.url();
|
|
126
131
|
const html = await extractAndProcessHtml(page, removeUnnecessaryHTML);
|
|
127
132
|
|
|
133
|
+
logger.info(`get_current_html completed: got HTML from ${currentUrl}`);
|
|
134
|
+
|
|
128
135
|
return new GetCurrentHtmlSuccessResponse(
|
|
129
136
|
currentUrl,
|
|
130
137
|
html,
|
|
@@ -135,6 +142,7 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
|
|
|
135
142
|
]
|
|
136
143
|
);
|
|
137
144
|
} catch (err) {
|
|
145
|
+
logger.error(`get_current_html failed: ${err.message}`);
|
|
138
146
|
return new ErrorResponse(
|
|
139
147
|
`Failed to get HTML: ${err.message}`,
|
|
140
148
|
[
|
package/src/actions/type-text.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { getBrowser, domainPages } from '../core/browser.js';
|
|
6
6
|
import { extractAndProcessHtml, waitForPageStability } from '../core/page.js';
|
|
7
7
|
import { MCPResponse, ErrorResponse } from '../core/responses.js';
|
|
8
|
+
import logger from '../core/logger.js';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
|
|
@@ -121,6 +122,9 @@ export const TYPE_TEXT_TOOL = {
|
|
|
121
122
|
* @returns {Promise<Object>} Result object with success status and details
|
|
122
123
|
*/
|
|
123
124
|
export async function typeText({ url, selector, text, clear = true, typeDelay = 50, waitForElementTimeout = 30000, returnHtml = true, removeUnnecessaryHTML = true, postTypeWait = 1000 }) {
|
|
125
|
+
const startTime = Date.now();
|
|
126
|
+
logger.info(`type_text called: selector=${selector}, url=${url}`);
|
|
127
|
+
|
|
124
128
|
if (!url) {
|
|
125
129
|
throw new Error("url parameter is required");
|
|
126
130
|
}
|
|
@@ -144,6 +148,7 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
|
|
|
144
148
|
let page = domainPages.get(hostname);
|
|
145
149
|
|
|
146
150
|
if (!page || page.isClosed()) {
|
|
151
|
+
logger.error(`type_text: No open page found for ${hostname}`);
|
|
147
152
|
return new ErrorResponse(
|
|
148
153
|
`No open page found for ${hostname}. Please fetch the page first using fetch_webpage_protected.`,
|
|
149
154
|
[
|
|
@@ -160,10 +165,12 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
|
|
|
160
165
|
await page.keyboard.press('Backspace');
|
|
161
166
|
}
|
|
162
167
|
|
|
168
|
+
logger.info(`Typing into: ${selector}`);
|
|
163
169
|
await page.type(selector, String(text), { delay: typeDelay });
|
|
164
170
|
|
|
165
171
|
if (returnHtml) {
|
|
166
172
|
// Wait for page to stabilize (handles form validation, autocomplete, etc.)
|
|
173
|
+
logger.info('Waiting for page stability after typing...');
|
|
167
174
|
await waitForPageStability(page);
|
|
168
175
|
|
|
169
176
|
// Wait for SPAs to render dynamic content after typing
|
|
@@ -174,6 +181,8 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
|
|
|
174
181
|
const currentUrl = page.url();
|
|
175
182
|
const html = await extractAndProcessHtml(page, removeUnnecessaryHTML);
|
|
176
183
|
|
|
184
|
+
logger.info(`type_text completed: typed into ${selector}`);
|
|
185
|
+
|
|
177
186
|
return new TypeTextSuccessResponse(
|
|
178
187
|
currentUrl,
|
|
179
188
|
`Typed text into: ${selector}`,
|
|
@@ -187,6 +196,7 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
|
|
|
187
196
|
);
|
|
188
197
|
} else {
|
|
189
198
|
// Wait for page to stabilize even without returning HTML
|
|
199
|
+
logger.info('Waiting for page stability after typing (fast mode)...');
|
|
190
200
|
await waitForPageStability(page);
|
|
191
201
|
|
|
192
202
|
// Wait for SPAs to render dynamic content after typing
|
|
@@ -196,6 +206,8 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
|
|
|
196
206
|
|
|
197
207
|
const currentUrl = page.url();
|
|
198
208
|
|
|
209
|
+
logger.info(`type_text completed: typed into ${selector} (no HTML)`);
|
|
210
|
+
|
|
199
211
|
return new TypeTextSuccessResponse(
|
|
200
212
|
currentUrl,
|
|
201
213
|
`Typed text into: ${selector}`,
|
|
@@ -208,6 +220,7 @@ export async function typeText({ url, selector, text, clear = true, typeDelay =
|
|
|
208
220
|
);
|
|
209
221
|
}
|
|
210
222
|
} catch (err) {
|
|
223
|
+
logger.error(`type_text failed: ${err.message}`);
|
|
211
224
|
return new ErrorResponse(
|
|
212
225
|
`Failed to type text: ${err.message}`,
|
|
213
226
|
[
|
|
@@ -10,6 +10,7 @@ import { existsSync } from "fs";
|
|
|
10
10
|
import os from "os";
|
|
11
11
|
import path from "path";
|
|
12
12
|
import { spawn } from "child_process";
|
|
13
|
+
import logger from '../core/logger.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Base class for all Chromium-based browsers
|
|
@@ -115,7 +116,7 @@ export class ChromiumBrowser extends BaseBrowser {
|
|
|
115
116
|
'--no-default-browser-check'
|
|
116
117
|
];
|
|
117
118
|
|
|
118
|
-
|
|
119
|
+
logger.info(`Launching ${this.config.name} with remote debugging on port ${this.config.port}...`);
|
|
119
120
|
|
|
120
121
|
const child = spawn(execPath, args, {
|
|
121
122
|
detached: true,
|
|
@@ -129,7 +130,7 @@ export class ChromiumBrowser extends BaseBrowser {
|
|
|
129
130
|
|
|
130
131
|
while (Date.now() - startTime < maxWaitTime) {
|
|
131
132
|
if (await this.devtoolsAvailable()) {
|
|
132
|
-
|
|
133
|
+
logger.info(`Connected to ${this.config.name} on port ${this.config.port}`);
|
|
133
134
|
return;
|
|
134
135
|
}
|
|
135
136
|
await new Promise(resolve => setTimeout(resolve, 500));
|
package/src/core/auth.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { getBaseDomain } from '../utils.js';
|
|
6
|
+
import logger from './logger.js';
|
|
6
7
|
|
|
7
8
|
// ============================================================================
|
|
8
9
|
// AUTH URL DETECTION
|
|
@@ -123,7 +124,7 @@ export function detectRedirectType(url, hostname, currentUrl, currentHostname) {
|
|
|
123
124
|
* @returns {Promise<Object>} Object with success status and final hostname
|
|
124
125
|
*/
|
|
125
126
|
export async function waitForAutoAuth(page, timeoutMs = DEFAULT_AUTO_AUTH_TIMEOUT) {
|
|
126
|
-
|
|
127
|
+
logger.info(`Checking for auto-authentication (${timeoutMs}ms timeout)...`);
|
|
127
128
|
|
|
128
129
|
const deadline = Date.now() + timeoutMs;
|
|
129
130
|
|
|
@@ -135,7 +136,7 @@ export async function waitForAutoAuth(page, timeoutMs = DEFAULT_AUTO_AUTH_TIMEOU
|
|
|
135
136
|
// Browser handles redirects - we just need to detect when auth flow ends
|
|
136
137
|
if (!isLikelyAuthUrl(checkUrl)) {
|
|
137
138
|
const checkHostname = new URL(checkUrl).hostname;
|
|
138
|
-
|
|
139
|
+
logger.info(`Auto-authentication successful: ${checkUrl}`);
|
|
139
140
|
return { success: true, hostname: checkHostname };
|
|
140
141
|
}
|
|
141
142
|
|
|
@@ -241,12 +242,11 @@ export async function waitForManualAuth(page, timeoutMs = DEFAULT_MANUAL_AUTH_TI
|
|
|
241
242
|
|
|
242
243
|
// Log login page detection
|
|
243
244
|
if (isLoginPage && shouldExtendTimeout) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
console.error(`[MCPBrowser] Extended wait time to ${effectiveTimeoutMinutes} minutes for user authentication`);
|
|
245
|
+
logger.info(`Login page detected: ${page.url()} (${loginDetection.indicators.join(', ')})`);
|
|
246
|
+
logger.info(`Extended wait time to ${effectiveTimeoutMinutes} minutes for user authentication`);
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
logger.info(`Waiting for manual authentication (${effectiveTimeoutMinutes} min timeout, loginPage=${isLoginPage})...`);
|
|
250
250
|
|
|
251
251
|
// Send initial waiting notification
|
|
252
252
|
if (onStatusChange) {
|
|
@@ -262,8 +262,6 @@ export async function waitForManualAuth(page, timeoutMs = DEFAULT_MANUAL_AUTH_TI
|
|
|
262
262
|
});
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
-
console.error(`[MCPBrowser] Waiting for user to complete authentication (${effectiveTimeoutMinutes} min timeout)...`);
|
|
266
|
-
|
|
267
265
|
const deadline = Date.now() + effectiveTimeout;
|
|
268
266
|
let lastStatusUpdate = Date.now();
|
|
269
267
|
|
|
@@ -274,7 +272,7 @@ export async function waitForManualAuth(page, timeoutMs = DEFAULT_MANUAL_AUTH_TI
|
|
|
274
272
|
// Auth complete when we leave the auth page
|
|
275
273
|
if (!isLikelyAuthUrl(checkUrl)) {
|
|
276
274
|
const checkHostname = new URL(checkUrl).hostname;
|
|
277
|
-
|
|
275
|
+
logger.info(`Manual authentication successful: ${checkUrl}`);
|
|
278
276
|
|
|
279
277
|
if (onStatusChange) {
|
|
280
278
|
onStatusChange({
|
package/src/core/browser.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { ChromeBrowser } from '../browsers/chrome.js';
|
|
7
7
|
import { EdgeBrowser } from '../browsers/edge.js';
|
|
8
8
|
import os from 'os';
|
|
9
|
+
import logger from './logger.js';
|
|
9
10
|
|
|
10
11
|
// Browser state
|
|
11
12
|
export let cachedBrowser = null;
|
|
@@ -29,13 +30,13 @@ async function detectDefaultBrowser() {
|
|
|
29
30
|
|
|
30
31
|
for (const browser of browsers) {
|
|
31
32
|
if (await browser.isAvailable()) {
|
|
32
|
-
|
|
33
|
+
logger.info(`Auto-detected ${browser.getType()} as default browser`);
|
|
33
34
|
return browser.getType();
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
// Fallback to Chrome
|
|
38
|
-
|
|
39
|
+
logger.info('No browser detected, defaulting to Chrome');
|
|
39
40
|
return 'chrome';
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -88,7 +89,7 @@ export async function GetBrowser(type = '') {
|
|
|
88
89
|
export async function rebuildDomainPagesMap(browser) {
|
|
89
90
|
try {
|
|
90
91
|
const pages = await browser.pages();
|
|
91
|
-
|
|
92
|
+
logger.info(`Reconnected to browser with ${pages.length} existing tabs`);
|
|
92
93
|
|
|
93
94
|
for (const page of pages) {
|
|
94
95
|
try {
|
|
@@ -105,7 +106,7 @@ export async function rebuildDomainPagesMap(browser) {
|
|
|
105
106
|
const hostname = new URL(pageUrl).hostname;
|
|
106
107
|
if (hostname && !domainPages.has(hostname)) {
|
|
107
108
|
domainPages.set(hostname, page);
|
|
108
|
-
|
|
109
|
+
logger.info(`Tab mapped: ${hostname}`);
|
|
109
110
|
}
|
|
110
111
|
} catch (err) {
|
|
111
112
|
// Skip pages that are inaccessible or have invalid URLs
|
|
@@ -114,10 +115,10 @@ export async function rebuildDomainPagesMap(browser) {
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
if (domainPages.size > 0) {
|
|
117
|
-
|
|
118
|
+
logger.info(`Restored ${domainPages.size} domain-to-tab mappings`);
|
|
118
119
|
}
|
|
119
120
|
} catch (err) {
|
|
120
|
-
|
|
121
|
+
logger.info(`Could not rebuild domain pages map: ${err.message}`);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger - Simple logging for MCPBrowser
|
|
3
|
+
*
|
|
4
|
+
* All output goes to stderr so it doesn't interfere with MCP protocol on stdout.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const PREFIX = '[MCPBrowser]';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Log an info message
|
|
11
|
+
* @param {string} message - The message to log
|
|
12
|
+
*/
|
|
13
|
+
function info(message) {
|
|
14
|
+
console.error(`${PREFIX} ${message}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Log an error message
|
|
19
|
+
* @param {string} message - The message to log
|
|
20
|
+
*/
|
|
21
|
+
function error(message) {
|
|
22
|
+
console.error(`${PREFIX} ❌ ${message}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const logger = { info, error };
|
|
26
|
+
export default logger;
|
package/src/core/page.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { domainPages } from './browser.js';
|
|
9
9
|
import { cleanHtml, enrichHtml } from './html.js';
|
|
10
|
+
import logger from './logger.js';
|
|
10
11
|
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// SIMPLE REQUEST QUEUE (No Locks)
|
|
@@ -42,7 +43,7 @@ async function processQueue() {
|
|
|
42
43
|
const queueLength = requestQueue.length;
|
|
43
44
|
|
|
44
45
|
if (queueLength > 0) {
|
|
45
|
-
|
|
46
|
+
logger.info(`Queue: ${queueLength} requests waiting`);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
try {
|
|
@@ -78,7 +79,7 @@ export async function getOrCreatePage(browser, hostname, reuseLastKeptPage = tru
|
|
|
78
79
|
if (!existingPage.isClosed()) {
|
|
79
80
|
page = existingPage;
|
|
80
81
|
await page.bringToFront().catch(() => {});
|
|
81
|
-
|
|
82
|
+
logger.info(`Tab reused: ${hostname}`);
|
|
82
83
|
} else {
|
|
83
84
|
// Page was closed externally, remove from map
|
|
84
85
|
domainPages.delete(hostname);
|
|
@@ -110,7 +111,7 @@ export async function getOrCreatePage(browser, hostname, reuseLastKeptPage = tru
|
|
|
110
111
|
}
|
|
111
112
|
// Add new page to domain map
|
|
112
113
|
domainPages.set(hostname, page);
|
|
113
|
-
|
|
114
|
+
logger.info(`Tab created: ${hostname}`);
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
return page;
|
|
@@ -196,7 +197,7 @@ export async function isItSPA(page) {
|
|
|
196
197
|
* @returns {Promise<void>}
|
|
197
198
|
*/
|
|
198
199
|
export async function navigateToUrl(page, url, waitUntil, timeout) {
|
|
199
|
-
|
|
200
|
+
logger.info(`Navigating to: ${url}`);
|
|
200
201
|
|
|
201
202
|
const startTime = Date.now();
|
|
202
203
|
|
|
@@ -204,10 +205,10 @@ export async function navigateToUrl(page, url, waitUntil, timeout) {
|
|
|
204
205
|
await page.goto(url, { waitUntil, timeout });
|
|
205
206
|
|
|
206
207
|
const loadTime = Date.now() - startTime;
|
|
207
|
-
|
|
208
|
+
logger.info(`Navigation complete: ${page.url()} (${loadTime}ms)`);
|
|
208
209
|
} catch (error) {
|
|
209
210
|
const elapsed = Date.now() - startTime;
|
|
210
|
-
|
|
211
|
+
logger.error(`Navigation failed: ${error.message} after ${elapsed}ms`);
|
|
211
212
|
throw error;
|
|
212
213
|
}
|
|
213
214
|
}
|
|
@@ -222,8 +223,7 @@ export async function waitForPageReady(page) {
|
|
|
222
223
|
const spaCheck = await isItSPA(page);
|
|
223
224
|
|
|
224
225
|
if (spaCheck.isSPA) {
|
|
225
|
-
|
|
226
|
-
console.error(`[MCPBrowser] Waiting for JavaScript to render content...`);
|
|
226
|
+
logger.info(`SPA detected: ${spaCheck.indicators.join(', ')}`);
|
|
227
227
|
|
|
228
228
|
// Wait for SPA to render
|
|
229
229
|
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
@@ -234,7 +234,7 @@ export async function waitForPageReady(page) {
|
|
|
234
234
|
} catch {
|
|
235
235
|
// OK if timeout - SPA might have websockets or long-polling
|
|
236
236
|
}
|
|
237
|
-
|
|
237
|
+
logger.info('SPA content ready');
|
|
238
238
|
} else {
|
|
239
239
|
// For non-SPAs, just wait briefly for any pending network requests
|
|
240
240
|
try {
|
|
@@ -252,17 +252,17 @@ export async function waitForPageReady(page) {
|
|
|
252
252
|
* @returns {Promise<void>}
|
|
253
253
|
*/
|
|
254
254
|
export async function waitForPageStability(page) {
|
|
255
|
-
|
|
255
|
+
logger.info('Waiting for page stability (network idle)...');
|
|
256
256
|
|
|
257
257
|
// Give time for any triggered actions to complete
|
|
258
258
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
259
259
|
|
|
260
260
|
try {
|
|
261
261
|
await page.waitForNetworkIdle({ timeout: 5000 });
|
|
262
|
-
|
|
262
|
+
logger.info('Page stabilized');
|
|
263
263
|
} catch {
|
|
264
264
|
// Ignore timeout - page may have long-polling or websockets
|
|
265
|
-
|
|
265
|
+
logger.info('Network still active, continuing anyway');
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
package/src/mcp-browser.js
CHANGED
|
@@ -14,6 +14,7 @@ import { dirname, join } from 'path';
|
|
|
14
14
|
|
|
15
15
|
// Import response classes
|
|
16
16
|
import { ErrorResponse } from './core/responses.js';
|
|
17
|
+
import logger from './core/logger.js';
|
|
17
18
|
|
|
18
19
|
// Import core functionality
|
|
19
20
|
import { fetchPage, FETCH_WEBPAGE_TOOL } from './actions/fetch-page.js';
|
|
@@ -91,6 +92,7 @@ async function main() {
|
|
|
91
92
|
|
|
92
93
|
const transport = new StdioServerTransport();
|
|
93
94
|
await server.connect(transport);
|
|
95
|
+
logger.info(`MCPBrowser server v${packageJson.version} started`);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
// Export for testing
|
|
@@ -123,7 +125,7 @@ export {
|
|
|
123
125
|
if (import.meta.url === new URL(process.argv[1], 'file://').href ||
|
|
124
126
|
fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
125
127
|
main().catch((err) => {
|
|
126
|
-
|
|
128
|
+
logger.error(`Server failed: ${err.message}`);
|
|
127
129
|
process.exit(1);
|
|
128
130
|
});
|
|
129
131
|
}
|