nodebench-mcp 2.25.0 → 2.27.0
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/NODEBENCH_AGENTS.md +5 -4
- package/README.md +145 -16
- package/dist/__tests__/architectComplex.test.js +3 -5
- package/dist/__tests__/architectComplex.test.js.map +1 -1
- package/dist/__tests__/batchAutopilot.test.d.ts +8 -0
- package/dist/__tests__/batchAutopilot.test.js +218 -0
- package/dist/__tests__/batchAutopilot.test.js.map +1 -0
- package/dist/__tests__/cliSubcommands.test.d.ts +1 -0
- package/dist/__tests__/cliSubcommands.test.js +138 -0
- package/dist/__tests__/cliSubcommands.test.js.map +1 -0
- package/dist/__tests__/evalHarness.test.js +1 -1
- package/dist/__tests__/forecastingDogfood.test.d.ts +9 -0
- package/dist/__tests__/forecastingDogfood.test.js +284 -0
- package/dist/__tests__/forecastingDogfood.test.js.map +1 -0
- package/dist/__tests__/forecastingScoring.test.d.ts +9 -0
- package/dist/__tests__/forecastingScoring.test.js +202 -0
- package/dist/__tests__/forecastingScoring.test.js.map +1 -0
- package/dist/__tests__/localDashboard.test.d.ts +1 -0
- package/dist/__tests__/localDashboard.test.js +226 -0
- package/dist/__tests__/localDashboard.test.js.map +1 -0
- package/dist/__tests__/multiHopDogfood.test.js +11 -11
- package/dist/__tests__/multiHopDogfood.test.js.map +1 -1
- package/dist/__tests__/openclawDogfood.test.d.ts +23 -0
- package/dist/__tests__/openclawDogfood.test.js +535 -0
- package/dist/__tests__/openclawDogfood.test.js.map +1 -0
- package/dist/__tests__/openclawMessaging.test.d.ts +14 -0
- package/dist/__tests__/openclawMessaging.test.js +232 -0
- package/dist/__tests__/openclawMessaging.test.js.map +1 -0
- package/dist/__tests__/presetRealWorldBench.test.js +0 -2
- package/dist/__tests__/presetRealWorldBench.test.js.map +1 -1
- package/dist/__tests__/tools.test.js +9 -157
- package/dist/__tests__/tools.test.js.map +1 -1
- package/dist/__tests__/toolsetGatingEval.test.js +0 -2
- package/dist/__tests__/toolsetGatingEval.test.js.map +1 -1
- package/dist/__tests__/traceabilityDogfood.test.d.ts +12 -0
- package/dist/__tests__/traceabilityDogfood.test.js +241 -0
- package/dist/__tests__/traceabilityDogfood.test.js.map +1 -0
- package/dist/__tests__/webmcpTools.test.d.ts +7 -0
- package/dist/__tests__/webmcpTools.test.js +195 -0
- package/dist/__tests__/webmcpTools.test.js.map +1 -0
- package/dist/dashboard/briefHtml.d.ts +20 -0
- package/dist/dashboard/briefHtml.js +1000 -0
- package/dist/dashboard/briefHtml.js.map +1 -0
- package/dist/dashboard/briefServer.d.ts +18 -0
- package/dist/dashboard/briefServer.js +320 -0
- package/dist/dashboard/briefServer.js.map +1 -0
- package/dist/dashboard/html.js +1470 -1230
- package/dist/dashboard/html.js.map +1 -1
- package/dist/dashboard/server.js +166 -41
- package/dist/dashboard/server.js.map +1 -1
- package/dist/index.js +210 -14
- package/dist/index.js.map +1 -1
- package/dist/tools/critterTools.js +4 -0
- package/dist/tools/critterTools.js.map +1 -1
- package/dist/tools/forecastingTools.d.ts +11 -0
- package/dist/tools/forecastingTools.js +616 -0
- package/dist/tools/forecastingTools.js.map +1 -0
- package/dist/tools/localDashboardTools.d.ts +8 -0
- package/dist/tools/localDashboardTools.js +332 -0
- package/dist/tools/localDashboardTools.js.map +1 -0
- package/dist/tools/metaTools.js +170 -1
- package/dist/tools/metaTools.js.map +1 -1
- package/dist/tools/openclawTools.d.ts +11 -0
- package/dist/tools/openclawTools.js +1017 -0
- package/dist/tools/openclawTools.js.map +1 -0
- package/dist/tools/overstoryTools.d.ts +14 -0
- package/dist/tools/overstoryTools.js +426 -0
- package/dist/tools/overstoryTools.js.map +1 -0
- package/dist/tools/progressiveDiscoveryTools.js +50 -115
- package/dist/tools/progressiveDiscoveryTools.js.map +1 -1
- package/dist/tools/selfEvalTools.js +8 -1
- package/dist/tools/selfEvalTools.js.map +1 -1
- package/dist/tools/sessionMemoryTools.js +14 -2
- package/dist/tools/sessionMemoryTools.js.map +1 -1
- package/dist/tools/toolRegistry.d.ts +1 -15
- package/dist/tools/toolRegistry.js +243 -228
- package/dist/tools/toolRegistry.js.map +1 -1
- package/dist/tools/visualQaTools.d.ts +2 -0
- package/dist/tools/visualQaTools.js +1088 -0
- package/dist/tools/visualQaTools.js.map +1 -0
- package/dist/tools/webmcpTools.d.ts +16 -0
- package/dist/tools/webmcpTools.js +703 -0
- package/dist/tools/webmcpTools.js.map +1 -0
- package/dist/toolsetRegistry.js +6 -2
- package/dist/toolsetRegistry.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebMCP Bridge Tools — Consumer mode for WebMCP-enabled websites.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors mcpBridgeTools.ts pattern: Map-based connection registry,
|
|
5
|
+
* connect/list/call/disconnect lifecycle. The key difference is the
|
|
6
|
+
* transport: Playwright browser pages instead of StdioClientTransport.
|
|
7
|
+
*
|
|
8
|
+
* Discovery: addInitScript intercepts navigator.modelContext.provideContext()
|
|
9
|
+
* and registerTool() BEFORE page code runs, capturing tool metadata.
|
|
10
|
+
*
|
|
11
|
+
* Security: SSRF checks on URLs, arg pattern scanning, result anomaly
|
|
12
|
+
* detection — reusing proven patterns from OpenClaw proxyTools.
|
|
13
|
+
*/
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { homedir } from "node:os";
|
|
18
|
+
// ── Lazy Playwright import (optional dependency) ────────────────────────
|
|
19
|
+
let _playwright = null;
|
|
20
|
+
let _playwrightChecked = false;
|
|
21
|
+
function getPlaywright() {
|
|
22
|
+
if (_playwrightChecked)
|
|
23
|
+
return _playwright;
|
|
24
|
+
_playwrightChecked = true;
|
|
25
|
+
try {
|
|
26
|
+
const req = createRequire(import.meta.url);
|
|
27
|
+
_playwright = req("playwright");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
_playwright = null;
|
|
31
|
+
}
|
|
32
|
+
return _playwright;
|
|
33
|
+
}
|
|
34
|
+
function isPlaywrightAvailable() {
|
|
35
|
+
return getPlaywright() !== null;
|
|
36
|
+
}
|
|
37
|
+
// ── SSRF / URL Validation (inline, mirrors mcpSecurity.ts) ─────────────
|
|
38
|
+
const PRIVATE_IP_PATTERNS = [
|
|
39
|
+
/^127\./,
|
|
40
|
+
/^10\./,
|
|
41
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
42
|
+
/^192\.168\./,
|
|
43
|
+
/^169\.254\./,
|
|
44
|
+
/^0\./,
|
|
45
|
+
/^fc00:/i,
|
|
46
|
+
/^fe80:/i,
|
|
47
|
+
/^::1$/,
|
|
48
|
+
/^localhost$/i,
|
|
49
|
+
];
|
|
50
|
+
function validateOriginUrl(url) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = new URL(url);
|
|
53
|
+
if (parsed.protocol !== "https:") {
|
|
54
|
+
return { valid: false, reason: "HTTPS required — WebMCP requires a secure context" };
|
|
55
|
+
}
|
|
56
|
+
const host = parsed.hostname;
|
|
57
|
+
for (const p of PRIVATE_IP_PATTERNS) {
|
|
58
|
+
if (p.test(host)) {
|
|
59
|
+
return { valid: false, reason: `Blocked: private/loopback address '${host}'` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { valid: true };
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { valid: false, reason: `Invalid URL: '${url}'` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── Security Scanners (lightweight, from OpenClaw patterns) ─────────────
|
|
69
|
+
const SUSPICIOUS_ARG_PATTERNS = [
|
|
70
|
+
/\b(rm\s+-rf|del\s+\/[sf]|format\s+c:)/i,
|
|
71
|
+
/\b(eval|exec|system|popen)\s*\(/i,
|
|
72
|
+
/;\s*(curl|wget|nc|ncat)\s/i,
|
|
73
|
+
/\|\s*(bash|sh|cmd|powershell)\b/i,
|
|
74
|
+
/\$\{.*\}/, // template injection
|
|
75
|
+
];
|
|
76
|
+
function scanArgs(args) {
|
|
77
|
+
const str = JSON.stringify(args);
|
|
78
|
+
for (const p of SUSPICIOUS_ARG_PATTERNS) {
|
|
79
|
+
if (p.test(str))
|
|
80
|
+
return `Suspicious argument pattern: ${p.source}`;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const ANOMALY_PATTERNS = [
|
|
85
|
+
/\b(api_key|apikey|secret_key|access_token|auth_token)\s*[:=]\s*\S{8,}/i,
|
|
86
|
+
/\b(password|passwd)\s*[:=]\s*\S+/i,
|
|
87
|
+
/\b(AKIA|ASIA)[A-Z0-9]{16}\b/,
|
|
88
|
+
/\bghp_[a-zA-Z0-9]{36}\b/,
|
|
89
|
+
/\bsk-[a-zA-Z0-9]{20,}\b/,
|
|
90
|
+
/\bnpm_[a-zA-Z0-9]{36}\b/,
|
|
91
|
+
/\/(etc\/passwd|etc\/shadow|\.ssh\/|\.aws\/)/i,
|
|
92
|
+
/[A-Za-z0-9+/=]{500,}/, // large base64 blob (potential exfiltration)
|
|
93
|
+
];
|
|
94
|
+
function scanResult(result) {
|
|
95
|
+
const str = typeof result === "string" ? result : JSON.stringify(result);
|
|
96
|
+
for (const p of ANOMALY_PATTERNS) {
|
|
97
|
+
if (p.test(str))
|
|
98
|
+
return `Anomaly in result: ${p.source}`;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const _webmcpConnections = new Map();
|
|
103
|
+
// For testing: reset all connections
|
|
104
|
+
export function _resetConnectionsForTesting() {
|
|
105
|
+
_webmcpConnections.clear();
|
|
106
|
+
}
|
|
107
|
+
// ── Discovery Script (injected before page load) ────────────────────────
|
|
108
|
+
const DISCOVERY_INIT_SCRIPT = `
|
|
109
|
+
(() => {
|
|
110
|
+
window.__webmcp_tools = [];
|
|
111
|
+
window.__webmcp_executors = new Map();
|
|
112
|
+
|
|
113
|
+
// Polyfill navigator.modelContext if not present (for testing / pre-spec browsers)
|
|
114
|
+
if (!navigator.modelContext) {
|
|
115
|
+
navigator.modelContext = {
|
|
116
|
+
provideContext: () => {},
|
|
117
|
+
registerTool: () => {},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const mc = navigator.modelContext;
|
|
122
|
+
const origProvide = mc.provideContext?.bind(mc);
|
|
123
|
+
const origRegister = mc.registerTool?.bind(mc);
|
|
124
|
+
|
|
125
|
+
mc.provideContext = function(opts) {
|
|
126
|
+
if (opts && opts.tools) {
|
|
127
|
+
for (const t of opts.tools) {
|
|
128
|
+
window.__webmcp_tools.push({
|
|
129
|
+
name: t.name || 'unnamed',
|
|
130
|
+
description: t.description || '',
|
|
131
|
+
inputSchema: t.inputSchema || null,
|
|
132
|
+
annotations: t.annotations || null,
|
|
133
|
+
});
|
|
134
|
+
if (t.execute) window.__webmcp_executors.set(t.name, t.execute);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return origProvide ? origProvide(opts) : undefined;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
mc.registerTool = function(t) {
|
|
141
|
+
window.__webmcp_tools.push({
|
|
142
|
+
name: t.name || 'unnamed',
|
|
143
|
+
description: t.description || '',
|
|
144
|
+
inputSchema: t.inputSchema || null,
|
|
145
|
+
annotations: t.annotations || null,
|
|
146
|
+
});
|
|
147
|
+
if (t.execute) window.__webmcp_executors.set(t.name, t.execute);
|
|
148
|
+
return origRegister ? origRegister(t) : undefined;
|
|
149
|
+
};
|
|
150
|
+
})();
|
|
151
|
+
`;
|
|
152
|
+
// ── Filesystem cache ────────────────────────────────────────────────────
|
|
153
|
+
function getWebmcpCacheDir() {
|
|
154
|
+
const dir = join(homedir(), ".nodebench", "webmcp_cache");
|
|
155
|
+
if (!existsSync(dir))
|
|
156
|
+
mkdirSync(dir, { recursive: true });
|
|
157
|
+
return dir;
|
|
158
|
+
}
|
|
159
|
+
function cacheOriginTools(origin, tools) {
|
|
160
|
+
const dir = getWebmcpCacheDir();
|
|
161
|
+
const safe = origin.replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
162
|
+
const path = join(dir, `${safe}.json`);
|
|
163
|
+
writeFileSync(path, JSON.stringify({ origin, tools, cachedAt: Date.now() }, null, 2), "utf-8");
|
|
164
|
+
return path;
|
|
165
|
+
}
|
|
166
|
+
function readCachedOriginTools(origin) {
|
|
167
|
+
const dir = getWebmcpCacheDir();
|
|
168
|
+
const safe = origin.replace(/[^a-zA-Z0-9.-]/g, "_");
|
|
169
|
+
const path = join(dir, `${safe}.json`);
|
|
170
|
+
if (!existsSync(path))
|
|
171
|
+
return null;
|
|
172
|
+
try {
|
|
173
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
174
|
+
return { tools: data.tools ?? [], cachedAt: data.cachedAt ?? 0 };
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ── Tools ───────────────────────────────────────────────────────────────
|
|
181
|
+
export const webmcpTools = [
|
|
182
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
183
|
+
// 1. connect_webmcp_origin
|
|
184
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
185
|
+
{
|
|
186
|
+
name: "connect_webmcp_origin",
|
|
187
|
+
description: "Connect to a WebMCP-enabled website via Playwright. Navigates to the URL, intercepts navigator.modelContext tool registrations, and makes discovered tools available for invocation via call_webmcp_tool. Requires Playwright (npm install playwright && npx playwright install chromium). Use check_webmcp_setup to verify prerequisites.",
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: "object",
|
|
190
|
+
properties: {
|
|
191
|
+
url: {
|
|
192
|
+
type: "string",
|
|
193
|
+
description: "URL of the WebMCP-enabled site (HTTPS required)",
|
|
194
|
+
},
|
|
195
|
+
label: {
|
|
196
|
+
type: "string",
|
|
197
|
+
description: "Human-readable label for this origin (default: derived from hostname)",
|
|
198
|
+
},
|
|
199
|
+
headless: {
|
|
200
|
+
type: "boolean",
|
|
201
|
+
description: "Run browser in headless mode (default: true)",
|
|
202
|
+
},
|
|
203
|
+
waitMs: {
|
|
204
|
+
type: "number",
|
|
205
|
+
description: "Milliseconds to wait after navigation for tool registration (default: 3000)",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
required: ["url"],
|
|
209
|
+
},
|
|
210
|
+
handler: async (args) => {
|
|
211
|
+
const { url, label, headless = true, waitMs = 3000 } = args;
|
|
212
|
+
// Validate URL
|
|
213
|
+
const validation = validateOriginUrl(url);
|
|
214
|
+
if (!validation.valid) {
|
|
215
|
+
return { success: false, error: true, message: validation.reason };
|
|
216
|
+
}
|
|
217
|
+
// Check Playwright
|
|
218
|
+
const pw = getPlaywright();
|
|
219
|
+
if (!pw) {
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
error: true,
|
|
223
|
+
message: "Playwright not installed.",
|
|
224
|
+
setupInstructions: [
|
|
225
|
+
"npm install playwright",
|
|
226
|
+
"npx playwright install chromium",
|
|
227
|
+
],
|
|
228
|
+
_hint: "Run check_webmcp_setup for detailed setup instructions.",
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const origin = new URL(url).origin;
|
|
232
|
+
// Check if already connected
|
|
233
|
+
if (_webmcpConnections.has(origin)) {
|
|
234
|
+
const existing = _webmcpConnections.get(origin);
|
|
235
|
+
return {
|
|
236
|
+
success: true,
|
|
237
|
+
alreadyConnected: true,
|
|
238
|
+
origin,
|
|
239
|
+
label: existing.label,
|
|
240
|
+
toolCount: existing.tools.length,
|
|
241
|
+
tools: existing.tools.map(t => ({ name: t.name, description: t.description.slice(0, 100) })),
|
|
242
|
+
connectedAt: existing.connectedAt,
|
|
243
|
+
_hint: `Already connected to '${origin}' with ${existing.tools.length} tools. Use call_webmcp_tool to invoke them.`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const browser = await pw.chromium.launch({ headless });
|
|
248
|
+
const page = await browser.newPage();
|
|
249
|
+
// Inject discovery script BEFORE navigation
|
|
250
|
+
await page.addInitScript(DISCOVERY_INIT_SCRIPT);
|
|
251
|
+
// Navigate
|
|
252
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
253
|
+
// Wait for tool registrations
|
|
254
|
+
await page.waitForTimeout(waitMs);
|
|
255
|
+
// Extract discovered tools
|
|
256
|
+
const tools = await page.evaluate(() => {
|
|
257
|
+
return globalThis.__webmcp_tools ?? [];
|
|
258
|
+
});
|
|
259
|
+
const resolvedLabel = label ?? new URL(url).hostname;
|
|
260
|
+
const connection = {
|
|
261
|
+
origin,
|
|
262
|
+
label: resolvedLabel,
|
|
263
|
+
browser,
|
|
264
|
+
page,
|
|
265
|
+
tools,
|
|
266
|
+
connectedAt: new Date().toISOString(),
|
|
267
|
+
};
|
|
268
|
+
_webmcpConnections.set(origin, connection);
|
|
269
|
+
// Cache tools for offline lookup
|
|
270
|
+
cacheOriginTools(origin, tools);
|
|
271
|
+
return {
|
|
272
|
+
success: true,
|
|
273
|
+
connected: true,
|
|
274
|
+
origin,
|
|
275
|
+
label: resolvedLabel,
|
|
276
|
+
toolCount: tools.length,
|
|
277
|
+
tools: tools.map(t => ({ name: t.name, description: t.description.slice(0, 100) })),
|
|
278
|
+
_hint: tools.length > 0
|
|
279
|
+
? `Discovered ${tools.length} WebMCP tools. Use call_webmcp_tool({ origin: "${origin}", tool: "<name>", args: {} }) to invoke them.`
|
|
280
|
+
: "No WebMCP tools found — the site may not support WebMCP or tools may load asynchronously. Try increasing waitMs.",
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
return {
|
|
285
|
+
success: false,
|
|
286
|
+
error: true,
|
|
287
|
+
message: `Failed to connect to '${url}': ${e.message}`,
|
|
288
|
+
troubleshooting: [
|
|
289
|
+
"1. Verify the site is accessible and uses HTTPS",
|
|
290
|
+
"2. Check if the site registers WebMCP tools (navigator.modelContext)",
|
|
291
|
+
"3. Try increasing waitMs if tools load asynchronously",
|
|
292
|
+
"4. Ensure Playwright browsers are installed: npx playwright install chromium",
|
|
293
|
+
],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
299
|
+
// 2. list_webmcp_tools
|
|
300
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
301
|
+
{
|
|
302
|
+
name: "list_webmcp_tools",
|
|
303
|
+
description: "List all tools discovered from connected WebMCP origins. Shows tool names, descriptions, and input schemas. Optionally filter by origin.",
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: "object",
|
|
306
|
+
properties: {
|
|
307
|
+
origin: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Filter by origin URL (omit to list all connected origins)",
|
|
310
|
+
},
|
|
311
|
+
verbose: {
|
|
312
|
+
type: "boolean",
|
|
313
|
+
description: "Include full input schemas (default: false)",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
handler: async (args) => {
|
|
318
|
+
const { origin, verbose } = args;
|
|
319
|
+
if (_webmcpConnections.size === 0) {
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
connected: false,
|
|
323
|
+
message: "No WebMCP origins connected.",
|
|
324
|
+
_hint: 'Connect first: connect_webmcp_origin({ url: "https://example.com" })',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
if (origin) {
|
|
328
|
+
const conn = _webmcpConnections.get(origin);
|
|
329
|
+
if (!conn) {
|
|
330
|
+
return {
|
|
331
|
+
success: false,
|
|
332
|
+
error: true,
|
|
333
|
+
message: `Origin '${origin}' not connected.`,
|
|
334
|
+
connectedOrigins: [..._webmcpConnections.keys()],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
success: true,
|
|
339
|
+
origin,
|
|
340
|
+
label: conn.label,
|
|
341
|
+
connectedAt: conn.connectedAt,
|
|
342
|
+
toolCount: conn.tools.length,
|
|
343
|
+
tools: verbose
|
|
344
|
+
? conn.tools.map(t => ({ name: t.name, description: t.description, inputSchema: t.inputSchema, annotations: t.annotations }))
|
|
345
|
+
: conn.tools.map(t => ({ name: t.name, description: t.description.slice(0, 120) })),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
// All connected origins
|
|
349
|
+
const origins = {};
|
|
350
|
+
for (const [key, conn] of _webmcpConnections) {
|
|
351
|
+
origins[key] = {
|
|
352
|
+
label: conn.label,
|
|
353
|
+
connectedAt: conn.connectedAt,
|
|
354
|
+
toolCount: conn.tools.length,
|
|
355
|
+
tools: conn.tools.map(t => t.name),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
success: true,
|
|
360
|
+
connectedOrigins: _webmcpConnections.size,
|
|
361
|
+
origins,
|
|
362
|
+
totalTools: [..._webmcpConnections.values()].reduce((s, c) => s + c.tools.length, 0),
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
},
|
|
366
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
367
|
+
// 3. call_webmcp_tool
|
|
368
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
369
|
+
{
|
|
370
|
+
name: "call_webmcp_tool",
|
|
371
|
+
description: "Invoke a WebMCP tool on a connected origin. The tool is executed in the browser page context via page.evaluate(). Args are validated for suspicious patterns and results are scanned for anomalies. Use list_webmcp_tools to see available tools first.",
|
|
372
|
+
inputSchema: {
|
|
373
|
+
type: "object",
|
|
374
|
+
properties: {
|
|
375
|
+
origin: {
|
|
376
|
+
type: "string",
|
|
377
|
+
description: "Origin URL of the connected site",
|
|
378
|
+
},
|
|
379
|
+
tool: {
|
|
380
|
+
type: "string",
|
|
381
|
+
description: "Tool name to invoke",
|
|
382
|
+
},
|
|
383
|
+
args: {
|
|
384
|
+
type: "object",
|
|
385
|
+
description: "Arguments to pass to the tool (matches the tool's inputSchema)",
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
required: ["origin", "tool"],
|
|
389
|
+
},
|
|
390
|
+
handler: async (toolArgs) => {
|
|
391
|
+
const { origin, tool, args } = toolArgs;
|
|
392
|
+
const conn = _webmcpConnections.get(origin);
|
|
393
|
+
if (!conn) {
|
|
394
|
+
return {
|
|
395
|
+
success: false,
|
|
396
|
+
error: true,
|
|
397
|
+
message: `Origin '${origin}' not connected.`,
|
|
398
|
+
connectedOrigins: [..._webmcpConnections.keys()],
|
|
399
|
+
_hint: `Connect first: connect_webmcp_origin({ url: "${origin}" })`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
// Validate tool exists
|
|
403
|
+
const toolMeta = conn.tools.find(t => t.name === tool);
|
|
404
|
+
if (!toolMeta) {
|
|
405
|
+
const suggestions = conn.tools
|
|
406
|
+
.filter(t => t.name.includes(tool) || tool.includes(t.name))
|
|
407
|
+
.map(t => t.name)
|
|
408
|
+
.slice(0, 5);
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
error: true,
|
|
412
|
+
message: `Tool '${tool}' not found on origin '${origin}'.`,
|
|
413
|
+
availableTools: conn.tools.map(t => t.name),
|
|
414
|
+
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
// Security: scan args
|
|
418
|
+
const argWarning = scanArgs(args ?? {});
|
|
419
|
+
if (argWarning) {
|
|
420
|
+
return {
|
|
421
|
+
success: false,
|
|
422
|
+
error: true,
|
|
423
|
+
message: `Blocked: ${argWarning}`,
|
|
424
|
+
origin,
|
|
425
|
+
tool,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
// Execute tool in page context
|
|
429
|
+
const startMs = Date.now();
|
|
430
|
+
try {
|
|
431
|
+
const result = await conn.page.evaluate(async ({ toolName, toolArgs: tArgs }) => {
|
|
432
|
+
const executor = globalThis.__webmcp_executors?.get(toolName);
|
|
433
|
+
if (!executor)
|
|
434
|
+
return { __webmcp_error: `No executor for tool '${toolName}'` };
|
|
435
|
+
try {
|
|
436
|
+
return await executor(tArgs, {});
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
return { __webmcp_error: e.message ?? String(e) };
|
|
440
|
+
}
|
|
441
|
+
}, { toolName: tool, toolArgs: args ?? {} });
|
|
442
|
+
const latencyMs = Date.now() - startMs;
|
|
443
|
+
// Check for in-page error
|
|
444
|
+
if (result && typeof result === "object" && "__webmcp_error" in result) {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
error: true,
|
|
448
|
+
origin,
|
|
449
|
+
tool,
|
|
450
|
+
message: result.__webmcp_error,
|
|
451
|
+
latencyMs,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
// Security: scan result
|
|
455
|
+
const anomaly = scanResult(result);
|
|
456
|
+
if (anomaly) {
|
|
457
|
+
return {
|
|
458
|
+
success: false,
|
|
459
|
+
error: true,
|
|
460
|
+
message: `Result blocked — ${anomaly}`,
|
|
461
|
+
origin,
|
|
462
|
+
tool,
|
|
463
|
+
latencyMs,
|
|
464
|
+
_hint: "The tool result contained suspicious content and was blocked for safety.",
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return {
|
|
468
|
+
success: true,
|
|
469
|
+
origin,
|
|
470
|
+
tool,
|
|
471
|
+
result,
|
|
472
|
+
latencyMs,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
return {
|
|
477
|
+
success: false,
|
|
478
|
+
error: true,
|
|
479
|
+
origin,
|
|
480
|
+
tool,
|
|
481
|
+
message: `Tool call failed: ${e.message}`,
|
|
482
|
+
latencyMs: Date.now() - startMs,
|
|
483
|
+
_hint: "The browser page may have navigated away or crashed. Try disconnect + reconnect.",
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
489
|
+
// 4. disconnect_webmcp_origin
|
|
490
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
491
|
+
{
|
|
492
|
+
name: "disconnect_webmcp_origin",
|
|
493
|
+
description: "Disconnect from a WebMCP origin and close the browser page. Use this to clean up resources or to reconnect with different settings.",
|
|
494
|
+
inputSchema: {
|
|
495
|
+
type: "object",
|
|
496
|
+
properties: {
|
|
497
|
+
origin: {
|
|
498
|
+
type: "string",
|
|
499
|
+
description: "Origin URL to disconnect (omit to disconnect all)",
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
handler: async (args) => {
|
|
504
|
+
const { origin } = args;
|
|
505
|
+
if (origin) {
|
|
506
|
+
const conn = _webmcpConnections.get(origin);
|
|
507
|
+
if (!conn) {
|
|
508
|
+
return {
|
|
509
|
+
success: true,
|
|
510
|
+
message: `Origin '${origin}' was not connected.`,
|
|
511
|
+
connectedOrigins: [..._webmcpConnections.keys()],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
await conn.page.close();
|
|
516
|
+
}
|
|
517
|
+
catch { /* ignore */ }
|
|
518
|
+
try {
|
|
519
|
+
await conn.browser.close();
|
|
520
|
+
}
|
|
521
|
+
catch { /* ignore */ }
|
|
522
|
+
_webmcpConnections.delete(origin);
|
|
523
|
+
return {
|
|
524
|
+
success: true,
|
|
525
|
+
disconnected: origin,
|
|
526
|
+
remainingConnections: _webmcpConnections.size,
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
// Disconnect all
|
|
530
|
+
const origins = [..._webmcpConnections.keys()];
|
|
531
|
+
for (const [key, conn] of _webmcpConnections) {
|
|
532
|
+
try {
|
|
533
|
+
await conn.page.close();
|
|
534
|
+
}
|
|
535
|
+
catch { /* ignore */ }
|
|
536
|
+
try {
|
|
537
|
+
await conn.browser.close();
|
|
538
|
+
}
|
|
539
|
+
catch { /* ignore */ }
|
|
540
|
+
}
|
|
541
|
+
_webmcpConnections.clear();
|
|
542
|
+
return {
|
|
543
|
+
success: true,
|
|
544
|
+
disconnectedAll: true,
|
|
545
|
+
origins,
|
|
546
|
+
_hint: "All WebMCP connections closed.",
|
|
547
|
+
};
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
551
|
+
// 5. scan_webmcp_origin
|
|
552
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
553
|
+
{
|
|
554
|
+
name: "scan_webmcp_origin",
|
|
555
|
+
description: "One-shot scan: connect to a WebMCP-enabled site, discover tools, cache the manifest, and disconnect. Useful for inventorying WebMCP tools without keeping a browser open. Results are cached to ~/.nodebench/webmcp_cache/.",
|
|
556
|
+
inputSchema: {
|
|
557
|
+
type: "object",
|
|
558
|
+
properties: {
|
|
559
|
+
url: {
|
|
560
|
+
type: "string",
|
|
561
|
+
description: "URL of the WebMCP-enabled site to scan (HTTPS required)",
|
|
562
|
+
},
|
|
563
|
+
waitMs: {
|
|
564
|
+
type: "number",
|
|
565
|
+
description: "Milliseconds to wait for tool registration (default: 3000)",
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
required: ["url"],
|
|
569
|
+
},
|
|
570
|
+
handler: async (args) => {
|
|
571
|
+
const { url, waitMs = 3000 } = args;
|
|
572
|
+
// Validate URL
|
|
573
|
+
const validation = validateOriginUrl(url);
|
|
574
|
+
if (!validation.valid) {
|
|
575
|
+
return { success: false, error: true, message: validation.reason };
|
|
576
|
+
}
|
|
577
|
+
// Check Playwright
|
|
578
|
+
const pw = getPlaywright();
|
|
579
|
+
if (!pw) {
|
|
580
|
+
// Fall back to cached data if available
|
|
581
|
+
const origin = new URL(url).origin;
|
|
582
|
+
const cached = readCachedOriginTools(origin);
|
|
583
|
+
if (cached) {
|
|
584
|
+
return {
|
|
585
|
+
success: true,
|
|
586
|
+
fromCache: true,
|
|
587
|
+
origin,
|
|
588
|
+
toolCount: cached.tools.length,
|
|
589
|
+
tools: cached.tools.map(t => ({ name: t.name, description: t.description.slice(0, 100) })),
|
|
590
|
+
cachedAt: new Date(cached.cachedAt).toISOString(),
|
|
591
|
+
_hint: "Playwright not installed — returning cached results. Install Playwright for live scanning.",
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
return {
|
|
595
|
+
success: false,
|
|
596
|
+
error: true,
|
|
597
|
+
message: "Playwright not installed and no cached data available.",
|
|
598
|
+
setupInstructions: ["npm install playwright", "npx playwright install chromium"],
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const origin = new URL(url).origin;
|
|
602
|
+
try {
|
|
603
|
+
const browser = await pw.chromium.launch({ headless: true });
|
|
604
|
+
const page = await browser.newPage();
|
|
605
|
+
await page.addInitScript(DISCOVERY_INIT_SCRIPT);
|
|
606
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
|
607
|
+
await page.waitForTimeout(waitMs);
|
|
608
|
+
const tools = await page.evaluate(() => {
|
|
609
|
+
return globalThis.__webmcp_tools ?? [];
|
|
610
|
+
});
|
|
611
|
+
await page.close();
|
|
612
|
+
await browser.close();
|
|
613
|
+
// Cache
|
|
614
|
+
const cachePath = cacheOriginTools(origin, tools);
|
|
615
|
+
return {
|
|
616
|
+
success: true,
|
|
617
|
+
origin,
|
|
618
|
+
toolCount: tools.length,
|
|
619
|
+
tools: tools.map(t => ({ name: t.name, description: t.description.slice(0, 100), hasInputSchema: !!t.inputSchema })),
|
|
620
|
+
cachedTo: cachePath,
|
|
621
|
+
_hint: tools.length > 0
|
|
622
|
+
? `Found ${tools.length} tools. Connect with connect_webmcp_origin to invoke them.`
|
|
623
|
+
: "No WebMCP tools found. The site may not support WebMCP.",
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
catch (e) {
|
|
627
|
+
return {
|
|
628
|
+
success: false,
|
|
629
|
+
error: true,
|
|
630
|
+
message: `Scan failed: ${e.message}`,
|
|
631
|
+
origin,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
637
|
+
// 6. check_webmcp_setup
|
|
638
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
639
|
+
{
|
|
640
|
+
name: "check_webmcp_setup",
|
|
641
|
+
description: "Check WebMCP prerequisites: Playwright installation, Chromium browser availability, and any cached origin data. Returns setup instructions if anything is missing.",
|
|
642
|
+
inputSchema: {
|
|
643
|
+
type: "object",
|
|
644
|
+
properties: {},
|
|
645
|
+
},
|
|
646
|
+
handler: async () => {
|
|
647
|
+
const pw = getPlaywright();
|
|
648
|
+
const playwrightInstalled = pw !== null;
|
|
649
|
+
// Check for Chromium
|
|
650
|
+
let chromiumAvailable = false;
|
|
651
|
+
if (playwrightInstalled) {
|
|
652
|
+
try {
|
|
653
|
+
const cacheDir = join(process.env.HOME ?? process.env.USERPROFILE ?? homedir(), ".cache", "ms-playwright");
|
|
654
|
+
chromiumAvailable = existsSync(cacheDir);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
chromiumAvailable = false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
// Check cached origins
|
|
661
|
+
const cacheDir = getWebmcpCacheDir();
|
|
662
|
+
let cachedOrigins = [];
|
|
663
|
+
try {
|
|
664
|
+
cachedOrigins = readdirSync(cacheDir)
|
|
665
|
+
.filter(f => f.endsWith(".json"))
|
|
666
|
+
.map(f => f.replace(".json", "").replace(/_/g, "/"));
|
|
667
|
+
}
|
|
668
|
+
catch { /* ignore */ }
|
|
669
|
+
// Active connections
|
|
670
|
+
const activeConnections = [..._webmcpConnections.entries()].map(([origin, conn]) => ({
|
|
671
|
+
origin,
|
|
672
|
+
label: conn.label,
|
|
673
|
+
toolCount: conn.tools.length,
|
|
674
|
+
connectedAt: conn.connectedAt,
|
|
675
|
+
}));
|
|
676
|
+
const ready = playwrightInstalled && chromiumAvailable;
|
|
677
|
+
return {
|
|
678
|
+
success: true,
|
|
679
|
+
ready,
|
|
680
|
+
playwright: {
|
|
681
|
+
installed: playwrightInstalled,
|
|
682
|
+
chromiumAvailable,
|
|
683
|
+
},
|
|
684
|
+
activeConnections: activeConnections.length > 0 ? activeConnections : undefined,
|
|
685
|
+
cachedOrigins: cachedOrigins.length > 0 ? cachedOrigins : undefined,
|
|
686
|
+
setupInstructions: ready ? undefined : [
|
|
687
|
+
...(playwrightInstalled ? [] : ["1. Install Playwright: npm install playwright"]),
|
|
688
|
+
...(chromiumAvailable ? [] : ["2. Install Chromium: npx playwright install chromium"]),
|
|
689
|
+
],
|
|
690
|
+
quickRef: {
|
|
691
|
+
nextTools: ready
|
|
692
|
+
? ["connect_webmcp_origin", "scan_webmcp_origin"]
|
|
693
|
+
: [],
|
|
694
|
+
methodology: "webmcp_discovery",
|
|
695
|
+
tip: ready
|
|
696
|
+
? "Playwright ready. Connect to a WebMCP-enabled site to discover tools."
|
|
697
|
+
: "Install Playwright first, then connect to WebMCP origins.",
|
|
698
|
+
},
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
];
|
|
703
|
+
//# sourceMappingURL=webmcpTools.js.map
|