mcpbrowser 0.3.33 → 0.3.35

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.33",
3
+ "version": "0.3.35",
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 } from '../core/plugin-loader.js';
31
32
 
32
33
  /**
33
34
  * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
@@ -332,7 +333,8 @@ export async function clickElement({ url, selector, text, waitForElementTimeout
332
333
  "Use MCPBrowser's type_text to fill forms if needed",
333
334
  "Use MCPBrowser's get_current_html to refresh page state",
334
335
  "Use MCPBrowser's take_screenshot if page has popups or visual content that's hard to parse from HTML",
335
- "Use MCPBrowser's close_tab when finished"
336
+ "Use MCPBrowser's close_tab when finished",
337
+ ...(html ? getPluginNextSteps(currentUrl, html) : [])
336
338
  ]
337
339
  : [
338
340
  "Use MCPBrowser's get_current_html to see updated page state",
@@ -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 } 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;
@@ -225,7 +226,8 @@ export async function executeJavascript({ url, script, timeoutMs = EXECUTION_TIM
225
226
  nextSteps: [
226
227
  'Use click_element or type_text for follow-up actions',
227
228
  'Inspect urlChanged to decide if navigation occurred',
228
- serialization.truncated ? 'Narrow your selector or reduce returned fields to avoid truncation' : 'Proceed with the returned data'
229
+ serialization.truncated ? 'Narrow your selector or reduce returned fields to avoid truncation' : 'Proceed with the returned data',
230
+ ...getPluginNextSteps(currentUrl, '')
229
231
  ]
230
232
  });
231
233
  }
@@ -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 } from '../core/plugin-loader.js';
11
12
 
12
13
  /**
13
14
  * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
@@ -224,7 +225,8 @@ async function doFetchPage({ url, browser, removeUnnecessaryHTML, postLoadWait }
224
225
  "Use MCPBrowser's type_text to fill in form fields",
225
226
  "Use MCPBrowser's get_current_html to re-check page state after interactions",
226
227
  "Use MCPBrowser's take_screenshot if page has charts, images, or complex visual layout that's hard to understand from HTML",
227
- "Use MCPBrowser's close_tab when finished to free browser resources"
228
+ "Use MCPBrowser's close_tab when finished to free browser resources",
229
+ ...getPluginNextSteps(page.url(), processedHtml)
228
230
  ]
229
231
  );
230
232
  } catch (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 } from '../core/plugin-loader.js';
9
10
 
10
11
  /**
11
12
  * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
@@ -160,7 +161,8 @@ export async function getCurrentHtml({ url, removeUnnecessaryHTML = true }) {
160
161
  "Use MCPBrowser's click_element to interact with elements",
161
162
  "Use MCPBrowser's type_text to fill forms",
162
163
  "Use MCPBrowser's take_screenshot if page layout or visual content is hard to understand from HTML",
163
- "Use MCPBrowser's close_tab to free resources when done"
164
+ "Use MCPBrowser's close_tab to free resources when done",
165
+ ...getPluginNextSteps(currentUrl, html)
164
166
  ]
165
167
  );
166
168
  } catch (err) {
@@ -0,0 +1,248 @@
1
+ /**
2
+ * navigate-history.js - Browser back/forward navigation
3
+ * Navigates browser history on an already-loaded page.
4
+ */
5
+
6
+ import { getBrowser, getValidatedPage, domainPages } from '../core/browser.js';
7
+ import { extractAndProcessHtml, waitForPageReady } from '../core/page.js';
8
+ import { MCPResponse, InformationalResponse } from '../core/responses.js';
9
+ import logger from '../core/logger.js';
10
+
11
+ /**
12
+ * @typedef {import('@modelcontextprotocol/sdk/types.js').Tool} Tool
13
+ */
14
+
15
+ // ============================================================================
16
+ // RESPONSE CLASS
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Response for successful navigate_history operations
21
+ */
22
+ export class NavigateHistorySuccessResponse extends MCPResponse {
23
+ /**
24
+ * @param {string} direction - Navigation direction (back or forward)
25
+ * @param {string} previousUrl - URL before navigation
26
+ * @param {string} currentUrl - URL after navigation
27
+ * @param {string|null} html - Page HTML content (null if returnHtml=false)
28
+ * @param {string[]} nextSteps - Suggested next actions
29
+ */
30
+ constructor(direction, previousUrl, currentUrl, html, nextSteps) {
31
+ super(nextSteps);
32
+
33
+ if (typeof direction !== 'string') {
34
+ throw new TypeError('direction must be a string');
35
+ }
36
+ if (typeof previousUrl !== 'string') {
37
+ throw new TypeError('previousUrl must be a string');
38
+ }
39
+ if (typeof currentUrl !== 'string') {
40
+ throw new TypeError('currentUrl must be a string');
41
+ }
42
+ if (html !== null && typeof html !== 'string') {
43
+ throw new TypeError('html must be a string or null');
44
+ }
45
+
46
+ this.direction = direction;
47
+ this.previousUrl = previousUrl;
48
+ this.currentUrl = currentUrl;
49
+ this.html = html;
50
+ }
51
+
52
+ _getAdditionalFields() {
53
+ return {
54
+ direction: this.direction,
55
+ previousUrl: this.previousUrl,
56
+ currentUrl: this.currentUrl,
57
+ html: this.html
58
+ };
59
+ }
60
+
61
+ getTextSummary() {
62
+ return `Navigated ${this.direction}: ${this.previousUrl} → ${this.currentUrl}`;
63
+ }
64
+ }
65
+
66
+ // ============================================================================
67
+ // TOOL DEFINITION
68
+ // ============================================================================
69
+
70
+ /** @type {Tool} */
71
+ export const NAVIGATE_HISTORY_TOOL = {
72
+ name: "navigate_history",
73
+ title: "Navigate Back/Forward",
74
+ description: "**BROWSER HISTORY NAVIGATION** - Navigate back or forward in browser history on an already-loaded page. Use after clicking links to return to the previous page, or to go forward after going back.\n\n**PREREQUISITE**: Page MUST be loaded with fetch_webpage first. This tool navigates the history of an existing browser tab.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ url: { type: "string", description: "URL of the already-loaded page (identifies which tab to navigate)" },
79
+ direction: {
80
+ type: "string",
81
+ enum: ["back", "forward"],
82
+ description: "Navigation direction: 'back' to go to previous page, 'forward' to go to next page",
83
+ default: "back"
84
+ },
85
+ returnHtml: { type: "boolean", description: "Return page HTML after navigation", default: true },
86
+ removeUnnecessaryHTML: { type: "boolean", description: "Remove unnecessary HTML elements (scripts, styles, etc.) for size reduction.", default: true }
87
+ },
88
+ required: ["url"],
89
+ additionalProperties: false
90
+ },
91
+ outputSchema: {
92
+ type: "object",
93
+ properties: {
94
+ direction: { type: "string", enum: ["back", "forward"], description: "Navigation direction used" },
95
+ previousUrl: { type: "string", description: "URL before navigation" },
96
+ currentUrl: { type: "string", description: "URL after navigation" },
97
+ html: { type: ["string", "null"], description: "Page HTML content after navigation (null if returnHtml=false)" },
98
+ nextSteps: {
99
+ type: "array",
100
+ items: { type: "string" },
101
+ description: "Suggested next actions"
102
+ }
103
+ },
104
+ required: ["direction", "previousUrl", "currentUrl", "nextSteps"],
105
+ additionalProperties: false
106
+ },
107
+ annotations: {
108
+ title: "Navigate Back/Forward",
109
+ readOnlyHint: false,
110
+ destructiveHint: false,
111
+ openWorldHint: true
112
+ }
113
+ };
114
+
115
+ // ============================================================================
116
+ // ACTION FUNCTION
117
+ // ============================================================================
118
+
119
+ /**
120
+ * Navigate browser history (back/forward) on an already-loaded page
121
+ * @param {Object} params - Parameters
122
+ * @param {string} params.url - URL of the already-loaded page
123
+ * @param {string} [params.direction='back'] - Navigation direction
124
+ * @param {boolean} [params.returnHtml=true] - Return page HTML after navigation
125
+ * @param {boolean} [params.removeUnnecessaryHTML=true] - Clean HTML
126
+ * @returns {Promise<MCPResponse>} Navigation result
127
+ */
128
+ export async function navigateHistory({ url, direction = 'back', returnHtml = true, removeUnnecessaryHTML = true }) {
129
+ logger.info(`navigate_history called: url=${url}, direction=${direction}`);
130
+
131
+ if (!url) {
132
+ throw new Error("url parameter is required");
133
+ }
134
+
135
+ let hostname;
136
+ try {
137
+ hostname = new URL(url).hostname;
138
+ } catch {
139
+ throw new Error(`Invalid URL: ${url}`);
140
+ }
141
+
142
+ // Ensure browser connection
143
+ try {
144
+ await getBrowser();
145
+ } catch (err) {
146
+ logger.error(`navigate_history: Failed to connect to browser: ${err.message}`);
147
+ return new InformationalResponse(
148
+ `Browser connection failed: ${err.message}`,
149
+ 'The browser must be running with remote debugging enabled.',
150
+ [
151
+ 'Ensure the browser is installed and running',
152
+ 'Check that remote debugging is enabled (--remote-debugging-port)',
153
+ 'Try restarting the MCP server'
154
+ ]
155
+ );
156
+ }
157
+
158
+ // Validate page exists and is usable
159
+ const { page, error: pageError } = await getValidatedPage(hostname);
160
+
161
+ if (!page) {
162
+ const isConnectionLost = pageError && pageError.includes('connection');
163
+ logger.debug(`navigate_history: ${pageError || 'No page found for ' + hostname}`);
164
+ return new InformationalResponse(
165
+ isConnectionLost ? `Page connection lost for ${hostname}` : `No open page found for ${hostname}`,
166
+ isConnectionLost
167
+ ? 'The browser tab was closed or the connection was lost. The page needs to be reloaded.'
168
+ : 'The page must be loaded before you can navigate its history.',
169
+ [
170
+ "Use MCPBrowser's fetch_webpage tool to load the page first",
171
+ "Then retry MCPBrowser's navigate_history with the same URL"
172
+ ]
173
+ );
174
+ }
175
+
176
+ try {
177
+ const previousUrl = page.url();
178
+
179
+ // Navigate history
180
+ let response;
181
+ if (direction === 'forward') {
182
+ response = await page.goForward({ waitUntil: 'domcontentloaded', timeout: 30000 });
183
+ } else {
184
+ response = await page.goBack({ waitUntil: 'domcontentloaded', timeout: 30000 });
185
+ }
186
+
187
+ // goBack/goForward return null if there's no history entry
188
+ if (response === null) {
189
+ logger.info(`navigate_history: No ${direction} history entry available`);
190
+ return new InformationalResponse(
191
+ `No ${direction} history entry available`,
192
+ `The page has no ${direction} history to navigate to. This means you're already at the ${direction === 'back' ? 'first' : 'last'} page in the browsing history for this tab.`,
193
+ [
194
+ direction === 'back'
195
+ ? "Use MCPBrowser's fetch_webpage to navigate to a different URL"
196
+ : "Use MCPBrowser's navigate_history with direction='back' to go back instead",
197
+ "Use MCPBrowser's get_current_html to check the current page content"
198
+ ]
199
+ );
200
+ }
201
+
202
+ const currentUrl = page.url();
203
+
204
+ // Update domainPages if hostname changed after navigation
205
+ try {
206
+ const newHostname = new URL(currentUrl).hostname;
207
+ if (newHostname !== hostname) {
208
+ domainPages.delete(hostname);
209
+ domainPages.set(newHostname, page);
210
+ logger.info(`navigate_history: Updated domainPages mapping: ${hostname} → ${newHostname}`);
211
+ }
212
+ } catch {
213
+ // If URL parsing fails, keep existing mapping
214
+ }
215
+
216
+ // Extract HTML if requested
217
+ let html = null;
218
+ if (returnHtml) {
219
+ await waitForPageReady(page);
220
+ html = await extractAndProcessHtml(page, removeUnnecessaryHTML);
221
+ }
222
+
223
+ logger.info(`navigate_history completed: ${direction} from ${previousUrl} to ${currentUrl}`);
224
+
225
+ return new NavigateHistorySuccessResponse(
226
+ direction,
227
+ previousUrl,
228
+ currentUrl,
229
+ html,
230
+ [
231
+ "Use MCPBrowser's navigate_history to go back or forward again",
232
+ "Use MCPBrowser's click_element to interact with elements on the page",
233
+ "Use MCPBrowser's get_current_html to re-read the page content",
234
+ "Use MCPBrowser's fetch_webpage to navigate to a new URL"
235
+ ]
236
+ );
237
+ } catch (err) {
238
+ logger.error(`navigate_history failed: ${err.message}`);
239
+ return new InformationalResponse(
240
+ `Navigation ${direction} failed: ${err.message}`,
241
+ 'The browser could not navigate. The page may have been closed or the connection was lost.',
242
+ [
243
+ "Try MCPBrowser's fetch_webpage to reload the page",
244
+ "Use MCPBrowser's close_tab and start fresh if needed"
245
+ ]
246
+ );
247
+ }
248
+ }
@@ -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
+ }