@alfe.ai/openclaw-mcp-bundler 0.0.9 → 0.0.11

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;;ICIF,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;EAuN7C,KAAA,CAAA,EAAA,MA+GL;EAAA,WAAA,EAAA,MAAA;YAnGe,EAlQF,MAkQE,CAAA,MAAA,EAAA,OAAA,CAAA;SA+FE,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,CAAA,EA7VL,WA6VK,EAAA,QAAA,CAAA,EAAA,CAAA,MAAA,EAAA,OAAA,EAAA,GAAA,IAAA,EAAA,GA3VX,OA2VW,CA3VH,eA2VG,CAAA;;UAxVR,kBAAA;WACC;kBACO;2BACS;;UAGjB,qBAAA;;mCAEyB;mCACA;;UAGzB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;8BAGlC;;cAuNxB;;;;;;;;gBAYU;kBA+FE"}
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;EAyU7C,KAAA,CAAA,EAAA,MAsML;EAAA,WAAA,EAAA,MAAA;YAjIe,EA7aF,MA6aE,CAAA,MAAA,EAAA,OAAA,CAAA;SA6HE,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,CAAA,EAtiBL,WAsiBK,EAAA,QAAA,CAAA,EAAA,CAAA,MAAA,EAAA,OAAA,EAAA,GAAA,IAAA,EAAA,GApiBX,OAoiBW,CApiBH,eAoiBG,CAAA;;UAjiBR,kBAAA;WACC;kBACO;2BACS;;UAGjB,qBAAA;;mCAEyB;mCACA;;UAGzB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;8BAGlC;;cAyUxB;;;;;;;;gBAqEU;kBA6HE"}
@@ -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;;ICIF,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;EAuN7C,KAAA,CAAA,EAAA,MA+GL;EAAA,WAAA,EAAA,MAAA;YAnGe,EAlQF,MAkQE,CAAA,MAAA,EAAA,OAAA,CAAA;SA+FE,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,CAAA,EA7VL,WA6VK,EAAA,QAAA,CAAA,EAAA,CAAA,MAAA,EAAA,OAAA,EAAA,GAAA,IAAA,EAAA,GA3VX,OA2VW,CA3VH,eA2VG,CAAA;;UAxVR,kBAAA;WACC;kBACO;2BACS;;UAGjB,qBAAA;;mCAEyB;mCACA;;UAGzB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;8BAGlC;;cAuNxB;;;;;;;;gBAYU;kBA+FE"}
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;EAyU7C,KAAA,CAAA,EAAA,MAsML;EAAA,WAAA,EAAA,MAAA;YAjIe,EA7aF,MA6aE,CAAA,MAAA,EAAA,OAAA,CAAA;SA6HE,EAAA,CAAA,UAAA,EAAA,MAAA,EAAA,MAAA,EAAA,OAAA,EAAA,MAAA,CAAA,EAtiBL,WAsiBK,EAAA,QAAA,CAAA,EAAA,CAAA,MAAA,EAAA,OAAA,EAAA,GAAA,IAAA,EAAA,GApiBX,OAoiBW,CApiBH,eAoiBG,CAAA;;UAjiBR,kBAAA;WACC;kBACO;2BACS;;UAGjB,qBAAA;;mCAEyB;mCACA;;UAGzB,iBAAA;UACA;WACC;;;;;;;sBAG8B;;;uBAE/B,mBAAmB,uBAAuB,YAAY;;;;;8BAGlC;;cAyUxB;;;;;;;;gBAqEU;kBA6HE"}
package/dist/plugin2.cjs CHANGED
@@ -3,6 +3,7 @@ let node_os = require("node:os");
3
3
  let node_path = require("node:path");
4
4
  let node_crypto = require("node:crypto");
5
5
  let node_module = require("node:module");
6
+ let node_fs = require("node:fs");
6
7
  //#region src/cli-backend-detect.ts
7
8
  /**
8
9
  * Returns true when openclaw.json#mcp.servers has at least one entry —
@@ -234,22 +235,24 @@ function isIpcResponse(value) {
234
235
  //#endregion
235
236
  //#region src/plugin.ts
236
237
  const pkg = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href)("../package.json");
238
+ const CACHE_FILE = (0, node_path.join)((0, node_os.homedir)(), ".alfe", "mcp", "openclaw-bundler-cache.json");
237
239
  let ipcClient;
238
- let cachedTools = [];
240
+ let cachedDescriptors = [];
239
241
  let refreshInFlight = null;
240
242
  const ACTIVATED_KEY = "__alfeMcpBundlerPluginActivated";
241
243
  const g = globalThis;
242
244
  function resolveLiveConfig(api, ctx) {
243
245
  return ctx?.runtimeConfig ?? ctx?.getRuntimeConfig?.() ?? api.runtime?.config?.current?.() ?? ctx?.config ?? api.config;
244
246
  }
245
- function buildAgentTool(api, tool, ipc) {
247
+ function buildAgentTool(api, tool) {
246
248
  return {
247
249
  name: tool.prefixed,
248
250
  label: tool.label,
249
251
  description: tool.description || `MCP tool ${tool.original} from server ${tool.server}`,
250
252
  parameters: tool.parameters,
251
253
  async execute(_toolCallId, params) {
252
- const response = await ipc.call("mcp.call_tool", {
254
+ if (!ipcClient?.isConnected()) return errorResult(`mcp ${tool.prefixed}: bundler IPC not connected yet. Try again in a moment.`);
255
+ const response = await ipcClient.call("mcp.call_tool", {
253
256
  name: tool.prefixed,
254
257
  args: params
255
258
  });
@@ -278,6 +281,63 @@ function errorResult(message) {
278
281
  };
279
282
  }
280
283
  /**
284
+ * Async-aware shim around the module-scoped `ipcClient`. The management
285
+ * tools and dynamic MCP tools registered in `activate()` execute LATER
286
+ * (when the agent invokes them), but `ipcClient` is only set inside
287
+ * `startService`. The `?.` chains return useful errors when the agent
288
+ * tries to call a tool before the IPC is ready, instead of throwing.
289
+ */
290
+ async function callIpc(method, params = {}) {
291
+ if (!ipcClient) return {
292
+ id: "",
293
+ ok: false,
294
+ error: {
295
+ code: "NOT_CONNECTED",
296
+ message: "Bundler IPC client not initialized yet"
297
+ }
298
+ };
299
+ return ipcClient.call(method, params);
300
+ }
301
+ /**
302
+ * Load the last-known tool catalogue from disk. Used at `activate()`
303
+ * time so the agent's tool list contains real tools even in tool-
304
+ * discovery mode (where `service.start` — and therefore the IPC
305
+ * connection — never runs). Silently falls back to an empty list on
306
+ * any read/parse failure so a first-run agent (no cache file yet)
307
+ * still loads cleanly.
308
+ */
309
+ function isValidDescriptor(value) {
310
+ if (typeof value !== "object" || value === null) return false;
311
+ const v = value;
312
+ return typeof v.prefixed === "string" && typeof v.original === "string" && typeof v.server === "string" && typeof v.parameters === "object" && v.parameters !== null;
313
+ }
314
+ function loadCachedDescriptorsFromDisk(logger) {
315
+ try {
316
+ const raw = (0, node_fs.readFileSync)(CACHE_FILE, "utf-8");
317
+ const parsed = JSON.parse(raw);
318
+ if (!Array.isArray(parsed)) return [];
319
+ return parsed.filter(isValidDescriptor);
320
+ } catch (err) {
321
+ if (err instanceof Error && err.code !== "ENOENT") logger.debug("[mcp-bundler] cache read failed", { err: err.message });
322
+ return [];
323
+ }
324
+ }
325
+ /**
326
+ * Persist the current catalogue to disk so the next tool-discovery
327
+ * load sees the tools. Best-effort — disk failures are logged at
328
+ * `warn` and otherwise swallowed.
329
+ */
330
+ function persistCachedDescriptorsToDisk(descriptors, logger) {
331
+ const tmp = `${CACHE_FILE}.${(0, node_crypto.randomUUID)()}.tmp`;
332
+ try {
333
+ (0, node_fs.mkdirSync)((0, node_path.dirname)(CACHE_FILE), { recursive: true });
334
+ (0, node_fs.writeFileSync)(tmp, JSON.stringify(descriptors));
335
+ (0, node_fs.renameSync)(tmp, CACHE_FILE);
336
+ } catch (err) {
337
+ logger.warn("[mcp-bundler] cache write failed", { err: err instanceof Error ? err.message : String(err) });
338
+ }
339
+ }
340
+ /**
281
341
  * Self-service tools the agent can call to add/remove MCP servers in
282
342
  * the Alfe bundler store. The agent gets the same surface the CLI's
283
343
  * `alfe mcp add/remove/list` commands expose — same IPC methods, same
@@ -285,8 +345,6 @@ function errorResult(message) {
285
345
  * `'integration:*'`) entries can't be clobbered.
286
346
  */
287
347
  function registerAgentMcpManagementTools(api) {
288
- if (!ipcClient) return;
289
- const ipc = ipcClient;
290
348
  api.registerTool({
291
349
  name: "alfe_mcp_list",
292
350
  label: "List Alfe MCP servers",
@@ -297,7 +355,7 @@ function registerAgentMcpManagementTools(api) {
297
355
  additionalProperties: false
298
356
  },
299
357
  async execute() {
300
- const response = await ipc.call("mcp.list_servers", {});
358
+ const response = await callIpc("mcp.list_servers", {});
301
359
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_list failed");
302
360
  return {
303
361
  content: [{
@@ -356,7 +414,7 @@ function registerAgentMcpManagementTools(api) {
356
414
  additionalProperties: false
357
415
  },
358
416
  async execute(_toolCallId, params) {
359
- const response = await ipc.call("mcp.add_server", params ?? {});
417
+ const response = await callIpc("mcp.add_server", params ?? {});
360
418
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_add failed");
361
419
  return {
362
420
  content: [{
@@ -381,7 +439,7 @@ function registerAgentMcpManagementTools(api) {
381
439
  additionalProperties: false
382
440
  },
383
441
  async execute(_toolCallId, params) {
384
- const response = await ipc.call("mcp.remove_server", params ?? {});
442
+ const response = await callIpc("mcp.remove_server", params ?? {});
385
443
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_remove failed");
386
444
  return {
387
445
  content: [{
@@ -401,15 +459,15 @@ function registerAgentMcpManagementTools(api) {
401
459
  function refreshCachedTools(api) {
402
460
  if (!ipcClient) return Promise.resolve();
403
461
  if (refreshInFlight) return refreshInFlight;
404
- const ipc = ipcClient;
405
462
  refreshInFlight = (async () => {
406
463
  try {
407
- const response = await ipc.call("mcp.list_tools", {});
464
+ const response = await callIpc("mcp.list_tools", {});
408
465
  if (!response.ok || !response.payload) {
409
466
  api.logger.debug("[mcp-bundler] list_tools failed", { err: response.error?.message ?? "unknown" });
410
467
  return;
411
468
  }
412
- cachedTools = response.payload.tools.map((t) => buildAgentTool(api, t, ipc));
469
+ cachedDescriptors = response.payload.tools;
470
+ persistCachedDescriptorsToDisk(cachedDescriptors, api.logger);
413
471
  } finally {
414
472
  refreshInFlight = null;
415
473
  }
@@ -422,10 +480,41 @@ const plugin = {
422
480
  description: "Proxies the daemon-hosted MCP bundler tools to the LLM",
423
481
  version: pkg.version,
424
482
  contracts: { tools: [
425
- "mcp__*",
426
483
  "alfe_mcp_list",
427
484
  "alfe_mcp_add",
428
- "alfe_mcp_remove"
485
+ "alfe_mcp_remove",
486
+ "atlassian-atlassian__atlassian_list_accounts",
487
+ "atlassian-atlassian__atlassian_check_connection",
488
+ "atlassian-atlassian__atlassian_refresh_token",
489
+ "notion-notion__notion_list_accounts",
490
+ "notion-notion__notion_check_connection",
491
+ "notion-notion__notion_refresh_token",
492
+ "github-github__github_list_accounts",
493
+ "github-github__github_check_connection",
494
+ "github-github__github_refresh_token",
495
+ "microsoft-microsoft__microsoft_list_accounts",
496
+ "microsoft-microsoft__microsoft_check_connection",
497
+ "microsoft-microsoft__microsoft_refresh_token",
498
+ "google-google__google_list_accounts",
499
+ "google-google__google_check_connection",
500
+ "google-google__google_refresh_token",
501
+ "slack-slack__slack_list_accounts",
502
+ "slack-slack__slack_check_connection",
503
+ "slack-slack__slack_refresh_token",
504
+ "discord-discord__discord_list_accounts",
505
+ "discord-discord__discord_check_connection",
506
+ "discord-discord__discord_refresh_token",
507
+ "mobile-mobile__mobile_list_accounts",
508
+ "mobile-mobile__mobile_check_connection",
509
+ "mobile-mobile__mobile_refresh_token",
510
+ "myob-myob__myob_list_accounts",
511
+ "myob-myob__myob_check_connection",
512
+ "myob-myob__myob_refresh_token",
513
+ "xero-xero__xero_list_accounts",
514
+ "xero-xero__xero_check_connection",
515
+ "xero-xero__xero_refresh_token",
516
+ "alfe-platform__session_status",
517
+ "alfe-platform__gateway_status"
429
518
  ] },
430
519
  activate(api) {
431
520
  const log = api.logger;
@@ -446,6 +535,13 @@ const plugin = {
446
535
  }
447
536
  const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));
448
537
  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");
538
+ cachedDescriptors = loadCachedDescriptorsFromDisk(log);
539
+ registerAgentMcpManagementTools(api);
540
+ if (!deferToNative) api.registerTool((ctx) => {
541
+ if (hasNativeOpenclawMcpServers(resolveLiveConfig(api, ctx))) return null;
542
+ refreshCachedTools(api);
543
+ return cachedDescriptors.map((d) => buildAgentTool(api, d));
544
+ }, { optional: true });
449
545
  const startService = () => {
450
546
  ipcClient = new McpIpcClient({
451
547
  logger: log,
@@ -458,17 +554,11 @@ const plugin = {
458
554
  }
459
555
  });
460
556
  ipcClient.start();
461
- registerAgentMcpManagementTools(api);
462
557
  if (deferToNative) {
463
558
  log.info(`[mcp-bundler] activated v${plugin.version} (management tools only — native bundleMcp owns runtime)`);
464
559
  return;
465
560
  }
466
- api.registerTool((ctx) => {
467
- if (hasNativeOpenclawMcpServers(resolveLiveConfig(api, ctx))) return null;
468
- refreshCachedTools(api);
469
- return cachedTools;
470
- }, { optional: true });
471
- log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client)`);
561
+ log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client, tools registered in activate)`);
472
562
  };
473
563
  const stopService = () => {
474
564
  log.info("[mcp-bundler] stopping");
@@ -476,8 +566,8 @@ const plugin = {
476
566
  ipcClient.stop();
477
567
  ipcClient = void 0;
478
568
  }
479
- cachedTools = [];
480
569
  refreshInFlight = null;
570
+ g[ACTIVATED_KEY] = false;
481
571
  return Promise.resolve();
482
572
  };
483
573
  api.registerService({
package/dist/plugin2.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { createConnection } from "node:net";
3
3
  import { homedir } from "node:os";
4
- import { join } from "node:path";
4
+ import { dirname, join } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
7
  //#region src/cli-backend-detect.ts
7
8
  /**
8
9
  * Returns true when openclaw.json#mcp.servers has at least one entry —
@@ -234,22 +235,24 @@ function isIpcResponse(value) {
234
235
  //#endregion
235
236
  //#region src/plugin.ts
236
237
  const pkg = createRequire(import.meta.url)("../package.json");
238
+ const CACHE_FILE = join(homedir(), ".alfe", "mcp", "openclaw-bundler-cache.json");
237
239
  let ipcClient;
238
- let cachedTools = [];
240
+ let cachedDescriptors = [];
239
241
  let refreshInFlight = null;
240
242
  const ACTIVATED_KEY = "__alfeMcpBundlerPluginActivated";
241
243
  const g = globalThis;
242
244
  function resolveLiveConfig(api, ctx) {
243
245
  return ctx?.runtimeConfig ?? ctx?.getRuntimeConfig?.() ?? api.runtime?.config?.current?.() ?? ctx?.config ?? api.config;
244
246
  }
245
- function buildAgentTool(api, tool, ipc) {
247
+ function buildAgentTool(api, tool) {
246
248
  return {
247
249
  name: tool.prefixed,
248
250
  label: tool.label,
249
251
  description: tool.description || `MCP tool ${tool.original} from server ${tool.server}`,
250
252
  parameters: tool.parameters,
251
253
  async execute(_toolCallId, params) {
252
- const response = await ipc.call("mcp.call_tool", {
254
+ if (!ipcClient?.isConnected()) return errorResult(`mcp ${tool.prefixed}: bundler IPC not connected yet. Try again in a moment.`);
255
+ const response = await ipcClient.call("mcp.call_tool", {
253
256
  name: tool.prefixed,
254
257
  args: params
255
258
  });
@@ -278,6 +281,63 @@ function errorResult(message) {
278
281
  };
279
282
  }
280
283
  /**
284
+ * Async-aware shim around the module-scoped `ipcClient`. The management
285
+ * tools and dynamic MCP tools registered in `activate()` execute LATER
286
+ * (when the agent invokes them), but `ipcClient` is only set inside
287
+ * `startService`. The `?.` chains return useful errors when the agent
288
+ * tries to call a tool before the IPC is ready, instead of throwing.
289
+ */
290
+ async function callIpc(method, params = {}) {
291
+ if (!ipcClient) return {
292
+ id: "",
293
+ ok: false,
294
+ error: {
295
+ code: "NOT_CONNECTED",
296
+ message: "Bundler IPC client not initialized yet"
297
+ }
298
+ };
299
+ return ipcClient.call(method, params);
300
+ }
301
+ /**
302
+ * Load the last-known tool catalogue from disk. Used at `activate()`
303
+ * time so the agent's tool list contains real tools even in tool-
304
+ * discovery mode (where `service.start` — and therefore the IPC
305
+ * connection — never runs). Silently falls back to an empty list on
306
+ * any read/parse failure so a first-run agent (no cache file yet)
307
+ * still loads cleanly.
308
+ */
309
+ function isValidDescriptor(value) {
310
+ if (typeof value !== "object" || value === null) return false;
311
+ const v = value;
312
+ return typeof v.prefixed === "string" && typeof v.original === "string" && typeof v.server === "string" && typeof v.parameters === "object" && v.parameters !== null;
313
+ }
314
+ function loadCachedDescriptorsFromDisk(logger) {
315
+ try {
316
+ const raw = readFileSync(CACHE_FILE, "utf-8");
317
+ const parsed = JSON.parse(raw);
318
+ if (!Array.isArray(parsed)) return [];
319
+ return parsed.filter(isValidDescriptor);
320
+ } catch (err) {
321
+ if (err instanceof Error && err.code !== "ENOENT") logger.debug("[mcp-bundler] cache read failed", { err: err.message });
322
+ return [];
323
+ }
324
+ }
325
+ /**
326
+ * Persist the current catalogue to disk so the next tool-discovery
327
+ * load sees the tools. Best-effort — disk failures are logged at
328
+ * `warn` and otherwise swallowed.
329
+ */
330
+ function persistCachedDescriptorsToDisk(descriptors, logger) {
331
+ const tmp = `${CACHE_FILE}.${randomUUID()}.tmp`;
332
+ try {
333
+ mkdirSync(dirname(CACHE_FILE), { recursive: true });
334
+ writeFileSync(tmp, JSON.stringify(descriptors));
335
+ renameSync(tmp, CACHE_FILE);
336
+ } catch (err) {
337
+ logger.warn("[mcp-bundler] cache write failed", { err: err instanceof Error ? err.message : String(err) });
338
+ }
339
+ }
340
+ /**
281
341
  * Self-service tools the agent can call to add/remove MCP servers in
282
342
  * the Alfe bundler store. The agent gets the same surface the CLI's
283
343
  * `alfe mcp add/remove/list` commands expose — same IPC methods, same
@@ -285,8 +345,6 @@ function errorResult(message) {
285
345
  * `'integration:*'`) entries can't be clobbered.
286
346
  */
287
347
  function registerAgentMcpManagementTools(api) {
288
- if (!ipcClient) return;
289
- const ipc = ipcClient;
290
348
  api.registerTool({
291
349
  name: "alfe_mcp_list",
292
350
  label: "List Alfe MCP servers",
@@ -297,7 +355,7 @@ function registerAgentMcpManagementTools(api) {
297
355
  additionalProperties: false
298
356
  },
299
357
  async execute() {
300
- const response = await ipc.call("mcp.list_servers", {});
358
+ const response = await callIpc("mcp.list_servers", {});
301
359
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_list failed");
302
360
  return {
303
361
  content: [{
@@ -356,7 +414,7 @@ function registerAgentMcpManagementTools(api) {
356
414
  additionalProperties: false
357
415
  },
358
416
  async execute(_toolCallId, params) {
359
- const response = await ipc.call("mcp.add_server", params ?? {});
417
+ const response = await callIpc("mcp.add_server", params ?? {});
360
418
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_add failed");
361
419
  return {
362
420
  content: [{
@@ -381,7 +439,7 @@ function registerAgentMcpManagementTools(api) {
381
439
  additionalProperties: false
382
440
  },
383
441
  async execute(_toolCallId, params) {
384
- const response = await ipc.call("mcp.remove_server", params ?? {});
442
+ const response = await callIpc("mcp.remove_server", params ?? {});
385
443
  if (!response.ok) return errorResult(response.error?.message ?? "mcp_remove failed");
386
444
  return {
387
445
  content: [{
@@ -401,15 +459,15 @@ function registerAgentMcpManagementTools(api) {
401
459
  function refreshCachedTools(api) {
402
460
  if (!ipcClient) return Promise.resolve();
403
461
  if (refreshInFlight) return refreshInFlight;
404
- const ipc = ipcClient;
405
462
  refreshInFlight = (async () => {
406
463
  try {
407
- const response = await ipc.call("mcp.list_tools", {});
464
+ const response = await callIpc("mcp.list_tools", {});
408
465
  if (!response.ok || !response.payload) {
409
466
  api.logger.debug("[mcp-bundler] list_tools failed", { err: response.error?.message ?? "unknown" });
410
467
  return;
411
468
  }
412
- cachedTools = response.payload.tools.map((t) => buildAgentTool(api, t, ipc));
469
+ cachedDescriptors = response.payload.tools;
470
+ persistCachedDescriptorsToDisk(cachedDescriptors, api.logger);
413
471
  } finally {
414
472
  refreshInFlight = null;
415
473
  }
@@ -422,10 +480,41 @@ const plugin = {
422
480
  description: "Proxies the daemon-hosted MCP bundler tools to the LLM",
423
481
  version: pkg.version,
424
482
  contracts: { tools: [
425
- "mcp__*",
426
483
  "alfe_mcp_list",
427
484
  "alfe_mcp_add",
428
- "alfe_mcp_remove"
485
+ "alfe_mcp_remove",
486
+ "atlassian-atlassian__atlassian_list_accounts",
487
+ "atlassian-atlassian__atlassian_check_connection",
488
+ "atlassian-atlassian__atlassian_refresh_token",
489
+ "notion-notion__notion_list_accounts",
490
+ "notion-notion__notion_check_connection",
491
+ "notion-notion__notion_refresh_token",
492
+ "github-github__github_list_accounts",
493
+ "github-github__github_check_connection",
494
+ "github-github__github_refresh_token",
495
+ "microsoft-microsoft__microsoft_list_accounts",
496
+ "microsoft-microsoft__microsoft_check_connection",
497
+ "microsoft-microsoft__microsoft_refresh_token",
498
+ "google-google__google_list_accounts",
499
+ "google-google__google_check_connection",
500
+ "google-google__google_refresh_token",
501
+ "slack-slack__slack_list_accounts",
502
+ "slack-slack__slack_check_connection",
503
+ "slack-slack__slack_refresh_token",
504
+ "discord-discord__discord_list_accounts",
505
+ "discord-discord__discord_check_connection",
506
+ "discord-discord__discord_refresh_token",
507
+ "mobile-mobile__mobile_list_accounts",
508
+ "mobile-mobile__mobile_check_connection",
509
+ "mobile-mobile__mobile_refresh_token",
510
+ "myob-myob__myob_list_accounts",
511
+ "myob-myob__myob_check_connection",
512
+ "myob-myob__myob_refresh_token",
513
+ "xero-xero__xero_list_accounts",
514
+ "xero-xero__xero_check_connection",
515
+ "xero-xero__xero_refresh_token",
516
+ "alfe-platform__session_status",
517
+ "alfe-platform__gateway_status"
429
518
  ] },
430
519
  activate(api) {
431
520
  const log = api.logger;
@@ -446,6 +535,13 @@ const plugin = {
446
535
  }
447
536
  const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));
448
537
  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");
538
+ cachedDescriptors = loadCachedDescriptorsFromDisk(log);
539
+ registerAgentMcpManagementTools(api);
540
+ if (!deferToNative) api.registerTool((ctx) => {
541
+ if (hasNativeOpenclawMcpServers(resolveLiveConfig(api, ctx))) return null;
542
+ refreshCachedTools(api);
543
+ return cachedDescriptors.map((d) => buildAgentTool(api, d));
544
+ }, { optional: true });
449
545
  const startService = () => {
450
546
  ipcClient = new McpIpcClient({
451
547
  logger: log,
@@ -458,17 +554,11 @@ const plugin = {
458
554
  }
459
555
  });
460
556
  ipcClient.start();
461
- registerAgentMcpManagementTools(api);
462
557
  if (deferToNative) {
463
558
  log.info(`[mcp-bundler] activated v${plugin.version} (management tools only — native bundleMcp owns runtime)`);
464
559
  return;
465
560
  }
466
- api.registerTool((ctx) => {
467
- if (hasNativeOpenclawMcpServers(resolveLiveConfig(api, ctx))) return null;
468
- refreshCachedTools(api);
469
- return cachedTools;
470
- }, { optional: true });
471
- log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client)`);
561
+ log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client, tools registered in activate)`);
472
562
  };
473
563
  const stopService = () => {
474
564
  log.info("[mcp-bundler] stopping");
@@ -476,8 +566,8 @@ const plugin = {
476
566
  ipcClient.stop();
477
567
  ipcClient = void 0;
478
568
  }
479
- cachedTools = [];
480
569
  refreshInFlight = null;
570
+ g[ACTIVATED_KEY] = false;
481
571
  return Promise.resolve();
482
572
  };
483
573
  api.registerService({
@@ -1 +1 @@
1
- {"version":3,"file":"plugin2.js","names":[],"sources":["../src/cli-backend-detect.ts","../src/ipc-client.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Decide whether to defer to OpenClaw's native `bundleMcp:true` path for\n * this run. Pre-May-2026 this gated on backend type (claude-cli /\n * codex-cli), but the actual hazard was double-registration: when\n * `openclaw.json#mcp.servers` had entries AND the plugin registered the\n * same tools via `api.registerTool`, claude-cli's bundleMcp would spawn\n * its own copy and OpenClaw would suffix-disambiguate the duplicates.\n *\n * The single-source-of-truth refactor stops mirror-writing the alfe\n * store into openclaw.json, so `mcp.servers` is empty in practice. The\n * native path on claude-cli has nothing to spawn, our plugin's IPC path\n * is the only one in play, and we register on every backend.\n *\n * The escape hatch survives: a user who hand-edits `mcp.servers` in\n * openclaw.json (private dev MCP, etc.) gets the native path back —\n * this function returns true, the plugin no-ops, and claude-cli/codex-cli\n * spawn that entry themselves.\n */\n\ninterface ConfigLike {\n mcp?: {\n servers?: Record<string, unknown>;\n };\n}\n\n/**\n * Returns true when openclaw.json#mcp.servers has at least one entry —\n * signalling that the native bundleMcp path is in play for THIS run\n * (regardless of backend) and the plugin should defer to it.\n *\n * Empty object / missing key → false → plugin registers IPC-proxied tools.\n */\nexport function hasNativeOpenclawMcpServers(cfg: ConfigLike | undefined): boolean {\n if (!cfg) return false;\n const servers = cfg.mcp?.servers;\n if (!servers || typeof servers !== 'object') return false;\n return Object.keys(servers).length > 0;\n}\n\n/**\n * Back-compat alias for callers still on the old name. Internal — drop\n * once we cut the next major.\n */\nexport const detectNativeBundleMcp = hasNativeOpenclawMcpServers;\n","/**\n * Minimal newline-delimited-JSON IPC client over Unix socket. Talks to\n * the alfe-gateway daemon at `~/.alfe/gateway.sock` so this plugin can\n * proxy `mcp.list_tools` / `mcp.call_tool` requests to the daemon-hosted\n * `McpBundler` rather than spawning its own children.\n *\n * Why minimal: the @alfe.ai/openclaw package already ships a full IPC\n * client with reconnect + event handling, but pulling it in would create\n * a (semantically) circular dep — plugin importing from its host. This\n * file is ~120 lines and only does what the bundler plugin needs.\n *\n * The daemon's IPC server (`packages/gateway/src/ipc-server.ts`) speaks:\n * Request: { type: 'req', id, method, params }\n * Response: { id, ok, payload?, error? }\n * Event: { type: 'event', event, payload } ← ignored here, we never subscribe.\n */\nimport { createConnection, type Socket } from 'node:net';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nconst DEFAULT_SOCKET_PATH = join(homedir(), '.alfe', 'gateway.sock');\nconst RECONNECT_MIN_MS = 500;\nconst RECONNECT_MAX_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nexport interface IpcResponse<T = unknown> {\n id: string;\n ok: boolean;\n payload?: T;\n error?: { code: string; message: string };\n}\n\ninterface Logger {\n debug(msg: string, ctx?: Record<string, unknown>): void;\n info(msg: string, ctx?: Record<string, unknown>): void;\n warn(msg: string, ctx?: Record<string, unknown>): void;\n error(msg: string, ctx?: Record<string, unknown>): void;\n}\n\nexport interface McpIpcClientOptions {\n socketPath?: string;\n logger?: Logger;\n /** Plugin identity broadcast to the daemon on connect. */\n plugin: { name: string; version: string };\n /**\n * Fires every time the socket transitions from disconnected to\n * connected — i.e. on the initial connect AND on every reconnect after\n * the daemon restarts. Invoked AFTER `connected = true` is set so\n * `call(...)` from inside the handler will actually round-trip the\n * daemon (the previous shape called `refreshCachedTools` synchronously\n * inside the plugin's `startService` while `connected` was still false,\n * which silently returned `NOT_CONNECTED` and left the tool cache\n * permanently empty on the first session — see plugin.ts comment for\n * the QA Tester repro).\n *\n * Contract:\n * - Treated as fire-and-forget. The IPC client does NOT await the\n * handler's return value.\n * - Synchronous throws are caught + logged at warn — they will not\n * tear down the IPC client.\n * - If a handler returns a Promise that rejects later, the rejection\n * surfaces as an unhandled promise rejection. Wrap with `void` or\n * handle internally if you don't want that behaviour. The wired\n * handler in `plugin.ts` does exactly that:\n * `() => { void refreshCachedTools(api); }`.\n */\n onConnect?: () => void;\n}\n\ninterface PendingRequest {\n resolve: (response: IpcResponse) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Plugin-side IPC client. Single instance per plugin lifecycle: start on\n * `registerService.start`, stop on `registerService.stop`. Calls\n * before the socket is connected return a typed `NOT_CONNECTED` response\n * so the plugin can fall back to \"no tools yet\" rather than throwing.\n */\nexport class McpIpcClient {\n private socket: Socket | null = null;\n private buffer = '';\n private backoffMs = RECONNECT_MIN_MS;\n private closed = false;\n private connected = false;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private pending = new Map<string, PendingRequest>();\n private readonly socketPath: string;\n private readonly logger?: Logger;\n private readonly plugin: { name: string; version: string };\n private readonly onConnect?: () => void;\n\n constructor(opts: McpIpcClientOptions) {\n this.socketPath = opts.socketPath ?? DEFAULT_SOCKET_PATH;\n this.logger = opts.logger;\n this.plugin = opts.plugin;\n this.onConnect = opts.onConnect;\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n start(): void {\n this.closed = false;\n this.openSocket();\n }\n\n stop(): void {\n this.closed = true;\n this.clearReconnectTimer();\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer);\n pending.resolve({\n id,\n ok: false,\n error: { code: 'CLIENT_STOPPED', message: 'IPC client stopped' },\n });\n }\n this.pending.clear();\n if (this.socket) {\n try {\n this.socket.end();\n } catch {\n // ignore — socket may already be in a half-closed state\n }\n this.socket = null;\n }\n this.connected = false;\n }\n\n async call<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n ): Promise<IpcResponse<T>> {\n if (!this.connected || !this.socket) {\n return {\n id: '',\n ok: false,\n error: { code: 'NOT_CONNECTED', message: 'Not connected to alfe-gateway' },\n };\n }\n const id = randomUUID();\n return new Promise<IpcResponse<T>>((resolve) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n resolve({\n id,\n ok: false,\n error: { code: 'TIMEOUT', message: `${method} timed out after ${String(REQUEST_TIMEOUT_MS)}ms` },\n });\n }, REQUEST_TIMEOUT_MS);\n this.pending.set(id, { resolve: resolve as (r: IpcResponse) => void, timer });\n try {\n this.socket?.write(`${JSON.stringify({ type: 'req', id, method, params })}\\n`);\n } catch (err) {\n clearTimeout(timer);\n this.pending.delete(id);\n resolve({\n id,\n ok: false,\n error: {\n code: 'SEND_FAILED',\n message: err instanceof Error ? err.message : String(err),\n },\n });\n }\n });\n }\n\n private openSocket(): void {\n if (this.closed) return;\n this.buffer = '';\n const socket = createConnection(this.socketPath, () => {\n this.logger?.info('[mcp-bundler/ipc] connected', { socket: this.socketPath });\n this.connected = true;\n this.backoffMs = RECONNECT_MIN_MS;\n // Best-effort register. We don't await the response — the daemon\n // routes mcp.* methods even on unregistered connections, so the\n // tool-list / tool-call path doesn't need register to complete.\n void this.call('register', {\n name: this.plugin.name,\n version: this.plugin.version,\n protocolVersion: 1,\n capabilities: ['mcp.list_tools', 'mcp.call_tool'],\n pid: process.pid,\n });\n // Notify caller AFTER `connected = true` so any `.call(...)` issued\n // from the handler (e.g. plugin.ts's tool-cache refresh) actually\n // makes it onto the wire instead of getting the NOT_CONNECTED\n // short-circuit at the top of `call()`.\n if (this.onConnect) {\n try {\n this.onConnect();\n } catch (err) {\n this.logger?.warn('[mcp-bundler/ipc] onConnect handler threw', {\n err: err instanceof Error ? err.message : String(err),\n });\n }\n }\n });\n this.socket = socket;\n\n socket.on('data', (data: Buffer) => {\n this.buffer += data.toString();\n this.processBuffer();\n });\n\n socket.on('close', () => {\n this.connected = false;\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer);\n pending.resolve({\n id,\n ok: false,\n error: { code: 'DISCONNECTED', message: 'Connection closed' },\n });\n }\n this.pending.clear();\n this.socket = null;\n if (!this.closed) this.scheduleReconnect();\n });\n\n socket.on('error', (err: Error) => {\n this.logger?.debug('[mcp-bundler/ipc] socket error', { err: err.message });\n // 'close' fires after 'error'; reconnect handled there.\n });\n }\n\n private processBuffer(): void {\n let idx: number;\n while ((idx = this.buffer.indexOf('\\n')) !== -1) {\n const line = this.buffer.slice(0, idx).trim();\n this.buffer = this.buffer.slice(idx + 1);\n if (!line) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!isIpcResponse(parsed)) continue;\n const pending = this.pending.get(parsed.id);\n if (!pending) continue;\n clearTimeout(pending.timer);\n this.pending.delete(parsed.id);\n pending.resolve(parsed);\n }\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer) return;\n const delay = this.backoffMs;\n this.backoffMs = Math.min(this.backoffMs * 2, RECONNECT_MAX_MS);\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.openSocket();\n }, delay);\n if (typeof this.reconnectTimer === 'object' && 'unref' in this.reconnectTimer) {\n (this.reconnectTimer as { unref: () => void }).unref();\n }\n }\n\n private clearReconnectTimer(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n}\n\nfunction isIpcResponse(value: unknown): value is IpcResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'id' in value &&\n 'ok' in value &&\n typeof (value as { id: unknown }).id === 'string'\n );\n}\n","/**\n * @alfe.ai/openclaw-mcp-bundler — OpenClaw plugin entry\n *\n * Thin IPC client over the alfe-gateway daemon. The daemon hosts the\n * actual `McpBundler` (children + tool catalog), this plugin forwards\n * the OpenClaw tool-surface to that bundler via `mcp.list_tools` and\n * `mcp.call_tool` over `~/.alfe/gateway.sock`.\n *\n * Works on every LLM backend — `api.registerTool` registrations reach\n * MiniMax/Mistral/generic AND claude-cli/codex-cli. The previous\n * blanket no-op on claude-cli was load-bearing only when the same MCP\n * was ALSO in `openclaw.json#mcp.servers` (double registration);\n * since the alfe store no longer mirror-writes there, the native\n * bundleMcp path has nothing to spawn and we can register on every\n * backend uniformly.\n *\n * Escape hatch: a user who hand-adds an entry to\n * `openclaw.json#mcp.servers` for a private dev MCP gets the native\n * path back for that run — `hasNativeOpenclawMcpServers` returns true,\n * we no-op, claude-cli/codex-cli spawn that entry themselves.\n */\n\nimport type { McpToolDescriptor, McpToolCallResult } from '@alfe.ai/mcp-bundler';\nimport { hasNativeOpenclawMcpServers } from './cli-backend-detect.js';\nimport { McpIpcClient, type IpcResponse } from './ipc-client.js';\nimport { createRequire } from 'node:module';\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\n// ── Type shims for the OpenClaw plugin SDK ──────────────────────\n// We avoid a hard dep on the SDK to keep this plugin loadable across\n// OpenClaw versions; the surface used here is stable.\n\ninterface Logger {\n debug(msg: string, ...args: unknown[]): void;\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n}\n\ninterface ConfigSnapshot {\n agents?: { defaults?: { model?: string; cliBackends?: Record<string, unknown> } };\n mcp?: { servers?: Record<string, unknown> };\n}\n\ninterface AgentToolResult {\n content: Record<string, unknown>[];\n details?: Record<string, unknown>;\n}\n\ninterface AgentTool {\n name: string;\n label?: string;\n description: string;\n parameters: Record<string, unknown>;\n execute: (\n toolCallId: string,\n params: unknown,\n signal?: AbortSignal,\n onUpdate?: (update: unknown) => void,\n ) => Promise<AgentToolResult>;\n}\n\ninterface ToolFactoryContext {\n config?: ConfigSnapshot;\n runtimeConfig?: ConfigSnapshot;\n getRuntimeConfig?: () => ConfigSnapshot | undefined;\n}\n\ninterface OpenClawPluginService {\n id: string;\n start: (ctx?: unknown) => void | Promise<void>;\n stop?: (ctx?: unknown) => void | Promise<void>;\n}\n\ninterface OpenClawPluginApi {\n logger: Logger;\n config?: ConfigSnapshot;\n pluginConfig?: { disabled?: boolean };\n registrationMode?: string;\n runtime?: { config?: { current?: () => ConfigSnapshot | undefined } };\n registerTool: (\n tool: AgentTool | ((ctx: ToolFactoryContext) => AgentTool | AgentTool[] | null | undefined),\n opts?: { name?: string; names?: string[]; optional?: boolean },\n ) => void;\n registerService?: (service: OpenClawPluginService) => void;\n}\n\n// ── Plugin state ───────────────────────────────────────────────\nlet ipcClient: McpIpcClient | undefined;\nlet cachedTools: AgentTool[] = [];\nlet refreshInFlight: Promise<void> | null = null;\nconst ACTIVATED_KEY = '__alfeMcpBundlerPluginActivated';\nconst g = globalThis as Record<string, unknown>;\n\nfunction resolveLiveConfig(api: OpenClawPluginApi, ctx?: ToolFactoryContext): ConfigSnapshot | undefined {\n // Prefer most-recent → ctx live → ctx snapshot → api.runtime live → api.config snapshot\n return (\n ctx?.runtimeConfig ??\n ctx?.getRuntimeConfig?.() ??\n api.runtime?.config?.current?.() ??\n ctx?.config ??\n api.config\n );\n}\n\nfunction buildAgentTool(api: OpenClawPluginApi, tool: McpToolDescriptor, ipc: McpIpcClient): AgentTool {\n return {\n name: tool.prefixed,\n label: tool.label,\n description: tool.description || `MCP tool ${tool.original} from server ${tool.server}`,\n // OpenClaw expects a JSON-Schema-shaped object. The MCP server already\n // gives us one in `tool.parameters`. Pass through unchanged.\n parameters: tool.parameters,\n async execute(_toolCallId, params) {\n const response = await ipc.call<McpToolCallResult>('mcp.call_tool', {\n name: tool.prefixed,\n args: params,\n });\n if (!response.ok || !response.payload) {\n api.logger.warn('[mcp-bundler] tool call failed', {\n tool: tool.prefixed,\n err: response.error?.message ?? 'unknown error',\n });\n return errorResult(\n response.error?.message\n ? `mcp ${tool.prefixed}: ${response.error.message}`\n : `mcp ${tool.prefixed}: call failed`,\n );\n }\n const result = response.payload;\n return {\n content: result.content,\n details: { isError: result.isError ?? false },\n };\n },\n };\n}\n\nfunction errorResult(message: string): AgentToolResult {\n return {\n content: [{ type: 'text', text: message }],\n details: { isError: true },\n };\n}\n\n/**\n * 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 if (!ipcClient) return;\n const ipc = ipcClient;\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 ipc.call<{ 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 ipc.call<{ 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 ipc.call<{ removed: boolean }>(\n 'mcp.remove_server',\n (params ?? {}) as Record<string, unknown>,\n );\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_remove failed');\n }\n const removed = response.payload?.removed ?? false;\n return {\n content: [\n {\n type: 'text',\n text: removed\n ? `Removed MCP server. Its tools will disappear on the next turn.`\n : `No MCP server matched — nothing to remove.`,\n },\n ],\n details: { isError: false },\n };\n },\n });\n}\n\n/**\n * Pull the latest tool catalog from the daemon-hosted bundler. Coalesces\n * concurrent calls so the LLM hitting the factory many times in a tight\n * loop only triggers one in-flight IPC request.\n */\nfunction refreshCachedTools(api: OpenClawPluginApi): Promise<void> {\n if (!ipcClient) return Promise.resolve();\n if (refreshInFlight) return refreshInFlight;\n const ipc = ipcClient;\n refreshInFlight = (async () => {\n try {\n const response = await ipc.call<{ 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 cachedTools = response.payload.tools.map((t) => buildAgentTool(api, t, ipc));\n } finally {\n refreshInFlight = null;\n }\n })();\n return refreshInFlight;\n}\n\nconst plugin = {\n id: '@alfe.ai/openclaw-mcp-bundler',\n name: 'Alfe MCP Bundler',\n description: 'Proxies the daemon-hosted MCP bundler tools to the LLM',\n version: pkg.version,\n contracts: {\n // `mcp__*` covers the dynamic per-server proxied tools; the three\n // `alfe_mcp_*` entries are the agent self-service surface for\n // adding/removing MCP servers via the alfe bundler store.\n tools: ['mcp__*', 'alfe_mcp_list', 'alfe_mcp_add', 'alfe_mcp_remove'],\n },\n\n activate(api: OpenClawPluginApi) {\n const log = api.logger;\n\n if (g[ACTIVATED_KEY] === true) {\n log.debug('[mcp-bundler] already activated — skipping duplicate');\n return;\n }\n g[ACTIVATED_KEY] = true;\n\n if (api.pluginConfig?.disabled) {\n g[ACTIVATED_KEY] = false;\n log.info('[mcp-bundler] disabled via plugin config — no-op');\n return;\n }\n\n if (!api.registerService) {\n g[ACTIVATED_KEY] = false;\n log.warn('[mcp-bundler] api.registerService unavailable — plugin is a no-op on this runtime');\n return;\n }\n\n const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));\n if (deferToNative) {\n log.info(\n '[mcp-bundler] openclaw.json#mcp.servers has entries — MCP-proxy factory will defer to native bundleMcp, but agent self-service tools (alfe_mcp_*) stay available',\n );\n }\n\n const startService = (): void => {\n ipcClient = new McpIpcClient({\n logger: log,\n plugin: { name: plugin.id, version: plugin.version },\n // Refresh the tool cache the moment the IPC connection lands.\n // Previously we kicked off the first refresh synchronously below,\n // but `ipcClient.start()` only initiates the socket connect —\n // `connected` flips true ~50-300ms later inside the createConnection\n // callback. That left the first `mcp.list_tools` call returning\n // NOT_CONNECTED, `cachedTools` empty, and the factory permanently\n // shipping `[]` to OpenClaw (the QA Tester atlassian-tools-missing\n // symptom we traced 2026-06-09). Hooking onConnect makes the first\n // refresh fire AFTER the socket lands, and re-fires on every\n // reconnect after a daemon restart.\n onConnect: () => {\n void refreshCachedTools(api);\n },\n });\n ipcClient.start();\n\n // Agent self-service tools — always available, regardless of the\n // native-bundleMcp escape hatch. The alfe store is independent of\n // openclaw.json so agents can manage their MCP roster in either\n // mode.\n registerAgentMcpManagementTools(api);\n\n if (deferToNative) {\n log.info(`[mcp-bundler] activated v${plugin.version} (management tools only — native bundleMcp owns runtime)`);\n return;\n }\n\n // Belt + suspenders: the per-turn factory below also kicks off a\n // background refresh on every invocation. The `onConnect` hook\n // above guarantees `cachedTools` is populated by the time openclaw\n // first asks for the tool list; this keeps it fresh as new servers\n // are added to the daemon-hosted bundler mid-session.\n\n api.registerTool((ctx: ToolFactoryContext) => {\n const live = resolveLiveConfig(api, ctx);\n if (hasNativeOpenclawMcpServers(live)) return null;\n // Best-effort background refresh so each turn picks up any new\n // entries the daemon's bundler has discovered.\n void refreshCachedTools(api);\n return cachedTools;\n }, { optional: true });\n\n log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client)`);\n };\n\n const stopService = (): Promise<void> => {\n log.info('[mcp-bundler] stopping');\n if (ipcClient) {\n ipcClient.stop();\n ipcClient = undefined;\n }\n cachedTools = [];\n refreshInFlight = null;\n return Promise.resolve();\n };\n\n api.registerService({\n id: 'alfe-mcp-bundler',\n start: () => { startService(); },\n stop: async () => { await stopService(); },\n });\n },\n\n deactivate(api: OpenClawPluginApi) {\n g[ACTIVATED_KEY] = false;\n api.logger.debug('[mcp-bundler] deactivating');\n },\n};\n\nexport { type IpcResponse };\nexport default plugin;\n"],"mappings":"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"}
1
+ {"version":3,"file":"plugin2.js","names":[],"sources":["../src/cli-backend-detect.ts","../src/ipc-client.ts","../src/plugin.ts"],"sourcesContent":["/**\n * Decide whether to defer to OpenClaw's native `bundleMcp:true` path for\n * this run. Pre-May-2026 this gated on backend type (claude-cli /\n * codex-cli), but the actual hazard was double-registration: when\n * `openclaw.json#mcp.servers` had entries AND the plugin registered the\n * same tools via `api.registerTool`, claude-cli's bundleMcp would spawn\n * its own copy and OpenClaw would suffix-disambiguate the duplicates.\n *\n * The single-source-of-truth refactor stops mirror-writing the alfe\n * store into openclaw.json, so `mcp.servers` is empty in practice. The\n * native path on claude-cli has nothing to spawn, our plugin's IPC path\n * is the only one in play, and we register on every backend.\n *\n * The escape hatch survives: a user who hand-edits `mcp.servers` in\n * openclaw.json (private dev MCP, etc.) gets the native path back —\n * this function returns true, the plugin no-ops, and claude-cli/codex-cli\n * spawn that entry themselves.\n */\n\ninterface ConfigLike {\n mcp?: {\n servers?: Record<string, unknown>;\n };\n}\n\n/**\n * Returns true when openclaw.json#mcp.servers has at least one entry —\n * signalling that the native bundleMcp path is in play for THIS run\n * (regardless of backend) and the plugin should defer to it.\n *\n * Empty object / missing key → false → plugin registers IPC-proxied tools.\n */\nexport function hasNativeOpenclawMcpServers(cfg: ConfigLike | undefined): boolean {\n if (!cfg) return false;\n const servers = cfg.mcp?.servers;\n if (!servers || typeof servers !== 'object') return false;\n return Object.keys(servers).length > 0;\n}\n\n/**\n * Back-compat alias for callers still on the old name. Internal — drop\n * once we cut the next major.\n */\nexport const detectNativeBundleMcp = hasNativeOpenclawMcpServers;\n","/**\n * Minimal newline-delimited-JSON IPC client over Unix socket. Talks to\n * the alfe-gateway daemon at `~/.alfe/gateway.sock` so this plugin can\n * proxy `mcp.list_tools` / `mcp.call_tool` requests to the daemon-hosted\n * `McpBundler` rather than spawning its own children.\n *\n * Why minimal: the @alfe.ai/openclaw package already ships a full IPC\n * client with reconnect + event handling, but pulling it in would create\n * a (semantically) circular dep — plugin importing from its host. This\n * file is ~120 lines and only does what the bundler plugin needs.\n *\n * The daemon's IPC server (`packages/gateway/src/ipc-server.ts`) speaks:\n * Request: { type: 'req', id, method, params }\n * Response: { id, ok, payload?, error? }\n * Event: { type: 'event', event, payload } ← ignored here, we never subscribe.\n */\nimport { createConnection, type Socket } from 'node:net';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\n\nconst DEFAULT_SOCKET_PATH = join(homedir(), '.alfe', 'gateway.sock');\nconst RECONNECT_MIN_MS = 500;\nconst RECONNECT_MAX_MS = 10_000;\nconst REQUEST_TIMEOUT_MS = 30_000;\n\nexport interface IpcResponse<T = unknown> {\n id: string;\n ok: boolean;\n payload?: T;\n error?: { code: string; message: string };\n}\n\ninterface Logger {\n debug(msg: string, ctx?: Record<string, unknown>): void;\n info(msg: string, ctx?: Record<string, unknown>): void;\n warn(msg: string, ctx?: Record<string, unknown>): void;\n error(msg: string, ctx?: Record<string, unknown>): void;\n}\n\nexport interface McpIpcClientOptions {\n socketPath?: string;\n logger?: Logger;\n /** Plugin identity broadcast to the daemon on connect. */\n plugin: { name: string; version: string };\n /**\n * Fires every time the socket transitions from disconnected to\n * connected — i.e. on the initial connect AND on every reconnect after\n * the daemon restarts. Invoked AFTER `connected = true` is set so\n * `call(...)` from inside the handler will actually round-trip the\n * daemon (the previous shape called `refreshCachedTools` synchronously\n * inside the plugin's `startService` while `connected` was still false,\n * which silently returned `NOT_CONNECTED` and left the tool cache\n * permanently empty on the first session — see plugin.ts comment for\n * the QA Tester repro).\n *\n * Contract:\n * - Treated as fire-and-forget. The IPC client does NOT await the\n * handler's return value.\n * - Synchronous throws are caught + logged at warn — they will not\n * tear down the IPC client.\n * - If a handler returns a Promise that rejects later, the rejection\n * surfaces as an unhandled promise rejection. Wrap with `void` or\n * handle internally if you don't want that behaviour. The wired\n * handler in `plugin.ts` does exactly that:\n * `() => { void refreshCachedTools(api); }`.\n */\n onConnect?: () => void;\n}\n\ninterface PendingRequest {\n resolve: (response: IpcResponse) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Plugin-side IPC client. Single instance per plugin lifecycle: start on\n * `registerService.start`, stop on `registerService.stop`. Calls\n * before the socket is connected return a typed `NOT_CONNECTED` response\n * so the plugin can fall back to \"no tools yet\" rather than throwing.\n */\nexport class McpIpcClient {\n private socket: Socket | null = null;\n private buffer = '';\n private backoffMs = RECONNECT_MIN_MS;\n private closed = false;\n private connected = false;\n private reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n private pending = new Map<string, PendingRequest>();\n private readonly socketPath: string;\n private readonly logger?: Logger;\n private readonly plugin: { name: string; version: string };\n private readonly onConnect?: () => void;\n\n constructor(opts: McpIpcClientOptions) {\n this.socketPath = opts.socketPath ?? DEFAULT_SOCKET_PATH;\n this.logger = opts.logger;\n this.plugin = opts.plugin;\n this.onConnect = opts.onConnect;\n }\n\n isConnected(): boolean {\n return this.connected;\n }\n\n start(): void {\n this.closed = false;\n this.openSocket();\n }\n\n stop(): void {\n this.closed = true;\n this.clearReconnectTimer();\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer);\n pending.resolve({\n id,\n ok: false,\n error: { code: 'CLIENT_STOPPED', message: 'IPC client stopped' },\n });\n }\n this.pending.clear();\n if (this.socket) {\n try {\n this.socket.end();\n } catch {\n // ignore — socket may already be in a half-closed state\n }\n this.socket = null;\n }\n this.connected = false;\n }\n\n async call<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n ): Promise<IpcResponse<T>> {\n if (!this.connected || !this.socket) {\n return {\n id: '',\n ok: false,\n error: { code: 'NOT_CONNECTED', message: 'Not connected to alfe-gateway' },\n };\n }\n const id = randomUUID();\n return new Promise<IpcResponse<T>>((resolve) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n resolve({\n id,\n ok: false,\n error: { code: 'TIMEOUT', message: `${method} timed out after ${String(REQUEST_TIMEOUT_MS)}ms` },\n });\n }, REQUEST_TIMEOUT_MS);\n this.pending.set(id, { resolve: resolve as (r: IpcResponse) => void, timer });\n try {\n this.socket?.write(`${JSON.stringify({ type: 'req', id, method, params })}\\n`);\n } catch (err) {\n clearTimeout(timer);\n this.pending.delete(id);\n resolve({\n id,\n ok: false,\n error: {\n code: 'SEND_FAILED',\n message: err instanceof Error ? err.message : String(err),\n },\n });\n }\n });\n }\n\n private openSocket(): void {\n if (this.closed) return;\n this.buffer = '';\n const socket = createConnection(this.socketPath, () => {\n this.logger?.info('[mcp-bundler/ipc] connected', { socket: this.socketPath });\n this.connected = true;\n this.backoffMs = RECONNECT_MIN_MS;\n // Best-effort register. We don't await the response — the daemon\n // routes mcp.* methods even on unregistered connections, so the\n // tool-list / tool-call path doesn't need register to complete.\n void this.call('register', {\n name: this.plugin.name,\n version: this.plugin.version,\n protocolVersion: 1,\n capabilities: ['mcp.list_tools', 'mcp.call_tool'],\n pid: process.pid,\n });\n // Notify caller AFTER `connected = true` so any `.call(...)` issued\n // from the handler (e.g. plugin.ts's tool-cache refresh) actually\n // makes it onto the wire instead of getting the NOT_CONNECTED\n // short-circuit at the top of `call()`.\n if (this.onConnect) {\n try {\n this.onConnect();\n } catch (err) {\n this.logger?.warn('[mcp-bundler/ipc] onConnect handler threw', {\n err: err instanceof Error ? err.message : String(err),\n });\n }\n }\n });\n this.socket = socket;\n\n socket.on('data', (data: Buffer) => {\n this.buffer += data.toString();\n this.processBuffer();\n });\n\n socket.on('close', () => {\n this.connected = false;\n for (const [id, pending] of this.pending) {\n clearTimeout(pending.timer);\n pending.resolve({\n id,\n ok: false,\n error: { code: 'DISCONNECTED', message: 'Connection closed' },\n });\n }\n this.pending.clear();\n this.socket = null;\n if (!this.closed) this.scheduleReconnect();\n });\n\n socket.on('error', (err: Error) => {\n this.logger?.debug('[mcp-bundler/ipc] socket error', { err: err.message });\n // 'close' fires after 'error'; reconnect handled there.\n });\n }\n\n private processBuffer(): void {\n let idx: number;\n while ((idx = this.buffer.indexOf('\\n')) !== -1) {\n const line = this.buffer.slice(0, idx).trim();\n this.buffer = this.buffer.slice(idx + 1);\n if (!line) continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n continue;\n }\n if (!isIpcResponse(parsed)) continue;\n const pending = this.pending.get(parsed.id);\n if (!pending) continue;\n clearTimeout(pending.timer);\n this.pending.delete(parsed.id);\n pending.resolve(parsed);\n }\n }\n\n private scheduleReconnect(): void {\n if (this.reconnectTimer) return;\n const delay = this.backoffMs;\n this.backoffMs = Math.min(this.backoffMs * 2, RECONNECT_MAX_MS);\n this.reconnectTimer = setTimeout(() => {\n this.reconnectTimer = null;\n this.openSocket();\n }, delay);\n if (typeof this.reconnectTimer === 'object' && 'unref' in this.reconnectTimer) {\n (this.reconnectTimer as { unref: () => void }).unref();\n }\n }\n\n private clearReconnectTimer(): void {\n if (this.reconnectTimer) {\n clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n}\n\nfunction isIpcResponse(value: unknown): value is IpcResponse {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'id' in value &&\n 'ok' in value &&\n typeof (value as { id: unknown }).id === 'string'\n );\n}\n","/**\n * @alfe.ai/openclaw-mcp-bundler — OpenClaw plugin entry\n *\n * Thin IPC client over the alfe-gateway daemon. The daemon hosts the\n * actual `McpBundler` (children + tool catalog), this plugin forwards\n * the OpenClaw tool-surface to that bundler via `mcp.list_tools` and\n * `mcp.call_tool` over `~/.alfe/gateway.sock`.\n *\n * Works on every LLM backend — `api.registerTool` registrations reach\n * MiniMax/Mistral/generic AND claude-cli/codex-cli. The previous\n * blanket no-op on claude-cli was load-bearing only when the same MCP\n * was ALSO in `openclaw.json#mcp.servers` (double registration);\n * since the alfe store no longer mirror-writes there, the native\n * bundleMcp path has nothing to spawn and we can register on every\n * backend uniformly.\n *\n * Escape hatch: a user who hand-adds an entry to\n * `openclaw.json#mcp.servers` for a private dev MCP gets the native\n * path back for that run — `hasNativeOpenclawMcpServers` returns true,\n * we no-op, claude-cli/codex-cli spawn that entry themselves.\n */\n\nimport type { McpToolDescriptor, McpToolCallResult } from '@alfe.ai/mcp-bundler';\nimport { hasNativeOpenclawMcpServers } from './cli-backend-detect.js';\nimport { McpIpcClient, type IpcResponse } from './ipc-client.js';\nimport { createRequire } from 'node:module';\nimport { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\nconst require = createRequire(import.meta.url);\nconst pkg = require('../package.json') as { version: string };\n\n// Disk-backed snapshot of the daemon's last-known tool catalogue. Tool-\n// discovery mode (used by OpenClaw whenever it builds the agent's tool\n// list — see `resolvePluginTools` in openclaw's loader) sets\n// `runtimeChannel: false`, so `api.registerService(...)` callbacks\n// (where the IPC client lives) NEVER run in that mode. Without a disk\n// snapshot the plugin contributes zero tools to the agent's list every\n// time it's loaded for tool-discovery — exactly the QA Tester symptom\n// traced 2026-06-09 on bundler v0.0.10. Cache file is written by\n// `refreshCachedTools` on every successful `mcp.list_tools` and read at\n// `activate` time. Lives under the alfe daemon's state dir so the same\n// box's full-mode and tool-discovery-mode loads share it.\nconst CACHE_FILE = join(homedir(), '.alfe', 'mcp', 'openclaw-bundler-cache.json');\n\n// ── Type shims for the OpenClaw plugin SDK ──────────────────────\n// We avoid a hard dep on the SDK to keep this plugin loadable across\n// OpenClaw versions; the surface used here is stable.\n\ninterface Logger {\n debug(msg: string, ...args: unknown[]): void;\n info(msg: string, ...args: unknown[]): void;\n warn(msg: string, ...args: unknown[]): void;\n error(msg: string, ...args: unknown[]): void;\n}\n\ninterface ConfigSnapshot {\n agents?: { defaults?: { model?: string; cliBackends?: Record<string, unknown> } };\n mcp?: { servers?: Record<string, unknown> };\n}\n\ninterface AgentToolResult {\n content: Record<string, unknown>[];\n details?: Record<string, unknown>;\n}\n\ninterface AgentTool {\n name: string;\n label?: string;\n description: string;\n parameters: Record<string, unknown>;\n execute: (\n toolCallId: string,\n params: unknown,\n signal?: AbortSignal,\n onUpdate?: (update: unknown) => void,\n ) => Promise<AgentToolResult>;\n}\n\ninterface ToolFactoryContext {\n config?: ConfigSnapshot;\n runtimeConfig?: ConfigSnapshot;\n getRuntimeConfig?: () => ConfigSnapshot | undefined;\n}\n\ninterface OpenClawPluginService {\n id: string;\n start: (ctx?: unknown) => void | Promise<void>;\n stop?: (ctx?: unknown) => void | Promise<void>;\n}\n\ninterface OpenClawPluginApi {\n logger: Logger;\n config?: ConfigSnapshot;\n pluginConfig?: { disabled?: boolean };\n registrationMode?: string;\n runtime?: { config?: { current?: () => ConfigSnapshot | undefined } };\n registerTool: (\n tool: AgentTool | ((ctx: ToolFactoryContext) => AgentTool | AgentTool[] | null | undefined),\n opts?: { name?: string; names?: string[]; optional?: boolean },\n ) => void;\n registerService?: (service: OpenClawPluginService) => void;\n}\n\n// ── Plugin state ───────────────────────────────────────────────\nlet ipcClient: McpIpcClient | undefined;\n// Module-scoped catalogue. We keep `McpToolDescriptor` here (rather\n// than pre-baked `AgentTool` objects) so the catalogue is JSON-\n// serialisable for the disk cache. `buildAgentTool` constructs an\n// `AgentTool` at factory-invocation time and closes over the live\n// `ipcClient` via the module-scoped reference, so the\n// tools work as soon as IPC is ready — without serialising the\n// IPC client into the cache.\nlet cachedDescriptors: McpToolDescriptor[] = [];\nlet refreshInFlight: Promise<void> | null = null;\nconst ACTIVATED_KEY = '__alfeMcpBundlerPluginActivated';\nconst g = globalThis as Record<string, unknown>;\n\nfunction resolveLiveConfig(api: OpenClawPluginApi, ctx?: ToolFactoryContext): ConfigSnapshot | undefined {\n // Prefer most-recent → ctx live → ctx snapshot → api.runtime live → api.config snapshot\n return (\n ctx?.runtimeConfig ??\n ctx?.getRuntimeConfig?.() ??\n api.runtime?.config?.current?.() ??\n ctx?.config ??\n api.config\n );\n}\n\nfunction buildAgentTool(api: OpenClawPluginApi, tool: McpToolDescriptor): AgentTool {\n return {\n name: tool.prefixed,\n label: tool.label,\n description: tool.description || `MCP tool ${tool.original} from server ${tool.server}`,\n // OpenClaw expects a JSON-Schema-shaped object. The MCP server already\n // gives us one in `tool.parameters`. Pass through unchanged.\n parameters: tool.parameters,\n async execute(_toolCallId, params) {\n // `ipcClient` is module-scoped and set by `startService` at runtime.\n // In tool-discovery mode (no service.start) `ipcClient` is\n // undefined here — return a graceful error so the LLM gets a\n // useful message rather than a hard crash. In full mode this\n // closure resolves at execute() time, after `service.start` has\n // wired the IPC client.\n if (!ipcClient?.isConnected()) {\n return errorResult(\n `mcp ${tool.prefixed}: bundler IPC not connected yet. Try again in a moment.`,\n );\n }\n const response = await ipcClient.call<McpToolCallResult>('mcp.call_tool', {\n name: tool.prefixed,\n args: params,\n });\n if (!response.ok || !response.payload) {\n api.logger.warn('[mcp-bundler] tool call failed', {\n tool: tool.prefixed,\n err: response.error?.message ?? 'unknown error',\n });\n return errorResult(\n response.error?.message\n ? `mcp ${tool.prefixed}: ${response.error.message}`\n : `mcp ${tool.prefixed}: call failed`,\n );\n }\n const result = response.payload;\n return {\n content: result.content,\n details: { isError: result.isError ?? false },\n };\n },\n };\n}\n\nfunction errorResult(message: string): AgentToolResult {\n return {\n content: [{ type: 'text', text: message }],\n details: { isError: true },\n };\n}\n\n/**\n * Async-aware shim around the module-scoped `ipcClient`. The management\n * tools and dynamic MCP tools registered in `activate()` execute LATER\n * (when the agent invokes them), but `ipcClient` is only set inside\n * `startService`. The `?.` chains return useful errors when the agent\n * tries to call a tool before the IPC is ready, instead of throwing.\n */\nasync function callIpc<T = unknown>(\n method: string,\n params: Record<string, unknown> = {},\n): Promise<IpcResponse<T>> {\n if (!ipcClient) {\n return {\n id: '',\n ok: false,\n error: { code: 'NOT_CONNECTED', message: 'Bundler IPC client not initialized yet' },\n };\n }\n return ipcClient.call<T>(method, params);\n}\n\n/**\n * Load the last-known tool catalogue from disk. Used at `activate()`\n * time so the agent's tool list contains real tools even in tool-\n * discovery mode (where `service.start` — and therefore the IPC\n * connection — never runs). Silently falls back to an empty list on\n * any read/parse failure so a first-run agent (no cache file yet)\n * still loads cleanly.\n */\nfunction isValidDescriptor(value: unknown): value is McpToolDescriptor {\n // Cheap shape check guarding against a corrupt or torn-write cache —\n // a half-written JSON array that parses but holds garbage entries\n // would otherwise reach `buildAgentTool` where `tool.prefixed` ends up\n // undefined and registers a tool with `name: undefined`. Catching the\n // shape here protects the agent's tool list from disk-level corruption.\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n return (\n typeof v.prefixed === 'string' &&\n typeof v.original === 'string' &&\n typeof v.server === 'string' &&\n typeof v.parameters === 'object' &&\n v.parameters !== null\n );\n}\n\nfunction loadCachedDescriptorsFromDisk(logger: Logger): McpToolDescriptor[] {\n try {\n const raw = readFileSync(CACHE_FILE, 'utf-8');\n const parsed = JSON.parse(raw) as unknown;\n if (!Array.isArray(parsed)) return [];\n return parsed.filter(isValidDescriptor);\n } catch (err) {\n if (err instanceof Error && (err as NodeJS.ErrnoException).code !== 'ENOENT') {\n logger.debug('[mcp-bundler] cache read failed', { err: err.message });\n }\n return [];\n }\n}\n\n/**\n * Persist the current catalogue to disk so the next tool-discovery\n * load sees the tools. Best-effort — disk failures are logged at\n * `warn` and otherwise swallowed.\n */\nfunction persistCachedDescriptorsToDisk(\n descriptors: McpToolDescriptor[],\n logger: Logger,\n): void {\n // Atomic write: write to a temp sibling then rename. `writeFileSync` is\n // NOT atomic on its own — two processes racing on the same path (a\n // daemon restart concurrent with a tool-discovery load's inline\n // refresh, say) can interleave bytes and corrupt the JSON. The\n // tool-discovery load then silently drops the catalogue until the\n // next successful refresh. `renameSync` is atomic on the same\n // filesystem on every platform we ship to.\n const tmp = `${CACHE_FILE}.${randomUUID()}.tmp`;\n try {\n mkdirSync(dirname(CACHE_FILE), { recursive: true });\n writeFileSync(tmp, JSON.stringify(descriptors));\n renameSync(tmp, CACHE_FILE);\n } catch (err) {\n logger.warn('[mcp-bundler] cache write failed', {\n err: err instanceof Error ? err.message : String(err),\n });\n }\n}\n\n/**\n * Self-service tools the agent can call to add/remove MCP servers in\n * the Alfe bundler store. The agent gets the same surface the CLI's\n * `alfe mcp add/remove/list` commands expose — same IPC methods, same\n * `'manual'` ownership semantics. Daemon-owned (`'cli'` /\n * `'integration:*'`) entries can't be clobbered.\n */\nfunction registerAgentMcpManagementTools(api: OpenClawPluginApi): void {\n // Tools are registered during `activate()` (whether or not the\n // service has started yet), so we can't capture a live `ipcClient`\n // here. Each tool's `execute()` reaches the module-scoped\n // `ipcClient` via `callIpc()` at invocation time — by then,\n // `service.start` has wired the IPC client in full mode. In tool-\n // discovery mode (where service.start never runs) these tools still\n // appear in the agent's tool list, but executing them returns a\n // `NOT_CONNECTED` error from `callIpc` rather than crashing.\n\n api.registerTool({\n name: 'alfe_mcp_list',\n label: 'List Alfe MCP servers',\n description:\n 'List every MCP server currently registered in the Alfe bundler store, with id, owner, and transport. Use this before adding a new server to avoid name collisions or to confirm what is currently available to call.',\n parameters: { type: 'object', properties: {}, additionalProperties: false },\n async execute() {\n const response = await callIpc<{ servers: unknown[] }>('mcp.list_servers', {});\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_list failed');\n }\n return {\n content: [{ type: 'text', text: JSON.stringify(response.payload?.servers ?? [], null, 2) }],\n details: { isError: false },\n };\n },\n });\n\n api.registerTool({\n name: 'alfe_mcp_add',\n label: 'Register an MCP server with Alfe',\n description:\n 'Register a new MCP server in the Alfe bundler store. The Alfe daemon will spawn the child and its tools become available to the agent on the next turn (no runtime restart). Use a stdio command for local-process MCPs (most common) or url+transport for remote SSE / streamable-http MCPs.',\n parameters: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'Unique id under the bundler store. Letters, digits, hyphens. Becomes the `mcp__<id>__` prefix on every tool name from this server.',\n },\n command: {\n type: 'string',\n description: 'Executable to spawn for stdio transport (e.g. \"npx\", \"uvx\", \"/path/to/bin\"). Required for stdio mode.',\n },\n args: {\n type: 'array',\n items: { type: 'string' },\n description: 'Arguments to pass to the command. Optional.',\n },\n env: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'Environment variables for the child process. Optional.',\n },\n cwd: {\n type: 'string',\n description: 'Working directory for the child process. Optional.',\n },\n url: {\n type: 'string',\n description: 'Remote MCP endpoint URL. Required for sse / streamable-http transport.',\n },\n transport: {\n type: 'string',\n enum: ['sse', 'streamable-http'],\n description: 'Remote MCP transport. Defaults to \"sse\" when url is set.',\n },\n headers: {\n type: 'object',\n additionalProperties: { type: 'string' },\n description: 'HTTP headers for remote MCP transports. Optional.',\n },\n },\n required: ['id'],\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n const response = await callIpc<{ id: string }>('mcp.add_server', (params ?? {}) as Record<string, unknown>);\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_add failed');\n }\n return {\n content: [{ type: 'text', text: `Registered MCP server \"${response.payload?.id ?? '?'}\". Tools will appear on the next turn.` }],\n details: { isError: false },\n };\n },\n });\n\n api.registerTool({\n name: 'alfe_mcp_remove',\n label: 'Unregister an MCP server from Alfe',\n description:\n 'Remove an MCP server you previously registered with alfe_mcp_add. Only entries you registered (owner=manual) can be removed this way — integration-installed servers (atlassian, github, etc.) and the built-in alfe-platform server are owner-protected and must be removed via the dashboard or CLI.',\n parameters: {\n type: 'object',\n properties: {\n id: { type: 'string', description: 'Bundler store id of the entry to remove.' },\n },\n required: ['id'],\n additionalProperties: false,\n },\n async execute(_toolCallId, params) {\n const response = await callIpc<{ removed: boolean }>(\n 'mcp.remove_server',\n (params ?? {}) as Record<string, unknown>,\n );\n if (!response.ok) {\n return errorResult(response.error?.message ?? 'mcp_remove failed');\n }\n const removed = response.payload?.removed ?? false;\n return {\n content: [\n {\n type: 'text',\n text: removed\n ? `Removed MCP server. Its tools will disappear on the next turn.`\n : `No MCP server matched — nothing to remove.`,\n },\n ],\n details: { isError: false },\n };\n },\n });\n}\n\n/**\n * Pull the latest tool catalog from the daemon-hosted bundler. Coalesces\n * concurrent calls so the LLM hitting the factory many times in a tight\n * loop only triggers one in-flight IPC request.\n */\nfunction refreshCachedTools(api: OpenClawPluginApi): Promise<void> {\n if (!ipcClient) return Promise.resolve();\n if (refreshInFlight) return refreshInFlight;\n refreshInFlight = (async () => {\n try {\n const response = await callIpc<{ tools: McpToolDescriptor[] }>('mcp.list_tools', {});\n if (!response.ok || !response.payload) {\n // NOT_CONNECTED / TIMEOUT / etc — keep the prior cache so a\n // transient daemon hiccup doesn't drop tools mid-session.\n api.logger.debug('[mcp-bundler] list_tools failed', {\n err: response.error?.message ?? 'unknown',\n });\n return;\n }\n cachedDescriptors = response.payload.tools;\n // Persist for tool-discovery-mode loads — that's how this fix\n // lands tools on the agent's tool list. See CACHE_FILE comment.\n persistCachedDescriptorsToDisk(cachedDescriptors, api.logger);\n } finally {\n refreshInFlight = null;\n }\n })();\n return refreshInFlight;\n}\n\nconst plugin = {\n id: '@alfe.ai/openclaw-mcp-bundler',\n name: 'Alfe MCP Bundler',\n description: 'Proxies the daemon-hosted MCP bundler tools to the LLM',\n version: pkg.version,\n contracts: {\n // OpenClaw's `contracts.tools` matcher (in\n // `loader.js#registerTool` and `tools.js#resolvePluginTools`) uses\n // strict literal `Set.has()` — it does NOT honour glob patterns. So\n // the previous `'mcp__*'` entry was non-functional: every tool the\n // factory returned whose name started with anything else (e.g.\n // `atlassian-atlassian__atlassian_list_accounts`) was silently\n // rejected as \"undeclared\" at runtime, and never surfaced to the\n // agent. Diagnosed 2026-06-09 on QA Tester after the v0.0.9 IPC\n // fix proved correct but tools still didn't appear in chat\n // sessions. See CHANGELOG for the trace.\n //\n // This list enumerates every tool name the proxy can return today.\n // The three `alfe_mcp_*` entries are the agent self-service\n // surface. The `<provider>-<provider>__<provider>_*` entries are\n // the Pattern-A proxy meta-tools (list_accounts /\n // check_connection / refresh_token) each provider's MCP server\n // surfaces before a cloudId is selected. The `alfe-platform__*`\n // entries cover the CLI-managed alfe-platform MCP server.\n //\n // ADDING A NEW INTEGRATION: append its 3 Pattern-A meta-tools\n // here AND any post-cloudId tools the proxy exposes that you want\n // the agent to be able to invoke. There is no shortcut — until\n // OpenClaw learns glob patterns in `contracts.tools` (tracked\n // separately) every tool name must be listed literally.\n tools: [\n 'alfe_mcp_list',\n 'alfe_mcp_add',\n 'alfe_mcp_remove',\n 'atlassian-atlassian__atlassian_list_accounts',\n 'atlassian-atlassian__atlassian_check_connection',\n 'atlassian-atlassian__atlassian_refresh_token',\n 'notion-notion__notion_list_accounts',\n 'notion-notion__notion_check_connection',\n 'notion-notion__notion_refresh_token',\n 'github-github__github_list_accounts',\n 'github-github__github_check_connection',\n 'github-github__github_refresh_token',\n 'microsoft-microsoft__microsoft_list_accounts',\n 'microsoft-microsoft__microsoft_check_connection',\n 'microsoft-microsoft__microsoft_refresh_token',\n 'google-google__google_list_accounts',\n 'google-google__google_check_connection',\n 'google-google__google_refresh_token',\n 'slack-slack__slack_list_accounts',\n 'slack-slack__slack_check_connection',\n 'slack-slack__slack_refresh_token',\n 'discord-discord__discord_list_accounts',\n 'discord-discord__discord_check_connection',\n 'discord-discord__discord_refresh_token',\n 'mobile-mobile__mobile_list_accounts',\n 'mobile-mobile__mobile_check_connection',\n 'mobile-mobile__mobile_refresh_token',\n 'myob-myob__myob_list_accounts',\n 'myob-myob__myob_check_connection',\n 'myob-myob__myob_refresh_token',\n 'xero-xero__xero_list_accounts',\n 'xero-xero__xero_check_connection',\n 'xero-xero__xero_refresh_token',\n 'alfe-platform__session_status',\n 'alfe-platform__gateway_status',\n ],\n },\n\n activate(api: OpenClawPluginApi) {\n const log = api.logger;\n\n if (g[ACTIVATED_KEY] === true) {\n log.debug('[mcp-bundler] already activated — skipping duplicate');\n return;\n }\n g[ACTIVATED_KEY] = true;\n\n if (api.pluginConfig?.disabled) {\n g[ACTIVATED_KEY] = false;\n log.info('[mcp-bundler] disabled via plugin config — no-op');\n return;\n }\n\n if (!api.registerService) {\n g[ACTIVATED_KEY] = false;\n log.warn('[mcp-bundler] api.registerService unavailable — plugin is a no-op on this runtime');\n return;\n }\n\n const deferToNative = hasNativeOpenclawMcpServers(resolveLiveConfig(api));\n if (deferToNative) {\n log.info(\n '[mcp-bundler] openclaw.json#mcp.servers has entries — MCP-proxy factory will defer to native bundleMcp, but agent self-service tools (alfe_mcp_*) stay available',\n );\n }\n\n // ── Register tools synchronously in activate() ──────────────\n //\n // OpenClaw loads plugins in \"tool-discovery\" mode whenever it\n // builds the agent's tool list (see `resolvePluginTools` in\n // `loader.js`). The registration plan for that mode sets\n // `runtimeChannel: false`, which means `api.registerService(...)`\n // start/stop callbacks are NEVER invoked there — only `activate`\n // runs. v0.0.8 → v0.0.10 put tool registration inside\n // `service.start`, so tool-discovery loads contributed ZERO tools\n // to the agent's list (QA Tester repro traced 2026-06-09). We now\n // register every tool here in `activate` so they show up in both\n // modes. The IPC client + lifecycle stays in `registerService` —\n // each tool's `execute()` reaches the module-scoped `ipcClient`\n // lazily via `callIpc()`, so tools registered without a live IPC\n // still show up in the catalogue and return a graceful\n // NOT_CONNECTED error if invoked before `service.start` lands.\n\n // Seed the descriptor cache from disk so tool-discovery mode\n // (which has no IPC) can still return real MCP tools to the\n // agent. Refreshed and re-persisted by `refreshCachedTools` on\n // every IPC connect.\n cachedDescriptors = loadCachedDescriptorsFromDisk(log);\n\n // Agent self-service tools — always available, regardless of the\n // native-bundleMcp escape hatch. The alfe store is independent of\n // openclaw.json so agents can manage their MCP roster in either\n // mode.\n registerAgentMcpManagementTools(api);\n\n if (!deferToNative) {\n api.registerTool((ctx: ToolFactoryContext) => {\n const live = resolveLiveConfig(api, ctx);\n if (hasNativeOpenclawMcpServers(live)) return null;\n // Best-effort background refresh so each turn picks up any new\n // entries the daemon's bundler has discovered (only effective\n // when the IPC client is up — i.e. in full mode).\n void refreshCachedTools(api);\n return cachedDescriptors.map((d) => buildAgentTool(api, d));\n }, { optional: true });\n }\n\n // ── Service lifecycle: IPC client only ─────────────────────\n const startService = (): void => {\n ipcClient = new McpIpcClient({\n logger: log,\n plugin: { name: plugin.id, version: plugin.version },\n // Refresh the tool cache the moment the IPC connection lands.\n // `ipcClient.start()` only initiates the socket connect —\n // `connected` flips true ~50-300ms later inside the\n // createConnection callback. Hooking onConnect makes the first\n // refresh fire AFTER the socket lands, and re-fires on every\n // reconnect after a daemon restart. The refresh result is\n // persisted to disk so the NEXT tool-discovery load sees it.\n onConnect: () => {\n void refreshCachedTools(api);\n },\n });\n ipcClient.start();\n\n if (deferToNative) {\n log.info(`[mcp-bundler] activated v${plugin.version} (management tools only — native bundleMcp owns runtime)`);\n return;\n }\n\n log.info(`[mcp-bundler] activated v${plugin.version} (thin IPC client, tools registered in activate)`);\n };\n\n const stopService = (): Promise<void> => {\n log.info('[mcp-bundler] stopping');\n if (ipcClient) {\n ipcClient.stop();\n ipcClient = undefined;\n }\n // Keep `cachedDescriptors` and the on-disk cache intact across\n // restarts — that's the WHOLE POINT of the disk cache. We only\n // tear down the live IPC reference here.\n refreshInFlight = null;\n // Reset the duplicate-activate guard so OpenClaw can re-run\n // `activate(api)` after a `service.stop` without going through a\n // full `deactivate`. Without this, a stop-then-reactivate path\n // (e.g. an OpenClaw reload that doesn't tear the plugin module\n // down) would silently skip ALL tool registration on the second\n // activate and the agent ends up with no bundler tools. The\n // guard's only job is preventing the same module from running\n // activate() twice concurrently — once the service is stopped,\n // that risk is gone.\n g[ACTIVATED_KEY] = false;\n return Promise.resolve();\n };\n\n api.registerService({\n id: 'alfe-mcp-bundler',\n start: () => { startService(); },\n stop: async () => { await stopService(); },\n });\n },\n\n deactivate(api: OpenClawPluginApi) {\n g[ACTIVATED_KEY] = false;\n api.logger.debug('[mcp-bundler] deactivating');\n },\n};\n\nexport { type IpcResponse };\nexport default plugin;\n"],"mappings":"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"}
@@ -5,7 +5,43 @@
5
5
  "entry": "./dist/plugin.js",
6
6
  "activation": { "onStartup": true },
7
7
  "contracts": {
8
- "tools": ["mcp__*"]
8
+ "tools": [
9
+ "alfe_mcp_list",
10
+ "alfe_mcp_add",
11
+ "alfe_mcp_remove",
12
+ "atlassian-atlassian__atlassian_list_accounts",
13
+ "atlassian-atlassian__atlassian_check_connection",
14
+ "atlassian-atlassian__atlassian_refresh_token",
15
+ "notion-notion__notion_list_accounts",
16
+ "notion-notion__notion_check_connection",
17
+ "notion-notion__notion_refresh_token",
18
+ "github-github__github_list_accounts",
19
+ "github-github__github_check_connection",
20
+ "github-github__github_refresh_token",
21
+ "microsoft-microsoft__microsoft_list_accounts",
22
+ "microsoft-microsoft__microsoft_check_connection",
23
+ "microsoft-microsoft__microsoft_refresh_token",
24
+ "google-google__google_list_accounts",
25
+ "google-google__google_check_connection",
26
+ "google-google__google_refresh_token",
27
+ "slack-slack__slack_list_accounts",
28
+ "slack-slack__slack_check_connection",
29
+ "slack-slack__slack_refresh_token",
30
+ "discord-discord__discord_list_accounts",
31
+ "discord-discord__discord_check_connection",
32
+ "discord-discord__discord_refresh_token",
33
+ "mobile-mobile__mobile_list_accounts",
34
+ "mobile-mobile__mobile_check_connection",
35
+ "mobile-mobile__mobile_refresh_token",
36
+ "myob-myob__myob_list_accounts",
37
+ "myob-myob__myob_check_connection",
38
+ "myob-myob__myob_refresh_token",
39
+ "xero-xero__xero_list_accounts",
40
+ "xero-xero__xero_check_connection",
41
+ "xero-xero__xero_refresh_token",
42
+ "alfe-platform__session_status",
43
+ "alfe-platform__gateway_status"
44
+ ]
9
45
  },
10
46
  "configSchema": {
11
47
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfe.ai/openclaw-mcp-bundler",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
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",