mcpbrowser 0.3.44 → 0.3.46

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.
Files changed (43) hide show
  1. package/README.md +34 -34
  2. package/package.json +1 -1
  3. package/src/actions/accept-eula.js +5 -5
  4. package/src/actions/click-element.js +25 -25
  5. package/src/actions/close-tab.js +8 -8
  6. package/src/actions/execute-javascript.js +27 -9
  7. package/src/actions/fetch-page.js +20 -19
  8. package/src/actions/get-current-html.js +35 -19
  9. package/src/actions/navigate-history.js +21 -21
  10. package/src/actions/plugin-action.js +15 -15
  11. package/src/actions/plugin-info.js +10 -10
  12. package/src/actions/scroll-page.js +21 -21
  13. package/src/actions/take-screenshot.js +16 -16
  14. package/src/actions/type-text.js +28 -28
  15. package/src/cli/help.js +1 -1
  16. package/src/core/html.js +3 -2
  17. package/src/core/logger.js +51 -2
  18. package/src/core/page.js +39 -3
  19. package/src/core/plugin-loader.js +2 -2
  20. package/src/core/responses.js +4 -4
  21. package/src/mcp-browser.js +20 -12
  22. package/src/plugins/_example/index.js +2 -2
  23. package/src/plugins/gcal/actions/check-availability.js +1 -1
  24. package/src/plugins/gcal/actions/create-event.js +1 -1
  25. package/src/plugins/gcal/actions/delete-event.js +1 -1
  26. package/src/plugins/gcal/actions/edit-event.js +1 -1
  27. package/src/plugins/gcal/actions/list-events.js +1 -1
  28. package/src/plugins/gcal/actions/read-event.js +1 -1
  29. package/src/plugins/gcal/actions/rsvp-event.js +1 -1
  30. package/src/plugins/gcal/actions/search-events.js +1 -1
  31. package/src/plugins/gcal/helpers.js +2 -2
  32. package/src/plugins/gmail/actions/archive-email.js +1 -1
  33. package/src/plugins/gmail/actions/compose-email.js +1 -1
  34. package/src/plugins/gmail/actions/delete-email.js +1 -1
  35. package/src/plugins/gmail/actions/forward-email.js +1 -1
  36. package/src/plugins/gmail/actions/label-email.js +1 -1
  37. package/src/plugins/gmail/actions/list-emails.js +1 -1
  38. package/src/plugins/gmail/actions/mark-read.js +1 -1
  39. package/src/plugins/gmail/actions/mark-unread.js +1 -1
  40. package/src/plugins/gmail/actions/read-email.js +1 -1
  41. package/src/plugins/gmail/actions/reply-email.js +1 -1
  42. package/src/plugins/gmail/actions/search-emails.js +1 -1
  43. package/src/plugins/gmail/helpers.js +3 -3
@@ -16,7 +16,7 @@ import logger from '../core/logger.js';
16
16
  // ============================================================================
17
17
 
18
18
  /**
19
- * Response for successful take_screenshot operations
19
+ * Response for successful browser_take_screenshot operations
20
20
  * Returns screenshot as base64-encoded image
21
21
  */
22
22
  export class TakeScreenshotSuccessResponse extends MCPResponse {
@@ -87,9 +87,9 @@ export class TakeScreenshotSuccessResponse extends MCPResponse {
87
87
  * @type {Tool}
88
88
  */
89
89
  export const TAKE_SCREENSHOT_TOOL = {
90
- name: "take_screenshot",
90
+ name: "browser_take_screenshot",
91
91
  title: "Take Screenshot",
92
- description: "**VISUAL CAPTURE** - Takes a screenshot of an already-loaded page for visual analysis. Useful when HTML parsing is insufficient or you need to see visual layout, images, charts, or rendered content. Returns a PNG image.\n\n**PREREQUISITE**: Page MUST be loaded with fetch_webpage first. This tool captures the current visual state of the page.",
92
+ description: "**VISUAL CAPTURE** - Takes a screenshot of an already-loaded page for visual analysis. Useful when HTML parsing is insufficient or you need to see visual layout, images, charts, or rendered content. Returns a PNG image.\n\n**PREREQUISITE**: Page MUST be loaded with browser_fetch_webpage first. This tool captures the current visual state of the page.",
93
93
  inputSchema: {
94
94
  type: "object",
95
95
  properties: {
@@ -129,7 +129,7 @@ export const TAKE_SCREENSHOT_TOOL = {
129
129
  * @returns {Promise<Object>} Result object with screenshot data
130
130
  */
131
131
  export async function takeScreenshot({ url, fullPage = false }) {
132
- logger.info(`take_screenshot called: url=${url}, fullPage=${fullPage}`);
132
+ logger.info(`browser_take_screenshot called: url=${url}, fullPage=${fullPage}`);
133
133
 
134
134
  if (!url) {
135
135
  throw new Error("url parameter is required");
@@ -146,7 +146,7 @@ export async function takeScreenshot({ url, fullPage = false }) {
146
146
  try {
147
147
  await getBrowser();
148
148
  } catch (err) {
149
- logger.error(`take_screenshot: Failed to connect to browser: ${err.message}`);
149
+ logger.error(`browser_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.',
@@ -163,15 +163,15 @@ export async function takeScreenshot({ url, fullPage = false }) {
163
163
 
164
164
  if (!page) {
165
165
  const isConnectionLost = pageError && pageError.includes('connection');
166
- logger.debug(`take_screenshot: ${pageError || 'No page found for ' + hostname}`);
166
+ logger.debug(`browser_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
170
170
  ? 'The browser tab was closed or the connection was lost. The page needs to be reloaded.'
171
171
  : 'The page must be loaded before you can take a screenshot',
172
172
  [
173
- "Use MCPBrowser's fetch_webpage tool to load the page first",
174
- "Then retry MCPBrowser's take_screenshot with the same URL"
173
+ "Use MCPBrowser's browser_fetch_webpage tool to load the page first",
174
+ "Then retry MCPBrowser's browser_take_screenshot with the same URL"
175
175
  ]
176
176
  );
177
177
  }
@@ -186,27 +186,27 @@ export async function takeScreenshot({ url, fullPage = false }) {
186
186
  fullPage: fullPage
187
187
  });
188
188
 
189
- logger.info(`take_screenshot completed: captured from ${currentUrl} (fullPage=${fullPage})`);
189
+ logger.info(`browser_take_screenshot completed: captured from ${currentUrl} (fullPage=${fullPage})`);
190
190
 
191
191
  return new TakeScreenshotSuccessResponse(
192
192
  currentUrl,
193
193
  screenshotBuffer,
194
194
  'image/png',
195
195
  [
196
- "Use MCPBrowser's get_current_html if you need the HTML instead",
197
- "Use MCPBrowser's click_element to interact with elements",
198
- "Use MCPBrowser's type_text to fill forms",
199
- "Use MCPBrowser's close_tab to free resources when done"
196
+ "Use MCPBrowser's browser_get_current_html if you need the HTML instead",
197
+ "Use MCPBrowser's browser_click_element to interact with elements",
198
+ "Use MCPBrowser's browser_type_text to fill forms",
199
+ "Use MCPBrowser's browser_close_tab to free resources when done"
200
200
  ]
201
201
  );
202
202
  } catch (err) {
203
- logger.error(`take_screenshot failed: ${err.message}`);
203
+ logger.error(`browser_take_screenshot failed: ${err.message}`);
204
204
  return new InformationalResponse(
205
205
  `Failed to take screenshot: ${err.message}`,
206
206
  'Could not capture screenshot from the page. The page may have navigated away or the connection was lost.',
207
207
  [
208
- "Try MCPBrowser's fetch_webpage to reload the page",
209
- "Use MCPBrowser's close_tab and start fresh if needed"
208
+ "Try MCPBrowser's browser_fetch_webpage to reload the page",
209
+ "Use MCPBrowser's browser_close_tab and start fresh if needed"
210
210
  ]
211
211
  );
212
212
  }
@@ -16,7 +16,7 @@ import logger from '../core/logger.js';
16
16
  // ============================================================================
17
17
 
18
18
  /**
19
- * Response for successful type_text operations
19
+ * Response for successful browser_type_text operations
20
20
  */
21
21
  export class TypeTextSuccessResponse extends MCPResponse {
22
22
  /**
@@ -64,9 +64,9 @@ export class TypeTextSuccessResponse extends MCPResponse {
64
64
  * @type {Tool}
65
65
  */
66
66
  export const TYPE_TEXT_TOOL = {
67
- name: "type_text",
67
+ name: "browser_type_text",
68
68
  title: "Type Text",
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.",
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 browser_fetch_webpage first. This tool operates on an already-loaded page in the browser.",
70
70
  inputSchema: {
71
71
  type: "object",
72
72
  properties: {
@@ -137,7 +137,7 @@ const TYPE_DELAY_MS = 10;
137
137
  */
138
138
  export async function typeText({ url, fields, returnHtml = true, removeUnnecessaryHTML = true, postTypeWait = 1000 }) {
139
139
  const startTime = Date.now();
140
- logger.info(`type_text called: ${fields?.length || 0} fields, url=${url}`);
140
+ logger.info(`browser_type_text called: ${fields?.length || 0} fields, url=${url}`);
141
141
 
142
142
  if (!url) {
143
143
  throw new Error("url parameter is required");
@@ -169,7 +169,7 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
169
169
  try {
170
170
  await getBrowser();
171
171
  } catch (err) {
172
- logger.error(`type_text: Failed to connect to browser: ${err.message}`);
172
+ logger.error(`browser_type_text: Failed to connect to browser: ${err.message}`);
173
173
  return new InformationalResponse(
174
174
  `Browser connection failed: ${err.message}`,
175
175
  'The browser must be running with remote debugging enabled.',
@@ -186,15 +186,15 @@ 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.debug(`type_text: ${pageError || 'No page found for ' + hostname}`);
189
+ logger.debug(`browser_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
193
193
  ? 'The browser tab was closed or the connection was lost. The page needs to be reloaded.'
194
194
  : 'The page must be loaded before you can type text into elements',
195
195
  [
196
- "Use MCPBrowser's fetch_webpage tool to load the page first",
197
- "Then retry MCPBrowser's type_text with the same URL"
196
+ "Use MCPBrowser's browser_fetch_webpage tool to load the page first",
197
+ "Then retry MCPBrowser's browser_type_text with the same URL"
198
198
  ]
199
199
  );
200
200
  }
@@ -239,20 +239,20 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
239
239
  const html = returnHtml ? await extractAndProcessHtml(page, removeUnnecessaryHTML) : null;
240
240
  const nextSteps = returnHtml
241
241
  ? [
242
- "Use MCPBrowser's type_text to fill additional fields",
243
- "Use MCPBrowser's click_element to submit the form or navigate",
244
- "Use MCPBrowser's get_current_html to check for validation messages",
245
- "Use MCPBrowser's take_screenshot if form has visual feedback or validation that's hard to parse from HTML",
246
- "Use MCPBrowser's close_tab when finished"
242
+ "Use MCPBrowser's browser_type_text to fill additional fields",
243
+ "Use MCPBrowser's browser_click_element to submit the form or navigate",
244
+ "Use MCPBrowser's browser_get_current_html to check for validation messages",
245
+ "Use MCPBrowser's browser_take_screenshot if form has visual feedback or validation that's hard to parse from HTML",
246
+ "Use MCPBrowser's browser_close_tab when finished"
247
247
  ]
248
248
  : [
249
- "Use MCPBrowser's get_current_html to see updated page state",
250
- "Use MCPBrowser's take_screenshot if the page has visual feedback that's hard to parse",
251
- "Use MCPBrowser's type_text for additional fields or MCPBrowser's click_element to submit",
252
- "Use MCPBrowser's close_tab when finished"
249
+ "Use MCPBrowser's browser_get_current_html to see updated page state",
250
+ "Use MCPBrowser's browser_take_screenshot if the page has visual feedback that's hard to parse",
251
+ "Use MCPBrowser's browser_type_text for additional fields or MCPBrowser's browser_click_element to submit",
252
+ "Use MCPBrowser's browser_close_tab when finished"
253
253
  ];
254
254
 
255
- logger.info(`type_text completed: typed into ${fieldsSummary}${returnHtml ? '' : ' (no HTML)'}`);
255
+ logger.info(`browser_type_text completed: typed into ${fieldsSummary}${returnHtml ? '' : ' (no HTML)'}`);
256
256
 
257
257
  return new TypeTextSuccessResponse(currentUrl, `Typed text into: ${fieldsSummary}`, html, nextSteps);
258
258
  } catch (err) {
@@ -272,29 +272,29 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
272
272
  if (isNotFound) {
273
273
  reason = `Selector not found: "${currentSelector}". The element may not exist on the page or have a different selector.`;
274
274
  nextSteps = [
275
- "Use MCPBrowser's get_current_html to find the correct selector",
276
- "Use MCPBrowser's take_screenshot to visually inspect the form",
275
+ "Use MCPBrowser's browser_get_current_html to find the correct selector",
276
+ "Use MCPBrowser's browser_take_screenshot to visually inspect the form",
277
277
  "Check for typos in the selector or try a simpler selector (e.g., 'input[type=\"text\"]')",
278
278
  "The element may load dynamically - try increasing waitForElementTimeout"
279
279
  ];
280
280
  } else if (isNotVisible) {
281
281
  reason = `Element "${currentSelector}" exists but is not visible. It may be hidden, collapsed, or off-screen.`;
282
282
  nextSteps = [
283
- "Use MCPBrowser's take_screenshot to see the page state",
284
- "Use MCPBrowser's click_element to expand/show the form section first",
285
- "Use MCPBrowser's scroll_page to bring the element into view"
283
+ "Use MCPBrowser's browser_take_screenshot to see the page state",
284
+ "Use MCPBrowser's browser_click_element to expand/show the form section first",
285
+ "Use MCPBrowser's browser_scroll_page to bring the element into view"
286
286
  ];
287
287
  } else if (isDetached) {
288
288
  reason = `Element "${currentSelector}" was removed from the page during interaction. The page may have reloaded or updated.`;
289
289
  nextSteps = [
290
- "Use MCPBrowser's get_current_html to check current page state",
291
- "Retry the type_text call - the page may have stabilized"
290
+ "Use MCPBrowser's browser_get_current_html to check current page state",
291
+ "Retry the browser_type_text call - the page may have stabilized"
292
292
  ];
293
293
  } else {
294
294
  reason = `Failed to interact with "${currentSelector}": ${errorMsg}`;
295
295
  nextSteps = [
296
- "Use MCPBrowser's get_current_html to verify page state",
297
- "Use MCPBrowser's take_screenshot to see what's on the page visually",
296
+ "Use MCPBrowser's browser_get_current_html to verify page state",
297
+ "Use MCPBrowser's browser_take_screenshot to see what's on the page visually",
298
298
  "The element may be disabled or read-only"
299
299
  ];
300
300
  }
@@ -307,7 +307,7 @@ export async function typeText({ url, fields, returnHtml = true, removeUnnecessa
307
307
  progressInfo = `Failed on field ${failedFieldNum} of ${totalFields}. Successfully filled ${filledSelectors.length} field(s): ${filledSelectors.join(', ')}. Do NOT re-type these fields.`;
308
308
  }
309
309
 
310
- logger.error(`type_text failed on field ${failedFieldNum}/${totalFields} (${currentSelector}): ${errorMsg}`);
310
+ logger.error(`browser_type_text failed on field ${failedFieldNum}/${totalFields} (${currentSelector}): ${errorMsg}`);
311
311
 
312
312
  return new InformationalResponse(
313
313
  `${progressInfo}`,
package/src/cli/help.js CHANGED
@@ -162,7 +162,7 @@ export function printHelp() {
162
162
  o('━'.repeat(70));
163
163
  o('');
164
164
  o(' No arguments → starts MCP server (stdin/stdout JSON-RPC).');
165
- o(' CLI commands map 1:1 to MCP tools (fetch→fetch_webpage, etc.).');
165
+ o(' CLI commands map 1:1 to MCP tools (fetch→browser_fetch_webpage, etc.).');
166
166
  o('');
167
167
  o(' { "mcpServers": { "mcpbrowser": { "command": "npx", "args": ["-y", "mcpbrowser@latest"] } } }');
168
168
  o('');
package/src/core/html.js CHANGED
@@ -70,8 +70,9 @@ export function cleanHtml(html) {
70
70
  // Remove event handler attributes (onclick, onload, etc.)
71
71
  cleaned = cleaned.replace(/\s+on[a-z]+\s*=\s*["'][^"']*["']/gi, '');
72
72
 
73
- // Remove role attributes
74
- cleaned = cleaned.replace(/\s+role=["'][^"']*["']/gi, '');
73
+ // Keep role attributes — they're semantically valuable for LLM understanding
74
+ // and enable stable selectors like [role="main"], [role="navigation"]
75
+ // cleaned = cleaned.replace(/\s+role=["'][^"']*["']/gi, '');
75
76
 
76
77
  // Remove aria-* attributes
77
78
  cleaned = cleaned.replace(/\s+aria-[a-z0-9-]+=["'][^"']*["']/gi, '');
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Logger - Emits to stderr and, when available, via MCP logging notifications.
3
+ * Also sends MCP progress notifications (notifications/progress) when a
4
+ * progressToken is active, so agents see real-time status during tool execution.
3
5
  * Stderr stays the primary sink to avoid interfering with MCP stdout traffic.
4
6
  */
5
7
 
@@ -8,6 +10,10 @@ const PREFIX = '[MCPBrowser]';
8
10
  // Optional MCP server reference for notifications/message logs.
9
11
  let mcpServer = null;
10
12
 
13
+ // MCP progress tracking — set per-request by the request handler.
14
+ let _progressToken = null;
15
+ let _progressStep = 0;
16
+
11
17
  // Optional stdout mirroring (off by default to avoid corrupting MCP stdout).
12
18
  // Auto-enable during tests so test runners capture output.
13
19
  let consoleOutputEnabled = process.env.NODE_ENV === 'test';
@@ -32,6 +38,25 @@ function setConsoleOutput(enabled = true) {
32
38
  consoleOutputEnabled = !!enabled;
33
39
  }
34
40
 
41
+ /**
42
+ * Set the MCP progress token for the current request.
43
+ * While set, every logger.info() call also sends a notifications/progress
44
+ * message so the agent sees real-time status during tool execution.
45
+ * @param {string|number|null|undefined} token - progressToken from request._meta
46
+ */
47
+ function setProgressToken(token) {
48
+ _progressToken = token ?? null;
49
+ _progressStep = 0;
50
+ }
51
+
52
+ /**
53
+ * Clear the progress token after the request completes.
54
+ */
55
+ function clearProgressToken() {
56
+ _progressToken = null;
57
+ _progressStep = 0;
58
+ }
59
+
35
60
  async function notifyAgent(level, data) {
36
61
  if (!mcpServer?.sendLoggingMessage) return;
37
62
  try {
@@ -44,6 +69,28 @@ async function notifyAgent(level, data) {
44
69
  }
45
70
  }
46
71
 
72
+ /**
73
+ * Send an MCP progress notification if a progressToken is active.
74
+ * Called automatically from info-level log messages.
75
+ * @param {string} message - Human-readable progress message
76
+ */
77
+ async function sendProgress(message) {
78
+ if (!_progressToken || !mcpServer) return;
79
+ _progressStep++;
80
+ try {
81
+ await mcpServer.notification({
82
+ method: 'notifications/progress',
83
+ params: {
84
+ progressToken: _progressToken,
85
+ progress: _progressStep,
86
+ message
87
+ }
88
+ });
89
+ } catch {
90
+ // Fire and forget — don't break the action if progress fails
91
+ }
92
+ }
93
+
47
94
  function emit(level, message, symbol = '') {
48
95
  const line = symbol ? `${PREFIX} ${symbol} ${message}` : `${PREFIX} ${message}`;
49
96
  console.error(line);
@@ -55,6 +102,8 @@ function emit(level, message, symbol = '') {
55
102
 
56
103
  function info(message) {
57
104
  emit('info', message);
105
+ // Also send as MCP progress notification when token is active
106
+ void sendProgress(message);
58
107
  }
59
108
 
60
109
  function warn(message) {
@@ -69,6 +118,6 @@ function debug(message) {
69
118
  emit('debug', message, '🔍');
70
119
  }
71
120
 
72
- export const logger = { info, warn, error, debug, attachServer, setConsoleOutput };
73
- export { attachServer, setConsoleOutput };
121
+ export const logger = { info, warn, error, debug, attachServer, setConsoleOutput, setProgressToken, clearProgressToken };
122
+ export { attachServer, setConsoleOutput, setProgressToken, clearProgressToken };
74
123
  export default logger;
package/src/core/page.js CHANGED
@@ -475,23 +475,52 @@ async function waitForNavigationToSettle(page) {
475
475
  * settle and retries once.
476
476
  * @param {Page} page - The Puppeteer page instance
477
477
  * @param {boolean} removeUnnecessaryHTML - Whether to clean the HTML
478
+ * @param {string|null} [selector=null] - CSS selector to extract a DOM subtree instead of full page
478
479
  * @returns {Promise<string>} The processed HTML
479
480
  */
480
- export async function extractAndProcessHtml(page, removeUnnecessaryHTML) {
481
+ export async function extractAndProcessHtml(page, removeUnnecessaryHTML, selector = null) {
481
482
  let html;
483
+
484
+ const extractFn = selector
485
+ ? (sel) => {
486
+ const els = document.querySelectorAll(sel);
487
+ if (!els.length) return null;
488
+ return Array.from(els).map(el => el.outerHTML).join('\n');
489
+ }
490
+ : () => document.documentElement?.outerHTML || "";
491
+
492
+ const extractArg = selector || undefined;
493
+
482
494
  try {
483
- html = await page.evaluate(() => document.documentElement?.outerHTML || "");
495
+ html = await page.evaluate(extractFn, extractArg);
484
496
  } catch (err) {
485
497
  if (isNavigationError(err)) {
486
498
  logger.debug('Late navigation during HTML extraction, waiting for settle...');
487
499
  await waitForNavigationToSettle(page);
488
500
  // Re-run page readiness — the new page may be a SPA that needs rendering time
489
501
  await waitForPageReady(page);
490
- html = await page.evaluate(() => document.documentElement?.outerHTML || "");
502
+ html = await page.evaluate(extractFn, extractArg);
491
503
  } else {
492
504
  throw err;
493
505
  }
494
506
  }
507
+
508
+ // If selector matched nothing, fall back to full page with a note
509
+ if (selector && html === null) {
510
+ logger.debug(`Selector "${selector}" matched no elements, falling back to full page`);
511
+ try {
512
+ html = await page.evaluate(() => document.documentElement?.outerHTML || "");
513
+ } catch (err) {
514
+ if (isNavigationError(err)) {
515
+ await waitForNavigationToSettle(page);
516
+ await waitForPageReady(page);
517
+ html = await page.evaluate(() => document.documentElement?.outerHTML || "");
518
+ } else {
519
+ throw err;
520
+ }
521
+ }
522
+ html = `<!-- selector "${selector}" matched no elements; returning full page -->\n` + html;
523
+ }
495
524
 
496
525
  let processedHtml;
497
526
  if (removeUnnecessaryHTML) {
@@ -501,5 +530,12 @@ export async function extractAndProcessHtml(page, removeUnnecessaryHTML) {
501
530
  processedHtml = enrichHtml(html, page.url());
502
531
  }
503
532
 
533
+ // Warn when response is very large — the agent should use the selector parameter
534
+ // to scope extraction to a DOM subtree instead of fetching the entire page.
535
+ const htmlByteLength = new TextEncoder().encode(processedHtml).length;
536
+ if (htmlByteLength > 500_000) {
537
+ logger.warn(`Large HTML response (${(htmlByteLength / 1024).toFixed(0)}KB). Consider using the "selector" parameter to extract a specific DOM subtree instead of the full page.`);
538
+ }
539
+
504
540
  return processedHtml;
505
541
  }
@@ -302,7 +302,7 @@ export function getPluginNextSteps(url, html) {
302
302
  /**
303
303
  * Build the recommendedPlugins payload for structuredContent.
304
304
  * Returns full plugin metadata including action catalog with params,
305
- * so agents can call actions directly without needing plugin_info.
305
+ * so agents can call actions directly without needing browser_plugin_info.
306
306
  * @param {string} url - Current page URL
307
307
  * @param {string} html - Extracted page HTML
308
308
  * @returns {Array<{ plugin: string, recommendation: string, actions: Array, usage: string }>}
@@ -317,7 +317,7 @@ export function getRecommendedPlugins(url, html) {
317
317
  plugin: d.pluginName,
318
318
  recommendation: recommendationText,
319
319
  actions: info.actions || [],
320
- usage: `plugin_action({ plugin: '${d.pluginName}', action: '<name>', params: {...} })`
320
+ usage: `browser_plugin_action({ plugin: '${d.pluginName}', action: '<name>', params: {...} })`
321
321
  };
322
322
  });
323
323
  }
@@ -188,7 +188,7 @@ function getHttpStatusNextSteps(statusCode, url) {
188
188
  return [
189
189
  'Authentication may be required - try logging in first',
190
190
  'Check if you have permission to access this resource',
191
- "Use MCPBrowser's fetch_webpage to navigate to the login page first"
191
+ "Use MCPBrowser's browser_fetch_webpage to navigate to the login page first"
192
192
  ];
193
193
  }
194
194
 
@@ -204,14 +204,14 @@ function getHttpStatusNextSteps(statusCode, url) {
204
204
  return [
205
205
  'Rate limit exceeded - wait a few minutes before retrying',
206
206
  'Reduce request frequency',
207
- "Call MCPBrowser's fetch_webpage again after waiting"
207
+ "Call MCPBrowser's browser_fetch_webpage again after waiting"
208
208
  ];
209
209
  }
210
210
 
211
211
  if (statusCode >= 500 && statusCode < 600) {
212
212
  return [
213
213
  'The server is experiencing issues',
214
- "Wait a moment and try again with MCPBrowser's fetch_webpage",
214
+ "Wait a moment and try again with MCPBrowser's browser_fetch_webpage",
215
215
  'Check if the service has a status page for outages'
216
216
  ];
217
217
  }
@@ -219,7 +219,7 @@ function getHttpStatusNextSteps(statusCode, url) {
219
219
  return [
220
220
  ...baseSteps,
221
221
  'Try again later if this is a temporary issue',
222
- "Call MCPBrowser's fetch_webpage to retry the request"
222
+ "Call MCPBrowser's browser_fetch_webpage to retry the request"
223
223
  ];
224
224
  }
225
225
 
@@ -86,10 +86,16 @@ async function main() {
86
86
 
87
87
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
88
88
 
89
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
89
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
90
90
  const { name, arguments: args } = request.params;
91
91
  const safeArgs = args || {};
92
92
 
93
+ // Enable MCP progress notifications for this request if the client sent a progressToken.
94
+ // Every logger.info() call during tool execution will automatically send a
95
+ // notifications/progress message so the agent sees real-time status updates.
96
+ const progressToken = extra?._meta?.progressToken;
97
+ logger.setProgressToken(progressToken);
98
+
93
99
  let result;
94
100
 
95
101
  try {
@@ -104,47 +110,47 @@ async function main() {
104
110
  // result = await handleAcceptEula(safeArgs);
105
111
  // break;
106
112
 
107
- case "fetch_webpage":
113
+ case "browser_fetch_webpage":
108
114
  result = await fetchPage(safeArgs);
109
115
  break;
110
116
 
111
- case "execute_javascript":
117
+ case "browser_execute_javascript":
112
118
  result = await executeJavascript(safeArgs);
113
119
  break;
114
120
 
115
- case "click_element":
121
+ case "browser_click_element":
116
122
  result = await clickElement(safeArgs);
117
123
  break;
118
124
 
119
- case "type_text":
125
+ case "browser_type_text":
120
126
  result = await typeText(safeArgs);
121
127
  break;
122
128
 
123
- case "close_tab":
129
+ case "browser_close_tab":
124
130
  result = await closeTab(safeArgs);
125
131
  break;
126
132
 
127
- case "get_current_html":
133
+ case "browser_get_current_html":
128
134
  result = await getCurrentHtml(safeArgs);
129
135
  break;
130
136
 
131
- case "take_screenshot":
137
+ case "browser_take_screenshot":
132
138
  result = await takeScreenshot(safeArgs);
133
139
  break;
134
140
 
135
- case "scroll_page":
141
+ case "browser_scroll_page":
136
142
  result = await scrollPage(safeArgs);
137
143
  break;
138
144
 
139
- case "navigate_history":
145
+ case "browser_navigate_history":
140
146
  result = await navigateHistory(safeArgs);
141
147
  break;
142
148
 
143
- case "plugin_info":
149
+ case "browser_plugin_info":
144
150
  result = pluginInfo(safeArgs);
145
151
  break;
146
152
 
147
- case "plugin_action":
153
+ case "browser_plugin_action":
148
154
  result = await pluginAction(safeArgs);
149
155
  break;
150
156
 
@@ -161,6 +167,8 @@ async function main() {
161
167
  `${name} failed: ${error.message}`,
162
168
  ['Check browser is installed', 'Try specifying browser parameter explicitly (chrome, edge, or brave)', 'Check MCP server logs for details']
163
169
  ).toMcpFormat();
170
+ } finally {
171
+ logger.clearProgressToken();
164
172
  }
165
173
 
166
174
  // Transform result into MCP-compliant response using instance method
@@ -81,7 +81,7 @@ export function getActions() {
81
81
  }, limit);
82
82
 
83
83
  return new ExampleActionResponse(items, [
84
- "Call plugin_action with action 'get_item_detail' to read a specific item"
84
+ "Call browser_plugin_action with action 'get_item_detail' to read a specific item"
85
85
  ]);
86
86
  }
87
87
  },
@@ -114,7 +114,7 @@ export function getActions() {
114
114
  }
115
115
 
116
116
  return new ExampleActionResponse(detail, [
117
- "Use plugin_action with action 'list_items' to see all items"
117
+ "Use browser_plugin_action with action 'list_items' to see all items"
118
118
  ]);
119
119
  }
120
120
  }
@@ -85,7 +85,7 @@ export async function checkAvailability({ page, params }) {
85
85
  const pre = await checkPrecondition(page, 'on_calendar');
86
86
  if (!pre.met) {
87
87
  return new ErrorResponse(pre.error, [
88
- pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
88
+ pre.suggestion || "Use browser_fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
89
89
  ]);
90
90
  }
91
91
 
@@ -47,7 +47,7 @@ export async function createEvent({ page, params }) {
47
47
  const pre = await checkPrecondition(page, 'on_calendar');
48
48
  if (!pre.met) {
49
49
  return new ErrorResponse(pre.error, [
50
- pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
50
+ pre.suggestion || "Use browser_fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
51
51
  ]);
52
52
  }
53
53
 
@@ -44,7 +44,7 @@ export async function deleteEvent({ page, params }) {
44
44
  const pre = await checkPrecondition(page, 'on_calendar');
45
45
  if (!pre.met) {
46
46
  return new ErrorResponse(pre.error, [
47
- pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
47
+ pre.suggestion || "Use browser_fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
48
48
  ]);
49
49
  }
50
50
 
@@ -51,7 +51,7 @@ export async function editEvent({ page, params }) {
51
51
  const pre = await checkPrecondition(page, 'on_calendar');
52
52
  if (!pre.met) {
53
53
  return new ErrorResponse(pre.error, [
54
- pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
54
+ pre.suggestion || "Use browser_fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
55
55
  ]);
56
56
  }
57
57
 
@@ -34,7 +34,7 @@ export async function listEvents({ page, params }) {
34
34
  const pre = await checkPrecondition(page, 'on_calendar');
35
35
  if (!pre.met) {
36
36
  return new ErrorResponse(pre.error, [
37
- pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
37
+ pre.suggestion || "Use browser_fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
38
38
  ]);
39
39
  }
40
40
 
@@ -47,7 +47,7 @@ export async function readEvent({ page, params }) {
47
47
  const pre = await checkPrecondition(page, 'on_calendar');
48
48
  if (!pre.met) {
49
49
  return new ErrorResponse(pre.error, [
50
- pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
50
+ pre.suggestion || "Use browser_fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
51
51
  ]);
52
52
  }
53
53
 
@@ -62,7 +62,7 @@ export async function rsvpEvent({ page, params }) {
62
62
  const pre = await checkPrecondition(page, 'on_calendar');
63
63
  if (!pre.met) {
64
64
  return new ErrorResponse(pre.error, [
65
- pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
65
+ pre.suggestion || "Use browser_fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
66
66
  ]);
67
67
  }
68
68
 
@@ -42,7 +42,7 @@ export async function searchEvents({ page, params }) {
42
42
  const pre = await checkPrecondition(page, 'on_calendar');
43
43
  if (!pre.met) {
44
44
  return new ErrorResponse(pre.error, [
45
- pre.suggestion || "Use fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
45
+ pre.suggestion || "Use browser_fetch_webpage({ url: 'https://calendar.google.com' }) to open Google Calendar first."
46
46
  ]);
47
47
  }
48
48