@weavetab/mcp 2.5.0-beta.1 → 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 (89) 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 +284 -78
  8. package/dist/cdp/connector.d.ts +5 -3
  9. package/dist/cdp/connector.js +188 -71
  10. package/dist/cdp/lock.d.ts +9 -6
  11. package/dist/cdp/lock.js +72 -59
  12. package/dist/cdp/omni_broker.d.ts +1 -0
  13. package/dist/cdp/omni_broker.js +12 -6
  14. package/dist/cdp/profiles.d.ts +10 -0
  15. package/dist/cdp/profiles.js +65 -0
  16. package/dist/cdp/walker.js +50 -0
  17. package/dist/cli/plugin.d.ts +0 -2
  18. package/dist/cli/plugin.js +31 -683
  19. package/dist/cli/updater.js +2 -2
  20. package/dist/config/loader.d.ts +25 -6
  21. package/dist/config/loader.js +133 -20
  22. package/dist/index.js +48 -52
  23. package/dist/overlay/ghost.js +2 -2
  24. package/dist/overlay/injector.js +1 -3
  25. package/dist/plugin/extension_bridge.d.ts +40 -0
  26. package/dist/plugin/extension_bridge.js +158 -0
  27. package/dist/plugin/interface.d.ts +78 -16
  28. package/dist/plugin/loader.d.ts +4 -0
  29. package/dist/plugin/loader.js +178 -57
  30. package/dist/plugin/registry.js +11 -1
  31. package/dist/plugin/storage.d.ts +51 -0
  32. package/dist/plugin/storage.js +164 -0
  33. package/dist/server.js +213 -121
  34. package/dist/state/session.d.ts +2 -2
  35. package/dist/state/session.js +33 -13
  36. package/dist/tools/browser_detect.d.ts +2 -1
  37. package/dist/tools/browser_detect.js +6 -3
  38. package/dist/tools/burst.d.ts +1 -0
  39. package/dist/tools/burst.js +4 -6
  40. package/dist/tools/click.js +23 -28
  41. package/dist/tools/clipboard.js +31 -24
  42. package/dist/tools/navigate.d.ts +1 -1
  43. package/dist/tools/navigate.js +28 -10
  44. package/dist/tools/tabs.d.ts +10 -10
  45. package/dist/tools/tabs.js +47 -39
  46. package/dist/tools/type.js +2 -2
  47. package/docs/DOCUMENTATION.md +63 -6
  48. package/docs/architecture/blind-injection.md +20 -0
  49. package/docs/architecture/dual-injection.md +32 -0
  50. package/docs/architecture/overview.md +49 -0
  51. package/docs/architecture/security.md +35 -0
  52. package/docs/architecture/transport.md +37 -0
  53. package/docs/captcha-stealth/captcha-solver.md +75 -0
  54. package/docs/captcha-stealth/stealth-physics.md +60 -0
  55. package/docs/cli/cli-commands.md +44 -0
  56. package/docs/cli/cli-flags.md +20 -0
  57. package/docs/cli/cli-introduction.md +20 -0
  58. package/docs/cli/cli-scripting.md +32 -0
  59. package/docs/cli/g-ide/ide-antigravity.md +24 -0
  60. package/docs/cli/g-ide/ide-claude-code.md +24 -0
  61. package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
  62. package/docs/cli/g-ide/ide-cursor.md +31 -0
  63. package/docs/cli/g-ide/ide-generic.md +34 -0
  64. package/docs/cli/g-ide/ide-opencode.md +24 -0
  65. package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
  66. package/docs/cli/g-ide/ide-windsurf.md +24 -0
  67. package/docs/configuration/config-locations.md +24 -0
  68. package/docs/configuration/configuration.md +73 -0
  69. package/docs/configuration/env-variables.md +19 -0
  70. package/docs/configuration/folders.md +54 -0
  71. package/docs/desktop-automation/electron-apps.md +162 -0
  72. package/docs/get-started/installation.md +47 -0
  73. package/docs/get-started/introduction.md +60 -0
  74. package/docs/get-started/quickstart.md +54 -0
  75. package/docs/get-started/setup-wizard.md +26 -0
  76. package/docs/skills-ecosystem/skills-overview.md +35 -0
  77. package/docs/skills-ecosystem/skills-recipes.md +36 -0
  78. package/docs/tools/tools.md +293 -0
  79. package/docs/troubleshooting/trouble-browser.md +27 -0
  80. package/docs/troubleshooting/trouble-config.md +20 -0
  81. package/docs/troubleshooting/trouble-loop.md +31 -0
  82. package/ext-dist/content.js +0 -1
  83. package/mcp.json +8 -6
  84. package/package.json +11 -5
  85. package/server.json +3 -3
  86. package/docs/DEV_MODE.md +0 -68
  87. package/docs/PLUGINS.md +0 -538
  88. package/docs/TOOLS.md +0 -104
  89. package/docs/dev_plugin.md +0 -310
package/dist/server.js CHANGED
@@ -51,7 +51,7 @@ import { browserCaptcha } from "./tools/captcha.js";
51
51
  import { browserEmulate } from "./tools/emulation.js";
52
52
  import { getActiveEngine } from "./cdp/connector.js";
53
53
  import { resetLoopCounter } from "./intelligence/trail.js";
54
- import { discoverPlugins } from "./plugin/loader.js";
54
+ import { discoverPlugins, listInstalledPlugins, loadPluginByName } from "./plugin/loader.js";
55
55
  import { agentAction } from "./sensors/pacing.js";
56
56
  import { getCachedConfig } from "./config/loader.js";
57
57
  import { agentState } from "./state/agent.js";
@@ -104,7 +104,7 @@ function withTimeoutGuard(fn, timeoutMs, label) {
104
104
  });
105
105
  return Promise.race([fn(), timeoutPromise]).finally(() => clearTimeout(timer));
106
106
  }
107
- async function withSecurity(config, toolName, handler, isOfficial = false) {
107
+ async function withSecurity(config, toolName, handler, isOfficial = false, targetApp) {
108
108
  const liveConfig = getCachedConfig();
109
109
  const rl = checkRateLimit(liveConfig);
110
110
  if (!rl.allowed) {
@@ -117,7 +117,7 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
117
117
  throw new Error(`[!] ${roleToolCheck.reason}`);
118
118
  }
119
119
  }
120
- const { session } = await withTimeoutGuard(() => getOrConnect(liveConfig), 15000, `CDP Connection (${toolName})`);
120
+ const { session } = await withTimeoutGuard(() => getOrConnect(liveConfig, targetApp), 15000, `CDP Connection (${toolName}${targetApp ? `:${targetApp}` : ''})`);
121
121
  agentState.setSession(session);
122
122
  global.__wt_live_config = liveConfig;
123
123
  try {
@@ -141,8 +141,8 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
141
141
  }
142
142
  if (!isOfficial) {
143
143
  // Phase 7c: Domain policy check for read-only / deny / audit-only modes
144
- const activeClient = sessionRegistry.getActive(liveConfig.agentId);
145
- if (activeClient?.url) {
144
+ const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
145
+ if (activeClient?.url && !activeClient.isElectron) {
146
146
  const domainCheck = checkDomain(activeClient.url, liveConfig);
147
147
  if (!domainCheck.allowed) {
148
148
  throw new Error(`[!] ${domainCheck.reason}`);
@@ -198,7 +198,7 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
198
198
  const result = await withTimeoutGuard(() => handler(session, liveConfig), 22000, toolName);
199
199
  agentState.reportToolSuccess();
200
200
  const ms = Date.now() - t0;
201
- const activeClient = sessionRegistry.getActive(liveConfig.agentId);
201
+ const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
202
202
  logActionStructured({
203
203
  timestamp: new Date(t0).toISOString(),
204
204
  sessionId: activeClient?.targetId,
@@ -322,7 +322,7 @@ async function withSecurity(config, toolName, handler, isOfficial = false) {
322
322
  const ms = Date.now() - t0;
323
323
  const errMsg = err instanceof Error ? err.message : String(err);
324
324
  agentState.reportToolError();
325
- const activeClient = sessionRegistry.getActive(liveConfig.agentId);
325
+ const activeClient = sessionRegistry.getActive(liveConfig.agentId, targetApp);
326
326
  logActionStructured({
327
327
  timestamp: new Date(t0).toISOString(),
328
328
  sessionId: activeClient?.targetId,
@@ -371,6 +371,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
371
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."),
372
372
  ax_tree: z.boolean().optional().describe("Bypass the extension entirely and read raw Chrome Accessibility Tree. Use this if Shadow DOMs hide elements."),
373
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'."),
374
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."),
375
376
  force: z.boolean().optional().describe("Force a full DOM map rebuild even if loop detection says nothing changed. Bypasses the map skip guard."),
376
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."),
@@ -380,12 +381,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
380
381
  let targetSession = session;
381
382
  let targetId = "default";
382
383
  const { sessionRegistry } = await import("./state/session.js");
383
- const active = sessionRegistry.getActive(liveConfig.agentId);
384
+ const active = sessionRegistry.getActive(liveConfig.agentId, args.app);
384
385
  if (active)
385
386
  targetId = active.targetId;
386
387
  if (args.tab_id) {
387
388
  const { connectToTab } = await import("./cdp/connector.js");
388
- const client = await connectToTab(args.tab_id);
389
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
389
390
  targetSession = client.session;
390
391
  targetId = args.tab_id;
391
392
  }
@@ -409,30 +410,37 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
409
410
  }
410
411
  const result = await agentAction(targetSession, 'reading', () => browserMap(targetSession, args, targetId, liveConfig), 'browser_map');
411
412
  return { content: [{ type: "text", text: result }] };
412
- });
413
+ }, false, args.app);
413
414
  }
414
415
  catch (e) {
415
416
  return wrapError(e);
416
417
  }
417
418
  });
418
419
  // ── 2. browser_navigate ────────────────────────────────────────────────────
419
- server.tool("browser_navigate", "Navigate to a URL.", {
420
- 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."),
421
423
  force: z.boolean().optional().describe("If true, overwrites current tab even if it's protected (anchor tab). Otherwise opens a new tab automatically."),
422
424
  tab_id: z.string().optional().describe("Target a specific tab ID for navigation instead of the active tab.")
423
425
  }, async (args) => {
424
426
  try {
427
+ if (!args.url && !args.app) {
428
+ throw new Error("Either 'url' or 'app' must be provided to browser_navigate.");
429
+ }
425
430
  return await withSecurity(config, "browser_navigate", async (session, liveConfig) => {
426
431
  let targetSession = session;
427
432
  let targetId = "default";
428
433
  if (args.tab_id) {
429
434
  const { connectToTab } = await import("./cdp/connector.js");
430
- const client = await connectToTab(args.tab_id);
435
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
431
436
  targetSession = client.session;
432
437
  targetId = args.tab_id;
433
438
  }
434
- return await agentAction(targetSession, 'navigating', () => weaveNavigate(targetSession, args.url, liveConfig, targetId, args.force), 'browser_navigate');
435
- });
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);
436
444
  }
437
445
  catch (e) {
438
446
  return wrapError(e);
@@ -441,6 +449,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
441
449
  // ── 3. browser_click ───────────────────────────────────────────────────────
442
450
  server.tool("browser_click", "Click a DOM element. Use browser_canvas for charts/canvas. If stuck_warning=true, stop and use browser_map.", {
443
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')."),
444
453
  selector: z.string().optional().describe("CSS selector of the element to click."),
445
454
  target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
446
455
  label: z.string().optional().describe("Visible text or aria-label to click."),
@@ -450,22 +459,25 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
450
459
  y: z.number().optional().describe("Raw Y coordinate. Use for precise positional clicks when element ID is not available."),
451
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."),
452
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'."),
453
463
  }, async (args) => {
454
464
  try {
455
465
  return await withSecurity(config, "browser_click", async (session, liveConfig) => {
456
466
  let targetSession = session;
457
467
  if (args.tab_id) {
458
468
  const { connectToTab } = await import("./cdp/connector.js");
459
- const client = await connectToTab(args.tab_id);
469
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
460
470
  targetSession = client.session;
461
471
  }
462
472
  const normalizedArgs = { ...args };
473
+ if (args.ref && !normalizedArgs.id)
474
+ normalizedArgs.id = args.ref;
463
475
  if (args.selector && !normalizedArgs.id)
464
476
  normalizedArgs.id = args.selector;
465
477
  if (args.target && !normalizedArgs.id)
466
478
  normalizedArgs.id = args.target;
467
479
  return await agentAction(targetSession, 'clicking', () => weaveClick(targetSession, normalizedArgs, liveConfig), 'browser_click');
468
- });
480
+ }, false, args.app);
469
481
  }
470
482
  catch (e) {
471
483
  return wrapError(e);
@@ -474,6 +486,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
474
486
  // ── 4. browser_type ────────────────────────────────────────────────────────
475
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.", {
476
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')."),
477
490
  selector: z.string().optional().describe("CSS selector of the input field. Use when you know the exact selector."),
478
491
  target: z.string().optional().describe("CSS selector or query string. Alias for selector."),
479
492
  label: z.string().optional().describe("Field label or placeholder text. Used for fuzzy matching."),
@@ -485,22 +498,25 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
485
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."),
486
499
  include_map: z.boolean().optional().describe("Set true to include a full updated page_map in the response. Default false."),
487
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'."),
488
502
  }, async (args) => {
489
503
  try {
490
504
  return await withSecurity(config, "browser_type", async (session, liveConfig) => {
491
505
  let targetSession = session;
492
506
  if (args.tab_id) {
493
507
  const { connectToTab } = await import("./cdp/connector.js");
494
- const client = await connectToTab(args.tab_id);
508
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
495
509
  targetSession = client.session;
496
510
  }
497
511
  const normalizedArgs = { ...args };
512
+ if (args.ref && !normalizedArgs.id)
513
+ normalizedArgs.id = args.ref;
498
514
  if (args.selector && !normalizedArgs.id)
499
515
  normalizedArgs.id = args.selector;
500
516
  if (args.target && !normalizedArgs.id)
501
517
  normalizedArgs.id = args.target;
502
518
  return await agentAction(targetSession, 'typing', () => weaveType(targetSession, normalizedArgs, liveConfig), 'browser_type');
503
- });
519
+ }, false, args.app);
504
520
  }
505
521
  catch (e) {
506
522
  return wrapError(e);
@@ -509,13 +525,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
509
525
  // ── 4b. browser_type_secret ────────────────────────────────────────────────
510
526
  server.tool("browser_type_secret", "Inject a secret from an environment variable.", {
511
527
  id: z.string().optional().describe("Ref ID from browser_map."),
528
+ ref: z.string().optional().describe("Alias for id (e.g. 'w:42')."),
512
529
  target: z.string().optional().describe("Alternative target input element identifier or CSS selector."),
513
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'."),
514
532
  }, async (args) => {
515
533
  try {
516
534
  return await withSecurity(config, "browser_type_secret", async (session, liveConfig) => {
517
- return await agentAction(session, 'typing', () => weaveTypeSecret(session, args, liveConfig), 'browser_type_secret');
518
- });
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);
519
540
  }
520
541
  catch (e) {
521
542
  return wrapError(e);
@@ -526,18 +547,19 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
526
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."),
527
548
  modifiers: z.array(z.enum(["Alt", "Ctrl", "Meta", "Shift"])).optional().describe("Array of modifier keys to hold during the press."),
528
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'."),
529
551
  }, async (args) => {
530
- const { key, modifiers, tab_id } = args;
552
+ const { key, modifiers, tab_id, app } = args;
531
553
  try {
532
554
  return await withSecurity(config, "browser_key", async (session, liveConfig) => {
533
555
  let targetSession = session;
534
556
  if (tab_id) {
535
557
  const { connectToTab } = await import("./cdp/connector.js");
536
- const client = await connectToTab(tab_id);
558
+ const client = await connectToTab(tab_id, undefined, undefined, undefined, app);
537
559
  targetSession = client.session;
538
560
  }
539
561
  return await weaveKey(targetSession, { key, modifiers }, liveConfig);
540
- });
562
+ }, false, app);
541
563
  }
542
564
  catch (e) {
543
565
  return wrapError(e);
@@ -546,39 +568,48 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
546
568
  // ── 6. browser_scroll ──────────────────────────────────────────────────────
547
569
  server.tool("browser_scroll", "Scroll the page.", {
548
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')."),
549
572
  selector: z.string().optional().describe("CSS Selector of the element to scroll into view."),
550
573
  target: z.string().optional().describe("Alternative element identifier or query to scroll into view."),
551
574
  label: z.string().optional().describe("Element label to scroll into view."),
552
575
  direction: z.enum(["up", "down", "left", "right"]).describe("Scroll direction."),
553
576
  distance: z.number().optional().describe("Pixels to scroll (default 400)."),
554
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'."),
555
579
  }, async (args) => {
556
580
  try {
557
581
  return await withSecurity(config, "browser_scroll", async (session, liveConfig) => {
558
582
  let targetSession = session;
559
583
  if (args.tab_id) {
560
584
  const { connectToTab } = await import("./cdp/connector.js");
561
- const client = await connectToTab(args.tab_id);
585
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
562
586
  targetSession = client.session;
563
587
  }
564
588
  const normalizedArgs = { ...args };
589
+ if (args.ref && !normalizedArgs.id)
590
+ normalizedArgs.id = args.ref;
565
591
  if (args.selector && !normalizedArgs.id)
566
592
  normalizedArgs.id = args.selector;
567
593
  if (args.target && !normalizedArgs.id)
568
594
  normalizedArgs.id = args.target;
569
595
  return await browserScroll(targetSession, normalizedArgs, liveConfig);
570
- });
596
+ }, false, args.app);
571
597
  }
572
598
  catch (e) {
573
599
  return wrapError(e);
574
600
  }
575
601
  });
576
602
  // ── 7. browser_dialog ──────────────────────────────────────────────────────
577
- 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.", {
578
604
  action: z.enum(["accept", "dismiss"]).describe("Action to perform on the dialog."),
579
605
  promptText: z.string().optional().describe("Text to enter if it is a prompt dialog.")
580
606
  }, async (args) => {
581
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
+ }
582
613
  return await withSecurity(config, "browser_dialog", async (session) => {
583
614
  return await browserDialog(session, args);
584
615
  });
@@ -593,65 +624,67 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
593
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"]),
594
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? }.")
595
626
  })).describe("Ordered array of tool calls to execute serially. Use macro steps to save tokens."),
596
- 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'."),
597
629
  }, async (args) => {
598
630
  try {
599
631
  return await withSecurity(config, "browser_burst", async (session, liveConfig) => {
600
632
  const { browserBurst } = await import("./tools/burst.js");
601
633
  return await browserBurst(session, args, liveConfig);
602
- });
634
+ }, false, args.app);
603
635
  }
604
636
  catch (e) {
605
637
  return wrapError(e);
606
638
  }
607
639
  });
608
640
  // ── 9. browser_tabs ───────────────────────────────────────────────────────
609
- 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).", {
610
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."),
611
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'."),
612
645
  tabId: z.string().optional().describe("Target Tab ID (required for 'switch' or 'close')."),
613
646
  tabIds: z.array(z.string()).optional().describe("Multiple Tab IDs for batch 'close' action."),
614
647
  force: z.boolean().optional().describe("Force close protected (anchor) tabs. Use with extreme caution."),
615
648
  windowId: z.number().optional().describe("Window ID to focus (required for 'focus_window')."),
616
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."),
617
- }, async ({ action, url, tabId, tabIds, force, windowId, reuse }) => {
650
+ }, async ({ action, url, app, tabId, tabIds, force, windowId, reuse }) => {
618
651
  try {
619
652
  return await withSecurity(config, "browser_tabs", async (_, liveConfig) => {
620
653
  if (action === "list")
621
- return await WeavetabList(liveConfig);
654
+ return await WeavetabList(liveConfig, app);
622
655
  if (action === "open") {
623
656
  if (!url)
624
657
  throw new Error("url is required for action: open");
625
- return await WeavetabOpen(url, liveConfig, reuse !== false);
658
+ return await WeavetabOpen(url, liveConfig, reuse !== false, app);
626
659
  }
627
660
  if (action === "switch") {
628
661
  if (!tabId)
629
662
  throw new Error("tabId is required for action: switch");
630
- return await WeavetabSwitch(tabId, liveConfig);
663
+ return await WeavetabSwitch(tabId, liveConfig, app);
631
664
  }
632
665
  if (action === "close") {
633
666
  if (tabIds && tabIds.length > 0)
634
- return await WeavetabCloseBatch(tabIds, liveConfig, force);
667
+ return await WeavetabCloseBatch(tabIds, liveConfig, force, app);
635
668
  if (!tabId)
636
669
  throw new Error("tabId or tabIds is required for action: close");
637
- return await WeavetabClose(tabId, liveConfig, force);
670
+ return await WeavetabClose(tabId, liveConfig, force, app);
638
671
  }
639
672
  if (action === "list_windows") {
640
- return await weaveWindowList(liveConfig);
673
+ return await weaveWindowList(liveConfig, app);
641
674
  }
642
675
  if (action === "focus_window") {
643
676
  if (windowId === undefined)
644
677
  throw new Error("windowId is required for action: focus_window");
645
- return await weaveFocusWindow(windowId, liveConfig);
678
+ return await weaveFocusWindow(windowId, liveConfig, app);
646
679
  }
647
680
  if (action === "consolidate") {
648
- return await WeavetabConsolidate(liveConfig);
681
+ return await WeavetabConsolidate(liveConfig, app);
649
682
  }
650
683
  if (action === "heal") {
651
- return await WeavetabHeal(liveConfig);
684
+ return await WeavetabHeal(liveConfig, app);
652
685
  }
653
686
  throw new Error("Invalid action");
654
- });
687
+ }, false, app);
655
688
  }
656
689
  catch (e) {
657
690
  return wrapError(e);
@@ -663,17 +696,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
663
696
  value: z.string().optional().describe("CSS selector, text string, or URL pattern. Not needed for 'stable'/'network'."),
664
697
  timeout: z.number().optional().describe("Max wait in ms (default 8000)."),
665
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'."),
666
700
  }, async (args) => {
667
701
  try {
668
702
  return await withSecurity(config, "browser_wait", async (session, liveConfig) => {
669
703
  let targetSession = session;
670
704
  if (args.tab_id) {
671
705
  const { connectToTab } = await import("./cdp/connector.js");
672
- const client = await connectToTab(args.tab_id);
706
+ const client = await connectToTab(args.tab_id, undefined, undefined, undefined, args.app);
673
707
  targetSession = client.session;
674
708
  }
675
709
  return await browserWait(targetSession, args, liveConfig);
676
- });
710
+ }, false, args.app);
677
711
  }
678
712
  catch (e) {
679
713
  return wrapError(e);
@@ -699,8 +733,20 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
699
733
  message: z.string().describe("The question to ask, or the feedback/analysis to log."),
700
734
  input: z.enum(["sentence", "paragraph"]).optional().describe("Input field type for question mode (default: sentence)."),
701
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)."),
702
737
  }, async (args) => {
703
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
+ }
704
750
  return await withSecurity(config, "browser_thoughts", async (session) => {
705
751
  if (args.action === "feedback") {
706
752
  const { logActionStructured } = await import("./audit/logger.js");
@@ -736,7 +782,8 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
736
782
  end_x: z.number().optional().describe("Destination X coordinate (only for drag_and_drop)."),
737
783
  end_y: z.number().optional().describe("Destination Y coordinate (only for drag_and_drop)."),
738
784
  waitForChild: z.string().optional().describe("CSS Selector of a child element to wait for (only for hover)."),
739
- 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'."),
740
787
  }, async (args) => {
741
788
  try {
742
789
  return await withSecurity(config, "browser_pointer", async (session, liveConfig) => {
@@ -754,7 +801,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
754
801
  if (args.end_target && !normalizedArgs.end_ref)
755
802
  normalizedArgs.end_ref = args.end_target;
756
803
  return await browserPointer(session, normalizedArgs, liveConfig);
757
- });
804
+ }, false, args.app);
758
805
  }
759
806
  catch (e) {
760
807
  return wrapError(e);
@@ -765,11 +812,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
765
812
  nodeId: z.number().optional().describe("CDP backendNodeId."),
766
813
  selector: z.string().optional().describe("CSS Selector."),
767
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'."),
768
816
  }, async (args) => {
769
817
  try {
770
818
  return await withSecurity(config, "browser_select", async (session, liveConfig) => {
771
819
  return await browserSelect(session, args, liveConfig);
772
- });
820
+ }, false, args.app);
773
821
  }
774
822
  catch (e) {
775
823
  return wrapError(e);
@@ -782,11 +830,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
782
830
  value: z.string().describe("Value to fill/select/type into the field."),
783
831
  })).describe("Array of field ref+value pairs to fill sequentially."),
784
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'."),
785
834
  }, async (args) => {
786
835
  try {
787
836
  return await withSecurity(config, "browser_fill", async (session, liveConfig) => {
788
837
  return await agentAction(session, 'typing', () => browserFill(session, args, liveConfig), 'browser_fill');
789
- });
838
+ }, false, args.app);
790
839
  }
791
840
  catch (e) {
792
841
  return wrapError(e);
@@ -799,7 +848,8 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
799
848
  ref: z.string().optional().describe("Alternative element Ref ID (e.g. 'w:42')."),
800
849
  target: z.string().optional().describe("Alternative element identifier, CSS Selector, or query."),
801
850
  nodeId: z.number().optional().describe("CDP backendNodeId to highlight directly."),
802
- 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'."),
803
853
  }, async (args) => {
804
854
  try {
805
855
  return await withSecurity(config, "browser_highlight", async (session, liveConfig) => {
@@ -817,7 +867,7 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
817
867
  delete normalizedArgs.ref;
818
868
  delete normalizedArgs.target;
819
869
  return await browserHighlight(session, normalizedArgs, liveConfig);
820
- });
870
+ }, false, args.app);
821
871
  }
822
872
  catch (e) {
823
873
  return wrapError(e);
@@ -826,12 +876,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
826
876
  // ── 20. browser_upload ──────────────────────────────────────────────────────
827
877
  server.tool("browser_upload", "Upload files to a file input.", {
828
878
  nodeId: z.number().describe("CDP backendNodeId of the target <input type='file'> element."),
829
- 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'."),
830
881
  }, async (args) => {
831
882
  try {
832
883
  return await withSecurity(config, "browser_upload", async (session, liveConfig) => {
833
884
  return await browserUpload(session, args, liveConfig);
834
- });
885
+ }, false, args.app);
835
886
  }
836
887
  catch (e) {
837
888
  return wrapError(e);
@@ -839,12 +890,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
839
890
  });
840
891
  // ── 22. browser_find ────────────────────────────────────────────────────────
841
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.", {
842
- 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'."),
843
895
  }, async (args) => {
844
896
  try {
845
897
  return await withSecurity(config, "browser_find", async (session, liveConfig) => {
846
898
  return await agentAction(session, 'reading', () => browserFind(session, args, liveConfig), 'browser_find');
847
- });
899
+ }, false, args.app);
848
900
  }
849
901
  catch (e) {
850
902
  return wrapError(e);
@@ -856,11 +908,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
856
908
  action: z.enum(["get", "set", "clear"]).describe("Action to perform on the storage."),
857
909
  key: z.string().optional().describe("Key name (required for 'get' and 'set' actions)."),
858
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'."),
859
912
  }, async (args) => {
860
913
  try {
861
914
  return await withSecurity(config, "browser_storage", async (session, liveConfig) => {
862
915
  return await browserStorage(session, args, liveConfig);
863
- });
916
+ }, false, args.app);
864
917
  }
865
918
  catch (e) {
866
919
  return wrapError(e);
@@ -872,11 +925,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
872
925
  pattern: z.string().optional().describe("URL glob pattern to intercept or filter telemetry (e.g. '*api/login*')."),
873
926
  mockStatus: z.number().optional().describe("HTTP status code to return when enabling mock (default 200)."),
874
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'."),
875
929
  }, async (args) => {
876
930
  try {
877
931
  return await withSecurity(config, "browser_network_intercept", async (session, liveConfig) => {
878
932
  return await browserNetworkIntercept(session, args, liveConfig);
879
- });
933
+ }, false, args.app);
880
934
  }
881
935
  catch (e) {
882
936
  return wrapError(e);
@@ -884,17 +938,18 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
884
938
  });
885
939
  // ── 25. browser_console ─────────────────────────────────────────────────────
886
940
  server.tool("browser_console", "1-5ms latency. Read or clear buffered console API messages, JS exceptions, and network errors.", {
887
- 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'."),
888
943
  }, async (args) => {
889
944
  try {
890
945
  return await withSecurity(config, "browser_console", async (session, liveConfig) => {
891
946
  let targetId = "default";
892
947
  const { sessionRegistry } = await import("./state/session.js");
893
- const active = sessionRegistry.getActive(liveConfig.agentId);
948
+ const active = sessionRegistry.getActive(liveConfig.agentId, args.app);
894
949
  if (active)
895
950
  targetId = active.targetId;
896
951
  return await browserConsole(session, args, liveConfig, targetId);
897
- });
952
+ }, false, args.app);
898
953
  }
899
954
  catch (e) {
900
955
  return wrapError(e);
@@ -906,11 +961,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
906
961
  landscape: z.boolean().optional().describe("Print in landscape orientation (default false)."),
907
962
  scale: z.number().optional().describe("Scale factor (default 1.0)."),
908
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'."),
909
965
  }, async (args) => {
910
966
  try {
911
967
  return await withSecurity(config, "browser_pdf", async (session, liveConfig) => {
912
968
  return await browserPdf(session, args, liveConfig);
913
- });
969
+ }, false, args.app);
914
970
  }
915
971
  catch (e) {
916
972
  return wrapError(e);
@@ -919,12 +975,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
919
975
  // ── 27. browser_eval ────────────────────────────────────────────────────────
920
976
  server.tool("browser_eval", "Run arbitrary JS via CDP. Good for extracting app state. Cannot traverse shadow DOM/cross-origin iframes.", {
921
977
  expression: z.string().describe("JavaScript code to evaluate in the context of the page."),
922
- 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'."),
923
980
  }, async (args) => {
924
981
  try {
925
982
  return await withSecurity(config, "browser_eval", async (session, liveConfig) => {
926
983
  return await agentAction(session, 'reading', () => browserEval(session, args, liveConfig), 'browser_eval');
927
- });
984
+ }, false, args.app);
928
985
  }
929
986
  catch (e) {
930
987
  return wrapError(e);
@@ -938,11 +995,12 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
938
995
  domain: z.string().optional().describe("Domain for the cookie."),
939
996
  path: z.string().optional().describe("Path for the cookie."),
940
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'."),
941
999
  }, async (args) => {
942
1000
  try {
943
1001
  return await withSecurity(config, "browser_cookies", async (session, liveConfig) => {
944
1002
  return await browserCookies(session, args, liveConfig);
945
- });
1003
+ }, false, args.app);
946
1004
  }
947
1005
  catch (e) {
948
1006
  return wrapError(e);
@@ -956,12 +1014,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
956
1014
  label: z.string().optional().describe("Visible text or aria-label of the canvas element."),
957
1015
  ref: z.string().optional().describe("Alternative ref identifier for the canvas element."),
958
1016
  backendNodeId: z.number().optional().describe("Raw CDP backendNodeId for direct targeting."),
959
- 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'."),
960
1019
  }, async (args) => {
961
1020
  try {
962
1021
  return await withSecurity(config, "browser_canvas", async (session, liveConfig) => {
963
1022
  return await browserCanvas(session, args, liveConfig);
964
- });
1023
+ }, false, args.app);
965
1024
  }
966
1025
  catch (e) {
967
1026
  return wrapError(e);
@@ -972,12 +1031,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
972
1031
  query: z.string().describe("CSS Selector or XPath query to extract."),
973
1032
  type: z.enum(["css", "xpath"]).optional().describe("Query type (default 'css')."),
974
1033
  attributes: z.array(z.string()).optional().describe("Attributes to extract (e.g. ['textContent', 'href'], default ['textContent'])."),
975
- 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'."),
976
1036
  }, async (args) => {
977
1037
  try {
978
1038
  return await withSecurity(config, "browser_scrape", async (session, liveConfig) => {
979
1039
  return await agentAction(session, 'reading', () => browserScrape(session, args, liveConfig), 'browser_scrape');
980
- });
1040
+ }, false, args.app);
981
1041
  }
982
1042
  catch (e) {
983
1043
  return wrapError(e);
@@ -986,12 +1046,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
986
1046
  // ── 32. browser_snapshot ────────────────────────────────────────────────────
987
1047
  server.tool("browser_snapshot", "Capture or restore an MHTML snapshot. Best for offline archival, replay testing, and compliance audits.", {
988
1048
  action: z.enum(["capture", "restore"]).describe("Action to perform."),
989
- 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'."),
990
1051
  }, async (args) => {
991
1052
  try {
992
1053
  return await withSecurity(config, "browser_snapshot", async (session, liveConfig) => {
993
1054
  return await browserSnapshot(session, args, liveConfig);
994
- });
1055
+ }, false, args.app);
995
1056
  }
996
1057
  catch (e) {
997
1058
  return wrapError(e);
@@ -1007,12 +1068,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1007
1068
  y: z.number().describe("Y coordinate of clip region."),
1008
1069
  width: z.number().describe("Width of clip region."),
1009
1070
  height: z.number().describe("Height of clip region.")
1010
- }).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'."),
1011
1073
  }, async (args) => {
1012
1074
  try {
1013
1075
  return await withSecurity(config, "browser_screenshot", async (session, liveConfig) => {
1014
1076
  return await agentAction(session, 'reading', () => browserScreenshot(session, args, liveConfig), 'browser_screenshot');
1015
- });
1077
+ }, false, args.app);
1016
1078
  }
1017
1079
  catch (e) {
1018
1080
  return wrapError(e);
@@ -1024,12 +1086,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1024
1086
  width: z.number().optional().describe("Viewport width in pixels."),
1025
1087
  height: z.number().optional().describe("Viewport height in pixels."),
1026
1088
  deviceScaleFactor: z.number().optional().describe("Device Pixel Ratio (default 1)."),
1027
- 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'."),
1028
1091
  }, async (args) => {
1029
1092
  try {
1030
1093
  return await withSecurity(config, "browser_viewport", async (session, liveConfig) => {
1031
1094
  return await browserViewport(session, args, liveConfig);
1032
- });
1095
+ }, false, args.app);
1033
1096
  }
1034
1097
  catch (e) {
1035
1098
  return wrapError(e);
@@ -1040,12 +1103,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1040
1103
  action: z.enum(["read", "write"]).describe("Action to perform."),
1041
1104
  text: z.string().optional().describe("Text to write to clipboard (for write action)."),
1042
1105
  dataUrl: z.string().optional().describe("Base64 data URL of an image to write to clipboard (e.g. 'data:image/png;base64,...')."),
1043
- 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'."),
1044
1108
  }, async (args) => {
1045
1109
  try {
1046
1110
  return await withSecurity(config, "browser_clipboard", async (session, liveConfig) => {
1047
1111
  return await browserClipboard(session, args, liveConfig);
1048
- });
1112
+ }, false, args.app);
1049
1113
  }
1050
1114
  catch (e) {
1051
1115
  return wrapError(e);
@@ -1055,12 +1119,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1055
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.", {
1056
1120
  nodeId: z.number().optional().describe("CDP backendNodeId."),
1057
1121
  selector: z.string().optional().describe("CSS Selector."),
1058
- 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'."),
1059
1124
  }, async (args) => {
1060
1125
  try {
1061
1126
  return await withSecurity(config, "browser_inspect", async (session, liveConfig) => {
1062
1127
  return await agentAction(session, 'reading', () => browserInspect(session, args, liveConfig), 'browser_inspect');
1063
- });
1128
+ }, false, args.app);
1064
1129
  }
1065
1130
  catch (e) {
1066
1131
  return wrapError(e);
@@ -1073,12 +1138,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1073
1138
  outputDir: z.string().optional().describe("Optional custom directory to write extracted files into. If omitted, uses configured designDir (~/.weavetab/designs/<domain>-<timestamp>/)."),
1074
1139
  name: z.string().optional().describe("Optional custom name for the saved design folder (e.g. 'resend-hero', 'pricing-section')."),
1075
1140
  includeAssets: z.boolean().optional().describe("Whether to capture inline SVGs and asset references (default true)."),
1076
- 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'."),
1077
1143
  }, async (args) => {
1078
1144
  try {
1079
1145
  return await withSecurity(config, "browser_extract_design", async (session, liveConfig) => {
1080
1146
  return await agentAction(session, 'reading', () => browserExtractDesign(session, args, liveConfig), 'browser_extract_design');
1081
- });
1147
+ }, false, args.app);
1082
1148
  }
1083
1149
  catch (e) {
1084
1150
  return wrapError(e);
@@ -1091,12 +1157,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1091
1157
  script: z.string().optional().describe("JavaScript code to inject and run (required for start)."),
1092
1158
  persistent: z.boolean().optional().describe("If true, the script re-runs in a loop (default false)."),
1093
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'."),
1094
1161
  }, async (args) => {
1095
1162
  try {
1096
1163
  return await withSecurity(config, "browser_automation", async (session, liveConfig) => {
1097
1164
  const result = await browserAutomation(session, args, liveConfig);
1098
1165
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1099
- });
1166
+ }, false, args.app);
1100
1167
  }
1101
1168
  catch (e) {
1102
1169
  return wrapError(e);
@@ -1110,12 +1177,13 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1110
1177
  maxFrames: z.number().optional().describe("Maximum frames to capture (default 500)."),
1111
1178
  format: z.enum(["webp", "gif", "webm", "mp4", "frames"]).optional().describe("Output video format when stopping recording. Default 'webp' (pure Node native compilation)."),
1112
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'."),
1113
1181
  }, async (args) => {
1114
1182
  try {
1115
1183
  return await withSecurity(config, "browser_recording", async (session, liveConfig) => {
1116
1184
  const result = await browserRecording(session, args, liveConfig);
1117
1185
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1118
- });
1186
+ }, false, args.app);
1119
1187
  }
1120
1188
  catch (e) {
1121
1189
  return wrapError(e);
@@ -1139,12 +1207,14 @@ export async function startServer(config, transportMode = "stdio", wsPort = 3000
1139
1207
  }
1140
1208
  });
1141
1209
  // ── browser_performance ───────────────────────────────────────────────────
1142
- 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) => {
1143
1213
  try {
1144
1214
  return await withSecurity(config, "browser_performance", async (session, liveConfig) => {
1145
1215
  const result = await browserPerformance(session, liveConfig);
1146
1216
  return { content: [{ type: "text", text: JSON.stringify(result) }] };
1147
- });
1217
+ }, false, args.app);
1148
1218
  }
1149
1219
  catch (e) {
1150
1220
  return wrapError(e);
@@ -1285,11 +1355,12 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1285
1355
  }).optional().describe("Override geolocation coordinates"),
1286
1356
  timezoneId: z.string().optional().describe("Override browser timezone (e.g. 'America/New_York')"),
1287
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'."),
1288
1359
  }, async (args) => {
1289
1360
  try {
1290
1361
  return await withSecurity(config, "browser_emulate", async (session, liveConfig) => {
1291
1362
  return await browserEmulate(session, args, liveConfig);
1292
- });
1363
+ }, false, args.app);
1293
1364
  }
1294
1365
  catch (e) {
1295
1366
  return wrapError(e);
@@ -1409,14 +1480,13 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1409
1480
  }]
1410
1481
  };
1411
1482
  });
1412
- // ── Plugins Lifecycle (Add / Override / Remove) ───────────────────────────
1413
- const plugins = discoverPlugins();
1483
+ // ── Plugins Lifecycle & Dynamic Registry ───────────────────────────
1414
1484
  const { getVerifiedPackages, resolvePluginTrust } = await import("./plugin/registry.js");
1415
1485
  const verifiedPackages = await getVerifiedPackages();
1416
- let pluginAddedCount = 0;
1417
- let pluginOverrideCount = 0;
1418
- let pluginRemovedCount = 0;
1419
- for (const plugin of plugins) {
1486
+ const loadedPluginNames = new Set();
1487
+ const registerPluginTools = async (plugin) => {
1488
+ if (loadedPluginNames.has(plugin.name))
1489
+ return;
1420
1490
  const trust = resolvePluginTrust(plugin.name, verifiedPackages);
1421
1491
  const isOfficial = trust.tier === "official";
1422
1492
  const addedNames = [];
@@ -1431,7 +1501,6 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1431
1501
  if (server._registeredTools?.[toolName]) {
1432
1502
  delete server._registeredTools[toolName];
1433
1503
  removedNames.push(toolName);
1434
- pluginRemovedCount++;
1435
1504
  }
1436
1505
  }
1437
1506
  // 2. Tool Overrides (replace or wrap built-in tools)
@@ -1461,7 +1530,6 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1461
1530
  }
1462
1531
  });
1463
1532
  overriddenNames.push(ov.name);
1464
- pluginOverrideCount++;
1465
1533
  }
1466
1534
  // 3. Tool Additions (new tools)
1467
1535
  const newTools = plugin.tools || [];
@@ -1469,7 +1537,7 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1469
1537
  if (server._registeredTools?.[td.name]) {
1470
1538
  delete server._registeredTools[td.name];
1471
1539
  }
1472
- server.tool(td.name, td.description, td.schema, async (args) => {
1540
+ server.tool(td.name, td.description, td.schema || {}, async (args) => {
1473
1541
  try {
1474
1542
  return await withSecurity(config, td.name, async (session, liveConfig) => {
1475
1543
  const result = await td.handler(args, session, liveConfig);
@@ -1481,45 +1549,69 @@ DO NOT use browser_scrape or browser_map for binary documents — they cannot re
1481
1549
  }
1482
1550
  });
1483
1551
  addedNames.push(td.name);
1484
- pluginAddedCount++;
1485
1552
  }
1486
- // 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
+ };
1487
1563
  if (typeof plugin.onLoad === "function") {
1488
1564
  try {
1489
- await plugin.onLoad();
1565
+ await plugin.onLoad(pluginCtx);
1490
1566
  }
1491
1567
  catch (err) {
1492
1568
  process.stderr.write(` \u001b[31m[!] Plugin ${plugin.name} onLoad failed: ${err?.message}\u001b[0m\n`);
1493
1569
  }
1494
1570
  }
1495
- // Build permission and operations warning string
1496
- const ops = [];
1497
- if (addedNames.length > 0)
1498
- ops.push(`+${addedNames.length} added`);
1499
- if (overriddenNames.length > 0)
1500
- ops.push(`⚡overrode [${overriddenNames.join(", ")}]`);
1501
- if (removedNames.length > 0)
1502
- ops.push(`✖removed [${removedNames.join(", ")}]`);
1503
- const opsStr = ops.length > 0 ? ` (${ops.join(", ")})` : "";
1504
- let permWarning = "";
1505
- if (trust.tier === "unverified" && plugin.manifest?.permissions) {
1506
- const perms = plugin.manifest.permissions;
1507
- const flags = [];
1508
- if (perms.network)
1509
- flags.push("network");
1510
- if (perms.cdp)
1511
- flags.push("cdp");
1512
- if (perms.fs)
1513
- flags.push("fs");
1514
- if (perms.tools?.override?.length)
1515
- flags.push(`override:${perms.tools.override.join(",")}`);
1516
- if (perms.tools?.remove?.length)
1517
- flags.push(`remove:${perms.tools.remove.join(",")}`);
1518
- if (flags.length > 0) {
1519
- 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();
1520
1577
  }
1521
1578
  }
1522
- 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
+ }
1523
1615
  }
1524
1616
  let transport;
1525
1617
  if (transportMode === "ws") {