mcpbrowser 0.3.19 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpbrowser",
3
- "version": "0.3.19",
3
+ "version": "0.3.20",
4
4
  "mcpName": "io.github.cherchyk/mcpbrowser",
5
5
  "type": "module",
6
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.",
@@ -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
+ }
@@ -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.info(`click_element: ${pageError || 'No page found for ' + hostname}`);
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.info(`Clicking: ${selector || `text="${text}"`}`);
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.info('Waiting for page stability...');
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.info('Waiting for page stability (fast mode)...');
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
@@ -190,7 +190,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
190
190
  const redirectInfo = detectRedirectType(url, hostname, currentUrl, currentHostname);
191
191
 
192
192
  if (redirectInfo.type === 'requested_auth') {
193
- logger.info('User requested auth page directly, returning content');
193
+ logger.debug('User requested auth page directly, returning content');
194
194
  // Update domain mapping if needed
195
195
  if (redirectInfo.currentHostname !== hostname) {
196
196
  domainPages.delete(hostname);
@@ -198,13 +198,13 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
198
198
  hostname = redirectInfo.currentHostname;
199
199
  }
200
200
  } else if (redirectInfo.type === 'permanent') {
201
- logger.info(`Redirect: ${hostname} → ${redirectInfo.currentHostname}`);
201
+ logger.debug(`Redirect: ${hostname} → ${redirectInfo.currentHostname}`);
202
202
 
203
203
  // Check if we already have a tab for the redirected hostname
204
204
  // (can happen after reconnect - we mapped mail.google.com but not gmail.com)
205
205
  const existingPage = domainPages.get(redirectInfo.currentHostname);
206
206
  if (existingPage && existingPage !== page && !existingPage.isClosed()) {
207
- logger.info(`Found existing tab for ${redirectInfo.currentHostname}, reusing it`);
207
+ logger.debug(`Found existing tab for ${redirectInfo.currentHostname}, reusing it`);
208
208
  // Close the new tab we just opened, use the existing one
209
209
  await page.close().catch(() => {});
210
210
  domainPages.delete(hostname);
@@ -263,7 +263,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
263
263
 
264
264
  // Additional wait if requested (for pages that need extra time)
265
265
  if (postLoadWait > 0) {
266
- logger.info(`Waiting ${postLoadWait}ms (postLoadWait)...`);
266
+ logger.debug(`Waiting ${postLoadWait}ms (postLoadWait)...`);
267
267
  await new Promise(resolve => setTimeout(resolve, postLoadWait));
268
268
  }
269
269
 
@@ -274,7 +274,7 @@ async function doFetchPage({ url, hostname, browser, removeUnnecessaryHTML, post
274
274
 
275
275
  // Check for non-2xx HTTP status codes - return informational response (not red error)
276
276
  if (statusCode && (statusCode >= 400 && statusCode < 600)) {
277
- logger.info(`HTTP ${statusCode} ${statusText} - returning as informational response`);
277
+ logger.debug(`HTTP ${statusCode} ${statusText} - returning as informational response`);
278
278
  return new HttpStatusResponse(
279
279
  page.url(),
280
280
  statusCode,
@@ -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.info(`get_current_html: ${pageError || 'No page found for ' + hostname}`);
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
@@ -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.info(`scroll_page: ${pageError || 'No page found for ' + hostname}`);
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.info(`scroll_page: Scrolling to element: ${selector}`);
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.info(`scroll_page: Scrolling to absolute position: (${x}, ${y})`);
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.info(`scroll_page: Scrolling ${direction} by ${amount}px`);
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.info(`scroll_page: No scroll action specified, returning current position`);
274
+ logger.debug(`scroll_page: No scroll action specified, returning current position`);
275
275
  }
276
276
 
277
277
  // Small delay to let scroll complete
@@ -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.info(`take_screenshot: ${pageError || 'No page found for ' + hostname}`);
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
@@ -186,7 +186,7 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
186
186
 
187
187
  if (!page) {
188
188
  const isConnectionLost = pageError && pageError.includes('connection');
189
- logger.info(`type_text: ${pageError || 'No page found for ' + hostname}`);
189
+ logger.debug(`type_text: ${pageError || 'No page found for ' + hostname}`);
190
190
  return new InformationalResponse(
191
191
  isConnectionLost ? `Page connection lost for ${hostname}` : `No open page found for ${hostname}`,
192
192
  isConnectionLost
@@ -216,7 +216,7 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
216
216
  await page.keyboard.press('Backspace');
217
217
  }
218
218
 
219
- logger.info(`Typing into: ${selector}`);
219
+ logger.debug(`Typing into: ${selector}`);
220
220
  await page.type(selector, String(text), { delay: TYPE_DELAY_MS });
221
221
  filledSelectors.push(selector);
222
222
  currentFieldIndex++;
@@ -228,7 +228,7 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
228
228
 
229
229
  if (returnHtml) {
230
230
  // Wait for page to stabilize (handles form validation, autocomplete, etc.)
231
- logger.info('Waiting for page stability after typing...');
231
+ logger.debug('Waiting for page stability after typing...');
232
232
  await waitForPageStability(page);
233
233
 
234
234
  // Wait for SPAs to render dynamic content after typing
@@ -255,7 +255,7 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
255
255
  );
256
256
  } else {
257
257
  // Wait for page to stabilize even without returning HTML
258
- logger.info('Waiting for page stability after typing (fast mode)...');
258
+ logger.debug('Waiting for page stability after typing (fast mode)...');
259
259
  await waitForPageStability(page);
260
260
 
261
261
  // Wait for SPAs to render dynamic content after typing
@@ -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
+ }
@@ -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
- export const logger = { info, error };
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.info(`Queue: ${queueLength} requests waiting`);
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.info(`SPA detected: ${spaCheck.indicators.join(', ')}`);
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.info('SPA content ready');
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.info('Waiting for page stability (network idle)...');
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.info('Page stabilized');
324
+ logger.debug('Page stabilized');
325
325
  } catch {
326
326
  // Ignore timeout - page may have long-polling or websockets
327
- logger.info('Network still active, continuing anyway');
327
+ logger.debug('Network still active, continuing anyway');
328
328
  }
329
329
  }
330
330
 
@@ -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;
@@ -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)