@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 +0 -6
- package/dist/plugin.d.cts.map +1 -1
- package/dist/plugin.d.ts +0 -6
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin2.cjs +261 -146
- package/dist/plugin2.js +262 -147
- package/dist/plugin2.js.map +1 -1
- package/openclaw.plugin.json +3 -33
- package/package.json +1 -1
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;
|
package/dist/plugin.d.cts.map
CHANGED
|
@@ -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;;
|
|
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;
|
package/dist/plugin.d.ts.map
CHANGED
|
@@ -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;;
|
|
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
|
|
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)
|
|
290
|
-
* `
|
|
291
|
-
*
|
|
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
|
-
|
|
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
|
|
325
|
+
message: "Bundler IPC unavailable — no alfe-gateway daemon socket"
|
|
300
326
|
}
|
|
301
327
|
};
|
|
302
|
-
|
|
328
|
+
if (!client.isConnected()) await waitForIpcConnect(client, IPC_CONNECT_TIMEOUT_MS);
|
|
329
|
+
return client.call(method, params);
|
|
303
330
|
}
|
|
304
331
|
/**
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
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
|
|
459
|
-
*
|
|
460
|
-
*
|
|
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
|
-
|
|
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
|
-
"
|
|
490
|
-
"
|
|
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
|
-
|
|
535
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|