@alfe.ai/openclaw-mcp-bundler 0.0.22 → 0.0.24

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.
@@ -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;;IC6EF,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;;;;;;cAq3B1D;;;;;;;;gBAwCU;kBAsGE"}
1
+ {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;IC6EF,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;;;;;;cAy8B1D;;;;;;;;gBAwCU;kBAsGE"}
@@ -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;;IC6EF,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;;;;;;cAq3B1D;;;;;;;;gBAwCU;kBAsGE"}
1
+ {"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;IC6EF,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;;;;;;cAy8B1D;;;;;;;;gBAwCU;kBAsGE"}
package/dist/plugin2.cjs CHANGED
@@ -535,10 +535,21 @@ function registerAgentMcpManagementTools(api) {
535
535
  async execute() {
536
536
  const response = await callIpc("mcp.list_servers", {});
537
537
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_list failed");
538
+ const summary = (response.payload?.servers ?? []).map((s) => {
539
+ const row = {
540
+ id: s.id,
541
+ owner: s.entry?.owner ?? null,
542
+ transport: s.entry?.transport ?? null,
543
+ connected: s.status?.connected ?? null,
544
+ tools: s.status?.toolCount ?? null
545
+ };
546
+ if (s.status?.lastError) row.lastError = s.status.lastError;
547
+ return row;
548
+ });
538
549
  return {
539
550
  content: [{
540
551
  type: "text",
541
- text: JSON.stringify(response.payload?.servers ?? [], null, 2)
552
+ text: JSON.stringify(summary, null, 2)
542
553
  }],
543
554
  details: { isError: false }
544
555
  };
@@ -594,12 +605,18 @@ function registerAgentMcpManagementTools(api) {
594
605
  async execute(_toolCallId, params) {
595
606
  const response = await callIpc("mcp.add_server", params ?? {});
596
607
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_add failed");
608
+ const pl = response.payload;
609
+ const id = pl?.id ?? "?";
610
+ let text;
611
+ if (pl?.error) text = `Registered MCP server "${id}", but it FAILED to connect: ${pl.error}\nFix the command/url/credentials/headers and re-add it, or call alfe_mcp_list to check its status.`;
612
+ else if (pl?.connected) text = `Registered MCP server "${id}" — connected, ${String(pl.toolCount ?? 0)} tool(s) now available. Call alfe_mcp_list_tools to see them.`;
613
+ else text = `Registered MCP server "${id}". It may still be connecting — call alfe_mcp_list_tools to confirm its tools, or alfe_mcp_list to check its connection status.`;
597
614
  return {
598
615
  content: [{
599
616
  type: "text",
600
- text: `Registered MCP server "${response.payload?.id ?? "?"}". Tools will appear on the next turn.`
617
+ text
601
618
  }],
602
- details: { isError: false }
619
+ details: { isError: Boolean(pl?.error) }
603
620
  };
604
621
  }
605
622
  });
@@ -661,7 +678,7 @@ function registerAgentMcpManagementTools(api) {
661
678
  if (out.length === 0) return {
662
679
  content: [{
663
680
  type: "text",
664
- text: serverFilter ? `No MCP tools available for server "${serverFilter}". Call alfe_mcp_list to see registered servers.` : "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."
681
+ text: await describeEmptyToolList(serverFilter)
665
682
  }],
666
683
  details: { isError: false }
667
684
  };
@@ -731,6 +748,36 @@ async function listServersForFingerprint() {
731
748
  return response.payload.servers;
732
749
  }
733
750
  /**
751
+ * Build an honest message for an empty `alfe_mcp_list_tools` result. The old
752
+ * blanket "try again in a moment" hid the difference between three very
753
+ * different states, so a genuinely-broken server looked identical to one still
754
+ * warming. Cross-reference the live server roster + status to say which it is:
755
+ * none registered, still connecting, or failed with a concrete error. Falls
756
+ * back to the generic message if the roster is unavailable.
757
+ */
758
+ async function describeEmptyToolList(serverFilter) {
759
+ const suffix = " Call alfe_mcp_list to see registered servers and their status.";
760
+ let servers;
761
+ try {
762
+ servers = await listServersForFingerprint();
763
+ } catch {
764
+ servers = void 0;
765
+ }
766
+ if (!servers) return serverFilter ? `No MCP tools available for server "${serverFilter}" (could not reach the daemon).${suffix}` : `No MCP tools available yet (could not reach the daemon).${suffix}`;
767
+ const scoped = serverFilter ? servers.filter((s) => s.id === serverFilter) : servers;
768
+ if (scoped.length === 0) return serverFilter ? `No MCP server "${serverFilter}" is registered. Use alfe_mcp_add to register one.` : "No MCP servers are registered. Use alfe_mcp_add to register one.";
769
+ const failed = scoped.filter((s) => s.status && !s.status.connected && s.status.lastError);
770
+ const connecting = scoped.filter((s) => s.status && !s.status.connected && !s.status.lastError);
771
+ const parts = [];
772
+ for (const s of failed) parts.push(`"${s.id}" failed to connect: ${String(s.status?.lastError)}`);
773
+ if (connecting.length > 0) {
774
+ const names = connecting.map((s) => `"${s.id}"`).join(", ");
775
+ parts.push(`${names} still connecting — try again in a moment`);
776
+ }
777
+ if (parts.length > 0) return `${parts.join(". ")}.${suffix}`;
778
+ return serverFilter ? `Server "${serverFilter}" is registered but advertised no tools.${suffix}` : `No MCP tools available yet — registered servers advertised none.${suffix}`;
779
+ }
780
+ /**
734
781
  * Pull the latest tool catalog from the daemon-hosted bundler into
735
782
  * `cachedDescriptors` + the disk cache. Driven by the periodic timer
736
783
  * and the `onConnect` hook so the disk-cache fallback that
package/dist/plugin2.js CHANGED
@@ -535,10 +535,21 @@ function registerAgentMcpManagementTools(api) {
535
535
  async execute() {
536
536
  const response = await callIpc("mcp.list_servers", {});
537
537
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_list failed");
538
+ const summary = (response.payload?.servers ?? []).map((s) => {
539
+ const row = {
540
+ id: s.id,
541
+ owner: s.entry?.owner ?? null,
542
+ transport: s.entry?.transport ?? null,
543
+ connected: s.status?.connected ?? null,
544
+ tools: s.status?.toolCount ?? null
545
+ };
546
+ if (s.status?.lastError) row.lastError = s.status.lastError;
547
+ return row;
548
+ });
538
549
  return {
539
550
  content: [{
540
551
  type: "text",
541
- text: JSON.stringify(response.payload?.servers ?? [], null, 2)
552
+ text: JSON.stringify(summary, null, 2)
542
553
  }],
543
554
  details: { isError: false }
544
555
  };
@@ -594,12 +605,18 @@ function registerAgentMcpManagementTools(api) {
594
605
  async execute(_toolCallId, params) {
595
606
  const response = await callIpc("mcp.add_server", params ?? {});
596
607
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_add failed");
608
+ const pl = response.payload;
609
+ const id = pl?.id ?? "?";
610
+ let text;
611
+ if (pl?.error) text = `Registered MCP server "${id}", but it FAILED to connect: ${pl.error}\nFix the command/url/credentials/headers and re-add it, or call alfe_mcp_list to check its status.`;
612
+ else if (pl?.connected) text = `Registered MCP server "${id}" — connected, ${String(pl.toolCount ?? 0)} tool(s) now available. Call alfe_mcp_list_tools to see them.`;
613
+ else text = `Registered MCP server "${id}". It may still be connecting — call alfe_mcp_list_tools to confirm its tools, or alfe_mcp_list to check its connection status.`;
597
614
  return {
598
615
  content: [{
599
616
  type: "text",
600
- text: `Registered MCP server "${response.payload?.id ?? "?"}". Tools will appear on the next turn.`
617
+ text
601
618
  }],
602
- details: { isError: false }
619
+ details: { isError: Boolean(pl?.error) }
603
620
  };
604
621
  }
605
622
  });
@@ -661,7 +678,7 @@ function registerAgentMcpManagementTools(api) {
661
678
  if (out.length === 0) return {
662
679
  content: [{
663
680
  type: "text",
664
- text: serverFilter ? `No MCP tools available for server "${serverFilter}". Call alfe_mcp_list to see registered servers.` : "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."
681
+ text: await describeEmptyToolList(serverFilter)
665
682
  }],
666
683
  details: { isError: false }
667
684
  };
@@ -731,6 +748,36 @@ async function listServersForFingerprint() {
731
748
  return response.payload.servers;
732
749
  }
733
750
  /**
751
+ * Build an honest message for an empty `alfe_mcp_list_tools` result. The old
752
+ * blanket "try again in a moment" hid the difference between three very
753
+ * different states, so a genuinely-broken server looked identical to one still
754
+ * warming. Cross-reference the live server roster + status to say which it is:
755
+ * none registered, still connecting, or failed with a concrete error. Falls
756
+ * back to the generic message if the roster is unavailable.
757
+ */
758
+ async function describeEmptyToolList(serverFilter) {
759
+ const suffix = " Call alfe_mcp_list to see registered servers and their status.";
760
+ let servers;
761
+ try {
762
+ servers = await listServersForFingerprint();
763
+ } catch {
764
+ servers = void 0;
765
+ }
766
+ if (!servers) return serverFilter ? `No MCP tools available for server "${serverFilter}" (could not reach the daemon).${suffix}` : `No MCP tools available yet (could not reach the daemon).${suffix}`;
767
+ const scoped = serverFilter ? servers.filter((s) => s.id === serverFilter) : servers;
768
+ if (scoped.length === 0) return serverFilter ? `No MCP server "${serverFilter}" is registered. Use alfe_mcp_add to register one.` : "No MCP servers are registered. Use alfe_mcp_add to register one.";
769
+ const failed = scoped.filter((s) => s.status && !s.status.connected && s.status.lastError);
770
+ const connecting = scoped.filter((s) => s.status && !s.status.connected && !s.status.lastError);
771
+ const parts = [];
772
+ for (const s of failed) parts.push(`"${s.id}" failed to connect: ${String(s.status?.lastError)}`);
773
+ if (connecting.length > 0) {
774
+ const names = connecting.map((s) => `"${s.id}"`).join(", ");
775
+ parts.push(`${names} still connecting — try again in a moment`);
776
+ }
777
+ if (parts.length > 0) return `${parts.join(". ")}.${suffix}`;
778
+ return serverFilter ? `Server "${serverFilter}" is registered but advertised no tools.${suffix}` : `No MCP tools available yet — registered servers advertised none.${suffix}`;
779
+ }
780
+ /**
734
781
  * Pull the latest tool catalog from the daemon-hosted bundler into
735
782
  * `cachedDescriptors` + the disk cache. Driven by the periodic timer
736
783
  * and the `onConnect` hook so the disk-cache fallback that
@@ -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// Cache envelope schema version. v1 was a bare `McpToolDescriptor[]`\n// keyed ONLY by each descriptor's `server` field — with no notion of the\n// server's launch command/version. That let a stale slice survive a\n// version bump: when `@alfe.ai/openclaw-myob@0.2.0` crashed on startup\n// (zod bug) the bundler discovered 0 myob tools and the cache pinned an\n// empty myob slice; the integration upgrade to 0.2.3 (30 tools) changed\n// the launch command but NOTHING invalidated the cache, so the agent\n// kept reporting \"MYOB not installed\" until the file was deleted by hand.\n// v2 wraps the catalogue per-server with a fingerprint of the launch\n// command+args+env+version, so a version/command change is a cache miss\n// → forced re-discovery. See `CacheEnvelopeV2` + `mergeServerSlices`.\nconst CACHE_SCHEMA_VERSION = 2;\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// ── Cache envelope (v2) ─────────────────────────────────────────\n//\n// One slice per MCP server. `fingerprint` captures the server's launch\n// identity (command + args + env + url + transport + version pin) so a\n// version/command change is a cache MISS — the stale slice is dropped\n// and the daemon's fresh discovery wins. `descriptors` is that server's\n// tool set as of the last successful, NON-EMPTY discovery. We never\n// persist an empty slice as authoritative (a crashed-on-startup child\n// returns 0 tools transiently — pinning that empty set is exactly the\n// myob 0.2.0 bug), so the absence of a slice means \"not yet discovered\"\n// rather than \"known to have no tools\".\ninterface CacheServerSlice {\n fingerprint: string;\n descriptors: McpToolDescriptor[];\n}\ninterface CacheEnvelopeV2 {\n schema: number;\n servers: Record<string, CacheServerSlice>;\n}\n\n// ── Plugin state ───────────────────────────────────────────────\nlet ipcClient: McpIpcClient | undefined;\n// Module-scoped catalogue — the last-known MCP tool list, flattened\n// across server slices for the router. JSON-serialisable members\n// round-trip through the disk cache. The router's `alfe_mcp_list_tools`\n// returns a live `mcp.list_tools` result when the daemon is reachable\n// and falls back to this when it isn't.\nlet cachedDescriptors: McpToolDescriptor[] = [];\n// Per-server slices keyed by server id, each carrying the launch\n// fingerprint that gates staleness. This is the source of truth the disk\n// cache serialises; `cachedDescriptors` is the flattened view derived\n// from it via `flattenSlices`.\nlet cachedSlices: Record<string, CacheServerSlice> = {};\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\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 * Shape of one server entry as returned by the daemon's\n * `mcp.list_servers` IPC method (`handleMcpListServers` in\n * `packages/gateway/src/daemon.ts`). The `entry` is the bundler store's\n * `StoredServerEntry` — it carries the launch identity (command/args/\n * env/cwd for stdio, url/transport/headers for remote) plus the package\n * `version` pin. We only read the launch-identity fields here.\n */\ninterface ListedServer {\n id: string;\n entry?: {\n command?: unknown;\n args?: unknown;\n env?: unknown;\n cwd?: unknown;\n url?: unknown;\n transport?: unknown;\n headers?: unknown;\n version?: unknown;\n };\n}\n\n/**\n * Compute a stable fingerprint of a server's LAUNCH IDENTITY from its\n * `mcp.list_servers` entry. Two launches with the same fingerprint spawn\n * the same child and therefore advertise the same tools; a different\n * fingerprint (a version bump in the args, e.g.\n * `@alfe.ai/openclaw-myob@0.2.0` → `@0.2.3`, a changed command, or a\n * rotated env) MUST invalidate the cached slice. The version pin lives\n * inside `args` for npx-launched servers, so hashing args alone catches\n * the myob case; we include `version` + the rest defensively so a store\n * that records the version out-of-band (or a remote-URL change) is also\n * covered. Key order is fixed so the serialisation is deterministic.\n */\nfunction fingerprintServer(server: ListedServer): string {\n const e = server.entry ?? {};\n return JSON.stringify({\n command: e.command ?? null,\n args: e.args ?? null,\n env: e.env ?? null,\n cwd: e.cwd ?? null,\n url: e.url ?? null,\n transport: e.transport ?? null,\n headers: e.headers ?? null,\n version: e.version ?? null,\n });\n}\n\n/** Flatten per-server slices into the single descriptor list the router serves. */\nfunction flattenSlices(slices: Record<string, CacheServerSlice>): McpToolDescriptor[] {\n const out: McpToolDescriptor[] = [];\n for (const slice of Object.values(slices)) out.push(...slice.descriptors);\n return out;\n}\n\n/**\n * Load the last-known tool catalogue from disk into the per-server slice\n * map. Used at `activate()` time so the agent's tool list contains real\n * tools even in tool-discovery mode (where the IPC client never opens).\n *\n * Accepts BOTH formats:\n * - v2 envelope `{ schema, servers: { id: { fingerprint, descriptors } } }`\n * — the fingerprint-aware format this plugin now writes.\n * - v1 bare `McpToolDescriptor[]` — caches written by ≤ v0.0.16. These\n * have no fingerprints, so we bucket them by each descriptor's\n * `server` field with an EMPTY fingerprint. An empty fingerprint\n * never matches a real server fingerprint, so the first live refresh\n * treats every legacy slice as stale → forced re-discovery. That's\n * the desired migration: a box upgrading into the fix re-validates\n * its whole catalogue against live server launch identities once.\n *\n * Silently falls back to an empty map on any read/parse failure so a\n * first-run agent (no cache file yet) still loads cleanly.\n */\nfunction loadCachedSlicesFromDisk(logger: Logger): Record<string, CacheServerSlice> {\n try {\n const raw = readFileSync(CACHE_FILE, 'utf-8');\n const parsed = JSON.parse(raw) as unknown;\n const rawServers = parsedEnvelopeServers(parsed);\n if (rawServers) {\n const out: Record<string, CacheServerSlice> = {};\n for (const [id, value] of Object.entries(rawServers)) {\n if (typeof value !== 'object' || value === null) continue;\n const slice = value as { fingerprint?: unknown; descriptors?: unknown };\n const fingerprint = typeof slice.fingerprint === 'string' ? slice.fingerprint : '';\n const descriptors = Array.isArray(slice.descriptors)\n ? slice.descriptors.filter(isValidDescriptor)\n : [];\n out[id] = { fingerprint, descriptors };\n }\n return out;\n }\n if (Array.isArray(parsed)) {\n // Legacy v1 flat array — migrate by bucketing on `server`, with an\n // empty fingerprint so the first live refresh re-validates each.\n const valid = parsed.filter(isValidDescriptor);\n const out: Record<string, CacheServerSlice> = {};\n for (const d of valid) {\n const slice = (out[d.server] ??= { fingerprint: '', descriptors: [] });\n slice.descriptors.push(d);\n }\n return out;\n }\n return {};\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 * If `value` is a v2 cache envelope, return its raw (untyped) `servers`\n * map for field-by-field validation in the loader; otherwise `null`.\n * Returns `Record<string, unknown>` rather than `Record<string,\n * CacheServerSlice>` on purpose — the contents are unvalidated JSON, so\n * the loader must check each slice's fields at runtime.\n */\nfunction parsedEnvelopeServers(value: unknown): Record<string, unknown> | null {\n if (typeof value !== 'object' || value === null) return null;\n const v = value as Record<string, unknown>;\n if (typeof v.schema !== 'number') return null;\n if (typeof v.servers !== 'object' || v.servers === null) return null;\n return v.servers as Record<string, unknown>;\n}\n\n/**\n * Persist the current per-server slice map to disk as a v2 envelope so\n * the next tool-discovery load sees the tools AND their launch\n * fingerprints. Best-effort — disk failures are logged at `warn` and\n * otherwise swallowed.\n */\nfunction persistCachedSlicesToDisk(\n slices: Record<string, CacheServerSlice>,\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 envelope: CacheEnvelopeV2 = { schema: CACHE_SCHEMA_VERSION, servers: slices };\n const tmp = `${CACHE_FILE}.${randomUUID()}.tmp`;\n try {\n mkdirSync(dirname(CACHE_FILE), { recursive: true });\n writeFileSync(tmp, JSON.stringify(envelope));\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 * Merge a fresh live discovery into the cached per-server slices, gated\n * by each server's launch fingerprint. This is the heart of the\n * version-change + crashed-then-fixed fix.\n *\n * For every server in the LIVE roster (`mcp.list_servers`):\n * - Compute its launch fingerprint.\n * - Collect the freshly-discovered descriptors for that server.\n * - If the fingerprint CHANGED vs the cached slice → the cached slice\n * is stale (a different version/command). DROP the old descriptors\n * and adopt the fresh ones. If the fresh set is empty (the new\n * version hasn't finished spawning, or crashed), cache NO slice for\n * it — we'd rather show \"not discovered yet\" than serve the old\n * version's stale tools OR pin an empty set. The next refresh will\n * pick up the real tools once the child is up. THIS is what unsticks\n * the myob 0.2.0→0.2.3 bug: the version-pinned args change the\n * fingerprint, so the empty 0.2.0 slice can never survive the bump.\n * - If the fingerprint is UNCHANGED:\n * · fresh has tools → refresh the slice with them.\n * · fresh is empty → KEEP the prior good slice (transient daemon\n * cold-start / idle-reaped child returning [] for a moment must\n * not wipe a known-good catalogue). This is the per-server\n * generalisation of the old whole-catalogue \"don't shrink to\n * empty\" guard.\n *\n * Servers NOT in the live roster are dropped entirely — they've been\n * removed from the bundler store, so their tools should disappear.\n *\n * When the live roster is UNAVAILABLE (server list call failed), pass\n * `servers = undefined`: we can't fingerprint, so we conservatively keep\n * the existing slices untouched rather than risk corrupting them.\n */\nfunction mergeServerSlices(\n // Value typed `| undefined` so an index lookup is honestly nullable —\n // a server id absent from `prior` (a brand-new server) yields\n // undefined, which the fingerprint/keep-prior logic below relies on.\n prior: Record<string, CacheServerSlice | undefined>,\n freshDescriptors: McpToolDescriptor[],\n servers: ListedServer[] | undefined,\n): Record<string, CacheServerSlice> {\n if (!servers) return prior as Record<string, CacheServerSlice>;\n\n // Bucket the fresh descriptors by their originating server.\n const freshByServer = new Map<string, McpToolDescriptor[]>();\n for (const d of freshDescriptors) {\n const bucket = freshByServer.get(d.server);\n if (bucket) bucket.push(d);\n else freshByServer.set(d.server, [d]);\n }\n\n const next: Record<string, CacheServerSlice> = {};\n for (const server of servers) {\n const fingerprint = fingerprintServer(server);\n const priorSlice = prior[server.id];\n const fresh = freshByServer.get(server.id) ?? [];\n const fingerprintChanged = priorSlice?.fingerprint !== fingerprint;\n\n if (fingerprintChanged) {\n // Stale or new server: only cache a slice once real tools arrive.\n // Never carry the old version's descriptors or pin an empty set.\n if (fresh.length > 0) next[server.id] = { fingerprint, descriptors: fresh };\n continue;\n }\n // Same launch identity. `priorSlice` is necessarily defined here: an\n // unchanged fingerprint (`priorSlice?.fingerprint === fingerprint`)\n // cannot come from an absent slice (that would be `undefined !==\n // fingerprint` → changed). Refresh on a non-empty discovery;\n // otherwise hold the prior good slice (transient empty result).\n next[server.id] = fresh.length > 0 ? { fingerprint, descriptors: fresh } : priorSlice;\n }\n return next;\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 \"Discover and inspect ALL external integration tools available to this agent — built-in integrations (Atlassian/Jira, Confluence, Notion, GitHub, etc.) AND user-added custom connections (rostering, accounting, CRM, or any other domain API the user has connected). Returns each tool's exact name, description, and input schema. ALWAYS call this BEFORE telling the user that a capability or data source is unavailable — the integration you need may already be connected here and is invisible until you list it. After listing, invoke a tool 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) {\n // The list_tools CALL succeeded (even if it returned 0 tools).\n // Run the fingerprint-gated merge against the live server roster\n // — the SAME path as `refreshCachedTools`. Doing this even on an\n // empty result is load-bearing: a server whose launch fingerprint\n // changed (a version bump) must have its STALE slice dropped here\n // too, not just on the periodic refresh. Skipping the merge on\n // empty results was the gap that let a re-hydrated stale slice\n // (e.g. myob@0.2.0's 0-tool entry, or an old version's tools)\n // survive into the listing when the new version hadn't yet\n // discovered its tools. `mergeServerSlices` still HOLDS a prior\n // good slice when the SAME server transiently reports 0 tools, so\n // a momentary daemon hiccup doesn't wipe a known-good catalogue.\n const fresh = Array.isArray(response.payload.tools) ? response.payload.tools : [];\n const servers = await listServersForFingerprint();\n cachedSlices = mergeServerSlices(cachedSlices, fresh, servers);\n cachedDescriptors = flattenSlices(cachedSlices);\n descriptors = cachedDescriptors;\n if (moduleLog) persistCachedSlicesToDisk(cachedSlices, moduleLog);\n } else {\n // IPC call failed outright (NOT_CONNECTED / TIMEOUT) — daemon\n // unreachable. Serve the last-known cache untouched.\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 live server roster from the daemon so the merge can\n * fingerprint each server's launch identity. Returns `undefined` (NOT\n * `[]`) when the call fails — the caller MUST treat that as \"roster\n * unavailable, keep prior slices\" rather than \"no servers exist, drop\n * everything\". An empty array is a legitimate \"store has no servers\"\n * answer and DOES clear the cache.\n */\nasync function listServersForFingerprint(): Promise<ListedServer[] | undefined> {\n const response = await callIpc<{ servers: ListedServer[] }>('mcp.list_servers', {});\n if (!response.ok || !response.payload || !Array.isArray(response.payload.servers)) {\n return undefined;\n }\n return response.payload.servers;\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 // Pull the live server roster so we can fingerprint each server's\n // launch identity. The merge is fingerprint-gated: a version/\n // command change (myob 0.2.0 → 0.2.3) drops the stale slice, and a\n // server that transiently reports 0 tools (cold start, idle-reaped\n // child) keeps its prior good slice rather than being wiped. This\n // is the per-server replacement for the old whole-catalogue\n // \"don't shrink to empty\" guard.\n const servers = await listServersForFingerprint();\n cachedSlices = mergeServerSlices(cachedSlices, fresh, servers);\n cachedDescriptors = flattenSlices(cachedSlices);\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 persistCachedSlicesToDisk(cachedSlices, 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 // Loaded as fingerprint-tagged per-server slices (v2 envelope; a\n // legacy v1 flat array migrates in with empty fingerprints so the\n // first live refresh re-validates it). Refreshed live on every\n // `alfe_mcp_list_tools` call and by the periodic refresh.\n cachedSlices = loadCachedSlicesFromDisk(log);\n cachedDescriptors = flattenSlices(cachedSlices);\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 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// Cache envelope schema version. v1 was a bare `McpToolDescriptor[]`\n// keyed ONLY by each descriptor's `server` field — with no notion of the\n// server's launch command/version. That let a stale slice survive a\n// version bump: when `@alfe.ai/openclaw-myob@0.2.0` crashed on startup\n// (zod bug) the bundler discovered 0 myob tools and the cache pinned an\n// empty myob slice; the integration upgrade to 0.2.3 (30 tools) changed\n// the launch command but NOTHING invalidated the cache, so the agent\n// kept reporting \"MYOB not installed\" until the file was deleted by hand.\n// v2 wraps the catalogue per-server with a fingerprint of the launch\n// command+args+env+version, so a version/command change is a cache miss\n// → forced re-discovery. See `CacheEnvelopeV2` + `mergeServerSlices`.\nconst CACHE_SCHEMA_VERSION = 2;\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// ── Cache envelope (v2) ─────────────────────────────────────────\n//\n// One slice per MCP server. `fingerprint` captures the server's launch\n// identity (command + args + env + url + transport + version pin) so a\n// version/command change is a cache MISS — the stale slice is dropped\n// and the daemon's fresh discovery wins. `descriptors` is that server's\n// tool set as of the last successful, NON-EMPTY discovery. We never\n// persist an empty slice as authoritative (a crashed-on-startup child\n// returns 0 tools transiently — pinning that empty set is exactly the\n// myob 0.2.0 bug), so the absence of a slice means \"not yet discovered\"\n// rather than \"known to have no tools\".\ninterface CacheServerSlice {\n fingerprint: string;\n descriptors: McpToolDescriptor[];\n}\ninterface CacheEnvelopeV2 {\n schema: number;\n servers: Record<string, CacheServerSlice>;\n}\n\n// ── Plugin state ───────────────────────────────────────────────\nlet ipcClient: McpIpcClient | undefined;\n// Module-scoped catalogue — the last-known MCP tool list, flattened\n// across server slices for the router. JSON-serialisable members\n// round-trip through the disk cache. The router's `alfe_mcp_list_tools`\n// returns a live `mcp.list_tools` result when the daemon is reachable\n// and falls back to this when it isn't.\nlet cachedDescriptors: McpToolDescriptor[] = [];\n// Per-server slices keyed by server id, each carrying the launch\n// fingerprint that gates staleness. This is the source of truth the disk\n// cache serialises; `cachedDescriptors` is the flattened view derived\n// from it via `flattenSlices`.\nlet cachedSlices: Record<string, CacheServerSlice> = {};\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\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 * Shape of one server entry as returned by the daemon's\n * `mcp.list_servers` IPC method (`handleMcpListServers` in\n * `packages/gateway/src/daemon.ts`). The `entry` is the bundler store's\n * `StoredServerEntry` — it carries the launch identity (command/args/\n * env/cwd for stdio, url/transport/headers for remote) plus the package\n * `version` pin. We only read the launch-identity fields here.\n */\ninterface ListedServer {\n id: string;\n entry?: {\n owner?: unknown;\n command?: unknown;\n args?: unknown;\n env?: unknown;\n cwd?: unknown;\n url?: unknown;\n transport?: unknown;\n headers?: unknown;\n version?: unknown;\n };\n /**\n * Live connection status, added by `handleMcpListServers` in the daemon.\n * Absent when talking to an older daemon — callers must treat it as\n * optional and fall back to the config-only view.\n */\n status?: {\n connected?: boolean;\n toolCount?: number;\n consecutiveFailures?: number;\n lastError?: string;\n };\n}\n\n/**\n * Compute a stable fingerprint of a server's LAUNCH IDENTITY from its\n * `mcp.list_servers` entry. Two launches with the same fingerprint spawn\n * the same child and therefore advertise the same tools; a different\n * fingerprint (a version bump in the args, e.g.\n * `@alfe.ai/openclaw-myob@0.2.0` → `@0.2.3`, a changed command, or a\n * rotated env) MUST invalidate the cached slice. The version pin lives\n * inside `args` for npx-launched servers, so hashing args alone catches\n * the myob case; we include `version` + the rest defensively so a store\n * that records the version out-of-band (or a remote-URL change) is also\n * covered. Key order is fixed so the serialisation is deterministic.\n */\nfunction fingerprintServer(server: ListedServer): string {\n const e = server.entry ?? {};\n return JSON.stringify({\n command: e.command ?? null,\n args: e.args ?? null,\n env: e.env ?? null,\n cwd: e.cwd ?? null,\n url: e.url ?? null,\n transport: e.transport ?? null,\n headers: e.headers ?? null,\n version: e.version ?? null,\n });\n}\n\n/** Flatten per-server slices into the single descriptor list the router serves. */\nfunction flattenSlices(slices: Record<string, CacheServerSlice>): McpToolDescriptor[] {\n const out: McpToolDescriptor[] = [];\n for (const slice of Object.values(slices)) out.push(...slice.descriptors);\n return out;\n}\n\n/**\n * Load the last-known tool catalogue from disk into the per-server slice\n * map. Used at `activate()` time so the agent's tool list contains real\n * tools even in tool-discovery mode (where the IPC client never opens).\n *\n * Accepts BOTH formats:\n * - v2 envelope `{ schema, servers: { id: { fingerprint, descriptors } } }`\n * — the fingerprint-aware format this plugin now writes.\n * - v1 bare `McpToolDescriptor[]` — caches written by ≤ v0.0.16. These\n * have no fingerprints, so we bucket them by each descriptor's\n * `server` field with an EMPTY fingerprint. An empty fingerprint\n * never matches a real server fingerprint, so the first live refresh\n * treats every legacy slice as stale → forced re-discovery. That's\n * the desired migration: a box upgrading into the fix re-validates\n * its whole catalogue against live server launch identities once.\n *\n * Silently falls back to an empty map on any read/parse failure so a\n * first-run agent (no cache file yet) still loads cleanly.\n */\nfunction loadCachedSlicesFromDisk(logger: Logger): Record<string, CacheServerSlice> {\n try {\n const raw = readFileSync(CACHE_FILE, 'utf-8');\n const parsed = JSON.parse(raw) as unknown;\n const rawServers = parsedEnvelopeServers(parsed);\n if (rawServers) {\n const out: Record<string, CacheServerSlice> = {};\n for (const [id, value] of Object.entries(rawServers)) {\n if (typeof value !== 'object' || value === null) continue;\n const slice = value as { fingerprint?: unknown; descriptors?: unknown };\n const fingerprint = typeof slice.fingerprint === 'string' ? slice.fingerprint : '';\n const descriptors = Array.isArray(slice.descriptors)\n ? slice.descriptors.filter(isValidDescriptor)\n : [];\n out[id] = { fingerprint, descriptors };\n }\n return out;\n }\n if (Array.isArray(parsed)) {\n // Legacy v1 flat array — migrate by bucketing on `server`, with an\n // empty fingerprint so the first live refresh re-validates each.\n const valid = parsed.filter(isValidDescriptor);\n const out: Record<string, CacheServerSlice> = {};\n for (const d of valid) {\n const slice = (out[d.server] ??= { fingerprint: '', descriptors: [] });\n slice.descriptors.push(d);\n }\n return out;\n }\n return {};\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 * If `value` is a v2 cache envelope, return its raw (untyped) `servers`\n * map for field-by-field validation in the loader; otherwise `null`.\n * Returns `Record<string, unknown>` rather than `Record<string,\n * CacheServerSlice>` on purpose — the contents are unvalidated JSON, so\n * the loader must check each slice's fields at runtime.\n */\nfunction parsedEnvelopeServers(value: unknown): Record<string, unknown> | null {\n if (typeof value !== 'object' || value === null) return null;\n const v = value as Record<string, unknown>;\n if (typeof v.schema !== 'number') return null;\n if (typeof v.servers !== 'object' || v.servers === null) return null;\n return v.servers as Record<string, unknown>;\n}\n\n/**\n * Persist the current per-server slice map to disk as a v2 envelope so\n * the next tool-discovery load sees the tools AND their launch\n * fingerprints. Best-effort — disk failures are logged at `warn` and\n * otherwise swallowed.\n */\nfunction persistCachedSlicesToDisk(\n slices: Record<string, CacheServerSlice>,\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 envelope: CacheEnvelopeV2 = { schema: CACHE_SCHEMA_VERSION, servers: slices };\n const tmp = `${CACHE_FILE}.${randomUUID()}.tmp`;\n try {\n mkdirSync(dirname(CACHE_FILE), { recursive: true });\n writeFileSync(tmp, JSON.stringify(envelope));\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 * Merge a fresh live discovery into the cached per-server slices, gated\n * by each server's launch fingerprint. This is the heart of the\n * version-change + crashed-then-fixed fix.\n *\n * For every server in the LIVE roster (`mcp.list_servers`):\n * - Compute its launch fingerprint.\n * - Collect the freshly-discovered descriptors for that server.\n * - If the fingerprint CHANGED vs the cached slice → the cached slice\n * is stale (a different version/command). DROP the old descriptors\n * and adopt the fresh ones. If the fresh set is empty (the new\n * version hasn't finished spawning, or crashed), cache NO slice for\n * it — we'd rather show \"not discovered yet\" than serve the old\n * version's stale tools OR pin an empty set. The next refresh will\n * pick up the real tools once the child is up. THIS is what unsticks\n * the myob 0.2.0→0.2.3 bug: the version-pinned args change the\n * fingerprint, so the empty 0.2.0 slice can never survive the bump.\n * - If the fingerprint is UNCHANGED:\n * · fresh has tools → refresh the slice with them.\n * · fresh is empty → KEEP the prior good slice (transient daemon\n * cold-start / idle-reaped child returning [] for a moment must\n * not wipe a known-good catalogue). This is the per-server\n * generalisation of the old whole-catalogue \"don't shrink to\n * empty\" guard.\n *\n * Servers NOT in the live roster are dropped entirely — they've been\n * removed from the bundler store, so their tools should disappear.\n *\n * When the live roster is UNAVAILABLE (server list call failed), pass\n * `servers = undefined`: we can't fingerprint, so we conservatively keep\n * the existing slices untouched rather than risk corrupting them.\n */\nfunction mergeServerSlices(\n // Value typed `| undefined` so an index lookup is honestly nullable —\n // a server id absent from `prior` (a brand-new server) yields\n // undefined, which the fingerprint/keep-prior logic below relies on.\n prior: Record<string, CacheServerSlice | undefined>,\n freshDescriptors: McpToolDescriptor[],\n servers: ListedServer[] | undefined,\n): Record<string, CacheServerSlice> {\n if (!servers) return prior as Record<string, CacheServerSlice>;\n\n // Bucket the fresh descriptors by their originating server.\n const freshByServer = new Map<string, McpToolDescriptor[]>();\n for (const d of freshDescriptors) {\n const bucket = freshByServer.get(d.server);\n if (bucket) bucket.push(d);\n else freshByServer.set(d.server, [d]);\n }\n\n const next: Record<string, CacheServerSlice> = {};\n for (const server of servers) {\n const fingerprint = fingerprintServer(server);\n const priorSlice = prior[server.id];\n const fresh = freshByServer.get(server.id) ?? [];\n const fingerprintChanged = priorSlice?.fingerprint !== fingerprint;\n\n if (fingerprintChanged) {\n // Stale or new server: only cache a slice once real tools arrive.\n // Never carry the old version's descriptors or pin an empty set.\n if (fresh.length > 0) next[server.id] = { fingerprint, descriptors: fresh };\n continue;\n }\n // Same launch identity. `priorSlice` is necessarily defined here: an\n // unchanged fingerprint (`priorSlice?.fingerprint === fingerprint`)\n // cannot come from an absent slice (that would be `undefined !==\n // fingerprint` → changed). Refresh on a non-empty discovery;\n // otherwise hold the prior good slice (transient empty result).\n next[server.id] = fresh.length > 0 ? { fingerprint, descriptors: fresh } : priorSlice;\n }\n return next;\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: ListedServer[] }>('mcp.list_servers', {});\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_list failed');\n }\n // Project to a compact, status-aware shape so the agent can self-diagnose\n // (connected? how many tools? last connect error?) without wading through\n // the raw stored config.\n const summary = (response.payload?.servers ?? []).map((s) => {\n const row: Record<string, unknown> = {\n id: s.id,\n owner: s.entry?.owner ?? null,\n transport: s.entry?.transport ?? null,\n connected: s.status?.connected ?? null,\n tools: s.status?.toolCount ?? null,\n };\n if (s.status?.lastError) row.lastError = s.status.lastError;\n return row;\n });\n return {\n content: [{ type: 'text', text: JSON.stringify(summary, 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<{\n id: string;\n connected?: boolean;\n toolCount?: number;\n error?: string;\n }>('mcp.add_server', (params ?? {}) as Record<string, unknown>);\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_add failed');\n }\n const pl = response.payload;\n const id = pl?.id ?? '?';\n // The daemon confirms the connect before replying: report the real\n // outcome rather than optimistically claiming tools will appear.\n let text: string;\n if (pl?.error) {\n text = `Registered MCP server \"${id}\", but it FAILED to connect: ${pl.error}\\nFix the command/url/credentials/headers and re-add it, or call alfe_mcp_list to check its status.`;\n } else if (pl?.connected) {\n text = `Registered MCP server \"${id}\" — connected, ${String(pl.toolCount ?? 0)} tool(s) now available. Call alfe_mcp_list_tools to see them.`;\n } else {\n // No confirm field (older daemon) or still connecting past the probe window.\n text = `Registered MCP server \"${id}\". It may still be connecting — call alfe_mcp_list_tools to confirm its tools, or alfe_mcp_list to check its connection status.`;\n }\n return {\n content: [{ type: 'text', text }],\n details: { isError: Boolean(pl?.error) },\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 \"Discover and inspect ALL external integration tools available to this agent — built-in integrations (Atlassian/Jira, Confluence, Notion, GitHub, etc.) AND user-added custom connections (rostering, accounting, CRM, or any other domain API the user has connected). Returns each tool's exact name, description, and input schema. ALWAYS call this BEFORE telling the user that a capability or data source is unavailable — the integration you need may already be connected here and is invisible until you list it. After listing, invoke a tool 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) {\n // The list_tools CALL succeeded (even if it returned 0 tools).\n // Run the fingerprint-gated merge against the live server roster\n // — the SAME path as `refreshCachedTools`. Doing this even on an\n // empty result is load-bearing: a server whose launch fingerprint\n // changed (a version bump) must have its STALE slice dropped here\n // too, not just on the periodic refresh. Skipping the merge on\n // empty results was the gap that let a re-hydrated stale slice\n // (e.g. myob@0.2.0's 0-tool entry, or an old version's tools)\n // survive into the listing when the new version hadn't yet\n // discovered its tools. `mergeServerSlices` still HOLDS a prior\n // good slice when the SAME server transiently reports 0 tools, so\n // a momentary daemon hiccup doesn't wipe a known-good catalogue.\n const fresh = Array.isArray(response.payload.tools) ? response.payload.tools : [];\n const servers = await listServersForFingerprint();\n cachedSlices = mergeServerSlices(cachedSlices, fresh, servers);\n cachedDescriptors = flattenSlices(cachedSlices);\n descriptors = cachedDescriptors;\n if (moduleLog) persistCachedSlicesToDisk(cachedSlices, moduleLog);\n } else {\n // IPC call failed outright (NOT_CONNECTED / TIMEOUT) — daemon\n // unreachable. Serve the last-known cache untouched.\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: [{ type: 'text', text: await describeEmptyToolList(serverFilter) }],\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 live server roster from the daemon so the merge can\n * fingerprint each server's launch identity. Returns `undefined` (NOT\n * `[]`) when the call fails — the caller MUST treat that as \"roster\n * unavailable, keep prior slices\" rather than \"no servers exist, drop\n * everything\". An empty array is a legitimate \"store has no servers\"\n * answer and DOES clear the cache.\n */\nasync function listServersForFingerprint(): Promise<ListedServer[] | undefined> {\n const response = await callIpc<{ servers: ListedServer[] }>('mcp.list_servers', {});\n if (!response.ok || !response.payload || !Array.isArray(response.payload.servers)) {\n return undefined;\n }\n return response.payload.servers;\n}\n\n/**\n * Build an honest message for an empty `alfe_mcp_list_tools` result. The old\n * blanket \"try again in a moment\" hid the difference between three very\n * different states, so a genuinely-broken server looked identical to one still\n * warming. Cross-reference the live server roster + status to say which it is:\n * none registered, still connecting, or failed with a concrete error. Falls\n * back to the generic message if the roster is unavailable.\n */\nasync function describeEmptyToolList(serverFilter: string | undefined): Promise<string> {\n const suffix = ' Call alfe_mcp_list to see registered servers and their status.';\n let servers: ListedServer[] | undefined;\n try {\n servers = await listServersForFingerprint();\n } catch {\n servers = undefined;\n }\n if (!servers) {\n return serverFilter\n ? `No MCP tools available for server \"${serverFilter}\" (could not reach the daemon).${suffix}`\n : `No MCP tools available yet (could not reach the daemon).${suffix}`;\n }\n const scoped = serverFilter ? servers.filter((s) => s.id === serverFilter) : servers;\n if (scoped.length === 0) {\n return serverFilter\n ? `No MCP server \"${serverFilter}\" is registered. Use alfe_mcp_add to register one.`\n : 'No MCP servers are registered. Use alfe_mcp_add to register one.';\n }\n const failed = scoped.filter((s) => s.status && !s.status.connected && s.status.lastError);\n const connecting = scoped.filter((s) => s.status && !s.status.connected && !s.status.lastError);\n const parts: string[] = [];\n for (const s of failed) {\n parts.push(`\"${s.id}\" failed to connect: ${String(s.status?.lastError)}`);\n }\n if (connecting.length > 0) {\n const names = connecting.map((s) => `\"${s.id}\"`).join(', ');\n parts.push(`${names} still connecting — try again in a moment`);\n }\n if (parts.length > 0) {\n return `${parts.join('. ')}.${suffix}`;\n }\n // Servers exist and report connected, but advertise no tools (or older daemon\n // with no status). Keep it soft.\n return serverFilter\n ? `Server \"${serverFilter}\" is registered but advertised no tools.${suffix}`\n : `No MCP tools available yet — registered servers advertised none.${suffix}`;\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 // Pull the live server roster so we can fingerprint each server's\n // launch identity. The merge is fingerprint-gated: a version/\n // command change (myob 0.2.0 → 0.2.3) drops the stale slice, and a\n // server that transiently reports 0 tools (cold start, idle-reaped\n // child) keeps its prior good slice rather than being wiped. This\n // is the per-server replacement for the old whole-catalogue\n // \"don't shrink to empty\" guard.\n const servers = await listServersForFingerprint();\n cachedSlices = mergeServerSlices(cachedSlices, fresh, servers);\n cachedDescriptors = flattenSlices(cachedSlices);\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 persistCachedSlicesToDisk(cachedSlices, 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 // Loaded as fingerprint-tagged per-server slices (v2 envelope; a\n // legacy v1 flat array migrates in with empty fingerprints so the\n // first live refresh re-validates it). Refreshed live on every\n // `alfe_mcp_list_tools` call and by the periodic refresh.\n cachedSlices = loadCachedSlicesFromDisk(log);\n cachedDescriptors = flattenSlices(cachedSlices);\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 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.22",
3
+ "version": "0.0.24",
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",
@@ -27,7 +27,7 @@
27
27
  "openclaw.plugin.json"
28
28
  ],
29
29
  "dependencies": {
30
- "@alfe.ai/mcp-bundler": "0.2.2"
30
+ "@alfe.ai/mcp-bundler": "0.3.1"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "openclaw": ">=2026.3.0"