agent.libx.js 0.93.17 → 0.93.19
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/cli/cli.ts +19 -2
- package/dist/cli.js +210 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.js +97 -12
- package/dist/index.js.map +1 -1
- package/dist/{mcp-DGWuuWJm.d.ts → mcp-C5GuDinb.d.ts} +35 -6
- package/dist/mcp.client.d.ts +70 -7
- package/dist/mcp.client.js +212 -31
- package/dist/mcp.client.js.map +1 -1
- package/package.json +1 -1
|
@@ -55,18 +55,47 @@ interface MountedMcpLike {
|
|
|
55
55
|
callTool(name: string, args: unknown): Promise<unknown>;
|
|
56
56
|
};
|
|
57
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>;
|
|
58
75
|
/**
|
|
59
76
|
* Ergonomic deferred-mount over already-mounted MCP servers: flattens their specs into one
|
|
60
|
-
* `mcp__<server>__<tool>` namespace
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* Display names are `mcp__<server>__` prefixed, then deduped (sanitization can merge distinct
|
|
64
|
-
* names) so every tool stays uniquely addressable and routed to its own client.
|
|
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).
|
|
65
80
|
*/
|
|
66
81
|
declare function makeMcpToolSearchFromMounted(mounted: MountedMcpLike[], options?: McpToolSearchOptions): {
|
|
67
82
|
tools: AgentTool[];
|
|
68
83
|
serverNames: string[];
|
|
69
84
|
toolCount: number;
|
|
70
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
|
+
};
|
|
71
100
|
|
|
72
|
-
export { type McpCall as M, type McpImage as a, type
|
|
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 };
|
package/dist/mcp.client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
|
@@ -116,21 +116,84 @@ interface MountedMcp {
|
|
|
116
116
|
/** Connect one server, discover its tools, and adapt them to prefixed AgentTools (`mcp__<name>__`). */
|
|
117
117
|
declare function mountMcpServer(name: string, cfg: McpServerConfig): Promise<MountedMcp>;
|
|
118
118
|
/**
|
|
119
|
-
* Mount every configured server
|
|
120
|
-
*
|
|
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.
|
|
121
121
|
*/
|
|
122
|
-
declare function mountMcpServers(servers?: Record<string, McpServerConfig
|
|
122
|
+
declare function mountMcpServers(servers?: Record<string, McpServerConfig>, opts?: {
|
|
123
|
+
mountTimeoutMs?: number;
|
|
124
|
+
}): Promise<MountedMcp[]>;
|
|
123
125
|
/**
|
|
124
126
|
* One-shot deferred MCP mount: connect every entry (failures dropped, never block startup) and
|
|
125
127
|
* fold the survivors into a single `ToolSearch`/`McpCall` pair via `makeMcpToolSearchFromMounted`.
|
|
126
128
|
* The node-only counterpart to that edge-safe helper — returns `mounted` too so callers can close
|
|
127
|
-
* the clients on shutdown.
|
|
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`.
|
|
128
131
|
*/
|
|
129
|
-
declare function mountMcpDeferred(servers?: Record<string, McpServerConfig>, options?: McpToolSearchOptions
|
|
132
|
+
declare function mountMcpDeferred(servers?: Record<string, McpServerConfig>, options?: McpToolSearchOptions & {
|
|
133
|
+
mountTimeoutMs?: number;
|
|
134
|
+
}): Promise<{
|
|
130
135
|
tools: AgentTool[];
|
|
131
136
|
serverNames: string[];
|
|
132
137
|
toolCount: number;
|
|
133
138
|
mounted: MountedMcp[];
|
|
134
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
|
+
}>;
|
|
135
198
|
|
|
136
|
-
export { type HttpServerSpec, HttpTransport, McpAuthError, McpClient, type McpServerConfig, type McpTransport, type MountedMcp, type StdioServerSpec, StdioTransport, mountMcpDeferred, 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 };
|
package/dist/mcp.client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
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";
|
|
@@ -147,24 +148,36 @@ function makeMcpToolSearch(specs, callTool, options = {}) {
|
|
|
147
148
|
};
|
|
148
149
|
return [searchTool, callMcpTool];
|
|
149
150
|
}
|
|
150
|
-
function
|
|
151
|
+
function buildMcpCatalog(servers) {
|
|
151
152
|
const specs = [];
|
|
152
|
-
const
|
|
153
|
-
for (const m of
|
|
153
|
+
const routes = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const m of servers) {
|
|
154
155
|
for (const s of m.specs) {
|
|
155
156
|
const base = `mcp__${m.name}__${s.name}`.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 128);
|
|
156
157
|
let display = base;
|
|
157
|
-
for (let i = 2;
|
|
158
|
+
for (let i = 2; routes.has(display); i++) display = `${base.slice(0, 128 - String(i).length - 1)}_${i}`;
|
|
158
159
|
specs.push({ name: display, description: s.description, inputSchema: s.inputSchema });
|
|
159
|
-
|
|
160
|
+
routes.set(display, { server: m.name, rawName: s.name });
|
|
160
161
|
}
|
|
161
162
|
}
|
|
163
|
+
return { specs, routes };
|
|
164
|
+
}
|
|
165
|
+
function searchOverCatalog(servers, specs, routes, resolve, options) {
|
|
162
166
|
const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
|
|
163
|
-
const
|
|
164
|
-
if (!
|
|
165
|
-
return
|
|
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 ?? {});
|
|
166
170
|
}, options) : [];
|
|
167
|
-
return { tools, serverNames:
|
|
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);
|
|
168
181
|
}
|
|
169
182
|
|
|
170
183
|
// src/mcp.client.ts
|
|
@@ -360,42 +373,210 @@ var McpClient = class {
|
|
|
360
373
|
await this.transport.close();
|
|
361
374
|
}
|
|
362
375
|
};
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const client = new McpClient(transport);
|
|
366
|
-
const init = await client.connect();
|
|
367
|
-
const specs = await client.listTools();
|
|
368
|
-
const tools = mcpToolsToAgentTools(specs, (tool, a) => client.callTool(tool, a), `mcp__${name}__`);
|
|
369
|
-
return { name, client, tools, specs, 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 });
|
|
370
378
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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;
|
|
375
413
|
if (!cfg.command && !cfg.url) {
|
|
376
414
|
log2.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
377
|
-
|
|
415
|
+
return false;
|
|
378
416
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
+
});
|
|
388
435
|
return out;
|
|
389
436
|
}
|
|
390
437
|
async function mountMcpDeferred(servers = {}, options) {
|
|
391
|
-
const mounted = await mountMcpServers(servers);
|
|
438
|
+
const mounted = await mountMcpServers(servers, { mountTimeoutMs: options?.mountTimeoutMs });
|
|
392
439
|
return { ...makeMcpToolSearchFromMounted(mounted, options), mounted };
|
|
393
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));
|
|
518
|
+
try {
|
|
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 };
|
|
528
|
+
} catch (e) {
|
|
529
|
+
await client.close().catch(() => {
|
|
530
|
+
});
|
|
531
|
+
logMountFailure(name, e);
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
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 };
|
|
570
|
+
}
|
|
394
571
|
export {
|
|
395
572
|
HttpTransport,
|
|
396
573
|
McpAuthError,
|
|
397
574
|
McpClient,
|
|
575
|
+
McpPool,
|
|
576
|
+
MemMcpCatalog,
|
|
398
577
|
StdioTransport,
|
|
578
|
+
mcpConfigKey,
|
|
579
|
+
mountMcpCatalog,
|
|
399
580
|
mountMcpDeferred,
|
|
400
581
|
mountMcpServer,
|
|
401
582
|
mountMcpServers
|