agent.libx.js 0.93.16 → 0.93.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ import { A as AgentTool } from './tools-GPWp7oXq.js';
2
+
3
+ /**
4
+ * MCP bridge — adapt a Model Context Protocol tool list into the agent's AgentTool[],
5
+ * so any MCP server's tools become first-class agent tools (edge-safe: you supply the
6
+ * transport via `callTool`; this module has no node/network dependency of its own).
7
+ *
8
+ * Pass the server's advertised tools + a `callTool(name, args)` that performs the call
9
+ * (over stdio/HTTP/whatever you wire up); each becomes an AgentTool the Agent can use.
10
+ */
11
+ interface McpToolSpec {
12
+ name: string;
13
+ description?: string;
14
+ /** JSON Schema for the tool's arguments (MCP's `inputSchema`). */
15
+ inputSchema?: object;
16
+ }
17
+ /** Perform an MCP tool call; return its textual result. Throw to surface an error to the model. */
18
+ type McpCall = (name: string, args: any) => Promise<unknown>;
19
+ interface McpImage {
20
+ mimeType: string;
21
+ data: string;
22
+ }
23
+ interface McpToolResult {
24
+ text: string;
25
+ images?: McpImage[];
26
+ }
27
+ /** Adapt one MCP tool spec into an AgentTool backed by `callTool`. */
28
+ declare function mcpToolToAgentTool(spec: McpToolSpec, callTool: McpCall, prefix?: string): AgentTool;
29
+ /** Adapt a whole MCP tool list into AgentTool[] (names prefixed to avoid collisions).
30
+ * Optional `filter` pre-narrows the set at mount time (host allowlist), so a server with
31
+ * hundreds of tools needn't mount them all eagerly. */
32
+ declare function mcpToolsToAgentTools(specs: McpToolSpec[], callTool: McpCall, prefix?: string, filter?: (spec: McpToolSpec) => boolean): AgentTool[];
33
+ interface McpToolSearchOptions {
34
+ /** Prefix stripped from / shown on tool names (cosmetic; mirrors the adapter prefix). Default 'mcp__'. */
35
+ prefix?: string;
36
+ /** Max tools returned per `ToolSearch` query. Default 10. */
37
+ maxResults?: number;
38
+ }
39
+ /**
40
+ * Deferred-mount mode (ToolSearch-equivalent) for large MCP tool sets. Instead of mounting N
41
+ * tools into the wire schema (cost + latency + model confusion past a few dozen), mount exactly
42
+ * TWO bounded tools regardless of N:
43
+ * - `ToolSearch({ query })` — ranks the catalog by relevance and returns the top matches with
44
+ * their argument schemas, so the model discovers what's available on demand.
45
+ * - `McpCall({ name, args })` — invokes any catalog tool by name through the same transport.
46
+ * Keep `mcpToolsToAgentTools` (eager) as the default for small sets.
47
+ */
48
+ declare function makeMcpToolSearch(specs: McpToolSpec[], callTool: McpCall, options?: McpToolSearchOptions): AgentTool[];
49
+ /** Minimal shape of a mounted MCP server this module needs — structurally satisfied by
50
+ * `MountedMcp` from `mcp.client.ts`, kept local so `mcp.ts` stays node-free/edge-safe. */
51
+ interface MountedMcpLike {
52
+ name: string;
53
+ specs: McpToolSpec[];
54
+ client: {
55
+ callTool(name: string, args: unknown): Promise<unknown>;
56
+ };
57
+ }
58
+ /** A flattened catalog entry's origin: which server owns it + the un-prefixed raw tool name. */
59
+ interface McpRoute {
60
+ server: string;
61
+ rawName: string;
62
+ }
63
+ /** Flatten servers' specs into one `mcp__<server>__<tool>` namespace + a display→origin map.
64
+ * Sanitizing/truncating to provider name rules can MERGE distinct names (`a/b` & `a:b` → `a_b`),
65
+ * so dedupe with a numeric suffix — a silent overwrite would orphan a tool. */
66
+ declare function buildMcpCatalog(servers: {
67
+ name: string;
68
+ specs: McpToolSpec[];
69
+ }[]): {
70
+ specs: McpToolSpec[];
71
+ routes: Map<string, McpRoute>;
72
+ };
73
+ /** Resolve a routed MCP call to its result. Throw to surface an error to the model. */
74
+ type McpRouteResolver = (server: string, rawName: string, args: any) => Promise<unknown>;
75
+ /**
76
+ * Ergonomic deferred-mount over already-mounted MCP servers: flattens their specs into one
77
+ * `mcp__<server>__<tool>` namespace and wires a single `ToolSearch`/`McpCall` pair that routes
78
+ * each call to the owning server's RAW `callTool` (so result normalization happens exactly once —
79
+ * double-normalizing corrupts image blocks).
80
+ */
81
+ declare function makeMcpToolSearchFromMounted(mounted: MountedMcpLike[], options?: McpToolSearchOptions): {
82
+ tools: AgentTool[];
83
+ serverNames: string[];
84
+ toolCount: number;
85
+ };
86
+ /**
87
+ * Lazy variant: same `ToolSearch`/`McpCall` surface, but the server isn't required to be connected.
88
+ * Each `McpCall` resolves the owning server from the catalog and `resolve(server, rawName, args)`
89
+ * connects-on-demand — so a turn that calls no MCP tool opens ZERO connections. Edge-safe: the
90
+ * caller (`mountMcpCatalog` in `mcp.client.ts`) supplies the node-side connect/pool logic.
91
+ */
92
+ declare function makeLazyMcpToolSearch(servers: {
93
+ name: string;
94
+ specs: McpToolSpec[];
95
+ }[], resolve: McpRouteResolver, options?: McpToolSearchOptions): {
96
+ tools: AgentTool[];
97
+ serverNames: string[];
98
+ toolCount: number;
99
+ };
100
+
101
+ export { type McpCall as M, type McpImage as a, type McpRoute as b, type McpRouteResolver as c, type McpToolResult as d, type McpToolSearchOptions as e, type McpToolSpec as f, type MountedMcpLike as g, buildMcpCatalog as h, makeMcpToolSearch as i, makeMcpToolSearchFromMounted as j, mcpToolToAgentTool as k, mcpToolsToAgentTools as l, makeLazyMcpToolSearch as m };
@@ -1,4 +1,4 @@
1
- import { d as McpToolSpec } from './mcp-wwgXyhbi.js';
1
+ import { e as McpToolSearchOptions, f as McpToolSpec } from './mcp-C5GuDinb.js';
2
2
  import { A as AgentTool } from './tools-GPWp7oXq.js';
3
3
  import '@livx.cc/wcli/core';
4
4
 
@@ -105,6 +105,9 @@ interface MountedMcp {
105
105
  name: string;
106
106
  client: McpClient;
107
107
  tools: AgentTool[];
108
+ /** Raw tool specs discovered at mount (already fetched internally) — exposed so deferred-mount
109
+ * glue (`makeMcpToolSearchFromMounted`) needn't re-call `tools/list`. */
110
+ specs: McpToolSpec[];
108
111
  serverInfo?: {
109
112
  name?: string;
110
113
  version?: string;
@@ -113,9 +116,84 @@ interface MountedMcp {
113
116
  /** Connect one server, discover its tools, and adapt them to prefixed AgentTools (`mcp__<name>__`). */
114
117
  declare function mountMcpServer(name: string, cfg: McpServerConfig): Promise<MountedMcp>;
115
118
  /**
116
- * Mount every configured server. A server that fails (bad command, no handshake, timeout) is
117
- * logged and skipped one broken server can never block the agent from starting.
119
+ * Mount every configured server in PARALLEL (one slow/dead server no longer serializes the rest);
120
+ * each may carry a `mountTimeoutMs` deadline. A server that fails is logged and skipped.
118
121
  */
119
- declare function mountMcpServers(servers?: Record<string, McpServerConfig>): Promise<MountedMcp[]>;
122
+ declare function mountMcpServers(servers?: Record<string, McpServerConfig>, opts?: {
123
+ mountTimeoutMs?: number;
124
+ }): Promise<MountedMcp[]>;
125
+ /**
126
+ * One-shot deferred MCP mount: connect every entry (failures dropped, never block startup) and
127
+ * fold the survivors into a single `ToolSearch`/`McpCall` pair via `makeMcpToolSearchFromMounted`.
128
+ * The node-only counterpart to that edge-safe helper — returns `mounted` too so callers can close
129
+ * the clients on shutdown. Eager: it connects all servers up front. For lazy connect + a cached
130
+ * catalog (zero connections on a turn that uses no MCP tool), use `mountMcpCatalog`.
131
+ */
132
+ declare function mountMcpDeferred(servers?: Record<string, McpServerConfig>, options?: McpToolSearchOptions & {
133
+ mountTimeoutMs?: number;
134
+ }): Promise<{
135
+ tools: AgentTool[];
136
+ serverNames: string[];
137
+ toolCount: number;
138
+ mounted: MountedMcp[];
139
+ }>;
140
+ /** A persistent tool catalog: lets `mountMcpCatalog` build `ToolSearch` WITHOUT connecting when a
141
+ * fresh entry exists. The lib defines the interface; the consumer supplies the store (RAM/disk).
142
+ * Key = a hash of the server's resolved config (see `mcpConfigKey`). TTL/versioning is the store's. */
143
+ interface McpCatalogStore {
144
+ get(key: string): McpToolSpec[] | null;
145
+ set(key: string, specs: McpToolSpec[]): void;
146
+ }
147
+ /** Stable cache key for a server config — command/args/cwd + env NAMES (not secret values), or
148
+ * url + header names. Changing any of these invalidates the cached catalog; rotating a secret does not. */
149
+ declare function mcpConfigKey(cfg: McpServerConfig): string;
150
+ /** Default in-memory catalog: process-lifetime, hash-keyed, TTL-expiring (so a server that gains
151
+ * tools is picked up after the TTL without a restart). Shared module singleton → cross-turn reuse. */
152
+ declare class MemMcpCatalog implements McpCatalogStore {
153
+ private ttlMs;
154
+ private m;
155
+ constructor(ttlMs?: number);
156
+ get(key: string): McpToolSpec[] | null;
157
+ set(key: string, specs: McpToolSpec[]): void;
158
+ }
159
+ /** Opt-in warm pool: keeps stdio MCP clients connected across turns, reaping each after `ttlMs`
160
+ * idle. Most stdio MCPs are per-turn by design, so this is off by default; HTTP servers are
161
+ * stateless and never pooled. */
162
+ declare class McpPool {
163
+ private ttlMs;
164
+ private warm;
165
+ constructor(ttlMs?: number);
166
+ get(key: string): McpClient | null;
167
+ put(key: string, client: McpClient): void;
168
+ private arm;
169
+ private evict;
170
+ closeAll(): Promise<void>;
171
+ }
172
+ interface McpCatalogOptions extends McpToolSearchOptions {
173
+ /** Where to read/write discovered specs. Default: a shared process-lifetime `MemMcpCatalog`. */
174
+ catalog?: McpCatalogStore;
175
+ /** Per-server deadline for the connect+list on a cache MISS. A hung server is skipped, not blocking. */
176
+ mountTimeoutMs?: number;
177
+ /** Opt-in: keep stdio clients warm across turns (see `McpPool`). HTTP servers are never pooled. */
178
+ keepWarm?: boolean;
179
+ /** Warm-client pool. Default: a shared process-lifetime `McpPool` (only used when `keepWarm`). */
180
+ pool?: McpPool;
181
+ }
182
+ /**
183
+ * Lazy + cached MCP mount. Builds the `ToolSearch`/`McpCall` pair from the CACHED catalog when one
184
+ * exists — connecting NOTHING. On a cache miss it connects once (parallel, deadline-bounded), lists,
185
+ * caches, then disconnects (or keeps warm if `keepWarm`). A server is connected only when one of its
186
+ * tools is actually invoked via `McpCall` (memoized per turn; reused from the warm pool if enabled).
187
+ *
188
+ * Per-turn cost: a turn using NO MCP tool → 0 connections; a turn using one → exactly one. Latency
189
+ * scales with tools-used, not servers-configured. Returns `connect`/`close` for explicit control.
190
+ */
191
+ declare function mountMcpCatalog(servers?: Record<string, McpServerConfig>, opts?: McpCatalogOptions): Promise<{
192
+ tools: AgentTool[];
193
+ serverNames: string[];
194
+ toolCount: number;
195
+ connect(name: string): Promise<McpClient>;
196
+ close(): Promise<void>;
197
+ }>;
120
198
 
121
- export { type HttpServerSpec, HttpTransport, McpAuthError, McpClient, type McpServerConfig, type McpTransport, type MountedMcp, type StdioServerSpec, StdioTransport, mountMcpServer, mountMcpServers };
199
+ export { type HttpServerSpec, HttpTransport, McpAuthError, type McpCatalogOptions, type McpCatalogStore, McpClient, McpPool, type McpServerConfig, type McpTransport, MemMcpCatalog, type MountedMcp, type StdioServerSpec, StdioTransport, mcpConfigKey, mountMcpCatalog, mountMcpDeferred, mountMcpServer, mountMcpServers };
@@ -1,10 +1,76 @@
1
1
  // src/mcp.client.ts
2
2
  import { spawn } from "child_process";
3
+ import { createHash } from "crypto";
3
4
 
4
5
  // src/logging.ts
5
6
  import { log } from "libx.js/src/modules/log";
6
7
  var forComponent = (name) => log.forComponent(name);
7
8
 
9
+ // src/relevance.ts
10
+ var STOP = /* @__PURE__ */ new Set([
11
+ "the",
12
+ "and",
13
+ "for",
14
+ "with",
15
+ "that",
16
+ "this",
17
+ "from",
18
+ "into",
19
+ "your",
20
+ "you",
21
+ "are",
22
+ "was",
23
+ "will",
24
+ "use",
25
+ "using",
26
+ "run",
27
+ "add",
28
+ "fix",
29
+ "make",
30
+ "file",
31
+ "files",
32
+ "code",
33
+ "please",
34
+ "need",
35
+ "want",
36
+ "should",
37
+ "all"
38
+ ]);
39
+ function tokenize(s) {
40
+ const out = /* @__PURE__ */ new Set();
41
+ for (const w of String(s ?? "").toLowerCase().match(/[a-z0-9]+/g) ?? []) {
42
+ if (w.length >= 3 && !STOP.has(w)) out.add(w);
43
+ }
44
+ return out;
45
+ }
46
+ function idfWeights(corpus) {
47
+ const N = corpus.length;
48
+ if (N === 0) return /* @__PURE__ */ new Map();
49
+ const df = /* @__PURE__ */ new Map();
50
+ for (const doc of corpus) for (const t of tokenize(doc)) df.set(t, (df.get(t) ?? 0) + 1);
51
+ const idf = /* @__PURE__ */ new Map();
52
+ for (const [t, n] of df) idf.set(t, Math.log((N + 1) / (n + 1)) + 1);
53
+ return idf;
54
+ }
55
+ function relevanceScore(text, queryTokens, idf) {
56
+ if (queryTokens.size === 0) return 0;
57
+ const t = tokenize(text);
58
+ let score = 0;
59
+ for (const q of queryTokens) if (t.has(q)) score += idf?.get(q) ?? 1;
60
+ return score;
61
+ }
62
+ function topByRelevance(items, query, text, k, corpus) {
63
+ if (!Number.isInteger(k) || k < 1) return { kept: items, rest: [] };
64
+ if (items.length <= k || !query.trim()) return { kept: items, rest: [] };
65
+ const q = tokenize(query);
66
+ if (q.size === 0) return { kept: items.slice(0, k), rest: items.slice(k) };
67
+ const idf = corpus?.length ? idfWeights(corpus) : void 0;
68
+ const scored = items.map((x, i) => ({ i, s: relevanceScore(text(x), q, idf) }));
69
+ scored.sort((a, b) => b.s - a.s || a.i - b.i);
70
+ const keep = new Set(scored.slice(0, k).map((e) => e.i));
71
+ return { kept: items.filter((_, i) => keep.has(i)), rest: items.filter((_, i) => !keep.has(i)) };
72
+ }
73
+
8
74
  // src/mcp.ts
9
75
  function toResult(result) {
10
76
  if (result == null) return { text: "" };
@@ -41,6 +107,78 @@ function mcpToolToAgentTool(spec, callTool, prefix = "mcp__") {
41
107
  function mcpToolsToAgentTools(specs, callTool, prefix = "mcp__", filter) {
42
108
  return (filter ? specs.filter(filter) : specs).map((s) => mcpToolToAgentTool(s, callTool, prefix));
43
109
  }
110
+ function describeSpec(s) {
111
+ const schema = s.inputSchema ? `
112
+ args: ${JSON.stringify(s.inputSchema)}` : "";
113
+ return `${s.name} \u2014 ${s.description ?? "(no description)"}${schema}`;
114
+ }
115
+ function makeMcpToolSearch(specs, callTool, options = {}) {
116
+ const maxResults = options.maxResults ?? 10;
117
+ const byName = new Map(specs.map((s) => [s.name, s]));
118
+ const catalogLine = `${specs.length} MCP tool(s) available \u2014 search by keyword, then call by exact name.`;
119
+ const searchTool = {
120
+ name: "ToolSearch",
121
+ description: `Search the available MCP tools by keyword (${catalogLine}). Returns matching tool names + their argument schemas; call one with \`McpCall\`.`,
122
+ parameters: { type: "object", required: ["query"], properties: { query: { type: "string", description: "keywords to match against tool name + description" } } },
123
+ async run({ query }) {
124
+ const q = String(query ?? "").trim();
125
+ if (!q) return catalogLine;
126
+ const { kept } = topByRelevance(specs, q, (s) => `${s.name} ${s.description ?? ""}`, maxResults);
127
+ if (!kept.length) return `(no MCP tool matches "${q}" \u2014 try broader keywords)`;
128
+ return kept.map(describeSpec).join("\n");
129
+ }
130
+ };
131
+ const callMcpTool = {
132
+ name: "McpCall",
133
+ description: "Call an MCP tool discovered via `ToolSearch`, by its exact name. Pass its arguments as `args`.",
134
+ parameters: {
135
+ type: "object",
136
+ required: ["name"],
137
+ properties: {
138
+ name: { type: "string", description: "exact tool name from ToolSearch" },
139
+ args: { type: "object", description: "arguments object for the tool (per its schema)" }
140
+ }
141
+ },
142
+ async run({ name, args }) {
143
+ const n = String(name ?? "");
144
+ if (!byName.has(n)) return `Error: unknown MCP tool '${n}'. Use ToolSearch to find valid names.`;
145
+ const r = toResult(await callTool(n, args ?? {}));
146
+ return r.images?.length ? r : r.text;
147
+ }
148
+ };
149
+ return [searchTool, callMcpTool];
150
+ }
151
+ function buildMcpCatalog(servers) {
152
+ const specs = [];
153
+ const routes = /* @__PURE__ */ new Map();
154
+ for (const m of servers) {
155
+ for (const s of m.specs) {
156
+ const base = `mcp__${m.name}__${s.name}`.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128);
157
+ let display = base;
158
+ for (let i = 2; routes.has(display); i++) display = `${base.slice(0, 128 - String(i).length - 1)}_${i}`;
159
+ specs.push({ name: display, description: s.description, inputSchema: s.inputSchema });
160
+ routes.set(display, { server: m.name, rawName: s.name });
161
+ }
162
+ }
163
+ return { specs, routes };
164
+ }
165
+ function searchOverCatalog(servers, specs, routes, resolve, options) {
166
+ const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
167
+ const r = routes.get(name);
168
+ if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
169
+ return resolve(r.server, r.rawName, args ?? {});
170
+ }, options) : [];
171
+ return { tools, serverNames: servers, toolCount: specs.length };
172
+ }
173
+ function makeMcpToolSearchFromMounted(mounted, options) {
174
+ const { specs, routes } = buildMcpCatalog(mounted);
175
+ const byName = new Map(mounted.map((m) => [m.name, m]));
176
+ return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
177
+ }
178
+ function makeLazyMcpToolSearch(servers, resolve, options) {
179
+ const { specs, routes } = buildMcpCatalog(servers);
180
+ return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve, options);
181
+ }
44
182
 
45
183
  // src/mcp.client.ts
46
184
  var log2 = forComponent("mcp");
@@ -235,38 +373,211 @@ var McpClient = class {
235
373
  await this.transport.close();
236
374
  }
237
375
  };
238
- async function mountMcpServer(name, cfg) {
239
- const transport = cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs });
240
- const client = new McpClient(transport);
241
- const init = await client.connect();
242
- const specs = await client.listTools();
243
- const tools = mcpToolsToAgentTools(specs, (tool, a) => client.callTool(tool, a), `mcp__${name}__`);
244
- return { name, client, tools, serverInfo: init?.serverInfo };
376
+ function buildTransport(cfg) {
377
+ return cfg.url ? new HttpTransport({ url: cfg.url, headers: cfg.headers, bearerToken: cfg.bearerToken, timeoutMs: cfg.timeoutMs }) : new StdioTransport({ command: cfg.command, args: cfg.args, env: cfg.env, cwd: cfg.cwd, timeoutMs: cfg.timeoutMs });
245
378
  }
246
- async function mountMcpServers(servers = {}) {
247
- const out = [];
248
- for (const [name, cfg] of Object.entries(servers)) {
249
- if (!cfg || cfg.disabled) continue;
379
+ function withTimeout(p, ms, label) {
380
+ if (!ms || ms <= 0) return p;
381
+ return new Promise((resolve, reject) => {
382
+ const timer = setTimeout(() => reject(new Error(`MCP "${label}" mount exceeded ${ms}ms`)), ms);
383
+ timer.unref?.();
384
+ p.then((v) => {
385
+ clearTimeout(timer);
386
+ resolve(v);
387
+ }, (e) => {
388
+ clearTimeout(timer);
389
+ reject(e);
390
+ });
391
+ });
392
+ }
393
+ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
394
+ const client = new McpClient(buildTransport(cfg));
395
+ try {
396
+ return await withTimeout((async () => {
397
+ const init = await client.connect();
398
+ const specs = await client.listTools();
399
+ const tools = mcpToolsToAgentTools(specs, (tool, a) => client.callTool(tool, a), `mcp__${name}__`);
400
+ return { name, client, tools, specs, serverInfo: init?.serverInfo };
401
+ })(), mountTimeoutMs, name);
402
+ } catch (e) {
403
+ await client.close().catch((err) => log2.debug(`close after failed mount of "${name}": ${err}`));
404
+ throw e;
405
+ }
406
+ }
407
+ function mountMcpServer(name, cfg) {
408
+ return mountWithDeadline(name, cfg);
409
+ }
410
+ function validEntries(servers) {
411
+ return Object.entries(servers).filter(([name, cfg]) => {
412
+ if (!cfg || cfg.disabled) return false;
250
413
  if (!cfg.command && !cfg.url) {
251
414
  log2.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
252
- continue;
415
+ return false;
253
416
  }
417
+ return true;
418
+ });
419
+ }
420
+ function logMountFailure(name, e) {
421
+ if (e instanceof McpAuthError) log2.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
422
+ else log2.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
423
+ }
424
+ async function mountMcpServers(servers = {}, opts = {}) {
425
+ const entries = validEntries(servers);
426
+ const settled = await Promise.allSettled(entries.map(([name, cfg]) => mountWithDeadline(name, cfg, opts.mountTimeoutMs)));
427
+ const out = [];
428
+ settled.forEach((r, i) => {
429
+ const name = entries[i][0];
430
+ if (r.status === "fulfilled") {
431
+ out.push(r.value);
432
+ log2.info(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
433
+ } else logMountFailure(name, r.reason);
434
+ });
435
+ return out;
436
+ }
437
+ async function mountMcpDeferred(servers = {}, options) {
438
+ const mounted = await mountMcpServers(servers, { mountTimeoutMs: options?.mountTimeoutMs });
439
+ return { ...makeMcpToolSearchFromMounted(mounted, options), mounted };
440
+ }
441
+ function mcpConfigKey(cfg) {
442
+ const parts = cfg.url ? ["http", cfg.url, ...Object.keys(cfg.headers ?? {}).sort()] : ["stdio", cfg.command ?? "", ...cfg.args ?? [], cfg.cwd ?? "", ...Object.keys(cfg.env ?? {}).sort()];
443
+ return createHash("sha256").update(parts.join("\0")).digest("hex").slice(0, 16);
444
+ }
445
+ var MemMcpCatalog = class {
446
+ constructor(ttlMs = 5 * 6e4) {
447
+ this.ttlMs = ttlMs;
448
+ }
449
+ ttlMs;
450
+ m = /* @__PURE__ */ new Map();
451
+ get(key) {
452
+ const e = this.m.get(key);
453
+ if (!e) return null;
454
+ if (Date.now() > e.exp) {
455
+ this.m.delete(key);
456
+ return null;
457
+ }
458
+ return e.specs;
459
+ }
460
+ set(key, specs) {
461
+ this.m.set(key, { specs, exp: Date.now() + this.ttlMs });
462
+ }
463
+ };
464
+ var McpPool = class {
465
+ constructor(ttlMs = 5 * 6e4) {
466
+ this.ttlMs = ttlMs;
467
+ }
468
+ ttlMs;
469
+ warm = /* @__PURE__ */ new Map();
470
+ get(key) {
471
+ const e = this.warm.get(key);
472
+ if (!e) return null;
473
+ this.arm(key, e);
474
+ return e.client;
475
+ }
476
+ put(key, client) {
477
+ const prev = this.warm.get(key);
478
+ if (prev) {
479
+ clearTimeout(prev.timer);
480
+ if (prev.client !== client) void prev.client.close().catch((err) => log2.debug(`warm-pool replace close failed: ${err}`));
481
+ }
482
+ const e = { client, timer: void 0 };
483
+ this.warm.set(key, e);
484
+ this.arm(key, e);
485
+ }
486
+ arm(key, e) {
487
+ clearTimeout(e.timer);
488
+ e.timer = setTimeout(() => {
489
+ void this.evict(key);
490
+ }, this.ttlMs);
491
+ e.timer.unref?.();
492
+ }
493
+ async evict(key) {
494
+ const e = this.warm.get(key);
495
+ if (!e) return;
496
+ this.warm.delete(key);
497
+ await e.client.close().catch((err) => log2.debug(`warm-pool evict close failed: ${err}`));
498
+ }
499
+ async closeAll() {
500
+ for (const e of this.warm.values()) {
501
+ clearTimeout(e.timer);
502
+ await e.client.close().catch(() => {
503
+ });
504
+ }
505
+ this.warm.clear();
506
+ }
507
+ };
508
+ var defaultCatalog = new MemMcpCatalog();
509
+ var defaultPool = new McpPool();
510
+ async function mountMcpCatalog(servers = {}, opts = {}) {
511
+ const catalog = opts.catalog ?? defaultCatalog;
512
+ const pool = opts.pool ?? defaultPool;
513
+ const discovered = (await Promise.all(validEntries(servers).map(async ([name, cfg]) => {
514
+ const key = mcpConfigKey(cfg);
515
+ const cached = catalog.get(key);
516
+ if (cached) return { name, cfg, key, specs: cached };
517
+ const client = new McpClient(buildTransport(cfg));
254
518
  try {
255
- const m = await mountMcpServer(name, cfg);
256
- out.push(m);
257
- log2.info(`MCP "${name}" mounted \u2014 ${m.tools.length} tool(s)${m.serverInfo?.name ? ` from ${m.serverInfo.name}` : ""}`);
519
+ const specs = await withTimeout((async () => {
520
+ await client.connect();
521
+ return client.listTools();
522
+ })(), opts.mountTimeoutMs, name);
523
+ catalog.set(key, specs);
524
+ if (opts.keepWarm && !cfg.url) pool.put(key, client);
525
+ else await client.close().catch(() => {
526
+ });
527
+ return { name, cfg, key, specs };
258
528
  } catch (e) {
259
- if (e instanceof McpAuthError) log2.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
260
- else log2.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
529
+ await client.close().catch(() => {
530
+ });
531
+ logMountFailure(name, e);
532
+ return null;
261
533
  }
262
- }
263
- return out;
534
+ }))).filter(Boolean);
535
+ const byName = new Map(discovered.map((d) => [d.name, d]));
536
+ const inflight = /* @__PURE__ */ new Map();
537
+ const live = [];
538
+ const connect = (name) => {
539
+ let p = inflight.get(name);
540
+ if (p) return p;
541
+ p = (async () => {
542
+ const d = byName.get(name);
543
+ if (!d) throw new Error(`MCP server '${name}' is not in the catalog`);
544
+ const warmable = opts.keepWarm && !d.cfg.url;
545
+ if (warmable) {
546
+ const w = pool.get(d.key);
547
+ if (w) return w;
548
+ }
549
+ const client = new McpClient(buildTransport(d.cfg));
550
+ await client.connect();
551
+ if (warmable) pool.put(d.key, client);
552
+ else live.push(client);
553
+ return client;
554
+ })();
555
+ inflight.set(name, p);
556
+ return p;
557
+ };
558
+ const search = makeLazyMcpToolSearch(
559
+ discovered.map((d) => ({ name: d.name, specs: d.specs })),
560
+ async (server, rawName, args) => (await connect(server)).callTool(rawName, args),
561
+ opts
562
+ );
563
+ const close = async () => {
564
+ for (const c of live) await c.close().catch(() => {
565
+ });
566
+ live.length = 0;
567
+ inflight.clear();
568
+ };
569
+ return { ...search, connect, close };
264
570
  }
265
571
  export {
266
572
  HttpTransport,
267
573
  McpAuthError,
268
574
  McpClient,
575
+ McpPool,
576
+ MemMcpCatalog,
269
577
  StdioTransport,
578
+ mcpConfigKey,
579
+ mountMcpCatalog,
580
+ mountMcpDeferred,
270
581
  mountMcpServer,
271
582
  mountMcpServers
272
583
  };