@weavetab/mcp 2.5.0-beta.2 → 2.5.0-beta.5
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/AGENTS.md +1 -1
- package/ARCHITECTURE.md +6 -6
- package/LICENSE +216 -631
- package/README.md +153 -1074
- package/SECURITY.md +33 -22
- package/TROUBLESHOOTING.md +2 -2
- package/dist/cdp/bridge.js +2 -1
- package/dist/cdp/connector.d.ts +2 -0
- package/dist/cdp/connector.js +10 -6
- package/dist/cdp/helpers.d.ts +4 -0
- package/dist/cdp/helpers.js +181 -0
- package/dist/cdp/omni_broker.d.ts +1 -1
- package/dist/cdp/omni_broker.js +1 -1
- package/dist/cdp/walker.d.ts +25 -1
- package/dist/cdp/walker.js +304 -7
- package/dist/cli/plugin.d.ts +1 -0
- package/dist/cli/plugin.js +81 -18
- package/dist/cli/updater.d.ts +1 -1
- package/dist/cli/updater.js +27 -7
- package/dist/config/loader.d.ts +9 -1
- package/dist/config/loader.js +42 -3
- package/dist/desktop/discovery.d.ts +44 -0
- package/dist/desktop/discovery.js +364 -0
- package/dist/index.js +137 -21
- package/dist/intelligence/marks.d.ts +36 -0
- package/dist/intelligence/marks.js +30 -0
- package/dist/intelligence/patternCompiler.d.ts +118 -0
- package/dist/intelligence/patternCompiler.js +259 -0
- package/dist/intelligence/sessionMemory.d.ts +2 -0
- package/dist/intelligence/sessionMemory.js +18 -8
- package/dist/overlay/injector.js +2 -2
- package/dist/plugin/interface.d.ts +88 -5
- package/dist/plugin/loader.d.ts +12 -1
- package/dist/plugin/loader.js +280 -70
- package/dist/plugin/pipeline.d.ts +75 -0
- package/dist/plugin/pipeline.js +183 -0
- package/dist/plugin/registry.d.ts +2 -1
- package/dist/plugin/registry.js +24 -14
- package/dist/security/secrets.d.ts +7 -0
- package/dist/security/secrets.js +37 -0
- package/dist/sensors/actionability.js +1 -2
- package/dist/sensors/guard.js +4 -10
- package/dist/sensors/mutations.js +12 -0
- package/dist/sensors/operation_monitor.js +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +453 -178
- package/dist/state/agent.d.ts +1 -0
- package/dist/state/agent.js +3 -0
- package/dist/state/delta.d.ts +12 -4
- package/dist/state/delta.js +67 -14
- package/dist/state/session.d.ts +6 -0
- package/dist/state/session.js +27 -0
- package/dist/tools/browser_detect.d.ts +16 -3
- package/dist/tools/browser_detect.js +52 -7
- package/dist/tools/burst.d.ts +17 -8
- package/dist/tools/burst.js +236 -77
- package/dist/tools/captcha.js +1 -1
- package/dist/tools/checkpoint.d.ts +38 -0
- package/dist/tools/checkpoint.js +271 -0
- package/dist/tools/click.d.ts +1 -0
- package/dist/tools/click.js +74 -70
- package/dist/tools/design_extract.js +1 -1
- package/dist/tools/eval.js +1 -1
- package/dist/tools/fill.d.ts +20 -1
- package/dist/tools/fill.js +199 -40
- package/dist/tools/find.d.ts +16 -0
- package/dist/tools/find.js +67 -44
- package/dist/tools/github/index.d.ts +19 -4
- package/dist/tools/github/index.js +40 -4
- package/dist/tools/highlight.d.ts +6 -2
- package/dist/tools/highlight.js +158 -72
- package/dist/tools/inspect.js +1 -1
- package/dist/tools/key.js +65 -56
- package/dist/tools/navigate.d.ts +1 -1
- package/dist/tools/navigate.js +34 -39
- package/dist/tools/pdf.js +21 -13
- package/dist/tools/pointer.d.ts +18 -3
- package/dist/tools/pointer.js +89 -1
- package/dist/tools/read.d.ts +28 -8
- package/dist/tools/read.js +230 -56
- package/dist/tools/scrape.js +67 -13
- package/dist/tools/screenshot.js +21 -16
- package/dist/tools/scroll.js +6 -11
- package/dist/tools/select.js +8 -2
- package/dist/tools/snapshot.js +3 -34
- package/dist/tools/tabs.js +15 -7
- package/dist/tools/thought.d.ts +2 -0
- package/dist/tools/thought.js +13 -14
- package/dist/tools/type.d.ts +2 -0
- package/dist/tools/type.js +107 -58
- package/dist/tools/type_secret.js +42 -76
- package/dist/tools/upload.js +1 -1
- package/dist/tools/video_compiler.d.ts +1 -1
- package/dist/tools/video_compiler.js +11 -12
- package/dist/version.d.ts +5 -0
- package/dist/version.js +5 -0
- package/docs/DOCUMENTATION.md +37 -41
- package/docs/PLUGINS.md +121 -0
- package/docs/TOOLS.md +54 -0
- package/docs/architecture/blind-injection.md +83 -20
- package/docs/architecture/overview.md +49 -49
- package/docs/architecture/security.md +8 -7
- package/docs/architecture/transport.md +2 -2
- package/docs/cli/cli-commands.md +7 -1
- package/docs/cli/cli-flags.md +15 -7
- package/docs/cli/cli-scripting.md +6 -3
- package/docs/configuration/config-locations.md +3 -4
- package/docs/configuration/env-variables.md +4 -4
- package/docs/configuration/folders.md +55 -54
- package/docs/desktop-automation/electron-apps.md +214 -162
- package/docs/get-started/introduction.md +60 -60
- package/docs/get-started/setup-wizard.md +7 -6
- package/docs/intelligence/site-profiles.md +151 -0
- package/docs/tools/tools.md +631 -293
- package/docs/troubleshooting/trouble-browser.md +1 -1
- package/docs/troubleshooting/trouble-loop.md +31 -31
- package/ext-dist/background.js +10 -62
- package/ext-dist/content.js +333 -920
- package/ext-dist/hud/hud.js +9 -9
- package/ext-dist/manifest.json +2 -6
- package/ext-dist/styles.css +6 -1
- package/icon.png +0 -0
- package/mcp.json +87 -71
- package/package.json +118 -119
- package/server.json +5 -5
- package/ext-dist/sidepanel/sidepanel.js +0 -25
- package/ext-dist/sidepanel.html +0 -547
package/dist/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Weavetab — server.ts
|
|
3
|
-
* MCP server:
|
|
3
|
+
* MCP server: 44 browser automation tools with security, rate limiting, and visual overlay.
|
|
4
4
|
* Weavetab by fy2ne
|
|
5
5
|
*/
|
|
6
6
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
7
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
8
|
import { z } from "zod";
|
|
9
|
+
import { WEAVETAB_VERSION } from "./version.js";
|
|
9
10
|
import { getOrConnect } from "./cdp/connector.js";
|
|
10
11
|
import { checkRateLimit } from "./security/ratelimit.js";
|
|
11
12
|
import { checkDomain, isToolAllowedInMode } from "./security/blacklist.js";
|
|
@@ -44,31 +45,32 @@ import { browserExtractDesign } from "./tools/design_extract.js";
|
|
|
44
45
|
import { browserAutomation } from "./tools/automation.js";
|
|
45
46
|
import { browserRecording } from "./tools/recording.js";
|
|
46
47
|
import { browserPerformance } from "./tools/performance.js";
|
|
47
|
-
import { compileTraceToMacro } from "./tools/macro_compiler.js";
|
|
48
48
|
import { detectBrowsers } from "./tools/browser_detect.js";
|
|
49
49
|
import { browserThought } from "./tools/thought.js";
|
|
50
50
|
import { browserCaptcha } from "./tools/captcha.js";
|
|
51
51
|
import { browserEmulate } from "./tools/emulation.js";
|
|
52
|
+
import { browserCheckpoint } from "./tools/checkpoint.js";
|
|
52
53
|
import { getActiveEngine } from "./cdp/connector.js";
|
|
53
54
|
import { resetLoopCounter } from "./intelligence/trail.js";
|
|
54
|
-
import {
|
|
55
|
+
import { getSiteProfile, learnPattern, recordPatternExecution, recordQuirk, getPatternReliability, formatSiteProfileHint } from "./intelligence/patternCompiler.js";
|
|
56
|
+
import { discoverPlugins } from "./plugin/loader.js";
|
|
57
|
+
import { hookPipeline } from "./plugin/pipeline.js";
|
|
55
58
|
import { agentAction } from "./sensors/pacing.js";
|
|
56
|
-
import { getCachedConfig } from "./config/loader.js";
|
|
59
|
+
import { getCachedConfig, getElectronApps } from "./config/loader.js";
|
|
57
60
|
import { agentState } from "./state/agent.js";
|
|
61
|
+
import { SessionSecretTracker } from "./security/secrets.js";
|
|
58
62
|
import { sessionRegistry } from "./state/session.js";
|
|
59
63
|
// GitHub tools
|
|
60
|
-
import {
|
|
61
|
-
import { githubRead } from "./tools/github/read.js";
|
|
62
|
-
import { githubIssues } from "./tools/github/issues.js";
|
|
63
|
-
import { githubGetPr } from "./tools/github/pr.js";
|
|
64
|
+
import { browserGithub } from "./tools/github/index.js";
|
|
64
65
|
// Office document tools
|
|
65
66
|
import { browserOfficeParse, browserOfficeList } from "./tools/office/index.js";
|
|
67
|
+
const activeLoadedPlugins = new Map();
|
|
66
68
|
const ROLE_TOOL_ALLOW = {
|
|
67
69
|
admin: new Set(["*"]),
|
|
68
70
|
operator: new Set(["*"]),
|
|
69
|
-
viewer: new Set(["browser_map", "browser_find", "browser_scrape", "browser_console", "browser_eval", "browser_inspect", "browser_extract_design", "browser_snapshot", "browser_screenshot", "browser_pdf", "browser_cookies", "browser_tabs", "browser_wait"]),
|
|
71
|
+
viewer: new Set(["browser_map", "browser_find", "browser_scrape", "browser_console", "browser_eval", "browser_inspect", "browser_extract_design", "browser_snapshot", "browser_screenshot", "browser_pdf", "browser_cookies", "browser_tabs", "browser_wait", "browser_pattern_learn", "browser_github"]),
|
|
70
72
|
automation: new Set(["*"]), // Allowed all except interactive tools (enforced in isToolAllowedForRole)
|
|
71
|
-
system: new Set(["browser_thoughts", "
|
|
73
|
+
system: new Set(["browser_thoughts", "browser_detect", "browser_performance", "browser_checkpoint", "browser_pattern_learn"]),
|
|
72
74
|
};
|
|
73
75
|
function isToolAllowedForRole(role, toolName) {
|
|
74
76
|
if (role === "automation" && toolName === "browser_thoughts") {
|
|
@@ -81,7 +83,7 @@ function isToolAllowedForRole(role, toolName) {
|
|
|
81
83
|
return { allowed: true };
|
|
82
84
|
if (allowed.has(toolName))
|
|
83
85
|
return { allowed: true };
|
|
84
|
-
return { allowed: false, reason: `Role '${role}' does not have permission to use '${toolName}'. ${role === "viewer" ? "Viewer role only permits read-only tools (map/find/scrape/console/eval/inspect/snapshot/pdf/cookies/
|
|
86
|
+
return { allowed: false, reason: `Role '${role}' does not have permission to use '${toolName}'. ${role === "viewer" ? "Viewer role only permits read-only tools (map/find/scrape/console/eval/inspect/snapshot/pdf/cookies/browser_github)." : ""}` };
|
|
85
87
|
}
|
|
86
88
|
/** Push a rich log card to every active browser session's overlay. Only shown when devMode is on. */
|
|
87
89
|
async function pushRichLog(agentId, toolName, title, details, status, devMode = false) {
|
|
@@ -104,6 +106,68 @@ function withTimeoutGuard(fn, timeoutMs, label) {
|
|
|
104
106
|
});
|
|
105
107
|
return Promise.race([fn(), timeoutPromise]).finally(() => clearTimeout(timer));
|
|
106
108
|
}
|
|
109
|
+
function inferTargetApp(requestedApp, url, tabId) {
|
|
110
|
+
if (requestedApp) {
|
|
111
|
+
const lower = requestedApp.toLowerCase().trim();
|
|
112
|
+
if (lower === "browser" || lower === "chrome" || lower === "chromium") {
|
|
113
|
+
sessionRegistry.setLastActiveApp(null);
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
sessionRegistry.setLastActiveApp(lower);
|
|
117
|
+
return lower;
|
|
118
|
+
}
|
|
119
|
+
// 1. Infer from tabId if provided
|
|
120
|
+
if (tabId) {
|
|
121
|
+
const registered = sessionRegistry.findClientByTargetId(tabId);
|
|
122
|
+
if (registered && registered.isElectron && registered.appName) {
|
|
123
|
+
sessionRegistry.setLastActiveApp(registered.appName);
|
|
124
|
+
return registered.appName;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// 2. Infer from URL domain if provided
|
|
128
|
+
if (url) {
|
|
129
|
+
const config = getCachedConfig();
|
|
130
|
+
const apps = getElectronApps(config);
|
|
131
|
+
const u = url.toLowerCase();
|
|
132
|
+
for (const appKey of Object.keys(apps)) {
|
|
133
|
+
if (appKey === "discord" && (u.includes("discord.com") || u.startsWith("discord:"))) {
|
|
134
|
+
logAction("target_router", "auto_route", `Auto-routing URL to Electron app "${appKey}"`);
|
|
135
|
+
sessionRegistry.setLastActiveApp(appKey);
|
|
136
|
+
return appKey;
|
|
137
|
+
}
|
|
138
|
+
if (appKey === "slack" && (u.includes("slack.com") || u.startsWith("slack:"))) {
|
|
139
|
+
logAction("target_router", "auto_route", `Auto-routing URL to Electron app "${appKey}"`);
|
|
140
|
+
sessionRegistry.setLastActiveApp(appKey);
|
|
141
|
+
return appKey;
|
|
142
|
+
}
|
|
143
|
+
if (appKey === "linear" && (u.includes("linear.app") || u.startsWith("linear:"))) {
|
|
144
|
+
logAction("target_router", "auto_route", `Auto-routing URL to Electron app "${appKey}"`);
|
|
145
|
+
sessionRegistry.setLastActiveApp(appKey);
|
|
146
|
+
return appKey;
|
|
147
|
+
}
|
|
148
|
+
if (appKey === "notion" && (u.includes("notion.so") || u.startsWith("notion:"))) {
|
|
149
|
+
logAction("target_router", "auto_route", `Auto-routing URL to Electron app "${appKey}"`);
|
|
150
|
+
sessionRegistry.setLastActiveApp(appKey);
|
|
151
|
+
return appKey;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// 3. Fall back to sticky active session if no web browser is running
|
|
156
|
+
const hasActiveBrowser = sessionRegistry.isBrowserSessionActive();
|
|
157
|
+
if (!hasActiveBrowser) {
|
|
158
|
+
const lastApp = sessionRegistry.getLastActiveApp();
|
|
159
|
+
if (lastApp) {
|
|
160
|
+
return lastApp;
|
|
161
|
+
}
|
|
162
|
+
const allElectrons = sessionRegistry.getActiveElectronClients();
|
|
163
|
+
if (allElectrons.length === 1 && allElectrons[0]?.appName) {
|
|
164
|
+
const soleApp = allElectrons[0].appName;
|
|
165
|
+
sessionRegistry.setLastActiveApp(soleApp);
|
|
166
|
+
return soleApp;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
107
171
|
async function withSecurity(config, toolName, handler, isOfficial = false, targetApp) {
|
|
108
172
|
const liveConfig = getCachedConfig();
|
|
109
173
|
const rl = checkRateLimit(liveConfig);
|
|
@@ -117,7 +181,8 @@ async function withSecurity(config, toolName, handler, isOfficial = false, targe
|
|
|
117
181
|
throw new Error(`[!] ${roleToolCheck.reason}`);
|
|
118
182
|
}
|
|
119
183
|
}
|
|
120
|
-
const
|
|
184
|
+
const resolvedTargetApp = inferTargetApp(targetApp);
|
|
185
|
+
const { session } = await withTimeoutGuard(() => getOrConnect(liveConfig, resolvedTargetApp), 15000, `CDP Connection (${toolName}${resolvedTargetApp ? `:${resolvedTargetApp}` : ''})`);
|
|
121
186
|
agentState.setSession(session);
|
|
122
187
|
global.__wt_live_config = liveConfig;
|
|
123
188
|
try {
|
|
@@ -265,6 +330,13 @@ async function withSecurity(config, toolName, handler, isOfficial = false, targe
|
|
|
265
330
|
{ type: "image", data: base64Data, mimeType },
|
|
266
331
|
];
|
|
267
332
|
}
|
|
333
|
+
else if (resultAny && typeof resultAny === 'object' && Array.isArray(resultAny.content)) {
|
|
334
|
+
// Passthrough if already conforming McpToolResult (prevents double JSON wrapping)
|
|
335
|
+
content = resultAny.content;
|
|
336
|
+
}
|
|
337
|
+
else if (typeof result === 'string') {
|
|
338
|
+
content = [{ type: "text", text: result }];
|
|
339
|
+
}
|
|
268
340
|
else {
|
|
269
341
|
content = [{ type: "text", text: JSON.stringify(result) }];
|
|
270
342
|
}
|
|
@@ -340,13 +412,39 @@ async function withSecurity(config, toolName, handler, isOfficial = false, targe
|
|
|
340
412
|
}
|
|
341
413
|
}
|
|
342
414
|
function wrapError(err) {
|
|
343
|
-
|
|
415
|
+
let message = err instanceof Error ? err.message : String(err);
|
|
416
|
+
message = SessionSecretTracker.redactAll(message);
|
|
344
417
|
logAction("ERROR", "server", message);
|
|
345
418
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
346
419
|
}
|
|
347
420
|
export async function startServer(config, transportMode = "stdio", wsPort = 3000) {
|
|
348
|
-
const server = new McpServer({ name: "Weavetab MCP", version:
|
|
349
|
-
//
|
|
421
|
+
const server = new McpServer({ name: "Weavetab MCP", version: WEAVETAB_VERSION });
|
|
422
|
+
// Intercept server.tool to inject global plugin middleware hooks (onBeforeAction, onAfterAction)
|
|
423
|
+
const rawTool = server.tool.bind(server);
|
|
424
|
+
server.tool = function (name, ...rest) {
|
|
425
|
+
const rawHandler = rest[rest.length - 1];
|
|
426
|
+
if (typeof rawHandler === "function") {
|
|
427
|
+
rest[rest.length - 1] = async (args, extra) => {
|
|
428
|
+
try {
|
|
429
|
+
const session = agentState.getSession() ?? sessionRegistry.getActive(config.agentId)?.session;
|
|
430
|
+
const dispatchedArgs = await hookPipeline.dispatchBeforeToolCall(name, args || {}, session);
|
|
431
|
+
const result = await rawHandler(dispatchedArgs, extra);
|
|
432
|
+
const finalResult = await hookPipeline.dispatchAfterToolCall(name, dispatchedArgs, result, session);
|
|
433
|
+
return finalResult;
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
if (err?.isBlocked || err?.__blocked) {
|
|
437
|
+
return {
|
|
438
|
+
isError: true,
|
|
439
|
+
content: [{ type: "text", text: `[!] Action '${name}' blocked by plugin: ${err.message}` }],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
throw err;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return rawTool(name, ...rest);
|
|
447
|
+
};
|
|
350
448
|
agentState.setIdleTimeout(config.idleTimeout ?? 60000);
|
|
351
449
|
agentState.setCallbacks({
|
|
352
450
|
onStateChange: async (session, state, word) => {
|
|
@@ -360,21 +458,22 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
360
458
|
}
|
|
361
459
|
},
|
|
362
460
|
onUpdateTabTitle: async (_session, _state) => {
|
|
363
|
-
//
|
|
461
|
+
// handled via extension background & content scripts
|
|
364
462
|
},
|
|
365
463
|
});
|
|
366
464
|
// ── 1. browser_map ─────────────────────────────────────────────────────────
|
|
367
|
-
server.tool("browser_map", "Snapshot interactive DOM with volatile ref IDs that invalidate after DOM mutations. For complex SPAs (e.g. GitHub dashboards), use `scope` or `query` to reduce tokens. For forms/pagination/sequential loops, ALWAYS use `delta: true` to save 97% tokens.", {
|
|
465
|
+
server.tool("browser_map", "Snapshot interactive DOM with volatile ref IDs that invalidate after DOM mutations. For complex SPAs (e.g. GitHub dashboards), use `scope` or `query` to reduce tokens. For forms/pagination/sequential loops, ALWAYS use `delta: true` to save 97% tokens. For canvas/WebGL/Figma pages where the DOM has < 5 interactive elements, use `visual: \"auto\"` to get a numbered screenshot — then use browser_click({ mark: N }) to interact.", {
|
|
368
466
|
full: z.boolean().optional().describe("Set to true to return a full markdown table (heavier context) instead of the lite list."),
|
|
369
|
-
scope: z.enum(["navigation", "main_content", "sidebar", "form", "modal", "utility"]).optional().describe("Restrict parsing to a semantic page area to save tokens.
|
|
370
|
-
query: z.string().optional().describe("Filter returned elements by label or role substring.
|
|
467
|
+
scope: z.enum(["navigation", "main_content", "sidebar", "form", "modal", "utility"]).optional().describe("Restrict parsing to a semantic page area to save tokens. Useful on dense dashboards."),
|
|
468
|
+
query: z.string().optional().describe("Filter returned elements by label or role substring. Useful on dense dashboards."),
|
|
371
469
|
delta: z.boolean().optional().describe("Diff mode: return ONLY elements that changed since the last browser_map call (added, removed, or modified). Diffs across time — distinct from prune. Cuts prompt tokens by up to 97% on consecutive calls."),
|
|
372
470
|
ax_tree: z.boolean().optional().describe("Bypass the extension entirely and read raw Chrome Accessibility Tree. Use this if Shadow DOMs hide elements."),
|
|
373
471
|
tab_id: z.string().optional().describe("Read a specific background tab instead of the active one."),
|
|
374
472
|
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
375
473
|
show_overlays: z.boolean().optional().describe("Default false. Debug only — draws colored element borders via extension (ugly). The blue edge glow is the standard visual indicator, shown automatically during reading states. Only enable this for debugging element boundaries."),
|
|
376
|
-
force: z.boolean().optional().describe("Force a full DOM map rebuild even if loop detection says nothing changed. Bypasses the map skip guard."),
|
|
474
|
+
force: z.boolean().optional().describe("Force a full DOM map rebuild even if loop detection says nothing changed. Bypasses the map skip guard and resets loop strikes."),
|
|
377
475
|
prune: z.boolean().optional().describe("Semantic DOM pruning: compress the current snapshot before returning. Drops hidden elements with no interactive descendants and not referenced by any toggle trigger (aria-controls etc.). Replaces SVG icons with [SVG Icon: label] tokens. Distinct from delta:true — delta diffs across time, prune compresses this snapshot once. Combine both for maximum token efficiency."),
|
|
476
|
+
visual: z.union([z.boolean(), z.literal("auto")]).optional().describe("Set-of-Marks visual grounding. true = always inject numbered badges + return annotated screenshot. \"auto\" = only activate when DOM interactive count < 5 (canvas/WebGL/Figma pages). After receiving marks, use browser_click({ mark: N }) to interact with numbered elements."),
|
|
378
477
|
}, async (args) => {
|
|
379
478
|
try {
|
|
380
479
|
return await withSecurity(config, "browser_map", async (session, liveConfig) => {
|
|
@@ -390,6 +489,9 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
390
489
|
targetSession = client.session;
|
|
391
490
|
targetId = args.tab_id;
|
|
392
491
|
}
|
|
492
|
+
if (args.force) {
|
|
493
|
+
resetLoopCounter("map_force");
|
|
494
|
+
}
|
|
393
495
|
// Structural block — map after no-change click/type is a pure wasted round-trip
|
|
394
496
|
// Skip the skip guard if the user explicitly requests force or delta diff
|
|
395
497
|
if (!args.force && !args.delta) {
|
|
@@ -409,6 +511,17 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
409
511
|
}
|
|
410
512
|
}
|
|
411
513
|
const result = await agentAction(targetSession, 'reading', () => browserMap(targetSession, args, targetId, liveConfig), 'browser_map');
|
|
514
|
+
// Handle visual grounding result (text + annotated image)
|
|
515
|
+
if (result && typeof result === 'object' && 'text' in result) {
|
|
516
|
+
const vr = result;
|
|
517
|
+
const content = [
|
|
518
|
+
{ type: "text", text: vr.text },
|
|
519
|
+
];
|
|
520
|
+
if (vr.imageBase64) {
|
|
521
|
+
content.push({ type: "image", data: vr.imageBase64, mimeType: "image/jpeg" });
|
|
522
|
+
}
|
|
523
|
+
return { content };
|
|
524
|
+
}
|
|
412
525
|
return { content: [{ type: "text", text: result }] };
|
|
413
526
|
}, false, args.app);
|
|
414
527
|
}
|
|
@@ -427,33 +540,37 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
427
540
|
if (!args.url && !args.app) {
|
|
428
541
|
throw new Error("Either 'url' or 'app' must be provided to browser_navigate.");
|
|
429
542
|
}
|
|
543
|
+
const resolvedApp = inferTargetApp(args.app, args.url, args.tab_id);
|
|
430
544
|
return await withSecurity(config, "browser_navigate", async (session, liveConfig) => {
|
|
431
545
|
let targetSession = session;
|
|
432
546
|
let targetId = "default";
|
|
433
547
|
if (args.tab_id) {
|
|
434
548
|
const { connectToTab } = await import("./cdp/connector.js");
|
|
435
|
-
const client = await connectToTab(args.tab_id, undefined, undefined, undefined,
|
|
549
|
+
const client = await connectToTab(args.tab_id, undefined, undefined, undefined, resolvedApp);
|
|
436
550
|
targetSession = client.session;
|
|
437
551
|
targetId = args.tab_id;
|
|
438
552
|
}
|
|
439
553
|
if (args.url) {
|
|
440
|
-
|
|
554
|
+
const navResult = await agentAction(targetSession, 'navigating', () => weaveNavigate(targetSession, args.url, liveConfig, targetId, args.force), 'browser_navigate');
|
|
555
|
+
await hookPipeline.dispatchPageNavigate(args.url, targetSession);
|
|
556
|
+
return navResult;
|
|
441
557
|
}
|
|
442
|
-
return { success: true, app:
|
|
443
|
-
}, false,
|
|
558
|
+
return { success: true, app: resolvedApp, message: `Connected to Electron app "${resolvedApp}".` };
|
|
559
|
+
}, false, resolvedApp);
|
|
444
560
|
}
|
|
445
561
|
catch (e) {
|
|
446
562
|
return wrapError(e);
|
|
447
563
|
}
|
|
448
564
|
});
|
|
449
565
|
// ── 3. browser_click ───────────────────────────────────────────────────────
|
|
450
|
-
server.tool("browser_click", "Click a DOM element. Use browser_canvas for charts/canvas. If stuck_warning=true, stop and use browser_map.", {
|
|
566
|
+
server.tool("browser_click", "Click a DOM element. Use browser_canvas for charts/canvas. If stuck_warning=true, stop and use browser_map. Pass mark: N to click a numbered element from browser_map({ visual: true/\"auto\" }) without any DOM re-scan.", {
|
|
451
567
|
id: z.string().optional().describe("Ref ID from browser_map (e.g. 'w:42'). Primary targeting method. DO NOT SPAM. If stuck_warning is returned, stop retrying and call browser_map first."),
|
|
452
568
|
ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
|
|
453
569
|
selector: z.string().optional().describe("CSS selector of the element to click."),
|
|
454
570
|
target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
|
|
455
571
|
label: z.string().optional().describe("Visible text or aria-label to click."),
|
|
456
572
|
intent: z.string().optional().describe("Semantic description of the action (e.g. 'submit login form'). WeaveTab checks learned memory first — if a high-confidence selector is found it clicks immediately without DOM resolution. Falls through to normal resolution on a miss; does not error."),
|
|
573
|
+
mark: z.number().int().positive().optional().describe("Mark number from browser_map({ visual: true/\"auto\" }). Clicks the center of the numbered badge seen in the annotated screenshot. Zero DOM walk — instant coordinate dispatch. Use when the page is canvas/WebGL and DOM refs are unavailable."),
|
|
457
574
|
backendNodeId: z.number().optional().describe("Raw CDP backendNodeId for direct hardware targeting."),
|
|
458
575
|
x: z.number().optional().describe("Raw X coordinate. Use for precise positional clicks when element ID is not available."),
|
|
459
576
|
y: z.number().optional().describe("Raw Y coordinate. Use for precise positional clicks when element ID is not available."),
|
|
@@ -484,18 +601,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
484
601
|
}
|
|
485
602
|
});
|
|
486
603
|
// ── 4. browser_type ────────────────────────────────────────────────────────
|
|
487
|
-
server.tool("browser_type", "Type text into
|
|
488
|
-
id: z.string().optional().describe("Ref ID from browser_map.
|
|
604
|
+
server.tool("browser_type", "Type text into an input field or currently focused element. If id/selector/label is omitted, automatically types directly into document.activeElement without needing a redundant click or browser_map. If target is already focused, skips click to prevent unfocus bugs.", {
|
|
605
|
+
id: z.string().optional().describe("Ref ID from browser_map (e.g. 'w:42'). If omitted, types directly into the currently focused element or single page input."),
|
|
489
606
|
ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
|
|
490
|
-
selector: z.string().optional().describe("CSS selector of the input field.
|
|
607
|
+
selector: z.string().optional().describe("CSS selector of the input field."),
|
|
491
608
|
target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
|
|
492
609
|
label: z.string().optional().describe("Field label or placeholder text. Used for fuzzy matching."),
|
|
493
610
|
intent: z.string().optional().describe("Semantic description of the field (e.g. 'search box', 'username field'). Checked against learned memory first."),
|
|
494
|
-
x: z.number().optional().describe("Raw X coordinate to focus before typing.
|
|
495
|
-
y: z.number().optional().describe("Raw Y coordinate to focus before typing.
|
|
496
|
-
text: z.string().describe("Text to type
|
|
497
|
-
clearFirst: z.boolean().optional().describe("Wipe existing field content before typing.
|
|
498
|
-
paste: z.boolean().optional().describe("Instant paste mode via clipboard. Auto-enabled for text longer than 8 chars.
|
|
611
|
+
x: z.number().optional().describe("Raw X coordinate to focus before typing."),
|
|
612
|
+
y: z.number().optional().describe("Raw Y coordinate to focus before typing."),
|
|
613
|
+
text: z.string().describe("Text to type into the field."),
|
|
614
|
+
clearFirst: z.boolean().optional().describe("Wipe existing field content before typing."),
|
|
615
|
+
paste: z.boolean().optional().describe("Instant paste mode via clipboard. Auto-enabled for text longer than 8 chars."),
|
|
499
616
|
include_map: z.boolean().optional().describe("Set true to include a full updated page_map in the response. Default false."),
|
|
500
617
|
tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
|
|
501
618
|
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
@@ -619,12 +736,17 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
619
736
|
}
|
|
620
737
|
});
|
|
621
738
|
// ── 8. browser_burst ───────────────────────────────────────────────────────
|
|
622
|
-
server.tool("browser_burst", "
|
|
739
|
+
server.tool("browser_burst", "HIGH-SPEED MULTI-ACTION PIPELINE: Executes 2 or more sequential browser actions in a single round-trip without waiting for LLM turns between steps. PREFER THIS over individual single-action calls (e.g. typing + pressing Enter, filling multiple form fields, clicking tabs/buttons then waiting, searching, navigating + reading) to drastically slash latency and token burn. Supports native primitives (browser_click, browser_fill, browser_type, browser_type_secret, browser_navigate, browser_key, browser_scroll, browser_wait, browser_map, etc.) plus ergonomic macro steps (type_and_send, click_and_wait, navigate_and_read, scroll_and_read, fill_form). If compile_from_history: true is passed, automatically compiles recent successful session trail actions into a burst macro.", {
|
|
623
740
|
steps: z.array(z.object({
|
|
624
|
-
tool: z.
|
|
625
|
-
args: z.record(z.unknown()).describe("
|
|
626
|
-
})).describe("Ordered array of tool calls to execute serially.
|
|
627
|
-
|
|
741
|
+
tool: z.string().describe("Tool name or macro step (e.g. browser_click, browser_fill, browser_type, type_and_send, click_and_wait, navigate_and_read, scroll_and_read, fill_form)."),
|
|
742
|
+
args: z.record(z.unknown()).optional().describe("Tool arguments. Alternatively, arguments may be specified flat on the step object."),
|
|
743
|
+
}).passthrough()).optional().describe("Ordered array of tool calls or macros to execute serially. Either 'steps' or 'actions' may be passed."),
|
|
744
|
+
actions: z.array(z.any()).optional().describe("Alias for 'steps'."),
|
|
745
|
+
compile_from_history: z.boolean().optional().describe("Compile recent successful session trail actions into a macro. If steps are omitted, executes the compiled macro directly."),
|
|
746
|
+
history_limit: z.number().optional().describe("Number of recent session trail entries to consider when compile_from_history is true (default: 20)."),
|
|
747
|
+
dry_run: z.boolean().optional().describe("If true with compile_from_history, returns the compiled steps without executing them."),
|
|
748
|
+
stop_on_error: z.boolean().optional().describe("Halt execution immediately if a step fails to prevent cascading failures (default: true)."),
|
|
749
|
+
batch: z.boolean().optional().describe("If true, defers action map generation until all steps are complete to maximize throughput."),
|
|
628
750
|
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
629
751
|
}, async (args) => {
|
|
630
752
|
try {
|
|
@@ -638,7 +760,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
638
760
|
}
|
|
639
761
|
});
|
|
640
762
|
// ── 9. browser_tabs ───────────────────────────────────────────────────────
|
|
641
|
-
server.tool("browser_tabs", "Manage browser and Electron app tabs/windows (open, switch, close, list).", {
|
|
763
|
+
server.tool("browser_tabs", "Manage browser and Electron app tabs/windows (open, switch, close, list). Specify 'app' when targeting desktop apps (e.g. app: 'discord') to avoid opening web browser.", {
|
|
642
764
|
action: z.enum(["list", "open", "close", "switch", "list_windows", "focus_window", "consolidate", "heal"]).describe("Action to perform. 'consolidate' deduplicates tabs. 'heal' recovers crashed tabs."),
|
|
643
765
|
url: z.string().optional().describe("URL to open (required for 'open' action)."),
|
|
644
766
|
app: z.string().optional().describe("Optional Electron app name configured in config.json under 'electron'."),
|
|
@@ -649,42 +771,43 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
649
771
|
reuse: z.boolean().optional().describe("Default true. When opening a URL already loaded in another tab, focus that tab instead of opening a duplicate."),
|
|
650
772
|
}, async ({ action, url, app, tabId, tabIds, force, windowId, reuse }) => {
|
|
651
773
|
try {
|
|
774
|
+
const resolvedApp = inferTargetApp(app, url, tabId || (tabIds && tabIds.length > 0 ? tabIds[0] : undefined));
|
|
652
775
|
return await withSecurity(config, "browser_tabs", async (_, liveConfig) => {
|
|
653
776
|
if (action === "list")
|
|
654
|
-
return await WeavetabList(liveConfig,
|
|
777
|
+
return await WeavetabList(liveConfig, resolvedApp);
|
|
655
778
|
if (action === "open") {
|
|
656
779
|
if (!url)
|
|
657
780
|
throw new Error("url is required for action: open");
|
|
658
|
-
return await WeavetabOpen(url, liveConfig, reuse !== false,
|
|
781
|
+
return await WeavetabOpen(url, liveConfig, reuse !== false, resolvedApp);
|
|
659
782
|
}
|
|
660
783
|
if (action === "switch") {
|
|
661
784
|
if (!tabId)
|
|
662
785
|
throw new Error("tabId is required for action: switch");
|
|
663
|
-
return await WeavetabSwitch(tabId, liveConfig,
|
|
786
|
+
return await WeavetabSwitch(tabId, liveConfig, resolvedApp);
|
|
664
787
|
}
|
|
665
788
|
if (action === "close") {
|
|
666
789
|
if (tabIds && tabIds.length > 0)
|
|
667
|
-
return await WeavetabCloseBatch(tabIds, liveConfig, force,
|
|
790
|
+
return await WeavetabCloseBatch(tabIds, liveConfig, force, resolvedApp);
|
|
668
791
|
if (!tabId)
|
|
669
792
|
throw new Error("tabId or tabIds is required for action: close");
|
|
670
|
-
return await WeavetabClose(tabId, liveConfig, force,
|
|
793
|
+
return await WeavetabClose(tabId, liveConfig, force, resolvedApp);
|
|
671
794
|
}
|
|
672
795
|
if (action === "list_windows") {
|
|
673
|
-
return await weaveWindowList(liveConfig,
|
|
796
|
+
return await weaveWindowList(liveConfig, resolvedApp);
|
|
674
797
|
}
|
|
675
798
|
if (action === "focus_window") {
|
|
676
799
|
if (windowId === undefined)
|
|
677
800
|
throw new Error("windowId is required for action: focus_window");
|
|
678
|
-
return await weaveFocusWindow(windowId, liveConfig,
|
|
801
|
+
return await weaveFocusWindow(windowId, liveConfig, resolvedApp);
|
|
679
802
|
}
|
|
680
803
|
if (action === "consolidate") {
|
|
681
|
-
return await WeavetabConsolidate(liveConfig,
|
|
804
|
+
return await WeavetabConsolidate(liveConfig, resolvedApp);
|
|
682
805
|
}
|
|
683
806
|
if (action === "heal") {
|
|
684
|
-
return await WeavetabHeal(liveConfig,
|
|
807
|
+
return await WeavetabHeal(liveConfig, resolvedApp);
|
|
685
808
|
}
|
|
686
809
|
throw new Error("Invalid action");
|
|
687
|
-
}, false,
|
|
810
|
+
}, false, resolvedApp);
|
|
688
811
|
}
|
|
689
812
|
catch (e) {
|
|
690
813
|
return wrapError(e);
|
|
@@ -728,9 +851,11 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
728
851
|
}
|
|
729
852
|
});
|
|
730
853
|
// ── 12. browser_thoughts ─────────────────────────────────────────────────────────
|
|
731
|
-
server.tool("browser_thoughts", "Agent-to-user communication. Use 'question' mode to ask a free-text question. Use 'feedback' mode to provide status updates or
|
|
854
|
+
server.tool("browser_thoughts", "Agent-to-user communication. Renders in the middle down of the page. Use 'question' mode to ask a free-text question. Use 'feedback' mode to provide status updates, analysis, or decisions. Supports element anchoring (e.g. 'w:15') and presentation styles ('observation', 'decision', 'warning', 'question').", {
|
|
732
855
|
action: z.enum(["question", "feedback"]).describe("Action to perform."),
|
|
733
856
|
message: z.string().describe("The question to ask, or the feedback/analysis to log."),
|
|
857
|
+
anchor: z.string().optional().describe("Optional element Ref ID (e.g. 'w:15') or CSS selector to visually anchor this thought to on the page."),
|
|
858
|
+
style: z.enum(["observation", "decision", "warning", "question"]).optional().describe("Thought presentation style. Defaults to 'observation' for feedback and 'question' for question."),
|
|
734
859
|
input: z.enum(["sentence", "paragraph"]).optional().describe("Input field type for question mode (default: sentence)."),
|
|
735
860
|
timeout: z.number().optional().describe("Timeout in seconds for question mode before auto-cancel (default: 120)."),
|
|
736
861
|
app: z.string().optional().describe("Optional Electron app name. (Note: thought HUD overlay is browser-only)."),
|
|
@@ -755,10 +880,22 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
755
880
|
tool: "browser_thoughts",
|
|
756
881
|
result: `[AGENT FEEDBACK]: ${args.message}`
|
|
757
882
|
});
|
|
758
|
-
await browserThought(session, {
|
|
883
|
+
await browserThought(session, {
|
|
884
|
+
action: "feedback",
|
|
885
|
+
message: args.message,
|
|
886
|
+
anchor: args.anchor,
|
|
887
|
+
style: args.style,
|
|
888
|
+
});
|
|
759
889
|
return { content: [{ type: "text", text: "Feedback appended to mission log and HUD." }] };
|
|
760
890
|
}
|
|
761
|
-
const result = await browserThought(session, {
|
|
891
|
+
const result = await browserThought(session, {
|
|
892
|
+
action: "question",
|
|
893
|
+
message: args.message,
|
|
894
|
+
anchor: args.anchor,
|
|
895
|
+
style: args.style,
|
|
896
|
+
input: args.input,
|
|
897
|
+
timeout: args.timeout,
|
|
898
|
+
});
|
|
762
899
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
763
900
|
});
|
|
764
901
|
}
|
|
@@ -767,12 +904,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
767
904
|
}
|
|
768
905
|
});
|
|
769
906
|
// ── 13. browser_pointer ───────────────────────────────────────────────────────
|
|
770
|
-
server.tool("browser_pointer", "Advanced pointer control (hover,
|
|
771
|
-
action: z.enum(["hover", "click", "double_click", "drag_and_drop"]).describe("Action to perform."),
|
|
907
|
+
server.tool("browser_pointer", "Advanced pointer control (hover, click, double_click, drag_and_drop, hover_chain). Use hover_chain for Photoshop/Photopea, Figma, and multi-level flyout submenus.", {
|
|
908
|
+
action: z.enum(["hover", "click", "double_click", "drag_and_drop", "hover_chain"]).optional().describe("Action to perform (default: click, or hover_chain if hover_chain array is provided)."),
|
|
772
909
|
ref: z.string().optional().describe("Ref ID of the target element (e.g. w:42)."),
|
|
773
910
|
id: z.string().optional().describe("Alternative element ID / Ref ID from browser_map."),
|
|
774
911
|
selector: z.string().optional().describe("CSS Selector of the target element."),
|
|
775
912
|
target: z.string().optional().describe("Alternative target element identifier or query."),
|
|
913
|
+
label: z.string().optional().describe("Target element label or text."),
|
|
776
914
|
x: z.number().optional().describe("Raw X coordinate (fallback)."),
|
|
777
915
|
y: z.number().optional().describe("Raw Y coordinate (fallback)."),
|
|
778
916
|
end_ref: z.string().optional().describe("Destination Ref ID (only for drag_and_drop)."),
|
|
@@ -783,6 +921,9 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
783
921
|
end_y: z.number().optional().describe("Destination Y coordinate (only for drag_and_drop)."),
|
|
784
922
|
waitForChild: z.string().optional().describe("CSS Selector of a child element to wait for (only for hover)."),
|
|
785
923
|
holdMs: z.number().optional().describe("Milliseconds to hold the pointer down (only for hover)."),
|
|
924
|
+
hover_chain: z.array(z.string()).optional().describe("Array of element ref IDs, selectors, or labels to hover sequentially and click the last one (e.g. ['File', 'Export As', 'PNG']). Perfect for Photoshop/Photopea, Figma, Canva, and multi-level flyout submenus."),
|
|
925
|
+
chain: z.array(z.any()).optional().describe("Advanced chain step array of { action, ref, label, selector, x, y, holdMs }."),
|
|
926
|
+
clickFinal: z.boolean().optional().describe("Default true. Whether to click the final element in the hover chain."),
|
|
786
927
|
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
787
928
|
}, async (args) => {
|
|
788
929
|
try {
|
|
@@ -824,11 +965,19 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
824
965
|
}
|
|
825
966
|
});
|
|
826
967
|
// ── 14b. browser_fill ──────────────────────────────────────────────────────
|
|
827
|
-
server.tool("browser_fill", "Smart multi-field form-filling.
|
|
968
|
+
server.tool("browser_fill", "Smart multi-field form-filling and input inspector. Call with scan:true (or empty) to get a lightweight list of all editable inputs on the page without dumping the full DOM. Fill multiple fields in a single turn.", {
|
|
828
969
|
fields: z.array(z.object({
|
|
829
|
-
ref: z.string().describe("Element ref ID (w:NN) from browser_map."),
|
|
970
|
+
ref: z.string().optional().describe("Element ref ID (w:NN) from browser_map or input scan."),
|
|
971
|
+
selector: z.string().optional().describe("CSS selector of the field."),
|
|
972
|
+
target: z.string().optional().describe("Alternative selector or query."),
|
|
973
|
+
name: z.string().optional().describe("HTML name attribute of the field."),
|
|
974
|
+
label: z.string().optional().describe("Label or placeholder text."),
|
|
830
975
|
value: z.string().describe("Value to fill/select/type into the field."),
|
|
831
|
-
})).describe("Array of field ref+value pairs to fill sequentially."),
|
|
976
|
+
})).optional().describe("Array of field ref+value pairs to fill sequentially."),
|
|
977
|
+
scan: z.boolean().optional().describe("Set true to discover and return all fillable inputs on the page (with ref IDs, types, and values) in ~30 tokens without full browser_map."),
|
|
978
|
+
inputs: z.boolean().optional().describe("Alias for scan."),
|
|
979
|
+
scan_only: z.boolean().optional().describe("Validate field types without mutating DOM."),
|
|
980
|
+
blast_mode: z.boolean().optional().describe("Fill all fields simultaneously in a single CDP eval round-trip."),
|
|
832
981
|
submit: z.string().optional().describe("Optional ref ID (w:NN) of a submit button to click after filling all fields."),
|
|
833
982
|
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
834
983
|
}, async (args) => {
|
|
@@ -842,13 +991,17 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
842
991
|
}
|
|
843
992
|
});
|
|
844
993
|
// ── 18. browser_highlight ───────────────────────────────────────────────────
|
|
845
|
-
server.tool("browser_highlight", "Highlight
|
|
994
|
+
server.tool("browser_highlight", "Highlight elements on page. Supports single or multiple candidates (refs) with styles ('consideration' for candidate evaluation, 'target' for lock-on, 'danger' for destructive actions, 'success', or 'shimmer'), floating badges, and configurable duration.", {
|
|
846
995
|
selector: z.string().optional().describe("CSS Selector of the element to highlight."),
|
|
847
996
|
id: z.string().optional().describe("Alternative element Ref ID from browser_map (e.g. 'w:42')."),
|
|
848
997
|
ref: z.string().optional().describe("Alternative element Ref ID (e.g. 'w:42')."),
|
|
998
|
+
refs: z.array(z.string()).optional().describe("Array of element Ref IDs to highlight concurrently (e.g. ['w:12', 'w:15', 'w:23'])."),
|
|
849
999
|
target: z.string().optional().describe("Alternative element identifier, CSS Selector, or query."),
|
|
1000
|
+
style: z.enum(["consideration", "target", "danger", "success", "shimmer"]).optional().describe("Visual highlight style. Defaults to 'shimmer' (or 'consideration' when refs provided)."),
|
|
1001
|
+
label: z.string().optional().describe("Floating label badge text to display above the element(s)."),
|
|
1002
|
+
duration: z.number().optional().describe("Duration in ms to keep highlight visible (default: 2000ms for consideration/target, 600ms for shimmer)."),
|
|
850
1003
|
nodeId: z.number().optional().describe("CDP backendNodeId to highlight directly."),
|
|
851
|
-
color: z.string().optional().describe("CSS color to use for the overlay (e.g. 'rgba(255,0,0,0.5)')."),
|
|
1004
|
+
color: z.string().optional().describe("Custom CSS color to use for the overlay (e.g. 'rgba(255,0,0,0.5)')."),
|
|
852
1005
|
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
853
1006
|
}, async (args) => {
|
|
854
1007
|
try {
|
|
@@ -889,8 +1042,10 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
889
1042
|
}
|
|
890
1043
|
});
|
|
891
1044
|
// ── 22. browser_find ────────────────────────────────────────────────────────
|
|
892
|
-
server.tool("browser_find", "
|
|
893
|
-
query: z.string().describe("
|
|
1045
|
+
server.tool("browser_find", "Instant text & keyword search across page text, labels, and placeholders. Returns matched element coordinates, selectors, and ref IDs.", {
|
|
1046
|
+
query: z.string().describe("Text or keyword query to search for on the page."),
|
|
1047
|
+
all: z.boolean().optional().describe("Set true to return all matching elements on the page (default false returns first match)."),
|
|
1048
|
+
limit: z.number().optional().describe("Max number of matches to return when all: true (default 10)."),
|
|
894
1049
|
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
895
1050
|
}, async (args) => {
|
|
896
1051
|
try {
|
|
@@ -1116,7 +1271,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1116
1271
|
}
|
|
1117
1272
|
});
|
|
1118
1273
|
// ── 36. browser_inspect ─────────────────────────────────────────────────────
|
|
1119
|
-
server.tool("browser_inspect", "15-35ms latency, zero waste. Deep DOM inspection.
|
|
1274
|
+
server.tool("browser_inspect", "15-35ms latency, zero waste. Deep DOM inspection. Useful for debugging failed clicks, verifying z-index & visibility, or computed styles.", {
|
|
1120
1275
|
nodeId: z.number().optional().describe("CDP backendNodeId."),
|
|
1121
1276
|
selector: z.string().optional().describe("CSS Selector."),
|
|
1122
1277
|
ref: z.string().optional().describe("Ref ID (e.g. 'w:42')."),
|
|
@@ -1189,17 +1344,10 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1189
1344
|
return wrapError(e);
|
|
1190
1345
|
}
|
|
1191
1346
|
});
|
|
1192
|
-
// ── 38. browser_reset_loop_counter ─────────────────────────────────────────
|
|
1193
|
-
server.tool("browser_reset_loop_counter", "Reset the loop detection counter.", {
|
|
1194
|
-
reason: z.string().optional().describe("Optional reason for the reset (e.g. 'new_task', 'manual_recovery')."),
|
|
1195
|
-
}, async (args) => {
|
|
1196
|
-
resetLoopCounter(args.reason || "manual_tool_call");
|
|
1197
|
-
return { content: [{ type: "text", text: JSON.stringify({ success: true, reason: args.reason || "manual_tool_call" }) }] };
|
|
1198
|
-
});
|
|
1199
1347
|
// ── 37. browser_detect ─────────────────────────────────────────────────────
|
|
1200
|
-
server.tool("browser_detect", "Detect available browsers
|
|
1348
|
+
server.tool("browser_detect", "CRITICAL INITIALIZATION TOOL. Detect available browsers and configured desktop apps (Discord, Slack, etc.). Call this FIRST before interacting with the environment to know if apps are online/offline and what tabs they have open.", {}, async () => {
|
|
1201
1349
|
try {
|
|
1202
|
-
const result = detectBrowsers(getActiveEngine());
|
|
1350
|
+
const result = await detectBrowsers(getActiveEngine());
|
|
1203
1351
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1204
1352
|
}
|
|
1205
1353
|
catch (e) {
|
|
@@ -1220,73 +1368,113 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
|
|
|
1220
1368
|
return wrapError(e);
|
|
1221
1369
|
}
|
|
1222
1370
|
});
|
|
1223
|
-
// ──
|
|
1224
|
-
server.tool("
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
// Defence-in-depth: coerce to string | undefined at the server boundary.
|
|
1242
|
-
// Zod marks `repo` as optional(string) but (args as any) bypasses that
|
|
1243
|
-
// guarantee, so we normalise here before forwarding to githubAnalyze.
|
|
1244
|
-
const rawRepo = args.repo;
|
|
1245
|
-
const repo = typeof rawRepo === "string" && rawRepo.trim() !== ""
|
|
1246
|
-
? rawRepo.trim()
|
|
1247
|
-
: undefined;
|
|
1248
|
-
return await githubAnalyze(session, liveConfig, repo);
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
catch (e) {
|
|
1252
|
-
return wrapError(e);
|
|
1253
|
-
}
|
|
1254
|
-
});
|
|
1255
|
-
server.tool("github_read", "Read file contents from a GitHub repository.", {
|
|
1256
|
-
paths: z.array(z.string()).describe("List of file paths to read, relative to the repository root."),
|
|
1257
|
-
line_range: z.array(z.number()).length(2).optional().describe("Optional [start, end] line range to read specific lines instead of the default first 200 lines. 1-indexed.")
|
|
1258
|
-
}, async (args) => {
|
|
1259
|
-
try {
|
|
1260
|
-
return await withSecurity(config, "github_read", async (session, liveConfig) => {
|
|
1261
|
-
return await githubRead(session, args, liveConfig);
|
|
1262
|
-
});
|
|
1263
|
-
}
|
|
1264
|
-
catch (e) {
|
|
1265
|
-
return wrapError(e);
|
|
1266
|
-
}
|
|
1267
|
-
});
|
|
1268
|
-
server.tool("github_issues", "Interact with GitHub issues.", {
|
|
1269
|
-
search: z.string().optional().describe("Search query to find issues."),
|
|
1270
|
-
number: z.number().optional().describe("Specific issue number to fetch details and comments for."),
|
|
1271
|
-
labels: z.array(z.string()).optional().describe("Filter issues by labels."),
|
|
1272
|
-
state: z.enum(["open", "closed", "all"]).optional().describe("Filter issues by state."),
|
|
1273
|
-
limit: z.number().optional().describe("Max number of issues to list. Default 5.")
|
|
1371
|
+
// ── browser_pattern_learn ─────────────────────────────────────────────────
|
|
1372
|
+
server.tool("browser_pattern_learn", "Read, register, update, and record execution outcomes for named workflow patterns and site quirks in ~/.weavetab-system/memory/sites/. Use action='get' at session start to inspect a site profile and decide between fast-path replay (reliability ≥ 0.8) or exploratory navigation. Use action='learn' after completing a multi-step flow to persist it. Use action='record_execution' to update reliability scores after replay attempts.", {
|
|
1373
|
+
origin: z.string().optional().describe("Full origin associated with the pattern, e.g. 'https://github.com'. Required for write actions. If omitted with action='get', reads the active tab origin."),
|
|
1374
|
+
action: z.enum(["get", "learn", "record_execution", "record_quirk"]).default("learn").describe("'get': inspect site profile and known workflow patterns. 'learn': create/update a named pattern. 'record_execution': update reliability after replay. 'record_quirk': store a site quirk."),
|
|
1375
|
+
pattern: z.string().optional().describe("With action='get': returns reliability score for this specific named pattern only."),
|
|
1376
|
+
name: z.string().optional().describe("Pattern name in snake_case (required for 'learn' and 'record_execution')."),
|
|
1377
|
+
steps: z.array(z.object({
|
|
1378
|
+
tool: z.string().describe("MCP tool name, e.g. 'browser_click'."),
|
|
1379
|
+
target: z.string().optional().describe("Selector or URL."),
|
|
1380
|
+
value: z.string().optional().describe("Value for browser_type/browser_fill/browser_select."),
|
|
1381
|
+
extra: z.record(z.any()).optional().describe("Additional tool arguments.")
|
|
1382
|
+
})).optional().describe("Ordered workflow steps (required for 'learn')."),
|
|
1383
|
+
success_indicator: z.string().optional().describe("CSS selector or URL fragment confirming workflow success."),
|
|
1384
|
+
entry: z.string().optional().describe("URL path or pattern where this workflow starts."),
|
|
1385
|
+
success: z.boolean().optional().describe("Whether the execution succeeded (required for 'record_execution')."),
|
|
1386
|
+
duration_ms: z.number().optional().describe("Execution duration in ms (optional for 'record_execution')."),
|
|
1387
|
+
quirk_key: z.string().optional().describe("Quirk key: 'cookie_banner', 'rate_limit', or 'modal_popups' (required for 'record_quirk')."),
|
|
1388
|
+
quirk_data: z.record(z.any()).optional().describe("Quirk payload object (required for 'record_quirk').")
|
|
1274
1389
|
}, async (args) => {
|
|
1275
1390
|
try {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1391
|
+
if (args.action === "get") {
|
|
1392
|
+
let resolvedOrigin = args.origin;
|
|
1393
|
+
if (!resolvedOrigin) {
|
|
1394
|
+
return await withSecurity(config, "browser_pattern_learn", async (session, _liveConfig) => {
|
|
1395
|
+
try {
|
|
1396
|
+
const res = await session.Runtime.evaluate({
|
|
1397
|
+
expression: "window.location.origin",
|
|
1398
|
+
returnByValue: true,
|
|
1399
|
+
});
|
|
1400
|
+
resolvedOrigin = res?.result?.value;
|
|
1401
|
+
}
|
|
1402
|
+
catch { /* fall through */ }
|
|
1403
|
+
if (!resolvedOrigin) {
|
|
1404
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Could not determine origin from active tab. Pass origin explicitly." }) }] };
|
|
1405
|
+
}
|
|
1406
|
+
if (args.pattern) {
|
|
1407
|
+
const reliability = await getPatternReliability(resolvedOrigin, args.pattern);
|
|
1408
|
+
return { content: [{ type: "text", text: JSON.stringify({ origin: resolvedOrigin, pattern: args.pattern, reliability }) }] };
|
|
1409
|
+
}
|
|
1410
|
+
const profile = await getSiteProfile(resolvedOrigin);
|
|
1411
|
+
if (!profile) {
|
|
1412
|
+
return { content: [{ type: "text", text: JSON.stringify({ origin: resolvedOrigin, known: false, message: "No site profile found. This site has not been profiled yet." }) }] };
|
|
1413
|
+
}
|
|
1414
|
+
const hint = formatSiteProfileHint(profile);
|
|
1415
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...profile, _hint: hint }) }] };
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
if (args.pattern) {
|
|
1419
|
+
const reliability = await getPatternReliability(resolvedOrigin, args.pattern);
|
|
1420
|
+
return { content: [{ type: "text", text: JSON.stringify({ origin: resolvedOrigin, pattern: args.pattern, reliability }) }] };
|
|
1421
|
+
}
|
|
1422
|
+
const profile = await getSiteProfile(resolvedOrigin);
|
|
1423
|
+
if (!profile) {
|
|
1424
|
+
return { content: [{ type: "text", text: JSON.stringify({ origin: resolvedOrigin, known: false, message: "No site profile found. This site has not been profiled yet." }) }] };
|
|
1425
|
+
}
|
|
1426
|
+
const hint = formatSiteProfileHint(profile);
|
|
1427
|
+
return { content: [{ type: "text", text: JSON.stringify({ ...profile, _hint: hint }) }] };
|
|
1428
|
+
}
|
|
1429
|
+
if (!args.origin)
|
|
1430
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "origin is required for write actions" }) }] };
|
|
1431
|
+
if (args.action === "learn") {
|
|
1432
|
+
if (!args.name)
|
|
1433
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "name is required for action='learn'" }) }] };
|
|
1434
|
+
const pattern = await learnPattern(args.origin, args.name, {
|
|
1435
|
+
steps: args.steps ?? [],
|
|
1436
|
+
success_indicator: args.success_indicator,
|
|
1437
|
+
entry: args.entry,
|
|
1438
|
+
});
|
|
1439
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, pattern }) }] };
|
|
1440
|
+
}
|
|
1441
|
+
if (args.action === "record_execution") {
|
|
1442
|
+
if (!args.name)
|
|
1443
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "name is required for action='record_execution'" }) }] };
|
|
1444
|
+
if (args.success === undefined)
|
|
1445
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "success (boolean) is required for action='record_execution'" }) }] };
|
|
1446
|
+
const updated = await recordPatternExecution(args.origin, args.name, args.success, args.duration_ms);
|
|
1447
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, pattern: updated }) }] };
|
|
1448
|
+
}
|
|
1449
|
+
if (args.action === "record_quirk") {
|
|
1450
|
+
if (!args.quirk_key)
|
|
1451
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "quirk_key is required for action='record_quirk'" }) }] };
|
|
1452
|
+
if (!args.quirk_data)
|
|
1453
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "quirk_data is required for action='record_quirk'" }) }] };
|
|
1454
|
+
await recordQuirk(args.origin, args.quirk_key, args.quirk_data);
|
|
1455
|
+
return { content: [{ type: "text", text: JSON.stringify({ ok: true, origin: args.origin, quirk_key: args.quirk_key }) }] };
|
|
1456
|
+
}
|
|
1457
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: "Unknown action" }) }] };
|
|
1279
1458
|
}
|
|
1280
1459
|
catch (e) {
|
|
1281
1460
|
return wrapError(e);
|
|
1282
1461
|
}
|
|
1283
1462
|
});
|
|
1284
|
-
|
|
1285
|
-
|
|
1463
|
+
// ── GitHub Tool (v2.5.0 Consolidated) ──────────────────────────────────
|
|
1464
|
+
server.tool("browser_github", "Interact with GitHub repositories without burning browser turns. Analyze repo trees, read raw source files, search/inspect issues, and examine PR diffs and review comments.", {
|
|
1465
|
+
action: z.enum(["analyze", "read", "issues", "pr"]).describe("GitHub operation: 'analyze' (repo structure & README), 'read' (raw file contents), 'issues' (list/search/inspect issues), 'pr' (PR details, diff files & comments)."),
|
|
1466
|
+
repo: z.string().optional().describe("Optional 'owner/repo' slug or GitHub URL. If omitted, uses active tab's repository."),
|
|
1467
|
+
paths: z.array(z.string()).optional().describe("For action: 'read'. File paths relative to repo root."),
|
|
1468
|
+
line_range: z.array(z.number()).length(2).optional().describe("For action: 'read'. Optional [start, end] line range (1-indexed)."),
|
|
1469
|
+
number: z.number().optional().describe("For action: 'issues' or 'pr'. Issue or PR number to fetch details and comments."),
|
|
1470
|
+
search: z.string().optional().describe("For action: 'issues'. Search query to find issues."),
|
|
1471
|
+
labels: z.array(z.string()).optional().describe("For action: 'issues'. Filter issues by labels."),
|
|
1472
|
+
state: z.enum(["open", "closed", "all"]).optional().describe("For action: 'issues'. Filter issues by state."),
|
|
1473
|
+
limit: z.number().optional().describe("For action: 'issues'. Max number of issues to list (default 5).")
|
|
1286
1474
|
}, async (args) => {
|
|
1287
1475
|
try {
|
|
1288
|
-
return await withSecurity(config, "
|
|
1289
|
-
return await
|
|
1476
|
+
return await withSecurity(config, "browser_github", async (session, liveConfig) => {
|
|
1477
|
+
return await browserGithub(session, args, liveConfig);
|
|
1290
1478
|
});
|
|
1291
1479
|
}
|
|
1292
1480
|
catch (e) {
|
|
@@ -1366,7 +1554,22 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1366
1554
|
return wrapError(e);
|
|
1367
1555
|
}
|
|
1368
1556
|
});
|
|
1369
|
-
// ──
|
|
1557
|
+
// ── 45. browser_checkpoint ─────────────────────────────────────────────────
|
|
1558
|
+
server.tool("browser_checkpoint", "Snapshot and restore browser state (cookies, localStorage, sessionStorage, and scroll/URL) into named checkpoints to time-travel and rollback failed automation branches.", {
|
|
1559
|
+
action: z.enum(["save", "restore", "list", "delete"]).describe("Action to perform: 'save' a new checkpoint, 'restore' an existing checkpoint, 'list' all checkpoints, or 'delete' a checkpoint."),
|
|
1560
|
+
name: z.string().optional().describe("Checkpoint name identifier (required for 'save', 'restore', 'delete')."),
|
|
1561
|
+
includeDom: z.boolean().optional().describe("Whether to include a lightweight DOM snapshot during 'save' (default false)."),
|
|
1562
|
+
app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
|
|
1563
|
+
}, async (args) => {
|
|
1564
|
+
try {
|
|
1565
|
+
return await withSecurity(config, "browser_checkpoint", async (session, liveConfig) => {
|
|
1566
|
+
return await browserCheckpoint(session, args, liveConfig);
|
|
1567
|
+
}, false, args.app);
|
|
1568
|
+
}
|
|
1569
|
+
catch (e) {
|
|
1570
|
+
return wrapError(e);
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1370
1573
|
// page_context: zero-shot context injection. Invoke with /page_context
|
|
1371
1574
|
// in any MCP client that supports prompts (Claude Desktop, etc.).
|
|
1372
1575
|
server.prompt("page_context", "Analyze the current active tab. Use this to give the agent instant context about the page without wasting tool calls.", async () => {
|
|
@@ -1492,20 +1695,38 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1492
1695
|
const addedNames = [];
|
|
1493
1696
|
const overriddenNames = [];
|
|
1494
1697
|
const removedNames = [];
|
|
1698
|
+
// Register any hooks declared in the plugin
|
|
1699
|
+
if (plugin.hooks) {
|
|
1700
|
+
hookPipeline.registerPluginHooks(plugin.name, plugin.hooks);
|
|
1701
|
+
}
|
|
1495
1702
|
// 1. Tool Removals (disable built-in tools)
|
|
1703
|
+
const manifestPerms = plugin.manifest?.permissions;
|
|
1704
|
+
const declaredRemove = Array.isArray(manifestPerms)
|
|
1705
|
+
? []
|
|
1706
|
+
: manifestPerms?.tools?.remove || [];
|
|
1496
1707
|
const toRemove = [
|
|
1497
1708
|
...(plugin.removeTools || []),
|
|
1498
|
-
...(plugin.manifest?.permissions?.tools?.remove || []),
|
|
1499
1709
|
];
|
|
1500
1710
|
for (const toolName of toRemove) {
|
|
1711
|
+
if (!declaredRemove.includes(toolName)) {
|
|
1712
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} tried to remove "${toolName}" but it's not listed in permissions.tools.remove — skipped.\x1b[0m\n`);
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1501
1715
|
if (server._registeredTools?.[toolName]) {
|
|
1502
1716
|
delete server._registeredTools[toolName];
|
|
1503
1717
|
removedNames.push(toolName);
|
|
1504
1718
|
}
|
|
1505
1719
|
}
|
|
1506
1720
|
// 2. Tool Overrides (replace or wrap built-in tools)
|
|
1721
|
+
const declaredOverride = Array.isArray(manifestPerms)
|
|
1722
|
+
? []
|
|
1723
|
+
: manifestPerms?.tools?.override || [];
|
|
1507
1724
|
const overrides = plugin.overrides || [];
|
|
1508
1725
|
for (const ov of overrides) {
|
|
1726
|
+
if (!declaredOverride.includes(ov.name)) {
|
|
1727
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} tried to override "${ov.name}" but it's not listed in permissions.tools.override — skipped.\x1b[0m\n`);
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1509
1730
|
const existingTool = server._registeredTools?.[ov.name];
|
|
1510
1731
|
const existingDesc = existingTool?.description || "";
|
|
1511
1732
|
const existingSchema = existingTool?.parameters?.shape || {};
|
|
@@ -1516,13 +1737,15 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1516
1737
|
try {
|
|
1517
1738
|
return await withSecurity(config, ov.name, async (session, liveConfig) => {
|
|
1518
1739
|
const wrappedOrig = existingTool
|
|
1519
|
-
? async (origArgs,
|
|
1520
|
-
const
|
|
1740
|
+
? async (origArgs, origSess, origCfg) => {
|
|
1741
|
+
const finalSess = origSess || session;
|
|
1742
|
+
const finalCfg = origCfg || liveConfig;
|
|
1743
|
+
const res = await existingTool.handler(origArgs, finalSess, finalCfg);
|
|
1521
1744
|
return res;
|
|
1522
1745
|
}
|
|
1523
1746
|
: undefined;
|
|
1524
1747
|
const result = await ov.handler(args, session, liveConfig, wrappedOrig);
|
|
1525
|
-
return
|
|
1748
|
+
return result;
|
|
1526
1749
|
}, isOfficial);
|
|
1527
1750
|
}
|
|
1528
1751
|
catch (e) {
|
|
@@ -1532,16 +1755,22 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1532
1755
|
overriddenNames.push(ov.name);
|
|
1533
1756
|
}
|
|
1534
1757
|
// 3. Tool Additions (new tools)
|
|
1758
|
+
const canAdd = !Array.isArray(manifestPerms) && manifestPerms?.tools?.add === true;
|
|
1535
1759
|
const newTools = plugin.tools || [];
|
|
1536
1760
|
for (const td of newTools) {
|
|
1761
|
+
if (!canAdd) {
|
|
1762
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} tried to add tool "${td.name}" but permissions.tools.add is not declared — skipped.\x1b[0m\n`);
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1537
1765
|
if (server._registeredTools?.[td.name]) {
|
|
1766
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} registered tool "${td.name}" which replaced an existing tool.\x1b[0m\n`);
|
|
1538
1767
|
delete server._registeredTools[td.name];
|
|
1539
1768
|
}
|
|
1540
|
-
server.tool(td.name, td.description, td.schema || {}, async (args) => {
|
|
1769
|
+
server.tool(td.name, td.description, td.schema || td.parameters || {}, async (args) => {
|
|
1541
1770
|
try {
|
|
1542
1771
|
return await withSecurity(config, td.name, async (session, liveConfig) => {
|
|
1543
1772
|
const result = await td.handler(args, session, liveConfig);
|
|
1544
|
-
return
|
|
1773
|
+
return result;
|
|
1545
1774
|
}, isOfficial);
|
|
1546
1775
|
}
|
|
1547
1776
|
catch (e) {
|
|
@@ -1550,68 +1779,92 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
|
|
|
1550
1779
|
});
|
|
1551
1780
|
addedNames.push(td.name);
|
|
1552
1781
|
}
|
|
1553
|
-
// Pass the SDK context so
|
|
1554
|
-
const
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1782
|
+
// Pass the rich SDK context so dynamic tool registrations receive full security & session
|
|
1783
|
+
const fullPluginCtx = plugin._context;
|
|
1784
|
+
if (fullPluginCtx && fullPluginCtx.mcp) {
|
|
1785
|
+
fullPluginCtx.mcp.registerTool = (tool) => {
|
|
1786
|
+
if (!canAdd) {
|
|
1787
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic registerTool("${tool?.name}"): permissions.tools.add not declared — skipped.\x1b[0m\n`);
|
|
1788
|
+
return;
|
|
1560
1789
|
}
|
|
1561
|
-
|
|
1562
|
-
|
|
1790
|
+
if (server._registeredTools?.[tool.name]) {
|
|
1791
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic tool "${tool.name}" replaced an existing tool.\x1b[0m\n`);
|
|
1792
|
+
delete server._registeredTools[tool.name];
|
|
1793
|
+
}
|
|
1794
|
+
server.tool(tool.name, tool.description || "", tool.schema || tool.parameters || {}, async (args) => {
|
|
1795
|
+
return await withSecurity(config, tool.name, async (session, liveConfig) => {
|
|
1796
|
+
const result = await tool.handler(args, session, liveConfig);
|
|
1797
|
+
return result;
|
|
1798
|
+
}, isOfficial);
|
|
1799
|
+
});
|
|
1800
|
+
};
|
|
1801
|
+
fullPluginCtx.mcp.overrideTool = (toolName, overrideHandler) => {
|
|
1802
|
+
if (!declaredOverride.includes(toolName)) {
|
|
1803
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic overrideTool("${toolName}"): not in permissions.tools.override — skipped.\x1b[0m\n`);
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
const existingTool = server._registeredTools?.[toolName];
|
|
1807
|
+
if (existingTool) {
|
|
1808
|
+
delete server._registeredTools[toolName];
|
|
1809
|
+
}
|
|
1810
|
+
server.tool(toolName, `Tool overridden by ${plugin.name}`, {}, async (args) => {
|
|
1811
|
+
return await withSecurity(config, toolName, async (session, liveConfig) => {
|
|
1812
|
+
const wrappedOrig = existingTool
|
|
1813
|
+
? async (origArgs, origSess, origCfg) => {
|
|
1814
|
+
const finalSess = origSess || session;
|
|
1815
|
+
const finalCfg = origCfg || liveConfig;
|
|
1816
|
+
return await existingTool.handler(origArgs, finalSess, finalCfg);
|
|
1817
|
+
}
|
|
1818
|
+
: undefined;
|
|
1819
|
+
const result = await overrideHandler(args, session, liveConfig, wrappedOrig);
|
|
1820
|
+
return result;
|
|
1821
|
+
}, isOfficial);
|
|
1822
|
+
});
|
|
1823
|
+
};
|
|
1824
|
+
fullPluginCtx.mcp.removeTool = (toolName) => {
|
|
1825
|
+
if (!declaredRemove.includes(toolName)) {
|
|
1826
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic removeTool("${toolName}"): not in permissions.tools.remove — skipped.\x1b[0m\n`);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
if (server._registeredTools?.[toolName]) {
|
|
1830
|
+
delete server._registeredTools[toolName];
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
fullPluginCtx.mcp.unregisterTool = (toolName) => {
|
|
1834
|
+
if (!declaredRemove.includes(toolName)) {
|
|
1835
|
+
process.stderr.write(` \x1b[33m[!] Plugin ${plugin.name} dynamic unregisterTool("${toolName}"): not in permissions.tools.remove — skipped.\x1b[0m\n`);
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
if (server._registeredTools?.[toolName]) {
|
|
1839
|
+
delete server._registeredTools[toolName];
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1563
1843
|
if (typeof plugin.onLoad === "function") {
|
|
1564
1844
|
try {
|
|
1565
|
-
await plugin.onLoad(
|
|
1845
|
+
await plugin.onLoad(fullPluginCtx || plugin._context);
|
|
1566
1846
|
}
|
|
1567
1847
|
catch (err) {
|
|
1568
1848
|
process.stderr.write(` \u001b[31m[!] Plugin ${plugin.name} onLoad failed: ${err?.message}\u001b[0m\n`);
|
|
1569
1849
|
}
|
|
1570
1850
|
}
|
|
1571
1851
|
loadedPluginNames.add(plugin.name);
|
|
1852
|
+
activeLoadedPlugins.set(plugin.name, plugin);
|
|
1572
1853
|
process.stderr.write(` [Plugin Loaded] ${plugin.name} v${plugin.version}\n`);
|
|
1573
1854
|
// Notify the client that tools have changed
|
|
1574
1855
|
try {
|
|
1575
|
-
if (server.server) {
|
|
1856
|
+
if (server.server && typeof server.server.sendToolListChanged === "function") {
|
|
1576
1857
|
server.server.sendToolListChanged();
|
|
1577
1858
|
}
|
|
1578
1859
|
}
|
|
1579
|
-
catch
|
|
1860
|
+
catch {
|
|
1580
1861
|
// Ignore if client doesn't support it or not connected yet
|
|
1581
1862
|
}
|
|
1582
1863
|
};
|
|
1583
|
-
//
|
|
1584
|
-
server.tool("list_plugins", "List all available Weavetab plugins that can be loaded.", {}, async () => {
|
|
1585
|
-
const installed = listInstalledPlugins();
|
|
1586
|
-
const available = installed
|
|
1587
|
-
.filter(info => !loadedPluginNames.has(info.packageName))
|
|
1588
|
-
.map(info => `- ${info.packageName}: ${info.manifest?.description || "No description"}`);
|
|
1589
|
-
if (available.length === 0) {
|
|
1590
|
-
return { content: [{ type: "text", text: "No un-loaded plugins available." }] };
|
|
1591
|
-
}
|
|
1592
|
-
return { content: [{ type: "text", text: `Available plugins:\n${available.join("\n")}\n\nUse load_plugin to load one.` }] };
|
|
1593
|
-
});
|
|
1594
|
-
server.tool("load_plugin", "Load a Weavetab plugin into the MCP context dynamically.", {
|
|
1595
|
-
name: z.string().describe("The exact name of the plugin to load")
|
|
1596
|
-
}, async ({ name }) => {
|
|
1597
|
-
if (loadedPluginNames.has(name)) {
|
|
1598
|
-
return { content: [{ type: "text", text: `Plugin ${name} is already loaded.` }] };
|
|
1599
|
-
}
|
|
1600
|
-
const plugin = loadPluginByName(name);
|
|
1601
|
-
if (!plugin) {
|
|
1602
|
-
return { content: [{ type: "text", text: `Plugin ${name} not found.` }] };
|
|
1603
|
-
}
|
|
1604
|
-
await registerPluginTools(plugin);
|
|
1605
|
-
return { content: [{ type: "text", text: `Successfully loaded plugin ${name}. The tools have been registered in your context.` }] };
|
|
1606
|
-
});
|
|
1607
|
-
// We load official core plugins by default, but leave others lazy.
|
|
1864
|
+
// Auto-load all installed plugins at startup
|
|
1608
1865
|
const allPlugins = discoverPlugins();
|
|
1609
1866
|
for (const plugin of allPlugins) {
|
|
1610
|
-
|
|
1611
|
-
const isLazy = plugin.manifest?.environments?.includes("mcp");
|
|
1612
|
-
if (trust.tier === "official" || !isLazy) {
|
|
1613
|
-
await registerPluginTools(plugin);
|
|
1614
|
-
}
|
|
1867
|
+
await registerPluginTools(plugin);
|
|
1615
1868
|
}
|
|
1616
1869
|
let transport;
|
|
1617
1870
|
if (transportMode === "ws") {
|
|
@@ -1662,6 +1915,28 @@ async function cleanup() {
|
|
|
1662
1915
|
if (cleanupCalled)
|
|
1663
1916
|
return;
|
|
1664
1917
|
cleanupCalled = true;
|
|
1918
|
+
// Run plugin onUnload hooks and clear hook pipeline
|
|
1919
|
+
try {
|
|
1920
|
+
for (const plugin of activeLoadedPlugins.values()) {
|
|
1921
|
+
if (typeof plugin.onUnload === "function") {
|
|
1922
|
+
try {
|
|
1923
|
+
plugin.onUnload(plugin._context);
|
|
1924
|
+
}
|
|
1925
|
+
catch (e) {
|
|
1926
|
+
process.stderr.write(`[Weavetab] Error in plugin "${plugin.name}" onUnload: ${e?.message || e}\n`);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
activeLoadedPlugins.clear();
|
|
1931
|
+
hookPipeline.clearAllHooks();
|
|
1932
|
+
}
|
|
1933
|
+
catch { /* non-fatal */ }
|
|
1934
|
+
// Stop config file watcher handle
|
|
1935
|
+
try {
|
|
1936
|
+
const { stopConfigWatcher } = await import("./config/loader.js");
|
|
1937
|
+
stopConfigWatcher();
|
|
1938
|
+
}
|
|
1939
|
+
catch { /* non-fatal */ }
|
|
1665
1940
|
if (global.__wt_is_server) {
|
|
1666
1941
|
// In server mode the browser must survive server restarts. Only cancel
|
|
1667
1942
|
// analytics intervals and close CDP WebSocket connections. Never call
|