@alfe.ai/openclaw-mcp-bundler 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.d.cts CHANGED
@@ -44,11 +44,6 @@ interface ToolFactoryContext {
44
44
  runtimeConfig?: ConfigSnapshot;
45
45
  getRuntimeConfig?: () => ConfigSnapshot | undefined;
46
46
  }
47
- interface OpenClawPluginService {
48
- id: string;
49
- start: (ctx?: unknown) => void | Promise<void>;
50
- stop?: (ctx?: unknown) => void | Promise<void>;
51
- }
52
47
  interface OpenClawPluginApi {
53
48
  logger: Logger;
54
49
  config?: ConfigSnapshot;
@@ -66,7 +61,6 @@ interface OpenClawPluginApi {
66
61
  names?: string[];
67
62
  optional?: boolean;
68
63
  }) => void;
69
- registerService?: (service: OpenClawPluginService) => void;
70
64
  }
71
65
  declare const plugin: {
72
66
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICqBF,IAAA,EAAM,MAAA;IAON,OAAA,EAAA,MAAc;EAAA,CAAA;;;;;UAPd,MAAA,CA2BI;EAGJ,KAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAkB,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;EAAA,IAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;MACjB,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OACO,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;;UAzBR,cAAA,CA0B+B;EAG/B,MAAA,CAAA,EAAA;IAAqB,QAAA,CAAA,EAAA;MAEI,KAAA,CAAA,EAAA,MAAA;MACA,WAAA,CAAA,EA/BqB,MA+BrB,CAAA,MAAA,EAAA,OAAA,CAAA;IAAO,CAAA;EAGhC,CAAA;EAAiB,GAAA,CAAA,EAAA;IACjB,OAAA,CAAA,EAlCU,MAkCV,CAAA,MAAA,EAAA,OAAA,CAAA;;;UA/BA,eAAA,CAqCA;SAAmB,EApClB,MAoCkB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA;SAAuB,CAAA,EAnCxC,MAmCwC,CAAA,MAAA,EAAA,OAAA,CAAA;;UAhC1C,SAAA,CAmCoB;EAAqB,IAAA,EAAA,MAAA;EA6V7C,KAAA,CAAA,EAAA,MAsQL;EAAA,WAAA,EAAA,MAAA;YAjMe,EAjcF,MAicE,CAAA,MAAA,EAAA,OAAA,CAAA;SA6LE,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,CAAA,EA1nBL,WA0nBK,EAAA,QAAA,CAAA,EAAA,CAAA,MAAA,EAAA,OAAA,EAAA,GAAA,IAAA,EAAA,GAxnBX,OAwnBW,CAxnBH,eAwnBG,CAAA;;UArnBR,kBAAA;WACC;kBACO;2BACS;;UAGjB,qBAAA;;mCAEyB;mCACA;;UAGzB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;8BAGlC;;cA6VxB;;;;;;;;gBAqEU;kBA6LE"}
1
+ {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICiDF,IAAA,EAAM,MAAA;IAON,OAAA,EAAA,MAAc;EAAA,CAAA;;;;;UAPd,MAAA;;;;;;UAOA,cAAA;;;;oBAC8C;;;;cACpC;;;UAGV,eAAA;WACC;YACC;;UAGF,SAAA;;;;cAII;0DAID,sDAEN,QAAQ;;UAGL,kBAAA;WACC;kBACO;2BACS;;UAGjB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;;cAod1D;;;;;;;;gBAuEU;kBA8GE"}
package/dist/plugin.d.ts CHANGED
@@ -44,11 +44,6 @@ interface ToolFactoryContext {
44
44
  runtimeConfig?: ConfigSnapshot;
45
45
  getRuntimeConfig?: () => ConfigSnapshot | undefined;
46
46
  }
47
- interface OpenClawPluginService {
48
- id: string;
49
- start: (ctx?: unknown) => void | Promise<void>;
50
- stop?: (ctx?: unknown) => void | Promise<void>;
51
- }
52
47
  interface OpenClawPluginApi {
53
48
  logger: Logger;
54
49
  config?: ConfigSnapshot;
@@ -66,7 +61,6 @@ interface OpenClawPluginApi {
66
61
  names?: string[];
67
62
  optional?: boolean;
68
63
  }) => void;
69
- registerService?: (service: OpenClawPluginService) => void;
70
64
  }
71
65
  declare const plugin: {
72
66
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICqBF,IAAA,EAAM,MAAA;IAON,OAAA,EAAA,MAAc;EAAA,CAAA;;;;;UAPd,MAAA,CA2BI;EAGJ,KAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAkB,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;EAAA,IAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;MACjB,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OACO,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;;UAzBR,cAAA,CA0B+B;EAG/B,MAAA,CAAA,EAAA;IAAqB,QAAA,CAAA,EAAA;MAEI,KAAA,CAAA,EAAA,MAAA;MACA,WAAA,CAAA,EA/BqB,MA+BrB,CAAA,MAAA,EAAA,OAAA,CAAA;IAAO,CAAA;EAGhC,CAAA;EAAiB,GAAA,CAAA,EAAA;IACjB,OAAA,CAAA,EAlCU,MAkCV,CAAA,MAAA,EAAA,OAAA,CAAA;;;UA/BA,eAAA,CAqCA;SAAmB,EApClB,MAoCkB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA;SAAuB,CAAA,EAnCxC,MAmCwC,CAAA,MAAA,EAAA,OAAA,CAAA;;UAhC1C,SAAA,CAmCoB;EAAqB,IAAA,EAAA,MAAA;EA6V7C,KAAA,CAAA,EAAA,MAsQL;EAAA,WAAA,EAAA,MAAA;YAjMe,EAjcF,MAicE,CAAA,MAAA,EAAA,OAAA,CAAA;SA6LE,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,CAAA,EA1nBL,WA0nBK,EAAA,QAAA,CAAA,EAAA,CAAA,MAAA,EAAA,OAAA,EAAA,GAAA,IAAA,EAAA,GAxnBX,OAwnBW,CAxnBH,eAwnBG,CAAA;;UArnBR,kBAAA;WACC;kBACO;2BACS;;UAGjB,qBAAA;;mCAEyB;mCACA;;UAGzB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;8BAGlC;;cA6VxB;;;;;;;;gBAqEU;kBA6LE"}
1
+ {"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICiDF,IAAA,EAAM,MAAA;IAON,OAAA,EAAA,MAAc;EAAA,CAAA;;;;;UAPd,MAAA;;;;;;UAOA,cAAA;;;;oBAC8C;;;;cACpC;;;UAGV,eAAA;WACC;YACC;;UAGF,SAAA;;;;cAII;0DAID,sDAEN,QAAQ;;UAGL,kBAAA;WACC;kBACO;2BACS;;UAGjB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;;cAod1D;;;;;;;;gBAuEU;kBA8GE"}
package/dist/plugin2.cjs CHANGED
@@ -236,13 +236,13 @@ function isIpcResponse(value) {
236
236
  //#region src/plugin.ts
237
237
  const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
238
238
  const CACHE_FILE = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "mcp", "openclaw-bundler-cache.json");
239
+ const GATEWAY_SOCKET = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "gateway.sock");
239
240
  let ipcClient;
240
241
  let cachedDescriptors = [];
241
242
  let refreshInFlight = null;
242
243
  let periodicRefreshTimer = null;
243
244
  const ACTIVATED_KEY = "__alfeMcpBundlerPluginActivated";
244
245
  const g = globalThis;
245
- const PREWARM_TIMEOUT_MS = 3e3;
246
246
  const PERIODIC_REFRESH_INTERVAL_MS = 1e4;
247
247
  function resolveLiveConfig(api, ctx) {
248
248
  return ctx?.runtimeConfig ?? ctx?.getRuntimeConfig?.() ?? api.runtime?.config?.current?.() ?? ctx?.config ?? api.config;
@@ -286,9 +286,9 @@ function errorResult(message) {
286
286
  /**
287
287
  * Async-aware shim around the module-scoped `ipcClient`. The management
288
288
  * tools and dynamic MCP tools registered in `activate()` execute LATER
289
- * (when the agent invokes them), but `ipcClient` is only set inside
290
- * `startService`. The `?.` chains return useful errors when the agent
291
- * tries to call a tool before the IPC is ready, instead of throwing.
289
+ * (when the agent invokes them), but `ipcClient` is only constructed
290
+ * inside `startIpcClient`. The `?.` chains return useful errors when the
291
+ * agent tries to call a tool before the IPC is ready, instead of throwing.
292
292
  */
293
293
  async function callIpc(method, params = {}) {
294
294
  if (!ipcClient) return {
@@ -302,18 +302,24 @@ async function callIpc(method, params = {}) {
302
302
  return ipcClient.call(method, params);
303
303
  }
304
304
  /**
305
- * Load the last-known tool catalogue from disk. Used at `activate()`
306
- * time so the agent's tool list contains real tools even in tool-
307
- * discovery mode (where `service.start` and therefore the IPC
308
- * connection never runs). Silently falls back to an empty list on
309
- * any read/parse failure so a first-run agent (no cache file yet)
310
- * still loads cleanly.
305
+ * Cheap shape check guarding against a corrupt or torn-write cache —
306
+ * a half-written JSON array that parses but holds garbage entries
307
+ * would otherwise reach `buildAgentTool` where `tool.prefixed` ends up
308
+ * undefined and registers a tool with `name: undefined`. Catching the
309
+ * shape here protects the agent's tool list from disk-level corruption.
311
310
  */
312
311
  function isValidDescriptor(value) {
313
312
  if (typeof value !== "object" || value === null) return false;
314
313
  const v = value;
315
314
  return typeof v.prefixed === "string" && typeof v.original === "string" && typeof v.server === "string" && typeof v.parameters === "object" && v.parameters !== null;
316
315
  }
316
+ /**
317
+ * Load the last-known tool catalogue from disk. Used at `activate()`
318
+ * time so the agent's tool list contains real tools even in tool-
319
+ * discovery mode (where the IPC client never opens). Silently falls
320
+ * back to an empty list on any read/parse failure so a first-run agent
321
+ * (no cache file yet) still loads cleanly.
322
+ */
317
323
  function loadCachedDescriptorsFromDisk(logger) {
318
324
  try {
319
325
  const raw = (0, node_fs.readFileSync)(CACHE_FILE, "utf-8");
@@ -469,7 +475,12 @@ function refreshCachedTools(api) {
469
475
  api.logger.debug("[mcp-bundler] list_tools failed", { err: response.error?.message ?? "unknown" });
470
476
  return;
471
477
  }
472
- cachedDescriptors = response.payload.tools;
478
+ const fresh = response.payload.tools;
479
+ if (fresh.length === 0 && cachedDescriptors.length > 0) {
480
+ api.logger.debug(`[mcp-bundler] list_tools returned 0 tools — keeping prior cache of ${String(cachedDescriptors.length)}`);
481
+ return;
482
+ }
483
+ cachedDescriptors = fresh;
473
484
  persistCachedDescriptorsToDisk(cachedDescriptors, api.logger);
474
485
  } finally {
475
486
  refreshInFlight = null;
@@ -477,6 +488,88 @@ function refreshCachedTools(api) {
477
488
  })();
478
489
  return refreshInFlight;
479
490
  }
491
+ /**
492
+ * Should this `activate()` invocation also start the live IPC client?
493
+ *
494
+ * Decision rule: only in full agent-runtime mode. CLI invocations
495
+ * (`openclaw config set`, `openclaw plugins inspect`, `openclaw upgrade`,
496
+ * etc.) and tool-discovery loads (where OpenClaw builds the agent's
497
+ * tool list without running plugin services) should NOT open the
498
+ * gateway socket — they only need the disk cache and the static tool
499
+ * registrations.
500
+ *
501
+ * `api.registrationMode` is the canonical signal. OpenClaw 2026.5
502
+ * sets it to one of: `'full'`, `'discovery'`, `'tool-discovery'`,
503
+ * `'setup-only'`, `'setup-runtime'`, `'cli-metadata'`. Only `'full'`
504
+ * means "real agent runtime; everything is allowed to run".
505
+ *
506
+ * Defensive default: if `registrationMode` is unset (older OpenClaw,
507
+ * non-bundled host, test harness), assume full mode so we don't
508
+ * silently degrade. The IPC client itself handles a missing socket
509
+ * gracefully via reconnect, so an over-eager start is cheap.
510
+ */
511
+ function shouldStartIpcClient(api) {
512
+ const mode = api.registrationMode;
513
+ if (mode === void 0) return true;
514
+ return mode === "full";
515
+ }
516
+ /**
517
+ * Construct the live IPC client and wire its `onConnect` to a cache
518
+ * refresh. Module-scoped — the same client lifecycle outlasts a single
519
+ * `activate()` call so reconnects from daemon restarts continue
520
+ * automatically. Called from `activate()` ONLY in full agent-runtime
521
+ * mode (see `shouldStartIpcClient`).
522
+ *
523
+ * IMPORTANT: this function is NOT awaited. The `onConnect` callback
524
+ * fires asynchronously; the dynamic factory returns whatever is in
525
+ * `cachedDescriptors` at factory-invocation time (loaded from disk in
526
+ * `activate()`, then refreshed on connect, then periodically). A first-
527
+ * turn pre-warm is no longer needed — the disk cache covers turn-1,
528
+ * and any new tools discovered during the in-flight `onConnect`
529
+ * refresh land in `cachedDescriptors` before the LLM picks them up on
530
+ * turn-2.
531
+ *
532
+ * Removed the v0.0.13 PREWARM_TIMEOUT_MS / blocking pre-warm because
533
+ * v0.0.14 has no `service.start` await point to attach it to —
534
+ * removing `api.registerService` was the whole point of the fix. Disk
535
+ * cache + periodic refresh now carry the freshness contract.
536
+ */
537
+ function startIpcClient(api, log) {
538
+ if (ipcClient) return;
539
+ if (!(0, node_fs.existsSync)(GATEWAY_SOCKET)) log.debug(`[mcp-bundler] ${GATEWAY_SOCKET} not present at activate — IPC client will refresh when the daemon comes online`);
540
+ ipcClient = new McpIpcClient({
541
+ logger: log,
542
+ plugin: {
543
+ name: "@alfe.ai/openclaw-mcp-bundler",
544
+ version: pkg.version
545
+ },
546
+ onConnect: () => {
547
+ refreshCachedTools(api);
548
+ }
549
+ });
550
+ ipcClient.start();
551
+ periodicRefreshTimer = setInterval(() => {
552
+ if (ipcClient?.isConnected()) refreshCachedTools(api);
553
+ }, PERIODIC_REFRESH_INTERVAL_MS);
554
+ periodicRefreshTimer.unref?.();
555
+ }
556
+ /**
557
+ * Tear down the IPC client and periodic refresh timer. Called from
558
+ * `deactivate()` (the only lifecycle exit point now that v0.0.14
559
+ * dropped `api.registerService`). Safe to call multiple times — each
560
+ * tear-down is idempotent.
561
+ */
562
+ function stopIpcClient() {
563
+ if (periodicRefreshTimer !== null) {
564
+ clearInterval(periodicRefreshTimer);
565
+ periodicRefreshTimer = null;
566
+ }
567
+ if (ipcClient) {
568
+ ipcClient.stop();
569
+ ipcClient = void 0;
570
+ }
571
+ refreshInFlight = null;
572
+ }
480
573
  const plugin = {
481
574
  id: "@alfe.ai/openclaw-mcp-bundler",
482
575
  name: "Alfe MCP Bundler",
@@ -517,7 +610,9 @@ const plugin = {
517
610
  "xero-xero__xero_check_connection",
518
611
  "xero-xero__xero_refresh_token",
519
612
  "alfe-platform__session_status",
520
- "alfe-platform__gateway_status"
613
+ "alfe-platform__gateway_status",
614
+ "alfe-platform__integrations_browse_registry",
615
+ "alfe-platform__integrations_start_oauth"
521
616
  ] },
522
617
  activate(api) {
523
618
  const log = api.logger;
@@ -531,11 +626,6 @@ const plugin = {
531
626
  log.info("[mcp-bundler] disabled via plugin config — no-op");
532
627
  return;
533
628
  }
534
- if (!api.registerService) {
535
- g[ACTIVATED_KEY] = false;
536
- log.warn("[mcp-bundler] api.registerService unavailable — plugin is a no-op on this runtime");
537
- return;
538
- }
539
629
  const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));
540
630
  if (deferToNative) log.info("[mcp-bundler] openclaw.json#mcp.servers has entries — MCP-proxy factory will defer to native bundleMcp, but agent self-service tools (alfe_mcp_*) stay available");
541
631
  cachedDescriptors = loadCachedDescriptorsFromDisk(log);
@@ -544,69 +634,14 @@ const plugin = {
544
634
  if (hasNativeOpenclawMcpServers(resolveLiveConfig(api, ctx))) return null;
545
635
  refreshCachedTools(api);
546
636
  return cachedDescriptors.map((d) => buildAgentTool(api, d));
547
- }, { optional: true });
548
- const startService = async () => {
549
- const prewarmTimer = { handle: null };
550
- if (await new Promise((resolve) => {
551
- prewarmTimer.handle = setTimeout(() => {
552
- prewarmTimer.handle = null;
553
- resolve("timeout");
554
- }, PREWARM_TIMEOUT_MS);
555
- ipcClient = new McpIpcClient({
556
- logger: log,
557
- plugin: {
558
- name: plugin.id,
559
- version: plugin.version
560
- },
561
- onConnect: () => {
562
- refreshCachedTools(api).finally(() => {
563
- if (prewarmTimer.handle !== null) {
564
- clearTimeout(prewarmTimer.handle);
565
- prewarmTimer.handle = null;
566
- }
567
- resolve("completed");
568
- });
569
- }
570
- });
571
- ipcClient.start();
572
- }) === "timeout") log.warn(`[mcp-bundler] pre-warm timed out after ${String(PREWARM_TIMEOUT_MS)}ms — first chat turn may see a stale tool list (likely a slow or unreachable daemon at ~/.alfe/gateway.sock)`);
573
- periodicRefreshTimer = setInterval(() => {
574
- if (ipcClient?.isConnected()) refreshCachedTools(api);
575
- }, PERIODIC_REFRESH_INTERVAL_MS);
576
- periodicRefreshTimer.unref?.();
577
- if (deferToNative) {
578
- log.info(`[mcp-bundler] activated v${plugin.version} (management tools only — native bundleMcp owns runtime)`);
579
- return;
580
- }
581
- log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client, tools registered in activate)`);
582
- };
583
- const stopService = () => {
584
- log.info("[mcp-bundler] stopping");
585
- if (periodicRefreshTimer !== null) {
586
- clearInterval(periodicRefreshTimer);
587
- periodicRefreshTimer = null;
588
- }
589
- if (ipcClient) {
590
- ipcClient.stop();
591
- ipcClient = void 0;
592
- }
593
- refreshInFlight = null;
594
- g[ACTIVATED_KEY] = false;
595
- return Promise.resolve();
596
- };
597
- api.registerService({
598
- id: "alfe-mcp-bundler",
599
- start: async () => {
600
- await startService();
601
- },
602
- stop: async () => {
603
- await stopService();
604
- }
605
637
  });
638
+ log.info(`[mcp-bundler] activated v${plugin.version} (registrationMode=${api.registrationMode ?? "unknown"}, deferToNative=${String(deferToNative)}, cachedDescriptors=${String(cachedDescriptors.length)})`);
639
+ if (shouldStartIpcClient(api)) startIpcClient(api, log);
606
640
  },
607
641
  deactivate(api) {
608
642
  g[ACTIVATED_KEY] = false;
609
643
  api.logger.debug("[mcp-bundler] deactivating");
644
+ stopIpcClient();
610
645
  }
611
646
  };
612
647
  //#endregion
package/dist/plugin2.js CHANGED
@@ -3,7 +3,7 @@ import { createConnection } from "node:net";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
- import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
7
7
  //#region src/cli-backend-detect.ts
8
8
  /**
9
9
  * Returns true when openclaw.json#mcp.servers has at least one entry —
@@ -236,13 +236,13 @@ function isIpcResponse(value) {
236
236
  //#region src/plugin.ts
237
237
  const pkg = createRequire(import.meta.url)("../package.json");
238
238
  const CACHE_FILE = join(homedir(), ".alfe", "mcp", "openclaw-bundler-cache.json");
239
+ const GATEWAY_SOCKET = join(homedir(), ".alfe", "gateway.sock");
239
240
  let ipcClient;
240
241
  let cachedDescriptors = [];
241
242
  let refreshInFlight = null;
242
243
  let periodicRefreshTimer = null;
243
244
  const ACTIVATED_KEY = "__alfeMcpBundlerPluginActivated";
244
245
  const g = globalThis;
245
- const PREWARM_TIMEOUT_MS = 3e3;
246
246
  const PERIODIC_REFRESH_INTERVAL_MS = 1e4;
247
247
  function resolveLiveConfig(api, ctx) {
248
248
  return ctx?.runtimeConfig ?? ctx?.getRuntimeConfig?.() ?? api.runtime?.config?.current?.() ?? ctx?.config ?? api.config;
@@ -286,9 +286,9 @@ function errorResult(message) {
286
286
  /**
287
287
  * Async-aware shim around the module-scoped `ipcClient`. The management
288
288
  * tools and dynamic MCP tools registered in `activate()` execute LATER
289
- * (when the agent invokes them), but `ipcClient` is only set inside
290
- * `startService`. The `?.` chains return useful errors when the agent
291
- * tries to call a tool before the IPC is ready, instead of throwing.
289
+ * (when the agent invokes them), but `ipcClient` is only constructed
290
+ * inside `startIpcClient`. The `?.` chains return useful errors when the
291
+ * agent tries to call a tool before the IPC is ready, instead of throwing.
292
292
  */
293
293
  async function callIpc(method, params = {}) {
294
294
  if (!ipcClient) return {
@@ -302,18 +302,24 @@ async function callIpc(method, params = {}) {
302
302
  return ipcClient.call(method, params);
303
303
  }
304
304
  /**
305
- * Load the last-known tool catalogue from disk. Used at `activate()`
306
- * time so the agent's tool list contains real tools even in tool-
307
- * discovery mode (where `service.start` and therefore the IPC
308
- * connection never runs). Silently falls back to an empty list on
309
- * any read/parse failure so a first-run agent (no cache file yet)
310
- * still loads cleanly.
305
+ * Cheap shape check guarding against a corrupt or torn-write cache —
306
+ * a half-written JSON array that parses but holds garbage entries
307
+ * would otherwise reach `buildAgentTool` where `tool.prefixed` ends up
308
+ * undefined and registers a tool with `name: undefined`. Catching the
309
+ * shape here protects the agent's tool list from disk-level corruption.
311
310
  */
312
311
  function isValidDescriptor(value) {
313
312
  if (typeof value !== "object" || value === null) return false;
314
313
  const v = value;
315
314
  return typeof v.prefixed === "string" && typeof v.original === "string" && typeof v.server === "string" && typeof v.parameters === "object" && v.parameters !== null;
316
315
  }
316
+ /**
317
+ * Load the last-known tool catalogue from disk. Used at `activate()`
318
+ * time so the agent's tool list contains real tools even in tool-
319
+ * discovery mode (where the IPC client never opens). Silently falls
320
+ * back to an empty list on any read/parse failure so a first-run agent
321
+ * (no cache file yet) still loads cleanly.
322
+ */
317
323
  function loadCachedDescriptorsFromDisk(logger) {
318
324
  try {
319
325
  const raw = readFileSync(CACHE_FILE, "utf-8");
@@ -469,7 +475,12 @@ function refreshCachedTools(api) {
469
475
  api.logger.debug("[mcp-bundler] list_tools failed", { err: response.error?.message ?? "unknown" });
470
476
  return;
471
477
  }
472
- cachedDescriptors = response.payload.tools;
478
+ const fresh = response.payload.tools;
479
+ if (fresh.length === 0 && cachedDescriptors.length > 0) {
480
+ api.logger.debug(`[mcp-bundler] list_tools returned 0 tools — keeping prior cache of ${String(cachedDescriptors.length)}`);
481
+ return;
482
+ }
483
+ cachedDescriptors = fresh;
473
484
  persistCachedDescriptorsToDisk(cachedDescriptors, api.logger);
474
485
  } finally {
475
486
  refreshInFlight = null;
@@ -477,6 +488,88 @@ function refreshCachedTools(api) {
477
488
  })();
478
489
  return refreshInFlight;
479
490
  }
491
+ /**
492
+ * Should this `activate()` invocation also start the live IPC client?
493
+ *
494
+ * Decision rule: only in full agent-runtime mode. CLI invocations
495
+ * (`openclaw config set`, `openclaw plugins inspect`, `openclaw upgrade`,
496
+ * etc.) and tool-discovery loads (where OpenClaw builds the agent's
497
+ * tool list without running plugin services) should NOT open the
498
+ * gateway socket — they only need the disk cache and the static tool
499
+ * registrations.
500
+ *
501
+ * `api.registrationMode` is the canonical signal. OpenClaw 2026.5
502
+ * sets it to one of: `'full'`, `'discovery'`, `'tool-discovery'`,
503
+ * `'setup-only'`, `'setup-runtime'`, `'cli-metadata'`. Only `'full'`
504
+ * means "real agent runtime; everything is allowed to run".
505
+ *
506
+ * Defensive default: if `registrationMode` is unset (older OpenClaw,
507
+ * non-bundled host, test harness), assume full mode so we don't
508
+ * silently degrade. The IPC client itself handles a missing socket
509
+ * gracefully via reconnect, so an over-eager start is cheap.
510
+ */
511
+ function shouldStartIpcClient(api) {
512
+ const mode = api.registrationMode;
513
+ if (mode === void 0) return true;
514
+ return mode === "full";
515
+ }
516
+ /**
517
+ * Construct the live IPC client and wire its `onConnect` to a cache
518
+ * refresh. Module-scoped — the same client lifecycle outlasts a single
519
+ * `activate()` call so reconnects from daemon restarts continue
520
+ * automatically. Called from `activate()` ONLY in full agent-runtime
521
+ * mode (see `shouldStartIpcClient`).
522
+ *
523
+ * IMPORTANT: this function is NOT awaited. The `onConnect` callback
524
+ * fires asynchronously; the dynamic factory returns whatever is in
525
+ * `cachedDescriptors` at factory-invocation time (loaded from disk in
526
+ * `activate()`, then refreshed on connect, then periodically). A first-
527
+ * turn pre-warm is no longer needed — the disk cache covers turn-1,
528
+ * and any new tools discovered during the in-flight `onConnect`
529
+ * refresh land in `cachedDescriptors` before the LLM picks them up on
530
+ * turn-2.
531
+ *
532
+ * Removed the v0.0.13 PREWARM_TIMEOUT_MS / blocking pre-warm because
533
+ * v0.0.14 has no `service.start` await point to attach it to —
534
+ * removing `api.registerService` was the whole point of the fix. Disk
535
+ * cache + periodic refresh now carry the freshness contract.
536
+ */
537
+ function startIpcClient(api, log) {
538
+ if (ipcClient) return;
539
+ if (!existsSync(GATEWAY_SOCKET)) log.debug(`[mcp-bundler] ${GATEWAY_SOCKET} not present at activate — IPC client will refresh when the daemon comes online`);
540
+ ipcClient = new McpIpcClient({
541
+ logger: log,
542
+ plugin: {
543
+ name: "@alfe.ai/openclaw-mcp-bundler",
544
+ version: pkg.version
545
+ },
546
+ onConnect: () => {
547
+ refreshCachedTools(api);
548
+ }
549
+ });
550
+ ipcClient.start();
551
+ periodicRefreshTimer = setInterval(() => {
552
+ if (ipcClient?.isConnected()) refreshCachedTools(api);
553
+ }, PERIODIC_REFRESH_INTERVAL_MS);
554
+ periodicRefreshTimer.unref?.();
555
+ }
556
+ /**
557
+ * Tear down the IPC client and periodic refresh timer. Called from
558
+ * `deactivate()` (the only lifecycle exit point now that v0.0.14
559
+ * dropped `api.registerService`). Safe to call multiple times — each
560
+ * tear-down is idempotent.
561
+ */
562
+ function stopIpcClient() {
563
+ if (periodicRefreshTimer !== null) {
564
+ clearInterval(periodicRefreshTimer);
565
+ periodicRefreshTimer = null;
566
+ }
567
+ if (ipcClient) {
568
+ ipcClient.stop();
569
+ ipcClient = void 0;
570
+ }
571
+ refreshInFlight = null;
572
+ }
480
573
  const plugin = {
481
574
  id: "@alfe.ai/openclaw-mcp-bundler",
482
575
  name: "Alfe MCP Bundler",
@@ -517,7 +610,9 @@ const plugin = {
517
610
  "xero-xero__xero_check_connection",
518
611
  "xero-xero__xero_refresh_token",
519
612
  "alfe-platform__session_status",
520
- "alfe-platform__gateway_status"
613
+ "alfe-platform__gateway_status",
614
+ "alfe-platform__integrations_browse_registry",
615
+ "alfe-platform__integrations_start_oauth"
521
616
  ] },
522
617
  activate(api) {
523
618
  const log = api.logger;
@@ -531,11 +626,6 @@ const plugin = {
531
626
  log.info("[mcp-bundler] disabled via plugin config — no-op");
532
627
  return;
533
628
  }
534
- if (!api.registerService) {
535
- g[ACTIVATED_KEY] = false;
536
- log.warn("[mcp-bundler] api.registerService unavailable — plugin is a no-op on this runtime");
537
- return;
538
- }
539
629
  const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));
540
630
  if (deferToNative) log.info("[mcp-bundler] openclaw.json#mcp.servers has entries — MCP-proxy factory will defer to native bundleMcp, but agent self-service tools (alfe_mcp_*) stay available");
541
631
  cachedDescriptors = loadCachedDescriptorsFromDisk(log);
@@ -544,69 +634,14 @@ const plugin = {
544
634
  if (hasNativeOpenclawMcpServers(resolveLiveConfig(api, ctx))) return null;
545
635
  refreshCachedTools(api);
546
636
  return cachedDescriptors.map((d) => buildAgentTool(api, d));
547
- }, { optional: true });
548
- const startService = async () => {
549
- const prewarmTimer = { handle: null };
550
- if (await new Promise((resolve) => {
551
- prewarmTimer.handle = setTimeout(() => {
552
- prewarmTimer.handle = null;
553
- resolve("timeout");
554
- }, PREWARM_TIMEOUT_MS);
555
- ipcClient = new McpIpcClient({
556
- logger: log,
557
- plugin: {
558
- name: plugin.id,
559
- version: plugin.version
560
- },
561
- onConnect: () => {
562
- refreshCachedTools(api).finally(() => {
563
- if (prewarmTimer.handle !== null) {
564
- clearTimeout(prewarmTimer.handle);
565
- prewarmTimer.handle = null;
566
- }
567
- resolve("completed");
568
- });
569
- }
570
- });
571
- ipcClient.start();
572
- }) === "timeout") log.warn(`[mcp-bundler] pre-warm timed out after ${String(PREWARM_TIMEOUT_MS)}ms — first chat turn may see a stale tool list (likely a slow or unreachable daemon at ~/.alfe/gateway.sock)`);
573
- periodicRefreshTimer = setInterval(() => {
574
- if (ipcClient?.isConnected()) refreshCachedTools(api);
575
- }, PERIODIC_REFRESH_INTERVAL_MS);
576
- periodicRefreshTimer.unref?.();
577
- if (deferToNative) {
578
- log.info(`[mcp-bundler] activated v${plugin.version} (management tools only — native bundleMcp owns runtime)`);
579
- return;
580
- }
581
- log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client, tools registered in activate)`);
582
- };
583
- const stopService = () => {
584
- log.info("[mcp-bundler] stopping");
585
- if (periodicRefreshTimer !== null) {
586
- clearInterval(periodicRefreshTimer);
587
- periodicRefreshTimer = null;
588
- }
589
- if (ipcClient) {
590
- ipcClient.stop();
591
- ipcClient = void 0;
592
- }
593
- refreshInFlight = null;
594
- g[ACTIVATED_KEY] = false;
595
- return Promise.resolve();
596
- };
597
- api.registerService({
598
- id: "alfe-mcp-bundler",
599
- start: async () => {
600
- await startService();
601
- },
602
- stop: async () => {
603
- await stopService();
604
- }
605
637
  });
638
+ log.info(`[mcp-bundler] activated v${plugin.version} (registrationMode=${api.registrationMode ?? "unknown"}, deferToNative=${String(deferToNative)}, cachedDescriptors=${String(cachedDescriptors.length)})`);
639
+ if (shouldStartIpcClient(api)) startIpcClient(api, log);
606
640
  },
607
641
  deactivate(api) {
608
642
  g[ACTIVATED_KEY] = false;
609
643
  api.logger.debug("[mcp-bundler] deactivating");
644
+ stopIpcClient();
610
645
  }
611
646
  };
612
647
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"plugin2.js","names":[],"sources":["../src/cli-backend-detect.ts","../src/ipc-client.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Decide whether to defer to OpenClaw's native `bundleMcp:true` path for\n * this run. Pre-May-2026 this gated on backend type (claude-cli /\n * codex-cli), but the actual hazard was double-registration: when\n * `openclaw.json#mcp.servers` had entries AND the plugin registered the\n * same tools via `api.registerTool`, claude-cli's bundleMcp would spawn\n * its own copy and OpenClaw would suffix-disambiguate the duplicates.\n *\n * The single-source-of-truth refactor stops mirror-writing the alfe\n * store into openclaw.json, so `mcp.servers` is empty in practice. The\n * native path on claude-cli has nothing to spawn, our plugin's IPC path\n * is the only one in play, and we register on every backend.\n *\n * The escape hatch survives: a user who hand-edits `mcp.servers` in\n * openclaw.json (private dev MCP, etc.) gets the native path back —\n * this function returns true, the plugin no-ops, and claude-cli/codex-cli\n * spawn that entry themselves.\n */\n\ninterface ConfigLike {\n mcp?: {\n servers?: Record<string, unknown>;\n };\n}\n\n/**\n * Returns true when openclaw.json#mcp.servers has at least one entry —\n * signalling that the native bundleMcp path is in play for THIS run\n * (regardless of backend) and the plugin should defer to it.\n *\n * Empty object / missing key → false → plugin registers IPC-proxied tools.\n */\nexport function hasNativeOpenclawMcpServers(cfg: ConfigLike | undefined): boolean {\n if (!cfg) return false;\n const servers = cfg.mcp?.servers;\n if (!servers || typeof servers !== 'object') return false;\n return Object.keys(servers).length > 0;\n}\n\n/**\n * Back-compat alias for callers still on the old name. Internal — drop\n * once we cut the next major.\n */\nexport const detectNativeBundleMcp = hasNativeOpenclawMcpServers;\n","/**\n * Minimal newline-delimited-JSON IPC client over Unix socket. Talks to\n * the alfe-gateway daemon at `~/.alfe/gateway.sock` so this plugin can\n * proxy `mcp.list_tools` / `mcp.call_tool` requests to the daemon-hosted\n * `McpBundler` rather than spawning its own children.\n *\n * Why minimal: the @alfe.ai/openclaw package already ships a full IPC\n * client with reconnect + event handling, but pulling it in would create\n * a (semantically) circular dep — plugin importing from its host. This\n * file is ~120 lines and only does what the bundler plugin needs.\n *\n * The daemon's IPC server (`packages/gateway/src/ipc-server.ts`) speaks:\n * Request: { type: 'req', id, method, params }\n * Response: { id, ok, payload?, error? }\n * Event: { type: 'event', event, payload } ← ignored here, we never subscribe.\n */\nimport { createConnection, type Socket } from 'node:net';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nconst DEFAULT_SOCKET_PATH = join(homedir(), '.alfe', 'gateway.sock');\nconst RECONNECT_MIN_MS = 500;\nconst RECONNECT_MAX_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nexport interface IpcResponse<T = unknown> {\n id: string;\n ok: boolean;\n payload?: T;\n error?: { code: string; message: string };\n}\n\ninterface Logger {\n debug(msg: string, ctx?: Record<string, unknown>): void;\n info(msg: string, ctx?: Record<string, unknown>): void;\n warn(msg: string, ctx?: Record<string, unknown>): void;\n error(msg: string, ctx?: Record<string, unknown>): void;\n}\n\nexport interface McpIpcClientOptions {\n socketPath?: string;\n logger?: Logger;\n /** Plugin identity broadcast to the daemon on connect. */\n plugin: { name: string; version: string };\n /**\n * Fires every time the socket transitions from disconnected to\n * connected — i.e. on the initial connect AND on every reconnect after\n * the daemon restarts. Invoked AFTER `connected = true` is set so\n * `call(...)` from inside the handler will actually round-trip the\n * daemon (the previous shape called `refreshCachedTools` synchronously\n * inside the plugin's `startService` while `connected` was still false,\n * which silently returned `NOT_CONNECTED` and left the tool cache\n * permanently empty on the first session — see plugin.ts comment for\n * the QA Tester repro).\n *\n * Contract:\n * - Treated as fire-and-forget. The IPC client does NOT await the\n * handler's return value.\n * - Synchronous throws are caught + logged at warn — they will not\n * tear down the IPC client.\n * - If a handler returns a Promise that rejects later, the rejection\n * surfaces as an unhandled promise rejection. Wrap with `void` or\n * handle internally if you don't want that behaviour. The wired\n * handler in `plugin.ts` does exactly that:\n * `() => { void refreshCachedTools(api); }`.\n */\n onConnect?: () => void;\n}\n\ninterface PendingRequest {\n resolve: (response: IpcResponse) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Plugin-side IPC client. Single instance per plugin lifecycle: start on\n * `registerService.start`, stop on `registerService.stop`. Calls\n * before the socket is connected return a typed `NOT_CONNECTED` response\n * so the plugin can fall back to \"no tools yet\" rather than throwing.\n */\nexport class McpIpcClient {\n private socket: Socket | null = null;\n private buffer = '';\n private backoffMs = RECONNECT_MIN_MS;\n private closed = false;\n private connected = false;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private pending = new Map<string, PendingRequest>();\n private readonly socketPath: string;\n private readonly logger?: Logger;\n private readonly plugin: { name: string; version: string };\n private readonly onConnect?: () => void;\n\n constructor(opts: McpIpcClientOptions) {\n this.socketPath = opts.socketPath ?? DEFAULT_SOCKET_PATH;\n this.logger = opts.logger;\n this.plugin = opts.plugin;\n this.onConnect = opts.onConnect;\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n start(): void {\n this.closed = false;\n this.openSocket();\n }\n\n stop(): void {\n this.closed = true;\n this.clearReconnectTimer();\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer);\n pending.resolve({\n id,\n ok: false,\n error: { code: 'CLIENT_STOPPED', message: 'IPC client stopped' },\n });\n }\n this.pending.clear();\n if (this.socket) {\n try {\n this.socket.end();\n } catch {\n // ignore — socket may already be in a half-closed state\n }\n this.socket = null;\n }\n this.connected = false;\n }\n\n async call<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n ): Promise<IpcResponse<T>> {\n if (!this.connected || !this.socket) {\n return {\n id: '',\n ok: false,\n error: { code: 'NOT_CONNECTED', message: 'Not connected to alfe-gateway' },\n };\n }\n const id = randomUUID();\n return new Promise<IpcResponse<T>>((resolve) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n resolve({\n id,\n ok: false,\n error: { code: 'TIMEOUT', message: `${method} timed out after ${String(REQUEST_TIMEOUT_MS)}ms` },\n });\n }, REQUEST_TIMEOUT_MS);\n this.pending.set(id, { resolve: resolve as (r: IpcResponse) => void, timer });\n try {\n this.socket?.write(`${JSON.stringify({ type: 'req', id, method, params })}\\n`);\n } catch (err) {\n clearTimeout(timer);\n this.pending.delete(id);\n resolve({\n id,\n ok: false,\n error: {\n code: 'SEND_FAILED',\n message: err instanceof Error ? err.message : String(err),\n },\n });\n }\n });\n }\n\n private openSocket(): void {\n if (this.closed) return;\n this.buffer = '';\n const socket = createConnection(this.socketPath, () => {\n this.logger?.info('[mcp-bundler/ipc] connected', { socket: this.socketPath });\n this.connected = true;\n this.backoffMs = RECONNECT_MIN_MS;\n // Best-effort register. We don't await the response — the daemon\n // routes mcp.* methods even on unregistered connections, so the\n // tool-list / tool-call path doesn't need register to complete.\n void this.call('register', {\n name: this.plugin.name,\n version: this.plugin.version,\n protocolVersion: 1,\n capabilities: ['mcp.list_tools', 'mcp.call_tool'],\n pid: process.pid,\n });\n // Notify caller AFTER `connected = true` so any `.call(...)` issued\n // from the handler (e.g. plugin.ts's tool-cache refresh) actually\n // makes it onto the wire instead of getting the NOT_CONNECTED\n // short-circuit at the top of `call()`.\n if (this.onConnect) {\n try {\n this.onConnect();\n } catch (err) {\n this.logger?.warn('[mcp-bundler/ipc] onConnect handler threw', {\n err: err instanceof Error ? err.message : String(err),\n });\n }\n }\n });\n this.socket = socket;\n\n socket.on('data', (data: Buffer) => {\n this.buffer += data.toString();\n this.processBuffer();\n });\n\n socket.on('close', () => {\n this.connected = false;\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer);\n pending.resolve({\n id,\n ok: false,\n error: { code: 'DISCONNECTED', message: 'Connection closed' },\n });\n }\n this.pending.clear();\n this.socket = null;\n if (!this.closed) this.scheduleReconnect();\n });\n\n socket.on('error', (err: Error) => {\n this.logger?.debug('[mcp-bundler/ipc] socket error', { err: err.message });\n // 'close' fires after 'error'; reconnect handled there.\n });\n }\n\n private processBuffer(): void {\n let idx: number;\n while ((idx = this.buffer.indexOf('\\n')) !== -1) {\n const line = this.buffer.slice(0, idx).trim();\n this.buffer = this.buffer.slice(idx + 1);\n if (!line) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!isIpcResponse(parsed)) continue;\n const pending = this.pending.get(parsed.id);\n if (!pending) continue;\n clearTimeout(pending.timer);\n this.pending.delete(parsed.id);\n pending.resolve(parsed);\n }\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer) return;\n const delay = this.backoffMs;\n this.backoffMs = Math.min(this.backoffMs * 2, RECONNECT_MAX_MS);\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.openSocket();\n }, delay);\n if (typeof this.reconnectTimer === 'object' && 'unref' in this.reconnectTimer) {\n (this.reconnectTimer as { unref: () => void }).unref();\n }\n }\n\n private clearReconnectTimer(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n}\n\nfunction isIpcResponse(value: unknown): value is IpcResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'id' in value &&\n 'ok' in value &&\n typeof (value as { id: unknown }).id === 'string'\n );\n}\n","/**\n * @alfe.ai/openclaw-mcp-bundler — OpenClaw plugin entry\n *\n * Thin IPC client over the alfe-gateway daemon. The daemon hosts the\n * actual `McpBundler` (children + tool catalog), this plugin forwards\n * the OpenClaw tool-surface to that bundler via `mcp.list_tools` and\n * `mcp.call_tool` over `~/.alfe/gateway.sock`.\n *\n * Works on every LLM backend — `api.registerTool` registrations reach\n * MiniMax/Mistral/generic AND claude-cli/codex-cli. The previous\n * blanket no-op on claude-cli was load-bearing only when the same MCP\n * was ALSO in `openclaw.json#mcp.servers` (double registration);\n * since the alfe store no longer mirror-writes there, the native\n * bundleMcp path has nothing to spawn and we can register on every\n * backend uniformly.\n *\n * Escape hatch: a user who hand-adds an entry to\n * `openclaw.json#mcp.servers` for a private dev MCP gets the native\n * path back for that run — `hasNativeOpenclawMcpServers` returns true,\n * we no-op, claude-cli/codex-cli spawn that entry themselves.\n */\n\nimport type { McpToolDescriptor, McpToolCallResult } from '@alfe.ai/mcp-bundler';\nimport { hasNativeOpenclawMcpServers } from './cli-backend-detect.js';\nimport { McpIpcClient, type IpcResponse } from './ipc-client.js';\nimport { createRequire } from 'node:module';\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\n// Disk-backed snapshot of the daemon's last-known tool catalogue. Tool-\n// discovery mode (used by OpenClaw whenever it builds the agent's tool\n// list — see `resolvePluginTools` in openclaw's loader) sets\n// `runtimeChannel: false`, so `api.registerService(...)` callbacks\n// (where the IPC client lives) NEVER run in that mode. Without a disk\n// snapshot the plugin contributes zero tools to the agent's list every\n// time it's loaded for tool-discovery — exactly the QA Tester symptom\n// traced 2026-06-09 on bundler v0.0.10. Cache file is written by\n// `refreshCachedTools` on every successful `mcp.list_tools` and read at\n// `activate` time. Lives under the alfe daemon's state dir so the same\n// box's full-mode and tool-discovery-mode loads share it.\nconst CACHE_FILE = join(homedir(), '.alfe', 'mcp', 'openclaw-bundler-cache.json');\n\n// ── Type shims for the OpenClaw plugin SDK ──────────────────────\n// We avoid a hard dep on the SDK to keep this plugin loadable across\n// OpenClaw versions; the surface used here is stable.\n\ninterface Logger {\n debug(msg: string, ...args: unknown[]): void;\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n}\n\ninterface ConfigSnapshot {\n agents?: { defaults?: { model?: string; cliBackends?: Record<string, unknown> } };\n mcp?: { servers?: Record<string, unknown> };\n}\n\ninterface AgentToolResult {\n content: Record<string, unknown>[];\n details?: Record<string, unknown>;\n}\n\ninterface AgentTool {\n name: string;\n label?: string;\n description: string;\n parameters: Record<string, unknown>;\n execute: (\n toolCallId: string,\n params: unknown,\n signal?: AbortSignal,\n onUpdate?: (update: unknown) => void,\n ) => Promise<AgentToolResult>;\n}\n\ninterface ToolFactoryContext {\n config?: ConfigSnapshot;\n runtimeConfig?: ConfigSnapshot;\n getRuntimeConfig?: () => ConfigSnapshot | undefined;\n}\n\ninterface OpenClawPluginService {\n id: string;\n start: (ctx?: unknown) => void | Promise<void>;\n stop?: (ctx?: unknown) => void | Promise<void>;\n}\n\ninterface OpenClawPluginApi {\n logger: Logger;\n config?: ConfigSnapshot;\n pluginConfig?: { disabled?: boolean };\n registrationMode?: string;\n runtime?: { config?: { current?: () => ConfigSnapshot | undefined } };\n registerTool: (\n tool: AgentTool | ((ctx: ToolFactoryContext) => AgentTool | AgentTool[] | null | undefined),\n opts?: { name?: string; names?: string[]; optional?: boolean },\n ) => void;\n registerService?: (service: OpenClawPluginService) => void;\n}\n\n// ── Plugin state ───────────────────────────────────────────────\nlet ipcClient: McpIpcClient | undefined;\n// Module-scoped catalogue. We keep `McpToolDescriptor` here (rather\n// than pre-baked `AgentTool` objects) so the catalogue is JSON-\n// serialisable for the disk cache. `buildAgentTool` constructs an\n// `AgentTool` at factory-invocation time and closes over the live\n// `ipcClient` via the module-scoped reference, so the\n// tools work as soon as IPC is ready — without serialising the\n// IPC client into the cache.\nlet cachedDescriptors: McpToolDescriptor[] = [];\nlet refreshInFlight: Promise<void> | null = null;\nlet periodicRefreshTimer: ReturnType<typeof setInterval> | null = null;\nconst ACTIVATED_KEY = '__alfeMcpBundlerPluginActivated';\nconst g = globalThis as Record<string, unknown>;\n\n// Pre-warm budget for `service.start`. The daemon's bundler may take\n// 100-500ms to do the first `mcp.list_tools` round-trip; spawning\n// children via npx can push this higher. 3s is generous enough to\n// cover a cold start without blocking gateway startup indefinitely\n// if the socket is missing. The warn log on timeout (NOT silent) is\n// how we noticed the v0.0.8-v0.0.11 onStartup regression after the\n// fact — keep it loud.\nconst PREWARM_TIMEOUT_MS = 3000;\n\n// Periodic refresh cadence. The bundler's `onConnect` refresh fires\n// ONCE; if the daemon's MCP children come online after that initial\n// `mcp.list_tools` returns (e.g. a slow npx-fetched provider proxy),\n// without polling the bundler's snapshot stays stuck on the partial\n// catalog it captured at startup. 10s is small enough for the LLM's\n// next-turn tool list to converge while staying well below the IPC\n// cost threshold (one `mcp.list_tools` call → tiny daemon-local\n// payload, no remote network).\nconst PERIODIC_REFRESH_INTERVAL_MS = 10_000;\n\nfunction resolveLiveConfig(api: OpenClawPluginApi, ctx?: ToolFactoryContext): ConfigSnapshot | undefined {\n // Prefer most-recent → ctx live → ctx snapshot → api.runtime live → api.config snapshot\n return (\n ctx?.runtimeConfig ??\n ctx?.getRuntimeConfig?.() ??\n api.runtime?.config?.current?.() ??\n ctx?.config ??\n api.config\n );\n}\n\nfunction buildAgentTool(api: OpenClawPluginApi, tool: McpToolDescriptor): AgentTool {\n return {\n name: tool.prefixed,\n label: tool.label,\n description: tool.description || `MCP tool ${tool.original} from server ${tool.server}`,\n // OpenClaw expects a JSON-Schema-shaped object. The MCP server already\n // gives us one in `tool.parameters`. Pass through unchanged.\n parameters: tool.parameters,\n async execute(_toolCallId, params) {\n // `ipcClient` is module-scoped and set by `startService` at runtime.\n // In tool-discovery mode (no service.start) `ipcClient` is\n // undefined here — return a graceful error so the LLM gets a\n // useful message rather than a hard crash. In full mode this\n // closure resolves at execute() time, after `service.start` has\n // wired the IPC client.\n if (!ipcClient?.isConnected()) {\n return errorResult(\n `mcp ${tool.prefixed}: bundler IPC not connected yet. Try again in a moment.`,\n );\n }\n const response = await ipcClient.call<McpToolCallResult>('mcp.call_tool', {\n name: tool.prefixed,\n args: params,\n });\n if (!response.ok || !response.payload) {\n api.logger.warn('[mcp-bundler] tool call failed', {\n tool: tool.prefixed,\n err: response.error?.message ?? 'unknown error',\n });\n return errorResult(\n response.error?.message\n ? `mcp ${tool.prefixed}: ${response.error.message}`\n : `mcp ${tool.prefixed}: call failed`,\n );\n }\n const result = response.payload;\n return {\n content: result.content,\n details: { isError: result.isError ?? false },\n };\n },\n };\n}\n\nfunction errorResult(message: string): AgentToolResult {\n return {\n content: [{ type: 'text', text: message }],\n details: { isError: true },\n };\n}\n\n/**\n * Async-aware shim around the module-scoped `ipcClient`. The management\n * tools and dynamic MCP tools registered in `activate()` execute LATER\n * (when the agent invokes them), but `ipcClient` is only set inside\n * `startService`. The `?.` chains return useful errors when the agent\n * tries to call a tool before the IPC is ready, instead of throwing.\n */\nasync function callIpc<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n): Promise<IpcResponse<T>> {\n if (!ipcClient) {\n return {\n id: '',\n ok: false,\n error: { code: 'NOT_CONNECTED', message: 'Bundler IPC client not initialized yet' },\n };\n }\n return ipcClient.call<T>(method, params);\n}\n\n/**\n * Load the last-known tool catalogue from disk. Used at `activate()`\n * time so the agent's tool list contains real tools even in tool-\n * discovery mode (where `service.start` — and therefore the IPC\n * connection — never runs). Silently falls back to an empty list on\n * any read/parse failure so a first-run agent (no cache file yet)\n * still loads cleanly.\n */\nfunction isValidDescriptor(value: unknown): value is McpToolDescriptor {\n // Cheap shape check guarding against a corrupt or torn-write cache —\n // a half-written JSON array that parses but holds garbage entries\n // would otherwise reach `buildAgentTool` where `tool.prefixed` ends up\n // undefined and registers a tool with `name: undefined`. Catching the\n // shape here protects the agent's tool list from disk-level corruption.\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.prefixed === 'string' &&\n typeof v.original === 'string' &&\n typeof v.server === 'string' &&\n typeof v.parameters === 'object' &&\n v.parameters !== null\n );\n}\n\nfunction loadCachedDescriptorsFromDisk(logger: Logger): McpToolDescriptor[] {\n try {\n const raw = readFileSync(CACHE_FILE, 'utf-8');\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(isValidDescriptor);\n } catch (err) {\n if (err instanceof Error && (err as NodeJS.ErrnoException).code !== 'ENOENT') {\n logger.debug('[mcp-bundler] cache read failed', { err: err.message });\n }\n return [];\n }\n}\n\n/**\n * Persist the current catalogue to disk so the next tool-discovery\n * load sees the tools. Best-effort — disk failures are logged at\n * `warn` and otherwise swallowed.\n */\nfunction persistCachedDescriptorsToDisk(\n descriptors: McpToolDescriptor[],\n logger: Logger,\n): void {\n // Atomic write: write to a temp sibling then rename. `writeFileSync` is\n // NOT atomic on its own — two processes racing on the same path (a\n // daemon restart concurrent with a tool-discovery load's inline\n // refresh, say) can interleave bytes and corrupt the JSON. The\n // tool-discovery load then silently drops the catalogue until the\n // next successful refresh. `renameSync` is atomic on the same\n // filesystem on every platform we ship to.\n const tmp = `${CACHE_FILE}.${randomUUID()}.tmp`;\n try {\n mkdirSync(dirname(CACHE_FILE), { recursive: true });\n writeFileSync(tmp, JSON.stringify(descriptors));\n renameSync(tmp, CACHE_FILE);\n } catch (err) {\n logger.warn('[mcp-bundler] cache write failed', {\n err: err instanceof Error ? err.message : String(err),\n });\n }\n}\n\n/**\n * Self-service tools the agent can call to add/remove MCP servers in\n * the Alfe bundler store. The agent gets the same surface the CLI's\n * `alfe mcp add/remove/list` commands expose — same IPC methods, same\n * `'manual'` ownership semantics. Daemon-owned (`'cli'` /\n * `'integration:*'`) entries can't be clobbered.\n */\nfunction registerAgentMcpManagementTools(api: OpenClawPluginApi): void {\n // Tools are registered during `activate()` (whether or not the\n // service has started yet), so we can't capture a live `ipcClient`\n // here. Each tool's `execute()` reaches the module-scoped\n // `ipcClient` via `callIpc()` at invocation time — by then,\n // `service.start` has wired the IPC client in full mode. In tool-\n // discovery mode (where service.start never runs) these tools still\n // appear in the agent's tool list, but executing them returns a\n // `NOT_CONNECTED` error from `callIpc` rather than crashing.\n\n api.registerTool({\n name: 'alfe_mcp_list',\n label: 'List Alfe MCP servers',\n description:\n 'List every MCP server currently registered in the Alfe bundler store, with id, owner, and transport. Use this before adding a new server to avoid name collisions or to confirm what is currently available to call.',\n parameters: { type: 'object', properties: {}, additionalProperties: false },\n async execute() {\n const response = await callIpc<{ servers: unknown[] }>('mcp.list_servers', {});\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_list failed');\n }\n return {\n content: [{ type: 'text', text: JSON.stringify(response.payload?.servers ?? [], null, 2) }],\n details: { isError: false },\n };\n },\n });\n\n api.registerTool({\n name: 'alfe_mcp_add',\n label: 'Register an MCP server with Alfe',\n description:\n 'Register a new MCP server in the Alfe bundler store. The Alfe daemon will spawn the child and its tools become available to the agent on the next turn (no runtime restart). Use a stdio command for local-process MCPs (most common) or url+transport for remote SSE / streamable-http MCPs.',\n parameters: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique id under the bundler store. Letters, digits, hyphens. Becomes the `mcp__<id>__` prefix on every tool name from this server.',\n },\n command: {\n type: 'string',\n description: 'Executable to spawn for stdio transport (e.g. \"npx\", \"uvx\", \"/path/to/bin\"). Required for stdio mode.',\n },\n args: {\n type: 'array',\n items: { type: 'string' },\n description: 'Arguments to pass to the command. Optional.',\n },\n env: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'Environment variables for the child process. Optional.',\n },\n cwd: {\n type: 'string',\n description: 'Working directory for the child process. Optional.',\n },\n url: {\n type: 'string',\n description: 'Remote MCP endpoint URL. Required for sse / streamable-http transport.',\n },\n transport: {\n type: 'string',\n enum: ['sse', 'streamable-http'],\n description: 'Remote MCP transport. Defaults to \"sse\" when url is set.',\n },\n headers: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'HTTP headers for remote MCP transports. Optional.',\n },\n },\n required: ['id'],\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n const response = await callIpc<{ id: string }>('mcp.add_server', (params ?? {}) as Record<string, unknown>);\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_add failed');\n }\n return {\n content: [{ type: 'text', text: `Registered MCP server \"${response.payload?.id ?? '?'}\". Tools will appear on the next turn.` }],\n details: { isError: false },\n };\n },\n });\n\n api.registerTool({\n name: 'alfe_mcp_remove',\n label: 'Unregister an MCP server from Alfe',\n description:\n 'Remove an MCP server you previously registered with alfe_mcp_add. Only entries you registered (owner=manual) can be removed this way — integration-installed servers (atlassian, github, etc.) and the built-in alfe-platform server are owner-protected and must be removed via the dashboard or CLI.',\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Bundler store id of the entry to remove.' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n const response = await callIpc<{ removed: boolean }>(\n 'mcp.remove_server',\n (params ?? {}) as Record<string, unknown>,\n );\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_remove failed');\n }\n const removed = response.payload?.removed ?? false;\n return {\n content: [\n {\n type: 'text',\n text: removed\n ? `Removed MCP server. Its tools will disappear on the next turn.`\n : `No MCP server matched — nothing to remove.`,\n },\n ],\n details: { isError: false },\n };\n },\n });\n}\n\n/**\n * Pull the latest tool catalog from the daemon-hosted bundler. Coalesces\n * concurrent calls so the LLM hitting the factory many times in a tight\n * loop only triggers one in-flight IPC request.\n */\nfunction refreshCachedTools(api: OpenClawPluginApi): Promise<void> {\n if (!ipcClient) return Promise.resolve();\n if (refreshInFlight) return refreshInFlight;\n refreshInFlight = (async () => {\n try {\n const response = await callIpc<{ tools: McpToolDescriptor[] }>('mcp.list_tools', {});\n if (!response.ok || !response.payload) {\n // NOT_CONNECTED / TIMEOUT / etc — keep the prior cache so a\n // transient daemon hiccup doesn't drop tools mid-session.\n api.logger.debug('[mcp-bundler] list_tools failed', {\n err: response.error?.message ?? 'unknown',\n });\n return;\n }\n cachedDescriptors = response.payload.tools;\n // Persist for tool-discovery-mode loads — that's how this fix\n // lands tools on the agent's tool list. See CACHE_FILE comment.\n persistCachedDescriptorsToDisk(cachedDescriptors, api.logger);\n } finally {\n refreshInFlight = null;\n }\n })();\n return refreshInFlight;\n}\n\nconst plugin = {\n id: '@alfe.ai/openclaw-mcp-bundler',\n name: 'Alfe MCP Bundler',\n description: 'Proxies the daemon-hosted MCP bundler tools to the LLM',\n version: pkg.version,\n contracts: {\n // OpenClaw's `contracts.tools` matcher (in\n // `loader.js#registerTool` and `tools.js#resolvePluginTools`) uses\n // strict literal `Set.has()` — it does NOT honour glob patterns. So\n // the previous `'mcp__*'` entry was non-functional: every tool the\n // factory returned whose name started with anything else (e.g.\n // `atlassian-atlassian__atlassian_list_accounts`) was silently\n // rejected as \"undeclared\" at runtime, and never surfaced to the\n // agent. Diagnosed 2026-06-09 on QA Tester after the v0.0.9 IPC\n // fix proved correct but tools still didn't appear in chat\n // sessions. See CHANGELOG for the trace.\n //\n // This list enumerates every tool name the proxy can return today.\n // The three `alfe_mcp_*` entries are the agent self-service\n // surface. The `<provider>-<provider>__<provider>_*` entries are\n // the Pattern-A proxy meta-tools (list_accounts /\n // check_connection / refresh_token) each provider's MCP server\n // surfaces before a cloudId is selected. The `alfe-platform__*`\n // entries cover the CLI-managed alfe-platform MCP server.\n //\n // ADDING A NEW INTEGRATION: append its 3 Pattern-A meta-tools\n // here AND any post-cloudId tools the proxy exposes that you want\n // the agent to be able to invoke. There is no shortcut — until\n // OpenClaw learns glob patterns in `contracts.tools` (tracked\n // separately) every tool name must be listed literally.\n tools: [\n 'alfe_mcp_list',\n 'alfe_mcp_add',\n 'alfe_mcp_remove',\n 'atlassian-atlassian__atlassian_list_accounts',\n 'atlassian-atlassian__atlassian_check_connection',\n 'atlassian-atlassian__atlassian_refresh_token',\n 'notion-notion__notion_list_accounts',\n 'notion-notion__notion_check_connection',\n 'notion-notion__notion_refresh_token',\n 'github-github__github_list_accounts',\n 'github-github__github_check_connection',\n 'github-github__github_refresh_token',\n 'microsoft-microsoft__microsoft_list_accounts',\n 'microsoft-microsoft__microsoft_check_connection',\n 'microsoft-microsoft__microsoft_refresh_token',\n 'google-google__google_list_accounts',\n 'google-google__google_check_connection',\n 'google-google__google_refresh_token',\n 'slack-slack__slack_list_accounts',\n 'slack-slack__slack_check_connection',\n 'slack-slack__slack_refresh_token',\n 'discord-discord__discord_list_accounts',\n 'discord-discord__discord_check_connection',\n 'discord-discord__discord_refresh_token',\n 'mobile-mobile__mobile_list_accounts',\n 'mobile-mobile__mobile_check_connection',\n 'mobile-mobile__mobile_refresh_token',\n 'myob-myob__myob_list_accounts',\n 'myob-myob__myob_check_connection',\n 'myob-myob__myob_refresh_token',\n 'xero-xero__xero_list_accounts',\n 'xero-xero__xero_check_connection',\n 'xero-xero__xero_refresh_token',\n 'alfe-platform__session_status',\n 'alfe-platform__gateway_status',\n ],\n },\n\n activate(api: OpenClawPluginApi) {\n const log = api.logger;\n\n if (g[ACTIVATED_KEY] === true) {\n log.debug('[mcp-bundler] already activated — skipping duplicate');\n return;\n }\n g[ACTIVATED_KEY] = true;\n\n if (api.pluginConfig?.disabled) {\n g[ACTIVATED_KEY] = false;\n log.info('[mcp-bundler] disabled via plugin config — no-op');\n return;\n }\n\n if (!api.registerService) {\n g[ACTIVATED_KEY] = false;\n log.warn('[mcp-bundler] api.registerService unavailable — plugin is a no-op on this runtime');\n return;\n }\n\n const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));\n if (deferToNative) {\n log.info(\n '[mcp-bundler] openclaw.json#mcp.servers has entries — MCP-proxy factory will defer to native bundleMcp, but agent self-service tools (alfe_mcp_*) stay available',\n );\n }\n\n // ── Register tools synchronously in activate() ──────────────\n //\n // OpenClaw loads plugins in \"tool-discovery\" mode whenever it\n // builds the agent's tool list (see `resolvePluginTools` in\n // `loader.js`). The registration plan for that mode sets\n // `runtimeChannel: false`, which means `api.registerService(...)`\n // start/stop callbacks are NEVER invoked there — only `activate`\n // runs. v0.0.8 → v0.0.10 put tool registration inside\n // `service.start`, so tool-discovery loads contributed ZERO tools\n // to the agent's list (QA Tester repro traced 2026-06-09). We now\n // register every tool here in `activate` so they show up in both\n // modes. The IPC client + lifecycle stays in `registerService` —\n // each tool's `execute()` reaches the module-scoped `ipcClient`\n // lazily via `callIpc()`, so tools registered without a live IPC\n // still show up in the catalogue and return a graceful\n // NOT_CONNECTED error if invoked before `service.start` lands.\n\n // Seed the descriptor cache from disk so tool-discovery mode\n // (which has no IPC) can still return real MCP tools to the\n // agent. Refreshed and re-persisted by `refreshCachedTools` on\n // every IPC connect.\n cachedDescriptors = loadCachedDescriptorsFromDisk(log);\n\n // Agent self-service tools — always available, regardless of the\n // native-bundleMcp escape hatch. The alfe store is independent of\n // openclaw.json so agents can manage their MCP roster in either\n // mode.\n registerAgentMcpManagementTools(api);\n\n if (!deferToNative) {\n api.registerTool((ctx: ToolFactoryContext) => {\n const live = resolveLiveConfig(api, ctx);\n if (hasNativeOpenclawMcpServers(live)) return null;\n // Best-effort background refresh so each turn picks up any new\n // entries the daemon's bundler has discovered (only effective\n // when the IPC client is up — i.e. in full mode).\n void refreshCachedTools(api);\n return cachedDescriptors.map((d) => buildAgentTool(api, d));\n }, { optional: true });\n }\n\n // ── Service lifecycle: IPC client + cache freshness ────────\n //\n // Cache freshness is layered. Three independent tiers:\n //\n // 1. Disk cache (`~/.alfe/mcp/openclaw-bundler-cache.json`) —\n // the only source available in tool-discovery mode (no IPC).\n // Loaded in `activate()` BEFORE this service runs.\n // 2. Pre-warm — `service.start` awaits the first\n // `refreshCachedTools` (bounded by PREWARM_TIMEOUT_MS). The\n // first chat-turn after gateway boot then sees a fresh\n // catalog, NOT whatever stale snapshot the disk cache held.\n // 3. Periodic refresh — a setInterval picks up MCP children that\n // come online AFTER the pre-warm completed (slow npx fetches,\n // delayed integration provider spawns, late OAuth callbacks).\n //\n // Each tier is independent: a failure in one doesn't break the\n // others. The disk cache covers cold start in tool-discovery mode,\n // pre-warm covers turn-1 in agent-runtime mode, periodic refresh\n // covers steady-state staleness.\n const startService = async (): Promise<void> => {\n // Pre-warm deferred. Resolved either by `onConnect` finishing its\n // first refresh OR by a 3s timeout. We DO NOT throw on timeout —\n // a slow daemon shouldn't block gateway startup forever. We DO\n // log warn (not silent) so a consistently-timing-out daemon\n // shows up in CloudWatch instead of degrading quietly: the v0.0.8\n // - v0.0.11 onStartup regression took weeks to notice precisely\n // because there was no signal.\n const prewarmTimer: { handle: ReturnType<typeof setTimeout> | null } = { handle: null };\n const prewarm = new Promise<'completed' | 'timeout'>((resolve) => {\n prewarmTimer.handle = setTimeout(() => {\n // Clear the handle so the symmetric reset path in\n // `onConnect`'s `.finally` no-ops, instead of clearing an\n // already-fired timer.\n prewarmTimer.handle = null;\n resolve('timeout');\n }, PREWARM_TIMEOUT_MS);\n ipcClient = new McpIpcClient({\n logger: log,\n plugin: { name: plugin.id, version: plugin.version },\n // The onConnect path is the FAST path: IPC connects in\n // 50-300ms after `start()`, the refresh fires, and pre-warm\n // resolves. Only daemons that aren't reachable at all\n // (missing socket, dead daemon) hit the timeout branch.\n onConnect: () => {\n void refreshCachedTools(api).finally(() => {\n if (prewarmTimer.handle !== null) {\n clearTimeout(prewarmTimer.handle);\n prewarmTimer.handle = null;\n }\n resolve('completed');\n });\n },\n });\n ipcClient.start();\n });\n\n const prewarmResult = await prewarm;\n if (prewarmResult === 'timeout') {\n log.warn(\n `[mcp-bundler] pre-warm timed out after ${String(PREWARM_TIMEOUT_MS)}ms — first chat turn may see a stale tool list (likely a slow or unreachable daemon at ~/.alfe/gateway.sock)`,\n );\n }\n\n // Periodic refresh — see comment block above the function for\n // why this tier exists alongside pre-warm. Only fires when IPC\n // is connected; otherwise `refreshCachedTools` is a no-op.\n periodicRefreshTimer = setInterval(() => {\n if (ipcClient?.isConnected()) {\n void refreshCachedTools(api);\n }\n }, PERIODIC_REFRESH_INTERVAL_MS);\n // `unref()` lets the host process exit even if this timer is\n // still scheduled — important for the per-turn openclaw runtime\n // mode where a hanging interval would block process teardown.\n // Optional-chained because non-Node hosts (browsers, edge\n // runtimes) don't expose `.unref()` on Timeout handles.\n (periodicRefreshTimer as { unref?: () => void }).unref?.();\n\n if (deferToNative) {\n log.info(`[mcp-bundler] activated v${plugin.version} (management tools only — native bundleMcp owns runtime)`);\n return;\n }\n\n log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client, tools registered in activate)`);\n };\n\n const stopService = (): Promise<void> => {\n log.info('[mcp-bundler] stopping');\n if (periodicRefreshTimer !== null) {\n clearInterval(periodicRefreshTimer);\n periodicRefreshTimer = null;\n }\n if (ipcClient) {\n ipcClient.stop();\n ipcClient = undefined;\n }\n // Keep `cachedDescriptors` and the on-disk cache intact across\n // restarts — that's the WHOLE POINT of the disk cache. We only\n // tear down the live IPC reference here.\n refreshInFlight = null;\n // Reset the duplicate-activate guard so OpenClaw can re-run\n // `activate(api)` after a `service.stop` without going through a\n // full `deactivate`. Without this, a stop-then-reactivate path\n // (e.g. an OpenClaw reload that doesn't tear the plugin module\n // down) would silently skip ALL tool registration on the second\n // activate and the agent ends up with no bundler tools. The\n // guard's only job is preventing the same module from running\n // activate() twice concurrently — once the service is stopped,\n // that risk is gone.\n g[ACTIVATED_KEY] = false;\n return Promise.resolve();\n };\n\n api.registerService({\n id: 'alfe-mcp-bundler',\n start: async () => { await startService(); },\n stop: async () => { await stopService(); },\n });\n },\n\n deactivate(api: OpenClawPluginApi) {\n g[ACTIVATED_KEY] = false;\n api.logger.debug('[mcp-bundler] deactivating');\n },\n};\n\nexport { type IpcResponse };\nexport default plugin;\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin2.js","names":[],"sources":["../src/cli-backend-detect.ts","../src/ipc-client.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Decide whether to defer to OpenClaw's native `bundleMcp:true` path for\n * this run. Pre-May-2026 this gated on backend type (claude-cli /\n * codex-cli), but the actual hazard was double-registration: when\n * `openclaw.json#mcp.servers` had entries AND the plugin registered the\n * same tools via `api.registerTool`, claude-cli's bundleMcp would spawn\n * its own copy and OpenClaw would suffix-disambiguate the duplicates.\n *\n * The single-source-of-truth refactor stops mirror-writing the alfe\n * store into openclaw.json, so `mcp.servers` is empty in practice. The\n * native path on claude-cli has nothing to spawn, our plugin's IPC path\n * is the only one in play, and we register on every backend.\n *\n * The escape hatch survives: a user who hand-edits `mcp.servers` in\n * openclaw.json (private dev MCP, etc.) gets the native path back —\n * this function returns true, the plugin no-ops, and claude-cli/codex-cli\n * spawn that entry themselves.\n */\n\ninterface ConfigLike {\n mcp?: {\n servers?: Record<string, unknown>;\n };\n}\n\n/**\n * Returns true when openclaw.json#mcp.servers has at least one entry —\n * signalling that the native bundleMcp path is in play for THIS run\n * (regardless of backend) and the plugin should defer to it.\n *\n * Empty object / missing key → false → plugin registers IPC-proxied tools.\n */\nexport function hasNativeOpenclawMcpServers(cfg: ConfigLike | undefined): boolean {\n if (!cfg) return false;\n const servers = cfg.mcp?.servers;\n if (!servers || typeof servers !== 'object') return false;\n return Object.keys(servers).length > 0;\n}\n\n/**\n * Back-compat alias for callers still on the old name. Internal — drop\n * once we cut the next major.\n */\nexport const detectNativeBundleMcp = hasNativeOpenclawMcpServers;\n","/**\n * Minimal newline-delimited-JSON IPC client over Unix socket. Talks to\n * the alfe-gateway daemon at `~/.alfe/gateway.sock` so this plugin can\n * proxy `mcp.list_tools` / `mcp.call_tool` requests to the daemon-hosted\n * `McpBundler` rather than spawning its own children.\n *\n * Why minimal: the @alfe.ai/openclaw package already ships a full IPC\n * client with reconnect + event handling, but pulling it in would create\n * a (semantically) circular dep — plugin importing from its host. This\n * file is ~120 lines and only does what the bundler plugin needs.\n *\n * The daemon's IPC server (`packages/gateway/src/ipc-server.ts`) speaks:\n * Request: { type: 'req', id, method, params }\n * Response: { id, ok, payload?, error? }\n * Event: { type: 'event', event, payload } ← ignored here, we never subscribe.\n */\nimport { createConnection, type Socket } from 'node:net';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nconst DEFAULT_SOCKET_PATH = join(homedir(), '.alfe', 'gateway.sock');\nconst RECONNECT_MIN_MS = 500;\nconst RECONNECT_MAX_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nexport interface IpcResponse<T = unknown> {\n id: string;\n ok: boolean;\n payload?: T;\n error?: { code: string; message: string };\n}\n\ninterface Logger {\n debug(msg: string, ctx?: Record<string, unknown>): void;\n info(msg: string, ctx?: Record<string, unknown>): void;\n warn(msg: string, ctx?: Record<string, unknown>): void;\n error(msg: string, ctx?: Record<string, unknown>): void;\n}\n\nexport interface McpIpcClientOptions {\n socketPath?: string;\n logger?: Logger;\n /** Plugin identity broadcast to the daemon on connect. */\n plugin: { name: string; version: string };\n /**\n * Fires every time the socket transitions from disconnected to\n * connected — i.e. on the initial connect AND on every reconnect after\n * the daemon restarts. Invoked AFTER `connected = true` is set so\n * `call(...)` from inside the handler will actually round-trip the\n * daemon (the previous shape called `refreshCachedTools` synchronously\n * inside the plugin's `startService` while `connected` was still false,\n * which silently returned `NOT_CONNECTED` and left the tool cache\n * permanently empty on the first session — see plugin.ts comment for\n * the QA Tester repro).\n *\n * Contract:\n * - Treated as fire-and-forget. The IPC client does NOT await the\n * handler's return value.\n * - Synchronous throws are caught + logged at warn — they will not\n * tear down the IPC client.\n * - If a handler returns a Promise that rejects later, the rejection\n * surfaces as an unhandled promise rejection. Wrap with `void` or\n * handle internally if you don't want that behaviour. The wired\n * handler in `plugin.ts` does exactly that:\n * `() => { void refreshCachedTools(api); }`.\n */\n onConnect?: () => void;\n}\n\ninterface PendingRequest {\n resolve: (response: IpcResponse) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Plugin-side IPC client. Single instance per plugin lifecycle: start on\n * `registerService.start`, stop on `registerService.stop`. Calls\n * before the socket is connected return a typed `NOT_CONNECTED` response\n * so the plugin can fall back to \"no tools yet\" rather than throwing.\n */\nexport class McpIpcClient {\n private socket: Socket | null = null;\n private buffer = '';\n private backoffMs = RECONNECT_MIN_MS;\n private closed = false;\n private connected = false;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private pending = new Map<string, PendingRequest>();\n private readonly socketPath: string;\n private readonly logger?: Logger;\n private readonly plugin: { name: string; version: string };\n private readonly onConnect?: () => void;\n\n constructor(opts: McpIpcClientOptions) {\n this.socketPath = opts.socketPath ?? DEFAULT_SOCKET_PATH;\n this.logger = opts.logger;\n this.plugin = opts.plugin;\n this.onConnect = opts.onConnect;\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n start(): void {\n this.closed = false;\n this.openSocket();\n }\n\n stop(): void {\n this.closed = true;\n this.clearReconnectTimer();\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer);\n pending.resolve({\n id,\n ok: false,\n error: { code: 'CLIENT_STOPPED', message: 'IPC client stopped' },\n });\n }\n this.pending.clear();\n if (this.socket) {\n try {\n this.socket.end();\n } catch {\n // ignore — socket may already be in a half-closed state\n }\n this.socket = null;\n }\n this.connected = false;\n }\n\n async call<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n ): Promise<IpcResponse<T>> {\n if (!this.connected || !this.socket) {\n return {\n id: '',\n ok: false,\n error: { code: 'NOT_CONNECTED', message: 'Not connected to alfe-gateway' },\n };\n }\n const id = randomUUID();\n return new Promise<IpcResponse<T>>((resolve) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n resolve({\n id,\n ok: false,\n error: { code: 'TIMEOUT', message: `${method} timed out after ${String(REQUEST_TIMEOUT_MS)}ms` },\n });\n }, REQUEST_TIMEOUT_MS);\n this.pending.set(id, { resolve: resolve as (r: IpcResponse) => void, timer });\n try {\n this.socket?.write(`${JSON.stringify({ type: 'req', id, method, params })}\\n`);\n } catch (err) {\n clearTimeout(timer);\n this.pending.delete(id);\n resolve({\n id,\n ok: false,\n error: {\n code: 'SEND_FAILED',\n message: err instanceof Error ? err.message : String(err),\n },\n });\n }\n });\n }\n\n private openSocket(): void {\n if (this.closed) return;\n this.buffer = '';\n const socket = createConnection(this.socketPath, () => {\n this.logger?.info('[mcp-bundler/ipc] connected', { socket: this.socketPath });\n this.connected = true;\n this.backoffMs = RECONNECT_MIN_MS;\n // Best-effort register. We don't await the response — the daemon\n // routes mcp.* methods even on unregistered connections, so the\n // tool-list / tool-call path doesn't need register to complete.\n void this.call('register', {\n name: this.plugin.name,\n version: this.plugin.version,\n protocolVersion: 1,\n capabilities: ['mcp.list_tools', 'mcp.call_tool'],\n pid: process.pid,\n });\n // Notify caller AFTER `connected = true` so any `.call(...)` issued\n // from the handler (e.g. plugin.ts's tool-cache refresh) actually\n // makes it onto the wire instead of getting the NOT_CONNECTED\n // short-circuit at the top of `call()`.\n if (this.onConnect) {\n try {\n this.onConnect();\n } catch (err) {\n this.logger?.warn('[mcp-bundler/ipc] onConnect handler threw', {\n err: err instanceof Error ? err.message : String(err),\n });\n }\n }\n });\n this.socket = socket;\n\n socket.on('data', (data: Buffer) => {\n this.buffer += data.toString();\n this.processBuffer();\n });\n\n socket.on('close', () => {\n this.connected = false;\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer);\n pending.resolve({\n id,\n ok: false,\n error: { code: 'DISCONNECTED', message: 'Connection closed' },\n });\n }\n this.pending.clear();\n this.socket = null;\n if (!this.closed) this.scheduleReconnect();\n });\n\n socket.on('error', (err: Error) => {\n this.logger?.debug('[mcp-bundler/ipc] socket error', { err: err.message });\n // 'close' fires after 'error'; reconnect handled there.\n });\n }\n\n private processBuffer(): void {\n let idx: number;\n while ((idx = this.buffer.indexOf('\\n')) !== -1) {\n const line = this.buffer.slice(0, idx).trim();\n this.buffer = this.buffer.slice(idx + 1);\n if (!line) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!isIpcResponse(parsed)) continue;\n const pending = this.pending.get(parsed.id);\n if (!pending) continue;\n clearTimeout(pending.timer);\n this.pending.delete(parsed.id);\n pending.resolve(parsed);\n }\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer) return;\n const delay = this.backoffMs;\n this.backoffMs = Math.min(this.backoffMs * 2, RECONNECT_MAX_MS);\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.openSocket();\n }, delay);\n if (typeof this.reconnectTimer === 'object' && 'unref' in this.reconnectTimer) {\n (this.reconnectTimer as { unref: () => void }).unref();\n }\n }\n\n private clearReconnectTimer(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n}\n\nfunction isIpcResponse(value: unknown): value is IpcResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'id' in value &&\n 'ok' in value &&\n typeof (value as { id: unknown }).id === 'string'\n );\n}\n","/**\n * @alfe.ai/openclaw-mcp-bundler — OpenClaw plugin entry\n *\n * Thin IPC client over the alfe-gateway daemon. The daemon hosts the\n * actual `McpBundler` (children + tool catalog), this plugin forwards\n * the OpenClaw tool-surface to that bundler via `mcp.list_tools` and\n * `mcp.call_tool` over `~/.alfe/gateway.sock`.\n *\n * Works on every LLM backend — `api.registerTool` registrations reach\n * MiniMax/Mistral/generic AND claude-cli/codex-cli. The previous\n * blanket no-op on claude-cli was load-bearing only when the same MCP\n * was ALSO in `openclaw.json#mcp.servers` (double registration);\n * since the alfe store no longer mirror-writes there, the native\n * bundleMcp path has nothing to spawn and we can register on every\n * backend uniformly.\n *\n * Escape hatch: a user who hand-adds an entry to\n * `openclaw.json#mcp.servers` for a private dev MCP gets the native\n * path back for that run — `hasNativeOpenclawMcpServers` returns true,\n * we no-op, claude-cli/codex-cli spawn that entry themselves.\n *\n * ── v0.0.14: NO `api.registerService` ─────────────────────────────\n *\n * Through v0.0.13 the IPC client lifecycle was wired into\n * `api.registerService({id, start, stop})`. Empirically (QA Tester\n * 2026-06-10, OpenClaw 2026.5.3-1) this combination broke tool\n * surfacing to the LLM whenever `onStartup: false`: the bundler's\n * tools got dropped from the agent's effective tool list during the\n * `gateway-bindable` runtime-subagent-mode registry rebuild that\n * happens at chat-turn time.\n *\n * Sibling tool plugins (`@alfe.ai/openclaw-search`, `-secrets`,\n * `-database`, `-identity`, `-memory-cloud`) do NOT call\n * `registerService` and all surface their tools correctly with\n * `onStartup: false`. The bundler was the singular outlier.\n *\n * v0.0.14 removes `api.registerService` entirely and moves the IPC\n * client lifecycle to module-scope lazy init driven by `activate()`\n * itself (gated on `api.registrationMode === 'full'` so CLI / tool-\n * discovery loads don't open the socket). This matches the sibling\n * pattern and the LLM sees every bundler tool on the first chat turn.\n *\n * See `packages/openclaw-mcp-bundler/DEVELOPING.md` → \"v0.0.14 lifecycle\"\n * for the full diagnosis.\n */\n\nimport type { McpToolDescriptor, McpToolCallResult } from '@alfe.ai/mcp-bundler';\nimport { hasNativeOpenclawMcpServers } from './cli-backend-detect.js';\nimport { McpIpcClient, type IpcResponse } from './ipc-client.js';\nimport { createRequire } from 'node:module';\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\n// Disk-backed snapshot of the daemon's last-known tool catalogue. Tool-\n// discovery mode (used by OpenClaw whenever it builds the agent's tool\n// list — see `resolvePluginTools` in openclaw's loader) and CLI mode\n// both load the plugin WITHOUT opening the gateway socket. Without a\n// disk snapshot the plugin contributes zero MCP tools in those modes —\n// the v0.0.10 QA Tester repro. Cache file is written by\n// `refreshCachedTools` on every successful `mcp.list_tools` and read at\n// `activate` time. Lives under the alfe daemon's state dir so the same\n// box's full-mode and tool-discovery-mode loads share it.\nconst CACHE_FILE = join(homedir(), '.alfe', 'mcp', 'openclaw-bundler-cache.json');\n// Path to the alfe-gateway daemon's Unix socket. The McpIpcClient\n// defaults to this same path internally; we re-declare it here so\n// `activate` can pre-check existence and skip blocking on\n// pre-warm when there's no daemon to talk to (CI, fresh installs,\n// daemon-not-yet-started cases).\nconst GATEWAY_SOCKET = join(homedir(), '.alfe', 'gateway.sock');\n\n// ── Type shims for the OpenClaw plugin SDK ──────────────────────\n// We avoid a hard dep on the SDK to keep this plugin loadable across\n// OpenClaw versions; the surface used here is stable.\n\ninterface Logger {\n debug(msg: string, ...args: unknown[]): void;\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n}\n\ninterface ConfigSnapshot {\n agents?: { defaults?: { model?: string; cliBackends?: Record<string, unknown> } };\n mcp?: { servers?: Record<string, unknown> };\n}\n\ninterface AgentToolResult {\n content: Record<string, unknown>[];\n details?: Record<string, unknown>;\n}\n\ninterface AgentTool {\n name: string;\n label?: string;\n description: string;\n parameters: Record<string, unknown>;\n execute: (\n toolCallId: string,\n params: unknown,\n signal?: AbortSignal,\n onUpdate?: (update: unknown) => void,\n ) => Promise<AgentToolResult>;\n}\n\ninterface ToolFactoryContext {\n config?: ConfigSnapshot;\n runtimeConfig?: ConfigSnapshot;\n getRuntimeConfig?: () => ConfigSnapshot | undefined;\n}\n\ninterface OpenClawPluginApi {\n logger: Logger;\n config?: ConfigSnapshot;\n pluginConfig?: { disabled?: boolean };\n registrationMode?: string;\n runtime?: { config?: { current?: () => ConfigSnapshot | undefined } };\n registerTool: (\n tool: AgentTool | ((ctx: ToolFactoryContext) => AgentTool | AgentTool[] | null | undefined),\n opts?: { name?: string; names?: string[]; optional?: boolean },\n ) => void;\n}\n\n// ── Plugin state ───────────────────────────────────────────────\nlet ipcClient: McpIpcClient | undefined;\n// Module-scoped catalogue. We keep `McpToolDescriptor` here (rather\n// than pre-baked `AgentTool` objects) so the catalogue is JSON-\n// serialisable for the disk cache. `buildAgentTool` constructs an\n// `AgentTool` at factory-invocation time and closes over the live\n// `ipcClient` via the module-scoped reference, so the\n// tools work as soon as IPC is ready — without serialising the\n// IPC client into the cache.\nlet cachedDescriptors: McpToolDescriptor[] = [];\nlet refreshInFlight: Promise<void> | null = null;\nlet periodicRefreshTimer: ReturnType<typeof setInterval> | null = null;\nconst ACTIVATED_KEY = '__alfeMcpBundlerPluginActivated';\nconst g = globalThis as Record<string, unknown>;\n\n// Periodic refresh cadence. The bundler's `onConnect` refresh fires\n// ONCE; if the daemon's MCP children come online after that initial\n// `mcp.list_tools` returns (e.g. a slow npx-fetched provider proxy),\n// without polling the bundler's snapshot stays stuck on the partial\n// catalog it captured at startup. 10s is small enough for the LLM's\n// next-turn tool list to converge while staying well below the IPC\n// cost threshold (one `mcp.list_tools` call → tiny daemon-local\n// payload, no remote network).\nconst PERIODIC_REFRESH_INTERVAL_MS = 10_000;\n\nfunction resolveLiveConfig(api: OpenClawPluginApi, ctx?: ToolFactoryContext): ConfigSnapshot | undefined {\n // Prefer most-recent → ctx live → ctx snapshot → api.runtime live → api.config snapshot\n return (\n ctx?.runtimeConfig ??\n ctx?.getRuntimeConfig?.() ??\n api.runtime?.config?.current?.() ??\n ctx?.config ??\n api.config\n );\n}\n\nfunction buildAgentTool(api: OpenClawPluginApi, tool: McpToolDescriptor): AgentTool {\n return {\n name: tool.prefixed,\n label: tool.label,\n description: tool.description || `MCP tool ${tool.original} from server ${tool.server}`,\n // OpenClaw expects a JSON-Schema-shaped object. The MCP server already\n // gives us one in `tool.parameters`. Pass through unchanged.\n parameters: tool.parameters,\n async execute(_toolCallId, params) {\n // `ipcClient` is module-scoped and set by `startIpcClient` at\n // activate time in full mode. In tool-discovery mode (no IPC,\n // see `shouldStartIpcClient`) `ipcClient` is undefined here —\n // return a graceful error so the LLM gets a useful message\n // rather than a hard crash. In full mode this closure resolves\n // at execute() time, after the IPC client has been wired.\n if (!ipcClient?.isConnected()) {\n return errorResult(\n `mcp ${tool.prefixed}: bundler IPC not connected yet. Try again in a moment.`,\n );\n }\n const response = await ipcClient.call<McpToolCallResult>('mcp.call_tool', {\n name: tool.prefixed,\n args: params,\n });\n if (!response.ok || !response.payload) {\n api.logger.warn('[mcp-bundler] tool call failed', {\n tool: tool.prefixed,\n err: response.error?.message ?? 'unknown error',\n });\n return errorResult(\n response.error?.message\n ? `mcp ${tool.prefixed}: ${response.error.message}`\n : `mcp ${tool.prefixed}: call failed`,\n );\n }\n const result = response.payload;\n return {\n content: result.content,\n details: { isError: result.isError ?? false },\n };\n },\n };\n}\n\nfunction errorResult(message: string): AgentToolResult {\n return {\n content: [{ type: 'text', text: message }],\n details: { isError: true },\n };\n}\n\n/**\n * Async-aware shim around the module-scoped `ipcClient`. The management\n * tools and dynamic MCP tools registered in `activate()` execute LATER\n * (when the agent invokes them), but `ipcClient` is only constructed\n * inside `startIpcClient`. The `?.` chains return useful errors when the\n * agent tries to call a tool before the IPC is ready, instead of throwing.\n */\nasync function callIpc<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n): Promise<IpcResponse<T>> {\n if (!ipcClient) {\n return {\n id: '',\n ok: false,\n error: { code: 'NOT_CONNECTED', message: 'Bundler IPC client not initialized yet' },\n };\n }\n return ipcClient.call<T>(method, params);\n}\n\n/**\n * Cheap shape check guarding against a corrupt or torn-write cache —\n * a half-written JSON array that parses but holds garbage entries\n * would otherwise reach `buildAgentTool` where `tool.prefixed` ends up\n * undefined and registers a tool with `name: undefined`. Catching the\n * shape here protects the agent's tool list from disk-level corruption.\n */\nfunction isValidDescriptor(value: unknown): value is McpToolDescriptor {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.prefixed === 'string' &&\n typeof v.original === 'string' &&\n typeof v.server === 'string' &&\n typeof v.parameters === 'object' &&\n v.parameters !== null\n );\n}\n\n/**\n * Load the last-known tool catalogue from disk. Used at `activate()`\n * time so the agent's tool list contains real tools even in tool-\n * discovery mode (where the IPC client never opens). Silently falls\n * back to an empty list on any read/parse failure so a first-run agent\n * (no cache file yet) still loads cleanly.\n */\nfunction loadCachedDescriptorsFromDisk(logger: Logger): McpToolDescriptor[] {\n try {\n const raw = readFileSync(CACHE_FILE, 'utf-8');\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(isValidDescriptor);\n } catch (err) {\n if (err instanceof Error && (err as NodeJS.ErrnoException).code !== 'ENOENT') {\n logger.debug('[mcp-bundler] cache read failed', { err: err.message });\n }\n return [];\n }\n}\n\n/**\n * Persist the current catalogue to disk so the next tool-discovery\n * load sees the tools. Best-effort — disk failures are logged at\n * `warn` and otherwise swallowed.\n */\nfunction persistCachedDescriptorsToDisk(\n descriptors: McpToolDescriptor[],\n logger: Logger,\n): void {\n // Atomic write: write to a temp sibling then rename. `writeFileSync` is\n // NOT atomic on its own — two processes racing on the same path (a\n // daemon restart concurrent with a tool-discovery load's inline\n // refresh, say) can interleave bytes and corrupt the JSON. The\n // tool-discovery load then silently drops the catalogue until the\n // next successful refresh. `renameSync` is atomic on the same\n // filesystem on every platform we ship to.\n const tmp = `${CACHE_FILE}.${randomUUID()}.tmp`;\n try {\n mkdirSync(dirname(CACHE_FILE), { recursive: true });\n writeFileSync(tmp, JSON.stringify(descriptors));\n renameSync(tmp, CACHE_FILE);\n } catch (err) {\n logger.warn('[mcp-bundler] cache write failed', {\n err: err instanceof Error ? err.message : String(err),\n });\n }\n}\n\n/**\n * Self-service tools the agent can call to add/remove MCP servers in\n * the Alfe bundler store. The agent gets the same surface the CLI's\n * `alfe mcp add/remove/list` commands expose — same IPC methods, same\n * `'manual'` ownership semantics. Daemon-owned (`'cli'` /\n * `'integration:*'`) entries can't be clobbered.\n */\nfunction registerAgentMcpManagementTools(api: OpenClawPluginApi): void {\n // Tools are registered during `activate()` (whether or not the IPC\n // client has been wired yet — see `shouldStartIpcClient` for the\n // mode gating). Each tool's `execute()` reaches the module-scoped\n // `ipcClient` via `callIpc()` at invocation time. In tool-discovery\n // / CLI modes where the IPC client is never started, these tools\n // still appear in the agent's tool list, but executing them returns\n // a `NOT_CONNECTED` error from `callIpc` rather than crashing.\n\n api.registerTool({\n name: 'alfe_mcp_list',\n label: 'List Alfe MCP servers',\n description:\n 'List every MCP server currently registered in the Alfe bundler store, with id, owner, and transport. Use this before adding a new server to avoid name collisions or to confirm what is currently available to call.',\n parameters: { type: 'object', properties: {}, additionalProperties: false },\n async execute() {\n const response = await callIpc<{ servers: unknown[] }>('mcp.list_servers', {});\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_list failed');\n }\n return {\n content: [{ type: 'text', text: JSON.stringify(response.payload?.servers ?? [], null, 2) }],\n details: { isError: false },\n };\n },\n });\n\n api.registerTool({\n name: 'alfe_mcp_add',\n label: 'Register an MCP server with Alfe',\n description:\n 'Register a new MCP server in the Alfe bundler store. The Alfe daemon will spawn the child and its tools become available to the agent on the next turn (no runtime restart). Use a stdio command for local-process MCPs (most common) or url+transport for remote SSE / streamable-http MCPs.',\n parameters: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique id under the bundler store. Letters, digits, hyphens. Becomes the `mcp__<id>__` prefix on every tool name from this server.',\n },\n command: {\n type: 'string',\n description: 'Executable to spawn for stdio transport (e.g. \"npx\", \"uvx\", \"/path/to/bin\"). Required for stdio mode.',\n },\n args: {\n type: 'array',\n items: { type: 'string' },\n description: 'Arguments to pass to the command. Optional.',\n },\n env: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'Environment variables for the child process. Optional.',\n },\n cwd: {\n type: 'string',\n description: 'Working directory for the child process. Optional.',\n },\n url: {\n type: 'string',\n description: 'Remote MCP endpoint URL. Required for sse / streamable-http transport.',\n },\n transport: {\n type: 'string',\n enum: ['sse', 'streamable-http'],\n description: 'Remote MCP transport. Defaults to \"sse\" when url is set.',\n },\n headers: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'HTTP headers for remote MCP transports. Optional.',\n },\n },\n required: ['id'],\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n const response = await callIpc<{ id: string }>('mcp.add_server', (params ?? {}) as Record<string, unknown>);\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_add failed');\n }\n return {\n content: [{ type: 'text', text: `Registered MCP server \"${response.payload?.id ?? '?'}\". Tools will appear on the next turn.` }],\n details: { isError: false },\n };\n },\n });\n\n api.registerTool({\n name: 'alfe_mcp_remove',\n label: 'Unregister an MCP server from Alfe',\n description:\n 'Remove an MCP server you previously registered with alfe_mcp_add. Only entries you registered (owner=manual) can be removed this way — integration-installed servers (atlassian, github, etc.) and the built-in alfe-platform server are owner-protected and must be removed via the dashboard or CLI.',\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Bundler store id of the entry to remove.' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n const response = await callIpc<{ removed: boolean }>(\n 'mcp.remove_server',\n (params ?? {}) as Record<string, unknown>,\n );\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_remove failed');\n }\n const removed = response.payload?.removed ?? false;\n return {\n content: [\n {\n type: 'text',\n text: removed\n ? `Removed MCP server. Its tools will disappear on the next turn.`\n : `No MCP server matched — nothing to remove.`,\n },\n ],\n details: { isError: false },\n };\n },\n });\n}\n\n/**\n * Pull the latest tool catalog from the daemon-hosted bundler. Coalesces\n * concurrent calls so the LLM hitting the factory many times in a tight\n * loop only triggers one in-flight IPC request.\n */\nfunction refreshCachedTools(api: OpenClawPluginApi): Promise<void> {\n if (!ipcClient) return Promise.resolve();\n if (refreshInFlight) return refreshInFlight;\n refreshInFlight = (async () => {\n try {\n const response = await callIpc<{ tools: McpToolDescriptor[] }>('mcp.list_tools', {});\n if (!response.ok || !response.payload) {\n // NOT_CONNECTED / TIMEOUT / etc — keep the prior cache so a\n // transient daemon hiccup doesn't drop tools mid-session.\n api.logger.debug('[mcp-bundler] list_tools failed', {\n err: response.error?.message ?? 'unknown',\n });\n return;\n }\n const fresh = response.payload.tools;\n // Defensive: never SHRINK the cache to empty when we already had\n // tools. The daemon can transiently return `[]` during cold start\n // before its MCP children fully spawn (npx fetch of a provider\n // proxy, slow integration init, etc.). If we wrote that empty\n // result to disk, the next agent process spinning up in tool-\n // discovery mode would see the empty cache and the LLM would lose\n // every dynamic MCP tool until the daemon backfills. Hold the\n // prior catalogue until the daemon returns a non-empty list\n // again — the periodic refresh will pick the new tools up within\n // 10s of the daemon coming fully online.\n if (fresh.length === 0 && cachedDescriptors.length > 0) {\n api.logger.debug(\n `[mcp-bundler] list_tools returned 0 tools — keeping prior cache of ${String(cachedDescriptors.length)}`,\n );\n return;\n }\n cachedDescriptors = fresh;\n // Persist for tool-discovery-mode loads — that's how this fix\n // lands tools on the agent's tool list. See CACHE_FILE comment.\n persistCachedDescriptorsToDisk(cachedDescriptors, api.logger);\n } finally {\n refreshInFlight = null;\n }\n })();\n return refreshInFlight;\n}\n\n/**\n * Should this `activate()` invocation also start the live IPC client?\n *\n * Decision rule: only in full agent-runtime mode. CLI invocations\n * (`openclaw config set`, `openclaw plugins inspect`, `openclaw upgrade`,\n * etc.) and tool-discovery loads (where OpenClaw builds the agent's\n * tool list without running plugin services) should NOT open the\n * gateway socket — they only need the disk cache and the static tool\n * registrations.\n *\n * `api.registrationMode` is the canonical signal. OpenClaw 2026.5\n * sets it to one of: `'full'`, `'discovery'`, `'tool-discovery'`,\n * `'setup-only'`, `'setup-runtime'`, `'cli-metadata'`. Only `'full'`\n * means \"real agent runtime; everything is allowed to run\".\n *\n * Defensive default: if `registrationMode` is unset (older OpenClaw,\n * non-bundled host, test harness), assume full mode so we don't\n * silently degrade. The IPC client itself handles a missing socket\n * gracefully via reconnect, so an over-eager start is cheap.\n */\nfunction shouldStartIpcClient(api: OpenClawPluginApi): boolean {\n const mode = api.registrationMode;\n if (mode === undefined) return true;\n return mode === 'full';\n}\n\n/**\n * Construct the live IPC client and wire its `onConnect` to a cache\n * refresh. Module-scoped — the same client lifecycle outlasts a single\n * `activate()` call so reconnects from daemon restarts continue\n * automatically. Called from `activate()` ONLY in full agent-runtime\n * mode (see `shouldStartIpcClient`).\n *\n * IMPORTANT: this function is NOT awaited. The `onConnect` callback\n * fires asynchronously; the dynamic factory returns whatever is in\n * `cachedDescriptors` at factory-invocation time (loaded from disk in\n * `activate()`, then refreshed on connect, then periodically). A first-\n * turn pre-warm is no longer needed — the disk cache covers turn-1,\n * and any new tools discovered during the in-flight `onConnect`\n * refresh land in `cachedDescriptors` before the LLM picks them up on\n * turn-2.\n *\n * Removed the v0.0.13 PREWARM_TIMEOUT_MS / blocking pre-warm because\n * v0.0.14 has no `service.start` await point to attach it to —\n * removing `api.registerService` was the whole point of the fix. Disk\n * cache + periodic refresh now carry the freshness contract.\n */\nfunction startIpcClient(api: OpenClawPluginApi, log: Logger): void {\n if (ipcClient) return; // already started — idempotent.\n\n // Pre-flight: if the gateway socket doesn't exist, still construct\n // the client (its reconnect loop will pick the socket up later) but\n // log at debug rather than info so a missing socket on CI / fresh\n // installs doesn't pollute the signal reserved for actual daemon\n // problems.\n const socketPresent = existsSync(GATEWAY_SOCKET);\n if (!socketPresent) {\n log.debug(\n `[mcp-bundler] ${GATEWAY_SOCKET} not present at activate — IPC client will refresh when the daemon comes online`,\n );\n }\n\n ipcClient = new McpIpcClient({\n logger: log,\n plugin: { name: '@alfe.ai/openclaw-mcp-bundler', version: pkg.version },\n onConnect: () => {\n void refreshCachedTools(api);\n },\n });\n ipcClient.start();\n\n // Periodic refresh — picks up MCP children that come online AFTER\n // the initial onConnect refresh fires (slow npx fetches, lagging\n // integration provider spawns). Only fires when IPC is connected;\n // otherwise `refreshCachedTools` is a no-op.\n periodicRefreshTimer = setInterval(() => {\n if (ipcClient?.isConnected()) {\n void refreshCachedTools(api);\n }\n }, PERIODIC_REFRESH_INTERVAL_MS);\n // `unref()` lets the host process exit even if this timer is still\n // scheduled — important for short-lived runs where a hanging interval\n // would block process teardown. Optional-chained because non-Node\n // hosts (browsers, edge runtimes) don't expose `.unref()` on Timeout\n // handles.\n (periodicRefreshTimer as { unref?: () => void }).unref?.();\n}\n\n/**\n * Tear down the IPC client and periodic refresh timer. Called from\n * `deactivate()` (the only lifecycle exit point now that v0.0.14\n * dropped `api.registerService`). Safe to call multiple times — each\n * tear-down is idempotent.\n */\nfunction stopIpcClient(): void {\n if (periodicRefreshTimer !== null) {\n clearInterval(periodicRefreshTimer);\n periodicRefreshTimer = null;\n }\n if (ipcClient) {\n ipcClient.stop();\n ipcClient = undefined;\n }\n // Keep `cachedDescriptors` and the on-disk cache intact across\n // restarts — that's the WHOLE POINT of the disk cache. We only tear\n // down the live IPC reference here.\n refreshInFlight = null;\n}\n\nconst plugin = {\n id: '@alfe.ai/openclaw-mcp-bundler',\n name: 'Alfe MCP Bundler',\n description: 'Proxies the daemon-hosted MCP bundler tools to the LLM',\n version: pkg.version,\n contracts: {\n // OpenClaw's `contracts.tools` matcher (in\n // `loader.js#registerTool` and `tools.js#resolvePluginTools`) uses\n // strict literal `Set.has()` — it does NOT honour glob patterns. So\n // the previous `'mcp__*'` entry was non-functional: every tool the\n // factory returned whose name started with anything else (e.g.\n // `atlassian-atlassian__atlassian_list_accounts`) was silently\n // rejected as \"undeclared\" at runtime, and never surfaced to the\n // agent. Diagnosed 2026-06-09 on QA Tester after the v0.0.9 IPC\n // fix proved correct but tools still didn't appear in chat\n // sessions. See CHANGELOG for the trace.\n //\n // This list enumerates every tool name the proxy can return today.\n // The three `alfe_mcp_*` entries are the agent self-service\n // surface. The `<provider>-<provider>__<provider>_*` entries are\n // the Pattern-A proxy meta-tools (list_accounts /\n // check_connection / refresh_token) each provider's MCP server\n // surfaces before a cloudId is selected. The `alfe-platform__*`\n // entries cover the CLI-managed alfe-platform MCP server.\n //\n // ADDING A NEW INTEGRATION: append its 3 Pattern-A meta-tools\n // here AND any post-cloudId tools the proxy exposes that you want\n // the agent to be able to invoke. There is no shortcut — until\n // OpenClaw learns glob patterns in `contracts.tools` (tracked\n // separately) every tool name must be listed literally.\n tools: [\n 'alfe_mcp_list',\n 'alfe_mcp_add',\n 'alfe_mcp_remove',\n 'atlassian-atlassian__atlassian_list_accounts',\n 'atlassian-atlassian__atlassian_check_connection',\n 'atlassian-atlassian__atlassian_refresh_token',\n 'notion-notion__notion_list_accounts',\n 'notion-notion__notion_check_connection',\n 'notion-notion__notion_refresh_token',\n 'github-github__github_list_accounts',\n 'github-github__github_check_connection',\n 'github-github__github_refresh_token',\n 'microsoft-microsoft__microsoft_list_accounts',\n 'microsoft-microsoft__microsoft_check_connection',\n 'microsoft-microsoft__microsoft_refresh_token',\n 'google-google__google_list_accounts',\n 'google-google__google_check_connection',\n 'google-google__google_refresh_token',\n 'slack-slack__slack_list_accounts',\n 'slack-slack__slack_check_connection',\n 'slack-slack__slack_refresh_token',\n 'discord-discord__discord_list_accounts',\n 'discord-discord__discord_check_connection',\n 'discord-discord__discord_refresh_token',\n 'mobile-mobile__mobile_list_accounts',\n 'mobile-mobile__mobile_check_connection',\n 'mobile-mobile__mobile_refresh_token',\n 'myob-myob__myob_list_accounts',\n 'myob-myob__myob_check_connection',\n 'myob-myob__myob_refresh_token',\n 'xero-xero__xero_list_accounts',\n 'xero-xero__xero_check_connection',\n 'xero-xero__xero_refresh_token',\n 'alfe-platform__session_status',\n 'alfe-platform__gateway_status',\n 'alfe-platform__integrations_browse_registry',\n 'alfe-platform__integrations_start_oauth',\n ],\n },\n\n activate(api: OpenClawPluginApi) {\n const log = api.logger;\n\n if (g[ACTIVATED_KEY] === true) {\n log.debug('[mcp-bundler] already activated — skipping duplicate');\n return;\n }\n g[ACTIVATED_KEY] = true;\n\n if (api.pluginConfig?.disabled) {\n g[ACTIVATED_KEY] = false;\n log.info('[mcp-bundler] disabled via plugin config — no-op');\n return;\n }\n\n const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));\n if (deferToNative) {\n log.info(\n '[mcp-bundler] openclaw.json#mcp.servers has entries — MCP-proxy factory will defer to native bundleMcp, but agent self-service tools (alfe_mcp_*) stay available',\n );\n }\n\n // ── Register tools synchronously in activate() ──────────────\n //\n // OpenClaw loads plugins in \"tool-discovery\" mode whenever it\n // builds the agent's tool list (see `resolvePluginTools` in\n // `loader.js`). Through v0.0.13 we wired tool registration AND the\n // IPC client into `api.registerService({id, start, stop})` — but\n // empirically (QA Tester 2026-06-10) the combination of\n // `registerService` + `onStartup: false` drops the bundler's tools\n // from the agent's effective tool list. Every sibling tool plugin\n // (`-search`, `-secrets`, `-database`, ...) does NOT call\n // `registerService` and surfaces tools correctly.\n //\n // v0.0.14 contract: NO `registerService`. Tools register here in\n // `activate()`. The IPC client lifecycle moves to module-scoped\n // lazy init driven by `activate()` itself (in agent-runtime mode\n // only — `shouldStartIpcClient` gates on `api.registrationMode`).\n // `deactivate()` tears the client down.\n\n // Seed the descriptor cache from disk so tool-discovery mode\n // (which has no IPC) can still return real MCP tools to the\n // agent. Refreshed and re-persisted by `refreshCachedTools` on\n // every IPC connect and periodically.\n cachedDescriptors = loadCachedDescriptorsFromDisk(log);\n\n // Agent self-service tools — always available, regardless of the\n // native-bundleMcp escape hatch. The alfe store is independent of\n // openclaw.json so agents can manage their MCP roster in either\n // mode.\n registerAgentMcpManagementTools(api);\n\n if (!deferToNative) {\n // NOTE: Do NOT pass `{ optional: true }` here. OpenClaw's\n // `isOptionalToolAllowed` filter (tools-J2MZN_m2.js:321) drops\n // optional factory results when `pluginToolAllowlist` is empty\n // (the default agent config). Pre-2026-06-11 the bundler was\n // marked optional and its dynamic MCP-proxy tools silently never\n // surfaced even though the static management tools did — that\n // was the symptom of the QA Tester repro after the v0.0.14\n // registerService removal landed. The tools are governed by\n // `contracts.tools` literal allowlist anyway, so they don't need\n // the additional opt-in gate.\n // NOTE: We use the `log` reference captured at activate time\n // INSIDE the factory closure rather than `api.logger`. OpenClaw\n // wraps `api` in a guarded Proxy (createGuardedPluginRegistrationApi)\n // that closes the moment `register()` returns — so by the time\n // OpenClaw invokes our factory during tool resolution, the\n // guarded `api.logger.foo(...)` calls silently no-op.\n api.registerTool((ctx: ToolFactoryContext) => {\n const live = resolveLiveConfig(api, ctx);\n if (hasNativeOpenclawMcpServers(live)) return null;\n // Best-effort background refresh so each turn picks up any new\n // entries the daemon's bundler has discovered (only effective\n // when the IPC client is up — i.e. in full mode).\n void refreshCachedTools(api);\n return cachedDescriptors.map((d) => buildAgentTool(api, d));\n });\n }\n\n // ── Live IPC client (full agent-runtime mode only) ─────────\n //\n // Construct the IPC client here so the gateway socket opens at\n // activate time, NOT lazily on first tool invocation. Two reasons:\n //\n // 1. The factory's `void refreshCachedTools(api)` fires on every\n // tool resolution. If the IPC client is uninitialised, that\n // call short-circuits to NOT_CONNECTED and the cache never\n // grows. By constructing here we ensure the very first\n // factory invocation sees a connecting (or connected) client.\n // 2. The disk cache handles tool-discovery / CLI modes where\n // `shouldStartIpcClient` returns false — we never NEED a live\n // IPC there.\n //\n // The client itself is non-blocking: `start()` initiates the\n // connect but returns immediately. The `onConnect` callback runs\n // microseconds later when the socket is up.\n // Unconditional info log so an activate is always observable in\n // the gateway journal. Sibling plugins log their activation\n // unconditionally too. Without this, debugging \"did the bundler\n // even activate?\" requires guessing at registrationMode.\n log.info(\n `[mcp-bundler] activated v${plugin.version} (registrationMode=${api.registrationMode ?? 'unknown'}, deferToNative=${String(deferToNative)}, cachedDescriptors=${String(cachedDescriptors.length)})`,\n );\n\n if (shouldStartIpcClient(api)) {\n startIpcClient(api, log);\n }\n },\n\n deactivate(api: OpenClawPluginApi) {\n g[ACTIVATED_KEY] = false;\n api.logger.debug('[mcp-bundler] deactivating');\n stopIpcClient();\n },\n};\n\nexport { type IpcResponse };\nexport default plugin;\n"],"mappings":"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"}
@@ -40,7 +40,9 @@
40
40
  "xero-xero__xero_check_connection",
41
41
  "xero-xero__xero_refresh_token",
42
42
  "alfe-platform__session_status",
43
- "alfe-platform__gateway_status"
43
+ "alfe-platform__gateway_status",
44
+ "alfe-platform__integrations_browse_registry",
45
+ "alfe-platform__integrations_start_oauth"
44
46
  ]
45
47
  },
46
48
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-mcp-bundler",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "OpenClaw plugin: thin IPC client over the alfe-gateway daemon's hosted MCP bundler. Surfaces every MCP tool the daemon knows about to the agent and exposes self-service alfe_mcp_list / add / remove tools for agent-driven roster management.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",