@livx.cc/agentx 0.94.38
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/LICENSE +21 -0
- package/README.md +164 -0
- package/dist/Agent-1DRfsYaK.d.ts +408 -0
- package/dist/cli.d.ts +293 -0
- package/dist/cli.js +12796 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1319 -0
- package/dist/index.js +5998 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-CnzmQ8JE.d.ts +101 -0
- package/dist/mcp.client.d.ts +228 -0
- package/dist/mcp.client.js +617 -0
- package/dist/mcp.client.js.map +1 -0
- package/dist/tools-DtpN8Agv.d.ts +274 -0
- package/dist/tools.shell.d.ts +115 -0
- package/dist/tools.shell.js +539 -0
- package/dist/tools.shell.js.map +1 -0
- package/package.json +81 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
// src/mcp.client.ts
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
|
|
5
|
+
// src/logging.ts
|
|
6
|
+
import { log } from "libx.js/src/modules/log";
|
|
7
|
+
var forComponent = (name) => log.forComponent(name);
|
|
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
|
+
|
|
74
|
+
// src/mcp.ts
|
|
75
|
+
function toResult(result) {
|
|
76
|
+
if (result == null) return { text: "" };
|
|
77
|
+
if (typeof result === "string") return { text: result };
|
|
78
|
+
const content = result.content;
|
|
79
|
+
if (Array.isArray(content)) {
|
|
80
|
+
const texts = [];
|
|
81
|
+
const images = [];
|
|
82
|
+
for (const c of content) {
|
|
83
|
+
if (c?.type === "image" && typeof c.data === "string" && c.mimeType) {
|
|
84
|
+
images.push({ mimeType: c.mimeType, data: c.data });
|
|
85
|
+
} else if (typeof c?.text === "string") {
|
|
86
|
+
texts.push(c.text);
|
|
87
|
+
} else {
|
|
88
|
+
texts.push(JSON.stringify(c));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const text = texts.join("\n");
|
|
92
|
+
if (text || images.length) return { text, ...images.length ? { images } : {} };
|
|
93
|
+
}
|
|
94
|
+
return { text: JSON.stringify(result) };
|
|
95
|
+
}
|
|
96
|
+
function mcpToolToAgentTool(spec, callTool, prefix = "mcp__") {
|
|
97
|
+
return {
|
|
98
|
+
name: `${prefix}${spec.name}`,
|
|
99
|
+
description: spec.description ?? `MCP tool ${spec.name}`,
|
|
100
|
+
parameters: spec.inputSchema ?? { type: "object", properties: {} },
|
|
101
|
+
async run(args, _ctx) {
|
|
102
|
+
const r = toResult(await callTool(spec.name, args ?? {}));
|
|
103
|
+
return r.images?.length ? r : r.text;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function mcpToolsToAgentTools(specs, callTool, prefix = "mcp__", filter) {
|
|
108
|
+
return (filter ? specs.filter(filter) : specs).map((s) => mcpToolToAgentTool(s, callTool, prefix));
|
|
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
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/mcp.client.ts
|
|
184
|
+
var log2 = forComponent("mcp");
|
|
185
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
186
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
187
|
+
var StdioTransport = class {
|
|
188
|
+
constructor(spec) {
|
|
189
|
+
this.spec = spec;
|
|
190
|
+
}
|
|
191
|
+
spec;
|
|
192
|
+
proc;
|
|
193
|
+
buf = "";
|
|
194
|
+
nextId = 1;
|
|
195
|
+
pending = /* @__PURE__ */ new Map();
|
|
196
|
+
async start() {
|
|
197
|
+
const { command, args = [], env, cwd } = this.spec;
|
|
198
|
+
const proc = spawn(command, args, { stdio: ["pipe", "pipe", "pipe"], env: { ...process.env, ...env }, cwd });
|
|
199
|
+
this.proc = proc;
|
|
200
|
+
proc.stdout.setEncoding("utf8");
|
|
201
|
+
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
202
|
+
proc.stderr.setEncoding("utf8");
|
|
203
|
+
proc.stderr.on("data", (chunk) => log2.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
204
|
+
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
205
|
+
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
206
|
+
}
|
|
207
|
+
onData(chunk) {
|
|
208
|
+
this.buf += chunk;
|
|
209
|
+
let nl;
|
|
210
|
+
while ((nl = this.buf.indexOf("\n")) >= 0) {
|
|
211
|
+
const line = this.buf.slice(0, nl).trim();
|
|
212
|
+
this.buf = this.buf.slice(nl + 1);
|
|
213
|
+
if (!line) continue;
|
|
214
|
+
try {
|
|
215
|
+
this.dispatch(JSON.parse(line));
|
|
216
|
+
} catch (e) {
|
|
217
|
+
log2.debug("dropping non-JSON line from MCP server:", line, e);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
dispatch(msg) {
|
|
222
|
+
if (msg?.id == null || !this.pending.has(msg.id)) return;
|
|
223
|
+
const p = this.pending.get(msg.id);
|
|
224
|
+
this.pending.delete(msg.id);
|
|
225
|
+
clearTimeout(p.timer);
|
|
226
|
+
if (msg.error) p.reject(new Error(msg.error?.message ?? JSON.stringify(msg.error)));
|
|
227
|
+
else p.resolve(msg.result);
|
|
228
|
+
}
|
|
229
|
+
failAll(e) {
|
|
230
|
+
for (const p of this.pending.values()) {
|
|
231
|
+
clearTimeout(p.timer);
|
|
232
|
+
p.reject(e);
|
|
233
|
+
}
|
|
234
|
+
this.pending.clear();
|
|
235
|
+
}
|
|
236
|
+
write(msg) {
|
|
237
|
+
if (!this.proc?.stdin) throw new Error("MCP stdio transport not started");
|
|
238
|
+
this.proc.stdin.write(JSON.stringify(msg) + "\n");
|
|
239
|
+
}
|
|
240
|
+
request(method, params) {
|
|
241
|
+
const id = this.nextId++;
|
|
242
|
+
const timeoutMs = this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const timer = setTimeout(() => {
|
|
245
|
+
this.pending.delete(id);
|
|
246
|
+
reject(new Error(`MCP request "${method}" timed out after ${timeoutMs}ms`));
|
|
247
|
+
}, timeoutMs);
|
|
248
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
249
|
+
try {
|
|
250
|
+
this.write({ jsonrpc: "2.0", id, method, params });
|
|
251
|
+
} catch (e) {
|
|
252
|
+
clearTimeout(timer);
|
|
253
|
+
this.pending.delete(id);
|
|
254
|
+
reject(e);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async notify(method, params) {
|
|
259
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
260
|
+
}
|
|
261
|
+
async close() {
|
|
262
|
+
this.failAll(new Error("MCP transport closed"));
|
|
263
|
+
try {
|
|
264
|
+
this.proc?.stdin?.end();
|
|
265
|
+
} catch (e) {
|
|
266
|
+
log2.debug("stdin end failed", e);
|
|
267
|
+
}
|
|
268
|
+
this.proc?.kill();
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
var McpAuthError = class extends Error {
|
|
272
|
+
constructor(status, serverName) {
|
|
273
|
+
super(`MCP server${serverName ? ` "${serverName}"` : ""} requires authentication (HTTP ${status}) \u2014 set bearerToken or headers`);
|
|
274
|
+
this.status = status;
|
|
275
|
+
this.serverName = serverName;
|
|
276
|
+
this.name = "McpAuthError";
|
|
277
|
+
}
|
|
278
|
+
status;
|
|
279
|
+
serverName;
|
|
280
|
+
needsAuth = true;
|
|
281
|
+
};
|
|
282
|
+
var HttpTransport = class {
|
|
283
|
+
constructor(spec, fetchImpl) {
|
|
284
|
+
this.spec = spec;
|
|
285
|
+
this.fetchImpl = fetchImpl ?? fetch;
|
|
286
|
+
}
|
|
287
|
+
spec;
|
|
288
|
+
nextId = 1;
|
|
289
|
+
sessionId;
|
|
290
|
+
fetchImpl;
|
|
291
|
+
async start() {
|
|
292
|
+
}
|
|
293
|
+
request(method, params) {
|
|
294
|
+
return this.post({ jsonrpc: "2.0", id: this.nextId++, method, params }, false);
|
|
295
|
+
}
|
|
296
|
+
async notify(method, params) {
|
|
297
|
+
await this.post({ jsonrpc: "2.0", method, params }, true);
|
|
298
|
+
}
|
|
299
|
+
async post(msg, isNotify) {
|
|
300
|
+
const headers = {
|
|
301
|
+
"content-type": "application/json",
|
|
302
|
+
accept: "application/json, text/event-stream",
|
|
303
|
+
...this.spec.bearerToken ? { authorization: `Bearer ${this.spec.bearerToken}` } : {},
|
|
304
|
+
...this.spec.headers,
|
|
305
|
+
// explicit headers win — lets a caller override the bearer sugar if needed
|
|
306
|
+
...this.sessionId ? { "mcp-session-id": this.sessionId } : {}
|
|
307
|
+
};
|
|
308
|
+
const resp = await this.fetchImpl(this.spec.url, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
headers,
|
|
311
|
+
body: JSON.stringify(msg),
|
|
312
|
+
signal: AbortSignal.timeout(this.spec.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
313
|
+
});
|
|
314
|
+
const sid = resp.headers.get("mcp-session-id");
|
|
315
|
+
if (sid) this.sessionId = sid;
|
|
316
|
+
if (resp.status === 401 || resp.status === 403) throw new McpAuthError(resp.status);
|
|
317
|
+
if (!resp.ok) throw new Error(`MCP HTTP ${resp.status} ${resp.statusText}`);
|
|
318
|
+
if (isNotify) return;
|
|
319
|
+
const ct = resp.headers.get("content-type") ?? "";
|
|
320
|
+
const data = ct.includes("text/event-stream") ? parseSseResponse(await resp.text()) : await resp.json();
|
|
321
|
+
if (data?.error) throw new Error(data.error?.message ?? JSON.stringify(data.error));
|
|
322
|
+
return data?.result;
|
|
323
|
+
}
|
|
324
|
+
async close() {
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
function parseSseResponse(body) {
|
|
328
|
+
for (const line of body.split("\n")) {
|
|
329
|
+
const trimmed = line.trimStart();
|
|
330
|
+
if (!trimmed.startsWith("data:")) continue;
|
|
331
|
+
try {
|
|
332
|
+
const obj = JSON.parse(trimmed.slice(5).trim());
|
|
333
|
+
if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
|
|
334
|
+
} catch (e) {
|
|
335
|
+
log2.debug("skipping unparseable SSE data line", e);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return {};
|
|
339
|
+
}
|
|
340
|
+
var McpClient = class {
|
|
341
|
+
constructor(transport, clientInfo = { name: "agentx", version: "0" }) {
|
|
342
|
+
this.transport = transport;
|
|
343
|
+
this.clientInfo = clientInfo;
|
|
344
|
+
}
|
|
345
|
+
transport;
|
|
346
|
+
clientInfo;
|
|
347
|
+
/** `initialize` handshake → `notifications/initialized`. Returns the server's init result. */
|
|
348
|
+
async connect() {
|
|
349
|
+
await this.transport.start();
|
|
350
|
+
const result = await this.transport.request("initialize", {
|
|
351
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
352
|
+
capabilities: {},
|
|
353
|
+
clientInfo: this.clientInfo
|
|
354
|
+
});
|
|
355
|
+
await this.transport.notify("notifications/initialized");
|
|
356
|
+
return result ?? {};
|
|
357
|
+
}
|
|
358
|
+
async listTools() {
|
|
359
|
+
const r = await this.transport.request("tools/list");
|
|
360
|
+
return Array.isArray(r?.tools) ? r.tools : [];
|
|
361
|
+
}
|
|
362
|
+
async callTool(name, args) {
|
|
363
|
+
return this.transport.request("tools/call", { name, arguments: args ?? {} });
|
|
364
|
+
}
|
|
365
|
+
async listResources() {
|
|
366
|
+
const r = await this.transport.request("resources/list");
|
|
367
|
+
return Array.isArray(r?.resources) ? r.resources : [];
|
|
368
|
+
}
|
|
369
|
+
async readResource(uri) {
|
|
370
|
+
return this.transport.request("resources/read", { uri });
|
|
371
|
+
}
|
|
372
|
+
async close() {
|
|
373
|
+
await this.transport.close();
|
|
374
|
+
}
|
|
375
|
+
};
|
|
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 });
|
|
378
|
+
}
|
|
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, config: cfg };
|
|
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;
|
|
413
|
+
if (!cfg.command && !cfg.url) {
|
|
414
|
+
log2.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
415
|
+
return false;
|
|
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.debug(`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
|
+
var defaultFailures = /* @__PURE__ */ new Map();
|
|
511
|
+
var bgInflight = /* @__PURE__ */ new Set();
|
|
512
|
+
var DEFAULT_FAILURE_COOLDOWN_MS = 6e4;
|
|
513
|
+
async function connectAndList(name, cfg, key, opts) {
|
|
514
|
+
const catalog = opts.catalog ?? defaultCatalog;
|
|
515
|
+
const pool = opts.pool ?? defaultPool;
|
|
516
|
+
const failures = opts.failures ?? defaultFailures;
|
|
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
|
+
failures.delete(key);
|
|
525
|
+
if (opts.keepWarm && !cfg.url) pool.put(key, client);
|
|
526
|
+
else await client.close().catch(() => {
|
|
527
|
+
});
|
|
528
|
+
return { name, cfg, key, specs };
|
|
529
|
+
} catch (e) {
|
|
530
|
+
await client.close().catch(() => {
|
|
531
|
+
});
|
|
532
|
+
failures.set(key, Date.now() + (opts.failureCooldownMs ?? DEFAULT_FAILURE_COOLDOWN_MS));
|
|
533
|
+
logMountFailure(name, e);
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function discoverOne(name, cfg, opts) {
|
|
538
|
+
const catalog = opts.catalog ?? defaultCatalog;
|
|
539
|
+
const failures = opts.failures ?? defaultFailures;
|
|
540
|
+
const key = mcpConfigKey(cfg);
|
|
541
|
+
const cached = catalog.get(key);
|
|
542
|
+
if (cached) return { name, cfg, key, specs: cached };
|
|
543
|
+
const until = failures.get(key);
|
|
544
|
+
if (until && until > Date.now()) return null;
|
|
545
|
+
if (opts.discover === "cache-only") {
|
|
546
|
+
if (!bgInflight.has(key)) {
|
|
547
|
+
bgInflight.add(key);
|
|
548
|
+
void connectAndList(name, cfg, key, opts).finally(() => bgInflight.delete(key));
|
|
549
|
+
}
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
return connectAndList(name, cfg, key, opts);
|
|
553
|
+
}
|
|
554
|
+
async function mountMcpCatalog(servers = {}, opts = {}) {
|
|
555
|
+
const pool = opts.pool ?? defaultPool;
|
|
556
|
+
const discovered = (await Promise.all(validEntries(servers).map(([name, cfg]) => discoverOne(name, cfg, opts)))).filter(Boolean);
|
|
557
|
+
const byName = new Map(discovered.map((d) => [d.name, d]));
|
|
558
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
559
|
+
const live = [];
|
|
560
|
+
const connect = (name) => {
|
|
561
|
+
let p = inflight.get(name);
|
|
562
|
+
if (p) return p;
|
|
563
|
+
p = (async () => {
|
|
564
|
+
const d = byName.get(name);
|
|
565
|
+
if (!d) throw new Error(`MCP server '${name}' is not in the catalog`);
|
|
566
|
+
const warmable = opts.keepWarm && !d.cfg.url;
|
|
567
|
+
if (warmable) {
|
|
568
|
+
const w = pool.get(d.key);
|
|
569
|
+
if (w) return w;
|
|
570
|
+
}
|
|
571
|
+
const client = new McpClient(buildTransport(d.cfg));
|
|
572
|
+
await client.connect();
|
|
573
|
+
if (warmable) pool.put(d.key, client);
|
|
574
|
+
else live.push(client);
|
|
575
|
+
return client;
|
|
576
|
+
})();
|
|
577
|
+
inflight.set(name, p);
|
|
578
|
+
return p;
|
|
579
|
+
};
|
|
580
|
+
const search = makeLazyMcpToolSearch(
|
|
581
|
+
discovered.map((d) => ({ name: d.name, specs: d.specs })),
|
|
582
|
+
async (server, rawName, args) => (await connect(server)).callTool(rawName, args),
|
|
583
|
+
opts
|
|
584
|
+
);
|
|
585
|
+
const close = async () => {
|
|
586
|
+
for (const c of live) await c.close().catch(() => {
|
|
587
|
+
});
|
|
588
|
+
live.length = 0;
|
|
589
|
+
inflight.clear();
|
|
590
|
+
};
|
|
591
|
+
return { ...search, connect, close };
|
|
592
|
+
}
|
|
593
|
+
async function warmMcpCatalog(servers = {}, opts = {}) {
|
|
594
|
+
const entries = validEntries(servers);
|
|
595
|
+
const discovered = (await Promise.all(entries.map(([name, cfg]) => discoverOne(name, cfg, { ...opts, discover: "connect" })))).filter(Boolean);
|
|
596
|
+
const warmedNames = new Set(discovered.map((d) => d.name));
|
|
597
|
+
return {
|
|
598
|
+
warmed: discovered.map((d) => d.name),
|
|
599
|
+
failed: entries.map(([n]) => n).filter((n) => !warmedNames.has(n)),
|
|
600
|
+
toolCount: discovered.reduce((s, d) => s + d.specs.length, 0)
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
export {
|
|
604
|
+
HttpTransport,
|
|
605
|
+
McpAuthError,
|
|
606
|
+
McpClient,
|
|
607
|
+
McpPool,
|
|
608
|
+
MemMcpCatalog,
|
|
609
|
+
StdioTransport,
|
|
610
|
+
mcpConfigKey,
|
|
611
|
+
mountMcpCatalog,
|
|
612
|
+
mountMcpDeferred,
|
|
613
|
+
mountMcpServer,
|
|
614
|
+
mountMcpServers,
|
|
615
|
+
warmMcpCatalog
|
|
616
|
+
};
|
|
617
|
+
//# sourceMappingURL=mcp.client.js.map
|