mcpbrowser 0.3.34 → 0.3.36

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 (37) hide show
  1. package/package.json +1 -1
  2. package/src/actions/click-element.js +8 -3
  3. package/src/actions/execute-javascript.js +8 -3
  4. package/src/actions/fetch-page.js +9 -3
  5. package/src/actions/get-current-html.js +9 -3
  6. package/src/actions/plugin-action.js +180 -0
  7. package/src/actions/plugin-info.js +170 -0
  8. package/src/core/logger.js +3 -7
  9. package/src/core/plugin-loader.js +344 -0
  10. package/src/mcp-browser.js +34 -2
  11. package/src/plugins/_example/index.js +140 -0
  12. package/src/plugins/gcal/actions/check-availability.js +185 -0
  13. package/src/plugins/gcal/actions/create-event.js +238 -0
  14. package/src/plugins/gcal/actions/delete-event.js +138 -0
  15. package/src/plugins/gcal/actions/edit-event.js +244 -0
  16. package/src/plugins/gcal/actions/list-events.js +96 -0
  17. package/src/plugins/gcal/actions/read-event.js +174 -0
  18. package/src/plugins/gcal/actions/rsvp-event.js +149 -0
  19. package/src/plugins/gcal/actions/search-events.js +121 -0
  20. package/src/plugins/gcal/helpers.js +415 -0
  21. package/src/plugins/gcal/index.js +148 -0
  22. package/src/plugins/gcal/selectors.js +54 -0
  23. package/src/plugins/gmail/actions/archive-email.js +65 -0
  24. package/src/plugins/gmail/actions/compose-email.js +116 -0
  25. package/src/plugins/gmail/actions/delete-email.js +65 -0
  26. package/src/plugins/gmail/actions/forward-email.js +95 -0
  27. package/src/plugins/gmail/actions/label-email.js +107 -0
  28. package/src/plugins/gmail/actions/list-emails.js +61 -0
  29. package/src/plugins/gmail/actions/mark-read.js +71 -0
  30. package/src/plugins/gmail/actions/mark-unread.js +71 -0
  31. package/src/plugins/gmail/actions/read-email.js +149 -0
  32. package/src/plugins/gmail/actions/reply-email.js +87 -0
  33. package/src/plugins/gmail/actions/search-emails.js +95 -0
  34. package/src/plugins/gmail/helpers.js +419 -0
  35. package/src/plugins/gmail/index.js +195 -0
  36. package/src/plugins/gmail/selectors.js +82 -0
  37. package/src/plugins.json +4 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcpbrowser",
3
- "version": "0.3.34",
3
+ "version": "0.3.36",
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.",
@@ -28,6 +28,7 @@ import { getBrowser, getValidatedPage } from '../core/browser.js';
28
28
  import { extractAndProcessHtml, waitForPageReady } from '../core/page.js';
29
29
  import { MCPResponse, InformationalResponse } from '../core/responses.js';
30
30
  import logger from '../core/logger.js';
31
+ import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
31
32
 
32
33
  /**
33
34
  * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
@@ -37,7 +38,7 @@ import logger from '../core/logger.js';
37
38
  * Structured response for click_element with JS fallback metadata
38
39
  */
39
40
  export class ClickWithFallbackResponse extends MCPResponse {
40
- constructor({ status, fallbackUsed = false, nativeAttempt, fallbackAttempt, postClickWait, currentUrl, html = null, message, nextSteps = [] }) {
41
+ constructor({ status, fallbackUsed = false, nativeAttempt, fallbackAttempt, postClickWait, currentUrl, html = null, message, nextSteps = [], recommendedPlugins = [] }) {
41
42
  super(nextSteps);
42
43
  this.status = status;
43
44
  this.fallbackUsed = fallbackUsed;
@@ -47,6 +48,7 @@ export class ClickWithFallbackResponse extends MCPResponse {
47
48
  this.currentUrl = currentUrl;
48
49
  this.html = html;
49
50
  this.message = message;
51
+ this.recommendedPlugins = recommendedPlugins;
50
52
  }
51
53
 
52
54
  _getAdditionalFields() {
@@ -58,7 +60,8 @@ export class ClickWithFallbackResponse extends MCPResponse {
58
60
  postClickWait: this.postClickWait,
59
61
  currentUrl: this.currentUrl,
60
62
  html: this.html,
61
- message: this.message
63
+ message: this.message,
64
+ recommendedPlugins: this.recommendedPlugins
62
65
  };
63
66
  }
64
67
 
@@ -328,6 +331,7 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
328
331
 
329
332
  const nextSteps = returnHtml
330
333
  ? [
334
+ ...(html ? getPluginNextSteps(currentUrl, html) : []),
331
335
  "Use MCPBrowser's click_element again to navigate further",
332
336
  "Use MCPBrowser's type_text to fill forms if needed",
333
337
  "Use MCPBrowser's get_current_html to refresh page state",
@@ -352,7 +356,8 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
352
356
  currentUrl,
353
357
  html,
354
358
  message,
355
- nextSteps
359
+ nextSteps,
360
+ recommendedPlugins: html ? getRecommendedPlugins(currentUrl, html) : []
356
361
  });
357
362
  } catch (err) {
358
363
  logger.error(`click_element failed: ${err.message}`);
@@ -7,6 +7,7 @@ import { waitForPageReady } from '../core/page.js';
7
7
  import { MCPResponse, InformationalResponse } from '../core/responses.js';
8
8
  import logger from '../core/logger.js';
9
9
  import { serializeExecutionResult } from '../utils.js';
10
+ import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
10
11
 
11
12
  // Shared execution defaults for script actions
12
13
  export const EXECUTION_TIMEOUT_DEFAULT_MS = 30_000;
@@ -21,7 +22,7 @@ export const EXECUTION_RESULT_MAX_BYTES = 100_000;
21
22
  * Structured response for execute_javascript action
22
23
  */
23
24
  export class ExecuteJavascriptResponse extends MCPResponse {
24
- constructor({ result, type, executionTimeMs, truncated = false, urlChanged = false, currentUrl = '', error = null, nextSteps = [] }) {
25
+ constructor({ result, type, executionTimeMs, truncated = false, urlChanged = false, currentUrl = '', error = null, nextSteps = [], recommendedPlugins = [] }) {
25
26
  super(nextSteps);
26
27
 
27
28
  this.result = result;
@@ -31,6 +32,7 @@ export class ExecuteJavascriptResponse extends MCPResponse {
31
32
  this.urlChanged = urlChanged;
32
33
  this.currentUrl = currentUrl;
33
34
  this.error = error;
35
+ this.recommendedPlugins = recommendedPlugins;
34
36
  }
35
37
 
36
38
  _getAdditionalFields() {
@@ -41,7 +43,8 @@ export class ExecuteJavascriptResponse extends MCPResponse {
41
43
  truncated: this.truncated,
42
44
  urlChanged: this.urlChanged,
43
45
  currentUrl: this.currentUrl,
44
- error: this.error || undefined
46
+ error: this.error || undefined,
47
+ recommendedPlugins: this.recommendedPlugins
45
48
  };
46
49
  }
47
50
 
@@ -223,10 +226,12 @@ export async function executeJavascript({ url, script, timeoutMs = EXECUTION_TIM
223
226
  urlChanged,
224
227
  currentUrl,
225
228
  nextSteps: [
229
+ ...getPluginNextSteps(currentUrl, ''),
226
230
  'Use click_element or type_text for follow-up actions',
227
231
  'Inspect urlChanged to decide if navigation occurred',
228
232
  serialization.truncated ? 'Narrow your selector or reduce returned fields to avoid truncation' : 'Proceed with the returned data'
229
- ]
233
+ ],
234
+ recommendedPlugins: getRecommendedPlugins(currentUrl, '')
230
235
  });
231
236
  }
232
237
 
@@ -8,6 +8,7 @@ import { getOrCreatePage, queueRequest, navigateToUrl, waitForPageReady, extract
8
8
  import { isLikelyAuthUrl, waitForAuth } from '../core/auth.js';
9
9
  import { MCPResponse, ErrorResponse, HttpStatusResponse, InformationalResponse } from '../core/responses.js';
10
10
  import logger from '../core/logger.js';
11
+ import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
11
12
 
12
13
  /**
13
14
  * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
@@ -25,8 +26,9 @@ export class FetchPageSuccessResponse extends MCPResponse {
25
26
  * @param {string} currentUrl - Final URL after redirects
26
27
  * @param {string} html - Page HTML content
27
28
  * @param {string[]} nextSteps - Suggested next actions
29
+ * @param {Array} [recommendedPlugins] - Detected plugin metadata
28
30
  */
29
- constructor(currentUrl, html, nextSteps) {
31
+ constructor(currentUrl, html, nextSteps, recommendedPlugins = []) {
30
32
  super(nextSteps);
31
33
 
32
34
  if (typeof currentUrl !== 'string') {
@@ -38,12 +40,14 @@ export class FetchPageSuccessResponse extends MCPResponse {
38
40
 
39
41
  this.currentUrl = currentUrl;
40
42
  this.html = html;
43
+ this.recommendedPlugins = recommendedPlugins;
41
44
  }
42
45
 
43
46
  _getAdditionalFields() {
44
47
  return {
45
48
  currentUrl: this.currentUrl,
46
- html: this.html
49
+ html: this.html,
50
+ recommendedPlugins: this.recommendedPlugins
47
51
  };
48
52
  }
49
53
 
@@ -220,12 +224,14 @@ async function doFetchPage({ url, browser, removeUnnecessaryHTML, postLoadWait }
220
224
  page.url(),
221
225
  processedHtml,
222
226
  [
227
+ ...getPluginNextSteps(page.url(), processedHtml),
223
228
  "Use MCPBrowser's click_element to interact with buttons/links on the page",
224
229
  "Use MCPBrowser's type_text to fill in form fields",
225
230
  "Use MCPBrowser's get_current_html to re-check page state after interactions",
226
231
  "Use MCPBrowser's take_screenshot if page has charts, images, or complex visual layout that's hard to understand from HTML",
227
232
  "Use MCPBrowser's close_tab when finished to free browser resources"
228
- ]
233
+ ],
234
+ getRecommendedPlugins(page.url(), processedHtml)
229
235
  );
230
236
  } catch (err) {
231
237
  logger.error(`fetch_webpage failed: ${err.message || String(err)}`);
@@ -6,6 +6,7 @@ import { getBrowser, getValidatedPage } from '../core/browser.js';
6
6
  import { extractAndProcessHtml } from '../core/page.js';
7
7
  import { MCPResponse, InformationalResponse } from '../core/responses.js';
8
8
  import logger from '../core/logger.js';
9
+ import { getPluginNextSteps, getRecommendedPlugins } from '../core/plugin-loader.js';
9
10
 
10
11
  /**
11
12
  * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
@@ -23,8 +24,9 @@ export class GetCurrentHtmlSuccessResponse extends MCPResponse {
23
24
  * @param {string} currentUrl - Current page URL
24
25
  * @param {string} html - Page HTML content
25
26
  * @param {string[]} nextSteps - Suggested next actions
27
+ * @param {Array} [recommendedPlugins] - Detected plugin metadata
26
28
  */
27
- constructor(currentUrl, html, nextSteps) {
29
+ constructor(currentUrl, html, nextSteps, recommendedPlugins = []) {
28
30
  super(nextSteps);
29
31
 
30
32
  if (typeof currentUrl !== 'string') {
@@ -36,12 +38,14 @@ export class GetCurrentHtmlSuccessResponse extends MCPResponse {
36
38
 
37
39
  this.currentUrl = currentUrl;
38
40
  this.html = html;
41
+ this.recommendedPlugins = recommendedPlugins;
39
42
  }
40
43
 
41
44
  _getAdditionalFields() {
42
45
  return {
43
46
  currentUrl: this.currentUrl,
44
- html: this.html
47
+ html: this.html,
48
+ recommendedPlugins: this.recommendedPlugins
45
49
  };
46
50
  }
47
51
 
@@ -157,11 +161,13 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
157
161
  currentUrl,
158
162
  html,
159
163
  [
164
+ ...getPluginNextSteps(currentUrl, html),
160
165
  "Use MCPBrowser's click_element to interact with elements",
161
166
  "Use MCPBrowser's type_text to fill forms",
162
167
  "Use MCPBrowser's take_screenshot if page layout or visual content is hard to understand from HTML",
163
168
  "Use MCPBrowser's close_tab to free resources when done"
164
- ]
169
+ ],
170
+ getRecommendedPlugins(currentUrl, html)
165
171
  );
166
172
  } catch (err) {
167
173
  logger.error(`get_current_html failed: ${err.message}`);
@@ -0,0 +1,180 @@
1
+ /**
2
+ * plugin-action.js — MCP tool that dispatches to a plugin's action.
3
+ * Looks up the plugin by name, finds the action, provides the browser
4
+ * page object, and calls the action's execute function.
5
+ * Part of the plugin dispatch pair (plugin_info + plugin_action).
6
+ */
7
+
8
+ import { MCPResponse, ErrorResponse } from '../core/responses.js';
9
+ import { getLoadedPlugins, getPlugin } from '../core/plugin-loader.js';
10
+ import { getBrowser, getValidatedPage } from '../core/browser.js';
11
+ import logger from '../core/logger.js';
12
+
13
+ /**
14
+ * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
15
+ */
16
+
17
+ // ============================================================================
18
+ // RESPONSE CLASS
19
+ // ============================================================================
20
+
21
+ /** Response wrapping a plugin action's raw result */
22
+ export class PluginActionSuccessResponse extends MCPResponse {
23
+ constructor(pluginName, actionName, data, nextSteps) {
24
+ super(nextSteps);
25
+ this.pluginName = pluginName;
26
+ this.actionName = actionName;
27
+ this.data = data;
28
+ }
29
+ _getAdditionalFields() { return { pluginName: this.pluginName, actionName: this.actionName, data: this.data }; }
30
+ getTextSummary() { return `Plugin "${this.pluginName}" action "${this.actionName}" completed`; }
31
+ }
32
+
33
+ // ============================================================================
34
+ // TOOL DEFINITION
35
+ // ============================================================================
36
+
37
+ /** @type {Tool} */
38
+ export const PLUGIN_ACTION_TOOL = {
39
+ name: "plugin_action",
40
+ title: "Plugin Action",
41
+ description: "Execute a site-specific plugin action. Use plugin_info first to discover available actions and their parameters. Plugins provide specialized automation for UI-heavy websites like Gmail, Outlook, PowerBI, AWS, and Azure — faster and more reliable than generic DOM interaction.",
42
+ inputSchema: {
43
+ type: "object",
44
+ properties: {
45
+ plugin: {
46
+ type: "string",
47
+ description: "Plugin name (e.g., 'gmail', 'outlook', 'powerbi')"
48
+ },
49
+ action: {
50
+ type: "string",
51
+ description: "Action name within the plugin (e.g., 'list_emails', 'extract_grid')"
52
+ },
53
+ params: {
54
+ type: "object",
55
+ description: "Action parameters. Use plugin_info to discover accepted parameters.",
56
+ additionalProperties: true
57
+ }
58
+ },
59
+ required: ["plugin", "action"],
60
+ additionalProperties: false
61
+ },
62
+ outputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ nextSteps: {
66
+ type: "array",
67
+ items: { type: "string" },
68
+ description: "Suggested next actions"
69
+ }
70
+ },
71
+ required: ["nextSteps"],
72
+ additionalProperties: true
73
+ }
74
+ };
75
+
76
+ // ============================================================================
77
+ // ACTION FUNCTION
78
+ // ============================================================================
79
+
80
+ /**
81
+ * Dispatch to a plugin action.
82
+ * @param {Object} params
83
+ * @param {string} params.plugin - Plugin name
84
+ * @param {string} params.action - Action name within the plugin
85
+ * @param {Object} [params.params] - Action parameters
86
+ * @returns {Promise<MCPResponse>}
87
+ */
88
+ export async function pluginAction({ plugin: pluginName, action: actionName, params = {} }) {
89
+ logger.info(`plugin_action called: plugin=${pluginName} action=${actionName}`);
90
+
91
+ const loadedPlugins = getLoadedPlugins();
92
+
93
+ // Validate plugin exists
94
+ const pluginInstance = getPlugin(pluginName);
95
+ if (!pluginInstance) {
96
+ const available = [...loadedPlugins.keys()].join(', ') || '(none)';
97
+ return new ErrorResponse(
98
+ `Unknown plugin: '${pluginName}'. Available plugins: ${available}`,
99
+ ["Call plugin_info() to list all loaded plugins"]
100
+ );
101
+ }
102
+
103
+ // Validate action exists
104
+ const actions = pluginInstance.getActions();
105
+ const actionDef = actions.find(a => a.name === actionName);
106
+ if (!actionDef) {
107
+ const validActions = actions.map(a => a.name).join(', ');
108
+ return new ErrorResponse(
109
+ `Unknown action '${actionName}' for plugin '${pluginName}'. Available actions: ${validActions}`,
110
+ [`Call plugin_info({ plugin: '${pluginName}' }) to see all available actions and their parameters`]
111
+ );
112
+ }
113
+
114
+ // Get browser page — check if on correct domain (US5/T032)
115
+ let page;
116
+ try {
117
+ // Try to get a validated page for any of the plugin's URL patterns
118
+ const browser = await getBrowser();
119
+ const pages = await browser.pages();
120
+
121
+ // Find a page matching any of the plugin's URL patterns
122
+ let matchedPage = null;
123
+ for (const p of pages) {
124
+ try {
125
+ const pageUrl = p.url();
126
+ for (const pattern of pluginInstance.manifest.urlPatterns) {
127
+ if (pageUrl.includes(pattern)) {
128
+ matchedPage = p;
129
+ break;
130
+ }
131
+ }
132
+ if (matchedPage) break;
133
+ } catch { /* skip closed/errored pages */ }
134
+ }
135
+
136
+ if (!matchedPage) {
137
+ const targetPatterns = pluginInstance.manifest.urlPatterns.join(', ');
138
+ return new ErrorResponse(
139
+ `Plugin '${pluginName}' requires ${targetPatterns} but no matching page is open. Use fetch_webpage to navigate to the correct site first.`,
140
+ [`Use MCPBrowser's fetch_webpage to navigate to a page matching: ${targetPatterns}`, `Then retry plugin_action`]
141
+ );
142
+ }
143
+
144
+ page = matchedPage;
145
+ } catch (err) {
146
+ logger.error(`plugin_action: browser error — ${err.message}`);
147
+ return new ErrorResponse(
148
+ `Browser connection failed: ${err.message}`,
149
+ ["Ensure the browser is running with remote debugging enabled", "Retry plugin_action after browser is connected"]
150
+ );
151
+ }
152
+
153
+ // Execute the action
154
+ try {
155
+ const result = await actionDef.execute({ page, params });
156
+
157
+ // If result is already an MCPResponse subclass, return it directly
158
+ if (result && typeof result.toMcpFormat === 'function') {
159
+ return result;
160
+ }
161
+
162
+ // Wrap raw results
163
+ return new PluginActionSuccessResponse(
164
+ pluginName,
165
+ actionName,
166
+ result,
167
+ [`Use plugin_info({ plugin: '${pluginName}' }) to see other available actions`]
168
+ );
169
+ } catch (err) {
170
+ logger.error(`plugin_action: "${pluginName}/${actionName}" failed — ${err.message}`);
171
+ return new ErrorResponse(
172
+ `Plugin '${pluginName}' action '${actionName}' failed: ${err.message}. The site structure may have changed. You can fall back to generic MCPBrowser tools (click_element, get_current_html).`,
173
+ [
174
+ "Check if the page is on the correct site",
175
+ "Try MCPBrowser's get_current_html to inspect the page state",
176
+ "Use generic MCPBrowser tools as a fallback"
177
+ ]
178
+ );
179
+ }
180
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * plugin-info.js — MCP tool that returns information about installed plugins,
3
+ * their available actions, parameters, and high-level site context.
4
+ * Part of the plugin dispatch pair (plugin_info + plugin_action).
5
+ */
6
+
7
+ import { MCPResponse, ErrorResponse } from '../core/responses.js';
8
+ import { getLoadedPlugins, getPlugin } from '../core/plugin-loader.js';
9
+ import logger from '../core/logger.js';
10
+
11
+ /**
12
+ * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
13
+ */
14
+
15
+ // ============================================================================
16
+ // RESPONSE CLASSES
17
+ // ============================================================================
18
+
19
+ /** Response listing all loaded plugins */
20
+ export class PluginListResponse extends MCPResponse {
21
+ constructor(plugins, nextSteps) {
22
+ super(nextSteps);
23
+ this.plugins = plugins;
24
+ }
25
+ _getAdditionalFields() { return { plugins: this.plugins }; }
26
+ getTextSummary() { return `${this.plugins.length} plugin(s) loaded`; }
27
+ }
28
+
29
+ /** Response with plugin detail (action catalog + site context) */
30
+ export class PluginInfoResponse extends MCPResponse {
31
+ constructor(info, nextSteps) {
32
+ super(nextSteps);
33
+ this.pluginInfo = info;
34
+ }
35
+ _getAdditionalFields() { return { ...this.pluginInfo }; }
36
+ getTextSummary() { return `Plugin "${this.pluginInfo.name}": ${this.pluginInfo.actions?.length || 0} action(s)`; }
37
+ }
38
+
39
+ /** Response with single action detail */
40
+ export class PluginActionDetailResponse extends MCPResponse {
41
+ constructor(plugin, action, nextSteps) {
42
+ super(nextSteps);
43
+ this.plugin = plugin;
44
+ this.action = action;
45
+ }
46
+ _getAdditionalFields() { return { plugin: this.plugin, action: this.action }; }
47
+ getTextSummary() { return `Action "${this.action.name}" from plugin "${this.plugin}"`; }
48
+ }
49
+
50
+ // ============================================================================
51
+ // TOOL DEFINITION
52
+ // ============================================================================
53
+
54
+ /** @type {Tool} */
55
+ export const PLUGIN_INFO_TOOL = {
56
+ name: "plugin_info",
57
+ title: "Plugin Info",
58
+ description: "Get information about an installed site plugin — its available actions, parameters, and site context. Call this after a plugin is detected (recommended in nextSteps) to discover what actions you can perform via plugin_action. You can also call with no arguments to list all loaded plugins.",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ plugin: {
63
+ type: "string",
64
+ description: "Plugin name to get info for. Omit to list all loaded plugins."
65
+ },
66
+ action: {
67
+ type: "string",
68
+ description: "Optional. Specific action name to get detailed info for."
69
+ }
70
+ },
71
+ additionalProperties: false
72
+ },
73
+ outputSchema: {
74
+ type: "object",
75
+ properties: {
76
+ nextSteps: {
77
+ type: "array",
78
+ items: { type: "string" },
79
+ description: "Suggested next actions"
80
+ }
81
+ },
82
+ required: ["nextSteps"],
83
+ additionalProperties: true
84
+ }
85
+ };
86
+
87
+ // ============================================================================
88
+ // ACTION FUNCTION
89
+ // ============================================================================
90
+
91
+ /**
92
+ * Get plugin information — list all plugins, plugin detail, or action detail.
93
+ * @param {Object} params
94
+ * @param {string} [params.plugin] - Plugin name (omit to list all)
95
+ * @param {string} [params.action] - Action name (requires plugin)
96
+ * @returns {MCPResponse}
97
+ */
98
+ export function pluginInfo({ plugin, action } = {}) {
99
+ logger.info(`plugin_info called: plugin=${plugin || '(all)'} action=${action || '(all)'}`);
100
+
101
+ const loadedPlugins = getLoadedPlugins();
102
+
103
+ // Mode 1: List all plugins
104
+ if (!plugin) {
105
+ const plugins = [];
106
+ for (const [name, p] of loadedPlugins) {
107
+ plugins.push({
108
+ name,
109
+ description: p.manifest.description,
110
+ actionCount: p.getActions().length
111
+ });
112
+ }
113
+
114
+ const nextSteps = plugins.length > 0
115
+ ? plugins.map(p => `Call plugin_info({ plugin: '${p.name}' }) to see ${p.name}'s available actions`)
116
+ : ["No plugins are currently loaded. Add plugin names to plugins.json and restart the server."];
117
+
118
+ return new PluginListResponse(plugins, nextSteps);
119
+ }
120
+
121
+ // Validate plugin exists
122
+ const pluginInstance = getPlugin(plugin);
123
+ if (!pluginInstance) {
124
+ const available = [...loadedPlugins.keys()].join(', ') || '(none)';
125
+ return new ErrorResponse(
126
+ `Unknown plugin: '${plugin}'. Available plugins: ${available}`,
127
+ loadedPlugins.size > 0
128
+ ? [`Call plugin_info() with no arguments to list all plugins`]
129
+ : ["No plugins are currently loaded. Add plugin names to plugins.json and restart the server."]
130
+ );
131
+ }
132
+
133
+ // Mode 3: Single action detail
134
+ if (action) {
135
+ const actions = pluginInstance.getActions();
136
+ const actionDef = actions.find(a => a.name === action);
137
+ if (!actionDef) {
138
+ const validActions = actions.map(a => a.name).join(', ');
139
+ return new ErrorResponse(
140
+ `Unknown action '${action}' for plugin '${plugin}'. Available actions: ${validActions}`,
141
+ [`Call plugin_info({ plugin: '${plugin}' }) to see all available actions`]
142
+ );
143
+ }
144
+
145
+ return new PluginActionDetailResponse(
146
+ plugin,
147
+ { name: actionDef.name, description: actionDef.description, params: actionDef.params },
148
+ [`Call plugin_action({ plugin: '${plugin}', action: '${action}', params: { ... } })`]
149
+ );
150
+ }
151
+
152
+ // Mode 2: Plugin detail with full action catalog
153
+ const info = pluginInstance.getInfo();
154
+ const pluginDetail = {
155
+ name: plugin,
156
+ description: info.description,
157
+ targetPages: info.targetPages,
158
+ ...(info.authFlow ? { authFlow: info.authFlow } : {}),
159
+ actions: info.actions || []
160
+ };
161
+
162
+ const nextSteps = [
163
+ ...(info.actions || []).slice(0, 3).map(a =>
164
+ `Use plugin_action({ plugin: '${plugin}', action: '${a.name}' }) to ${a.description.toLowerCase()}`
165
+ ),
166
+ "Use fetch_webpage to navigate to the target site first if not already there"
167
+ ];
168
+
169
+ return new PluginInfoResponse(pluginDetail, nextSteps);
170
+ }
@@ -38,13 +38,9 @@ async function notifyAgent(level, data) {
38
38
  // Skip if client requested a higher threshold.
39
39
  if (mcpServer.isMessageIgnored?.(level)) return;
40
40
  await mcpServer.sendLoggingMessage({ level, logger: 'mcpbrowser', data });
41
- } catch (err) {
42
- // Fall back to stderr without recursing through logger.
43
- try {
44
- process.stderr.write(`${PREFIX} logging notification failed: ${err?.message || err}\n`);
45
- } catch (_) {
46
- /* ignore */
47
- }
41
+ } catch {
42
+ // Silently drop this is expected during startup before the MCP
43
+ // transport handshake completes. Stderr already has the message.
48
44
  }
49
45
  }
50
46