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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.d.cts CHANGED
@@ -44,11 +44,6 @@ interface ToolFactoryContext {
44
44
  runtimeConfig?: ConfigSnapshot;
45
45
  getRuntimeConfig?: () => ConfigSnapshot | undefined;
46
46
  }
47
- interface OpenClawPluginService {
48
- id: string;
49
- start: (ctx?: unknown) => void | Promise<void>;
50
- stop?: (ctx?: unknown) => void | Promise<void>;
51
- }
52
47
  interface OpenClawPluginApi {
53
48
  logger: Logger;
54
49
  config?: ConfigSnapshot;
@@ -66,7 +61,6 @@ interface OpenClawPluginApi {
66
61
  names?: string[];
67
62
  optional?: boolean;
68
63
  }) => void;
69
- registerService?: (service: OpenClawPluginService) => void;
70
64
  }
71
65
  declare const plugin: {
72
66
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICqBF,IAAA,EAAM,MAAA;IAON,OAAA,EAAA,MAAc;EAAA,CAAA;;;;;UAPd,MAAA,CA2BI;EAGJ,KAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAkB,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;EAAA,IAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;MACjB,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OACO,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;;UAzBR,cAAA,CA0B+B;EAG/B,MAAA,CAAA,EAAA;IAAqB,QAAA,CAAA,EAAA;MAEI,KAAA,CAAA,EAAA,MAAA;MACA,WAAA,CAAA,EA/BqB,MA+BrB,CAAA,MAAA,EAAA,OAAA,CAAA;IAAO,CAAA;EAGhC,CAAA;EAAiB,GAAA,CAAA,EAAA;IACjB,OAAA,CAAA,EAlCU,MAkCV,CAAA,MAAA,EAAA,OAAA,CAAA;;;UA/BA,eAAA,CAqCA;SAAmB,EApClB,MAoCkB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA;SAAuB,CAAA,EAnCxC,MAmCwC,CAAA,MAAA,EAAA,OAAA,CAAA;;UAhC1C,SAAA,CAmCoB;EAAqB,IAAA,EAAA,MAAA;EA6V7C,KAAA,CAAA,EAAA,MAsQL;EAAA,WAAA,EAAA,MAAA;YAjMe,EAjcF,MAicE,CAAA,MAAA,EAAA,OAAA,CAAA;SA6LE,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,CAAA,EA1nBL,WA0nBK,EAAA,QAAA,CAAA,EAAA,CAAA,MAAA,EAAA,OAAA,EAAA,GAAA,IAAA,EAAA,GAxnBX,OAwnBW,CAxnBH,eAwnBG,CAAA;;UArnBR,kBAAA;WACC;kBACO;2BACS;;UAGjB,qBAAA;;mCAEyB;mCACA;;UAGzB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;8BAGlC;;cA6VxB;;;;;;;;gBAqEU;kBA6LE"}
1
+ {"version":3,"file":"plugin.d.cts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICiEF,IAAA,EAAM,MAAA;IAON,OAAA,EAAA,MAAc;EAAA,CAAA;;;;;UAPd,MAAA;;;;;;UAOA,cAAA;;;;oBAC8C;;;;cACpC;;;UAGV,eAAA;WACC;YACC;;UAGF,SAAA;;;;cAII;0DAID,sDAEN,QAAQ;;UAGL,kBAAA;WACC;kBACO;2BACS;;UAGjB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;;cA8oB1D;;;;;;;;gBAwCU;kBAgGE"}
package/dist/plugin.d.ts CHANGED
@@ -44,11 +44,6 @@ interface ToolFactoryContext {
44
44
  runtimeConfig?: ConfigSnapshot;
45
45
  getRuntimeConfig?: () => ConfigSnapshot | undefined;
46
46
  }
47
- interface OpenClawPluginService {
48
- id: string;
49
- start: (ctx?: unknown) => void | Promise<void>;
50
- stop?: (ctx?: unknown) => void | Promise<void>;
51
- }
52
47
  interface OpenClawPluginApi {
53
48
  logger: Logger;
54
49
  config?: ConfigSnapshot;
@@ -66,7 +61,6 @@ interface OpenClawPluginApi {
66
61
  names?: string[];
67
62
  optional?: boolean;
68
63
  }) => void;
69
- registerService?: (service: OpenClawPluginService) => void;
70
64
  }
71
65
  declare const plugin: {
72
66
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICqBF,IAAA,EAAM,MAAA;IAON,OAAA,EAAA,MAAc;EAAA,CAAA;;;;;UAPd,MAAA,CA2BI;EAGJ,KAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAkB,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;EAAA,IAAA,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;MACjB,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;OACO,CAAA,GAAA,EAAA,MAAA,EAAA,GAAA,IAAA,EAAA,OAAA,EAAA,CAAA,EAAA,IAAA;;UAzBR,cAAA,CA0B+B;EAG/B,MAAA,CAAA,EAAA;IAAqB,QAAA,CAAA,EAAA;MAEI,KAAA,CAAA,EAAA,MAAA;MACA,WAAA,CAAA,EA/BqB,MA+BrB,CAAA,MAAA,EAAA,OAAA,CAAA;IAAO,CAAA;EAGhC,CAAA;EAAiB,GAAA,CAAA,EAAA;IACjB,OAAA,CAAA,EAlCU,MAkCV,CAAA,MAAA,EAAA,OAAA,CAAA;;;UA/BA,eAAA,CAqCA;SAAmB,EApClB,MAoCkB,CAAA,MAAA,EAAA,OAAA,CAAA,EAAA;SAAuB,CAAA,EAnCxC,MAmCwC,CAAA,MAAA,EAAA,OAAA,CAAA;;UAhC1C,SAAA,CAmCoB;EAAqB,IAAA,EAAA,MAAA;EA6V7C,KAAA,CAAA,EAAA,MAsQL;EAAA,WAAA,EAAA,MAAA;YAjMe,EAjcF,MAicE,CAAA,MAAA,EAAA,OAAA,CAAA;SA6LE,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,CAAA,EA1nBL,WA0nBK,EAAA,QAAA,CAAA,EAAA,CAAA,MAAA,EAAA,OAAA,EAAA,GAAA,IAAA,EAAA,GAxnBX,OAwnBW,CAxnBH,eAwnBG,CAAA;;UArnBR,kBAAA;WACC;kBACO;2BACS;;UAGjB,qBAAA;;mCAEyB;mCACA;;UAGzB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;8BAGlC;;cA6VxB;;;;;;;;gBAqEU;kBA6LE"}
1
+ {"version":3,"file":"plugin.d.ts","names":[],"sources":["../src/ipc-client.ts","../src/plugin.ts"],"mappings":";UA0BiB;EAAA,EAAA,EAAA,MAAA;;YAGL;;ICiEF,IAAA,EAAM,MAAA;IAON,OAAA,EAAA,MAAc;EAAA,CAAA;;;;;UAPd,MAAA;;;;;;UAOA,cAAA;;;;oBAC8C;;;;cACpC;;;UAGV,eAAA;WACC;YACC;;UAGF,SAAA;;;;cAII;0DAID,sDAEN,QAAQ;;UAGL,kBAAA;WACC;kBACO;2BACS;;UAGjB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;;cA8oB1D;;;;;;;;gBAwCU;kBAgGE"}
package/dist/plugin2.cjs CHANGED
@@ -236,44 +236,20 @@ function isIpcResponse(value) {
236
236
  //#region src/plugin.ts
237
237
  const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
238
238
  const CACHE_FILE = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "mcp", "openclaw-bundler-cache.json");
239
+ const GATEWAY_SOCKET = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "gateway.sock");
239
240
  let ipcClient;
240
241
  let cachedDescriptors = [];
241
242
  let refreshInFlight = null;
242
243
  let periodicRefreshTimer = null;
244
+ let moduleApi;
245
+ let moduleLog;
243
246
  const ACTIVATED_KEY = "__alfeMcpBundlerPluginActivated";
244
247
  const g = globalThis;
245
- const PREWARM_TIMEOUT_MS = 3e3;
248
+ const IPC_CONNECT_TIMEOUT_MS = 2500;
246
249
  const PERIODIC_REFRESH_INTERVAL_MS = 1e4;
247
250
  function resolveLiveConfig(api, ctx) {
248
251
  return ctx?.runtimeConfig ?? ctx?.getRuntimeConfig?.() ?? api.runtime?.config?.current?.() ?? ctx?.config ?? api.config;
249
252
  }
250
- function buildAgentTool(api, tool) {
251
- return {
252
- name: tool.prefixed,
253
- label: tool.label,
254
- description: tool.description || `MCP tool ${tool.original} from server ${tool.server}`,
255
- parameters: tool.parameters,
256
- async execute(_toolCallId, params) {
257
- if (!ipcClient?.isConnected()) return errorResult(`mcp ${tool.prefixed}: bundler IPC not connected yet. Try again in a moment.`);
258
- const response = await ipcClient.call("mcp.call_tool", {
259
- name: tool.prefixed,
260
- args: params
261
- });
262
- if (!response.ok || !response.payload) {
263
- api.logger.warn("[mcp-bundler] tool call failed", {
264
- tool: tool.prefixed,
265
- err: response.error?.message ?? "unknown error"
266
- });
267
- return errorResult(response.error?.message ? `mcp ${tool.prefixed}: ${response.error.message}` : `mcp ${tool.prefixed}: call failed`);
268
- }
269
- const result = response.payload;
270
- return {
271
- content: result.content,
272
- details: { isError: result.isError ?? false }
273
- };
274
- }
275
- };
276
- }
277
253
  function errorResult(message) {
278
254
  return {
279
255
  content: [{
@@ -284,36 +260,93 @@ function errorResult(message) {
284
260
  };
285
261
  }
286
262
  /**
263
+ * Lazily construct + connect the IPC client on first use.
264
+ *
265
+ * THE LOAD-BEARING FIX (v0.0.16, 2026-06-14). The gateway daemon runs
266
+ * the long-lived openclaw runtime in `registrationMode=tool-discovery`,
267
+ * NOT `full`. Pre-v0.0.16 the client was only constructed in `activate()`
268
+ * when `shouldStartIpcClient` returned true (full mode only) — so in the
269
+ * runtime that actually serves the agent's turns, the client was NEVER
270
+ * constructed. Verified on QA Tester: the 2.6-day-uptime runtime held
271
+ * zero connections to `~/.alfe/gateway.sock`. Listing worked (disk cache)
272
+ * but every tool EXECUTION returned NOT_CONNECTED → "bundler store isn't
273
+ * initialized."
274
+ *
275
+ * The correct trigger for opening the socket is "a tool is actually being
276
+ * invoked", not "we activated in full mode". Tool execution only ever
277
+ * happens in a live runtime process, and the plugin module is process-
278
+ * cached, so constructing here populates the module-scoped `ipcClient`
279
+ * for every later call in the same process. Pure tool-discovery loads
280
+ * that enumerate but never execute still never reach this path → no
281
+ * socket opened, the discovery invariant is preserved.
282
+ *
283
+ * Returns `undefined` only when there's genuinely no daemon to talk to
284
+ * (no socket file, or `activate()` never captured the module refs).
285
+ */
286
+ function ensureIpcClient() {
287
+ if (ipcClient) return ipcClient;
288
+ if (!moduleLog) return void 0;
289
+ if (!(0, node_fs.existsSync)(GATEWAY_SOCKET)) {
290
+ moduleLog.debug(`[mcp-bundler] ${GATEWAY_SOCKET} not present — cannot open IPC for this call`);
291
+ return;
292
+ }
293
+ startIpcClient(moduleLog);
294
+ moduleLog.info("[mcp-bundler] IPC client constructed lazily on first tool call");
295
+ return ipcClient;
296
+ }
297
+ /**
298
+ * Poll `isConnected()` until the socket is up or the budget expires.
299
+ * `McpIpcClient.start()` connects asynchronously, so a freshly-
300
+ * constructed client (or one mid-reconnect after a daemon bounce) needs
301
+ * a moment before `.call()` will reach the wire.
302
+ */
303
+ async function waitForIpcConnect(client, timeoutMs) {
304
+ if (client.isConnected()) return;
305
+ const deadline = Date.now() + timeoutMs;
306
+ while (!client.isConnected() && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 50));
307
+ }
308
+ /**
287
309
  * Async-aware shim around the module-scoped `ipcClient`. The management
288
310
  * tools and dynamic MCP tools registered in `activate()` execute LATER
289
- * (when the agent invokes them), but `ipcClient` is only set inside
290
- * `startService`. The `?.` chains return useful errors when the agent
291
- * tries to call a tool before the IPC is ready, instead of throwing.
311
+ * (when the agent invokes them). `callIpc` constructs the client on
312
+ * demand (`ensureIpcClient`) and waits for the socket to connect before
313
+ * forwarding, so the very first tool call works even in the tool-
314
+ * discovery runtime where the client was never eagerly started. Returns
315
+ * a typed `NOT_CONNECTED` response — never throws — when there's no
316
+ * daemon socket to reach.
292
317
  */
293
318
  async function callIpc(method, params = {}) {
294
- if (!ipcClient) return {
319
+ const client = ensureIpcClient();
320
+ if (!client) return {
295
321
  id: "",
296
322
  ok: false,
297
323
  error: {
298
324
  code: "NOT_CONNECTED",
299
- message: "Bundler IPC client not initialized yet"
325
+ message: "Bundler IPC unavailable no alfe-gateway daemon socket"
300
326
  }
301
327
  };
302
- return ipcClient.call(method, params);
328
+ if (!client.isConnected()) await waitForIpcConnect(client, IPC_CONNECT_TIMEOUT_MS);
329
+ return client.call(method, params);
303
330
  }
304
331
  /**
305
- * Load the last-known tool catalogue from disk. Used at `activate()`
306
- * time so the agent's tool list contains real tools even in tool-
307
- * discovery mode (where `service.start` and therefore the IPC
308
- * connection never runs). Silently falls back to an empty list on
309
- * any read/parse failure so a first-run agent (no cache file yet)
310
- * still loads cleanly.
332
+ * Cheap shape check guarding against a corrupt or torn-write cache —
333
+ * a half-written JSON array that parses but holds garbage entries
334
+ * would otherwise surface through `alfe_mcp_list_tools` with undefined
335
+ * names/schemas and mislead the agent. Catching the shape here keeps
336
+ * the listed catalogue clean even after disk-level corruption.
311
337
  */
312
338
  function isValidDescriptor(value) {
313
339
  if (typeof value !== "object" || value === null) return false;
314
340
  const v = value;
315
341
  return typeof v.prefixed === "string" && typeof v.original === "string" && typeof v.server === "string" && typeof v.parameters === "object" && v.parameters !== null;
316
342
  }
343
+ /**
344
+ * Load the last-known tool catalogue from disk. Used at `activate()`
345
+ * time so the agent's tool list contains real tools even in tool-
346
+ * discovery mode (where the IPC client never opens). Silently falls
347
+ * back to an empty list on any read/parse failure so a first-run agent
348
+ * (no cache file yet) still loads cleanly.
349
+ */
317
350
  function loadCachedDescriptorsFromDisk(logger) {
318
351
  try {
319
352
  const raw = (0, node_fs.readFileSync)(CACHE_FILE, "utf-8");
@@ -453,11 +486,99 @@ function registerAgentMcpManagementTools(api) {
453
486
  };
454
487
  }
455
488
  });
489
+ api.registerTool({
490
+ name: "alfe_mcp_list_tools",
491
+ label: "List available MCP tools",
492
+ description: "List every MCP tool currently available across all of this agent's connected integrations (Atlassian/Jira, Confluence, Notion, GitHub, etc.), with each tool's exact name, description, and input schema. ALWAYS call this first to discover what tools exist and how to call them, then invoke one with `alfe_mcp_call`. Optionally pass `server` to restrict the listing to a single MCP server (e.g. \"atlassian-atlassian\") when you already know which integration you need.",
493
+ parameters: {
494
+ type: "object",
495
+ properties: { server: {
496
+ type: "string",
497
+ description: "Optional. Restrict the listing to one MCP server id (e.g. \"atlassian-atlassian\", \"github-github\"). Omit to list every available tool."
498
+ } },
499
+ additionalProperties: false
500
+ },
501
+ async execute(_toolCallId, params) {
502
+ const response = await callIpc("mcp.list_tools", {});
503
+ let descriptors;
504
+ if (response.ok && response.payload?.tools && response.payload.tools.length > 0) {
505
+ descriptors = response.payload.tools;
506
+ cachedDescriptors = descriptors;
507
+ if (moduleLog) persistCachedDescriptorsToDisk(descriptors, moduleLog);
508
+ } else descriptors = cachedDescriptors;
509
+ const p = params ?? {};
510
+ const serverFilter = typeof p.server === "string" ? p.server : void 0;
511
+ const out = (serverFilter ? descriptors.filter((d) => d.server === serverFilter) : descriptors).map((d) => ({
512
+ tool: d.prefixed,
513
+ description: d.description || `MCP tool ${d.original} from server ${d.server}`,
514
+ inputSchema: d.parameters
515
+ }));
516
+ if (out.length === 0) return {
517
+ content: [{
518
+ type: "text",
519
+ 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."
520
+ }],
521
+ details: { isError: false }
522
+ };
523
+ return {
524
+ content: [{
525
+ type: "text",
526
+ 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)}`
527
+ }],
528
+ details: { isError: false }
529
+ };
530
+ }
531
+ });
532
+ api.registerTool({
533
+ name: "alfe_mcp_call",
534
+ label: "Call an MCP tool",
535
+ description: "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.",
536
+ parameters: {
537
+ type: "object",
538
+ properties: {
539
+ tool: {
540
+ type: "string",
541
+ description: "The exact prefixed tool name from alfe_mcp_list_tools (e.g. \"atlassian-atlassian__jira_get_issue\")."
542
+ },
543
+ args: {
544
+ type: "object",
545
+ description: "Arguments object matching the chosen tool's input schema (as shown by alfe_mcp_list_tools). Pass {} for tools that take no arguments.",
546
+ additionalProperties: true
547
+ }
548
+ },
549
+ required: ["tool"],
550
+ additionalProperties: false
551
+ },
552
+ async execute(_toolCallId, params) {
553
+ const p = params ?? {};
554
+ const tool = p.tool;
555
+ if (typeof tool !== "string" || !tool) return errorResult("alfe_mcp_call requires a `tool` name — call alfe_mcp_list_tools first to see the available tool names.");
556
+ const response = await callIpc("mcp.call_tool", {
557
+ name: tool,
558
+ args: p.args ?? {}
559
+ });
560
+ if (!response.ok || !response.payload) {
561
+ (moduleLog ?? api.logger).warn("[mcp-bundler] alfe_mcp_call failed", {
562
+ tool,
563
+ err: response.error?.message ?? "unknown error"
564
+ });
565
+ return errorResult(response.error?.message ? `mcp ${tool}: ${response.error.message}` : `mcp ${tool}: call failed (is the tool name exact? call alfe_mcp_list_tools to confirm)`);
566
+ }
567
+ const result = response.payload;
568
+ return {
569
+ content: result.content,
570
+ details: { isError: result.isError ?? false }
571
+ };
572
+ }
573
+ });
456
574
  }
457
575
  /**
458
- * Pull the latest tool catalog from the daemon-hosted bundler. Coalesces
459
- * concurrent calls so the LLM hitting the factory many times in a tight
460
- * loop only triggers one in-flight IPC request.
576
+ * Pull the latest tool catalog from the daemon-hosted bundler into
577
+ * `cachedDescriptors` + the disk cache. Driven by the periodic timer
578
+ * and the `onConnect` hook so the disk-cache fallback that
579
+ * `alfe_mcp_list_tools` reads stays warm even for tool-discovery loads
580
+ * that never execute a tool. Coalesces concurrent calls so a burst only
581
+ * triggers one in-flight IPC request.
461
582
  */
462
583
  function refreshCachedTools(api) {
463
584
  if (!ipcClient) return Promise.resolve();
@@ -469,7 +590,12 @@ function refreshCachedTools(api) {
469
590
  api.logger.debug("[mcp-bundler] list_tools failed", { err: response.error?.message ?? "unknown" });
470
591
  return;
471
592
  }
472
- cachedDescriptors = response.payload.tools;
593
+ const fresh = Array.isArray(response.payload.tools) ? response.payload.tools : [];
594
+ if (fresh.length === 0 && cachedDescriptors.length > 0) {
595
+ api.logger.debug(`[mcp-bundler] list_tools returned 0 tools — keeping prior cache of ${String(cachedDescriptors.length)}`);
596
+ return;
597
+ }
598
+ cachedDescriptors = fresh;
473
599
  persistCachedDescriptorsToDisk(cachedDescriptors, api.logger);
474
600
  } finally {
475
601
  refreshInFlight = null;
@@ -477,6 +603,88 @@ function refreshCachedTools(api) {
477
603
  })();
478
604
  return refreshInFlight;
479
605
  }
606
+ /**
607
+ * Should this `activate()` invocation also start the live IPC client?
608
+ *
609
+ * Decision rule: only in full agent-runtime mode. CLI invocations
610
+ * (`openclaw config set`, `openclaw plugins inspect`, `openclaw upgrade`,
611
+ * etc.) and tool-discovery loads (where OpenClaw builds the agent's
612
+ * tool list without running plugin services) should NOT open the
613
+ * gateway socket — they only need the disk cache and the static tool
614
+ * registrations.
615
+ *
616
+ * `api.registrationMode` is the canonical signal. OpenClaw 2026.5
617
+ * sets it to one of: `'full'`, `'discovery'`, `'tool-discovery'`,
618
+ * `'setup-only'`, `'setup-runtime'`, `'cli-metadata'`. Only `'full'`
619
+ * means "real agent runtime; everything is allowed to run".
620
+ *
621
+ * Defensive default: if `registrationMode` is unset (older OpenClaw,
622
+ * non-bundled host, test harness), assume full mode so we don't
623
+ * silently degrade. The IPC client itself handles a missing socket
624
+ * gracefully via reconnect, so an over-eager start is cheap.
625
+ */
626
+ function shouldStartIpcClient(api) {
627
+ const mode = api.registrationMode;
628
+ if (mode === void 0) return true;
629
+ return mode === "full";
630
+ }
631
+ /**
632
+ * Construct the live IPC client and wire its `onConnect` to a cache
633
+ * refresh. Module-scoped — the same client lifecycle outlasts a single
634
+ * `activate()` call so reconnects from daemon restarts continue
635
+ * automatically. Called from `activate()` ONLY in full agent-runtime
636
+ * mode (see `shouldStartIpcClient`).
637
+ *
638
+ * IMPORTANT: this function is NOT awaited. The `onConnect` callback
639
+ * fires asynchronously; `alfe_mcp_list_tools` returns whatever is in
640
+ * `cachedDescriptors` when it's called with a closed socket (loaded
641
+ * from disk in `activate()`, then refreshed on connect, then
642
+ * periodically). A first-turn pre-warm is no longer needed — the disk
643
+ * cache covers the cold window, and any new tools discovered during the
644
+ * in-flight `onConnect` refresh land in `cachedDescriptors` shortly
645
+ * after.
646
+ *
647
+ * Removed the v0.0.13 PREWARM_TIMEOUT_MS / blocking pre-warm because
648
+ * v0.0.14 has no `service.start` await point to attach it to —
649
+ * removing `api.registerService` was the whole point of the fix. Disk
650
+ * cache + periodic refresh now carry the freshness contract.
651
+ */
652
+ function startIpcClient(log) {
653
+ if (ipcClient) return;
654
+ if (!(0, node_fs.existsSync)(GATEWAY_SOCKET)) log.debug(`[mcp-bundler] ${GATEWAY_SOCKET} not present at activate — IPC client will refresh when the daemon comes online`);
655
+ ipcClient = new McpIpcClient({
656
+ logger: log,
657
+ plugin: {
658
+ name: "@alfe.ai/openclaw-mcp-bundler",
659
+ version: pkg.version
660
+ },
661
+ onConnect: () => {
662
+ if (moduleApi) refreshCachedTools(moduleApi);
663
+ }
664
+ });
665
+ ipcClient.start();
666
+ periodicRefreshTimer = setInterval(() => {
667
+ if (ipcClient?.isConnected() && moduleApi) refreshCachedTools(moduleApi);
668
+ }, PERIODIC_REFRESH_INTERVAL_MS);
669
+ periodicRefreshTimer.unref?.();
670
+ }
671
+ /**
672
+ * Tear down the IPC client and periodic refresh timer. Called from
673
+ * `deactivate()` (the only lifecycle exit point now that v0.0.14
674
+ * dropped `api.registerService`). Safe to call multiple times — each
675
+ * tear-down is idempotent.
676
+ */
677
+ function stopIpcClient() {
678
+ if (periodicRefreshTimer !== null) {
679
+ clearInterval(periodicRefreshTimer);
680
+ periodicRefreshTimer = null;
681
+ }
682
+ if (ipcClient) {
683
+ ipcClient.stop();
684
+ ipcClient = void 0;
685
+ }
686
+ refreshInFlight = null;
687
+ }
480
688
  const plugin = {
481
689
  id: "@alfe.ai/openclaw-mcp-bundler",
482
690
  name: "Alfe MCP Bundler",
@@ -486,38 +694,8 @@ const plugin = {
486
694
  "alfe_mcp_list",
487
695
  "alfe_mcp_add",
488
696
  "alfe_mcp_remove",
489
- "atlassian-atlassian__atlassian_list_accounts",
490
- "atlassian-atlassian__atlassian_check_connection",
491
- "atlassian-atlassian__atlassian_refresh_token",
492
- "notion-notion__notion_list_accounts",
493
- "notion-notion__notion_check_connection",
494
- "notion-notion__notion_refresh_token",
495
- "github-github__github_list_accounts",
496
- "github-github__github_check_connection",
497
- "github-github__github_refresh_token",
498
- "microsoft-microsoft__microsoft_list_accounts",
499
- "microsoft-microsoft__microsoft_check_connection",
500
- "microsoft-microsoft__microsoft_refresh_token",
501
- "google-google__google_list_accounts",
502
- "google-google__google_check_connection",
503
- "google-google__google_refresh_token",
504
- "slack-slack__slack_list_accounts",
505
- "slack-slack__slack_check_connection",
506
- "slack-slack__slack_refresh_token",
507
- "discord-discord__discord_list_accounts",
508
- "discord-discord__discord_check_connection",
509
- "discord-discord__discord_refresh_token",
510
- "mobile-mobile__mobile_list_accounts",
511
- "mobile-mobile__mobile_check_connection",
512
- "mobile-mobile__mobile_refresh_token",
513
- "myob-myob__myob_list_accounts",
514
- "myob-myob__myob_check_connection",
515
- "myob-myob__myob_refresh_token",
516
- "xero-xero__xero_list_accounts",
517
- "xero-xero__xero_check_connection",
518
- "xero-xero__xero_refresh_token",
519
- "alfe-platform__session_status",
520
- "alfe-platform__gateway_status"
697
+ "alfe_mcp_list_tools",
698
+ "alfe_mcp_call"
521
699
  ] },
522
700
  activate(api) {
523
701
  const log = api.logger;
@@ -531,82 +709,19 @@ const plugin = {
531
709
  log.info("[mcp-bundler] disabled via plugin config — no-op");
532
710
  return;
533
711
  }
534
- if (!api.registerService) {
535
- g[ACTIVATED_KEY] = false;
536
- log.warn("[mcp-bundler] api.registerService unavailable — plugin is a no-op on this runtime");
537
- return;
538
- }
539
- const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));
540
- if (deferToNative) log.info("[mcp-bundler] openclaw.json#mcp.servers has entries — MCP-proxy factory will defer to native bundleMcp, but agent self-service tools (alfe_mcp_*) stay available");
712
+ const hasNativeServers = hasNativeOpenclawMcpServers(resolveLiveConfig(api));
713
+ if (hasNativeServers) log.info("[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");
541
714
  cachedDescriptors = loadCachedDescriptorsFromDisk(log);
542
715
  registerAgentMcpManagementTools(api);
543
- if (!deferToNative) api.registerTool((ctx) => {
544
- if (hasNativeOpenclawMcpServers(resolveLiveConfig(api, ctx))) return null;
545
- refreshCachedTools(api);
546
- return cachedDescriptors.map((d) => buildAgentTool(api, d));
547
- }, { optional: true });
548
- const startService = async () => {
549
- const prewarmTimer = { handle: null };
550
- if (await new Promise((resolve) => {
551
- prewarmTimer.handle = setTimeout(() => {
552
- prewarmTimer.handle = null;
553
- resolve("timeout");
554
- }, PREWARM_TIMEOUT_MS);
555
- ipcClient = new McpIpcClient({
556
- logger: log,
557
- plugin: {
558
- name: plugin.id,
559
- version: plugin.version
560
- },
561
- onConnect: () => {
562
- refreshCachedTools(api).finally(() => {
563
- if (prewarmTimer.handle !== null) {
564
- clearTimeout(prewarmTimer.handle);
565
- prewarmTimer.handle = null;
566
- }
567
- resolve("completed");
568
- });
569
- }
570
- });
571
- ipcClient.start();
572
- }) === "timeout") log.warn(`[mcp-bundler] pre-warm timed out after ${String(PREWARM_TIMEOUT_MS)}ms — first chat turn may see a stale tool list (likely a slow or unreachable daemon at ~/.alfe/gateway.sock)`);
573
- periodicRefreshTimer = setInterval(() => {
574
- if (ipcClient?.isConnected()) refreshCachedTools(api);
575
- }, PERIODIC_REFRESH_INTERVAL_MS);
576
- periodicRefreshTimer.unref?.();
577
- if (deferToNative) {
578
- log.info(`[mcp-bundler] activated v${plugin.version} (management tools only — native bundleMcp owns runtime)`);
579
- return;
580
- }
581
- log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client, tools registered in activate)`);
582
- };
583
- const stopService = () => {
584
- log.info("[mcp-bundler] stopping");
585
- if (periodicRefreshTimer !== null) {
586
- clearInterval(periodicRefreshTimer);
587
- periodicRefreshTimer = null;
588
- }
589
- if (ipcClient) {
590
- ipcClient.stop();
591
- ipcClient = void 0;
592
- }
593
- refreshInFlight = null;
594
- g[ACTIVATED_KEY] = false;
595
- return Promise.resolve();
596
- };
597
- api.registerService({
598
- id: "alfe-mcp-bundler",
599
- start: async () => {
600
- await startService();
601
- },
602
- stop: async () => {
603
- await stopService();
604
- }
605
- });
716
+ moduleApi = api;
717
+ moduleLog = log;
718
+ log.info(`[mcp-bundler] activated v${plugin.version} (registrationMode=${api.registrationMode ?? "unknown"}, nativeMcpServers=${String(hasNativeServers)}, cachedDescriptors=${String(cachedDescriptors.length)})`);
719
+ if (shouldStartIpcClient(api)) startIpcClient(log);
606
720
  },
607
721
  deactivate(api) {
608
722
  g[ACTIVATED_KEY] = false;
609
723
  api.logger.debug("[mcp-bundler] deactivating");
724
+ stopIpcClient();
610
725
  }
611
726
  };
612
727
  //#endregion