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.
- package/package.json +1 -1
- package/src/actions/click-element.js +8 -3
- package/src/actions/execute-javascript.js +8 -3
- package/src/actions/fetch-page.js +9 -3
- package/src/actions/get-current-html.js +9 -3
- package/src/actions/plugin-action.js +180 -0
- package/src/actions/plugin-info.js +170 -0
- package/src/core/logger.js +3 -7
- package/src/core/plugin-loader.js +344 -0
- package/src/mcp-browser.js +34 -2
- package/src/plugins/_example/index.js +140 -0
- package/src/plugins/gcal/actions/check-availability.js +185 -0
- package/src/plugins/gcal/actions/create-event.js +238 -0
- package/src/plugins/gcal/actions/delete-event.js +138 -0
- package/src/plugins/gcal/actions/edit-event.js +244 -0
- package/src/plugins/gcal/actions/list-events.js +96 -0
- package/src/plugins/gcal/actions/read-event.js +174 -0
- package/src/plugins/gcal/actions/rsvp-event.js +149 -0
- package/src/plugins/gcal/actions/search-events.js +121 -0
- package/src/plugins/gcal/helpers.js +415 -0
- package/src/plugins/gcal/index.js +148 -0
- package/src/plugins/gcal/selectors.js +54 -0
- package/src/plugins/gmail/actions/archive-email.js +65 -0
- package/src/plugins/gmail/actions/compose-email.js +116 -0
- package/src/plugins/gmail/actions/delete-email.js +65 -0
- package/src/plugins/gmail/actions/forward-email.js +95 -0
- package/src/plugins/gmail/actions/label-email.js +107 -0
- package/src/plugins/gmail/actions/list-emails.js +61 -0
- package/src/plugins/gmail/actions/mark-read.js +71 -0
- package/src/plugins/gmail/actions/mark-unread.js +71 -0
- package/src/plugins/gmail/actions/read-email.js +149 -0
- package/src/plugins/gmail/actions/reply-email.js +87 -0
- package/src/plugins/gmail/actions/search-emails.js +95 -0
- package/src/plugins/gmail/helpers.js +419 -0
- package/src/plugins/gmail/index.js +195 -0
- package/src/plugins/gmail/selectors.js +82 -0
- package/src/plugins.json +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcpbrowser",
|
|
3
|
-
"version": "0.3.
|
|
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
|
+
}
|
package/src/core/logger.js
CHANGED
|
@@ -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
|
|
42
|
-
//
|
|
43
|
-
|
|
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
|
|