@weavetab/mcp 2.5.0-beta.0 → 2.5.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/AGENTS.md +1 -0
  2. package/LICENSE +2 -2
  3. package/README.md +208 -43
  4. package/SECURITY.md +3 -3
  5. package/TROUBLESHOOTING.md +1 -1
  6. package/dist/cdp/bridge.d.ts +10 -0
  7. package/dist/cdp/bridge.js +288 -68
  8. package/dist/cdp/connector.d.ts +5 -3
  9. package/dist/cdp/connector.js +188 -74
  10. package/dist/cdp/extension.js +0 -1
  11. package/dist/cdp/helpers.js +0 -2
  12. package/dist/cdp/lock.d.ts +9 -6
  13. package/dist/cdp/lock.js +72 -59
  14. package/dist/cdp/omni_broker.d.ts +1 -0
  15. package/dist/cdp/omni_broker.js +12 -6
  16. package/dist/cdp/profiles.d.ts +10 -0
  17. package/dist/cdp/profiles.js +65 -0
  18. package/dist/cdp/walker.js +50 -0
  19. package/dist/cli/plugin.d.ts +0 -2
  20. package/dist/cli/plugin.js +31 -683
  21. package/dist/cli/updater.js +2 -2
  22. package/dist/config/loader.d.ts +25 -6
  23. package/dist/config/loader.js +133 -20
  24. package/dist/index.js +48 -52
  25. package/dist/lifecycle/mission_manager.d.ts +0 -1
  26. package/dist/lifecycle/mission_manager.js +0 -3
  27. package/dist/overlay/ghost.d.ts +4 -0
  28. package/dist/overlay/ghost.js +118 -94
  29. package/dist/overlay/injector.d.ts +1 -1
  30. package/dist/overlay/injector.js +1 -15
  31. package/dist/plugin/extension_bridge.d.ts +40 -0
  32. package/dist/plugin/extension_bridge.js +158 -0
  33. package/dist/plugin/interface.d.ts +78 -16
  34. package/dist/plugin/loader.d.ts +4 -0
  35. package/dist/plugin/loader.js +178 -57
  36. package/dist/plugin/registry.js +11 -1
  37. package/dist/plugin/storage.d.ts +51 -0
  38. package/dist/plugin/storage.js +164 -0
  39. package/dist/server.js +217 -151
  40. package/dist/state/session.d.ts +2 -2
  41. package/dist/state/session.js +33 -13
  42. package/dist/tools/browser_detect.d.ts +2 -1
  43. package/dist/tools/browser_detect.js +6 -5
  44. package/dist/tools/burst.d.ts +1 -0
  45. package/dist/tools/burst.js +4 -6
  46. package/dist/tools/click.js +23 -28
  47. package/dist/tools/clipboard.js +31 -24
  48. package/dist/tools/navigate.d.ts +1 -1
  49. package/dist/tools/navigate.js +28 -10
  50. package/dist/tools/read.js +1 -1
  51. package/dist/tools/tabs.d.ts +10 -10
  52. package/dist/tools/tabs.js +47 -39
  53. package/dist/tools/type.js +2 -2
  54. package/docs/DOCUMENTATION.md +63 -6
  55. package/docs/architecture/blind-injection.md +20 -0
  56. package/docs/architecture/dual-injection.md +32 -0
  57. package/docs/architecture/overview.md +49 -0
  58. package/docs/architecture/security.md +35 -0
  59. package/docs/architecture/transport.md +37 -0
  60. package/docs/captcha-stealth/captcha-solver.md +75 -0
  61. package/docs/captcha-stealth/stealth-physics.md +60 -0
  62. package/docs/cli/cli-commands.md +44 -0
  63. package/docs/cli/cli-flags.md +20 -0
  64. package/docs/cli/cli-introduction.md +20 -0
  65. package/docs/cli/cli-scripting.md +32 -0
  66. package/docs/cli/g-ide/ide-antigravity.md +24 -0
  67. package/docs/cli/g-ide/ide-claude-code.md +24 -0
  68. package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
  69. package/docs/cli/g-ide/ide-cursor.md +31 -0
  70. package/docs/cli/g-ide/ide-generic.md +34 -0
  71. package/docs/cli/g-ide/ide-opencode.md +24 -0
  72. package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
  73. package/docs/cli/g-ide/ide-windsurf.md +24 -0
  74. package/docs/configuration/config-locations.md +24 -0
  75. package/docs/configuration/configuration.md +73 -0
  76. package/docs/configuration/env-variables.md +19 -0
  77. package/docs/configuration/folders.md +54 -0
  78. package/docs/desktop-automation/electron-apps.md +162 -0
  79. package/docs/get-started/installation.md +47 -0
  80. package/docs/get-started/introduction.md +60 -0
  81. package/docs/get-started/quickstart.md +54 -0
  82. package/docs/get-started/setup-wizard.md +26 -0
  83. package/docs/skills-ecosystem/skills-overview.md +35 -0
  84. package/docs/skills-ecosystem/skills-recipes.md +36 -0
  85. package/docs/tools/tools.md +293 -0
  86. package/docs/troubleshooting/trouble-browser.md +27 -0
  87. package/docs/troubleshooting/trouble-config.md +20 -0
  88. package/docs/troubleshooting/trouble-loop.md +31 -0
  89. package/ext-dist/content.js +126 -101
  90. package/mcp.json +8 -15
  91. package/package.json +11 -5
  92. package/server.json +3 -3
  93. package/dist/tools/plan.d.ts +0 -39
  94. package/dist/tools/plan.js +0 -161
  95. package/docs/DEV_MODE.md +0 -68
  96. package/docs/PLUGINS.md +0 -538
  97. package/docs/TOOLS.md +0 -104
  98. package/docs/dev_plugin.md +0 -310
package/dist/server.js CHANGED
@@ -11,7 +11,6 @@ import { checkRateLimit } from "./security/ratelimit.js";
11
11
  import { checkDomain, isToolAllowedInMode } from "./security/blacklist.js";
12
12
  import { logAction, logActionStructured } from "./audit/logger.js";
13
13
  import { recordToolUsage, broadcastTelemetry } from "./audit/telemetry.js";
14
- import { clearLock } from "./cdp/lock.js";
15
14
  import { browserMap } from "./tools/read.js";
16
15
  import { weaveNavigate } from "./tools/navigate.js";
17
16
  import { weaveClick } from "./tools/click.js";
@@ -25,7 +24,6 @@ import { browserWait } from "./tools/wait.js";
25
24
  import { browserPointer } from "./tools/pointer.js";
26
25
  import { browserSelect } from "./tools/select.js";
27
26
  import { browserFill } from "./tools/fill.js";
28
- import { weavePlan } from "./tools/plan.js";
29
27
  import { browserHighlight } from "./tools/highlight.js";
30
28
  import { browserUpload } from "./tools/upload.js";
31
29
  import { browserFind } from "./tools/find.js";
@@ -53,7 +51,7 @@ import { browserCaptcha } from "./tools/captcha.js";
53
51
  import { browserEmulate } from "./tools/emulation.js";
54
52
  import { getActiveEngine } from "./cdp/connector.js";
55
53
  import { resetLoopCounter } from "./intelligence/trail.js";
56
- import { discoverPlugins } from "./plugin/loader.js";
54
+ import { discoverPlugins, listInstalledPlugins, loadPluginByName } from "./plugin/loader.js";
57
55
  import { agentAction } from "./sensors/pacing.js";
58
56
  import { getCachedConfig } from "./config/loader.js";
59
57
  import { agentState } from "./state/agent.js";
@@ -106,7 +104,7 @@ function withTimeoutGuard(fn, timeoutMs, label) {
106
104
  });
107
105
  return Promise.race([fn(), timeoutPromise]).finally(() => clearTimeout(timer));
108
106
  }
109
- async function withSecurity(config, toolName, handler, isOfficial = false) {
107
+ async function withSecurity(config, toolName, handler, isOfficial = false, targetApp) {
110
108
  const liveConfig = getCachedConfig();
111
109
  const rl = checkRateLimit(liveConfig);
112
110
  if (!rl.allowed) {
@@ -119,7 +117,7 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
119
117
  throw new Error(`[!] ${roleToolCheck.reason}`);
120
118
  }
121
119
  }
122
- const { session } = await withTimeoutGuard(() => getOrConnect(liveConfig), 15000, `CDP Connection (${toolName})`);
120
+ const { session } = await withTimeoutGuard(() => getOrConnect(liveConfig, targetApp), 15000, `CDP Connection (${toolName}${targetApp ? `:${targetApp}` : ''})`);
123
121
  agentState.setSession(session);
124
122
  global.__wt_live_config = liveConfig;
125
123
  try {
@@ -143,8 +141,8 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
143
141
  }
144
142
  if (!isOfficial) {
145
143
  // Phase 7c: Domain policy check for read-only / deny / audit-only modes
146
- const activeClient = sessionRegistry.getActive(liveConfig.agentId);
147
- if (activeClient?.url) {
144
+ const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
145
+ if (activeClient?.url && !activeClient.isElectron) {
148
146
  const domainCheck = checkDomain(activeClient.url, liveConfig);
149
147
  if (!domainCheck.allowed) {
150
148
  throw new Error(`[!] ${domainCheck.reason}`);
@@ -200,7 +198,7 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
200
198
  const result = await withTimeoutGuard(() => handler(session, liveConfig), 22000, toolName);
201
199
  agentState.reportToolSuccess();
202
200
  const ms = Date.now() - t0;
203
- const activeClient = sessionRegistry.getActive(liveConfig.agentId);
201
+ const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
204
202
  logActionStructured({
205
203
  timestamp: new Date(t0).toISOString(),
206
204
  sessionId: activeClient?.targetId,
@@ -324,7 +322,7 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
324
322
  const ms = Date.now() - t0;
325
323
  const errMsg = err instanceof Error ? err.message : String(err);
326
324
  agentState.reportToolError();
327
- const activeClient = sessionRegistry.getActive(liveConfig.agentId);
325
+ const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
328
326
  logActionStructured({
329
327
  timestamp: new Date(t0).toISOString(),
330
328
  sessionId: activeClient?.targetId,
@@ -373,6 +371,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
373
371
  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."),
374
372
  ax_tree: z.boolean().optional().describe("Bypass the extension entirely and read raw Chrome Accessibility Tree. Use this if Shadow DOMs hide elements."),
375
373
  tab_id: z.string().optional().describe("Read a specific background tab instead of the active one."),
374
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
376
375
  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."),
377
376
  force: z.boolean().optional().describe("Force a full DOM map rebuild even if loop detection says nothing changed. Bypasses the map skip guard."),
378
377
  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."),
@@ -382,12 +381,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
382
381
  let targetSession = session;
383
382
  let targetId = "default";
384
383
  const { sessionRegistry } = await import("./state/session.js");
385
- const active = sessionRegistry.getActive(liveConfig.agentId);
384
+ const active = sessionRegistry.getActive(liveConfig.agentId, args.app);
386
385
  if (active)
387
386
  targetId = active.targetId;
388
387
  if (args.tab_id) {
389
388
  const { connectToTab } = await import("./cdp/connector.js");
390
- const client = await connectToTab(args.tab_id);
389
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
391
390
  targetSession = client.session;
392
391
  targetId = args.tab_id;
393
392
  }
@@ -411,30 +410,37 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
411
410
  }
412
411
  const result = await agentAction(targetSession, 'reading', () => browserMap(targetSession, args, targetId, liveConfig), 'browser_map');
413
412
  return { content: [{ type: "text", text: result }] };
414
- });
413
+ }, false, args.app);
415
414
  }
416
415
  catch (e) {
417
416
  return wrapError(e);
418
417
  }
419
418
  });
420
419
  // ── 2. browser_navigate ────────────────────────────────────────────────────
421
- server.tool("browser_navigate", "Navigate to a URL.", {
422
- url: z.string().describe("Absolute URL to navigate to (must include https://)."),
420
+ server.tool("browser_navigate", "Navigate to a URL or launch/focus a configured Electron app.", {
421
+ url: z.string().optional().describe("Absolute URL to navigate to (must include https:// or app URL). Optional if 'app' is specified."),
422
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron' (e.g. 'example' or 'my-dev-app'). When specified, targets or launches the Electron app."),
423
423
  force: z.boolean().optional().describe("If true, overwrites current tab even if it's protected (anchor tab). Otherwise opens a new tab automatically."),
424
424
  tab_id: z.string().optional().describe("Target a specific tab ID for navigation instead of the active tab.")
425
425
  }, async (args) => {
426
426
  try {
427
+ if (!args.url && !args.app) {
428
+ throw new Error("Either 'url' or 'app' must be provided to browser_navigate.");
429
+ }
427
430
  return await withSecurity(config, "browser_navigate", async (session, liveConfig) => {
428
431
  let targetSession = session;
429
432
  let targetId = "default";
430
433
  if (args.tab_id) {
431
434
  const { connectToTab } = await import("./cdp/connector.js");
432
- const client = await connectToTab(args.tab_id);
435
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
433
436
  targetSession = client.session;
434
437
  targetId = args.tab_id;
435
438
  }
436
- return await agentAction(targetSession, 'navigating', () => weaveNavigate(targetSession, args.url, liveConfig, targetId, args.force), 'browser_navigate');
437
- });
439
+ if (args.url) {
440
+ return await agentAction(targetSession, 'navigating', () => weaveNavigate(targetSession, args.url, liveConfig, targetId, args.force), 'browser_navigate');
441
+ }
442
+ return { success: true, app: args.app, message: `Connected to Electron app "${args.app}".` };
443
+ }, false, args.app);
438
444
  }
439
445
  catch (e) {
440
446
  return wrapError(e);
@@ -443,6 +449,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
443
449
  // ── 3. browser_click ───────────────────────────────────────────────────────
444
450
  server.tool("browser_click", "Click a DOM element. Use browser_canvas for charts/canvas. If stuck_warning=true, stop and use browser_map.", {
445
451
  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
+ ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
446
453
  selector: z.string().optional().describe("CSS selector of the element to click."),
447
454
  target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
448
455
  label: z.string().optional().describe("Visible text or aria-label to click."),
@@ -452,22 +459,25 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
452
459
  y: z.number().optional().describe("Raw Y coordinate. Use for precise positional clicks when element ID is not available."),
453
460
  include_map: z.boolean().optional().describe("Set true to include a full updated page_map in the response. Default false — omit when you already have ref IDs or don't need a DOM refresh."),
454
461
  tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
462
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
455
463
  }, async (args) => {
456
464
  try {
457
465
  return await withSecurity(config, "browser_click", async (session, liveConfig) => {
458
466
  let targetSession = session;
459
467
  if (args.tab_id) {
460
468
  const { connectToTab } = await import("./cdp/connector.js");
461
- const client = await connectToTab(args.tab_id);
469
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
462
470
  targetSession = client.session;
463
471
  }
464
472
  const normalizedArgs = { ...args };
473
+ if (args.ref && !normalizedArgs.id)
474
+ normalizedArgs.id = args.ref;
465
475
  if (args.selector && !normalizedArgs.id)
466
476
  normalizedArgs.id = args.selector;
467
477
  if (args.target && !normalizedArgs.id)
468
478
  normalizedArgs.id = args.target;
469
479
  return await agentAction(targetSession, 'clicking', () => weaveClick(targetSession, normalizedArgs, liveConfig), 'browser_click');
470
- });
480
+ }, false, args.app);
471
481
  }
472
482
  catch (e) {
473
483
  return wrapError(e);
@@ -476,6 +486,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
476
486
  // ── 4. browser_type ────────────────────────────────────────────────────────
477
487
  server.tool("browser_type", "Type text into a single input field (use browser_fill for smart multi-field forms). If NOT_EDITABLE, fallback to click + browser_key. Use browser_map if stuck.", {
478
488
  id: z.string().optional().describe("Ref ID from browser_map. Primary targeting — always prefer this over label when you have a ref ID."),
489
+ ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
479
490
  selector: z.string().optional().describe("CSS selector of the input field. Use when you know the exact selector."),
480
491
  target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
481
492
  label: z.string().optional().describe("Field label or placeholder text. Used for fuzzy matching."),
@@ -487,22 +498,25 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
487
498
  paste: z.boolean().optional().describe("Instant paste mode via clipboard. Auto-enabled for text longer than 8 chars. Best for long URLs or multi-word strings."),
488
499
  include_map: z.boolean().optional().describe("Set true to include a full updated page_map in the response. Default false."),
489
500
  tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
501
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
490
502
  }, async (args) => {
491
503
  try {
492
504
  return await withSecurity(config, "browser_type", async (session, liveConfig) => {
493
505
  let targetSession = session;
494
506
  if (args.tab_id) {
495
507
  const { connectToTab } = await import("./cdp/connector.js");
496
- const client = await connectToTab(args.tab_id);
508
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
497
509
  targetSession = client.session;
498
510
  }
499
511
  const normalizedArgs = { ...args };
512
+ if (args.ref && !normalizedArgs.id)
513
+ normalizedArgs.id = args.ref;
500
514
  if (args.selector && !normalizedArgs.id)
501
515
  normalizedArgs.id = args.selector;
502
516
  if (args.target && !normalizedArgs.id)
503
517
  normalizedArgs.id = args.target;
504
518
  return await agentAction(targetSession, 'typing', () => weaveType(targetSession, normalizedArgs, liveConfig), 'browser_type');
505
- });
519
+ }, false, args.app);
506
520
  }
507
521
  catch (e) {
508
522
  return wrapError(e);
@@ -511,13 +525,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
511
525
  // ── 4b. browser_type_secret ────────────────────────────────────────────────
512
526
  server.tool("browser_type_secret", "Inject a secret from an environment variable.", {
513
527
  id: z.string().optional().describe("Ref ID from browser_map."),
528
+ ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
514
529
  target: z.string().optional().describe("Alternative target input element identifier or CSS selector."),
515
530
  envKey: z.string().describe("The environment variable key containing the secret."),
531
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
516
532
  }, async (args) => {
517
533
  try {
518
534
  return await withSecurity(config, "browser_type_secret", async (session, liveConfig) => {
519
- return await agentAction(session, 'typing', () => weaveTypeSecret(session, args, liveConfig), 'browser_type_secret');
520
- });
535
+ const normalizedArgs = { ...args };
536
+ if (args.ref && !normalizedArgs.id)
537
+ normalizedArgs.id = args.ref;
538
+ return await agentAction(session, 'typing', () => weaveTypeSecret(session, normalizedArgs, liveConfig), 'browser_type_secret');
539
+ }, false, args.app);
521
540
  }
522
541
  catch (e) {
523
542
  return wrapError(e);
@@ -528,18 +547,19 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
528
547
  key: z.string().describe("Key name: Enter, Escape, Tab, ArrowDown, F5, etc. DO NOT SPAM. If action fails or loops, STOP calling this tool and use browser_map to re-evaluate the page state."),
529
548
  modifiers: z.array(z.enum(["Alt", "Ctrl", "Meta", "Shift"])).optional().describe("Array of modifier keys to hold during the press."),
530
549
  tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
550
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
531
551
  }, async (args) => {
532
- const { key, modifiers, tab_id } = args;
552
+ const { key, modifiers, tab_id, app } = args;
533
553
  try {
534
554
  return await withSecurity(config, "browser_key", async (session, liveConfig) => {
535
555
  let targetSession = session;
536
556
  if (tab_id) {
537
557
  const { connectToTab } = await import("./cdp/connector.js");
538
- const client = await connectToTab(tab_id);
558
+ const client = await connectToTab(tab_id, undefined, undefined, undefined, app);
539
559
  targetSession = client.session;
540
560
  }
541
561
  return await weaveKey(targetSession, { key, modifiers }, liveConfig);
542
- });
562
+ }, false, app);
543
563
  }
544
564
  catch (e) {
545
565
  return wrapError(e);
@@ -548,39 +568,48 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
548
568
  // ── 6. browser_scroll ──────────────────────────────────────────────────────
549
569
  server.tool("browser_scroll", "Scroll the page.", {
550
570
  id: z.string().optional().describe("Ref ID to scroll into view or scroll inside of. DO NOT SPAM. If action fails or loops, STOP calling this tool and use browser_map to re-evaluate the page state."),
571
+ ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
551
572
  selector: z.string().optional().describe("CSS Selector of the element to scroll into view."),
552
573
  target: z.string().optional().describe("Alternative element identifier or query to scroll into view."),
553
574
  label: z.string().optional().describe("Element label to scroll into view."),
554
575
  direction: z.enum(["up", "down", "left", "right"]).describe("Scroll direction."),
555
576
  distance: z.number().optional().describe("Pixels to scroll (default 400)."),
556
577
  tab_id: z.string().optional().describe("Execute in a specific tab instead of active. DO NOT SPAM. If action fails or loops, STOP calling this tool and use browser_map to re-evaluate the page state."),
578
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
557
579
  }, async (args) => {
558
580
  try {
559
581
  return await withSecurity(config, "browser_scroll", async (session, liveConfig) => {
560
582
  let targetSession = session;
561
583
  if (args.tab_id) {
562
584
  const { connectToTab } = await import("./cdp/connector.js");
563
- const client = await connectToTab(args.tab_id);
585
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
564
586
  targetSession = client.session;
565
587
  }
566
588
  const normalizedArgs = { ...args };
589
+ if (args.ref && !normalizedArgs.id)
590
+ normalizedArgs.id = args.ref;
567
591
  if (args.selector && !normalizedArgs.id)
568
592
  normalizedArgs.id = args.selector;
569
593
  if (args.target && !normalizedArgs.id)
570
594
  normalizedArgs.id = args.target;
571
595
  return await browserScroll(targetSession, normalizedArgs, liveConfig);
572
- });
596
+ }, false, args.app);
573
597
  }
574
598
  catch (e) {
575
599
  return wrapError(e);
576
600
  }
577
601
  });
578
602
  // ── 7. browser_dialog ──────────────────────────────────────────────────────
579
- server.tool("browser_dialog", "Accept or dismiss JS dialogs.", {
603
+ server.tool("browser_dialog", "Accept or dismiss JS dialogs (alert, confirm, prompt). Browser sessions only.", {
580
604
  action: z.enum(["accept", "dismiss"]).describe("Action to perform on the dialog."),
581
605
  promptText: z.string().optional().describe("Text to enter if it is a prompt dialog.")
582
606
  }, async (args) => {
583
607
  try {
608
+ const liveConfig = getCachedConfig();
609
+ const active = sessionRegistry.getActive(liveConfig.agentId);
610
+ if (active?.isElectron) {
611
+ return { content: [{ type: "text", text: "[Note] JS dialog interception is only available for web browser sessions (Electron apps handle dialogs natively)." }] };
612
+ }
584
613
  return await withSecurity(config, "browser_dialog", async (session) => {
585
614
  return await browserDialog(session, args);
586
615
  });
@@ -595,65 +624,67 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
595
624
  tool: z.enum(["browser_click", "browser_type", "browser_navigate", "browser_key", "browser_scroll", "browser_wait", "browser_map", "browser_pointer", "browser_eval", "browser_select", "type_and_send", "navigate_and_read", "click_and_wait", "scroll_and_read"]),
596
625
  args: z.record(z.unknown()).describe("Arguments for the respective tool. For type_and_send: { text, x?, y?, id?, label?, pressEnter? }. For navigate_and_read: { url }. For click_and_wait: { id?, label?, x?, y?, wait_for?, wait_value? }. For scroll_and_read: { direction, distance? }.")
597
626
  })).describe("Ordered array of tool calls to execute serially. Use macro steps to save tokens."),
598
- batch: z.boolean().optional().describe("If true, defers action map generation until all steps are complete to save time.")
627
+ batch: z.boolean().optional().describe("If true, defers action map generation until all steps are complete to save time."),
628
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
599
629
  }, async (args) => {
600
630
  try {
601
631
  return await withSecurity(config, "browser_burst", async (session, liveConfig) => {
602
632
  const { browserBurst } = await import("./tools/burst.js");
603
633
  return await browserBurst(session, args, liveConfig);
604
- });
634
+ }, false, args.app);
605
635
  }
606
636
  catch (e) {
607
637
  return wrapError(e);
608
638
  }
609
639
  });
610
640
  // ── 9. browser_tabs ───────────────────────────────────────────────────────
611
- server.tool("browser_tabs", "Manage browser tabs (open, switch, close, list).", {
641
+ server.tool("browser_tabs", "Manage browser and Electron app tabs/windows (open, switch, close, list).", {
612
642
  action: z.enum(["list", "open", "close", "switch", "list_windows", "focus_window", "consolidate", "heal"]).describe("Action to perform. 'consolidate' deduplicates tabs. 'heal' recovers crashed tabs."),
613
643
  url: z.string().optional().describe("URL to open (required for 'open' action)."),
644
+ app: z.string().optional().describe("Optional Electron app name configured in config.json under 'electron'."),
614
645
  tabId: z.string().optional().describe("Target Tab ID (required for 'switch' or 'close')."),
615
646
  tabIds: z.array(z.string()).optional().describe("Multiple Tab IDs for batch 'close' action."),
616
647
  force: z.boolean().optional().describe("Force close protected (anchor) tabs. Use with extreme caution."),
617
648
  windowId: z.number().optional().describe("Window ID to focus (required for 'focus_window')."),
618
649
  reuse: z.boolean().optional().describe("Default true. When opening a URL already loaded in another tab, focus that tab instead of opening a duplicate."),
619
- }, async ({ action, url, tabId, tabIds, force, windowId, reuse }) => {
650
+ }, async ({ action, url, app, tabId, tabIds, force, windowId, reuse }) => {
620
651
  try {
621
652
  return await withSecurity(config, "browser_tabs", async (_, liveConfig) => {
622
653
  if (action === "list")
623
- return await WeavetabList(liveConfig);
654
+ return await WeavetabList(liveConfig, app);
624
655
  if (action === "open") {
625
656
  if (!url)
626
657
  throw new Error("url is required for action: open");
627
- return await WeavetabOpen(url, liveConfig, reuse !== false);
658
+ return await WeavetabOpen(url, liveConfig, reuse !== false, app);
628
659
  }
629
660
  if (action === "switch") {
630
661
  if (!tabId)
631
662
  throw new Error("tabId is required for action: switch");
632
- return await WeavetabSwitch(tabId, liveConfig);
663
+ return await WeavetabSwitch(tabId, liveConfig, app);
633
664
  }
634
665
  if (action === "close") {
635
666
  if (tabIds && tabIds.length > 0)
636
- return await WeavetabCloseBatch(tabIds, liveConfig, force);
667
+ return await WeavetabCloseBatch(tabIds, liveConfig, force, app);
637
668
  if (!tabId)
638
669
  throw new Error("tabId or tabIds is required for action: close");
639
- return await WeavetabClose(tabId, liveConfig, force);
670
+ return await WeavetabClose(tabId, liveConfig, force, app);
640
671
  }
641
672
  if (action === "list_windows") {
642
- return await weaveWindowList(liveConfig);
673
+ return await weaveWindowList(liveConfig, app);
643
674
  }
644
675
  if (action === "focus_window") {
645
676
  if (windowId === undefined)
646
677
  throw new Error("windowId is required for action: focus_window");
647
- return await weaveFocusWindow(windowId, liveConfig);
678
+ return await weaveFocusWindow(windowId, liveConfig, app);
648
679
  }
649
680
  if (action === "consolidate") {
650
- return await WeavetabConsolidate(liveConfig);
681
+ return await WeavetabConsolidate(liveConfig, app);
651
682
  }
652
683
  if (action === "heal") {
653
- return await WeavetabHeal(liveConfig);
684
+ return await WeavetabHeal(liveConfig, app);
654
685
  }
655
686
  throw new Error("Invalid action");
656
- });
687
+ }, false, app);
657
688
  }
658
689
  catch (e) {
659
690
  return wrapError(e);
@@ -665,17 +696,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
665
696
  value: z.string().optional().describe("CSS selector, text string, or URL pattern. Not needed for 'stable'/'network'."),
666
697
  timeout: z.number().optional().describe("Max wait in ms (default 8000)."),
667
698
  tab_id: z.string().optional().describe("Execute in a specific tab instead of active."),
699
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
668
700
  }, async (args) => {
669
701
  try {
670
702
  return await withSecurity(config, "browser_wait", async (session, liveConfig) => {
671
703
  let targetSession = session;
672
704
  if (args.tab_id) {
673
705
  const { connectToTab } = await import("./cdp/connector.js");
674
- const client = await connectToTab(args.tab_id);
706
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
675
707
  targetSession = client.session;
676
708
  }
677
709
  return await browserWait(targetSession, args, liveConfig);
678
- });
710
+ }, false, args.app);
679
711
  }
680
712
  catch (e) {
681
713
  return wrapError(e);
@@ -701,8 +733,20 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
701
733
  message: z.string().describe("The question to ask, or the feedback/analysis to log."),
702
734
  input: z.enum(["sentence", "paragraph"]).optional().describe("Input field type for question mode (default: sentence)."),
703
735
  timeout: z.number().optional().describe("Timeout in seconds for question mode before auto-cancel (default: 120)."),
736
+ app: z.string().optional().describe("Optional Electron app name. (Note: thought HUD overlay is browser-only)."),
704
737
  }, async (args) => {
705
738
  try {
739
+ const liveConfig = getCachedConfig();
740
+ const active = sessionRegistry.getActive(liveConfig.agentId, args.app);
741
+ if (args.app || active?.isElectron) {
742
+ const { logActionStructured } = await import("./audit/logger.js");
743
+ logActionStructured({
744
+ timestamp: new Date().toISOString(),
745
+ tool: "browser_thoughts",
746
+ result: `[ELECTRON APP FEEDBACK]: ${args.message}`
747
+ });
748
+ return { content: [{ type: "text", text: "[Note] Thought/HUD overlay is disabled for Electron desktop apps (DOM-only execution). Feedback recorded in audit log." }] };
749
+ }
706
750
  return await withSecurity(config, "browser_thoughts", async (session) => {
707
751
  if (args.action === "feedback") {
708
752
  const { logActionStructured } = await import("./audit/logger.js");
@@ -738,7 +782,8 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
738
782
  end_x: z.number().optional().describe("Destination X coordinate (only for drag_and_drop)."),
739
783
  end_y: z.number().optional().describe("Destination Y coordinate (only for drag_and_drop)."),
740
784
  waitForChild: z.string().optional().describe("CSS Selector of a child element to wait for (only for hover)."),
741
- holdMs: z.number().optional().describe("Milliseconds to hold the pointer down (only for hover).")
785
+ holdMs: z.number().optional().describe("Milliseconds to hold the pointer down (only for hover)."),
786
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
742
787
  }, async (args) => {
743
788
  try {
744
789
  return await withSecurity(config, "browser_pointer", async (session, liveConfig) => {
@@ -756,7 +801,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
756
801
  if (args.end_target && !normalizedArgs.end_ref)
757
802
  normalizedArgs.end_ref = args.end_target;
758
803
  return await browserPointer(session, normalizedArgs, liveConfig);
759
- });
804
+ }, false, args.app);
760
805
  }
761
806
  catch (e) {
762
807
  return wrapError(e);
@@ -767,11 +812,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
767
812
  nodeId: z.number().optional().describe("CDP backendNodeId."),
768
813
  selector: z.string().optional().describe("CSS Selector."),
769
814
  value: z.string().describe("The exact text value to select."),
815
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
770
816
  }, async (args) => {
771
817
  try {
772
818
  return await withSecurity(config, "browser_select", async (session, liveConfig) => {
773
819
  return await browserSelect(session, args, liveConfig);
774
- });
820
+ }, false, args.app);
775
821
  }
776
822
  catch (e) {
777
823
  return wrapError(e);
@@ -784,33 +830,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
784
830
  value: z.string().describe("Value to fill/select/type into the field."),
785
831
  })).describe("Array of field ref+value pairs to fill sequentially."),
786
832
  submit: z.string().optional().describe("Optional ref ID (w:NN) of a submit button to click after filling all fields."),
833
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
787
834
  }, async (args) => {
788
835
  try {
789
836
  return await withSecurity(config, "browser_fill", async (session, liveConfig) => {
790
837
  return await agentAction(session, 'typing', () => browserFill(session, args, liveConfig), 'browser_fill');
791
- });
792
- }
793
- catch (e) {
794
- return wrapError(e);
795
- }
796
- });
797
- // ── 15. browser_plan ───────────────────────────────────────────────────────
798
- server.tool("browser_plan", "Create a live task panel in the browser.", {
799
- action: z.enum(["create", "update", "comments", "hide"]).describe("Action to perform."),
800
- goal: z.string().optional().describe("Mission goal description (required for create)."),
801
- tasks: z.array(z.object({
802
- id: z.number(),
803
- description: z.string(),
804
- status: z.enum(["pending", "in-progress", "done", "failed"]),
805
- })).optional().describe("Task list with statuses (required for create)."),
806
- taskId: z.number().optional().describe("Task ID to update (required for update)."),
807
- status: z.enum(["pending", "in-progress", "done", "failed"]).optional().describe("New status (required for update)."),
808
- }, async (args) => {
809
- try {
810
- return await withSecurity(config, "browser_plan", async (session, liveConfig) => {
811
- const result = await weavePlan(session, args, liveConfig);
812
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
813
- });
838
+ }, false, args.app);
814
839
  }
815
840
  catch (e) {
816
841
  return wrapError(e);
@@ -823,7 +848,8 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
823
848
  ref: z.string().optional().describe("Alternative element Ref ID (e.g. 'w:42')."),
824
849
  target: z.string().optional().describe("Alternative element identifier, CSS Selector, or query."),
825
850
  nodeId: z.number().optional().describe("CDP backendNodeId to highlight directly."),
826
- color: z.string().optional().describe("CSS color to use for the overlay (e.g. 'rgba(255,0,0,0.5)').")
851
+ color: z.string().optional().describe("CSS color to use for the overlay (e.g. 'rgba(255,0,0,0.5)')."),
852
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
827
853
  }, async (args) => {
828
854
  try {
829
855
  return await withSecurity(config, "browser_highlight", async (session, liveConfig) => {
@@ -841,7 +867,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
841
867
  delete normalizedArgs.ref;
842
868
  delete normalizedArgs.target;
843
869
  return await browserHighlight(session, normalizedArgs, liveConfig);
844
- });
870
+ }, false, args.app);
845
871
  }
846
872
  catch (e) {
847
873
  return wrapError(e);
@@ -850,12 +876,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
850
876
  // ── 20. browser_upload ──────────────────────────────────────────────────────
851
877
  server.tool("browser_upload", "Upload files to a file input.", {
852
878
  nodeId: z.number().describe("CDP backendNodeId of the target <input type='file'> element."),
853
- filePaths: z.array(z.string()).describe("List of absolute system file paths to upload.")
879
+ filePaths: z.array(z.string()).describe("List of absolute system file paths to upload."),
880
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
854
881
  }, async (args) => {
855
882
  try {
856
883
  return await withSecurity(config, "browser_upload", async (session, liveConfig) => {
857
884
  return await browserUpload(session, args, liveConfig);
858
- });
885
+ }, false, args.app);
859
886
  }
860
887
  catch (e) {
861
888
  return wrapError(e);
@@ -863,12 +890,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
863
890
  });
864
891
  // ── 22. browser_find ────────────────────────────────────────────────────────
865
892
  server.tool("browser_find", "Sub-5ms CDP text search. Zero token waste. Best for locating specific keywords/text on page without a full DOM walk. No refs returned (use browser_map for refs). No shadow roots/regex.", {
866
- query: z.string().describe("Exact text query to find on the page.")
893
+ query: z.string().describe("Exact text query to find on the page."),
894
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
867
895
  }, async (args) => {
868
896
  try {
869
897
  return await withSecurity(config, "browser_find", async (session, liveConfig) => {
870
898
  return await agentAction(session, 'reading', () => browserFind(session, args, liveConfig), 'browser_find');
871
- });
899
+ }, false, args.app);
872
900
  }
873
901
  catch (e) {
874
902
  return wrapError(e);
@@ -880,11 +908,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
880
908
  action: z.enum(["get", "set", "clear"]).describe("Action to perform on the storage."),
881
909
  key: z.string().optional().describe("Key name (required for 'get' and 'set' actions)."),
882
910
  value: z.string().optional().describe("String value (required for 'set' action)."),
911
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
883
912
  }, async (args) => {
884
913
  try {
885
914
  return await withSecurity(config, "browser_storage", async (session, liveConfig) => {
886
915
  return await browserStorage(session, args, liveConfig);
887
- });
916
+ }, false, args.app);
888
917
  }
889
918
  catch (e) {
890
919
  return wrapError(e);
@@ -896,11 +925,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
896
925
  pattern: z.string().optional().describe("URL glob pattern to intercept or filter telemetry (e.g. '*api/login*')."),
897
926
  mockStatus: z.number().optional().describe("HTTP status code to return when enabling mock (default 200)."),
898
927
  mockBody: z.string().optional().describe("JSON or plaintext body payload to return in the mocked response."),
928
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
899
929
  }, async (args) => {
900
930
  try {
901
931
  return await withSecurity(config, "browser_network_intercept", async (session, liveConfig) => {
902
932
  return await browserNetworkIntercept(session, args, liveConfig);
903
- });
933
+ }, false, args.app);
904
934
  }
905
935
  catch (e) {
906
936
  return wrapError(e);
@@ -908,17 +938,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
908
938
  });
909
939
  // ── 25. browser_console ─────────────────────────────────────────────────────
910
940
  server.tool("browser_console", "1-5ms latency. Read or clear buffered console API messages, JS exceptions, and network errors.", {
911
- action: z.enum(["read", "clear"]).describe("Action to perform on the console buffer.")
941
+ action: z.enum(["read", "clear"]).describe("Action to perform on the console buffer."),
942
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
912
943
  }, async (args) => {
913
944
  try {
914
945
  return await withSecurity(config, "browser_console", async (session, liveConfig) => {
915
946
  let targetId = "default";
916
947
  const { sessionRegistry } = await import("./state/session.js");
917
- const active = sessionRegistry.getActive(liveConfig.agentId);
948
+ const active = sessionRegistry.getActive(liveConfig.agentId, args.app);
918
949
  if (active)
919
950
  targetId = active.targetId;
920
951
  return await browserConsole(session, args, liveConfig, targetId);
921
- });
952
+ }, false, args.app);
922
953
  }
923
954
  catch (e) {
924
955
  return wrapError(e);
@@ -930,11 +961,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
930
961
  landscape: z.boolean().optional().describe("Print in landscape orientation (default false)."),
931
962
  scale: z.number().optional().describe("Scale factor (default 1.0)."),
932
963
  pageRanges: z.string().optional().describe("Page ranges to print (e.g. '1-5'). Leave empty for all pages."),
964
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
933
965
  }, async (args) => {
934
966
  try {
935
967
  return await withSecurity(config, "browser_pdf", async (session, liveConfig) => {
936
968
  return await browserPdf(session, args, liveConfig);
937
- });
969
+ }, false, args.app);
938
970
  }
939
971
  catch (e) {
940
972
  return wrapError(e);
@@ -943,12 +975,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
943
975
  // ── 27. browser_eval ────────────────────────────────────────────────────────
944
976
  server.tool("browser_eval", "Run arbitrary JS via CDP. Good for extracting app state. Cannot traverse shadow DOM/cross-origin iframes.", {
945
977
  expression: z.string().describe("JavaScript code to evaluate in the context of the page."),
946
- timeout: z.number().optional().describe("Timeout in ms (default 5000).")
978
+ timeout: z.number().optional().describe("Timeout in ms (default 5000)."),
979
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
947
980
  }, async (args) => {
948
981
  try {
949
982
  return await withSecurity(config, "browser_eval", async (session, liveConfig) => {
950
983
  return await agentAction(session, 'reading', () => browserEval(session, args, liveConfig), 'browser_eval');
951
- });
984
+ }, false, args.app);
952
985
  }
953
986
  catch (e) {
954
987
  return wrapError(e);
@@ -962,11 +995,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
962
995
  domain: z.string().optional().describe("Domain for the cookie."),
963
996
  path: z.string().optional().describe("Path for the cookie."),
964
997
  url: z.string().optional().describe("URL scope for 'get' action (defaults to current page)."),
998
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
965
999
  }, async (args) => {
966
1000
  try {
967
1001
  return await withSecurity(config, "browser_cookies", async (session, liveConfig) => {
968
1002
  return await browserCookies(session, args, liveConfig);
969
- });
1003
+ }, false, args.app);
970
1004
  }
971
1005
  catch (e) {
972
1006
  return wrapError(e);
@@ -980,12 +1014,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
980
1014
  label: z.string().optional().describe("Visible text or aria-label of the canvas element."),
981
1015
  ref: z.string().optional().describe("Alternative ref identifier for the canvas element."),
982
1016
  backendNodeId: z.number().optional().describe("Raw CDP backendNodeId for direct targeting."),
983
- points: z.array(z.object({ x: z.number(), y: z.number() })).optional().describe("Array of {x,y} points relative to canvas viewport for 'draw' action. Simulates a drag path across the canvas.")
1017
+ points: z.array(z.object({ x: z.number(), y: z.number() })).optional().describe("Array of {x,y} points relative to canvas viewport for 'draw' action. Simulates a drag path across the canvas."),
1018
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
984
1019
  }, async (args) => {
985
1020
  try {
986
1021
  return await withSecurity(config, "browser_canvas", async (session, liveConfig) => {
987
1022
  return await browserCanvas(session, args, liveConfig);
988
- });
1023
+ }, false, args.app);
989
1024
  }
990
1025
  catch (e) {
991
1026
  return wrapError(e);
@@ -996,12 +1031,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
996
1031
  query: z.string().describe("CSS Selector or XPath query to extract."),
997
1032
  type: z.enum(["css", "xpath"]).optional().describe("Query type (default 'css')."),
998
1033
  attributes: z.array(z.string()).optional().describe("Attributes to extract (e.g. ['textContent', 'href'], default ['textContent'])."),
999
- multiple: z.boolean().optional().describe("Whether to extract multiple elements (default true).")
1034
+ multiple: z.boolean().optional().describe("Whether to extract multiple elements (default true)."),
1035
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1000
1036
  }, async (args) => {
1001
1037
  try {
1002
1038
  return await withSecurity(config, "browser_scrape", async (session, liveConfig) => {
1003
1039
  return await agentAction(session, 'reading', () => browserScrape(session, args, liveConfig), 'browser_scrape');
1004
- });
1040
+ }, false, args.app);
1005
1041
  }
1006
1042
  catch (e) {
1007
1043
  return wrapError(e);
@@ -1010,12 +1046,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1010
1046
  // ── 32. browser_snapshot ────────────────────────────────────────────────────
1011
1047
  server.tool("browser_snapshot", "Capture or restore an MHTML snapshot. Best for offline archival, replay testing, and compliance audits.", {
1012
1048
  action: z.enum(["capture", "restore"]).describe("Action to perform."),
1013
- filePath: z.string().describe("Absolute file path to save/load the .mhtml snapshot.")
1049
+ filePath: z.string().describe("Absolute file path to save/load the .mhtml snapshot."),
1050
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1014
1051
  }, async (args) => {
1015
1052
  try {
1016
1053
  return await withSecurity(config, "browser_snapshot", async (session, liveConfig) => {
1017
1054
  return await browserSnapshot(session, args, liveConfig);
1018
- });
1055
+ }, false, args.app);
1019
1056
  }
1020
1057
  catch (e) {
1021
1058
  return wrapError(e);
@@ -1031,12 +1068,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1031
1068
  y: z.number().describe("Y coordinate of clip region."),
1032
1069
  width: z.number().describe("Width of clip region."),
1033
1070
  height: z.number().describe("Height of clip region.")
1034
- }).optional().describe("Clip region. Use to capture only part of the page (e.g. just a chart area) to reduce token cost.")
1071
+ }).optional().describe("Clip region. Use to capture only part of the page (e.g. just a chart area) to reduce token cost."),
1072
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1035
1073
  }, async (args) => {
1036
1074
  try {
1037
1075
  return await withSecurity(config, "browser_screenshot", async (session, liveConfig) => {
1038
1076
  return await agentAction(session, 'reading', () => browserScreenshot(session, args, liveConfig), 'browser_screenshot');
1039
- });
1077
+ }, false, args.app);
1040
1078
  }
1041
1079
  catch (e) {
1042
1080
  return wrapError(e);
@@ -1048,12 +1086,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1048
1086
  width: z.number().optional().describe("Viewport width in pixels."),
1049
1087
  height: z.number().optional().describe("Viewport height in pixels."),
1050
1088
  deviceScaleFactor: z.number().optional().describe("Device Pixel Ratio (default 1)."),
1051
- mobile: z.boolean().optional().describe("Whether to emulate a mobile device (default false).")
1089
+ mobile: z.boolean().optional().describe("Whether to emulate a mobile device (default false)."),
1090
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1052
1091
  }, async (args) => {
1053
1092
  try {
1054
1093
  return await withSecurity(config, "browser_viewport", async (session, liveConfig) => {
1055
1094
  return await browserViewport(session, args, liveConfig);
1056
- });
1095
+ }, false, args.app);
1057
1096
  }
1058
1097
  catch (e) {
1059
1098
  return wrapError(e);
@@ -1064,12 +1103,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1064
1103
  action: z.enum(["read", "write"]).describe("Action to perform."),
1065
1104
  text: z.string().optional().describe("Text to write to clipboard (for write action)."),
1066
1105
  dataUrl: z.string().optional().describe("Base64 data URL of an image to write to clipboard (e.g. 'data:image/png;base64,...')."),
1067
- mimeType: z.string().optional().describe("MIME type override for image write (auto-detected from dataUrl by default, e.g. 'image/png').")
1106
+ mimeType: z.string().optional().describe("MIME type override for image write (auto-detected from dataUrl by default, e.g. 'image/png')."),
1107
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1068
1108
  }, async (args) => {
1069
1109
  try {
1070
1110
  return await withSecurity(config, "browser_clipboard", async (session, liveConfig) => {
1071
1111
  return await browserClipboard(session, args, liveConfig);
1072
- });
1112
+ }, false, args.app);
1073
1113
  }
1074
1114
  catch (e) {
1075
1115
  return wrapError(e);
@@ -1079,12 +1119,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1079
1119
  server.tool("browser_inspect", "15-35ms latency, zero waste. Deep DOM inspection. Crucial for debugging why a click failed, verifying z-index & visibility, or computed styles.", {
1080
1120
  nodeId: z.number().optional().describe("CDP backendNodeId."),
1081
1121
  selector: z.string().optional().describe("CSS Selector."),
1082
- ref: z.string().optional().describe("Ref ID (e.g. 'w:42').")
1122
+ ref: z.string().optional().describe("Ref ID (e.g. 'w:42')."),
1123
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1083
1124
  }, async (args) => {
1084
1125
  try {
1085
1126
  return await withSecurity(config, "browser_inspect", async (session, liveConfig) => {
1086
1127
  return await agentAction(session, 'reading', () => browserInspect(session, args, liveConfig), 'browser_inspect');
1087
- });
1128
+ }, false, args.app);
1088
1129
  }
1089
1130
  catch (e) {
1090
1131
  return wrapError(e);
@@ -1097,12 +1138,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1097
1138
  outputDir: z.string().optional().describe("Optional custom directory to write extracted files into. If omitted, uses configured designDir (~/.weavetab/designs/<domain>-<timestamp>/)."),
1098
1139
  name: z.string().optional().describe("Optional custom name for the saved design folder (e.g. 'resend-hero', 'pricing-section')."),
1099
1140
  includeAssets: z.boolean().optional().describe("Whether to capture inline SVGs and asset references (default true)."),
1100
- hideOverlay: z.boolean().optional().describe("Whether to hide Weavetab overlay during capture (default true).")
1141
+ hideOverlay: z.boolean().optional().describe("Whether to hide Weavetab overlay during capture (default true)."),
1142
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1101
1143
  }, async (args) => {
1102
1144
  try {
1103
1145
  return await withSecurity(config, "browser_extract_design", async (session, liveConfig) => {
1104
1146
  return await agentAction(session, 'reading', () => browserExtractDesign(session, args, liveConfig), 'browser_extract_design');
1105
- });
1147
+ }, false, args.app);
1106
1148
  }
1107
1149
  catch (e) {
1108
1150
  return wrapError(e);
@@ -1115,12 +1157,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1115
1157
  script: z.string().optional().describe("JavaScript code to inject and run (required for start)."),
1116
1158
  persistent: z.boolean().optional().describe("If true, the script re-runs in a loop (default false)."),
1117
1159
  onResult: z.enum(["notify_agent", "store", "ignore"]).optional().describe("How to handle script results (default 'store')."),
1160
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1118
1161
  }, async (args) => {
1119
1162
  try {
1120
1163
  return await withSecurity(config, "browser_automation", async (session, liveConfig) => {
1121
1164
  const result = await browserAutomation(session, args, liveConfig);
1122
1165
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1123
- });
1166
+ }, false, args.app);
1124
1167
  }
1125
1168
  catch (e) {
1126
1169
  return wrapError(e);
@@ -1134,12 +1177,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1134
1177
  maxFrames: z.number().optional().describe("Maximum frames to capture (default 500)."),
1135
1178
  format: z.enum(["webp", "gif", "webm", "mp4", "frames"]).optional().describe("Output video format when stopping recording. Default 'webp' (pure Node native compilation)."),
1136
1179
  keepFrames: z.boolean().optional().describe("If true, keeps raw JPEG frames after video compilation (default false)."),
1180
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1137
1181
  }, async (args) => {
1138
1182
  try {
1139
1183
  return await withSecurity(config, "browser_recording", async (session, liveConfig) => {
1140
1184
  const result = await browserRecording(session, args, liveConfig);
1141
1185
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1142
- });
1186
+ }, false, args.app);
1143
1187
  }
1144
1188
  catch (e) {
1145
1189
  return wrapError(e);
@@ -1163,12 +1207,14 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1163
1207
  }
1164
1208
  });
1165
1209
  // ── browser_performance ───────────────────────────────────────────────────
1166
- server.tool("browser_performance", "Retrieve page performance metrics.", {}, async () => {
1210
+ server.tool("browser_performance", "Retrieve page performance metrics.", {
1211
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1212
+ }, async (args) => {
1167
1213
  try {
1168
1214
  return await withSecurity(config, "browser_performance", async (session, liveConfig) => {
1169
1215
  const result = await browserPerformance(session, liveConfig);
1170
1216
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1171
- });
1217
+ }, false, args.app);
1172
1218
  }
1173
1219
  catch (e) {
1174
1220
  return wrapError(e);
@@ -1309,11 +1355,12 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1309
1355
  }).optional().describe("Override geolocation coordinates"),
1310
1356
  timezoneId: z.string().optional().describe("Override browser timezone (e.g. 'America/New_York')"),
1311
1357
  userAgent: z.string().optional().describe("Override User Agent string"),
1358
+ app: z.string().optional().describe("Electron app name configured in config.json under 'electron'."),
1312
1359
  }, async (args) => {
1313
1360
  try {
1314
1361
  return await withSecurity(config, "browser_emulate", async (session, liveConfig) => {
1315
1362
  return await browserEmulate(session, args, liveConfig);
1316
- });
1363
+ }, false, args.app);
1317
1364
  }
1318
1365
  catch (e) {
1319
1366
  return wrapError(e);
@@ -1433,14 +1480,13 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1433
1480
  }]
1434
1481
  };
1435
1482
  });
1436
- // ── Plugins Lifecycle (Add / Override / Remove) ───────────────────────────
1437
- const plugins = discoverPlugins();
1483
+ // ── Plugins Lifecycle & Dynamic Registry ───────────────────────────
1438
1484
  const { getVerifiedPackages, resolvePluginTrust } = await import("./plugin/registry.js");
1439
1485
  const verifiedPackages = await getVerifiedPackages();
1440
- let pluginAddedCount = 0;
1441
- let pluginOverrideCount = 0;
1442
- let pluginRemovedCount = 0;
1443
- for (const plugin of plugins) {
1486
+ const loadedPluginNames = new Set();
1487
+ const registerPluginTools = async (plugin) => {
1488
+ if (loadedPluginNames.has(plugin.name))
1489
+ return;
1444
1490
  const trust = resolvePluginTrust(plugin.name, verifiedPackages);
1445
1491
  const isOfficial = trust.tier === "official";
1446
1492
  const addedNames = [];
@@ -1455,7 +1501,6 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1455
1501
  if (server._registeredTools?.[toolName]) {
1456
1502
  delete server._registeredTools[toolName];
1457
1503
  removedNames.push(toolName);
1458
- pluginRemovedCount++;
1459
1504
  }
1460
1505
  }
1461
1506
  // 2. Tool Overrides (replace or wrap built-in tools)
@@ -1485,7 +1530,6 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1485
1530
  }
1486
1531
  });
1487
1532
  overriddenNames.push(ov.name);
1488
- pluginOverrideCount++;
1489
1533
  }
1490
1534
  // 3. Tool Additions (new tools)
1491
1535
  const newTools = plugin.tools || [];
@@ -1493,7 +1537,7 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1493
1537
  if (server._registeredTools?.[td.name]) {
1494
1538
  delete server._registeredTools[td.name];
1495
1539
  }
1496
- server.tool(td.name, td.description, td.schema, async (args) => {
1540
+ server.tool(td.name, td.description, td.schema || {}, async (args) => {
1497
1541
  try {
1498
1542
  return await withSecurity(config, td.name, async (session, liveConfig) => {
1499
1543
  const result = await td.handler(args, session, liveConfig);
@@ -1505,45 +1549,69 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1505
1549
  }
1506
1550
  });
1507
1551
  addedNames.push(td.name);
1508
- pluginAddedCount++;
1509
1552
  }
1510
- // Execute plugin onLoad hook if present
1553
+ // Pass the SDK context so the plugin can dynamically register tools itself
1554
+ const pluginCtx = {
1555
+ mcp: {
1556
+ registerTool: (tool) => {
1557
+ server.tool(tool.name, tool.description, tool.schema, async (args) => {
1558
+ return await tool.handler(args);
1559
+ });
1560
+ }
1561
+ }
1562
+ };
1511
1563
  if (typeof plugin.onLoad === "function") {
1512
1564
  try {
1513
- await plugin.onLoad();
1565
+ await plugin.onLoad(pluginCtx);
1514
1566
  }
1515
1567
  catch (err) {
1516
1568
  process.stderr.write(` \u001b[31m[!] Plugin ${plugin.name} onLoad failed: ${err?.message}\u001b[0m\n`);
1517
1569
  }
1518
1570
  }
1519
- // Build permission and operations warning string
1520
- const ops = [];
1521
- if (addedNames.length > 0)
1522
- ops.push(`+${addedNames.length} added`);
1523
- if (overriddenNames.length > 0)
1524
- ops.push(`⚡overrode [${overriddenNames.join(", ")}]`);
1525
- if (removedNames.length > 0)
1526
- ops.push(`✖removed [${removedNames.join(", ")}]`);
1527
- const opsStr = ops.length > 0 ? ` (${ops.join(", ")})` : "";
1528
- let permWarning = "";
1529
- if (trust.tier === "unverified" && plugin.manifest?.permissions) {
1530
- const perms = plugin.manifest.permissions;
1531
- const flags = [];
1532
- if (perms.network)
1533
- flags.push("network");
1534
- if (perms.cdp)
1535
- flags.push("cdp");
1536
- if (perms.fs)
1537
- flags.push("fs");
1538
- if (perms.tools?.override?.length)
1539
- flags.push(`override:${perms.tools.override.join(",")}`);
1540
- if (perms.tools?.remove?.length)
1541
- flags.push(`remove:${perms.tools.remove.join(",")}`);
1542
- if (flags.length > 0) {
1543
- permWarning = ` \u001b[33m— requests: ${flags.join(", ")}\u001b[0m`;
1571
+ loadedPluginNames.add(plugin.name);
1572
+ process.stderr.write(` [Plugin Loaded] ${plugin.name} v${plugin.version}\n`);
1573
+ // Notify the client that tools have changed
1574
+ try {
1575
+ if (server.server) {
1576
+ server.server.sendToolListChanged();
1544
1577
  }
1545
1578
  }
1546
- process.stderr.write(` ${trust.badge} ${plugin.name} v${plugin.version}${opsStr}${permWarning}\n`);
1579
+ catch (e) {
1580
+ // Ignore if client doesn't support it or not connected yet
1581
+ }
1582
+ };
1583
+ // Add the built-in ecosystem tools
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.
1608
+ const allPlugins = discoverPlugins();
1609
+ for (const plugin of allPlugins) {
1610
+ const trust = resolvePluginTrust(plugin.name, verifiedPackages);
1611
+ const isLazy = plugin.manifest?.environments?.includes("mcp");
1612
+ if (trust.tier === "official" || !isLazy) {
1613
+ await registerPluginTools(plugin);
1614
+ }
1547
1615
  }
1548
1616
  let transport;
1549
1617
  if (transportMode === "ws") {
@@ -1678,10 +1746,8 @@ process.on("SIGTERM", async () => {
1678
1746
  process.exit(0);
1679
1747
  });
1680
1748
  process.on("exit", () => {
1681
- if (global.__wt_is_server)
1682
- return;
1683
- try {
1684
- clearLock();
1685
- }
1686
- catch { /* ignore */ }
1749
+ // We purposefully do NOT clear the lock file here.
1750
+ // Weavetab launches the browser detached, so it survives the server dying.
1751
+ // Leaving the lock file allows the next MCP server run to find and reuse
1752
+ // the existing browser instead of launching a duplicate.
1687
1753
  });