@alfe.ai/openclaw-mcp-bundler 0.0.16 → 0.0.18

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
@@ -61,6 +61,16 @@ interface OpenClawPluginApi {
61
61
  names?: string[];
62
62
  optional?: boolean;
63
63
  }) => void;
64
+ /**
65
+ * Register an OpenClaw lifecycle hook. Optional in this narrowed view
66
+ * because not every runtime mode exposes it; callers must guard with
67
+ * `typeof api.on === 'function'`. The `before_agent_start` hook may
68
+ * return `{ prependContext: string }` to prepend text to the agent's
69
+ * prompt (mirrors `@alfe.ai/openclaw-memory-cloud`).
70
+ */
71
+ on?: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: {
72
+ priority?: number;
73
+ }) => void;
64
74
  }
65
75
  declare const plugin: {
66
76
  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;;ICiEF,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;;;;;;cA8oB1D;;;;;;;;gBAwCU;kBAgGE"}
1
+ {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICiEF,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;;;;;;;;;;;;;;;;cAgxB1D;;;;;;;;gBAwCU;kBA6HE"}
package/dist/plugin.d.ts CHANGED
@@ -61,6 +61,16 @@ interface OpenClawPluginApi {
61
61
  names?: string[];
62
62
  optional?: boolean;
63
63
  }) => void;
64
+ /**
65
+ * Register an OpenClaw lifecycle hook. Optional in this narrowed view
66
+ * because not every runtime mode exposes it; callers must guard with
67
+ * `typeof api.on === 'function'`. The `before_agent_start` hook may
68
+ * return `{ prependContext: string }` to prepend text to the agent's
69
+ * prompt (mirrors `@alfe.ai/openclaw-memory-cloud`).
70
+ */
71
+ on?: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: {
72
+ priority?: number;
73
+ }) => void;
64
74
  }
65
75
  declare const plugin: {
66
76
  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;;ICiEF,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;;;;;;cA8oB1D;;;;;;;;gBAwCU;kBAgGE"}
1
+ {"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICiEF,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;;;;;;;;;;;;;;;;cAgxB1D;;;;;;;;gBAwCU;kBA6HE"}
package/dist/plugin2.cjs CHANGED
@@ -239,14 +239,99 @@ const CACHE_FILE = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "mcp", "
239
239
  const GATEWAY_SOCKET = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "gateway.sock");
240
240
  let ipcClient;
241
241
  let cachedDescriptors = [];
242
+ let beforeAgentStartHookRegistered = false;
242
243
  let refreshInFlight = null;
243
244
  let periodicRefreshTimer = null;
244
245
  let moduleApi;
245
246
  let moduleLog;
246
- const ACTIVATED_KEY = "__alfeMcpBundlerPluginActivated";
247
- const g = globalThis;
248
247
  const IPC_CONNECT_TIMEOUT_MS = 2500;
249
248
  const PERIODIC_REFRESH_INTERVAL_MS = 1e4;
249
+ /** Max chars of injected context — keep the per-session prompt cost bounded. */
250
+ const MAX_TOOLS_CONTEXT_CHARS = 1500;
251
+ /** Example tool names shown per server before collapsing to "+N more". */
252
+ const MAX_EXAMPLE_TOOLS_PER_SERVER = 4;
253
+ /** Nicer display names for well-known server ids; falls back to derivation. */
254
+ const FRIENDLY_SERVER_NAMES = {
255
+ github: "GitHub",
256
+ atlassian: "Atlassian",
257
+ notion: "Notion",
258
+ google: "Google",
259
+ microsoft: "Microsoft",
260
+ myob: "MYOB",
261
+ slack: "Slack",
262
+ discord: "Discord",
263
+ xero: "Xero",
264
+ "alfe-platform": "Alfe platform"
265
+ };
266
+ function titleCase(s) {
267
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
268
+ }
269
+ /**
270
+ * Render a bundler server id into a human label.
271
+ * - `custom:<manifestId>@<sha>#<conn>-<srv>` → `<manifestId>` (custom connections)
272
+ * - `<integrationId>-<integrationId>` → `<integrationId>` (the common case)
273
+ * - otherwise → title-cased words
274
+ */
275
+ function friendlyServerName(server) {
276
+ if (FRIENDLY_SERVER_NAMES[server]) return FRIENDLY_SERVER_NAMES[server];
277
+ let base = server;
278
+ if (server.startsWith("custom:")) base = server.slice(7).split("@")[0] || server;
279
+ else {
280
+ const segs = server.split("-");
281
+ if (segs.length >= 2 && segs[0] === segs[1]) base = segs[0];
282
+ }
283
+ if (FRIENDLY_SERVER_NAMES[base]) return FRIENDLY_SERVER_NAMES[base];
284
+ return base.split(/[-_]/).map(titleCase).join(" ");
285
+ }
286
+ /**
287
+ * Build the `prependContext` string for `before_agent_start` from the
288
+ * last-known tool catalog. Returns `undefined` when nothing is connected
289
+ * (inject nothing rather than noise). Collapses to counts-only if the
290
+ * example-name form exceeds the char budget.
291
+ */
292
+ function buildAvailableToolsContext(descriptors) {
293
+ if (descriptors.length === 0) return void 0;
294
+ const byServer = /* @__PURE__ */ new Map();
295
+ for (const d of descriptors) {
296
+ const arr = byServer.get(d.server);
297
+ if (arr) arr.push(d);
298
+ else byServer.set(d.server, [d]);
299
+ }
300
+ const servers = [...byServer.entries()].sort((a, b) => friendlyServerName(a[0]).localeCompare(friendlyServerName(b[0])));
301
+ const detailed = `<available_integrations>
302
+ You have external integrations available via MCP tools. To use them: call \`alfe_mcp_list_tools\` to see exact tool names + input schemas, then invoke one with \`alfe_mcp_call({ tool, args })\` (filter to one server with \`alfe_mcp_list_tools({ server })\`).
303
+
304
+ Connected now:
305
+ ${servers.map(([server, tools]) => {
306
+ const examples = tools.slice(0, MAX_EXAMPLE_TOOLS_PER_SERVER).map((t) => t.original);
307
+ const extra = tools.length - examples.length;
308
+ const exampleStr = examples.join(", ") + (extra > 0 ? `, +${String(extra)} more` : "");
309
+ return `- ${friendlyServerName(server)} — ${exampleStr} (${String(tools.length)} tool${tools.length === 1 ? "" : "s"})`;
310
+ }).join("\n")}
311
+
312
+ Before telling the user a capability isn't available, check \`alfe_mcp_list_tools\` first — the tool may exist in one of these.
313
+ </available_integrations>`;
314
+ if (detailed.length <= MAX_TOOLS_CONTEXT_CHARS) return detailed;
315
+ const compactLines = servers.map(([server, tools]) => `- ${friendlyServerName(server)} (${String(tools.length)} tool${tools.length === 1 ? "" : "s"})`);
316
+ const render = (lines, omitted) => {
317
+ const tail = omitted > 0 ? `\n- …and ${String(omitted)} more integration${omitted === 1 ? "" : "s"}` : "";
318
+ return `<available_integrations>
319
+ You have external integrations available via MCP tools. Call \`alfe_mcp_list_tools\` to see exact tool names + schemas, then invoke one with \`alfe_mcp_call({ tool, args })\`.
320
+
321
+ Connected now:
322
+ ${lines.join("\n")}${tail}
323
+
324
+ Before telling the user a capability isn't available, check \`alfe_mcp_list_tools\` first — the tool may exist in one of these.
325
+ </available_integrations>`;
326
+ };
327
+ let kept = compactLines.length;
328
+ let out = render(compactLines, 0);
329
+ while (kept > 1 && out.length > MAX_TOOLS_CONTEXT_CHARS) {
330
+ kept -= 1;
331
+ out = render(compactLines.slice(0, kept), compactLines.length - kept);
332
+ }
333
+ return out;
334
+ }
250
335
  function resolveLiveConfig(api, ctx) {
251
336
  return ctx?.runtimeConfig ?? ctx?.getRuntimeConfig?.() ?? api.runtime?.config?.current?.() ?? ctx?.config ?? api.config;
252
337
  }
@@ -699,13 +784,7 @@ const plugin = {
699
784
  ] },
700
785
  activate(api) {
701
786
  const log = api.logger;
702
- if (g[ACTIVATED_KEY] === true) {
703
- log.debug("[mcp-bundler] already activated — skipping duplicate");
704
- return;
705
- }
706
- g[ACTIVATED_KEY] = true;
707
787
  if (api.pluginConfig?.disabled) {
708
- g[ACTIVATED_KEY] = false;
709
788
  log.info("[mcp-bundler] disabled via plugin config — no-op");
710
789
  return;
711
790
  }
@@ -716,10 +795,21 @@ const plugin = {
716
795
  moduleApi = api;
717
796
  moduleLog = log;
718
797
  log.info(`[mcp-bundler] activated v${plugin.version} (registrationMode=${api.registrationMode ?? "unknown"}, nativeMcpServers=${String(hasNativeServers)}, cachedDescriptors=${String(cachedDescriptors.length)})`);
798
+ if (typeof api.on === "function" && !beforeAgentStartHookRegistered) {
799
+ beforeAgentStartHookRegistered = true;
800
+ api.on("before_agent_start", () => {
801
+ try {
802
+ const context = buildAvailableToolsContext(cachedDescriptors);
803
+ return context ? { prependContext: context } : void 0;
804
+ } catch (err) {
805
+ log.warn(`[mcp-bundler] before_agent_start context injection failed: ${err instanceof Error ? err.message : String(err)}`);
806
+ return;
807
+ }
808
+ }, { priority: 10 });
809
+ }
719
810
  if (shouldStartIpcClient(api)) startIpcClient(log);
720
811
  },
721
812
  deactivate(api) {
722
- g[ACTIVATED_KEY] = false;
723
813
  api.logger.debug("[mcp-bundler] deactivating");
724
814
  stopIpcClient();
725
815
  }
package/dist/plugin2.js CHANGED
@@ -239,14 +239,99 @@ const CACHE_FILE = join(homedir(), ".alfe", "mcp", "openclaw-bundler-cache.json"
239
239
  const GATEWAY_SOCKET = join(homedir(), ".alfe", "gateway.sock");
240
240
  let ipcClient;
241
241
  let cachedDescriptors = [];
242
+ let beforeAgentStartHookRegistered = false;
242
243
  let refreshInFlight = null;
243
244
  let periodicRefreshTimer = null;
244
245
  let moduleApi;
245
246
  let moduleLog;
246
- const ACTIVATED_KEY = "__alfeMcpBundlerPluginActivated";
247
- const g = globalThis;
248
247
  const IPC_CONNECT_TIMEOUT_MS = 2500;
249
248
  const PERIODIC_REFRESH_INTERVAL_MS = 1e4;
249
+ /** Max chars of injected context — keep the per-session prompt cost bounded. */
250
+ const MAX_TOOLS_CONTEXT_CHARS = 1500;
251
+ /** Example tool names shown per server before collapsing to "+N more". */
252
+ const MAX_EXAMPLE_TOOLS_PER_SERVER = 4;
253
+ /** Nicer display names for well-known server ids; falls back to derivation. */
254
+ const FRIENDLY_SERVER_NAMES = {
255
+ github: "GitHub",
256
+ atlassian: "Atlassian",
257
+ notion: "Notion",
258
+ google: "Google",
259
+ microsoft: "Microsoft",
260
+ myob: "MYOB",
261
+ slack: "Slack",
262
+ discord: "Discord",
263
+ xero: "Xero",
264
+ "alfe-platform": "Alfe platform"
265
+ };
266
+ function titleCase(s) {
267
+ return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
268
+ }
269
+ /**
270
+ * Render a bundler server id into a human label.
271
+ * - `custom:<manifestId>@<sha>#<conn>-<srv>` → `<manifestId>` (custom connections)
272
+ * - `<integrationId>-<integrationId>` → `<integrationId>` (the common case)
273
+ * - otherwise → title-cased words
274
+ */
275
+ function friendlyServerName(server) {
276
+ if (FRIENDLY_SERVER_NAMES[server]) return FRIENDLY_SERVER_NAMES[server];
277
+ let base = server;
278
+ if (server.startsWith("custom:")) base = server.slice(7).split("@")[0] || server;
279
+ else {
280
+ const segs = server.split("-");
281
+ if (segs.length >= 2 && segs[0] === segs[1]) base = segs[0];
282
+ }
283
+ if (FRIENDLY_SERVER_NAMES[base]) return FRIENDLY_SERVER_NAMES[base];
284
+ return base.split(/[-_]/).map(titleCase).join(" ");
285
+ }
286
+ /**
287
+ * Build the `prependContext` string for `before_agent_start` from the
288
+ * last-known tool catalog. Returns `undefined` when nothing is connected
289
+ * (inject nothing rather than noise). Collapses to counts-only if the
290
+ * example-name form exceeds the char budget.
291
+ */
292
+ function buildAvailableToolsContext(descriptors) {
293
+ if (descriptors.length === 0) return void 0;
294
+ const byServer = /* @__PURE__ */ new Map();
295
+ for (const d of descriptors) {
296
+ const arr = byServer.get(d.server);
297
+ if (arr) arr.push(d);
298
+ else byServer.set(d.server, [d]);
299
+ }
300
+ const servers = [...byServer.entries()].sort((a, b) => friendlyServerName(a[0]).localeCompare(friendlyServerName(b[0])));
301
+ const detailed = `<available_integrations>
302
+ You have external integrations available via MCP tools. To use them: call \`alfe_mcp_list_tools\` to see exact tool names + input schemas, then invoke one with \`alfe_mcp_call({ tool, args })\` (filter to one server with \`alfe_mcp_list_tools({ server })\`).
303
+
304
+ Connected now:
305
+ ${servers.map(([server, tools]) => {
306
+ const examples = tools.slice(0, MAX_EXAMPLE_TOOLS_PER_SERVER).map((t) => t.original);
307
+ const extra = tools.length - examples.length;
308
+ const exampleStr = examples.join(", ") + (extra > 0 ? `, +${String(extra)} more` : "");
309
+ return `- ${friendlyServerName(server)} — ${exampleStr} (${String(tools.length)} tool${tools.length === 1 ? "" : "s"})`;
310
+ }).join("\n")}
311
+
312
+ Before telling the user a capability isn't available, check \`alfe_mcp_list_tools\` first — the tool may exist in one of these.
313
+ </available_integrations>`;
314
+ if (detailed.length <= MAX_TOOLS_CONTEXT_CHARS) return detailed;
315
+ const compactLines = servers.map(([server, tools]) => `- ${friendlyServerName(server)} (${String(tools.length)} tool${tools.length === 1 ? "" : "s"})`);
316
+ const render = (lines, omitted) => {
317
+ const tail = omitted > 0 ? `\n- …and ${String(omitted)} more integration${omitted === 1 ? "" : "s"}` : "";
318
+ return `<available_integrations>
319
+ You have external integrations available via MCP tools. Call \`alfe_mcp_list_tools\` to see exact tool names + schemas, then invoke one with \`alfe_mcp_call({ tool, args })\`.
320
+
321
+ Connected now:
322
+ ${lines.join("\n")}${tail}
323
+
324
+ Before telling the user a capability isn't available, check \`alfe_mcp_list_tools\` first — the tool may exist in one of these.
325
+ </available_integrations>`;
326
+ };
327
+ let kept = compactLines.length;
328
+ let out = render(compactLines, 0);
329
+ while (kept > 1 && out.length > MAX_TOOLS_CONTEXT_CHARS) {
330
+ kept -= 1;
331
+ out = render(compactLines.slice(0, kept), compactLines.length - kept);
332
+ }
333
+ return out;
334
+ }
250
335
  function resolveLiveConfig(api, ctx) {
251
336
  return ctx?.runtimeConfig ?? ctx?.getRuntimeConfig?.() ?? api.runtime?.config?.current?.() ?? ctx?.config ?? api.config;
252
337
  }
@@ -699,13 +784,7 @@ const plugin = {
699
784
  ] },
700
785
  activate(api) {
701
786
  const log = api.logger;
702
- if (g[ACTIVATED_KEY] === true) {
703
- log.debug("[mcp-bundler] already activated — skipping duplicate");
704
- return;
705
- }
706
- g[ACTIVATED_KEY] = true;
707
787
  if (api.pluginConfig?.disabled) {
708
- g[ACTIVATED_KEY] = false;
709
788
  log.info("[mcp-bundler] disabled via plugin config — no-op");
710
789
  return;
711
790
  }
@@ -716,10 +795,21 @@ const plugin = {
716
795
  moduleApi = api;
717
796
  moduleLog = log;
718
797
  log.info(`[mcp-bundler] activated v${plugin.version} (registrationMode=${api.registrationMode ?? "unknown"}, nativeMcpServers=${String(hasNativeServers)}, cachedDescriptors=${String(cachedDescriptors.length)})`);
798
+ if (typeof api.on === "function" && !beforeAgentStartHookRegistered) {
799
+ beforeAgentStartHookRegistered = true;
800
+ api.on("before_agent_start", () => {
801
+ try {
802
+ const context = buildAvailableToolsContext(cachedDescriptors);
803
+ return context ? { prependContext: context } : void 0;
804
+ } catch (err) {
805
+ log.warn(`[mcp-bundler] before_agent_start context injection failed: ${err instanceof Error ? err.message : String(err)}`);
806
+ return;
807
+ }
808
+ }, { priority: 10 });
809
+ }
719
810
  if (shouldStartIpcClient(api)) startIpcClient(log);
720
811
  },
721
812
  deactivate(api) {
722
- g[ACTIVATED_KEY] = false;
723
813
  api.logger.debug("[mcp-bundler] deactivating");
724
814
  stopIpcClient();
725
815
  }
@@ -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 * ── v0.0.16: router tools, not native registration ────────────────\n *\n * The bundler no longer registers the daemon's proxied MCP tools\n * natively. OpenClaw's `contracts.tools` is a strict-literal allowlist\n * with a turn-2+ coverage check (`cachedDescriptorsCoverToolNames`):\n * any tool the plugin registers that isn't in that static manifest\n * list is rejected, and on the cached path a single missing name drops\n * the WHOLE plugin's tools. That's unwinnable for a dynamic aggregator\n * whose real catalog (Atlassian alone: 3 meta-tools → 76 post-cloudId)\n * is only known at runtime. Through v0.0.15 we tried to register them\n * via a dynamic factory; the static list could never match the live\n * catalog, so tools vanished from the LLM's prompt.\n *\n * Instead the plugin declares FIVE fixed tools — forever (see\n * `contracts.tools` at the bottom of this file and in\n * `openclaw.plugin.json`; the two MUST stay in sync):\n * - alfe_mcp_list / alfe_mcp_add / alfe_mcp_remove — server roster\n * - alfe_mcp_list_tools — returns the live MCP catalog from the\n * daemon (disk-cache fallback when the socket is closed)\n * - alfe_mcp_call — invokes any catalog tool by { tool, args }\n * The agent discovers the catalog, then calls into it. New MCP tools\n * are usable the instant the daemon sees them — no manifest change, no\n * reload, no coverage check. Adding an integration never touches\n * `contracts.tools` again.\n *\n * IPC lifecycle: two paths to the same module-scoped client. EAGER —\n * `activate()` opens the socket in full agent-runtime mode (gated by\n * `shouldStartIpcClient`) so embedded one-shot runs start warm. LAZY —\n * `ensureIpcClient` (via `callIpc`) opens it on the FIRST tool\n * execution in any mode. The lazy path is the v0.0.16 fix: the gateway\n * daemon runs the long-lived runtime in `tool-discovery` mode, where\n * the eager `=== 'full'` gate never fired, so tool calls failed with\n * \"store isn't initialized\". Pure tool-discovery loads that enumerate\n * but never execute still open no socket. No `api.registerService`\n * (its `onStartup:false` + service-lifecycle combo dropped tools during\n * the runtime-subagent-mode registry rebuild — the v0.0.13 bug).\n *\n * See `packages/openclaw-mcp-bundler/DEVELOPING.md` for the full\n * diagnosis (lazy-IPC, coverage check, router redesign).\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 — the last-known MCP tool list. JSON-\n// serialisable so it round-trips through the disk cache. The router's\n// `alfe_mcp_list_tools` returns a live `mcp.list_tools` result when the\n// daemon is reachable and falls back to this when it isn't.\nlet cachedDescriptors: McpToolDescriptor[] = [];\nlet refreshInFlight: Promise<void> | null = null;\nlet periodicRefreshTimer: ReturnType<typeof setInterval> | null = null;\n// Captured at `activate()` so the lazy IPC path (`ensureIpcClient`) can\n// construct the client on first tool call WITHOUT an `api` argument\n// threaded through every call site. `activate()` always runs before any\n// tool executes (a tool must be registered before it can be invoked),\n// and the plugin module is process-cached, so these are populated for\n// the lifetime of the runtime process by the time `callIpc` needs them.\nlet moduleApi: OpenClawPluginApi | undefined;\nlet moduleLog: Logger | undefined;\nconst ACTIVATED_KEY = '__alfeMcpBundlerPluginActivated';\nconst g = globalThis as Record<string, unknown>;\n\n// How long `callIpc` waits for the socket to finish connecting before\n// giving up. `McpIpcClient.start()` initiates the connect asynchronously\n// and `.call()` returns NOT_CONNECTED until `connected` flips true\n// (~50-300ms after start on a healthy local socket). Without this wait,\n// the FIRST tool call after a lazy construct would always race the\n// connect and fail. 2.5s covers a cold daemon socket with headroom.\nconst IPC_CONNECT_TIMEOUT_MS = 2500;\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 errorResult(message: string): AgentToolResult {\n return {\n content: [{ type: 'text', text: message }],\n details: { isError: true },\n };\n}\n\n/**\n * Lazily construct + connect the IPC client on first use.\n *\n * THE LOAD-BEARING FIX (v0.0.16, 2026-06-14). The gateway daemon runs\n * the long-lived openclaw runtime in `registrationMode=tool-discovery`,\n * NOT `full`. Pre-v0.0.16 the client was only constructed in `activate()`\n * when `shouldStartIpcClient` returned true (full mode only) — so in the\n * runtime that actually serves the agent's turns, the client was NEVER\n * constructed. Verified on QA Tester: the 2.6-day-uptime runtime held\n * zero connections to `~/.alfe/gateway.sock`. Listing worked (disk cache)\n * but every tool EXECUTION returned NOT_CONNECTED → \"bundler store isn't\n * initialized.\"\n *\n * The correct trigger for opening the socket is \"a tool is actually being\n * invoked\", not \"we activated in full mode\". Tool execution only ever\n * happens in a live runtime process, and the plugin module is process-\n * cached, so constructing here populates the module-scoped `ipcClient`\n * for every later call in the same process. Pure tool-discovery loads\n * that enumerate but never execute still never reach this path → no\n * socket opened, the discovery invariant is preserved.\n *\n * Returns `undefined` only when there's genuinely no daemon to talk to\n * (no socket file, or `activate()` never captured the module refs).\n */\nfunction ensureIpcClient(): McpIpcClient | undefined {\n if (ipcClient) return ipcClient;\n if (!moduleLog) return undefined; // activate() never ran in this process.\n if (!existsSync(GATEWAY_SOCKET)) {\n moduleLog.debug(\n `[mcp-bundler] ${GATEWAY_SOCKET} not present — cannot open IPC for this call`,\n );\n return undefined;\n }\n startIpcClient(moduleLog);\n moduleLog.info('[mcp-bundler] IPC client constructed lazily on first tool call');\n return ipcClient;\n}\n\n/**\n * Poll `isConnected()` until the socket is up or the budget expires.\n * `McpIpcClient.start()` connects asynchronously, so a freshly-\n * constructed client (or one mid-reconnect after a daemon bounce) needs\n * a moment before `.call()` will reach the wire.\n */\nasync function waitForIpcConnect(client: McpIpcClient, timeoutMs: number): Promise<void> {\n if (client.isConnected()) return;\n const deadline = Date.now() + timeoutMs;\n while (!client.isConnected() && Date.now() < deadline) {\n await new Promise<void>((resolve) => setTimeout(resolve, 50));\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). `callIpc` constructs the client on\n * demand (`ensureIpcClient`) and waits for the socket to connect before\n * forwarding, so the very first tool call works even in the tool-\n * discovery runtime where the client was never eagerly started. Returns\n * a typed `NOT_CONNECTED` response — never throws — when there's no\n * daemon socket to reach.\n */\nasync function callIpc<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n): Promise<IpcResponse<T>> {\n const client = ensureIpcClient();\n if (!client) {\n return {\n id: '',\n ok: false,\n error: {\n code: 'NOT_CONNECTED',\n message: 'Bundler IPC unavailable — no alfe-gateway daemon socket',\n },\n };\n }\n // Only pay the connect-wait when the socket isn't up yet (first call\n // after a lazy construct, or mid-reconnect). When already connected,\n // forward immediately — no extra async hop, so the warm path is as\n // cheap as a bare `client.call`.\n if (!client.isConnected()) {\n await waitForIpcConnect(client, IPC_CONNECT_TIMEOUT_MS);\n }\n return client.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 surface through `alfe_mcp_list_tools` with undefined\n * names/schemas and mislead the agent. Catching the shape here keeps\n * the listed catalogue clean even after 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 // ── Router tools (the dynamic-MCP bridge) ──────────────────────\n //\n // These two tools are the whole reason the bundler can surface an\n // arbitrary, changing MCP toolset to a non-Claude LLM without\n // hitting OpenClaw's static `contracts.tools` allowlist.\n //\n // OpenClaw requires every agent-callable tool to be declared by name\n // in `contracts.tools` (strict literal, no globs — confirmed in\n // `loader.js#registerTool` / `tools.js#resolvePluginTools`). A\n // dynamic aggregator like the bundler can't enumerate its tools at\n // build time — Atlassian alone jumps from 3 to 76 tools the moment a\n // cloudId is selected. Declaring all of them statically is\n // impossible; the daemon regenerating the manifest fights a one-\n // reload lag.\n //\n // The router sidesteps it: `alfe_mcp_list_tools` + `alfe_mcp_call`\n // are TWO fixed names, declared once, forever. The agent discovers\n // the live catalog via list_tools, then invokes any tool by name via\n // call. New MCP tools are usable the instant the daemon's bundler\n // sees them — no manifest change, no reload. This is the same\n // `use_mcp_tool` pattern Cline / Roo-Code ship and the \"dynamic\n // discover + execute\" pattern Anthropic recommends for MCP at scale.\n api.registerTool({\n name: 'alfe_mcp_list_tools',\n label: 'List available MCP tools',\n description:\n 'List every MCP tool currently available across all of this agent\\'s connected integrations (Atlassian/Jira, Confluence, Notion, GitHub, etc.), with each tool\\'s exact name, description, and input schema. ALWAYS call this first to discover what tools exist and how to call them, then invoke one with `alfe_mcp_call`. Optionally pass `server` to restrict the listing to a single MCP server (e.g. \"atlassian-atlassian\") when you already know which integration you need.',\n parameters: {\n type: 'object',\n properties: {\n server: {\n type: 'string',\n description:\n 'Optional. Restrict the listing to one MCP server id (e.g. \"atlassian-atlassian\", \"github-github\"). Omit to list every available tool.',\n },\n },\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n // Pull the live catalog over IPC (lazily connecting if needed).\n // Fall back to the on-disk cache if the daemon is momentarily\n // unreachable so the agent still sees the last-known toolset.\n const response = await callIpc<{ tools: McpToolDescriptor[] }>('mcp.list_tools', {});\n let descriptors: McpToolDescriptor[];\n if (response.ok && response.payload?.tools && response.payload.tools.length > 0) {\n descriptors = response.payload.tools;\n cachedDescriptors = descriptors;\n if (moduleLog) persistCachedDescriptorsToDisk(descriptors, moduleLog);\n } else {\n descriptors = cachedDescriptors;\n }\n\n const p = (params ?? {}) as { server?: unknown };\n const serverFilter = typeof p.server === 'string' ? p.server : undefined;\n const filtered = serverFilter\n ? descriptors.filter((d) => d.server === serverFilter)\n : descriptors;\n\n const out = filtered.map((d) => ({\n tool: d.prefixed,\n description: d.description || `MCP tool ${d.original} from server ${d.server}`,\n inputSchema: d.parameters,\n }));\n\n if (out.length === 0) {\n return {\n content: [\n {\n type: 'text',\n text: serverFilter\n ? `No MCP tools available for server \"${serverFilter}\". Call alfe_mcp_list to see registered servers.`\n : 'No MCP tools available yet. Connected integrations may still be initialising — try again in a moment, or call alfe_mcp_list to see registered servers.',\n },\n ],\n details: { isError: false },\n };\n }\n\n return {\n content: [\n {\n type: 'text',\n text: `${String(out.length)} MCP tool(s) available. Call any of them with alfe_mcp_call({ tool, args }):\\n\\n${JSON.stringify(out, null, 2)}`,\n },\n ],\n details: { isError: false },\n };\n },\n });\n\n api.registerTool({\n name: 'alfe_mcp_call',\n label: 'Call an MCP tool',\n description:\n 'Execute one of the MCP tools returned by `alfe_mcp_list_tools`. Pass the exact `tool` name (e.g. \"atlassian-atlassian__jira_search\") and an `args` object matching that tool\\'s input schema from the listing. This is how you actually use Jira, Confluence, Notion, GitHub, etc. — list the tools first, then call them here.',\n parameters: {\n type: 'object',\n properties: {\n tool: {\n type: 'string',\n description:\n 'The exact prefixed tool name from alfe_mcp_list_tools (e.g. \"atlassian-atlassian__jira_get_issue\").',\n },\n args: {\n type: 'object',\n description:\n \"Arguments object matching the chosen tool's input schema (as shown by alfe_mcp_list_tools). Pass {} for tools that take no arguments.\",\n additionalProperties: true,\n },\n },\n required: ['tool'],\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n const p = (params ?? {}) as { tool?: unknown; args?: Record<string, unknown> };\n const tool = p.tool;\n if (typeof tool !== 'string' || !tool) {\n return errorResult(\n 'alfe_mcp_call requires a `tool` name — call alfe_mcp_list_tools first to see the available tool names.',\n );\n }\n const args = p.args ?? {};\n const response = await callIpc<McpToolCallResult>('mcp.call_tool', { name: tool, args });\n if (!response.ok || !response.payload) {\n (moduleLog ?? api.logger).warn('[mcp-bundler] alfe_mcp_call failed', {\n tool,\n err: response.error?.message ?? 'unknown error',\n });\n return errorResult(\n response.error?.message\n ? `mcp ${tool}: ${response.error.message}`\n : `mcp ${tool}: call failed (is the tool name exact? call alfe_mcp_list_tools to confirm)`,\n );\n }\n const result = response.payload;\n return {\n content: result.content,\n details: { isError: result.isError ?? false },\n };\n },\n });\n}\n\n/**\n * Pull the latest tool catalog from the daemon-hosted bundler into\n * `cachedDescriptors` + the disk cache. Driven by the periodic timer\n * and the `onConnect` hook so the disk-cache fallback that\n * `alfe_mcp_list_tools` reads stays warm even for tool-discovery loads\n * that never execute a tool. Coalesces concurrent calls so a burst only\n * 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 // Guard against a malformed `ok` payload (no `tools` array) —\n // treat it like an empty result rather than crashing the\n // fire-and-forget refresh on `undefined.length`.\n const fresh = Array.isArray(response.payload.tools) ? 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; `alfe_mcp_list_tools` returns whatever is in\n * `cachedDescriptors` when it's called with a closed socket (loaded\n * from disk in `activate()`, then refreshed on connect, then\n * periodically). A first-turn pre-warm is no longer needed — the disk\n * cache covers the cold window, and any new tools discovered during the\n * in-flight `onConnect` refresh land in `cachedDescriptors` shortly\n * after.\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(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 // `moduleApi` is captured at activate(); it's always set by the\n // time the socket connects (activate ran first). Guard defensively\n // for the test harness / unexpected ordering.\n if (moduleApi) void refreshCachedTools(moduleApi);\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() && moduleApi) {\n void refreshCachedTools(moduleApi);\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 // FIVE fixed names — declared once, forever. This is the whole\n // point of the v0.0.16 router redesign.\n //\n // OpenClaw's `contracts.tools` matcher (`loader.js#registerTool`,\n // `tools.js#resolvePluginTools`) is a STRICT LITERAL `Set.has()` —\n // no globs, and any tool a plugin tries to register that isn't\n // listed here is rejected as \"undeclared\". That is a fundamental\n // mismatch for a DYNAMIC MCP aggregator: the bundler's real toolset\n // is whatever the daemon currently proxies, and it changes at\n // runtime (Atlassian alone goes 3 → 76 tools the moment a cloudId\n // is selected). Enumerating every possible tool here is impossible,\n // and regenerating the manifest from the live catalog fights a\n // one-reload lag.\n //\n // So we DON'T register the proxied tools natively at all. The two\n // router tools below — `alfe_mcp_list_tools` (discover) and\n // `alfe_mcp_call` (invoke by name) — are a stable, static surface\n // through which the agent reaches the entire live MCP catalog. New\n // tools are usable the instant the daemon sees them, with no\n // manifest change and no reload. (Same `use_mcp_tool` pattern as\n // Cline / Roo-Code; same \"dynamic discover + execute\" Anthropic\n // recommends for MCP at scale.)\n //\n // These five names are the complete, permanent contract. Adding a\n // new integration NEVER requires touching this list again.\n tools: [\n 'alfe_mcp_list',\n 'alfe_mcp_add',\n 'alfe_mcp_remove',\n 'alfe_mcp_list_tools',\n 'alfe_mcp_call',\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 // Informational only (v0.0.16): the router tools are always\n // registered regardless of native mcp.servers — unlike the old\n // dynamic factory, the router doesn't defer. We still detect &\n // log native mcp.servers because on claude-cli/codex backends\n // those tools ALSO surface via native bundleMcp, so an operator\n // seeing both the native tools and the alfe_mcp_* router tools\n // isn't a bug.\n const hasNativeServers = hasNativeOpenclawMcpServers(resolveLiveConfig(api));\n if (hasNativeServers) {\n log.info(\n '[mcp-bundler] openclaw.json#mcp.servers has entries — those surface natively on claude-cli/codex; the alfe_mcp_* router tools remain available on every backend',\n );\n }\n\n // ── Register the FIVE static tools in activate() ────────────\n //\n // v0.0.16 router redesign: the bundler no longer registers the\n // proxied MCP tools natively (that hit OpenClaw's static\n // `contracts.tools` allowlist — a dynamic aggregator can't\n // enumerate its tools at build time). Instead it registers five\n // fixed tools (3 management + `alfe_mcp_list_tools` +\n // `alfe_mcp_call`), and the agent reaches the entire live MCP\n // catalog through the two router tools. See the `contracts.tools`\n // comment above for the full rationale.\n //\n // No `api.registerService` (that combo drops tools — QA Tester\n // 2026-06-10), no dynamic factory (that needs per-tool declaration\n // — the whole problem this redesign removes). The IPC client is\n // module-scoped and lazily constructed on the first tool call (see\n // `ensureIpcClient`), which is what makes execution work in the\n // tool-discovery runtime that serves the agent's turns.\n\n // Seed the descriptor cache from disk so `alfe_mcp_list_tools` has\n // a last-known fallback if the daemon is momentarily unreachable.\n // Refreshed live on every `alfe_mcp_list_tools` call and by the\n // periodic refresh.\n cachedDescriptors = loadCachedDescriptorsFromDisk(log);\n\n // The five static tools (management + router). Always registered,\n // regardless of the native-bundleMcp escape hatch — the router is\n // a leaner, always-available access path and doesn't conflict with\n // native tools when claude-cli has them.\n registerAgentMcpManagementTools(api);\n\n // ── Live IPC client ────────────────────────────────────────\n //\n // Two paths to a live socket, both landing in the SAME module-\n // scoped `ipcClient`:\n //\n // 1. EAGER (full agent-runtime mode): open the socket now so the\n // first turn is already warm. Embedded one-shot runs benefit.\n // 2. LAZY (any other mode — crucially `tool-discovery`, which is\n // what the gateway daemon's long-lived runtime uses): the\n // socket opens on the FIRST tool execution via `callIpc` →\n // `ensureIpcClient`. This is the v0.0.16 fix — pre-v0.0.16\n // the runtime that serves the agent's turns never opened a\n // socket at all because it isn't `full` mode.\n //\n // Pure tool-discovery loads that enumerate tools but never execute\n // anything still never open a socket (the periodic refresh is a\n // no-op until something constructs the client). The discovery\n // invariant is preserved.\n //\n // Capture `api` + `log` module-scoped so the lazy path can\n // construct without threading args through every call site.\n moduleApi = api;\n moduleLog = log;\n\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'}, nativeMcpServers=${String(hasNativeServers)}, cachedDescriptors=${String(cachedDescriptors.length)})`,\n );\n\n if (shouldStartIpcClient(api)) {\n startIpcClient(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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"}
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.16: router tools, not native registration ────────────────\n *\n * The bundler no longer registers the daemon's proxied MCP tools\n * natively. OpenClaw's `contracts.tools` is a strict-literal allowlist\n * with a turn-2+ coverage check (`cachedDescriptorsCoverToolNames`):\n * any tool the plugin registers that isn't in that static manifest\n * list is rejected, and on the cached path a single missing name drops\n * the WHOLE plugin's tools. That's unwinnable for a dynamic aggregator\n * whose real catalog (Atlassian alone: 3 meta-tools → 76 post-cloudId)\n * is only known at runtime. Through v0.0.15 we tried to register them\n * via a dynamic factory; the static list could never match the live\n * catalog, so tools vanished from the LLM's prompt.\n *\n * Instead the plugin declares FIVE fixed tools — forever (see\n * `contracts.tools` at the bottom of this file and in\n * `openclaw.plugin.json`; the two MUST stay in sync):\n * - alfe_mcp_list / alfe_mcp_add / alfe_mcp_remove — server roster\n * - alfe_mcp_list_tools — returns the live MCP catalog from the\n * daemon (disk-cache fallback when the socket is closed)\n * - alfe_mcp_call — invokes any catalog tool by { tool, args }\n * The agent discovers the catalog, then calls into it. New MCP tools\n * are usable the instant the daemon sees them — no manifest change, no\n * reload, no coverage check. Adding an integration never touches\n * `contracts.tools` again.\n *\n * IPC lifecycle: two paths to the same module-scoped client. EAGER —\n * `activate()` opens the socket in full agent-runtime mode (gated by\n * `shouldStartIpcClient`) so embedded one-shot runs start warm. LAZY —\n * `ensureIpcClient` (via `callIpc`) opens it on the FIRST tool\n * execution in any mode. The lazy path is the v0.0.16 fix: the gateway\n * daemon runs the long-lived runtime in `tool-discovery` mode, where\n * the eager `=== 'full'` gate never fired, so tool calls failed with\n * \"store isn't initialized\". Pure tool-discovery loads that enumerate\n * but never execute still open no socket. No `api.registerService`\n * (its `onStartup:false` + service-lifecycle combo dropped tools during\n * the runtime-subagent-mode registry rebuild — the v0.0.13 bug).\n *\n * See `packages/openclaw-mcp-bundler/DEVELOPING.md` for the full\n * diagnosis (lazy-IPC, coverage check, router redesign).\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 * Register an OpenClaw lifecycle hook. Optional in this narrowed view\n * because not every runtime mode exposes it; callers must guard with\n * `typeof api.on === 'function'`. The `before_agent_start` hook may\n * return `{ prependContext: string }` to prepend text to the agent's\n * prompt (mirrors `@alfe.ai/openclaw-memory-cloud`).\n */\n on?: (\n hookName: string,\n handler: (...args: unknown[]) => unknown,\n opts?: { priority?: number },\n ) => void;\n}\n\n// ── Plugin state ───────────────────────────────────────────────\nlet ipcClient: McpIpcClient | undefined;\n// Module-scoped catalogue — the last-known MCP tool list. JSON-\n// serialisable so it round-trips through the disk cache. The router's\n// `alfe_mcp_list_tools` returns a live `mcp.list_tools` result when the\n// daemon is reachable and falls back to this when it isn't.\nlet cachedDescriptors: McpToolDescriptor[] = [];\nlet beforeAgentStartHookRegistered = false;\nlet refreshInFlight: Promise<void> | null = null;\nlet periodicRefreshTimer: ReturnType<typeof setInterval> | null = null;\n// Captured at `activate()` so the lazy IPC path (`ensureIpcClient`) can\n// construct the client on first tool call WITHOUT an `api` argument\n// threaded through every call site. `activate()` always runs before any\n// tool executes (a tool must be registered before it can be invoked),\n// and the plugin module is process-cached, so these are populated for\n// the lifetime of the runtime process by the time `callIpc` needs them.\nlet moduleApi: OpenClawPluginApi | undefined;\nlet moduleLog: Logger | undefined;\n\n// How long `callIpc` waits for the socket to finish connecting before\n// giving up. `McpIpcClient.start()` initiates the connect asynchronously\n// and `.call()` returns NOT_CONNECTED until `connected` flips true\n// (~50-300ms after start on a healthy local socket). Without this wait,\n// the FIRST tool call after a lazy construct would always race the\n// connect and fail. 2.5s covers a cold daemon socket with headroom.\nconst IPC_CONNECT_TIMEOUT_MS = 2500;\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\n// ── Session-start discoverability ──────────────────────────────\n//\n// Bundled MCP tools are reachable only through the router\n// (`alfe_mcp_list_tools` / `alfe_mcp_call`), so the agent has no static\n// awareness that e.g. a \"Luigi\" connection's `luigi_get_shifts` exists —\n// it has to think to call `alfe_mcp_list_tools`. We inject a short\n// `before_agent_start` context line listing the connected servers + a\n// few example tools so the model knows to look. Mirrors the\n// `before_agent_start` `prependContext` pattern in\n// `@alfe.ai/openclaw-memory-cloud`.\n\n/** Max chars of injected context — keep the per-session prompt cost bounded. */\nconst MAX_TOOLS_CONTEXT_CHARS = 1500;\n/** Example tool names shown per server before collapsing to \"+N more\". */\nconst MAX_EXAMPLE_TOOLS_PER_SERVER = 4;\n\n/** Nicer display names for well-known server ids; falls back to derivation. */\nconst FRIENDLY_SERVER_NAMES: Record<string, string> = {\n github: 'GitHub',\n atlassian: 'Atlassian',\n notion: 'Notion',\n google: 'Google',\n microsoft: 'Microsoft',\n myob: 'MYOB',\n slack: 'Slack',\n discord: 'Discord',\n xero: 'Xero',\n 'alfe-platform': 'Alfe platform',\n};\n\nfunction titleCase(s: string): string {\n return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;\n}\n\n/**\n * Render a bundler server id into a human label.\n * - `custom:<manifestId>@<sha>#<conn>-<srv>` → `<manifestId>` (custom connections)\n * - `<integrationId>-<integrationId>` → `<integrationId>` (the common case)\n * - otherwise → title-cased words\n */\nfunction friendlyServerName(server: string): string {\n if (FRIENDLY_SERVER_NAMES[server]) return FRIENDLY_SERVER_NAMES[server];\n let base = server;\n if (server.startsWith('custom:')) {\n base = server.slice('custom:'.length).split('@')[0] || server;\n } else {\n const segs = server.split('-');\n if (segs.length >= 2 && segs[0] === segs[1]) base = segs[0];\n }\n if (FRIENDLY_SERVER_NAMES[base]) return FRIENDLY_SERVER_NAMES[base];\n return base.split(/[-_]/).map(titleCase).join(' ');\n}\n\n/**\n * Build the `prependContext` string for `before_agent_start` from the\n * last-known tool catalog. Returns `undefined` when nothing is connected\n * (inject nothing rather than noise). Collapses to counts-only if the\n * example-name form exceeds the char budget.\n */\nfunction buildAvailableToolsContext(descriptors: McpToolDescriptor[]): string | undefined {\n if (descriptors.length === 0) return undefined;\n\n const byServer = new Map<string, McpToolDescriptor[]>();\n for (const d of descriptors) {\n const arr = byServer.get(d.server);\n if (arr) arr.push(d);\n else byServer.set(d.server, [d]);\n }\n\n const servers = [...byServer.entries()].sort((a, b) =>\n friendlyServerName(a[0]).localeCompare(friendlyServerName(b[0])),\n );\n\n const detailedLines = servers.map(([server, tools]) => {\n const examples = tools.slice(0, MAX_EXAMPLE_TOOLS_PER_SERVER).map((t) => t.original);\n const extra = tools.length - examples.length;\n const exampleStr = examples.join(', ') + (extra > 0 ? `, +${String(extra)} more` : '');\n return `- ${friendlyServerName(server)} — ${exampleStr} (${String(tools.length)} tool${tools.length === 1 ? '' : 's'})`;\n });\n\n const detailed = `<available_integrations>\nYou have external integrations available via MCP tools. To use them: call \\`alfe_mcp_list_tools\\` to see exact tool names + input schemas, then invoke one with \\`alfe_mcp_call({ tool, args })\\` (filter to one server with \\`alfe_mcp_list_tools({ server })\\`).\n\nConnected now:\n${detailedLines.join('\\n')}\n\nBefore telling the user a capability isn't available, check \\`alfe_mcp_list_tools\\` first — the tool may exist in one of these.\n</available_integrations>`;\n\n if (detailed.length <= MAX_TOOLS_CONTEXT_CHARS) return detailed;\n\n // Over budget — drop example names, keep server + counts. With many\n // servers even the compact form can exceed the budget, so cap the list\n // and summarise the remainder rather than emit an unbounded block.\n const compactLines = servers.map(\n ([server, tools]) =>\n `- ${friendlyServerName(server)} (${String(tools.length)} tool${tools.length === 1 ? '' : 's'})`,\n );\n const render = (lines: string[], omitted: number): string => {\n const tail = omitted > 0 ? `\\n- …and ${String(omitted)} more integration${omitted === 1 ? '' : 's'}` : '';\n return `<available_integrations>\nYou have external integrations available via MCP tools. Call \\`alfe_mcp_list_tools\\` to see exact tool names + schemas, then invoke one with \\`alfe_mcp_call({ tool, args })\\`.\n\nConnected now:\n${lines.join('\\n')}${tail}\n\nBefore telling the user a capability isn't available, check \\`alfe_mcp_list_tools\\` first — the tool may exist in one of these.\n</available_integrations>`;\n };\n\n let kept = compactLines.length;\n let out = render(compactLines, 0);\n while (kept > 1 && out.length > MAX_TOOLS_CONTEXT_CHARS) {\n kept -= 1;\n out = render(compactLines.slice(0, kept), compactLines.length - kept);\n }\n return out;\n}\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 errorResult(message: string): AgentToolResult {\n return {\n content: [{ type: 'text', text: message }],\n details: { isError: true },\n };\n}\n\n/**\n * Lazily construct + connect the IPC client on first use.\n *\n * THE LOAD-BEARING FIX (v0.0.16, 2026-06-14). The gateway daemon runs\n * the long-lived openclaw runtime in `registrationMode=tool-discovery`,\n * NOT `full`. Pre-v0.0.16 the client was only constructed in `activate()`\n * when `shouldStartIpcClient` returned true (full mode only) — so in the\n * runtime that actually serves the agent's turns, the client was NEVER\n * constructed. Verified on QA Tester: the 2.6-day-uptime runtime held\n * zero connections to `~/.alfe/gateway.sock`. Listing worked (disk cache)\n * but every tool EXECUTION returned NOT_CONNECTED → \"bundler store isn't\n * initialized.\"\n *\n * The correct trigger for opening the socket is \"a tool is actually being\n * invoked\", not \"we activated in full mode\". Tool execution only ever\n * happens in a live runtime process, and the plugin module is process-\n * cached, so constructing here populates the module-scoped `ipcClient`\n * for every later call in the same process. Pure tool-discovery loads\n * that enumerate but never execute still never reach this path → no\n * socket opened, the discovery invariant is preserved.\n *\n * Returns `undefined` only when there's genuinely no daemon to talk to\n * (no socket file, or `activate()` never captured the module refs).\n */\nfunction ensureIpcClient(): McpIpcClient | undefined {\n if (ipcClient) return ipcClient;\n if (!moduleLog) return undefined; // activate() never ran in this process.\n if (!existsSync(GATEWAY_SOCKET)) {\n moduleLog.debug(\n `[mcp-bundler] ${GATEWAY_SOCKET} not present — cannot open IPC for this call`,\n );\n return undefined;\n }\n startIpcClient(moduleLog);\n moduleLog.info('[mcp-bundler] IPC client constructed lazily on first tool call');\n return ipcClient;\n}\n\n/**\n * Poll `isConnected()` until the socket is up or the budget expires.\n * `McpIpcClient.start()` connects asynchronously, so a freshly-\n * constructed client (or one mid-reconnect after a daemon bounce) needs\n * a moment before `.call()` will reach the wire.\n */\nasync function waitForIpcConnect(client: McpIpcClient, timeoutMs: number): Promise<void> {\n if (client.isConnected()) return;\n const deadline = Date.now() + timeoutMs;\n while (!client.isConnected() && Date.now() < deadline) {\n await new Promise<void>((resolve) => setTimeout(resolve, 50));\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). `callIpc` constructs the client on\n * demand (`ensureIpcClient`) and waits for the socket to connect before\n * forwarding, so the very first tool call works even in the tool-\n * discovery runtime where the client was never eagerly started. Returns\n * a typed `NOT_CONNECTED` response — never throws — when there's no\n * daemon socket to reach.\n */\nasync function callIpc<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n): Promise<IpcResponse<T>> {\n const client = ensureIpcClient();\n if (!client) {\n return {\n id: '',\n ok: false,\n error: {\n code: 'NOT_CONNECTED',\n message: 'Bundler IPC unavailable — no alfe-gateway daemon socket',\n },\n };\n }\n // Only pay the connect-wait when the socket isn't up yet (first call\n // after a lazy construct, or mid-reconnect). When already connected,\n // forward immediately — no extra async hop, so the warm path is as\n // cheap as a bare `client.call`.\n if (!client.isConnected()) {\n await waitForIpcConnect(client, IPC_CONNECT_TIMEOUT_MS);\n }\n return client.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 surface through `alfe_mcp_list_tools` with undefined\n * names/schemas and mislead the agent. Catching the shape here keeps\n * the listed catalogue clean even after 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 // ── Router tools (the dynamic-MCP bridge) ──────────────────────\n //\n // These two tools are the whole reason the bundler can surface an\n // arbitrary, changing MCP toolset to a non-Claude LLM without\n // hitting OpenClaw's static `contracts.tools` allowlist.\n //\n // OpenClaw requires every agent-callable tool to be declared by name\n // in `contracts.tools` (strict literal, no globs — confirmed in\n // `loader.js#registerTool` / `tools.js#resolvePluginTools`). A\n // dynamic aggregator like the bundler can't enumerate its tools at\n // build time — Atlassian alone jumps from 3 to 76 tools the moment a\n // cloudId is selected. Declaring all of them statically is\n // impossible; the daemon regenerating the manifest fights a one-\n // reload lag.\n //\n // The router sidesteps it: `alfe_mcp_list_tools` + `alfe_mcp_call`\n // are TWO fixed names, declared once, forever. The agent discovers\n // the live catalog via list_tools, then invokes any tool by name via\n // call. New MCP tools are usable the instant the daemon's bundler\n // sees them — no manifest change, no reload. This is the same\n // `use_mcp_tool` pattern Cline / Roo-Code ship and the \"dynamic\n // discover + execute\" pattern Anthropic recommends for MCP at scale.\n api.registerTool({\n name: 'alfe_mcp_list_tools',\n label: 'List available MCP tools',\n description:\n 'List every MCP tool currently available across all of this agent\\'s connected integrations (Atlassian/Jira, Confluence, Notion, GitHub, etc.), with each tool\\'s exact name, description, and input schema. ALWAYS call this first to discover what tools exist and how to call them, then invoke one with `alfe_mcp_call`. Optionally pass `server` to restrict the listing to a single MCP server (e.g. \"atlassian-atlassian\") when you already know which integration you need.',\n parameters: {\n type: 'object',\n properties: {\n server: {\n type: 'string',\n description:\n 'Optional. Restrict the listing to one MCP server id (e.g. \"atlassian-atlassian\", \"github-github\"). Omit to list every available tool.',\n },\n },\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n // Pull the live catalog over IPC (lazily connecting if needed).\n // Fall back to the on-disk cache if the daemon is momentarily\n // unreachable so the agent still sees the last-known toolset.\n const response = await callIpc<{ tools: McpToolDescriptor[] }>('mcp.list_tools', {});\n let descriptors: McpToolDescriptor[];\n if (response.ok && response.payload?.tools && response.payload.tools.length > 0) {\n descriptors = response.payload.tools;\n cachedDescriptors = descriptors;\n if (moduleLog) persistCachedDescriptorsToDisk(descriptors, moduleLog);\n } else {\n descriptors = cachedDescriptors;\n }\n\n const p = (params ?? {}) as { server?: unknown };\n const serverFilter = typeof p.server === 'string' ? p.server : undefined;\n const filtered = serverFilter\n ? descriptors.filter((d) => d.server === serverFilter)\n : descriptors;\n\n const out = filtered.map((d) => ({\n tool: d.prefixed,\n description: d.description || `MCP tool ${d.original} from server ${d.server}`,\n inputSchema: d.parameters,\n }));\n\n if (out.length === 0) {\n return {\n content: [\n {\n type: 'text',\n text: serverFilter\n ? `No MCP tools available for server \"${serverFilter}\". Call alfe_mcp_list to see registered servers.`\n : 'No MCP tools available yet. Connected integrations may still be initialising — try again in a moment, or call alfe_mcp_list to see registered servers.',\n },\n ],\n details: { isError: false },\n };\n }\n\n return {\n content: [\n {\n type: 'text',\n text: `${String(out.length)} MCP tool(s) available. Call any of them with alfe_mcp_call({ tool, args }):\\n\\n${JSON.stringify(out, null, 2)}`,\n },\n ],\n details: { isError: false },\n };\n },\n });\n\n api.registerTool({\n name: 'alfe_mcp_call',\n label: 'Call an MCP tool',\n description:\n 'Execute one of the MCP tools returned by `alfe_mcp_list_tools`. Pass the exact `tool` name (e.g. \"atlassian-atlassian__jira_search\") and an `args` object matching that tool\\'s input schema from the listing. This is how you actually use Jira, Confluence, Notion, GitHub, etc. — list the tools first, then call them here.',\n parameters: {\n type: 'object',\n properties: {\n tool: {\n type: 'string',\n description:\n 'The exact prefixed tool name from alfe_mcp_list_tools (e.g. \"atlassian-atlassian__jira_get_issue\").',\n },\n args: {\n type: 'object',\n description:\n \"Arguments object matching the chosen tool's input schema (as shown by alfe_mcp_list_tools). Pass {} for tools that take no arguments.\",\n additionalProperties: true,\n },\n },\n required: ['tool'],\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n const p = (params ?? {}) as { tool?: unknown; args?: Record<string, unknown> };\n const tool = p.tool;\n if (typeof tool !== 'string' || !tool) {\n return errorResult(\n 'alfe_mcp_call requires a `tool` name — call alfe_mcp_list_tools first to see the available tool names.',\n );\n }\n const args = p.args ?? {};\n const response = await callIpc<McpToolCallResult>('mcp.call_tool', { name: tool, args });\n if (!response.ok || !response.payload) {\n (moduleLog ?? api.logger).warn('[mcp-bundler] alfe_mcp_call failed', {\n tool,\n err: response.error?.message ?? 'unknown error',\n });\n return errorResult(\n response.error?.message\n ? `mcp ${tool}: ${response.error.message}`\n : `mcp ${tool}: call failed (is the tool name exact? call alfe_mcp_list_tools to confirm)`,\n );\n }\n const result = response.payload;\n return {\n content: result.content,\n details: { isError: result.isError ?? false },\n };\n },\n });\n}\n\n/**\n * Pull the latest tool catalog from the daemon-hosted bundler into\n * `cachedDescriptors` + the disk cache. Driven by the periodic timer\n * and the `onConnect` hook so the disk-cache fallback that\n * `alfe_mcp_list_tools` reads stays warm even for tool-discovery loads\n * that never execute a tool. Coalesces concurrent calls so a burst only\n * 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 // Guard against a malformed `ok` payload (no `tools` array) —\n // treat it like an empty result rather than crashing the\n // fire-and-forget refresh on `undefined.length`.\n const fresh = Array.isArray(response.payload.tools) ? 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; `alfe_mcp_list_tools` returns whatever is in\n * `cachedDescriptors` when it's called with a closed socket (loaded\n * from disk in `activate()`, then refreshed on connect, then\n * periodically). A first-turn pre-warm is no longer needed — the disk\n * cache covers the cold window, and any new tools discovered during the\n * in-flight `onConnect` refresh land in `cachedDescriptors` shortly\n * after.\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(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 // `moduleApi` is captured at activate(); it's always set by the\n // time the socket connects (activate ran first). Guard defensively\n // for the test harness / unexpected ordering.\n if (moduleApi) void refreshCachedTools(moduleApi);\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() && moduleApi) {\n void refreshCachedTools(moduleApi);\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 // FIVE fixed names — declared once, forever. This is the whole\n // point of the v0.0.16 router redesign.\n //\n // OpenClaw's `contracts.tools` matcher (`loader.js#registerTool`,\n // `tools.js#resolvePluginTools`) is a STRICT LITERAL `Set.has()` —\n // no globs, and any tool a plugin tries to register that isn't\n // listed here is rejected as \"undeclared\". That is a fundamental\n // mismatch for a DYNAMIC MCP aggregator: the bundler's real toolset\n // is whatever the daemon currently proxies, and it changes at\n // runtime (Atlassian alone goes 3 → 76 tools the moment a cloudId\n // is selected). Enumerating every possible tool here is impossible,\n // and regenerating the manifest from the live catalog fights a\n // one-reload lag.\n //\n // So we DON'T register the proxied tools natively at all. The two\n // router tools below — `alfe_mcp_list_tools` (discover) and\n // `alfe_mcp_call` (invoke by name) — are a stable, static surface\n // through which the agent reaches the entire live MCP catalog. New\n // tools are usable the instant the daemon sees them, with no\n // manifest change and no reload. (Same `use_mcp_tool` pattern as\n // Cline / Roo-Code; same \"dynamic discover + execute\" Anthropic\n // recommends for MCP at scale.)\n //\n // These five names are the complete, permanent contract. Adding a\n // new integration NEVER requires touching this list again.\n tools: [\n 'alfe_mcp_list',\n 'alfe_mcp_add',\n 'alfe_mcp_remove',\n 'alfe_mcp_list_tools',\n 'alfe_mcp_call',\n ],\n },\n\n activate(api: OpenClawPluginApi) {\n const log = api.logger;\n\n // NOTE: no global \"already activated\" early-return. OpenClaw's\n // cached-descriptor proxy re-resolves the live registry at tool-call\n // time with `activate: false` + `onlyPluginIds: [bundler]`; if our\n // activate() early-returned on that re-load, the rebuilt registry\n // contained the plugin record but ZERO tool entries, and OpenClaw's\n // `registryHasScopedPluginTools` (pluginId-only, name-blind) accepted\n // it — so the by-name lookup threw \"plugin tool runtime unavailable\".\n // Re-registering the five tools on every activate() is idempotent\n // (OpenClaw dedups by name per registry); the IPC client + timer are\n // separately idempotent via `if (ipcClient) return`.\n if (api.pluginConfig?.disabled) {\n log.info('[mcp-bundler] disabled via plugin config — no-op');\n return;\n }\n\n // Informational only (v0.0.16): the router tools are always\n // registered regardless of native mcp.servers — unlike the old\n // dynamic factory, the router doesn't defer. We still detect &\n // log native mcp.servers because on claude-cli/codex backends\n // those tools ALSO surface via native bundleMcp, so an operator\n // seeing both the native tools and the alfe_mcp_* router tools\n // isn't a bug.\n const hasNativeServers = hasNativeOpenclawMcpServers(resolveLiveConfig(api));\n if (hasNativeServers) {\n log.info(\n '[mcp-bundler] openclaw.json#mcp.servers has entries — those surface natively on claude-cli/codex; the alfe_mcp_* router tools remain available on every backend',\n );\n }\n\n // ── Register the FIVE static tools in activate() ────────────\n //\n // v0.0.16 router redesign: the bundler no longer registers the\n // proxied MCP tools natively (that hit OpenClaw's static\n // `contracts.tools` allowlist — a dynamic aggregator can't\n // enumerate its tools at build time). Instead it registers five\n // fixed tools (3 management + `alfe_mcp_list_tools` +\n // `alfe_mcp_call`), and the agent reaches the entire live MCP\n // catalog through the two router tools. See the `contracts.tools`\n // comment above for the full rationale.\n //\n // No `api.registerService` (that combo drops tools — QA Tester\n // 2026-06-10), no dynamic factory (that needs per-tool declaration\n // — the whole problem this redesign removes). The IPC client is\n // module-scoped and lazily constructed on the first tool call (see\n // `ensureIpcClient`), which is what makes execution work in the\n // tool-discovery runtime that serves the agent's turns.\n\n // Seed the descriptor cache from disk so `alfe_mcp_list_tools` has\n // a last-known fallback if the daemon is momentarily unreachable.\n // Refreshed live on every `alfe_mcp_list_tools` call and by the\n // periodic refresh.\n cachedDescriptors = loadCachedDescriptorsFromDisk(log);\n\n // The five static tools (management + router). Always registered,\n // regardless of the native-bundleMcp escape hatch — the router is\n // a leaner, always-available access path and doesn't conflict with\n // native tools when claude-cli has them.\n registerAgentMcpManagementTools(api);\n\n // ── Live IPC client ────────────────────────────────────────\n //\n // Two paths to a live socket, both landing in the SAME module-\n // scoped `ipcClient`:\n //\n // 1. EAGER (full agent-runtime mode): open the socket now so the\n // first turn is already warm. Embedded one-shot runs benefit.\n // 2. LAZY (any other mode — crucially `tool-discovery`, which is\n // what the gateway daemon's long-lived runtime uses): the\n // socket opens on the FIRST tool execution via `callIpc` →\n // `ensureIpcClient`. This is the v0.0.16 fix — pre-v0.0.16\n // the runtime that serves the agent's turns never opened a\n // socket at all because it isn't `full` mode.\n //\n // Pure tool-discovery loads that enumerate tools but never execute\n // anything still never open a socket (the periodic refresh is a\n // no-op until something constructs the client). The discovery\n // invariant is preserved.\n //\n // Capture `api` + `log` module-scoped so the lazy path can\n // construct without threading args through every call site.\n moduleApi = api;\n moduleLog = log;\n\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'}, nativeMcpServers=${String(hasNativeServers)}, cachedDescriptors=${String(cachedDescriptors.length)})`,\n );\n\n // Advertise the live MCP catalog to the agent at session start so it\n // knows these tools exist without having to think to call\n // `alfe_mcp_list_tools` first. Reads the last-known `cachedDescriptors`\n // (no extra IPC), fires once per session, and fails open — never\n // throw out of a lifecycle hook. `activate()` can run more than once,\n // so guard against registering the hook twice (which would inject the\n // context block N times per session).\n if (typeof api.on === 'function' && !beforeAgentStartHookRegistered) {\n beforeAgentStartHookRegistered = true;\n api.on(\n 'before_agent_start',\n () => {\n try {\n const context = buildAvailableToolsContext(cachedDescriptors);\n return context ? { prependContext: context } : undefined;\n } catch (err) {\n log.warn(\n `[mcp-bundler] before_agent_start context injection failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return undefined;\n }\n },\n { priority: 10 },\n );\n }\n\n if (shouldStartIpcClient(api)) {\n startIpcClient(log);\n }\n },\n\n deactivate(api: OpenClawPluginApi) {\n api.logger.debug('[mcp-bundler] deactivating');\n stopIpcClient();\n },\n};\n\nexport { type IpcResponse };\nexport default plugin;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-mcp-bundler",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
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",