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 CHANGED
@@ -1,4 +1,4 @@
1
- # MCPBrowser (MCP Browser)
1
+ # MCPBrowser (MCP Browser)
2
2
 
3
3
  [![VS Code Marketplace](https://img.shields.io/visual-studio-marketplace/v/cherchyk.mcpbrowser.svg)](https://marketplace.visualstudio.com/items?itemName=cherchyk.mcpbrowser)
4
4
  [![npm version](https://img.shields.io/npm/v/mcpbrowser.svg)](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.8",
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
  [
@@ -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
- console.error(`[MCPBrowser] Closed tab for hostname: ${hostname}`);
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
- console.error(`[MCPBrowser] Error closing tab:`, error);
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
- console.error(`[MCPBrowser] User requested auth page directly, returning content`);
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
- console.error(`[MCPBrowser] Permanent redirect detected: ${hostname} → ${redirectInfo.currentHostname}`);
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
- console.error(`[MCPBrowser] Found existing tab for ${redirectInfo.currentHostname}, reusing it`);
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
- console.error(`[MCPBrowser] Authentication flow detected (${redirectInfo.flowType})`);
203
- console.error(`[MCPBrowser] Current location: ${redirectInfo.currentUrl}`);
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
- console.error(`[MCPBrowser] Waiting ${postLoadWait}ms (postLoadWait)...`);
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
  [
@@ -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
- console.error(`[MCPBrowser] Launching ${this.config.name} with remote debugging on port ${this.config.port}...`);
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
- console.error(`[MCPBrowser] ${this.config.name} is ready on port ${this.config.port}`);
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
- console.error(`[MCPBrowser] Checking for auto-authentication (${timeoutMs / 1000} sec)...`);
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
- console.error(`[MCPBrowser] Auto-authentication successful! Now at: ${checkUrl}`);
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
- console.error(`[MCPBrowser] 🔐 LOGIN PAGE DETECTED!`);
245
- console.error(`[MCPBrowser] Indicators found: ${loginDetection.indicators.join(', ')}`);
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
- console.error(`[MCPBrowser] Auto-authentication did not complete. Waiting for user...`);
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
- console.error(`[MCPBrowser] Authentication completed! Now at: ${checkUrl}`);
275
+ logger.info(`Manual authentication successful: ${checkUrl}`);
278
276
 
279
277
  if (onStatusChange) {
280
278
  onStatusChange({
@@ -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
- console.error(`[MCPBrowser] Auto-detected ${browser.getType()} as default browser`);
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
- console.error('[MCPBrowser] No browser detected, defaulting to Chrome');
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
- console.error(`[MCPBrowser] Reconnected to browser with ${pages.length} existing tabs`);
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
- console.error(`[MCPBrowser] Mapped existing tab for domain: ${hostname} (${pageUrl})`);
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
- console.error(`[MCPBrowser] Restored ${domainPages.size} domain-to-tab mappings`);
118
+ logger.info(`Restored ${domainPages.size} domain-to-tab mappings`);
118
119
  }
119
120
  } catch (err) {
120
- console.error(`[MCPBrowser] Warning: Could not rebuild domain pages map: ${err.message}`);
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
- console.error(`[MCPBrowser] Processing request (${queueLength} more in queue)`);
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
- console.error(`[MCPBrowser] Reusing existing tab for domain: ${hostname}`);
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
- console.error(`[MCPBrowser] Created new tab for domain: ${hostname}`);
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
- console.error(`[MCPBrowser] Navigating to: ${url}`);
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
- console.error(`[MCPBrowser] Navigation completed in ${loadTime}ms: ${page.url()}`);
208
+ logger.info(`Navigation complete: ${page.url()} (${loadTime}ms)`);
208
209
  } catch (error) {
209
210
  const elapsed = Date.now() - startTime;
210
- console.error(`[MCPBrowser] Navigation error after ${elapsed}ms: ${error.message}`);
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
- console.error(`[MCPBrowser] 🔄 SPA detected: ${spaCheck.indicators.join(', ')}`);
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
- console.error(`[MCPBrowser] SPA content ready`);
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
- console.error(`[MCPBrowser] Waiting for page to stabilize...`);
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
- console.error(`[MCPBrowser] Page stabilized`);
262
+ logger.info('Page stabilized');
263
263
  } catch {
264
264
  // Ignore timeout - page may have long-polling or websockets
265
- console.error(`[MCPBrowser] Network still active, continuing anyway`);
265
+ logger.info('Network still active, continuing anyway');
266
266
  }
267
267
  }
268
268
 
@@ -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
- console.error(err);
128
+ logger.error(`Server failed: ${err.message}`);
127
129
  process.exit(1);
128
130
  });
129
131
  }