@dudko.dev/agent-web 0.0.7 → 0.0.9

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/mcp.cjs CHANGED
@@ -20,17 +20,386 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/mcp/index.ts
21
21
  var mcp_exports = {};
22
22
  __export(mcp_exports, {
23
+ BrowserOAuthProvider: () => BrowserOAuthProvider,
24
+ MemoryOAuthStorage: () => MemoryOAuthStorage,
25
+ VaultOAuthStorage: () => VaultOAuthStorage,
26
+ beginMcpOAuth: () => beginMcpOAuth,
23
27
  connectMcpHttp: () => connectMcpHttp,
24
- flattenContent: () => flattenContent
28
+ finishMcpOAuth: () => finishMcpOAuth,
29
+ flattenContent: () => flattenContent,
30
+ readOAuthCallback: () => readOAuthCallback
25
31
  });
26
32
  module.exports = __toCommonJS(mcp_exports);
27
33
 
28
34
  // src/mcp/http.ts
35
+ var import_auth2 = require("@modelcontextprotocol/sdk/client/auth.js");
29
36
  var import_client = require("@modelcontextprotocol/sdk/client/index.js");
30
37
  var import_streamableHttp = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
38
+ var import_types = require("@modelcontextprotocol/sdk/types.js");
31
39
  var import_ai = require("ai");
40
+
41
+ // src/mcp/oauth.ts
42
+ var import_auth = require("@modelcontextprotocol/sdk/client/auth.js");
43
+
44
+ // src/storage/db.ts
45
+ var import_idb = require("idb");
46
+ var KEYS_STORE = "keys";
47
+ var SECRETS_STORE = "secrets";
48
+ var SESSIONS_STORE = "sessions";
49
+ var cache = /* @__PURE__ */ new Map();
50
+ var openAgentWebDB = (opts = {}) => {
51
+ const name = opts.dbName ?? "agent-web";
52
+ let p = cache.get(name);
53
+ if (!p) {
54
+ p = (0, import_idb.openDB)(name, 1, {
55
+ upgrade(db) {
56
+ if (!db.objectStoreNames.contains(KEYS_STORE)) db.createObjectStore(KEYS_STORE);
57
+ if (!db.objectStoreNames.contains(SECRETS_STORE)) db.createObjectStore(SECRETS_STORE);
58
+ if (!db.objectStoreNames.contains(SESSIONS_STORE)) db.createObjectStore(SESSIONS_STORE);
59
+ }
60
+ });
61
+ cache.set(name, p);
62
+ p.catch(() => {
63
+ if (cache.get(name) === p) cache.delete(name);
64
+ });
65
+ }
66
+ return p;
67
+ };
68
+
69
+ // src/secrets/crypto.ts
70
+ var KEY_ID = "vault-key-v1";
71
+ var webcrypto = () => {
72
+ const c = globalThis.crypto;
73
+ if (!c?.subtle || !c.getRandomValues) {
74
+ throw new Error("WebCrypto (crypto.subtle) is unavailable in this environment");
75
+ }
76
+ return c;
77
+ };
78
+ var loadOrCreateKey = async (dbName) => {
79
+ const db = await openAgentWebDB({ dbName });
80
+ const existing = await db.get(KEYS_STORE, KEY_ID);
81
+ if (existing) return existing;
82
+ const key = await webcrypto().subtle.generateKey(
83
+ { name: "AES-GCM", length: 256 },
84
+ /* extractable */
85
+ false,
86
+ ["encrypt", "decrypt"]
87
+ );
88
+ const tx = db.transaction(KEYS_STORE, "readwrite");
89
+ const winner = await tx.store.get(KEY_ID);
90
+ if (winner) {
91
+ await tx.done;
92
+ return winner;
93
+ }
94
+ await tx.store.put(key, KEY_ID);
95
+ await tx.done;
96
+ return key;
97
+ };
98
+ var keyCache = /* @__PURE__ */ new Map();
99
+ var getOrCreateVaultKey = (dbName) => {
100
+ const cacheKey = dbName ?? "agent-web";
101
+ let p = keyCache.get(cacheKey);
102
+ if (!p) {
103
+ p = loadOrCreateKey(dbName);
104
+ keyCache.set(cacheKey, p);
105
+ p.catch(() => {
106
+ if (keyCache.get(cacheKey) === p) keyCache.delete(cacheKey);
107
+ });
108
+ }
109
+ return p;
110
+ };
111
+ var textEncoder = new TextEncoder();
112
+ var textDecoder = new TextDecoder();
113
+ var encryptJSON = async (key, value) => {
114
+ const c = webcrypto();
115
+ const iv = c.getRandomValues(new Uint8Array(12));
116
+ const data = textEncoder.encode(JSON.stringify(value));
117
+ const ciphertext = await c.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
118
+ return { iv, ciphertext };
119
+ };
120
+ var decryptJSON = async (key, blob) => {
121
+ const iv = new Uint8Array(blob.iv);
122
+ const plain = await webcrypto().subtle.decrypt({ name: "AES-GCM", iv }, key, blob.ciphertext);
123
+ return JSON.parse(textDecoder.decode(plain));
124
+ };
125
+
126
+ // src/secrets/vault.ts
127
+ var IndexedDBVault = class {
128
+ dbName;
129
+ constructor(opts = {}) {
130
+ this.dbName = opts.dbName;
131
+ }
132
+ key() {
133
+ return getOrCreateVaultKey(this.dbName);
134
+ }
135
+ /** Encrypt and store a secret string under `id`. */
136
+ async setSecret(id, value) {
137
+ const blob = await encryptJSON(await this.key(), value);
138
+ const db = await openAgentWebDB({ dbName: this.dbName });
139
+ await db.put(SECRETS_STORE, blob, id);
140
+ }
141
+ /** Fetch and decrypt a secret, or undefined if absent. */
142
+ async getSecret(id) {
143
+ const db = await openAgentWebDB({ dbName: this.dbName });
144
+ const blob = await db.get(SECRETS_STORE, id);
145
+ if (!blob) return void 0;
146
+ return decryptJSON(await this.key(), blob);
147
+ }
148
+ async deleteSecret(id) {
149
+ const db = await openAgentWebDB({ dbName: this.dbName });
150
+ await db.delete(SECRETS_STORE, id);
151
+ }
152
+ async listSecretIds() {
153
+ const db = await openAgentWebDB({ dbName: this.dbName });
154
+ return (await db.getAllKeys(SECRETS_STORE)).map(String);
155
+ }
156
+ };
157
+
158
+ // src/mcp/oauth.ts
159
+ var VaultOAuthStorage = class {
160
+ vault;
161
+ constructor(opts = {}) {
162
+ this.vault = new IndexedDBVault(opts);
163
+ }
164
+ get(key) {
165
+ return this.vault.getSecret(key);
166
+ }
167
+ set(key, value) {
168
+ return this.vault.setSecret(key, value);
169
+ }
170
+ delete(key) {
171
+ return this.vault.deleteSecret(key);
172
+ }
173
+ };
174
+ var MemoryOAuthStorage = class {
175
+ map = /* @__PURE__ */ new Map();
176
+ async get(key) {
177
+ return this.map.get(key);
178
+ }
179
+ async set(key, value) {
180
+ this.map.set(key, value);
181
+ }
182
+ async delete(key) {
183
+ this.map.delete(key);
184
+ }
185
+ };
186
+ var DEFAULT_SKEW_SECONDS = 30;
187
+ var namespaceFor = (serverUrl) => {
188
+ const u = new URL(serverUrl);
189
+ return `mcp-oauth:${u.origin}${u.pathname.replace(/\/+$/, "")}${u.search}`;
190
+ };
191
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
192
+ var assertSecureOAuthUrl = (url, label = "MCP server") => {
193
+ let parsed;
194
+ try {
195
+ parsed = new URL(url);
196
+ } catch {
197
+ throw new Error(`${label}: "${url}" is not a valid URL`);
198
+ }
199
+ if (parsed.protocol === "https:" || LOOPBACK_HOSTS.has(parsed.hostname)) return;
200
+ throw new Error(
201
+ `${label}: MCP OAuth requires https (or a loopback host) - refusing to send tokens to "${url}"`
202
+ );
203
+ };
204
+ var randomToken = () => {
205
+ const bytes = new Uint8Array(32);
206
+ globalThis.crypto.getRandomValues(bytes);
207
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
208
+ };
209
+ var BrowserOAuthProvider = class {
210
+ /** Set when the SDK asks for an authorization redirect; read it in your UI. */
211
+ authorizationUrl;
212
+ serverUrl;
213
+ storage;
214
+ ns;
215
+ _redirectUrl;
216
+ _clientMetadata;
217
+ skewMs;
218
+ onRedirect;
219
+ constructor(opts) {
220
+ assertSecureOAuthUrl(opts.serverUrl);
221
+ this.serverUrl = opts.serverUrl;
222
+ this.storage = opts.storage ?? new VaultOAuthStorage();
223
+ this.ns = namespaceFor(opts.serverUrl);
224
+ this._redirectUrl = String(opts.redirectUrl);
225
+ this.skewMs = (opts.expirySkewSeconds ?? DEFAULT_SKEW_SECONDS) * 1e3;
226
+ this.onRedirect = opts.onRedirect;
227
+ this._clientMetadata = {
228
+ client_name: opts.clientName ?? "agent-web",
229
+ redirect_uris: [this._redirectUrl],
230
+ grant_types: ["authorization_code", "refresh_token"],
231
+ response_types: ["code"],
232
+ // Public client: a browser cannot hold a client_secret. PKCE is what
233
+ // actually protects the code exchange.
234
+ token_endpoint_auth_method: "none",
235
+ ...opts.scope ? { scope: opts.scope } : {},
236
+ ...opts.clientMetadata
237
+ };
238
+ }
239
+ get redirectUrl() {
240
+ return this._redirectUrl;
241
+ }
242
+ get clientMetadata() {
243
+ return this._clientMetadata;
244
+ }
245
+ key(suffix) {
246
+ return `${this.ns}:${suffix}`;
247
+ }
248
+ async readJSON(suffix) {
249
+ try {
250
+ const raw = await this.storage.get(this.key(suffix));
251
+ if (raw === void 0) return void 0;
252
+ return JSON.parse(raw);
253
+ } catch {
254
+ await this.storage.delete(this.key(suffix)).catch(() => {
255
+ });
256
+ return void 0;
257
+ }
258
+ }
259
+ async state() {
260
+ const value = randomToken();
261
+ await this.storage.set(this.key("state"), value);
262
+ return value;
263
+ }
264
+ /**
265
+ * CSRF check for the redirect back. Single-use: the stored value is dropped
266
+ * whether or not it matched, so a replayed callback can't pass twice.
267
+ */
268
+ async verifyState(returned) {
269
+ const expected = await this.storage.get(this.key("state"));
270
+ await this.storage.delete(this.key("state"));
271
+ return expected !== void 0 && returned !== void 0 && expected === returned;
272
+ }
273
+ async clientInformation() {
274
+ return this.readJSON("client");
275
+ }
276
+ async saveClientInformation(info) {
277
+ await this.storage.set(this.key("client"), JSON.stringify(info));
278
+ }
279
+ async tokens() {
280
+ return (await this.readJSON("tokens"))?.tokens;
281
+ }
282
+ async saveTokens(tokens) {
283
+ const stored = {
284
+ tokens,
285
+ expiresAt: typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : void 0
286
+ };
287
+ await this.storage.set(this.key("tokens"), JSON.stringify(stored));
288
+ }
289
+ async saveCodeVerifier(verifier) {
290
+ await this.storage.set(this.key("verifier"), verifier);
291
+ }
292
+ async codeVerifier() {
293
+ const v = await this.storage.get(this.key("verifier"));
294
+ if (!v) {
295
+ throw new Error(
296
+ "No PKCE code verifier stored: the authorization was started in a different browser profile, or storage was cleared mid-flow"
297
+ );
298
+ }
299
+ return v;
300
+ }
301
+ async redirectToAuthorization(url) {
302
+ this.authorizationUrl = url;
303
+ await this.onRedirect?.(url);
304
+ }
305
+ /**
306
+ * Called by the SDK when the server rejects what we hold: `'tokens'` after an
307
+ * `invalid_grant` (dead refresh token), `'client'` / `'all'` after an
308
+ * `invalid_client` (the AS forgot our dynamic registration). Clearing lets
309
+ * the SDK re-register and re-authorize without the user clearing site data.
310
+ */
311
+ async invalidateCredentials(scope) {
312
+ const targets = scope === "all" ? ["tokens", "client", "verifier", "state"] : [scope];
313
+ await Promise.all(targets.map((t) => this.storage.delete(this.key(t))));
314
+ }
315
+ /** True when there is no access token, or it expires within the skew window. */
316
+ async isAccessTokenExpired() {
317
+ const stored = await this.readJSON("tokens");
318
+ if (!stored?.tokens?.access_token) return true;
319
+ if (stored.expiresAt === void 0) return false;
320
+ return Date.now() + this.skewMs >= stored.expiresAt;
321
+ }
322
+ /** True once an authorization has completed for this server. */
323
+ async isAuthorized() {
324
+ return await this.tokens() !== void 0;
325
+ }
326
+ /** Forget tokens, registration, verifier and state for this server. */
327
+ async reset() {
328
+ this.authorizationUrl = void 0;
329
+ await this.invalidateCredentials("all");
330
+ }
331
+ };
332
+ var readOAuthCallback = (input) => {
333
+ const href = input ? String(input) : typeof globalThis.location !== "undefined" ? globalThis.location.href : void 0;
334
+ if (!href) return void 0;
335
+ let url;
336
+ try {
337
+ url = new URL(href);
338
+ } catch {
339
+ return void 0;
340
+ }
341
+ const params = new URLSearchParams(url.search);
342
+ const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
343
+ const q = hash.indexOf("?");
344
+ if (q >= 0) {
345
+ for (const [k, v] of new URLSearchParams(hash.slice(q + 1))) {
346
+ if (!params.has(k)) params.set(k, v);
347
+ }
348
+ }
349
+ const code = params.get("code") ?? void 0;
350
+ const error = params.get("error") ?? void 0;
351
+ if (!code && !error) return void 0;
352
+ return {
353
+ code,
354
+ state: params.get("state") ?? void 0,
355
+ error,
356
+ errorDescription: params.get("error_description") ?? void 0
357
+ };
358
+ };
359
+ var finishMcpOAuth = async (provider, callback, opts = {}) => {
360
+ if (callback.error) {
361
+ throw new Error(
362
+ `Authorization denied by the server: ${callback.error}${callback.errorDescription ? ` - ${callback.errorDescription}` : ""}`
363
+ );
364
+ }
365
+ if (!callback.code) {
366
+ throw new Error("Authorization callback carried no code");
367
+ }
368
+ if (!await provider.verifyState(callback.state)) {
369
+ throw new Error("Authorization state mismatch; refusing to exchange the code (possible CSRF)");
370
+ }
371
+ const result = await (0, import_auth.auth)(provider, {
372
+ serverUrl: provider.serverUrl,
373
+ authorizationCode: callback.code,
374
+ fetchFn: opts.fetch
375
+ });
376
+ if (result !== "AUTHORIZED") {
377
+ throw new Error(`Token exchange did not complete (SDK returned "${result}")`);
378
+ }
379
+ };
380
+ var beginMcpOAuth = async (provider, opts = {}) => {
381
+ provider.authorizationUrl = void 0;
382
+ return (0, import_auth.auth)(provider, { serverUrl: provider.serverUrl, fetchFn: opts.fetch });
383
+ };
384
+
385
+ // src/mcp/http.ts
32
386
  var MAX_TOOL_NAME_LEN = 64;
387
+ var DEFAULT_RECONNECTION = {
388
+ maxReconnectionDelay: 3e4,
389
+ initialReconnectionDelay: 1e3,
390
+ reconnectionDelayGrowFactor: 1.5,
391
+ maxRetries: 5
392
+ };
33
393
  var sanitizeName = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "_");
394
+ var uniqueKey = (base, taken) => {
395
+ if (!Object.hasOwn(taken, base)) return base;
396
+ for (let n = 2; n < 100; n++) {
397
+ const suffix = `_${n}`;
398
+ const candidate = base.slice(0, MAX_TOOL_NAME_LEN - suffix.length) + suffix;
399
+ if (!Object.hasOwn(taken, candidate)) return candidate;
400
+ }
401
+ return "";
402
+ };
34
403
  var flattenContent = (content) => {
35
404
  if (!Array.isArray(content)) return content;
36
405
  const parts = content;
@@ -38,58 +407,96 @@ var flattenContent = (content) => {
38
407
  if (allText) return parts.map((p) => p.text).join("\n");
39
408
  return parts.map((p) => p?.type === "text" && typeof p.text === "string" ? p.text : p);
40
409
  };
41
- var connectOne = async (name, cfg, opts, log) => {
410
+ var RefreshOnlyAbort = class extends Error {
411
+ };
412
+ var refreshOnly = (p) => ({
413
+ // Must stay truthy: `auth()` reads a missing redirectUrl as "non-interactive
414
+ // grant" and would try to fetch a token without one.
415
+ get redirectUrl() {
416
+ return p.redirectUrl;
417
+ },
418
+ get clientMetadata() {
419
+ return p.clientMetadata;
420
+ },
421
+ clientInformation: () => p.clientInformation(),
422
+ saveClientInformation: p.saveClientInformation?.bind(p),
423
+ tokens: () => p.tokens(),
424
+ saveTokens: (t) => p.saveTokens(t),
425
+ invalidateCredentials: p.invalidateCredentials?.bind(p),
426
+ addClientAuthentication: p.addClientAuthentication?.bind(p),
427
+ validateResourceURL: p.validateResourceURL?.bind(p),
428
+ codeVerifier: () => p.codeVerifier(),
429
+ state: () => {
430
+ throw new RefreshOnlyAbort();
431
+ },
432
+ saveCodeVerifier: () => {
433
+ throw new RefreshOnlyAbort();
434
+ },
435
+ redirectToAuthorization: () => {
436
+ throw new RefreshOnlyAbort();
437
+ }
438
+ });
439
+ var refreshIfExpired = async (provider, serverUrl, fetchFn, log, name) => {
440
+ const check = provider.isAccessTokenExpired;
441
+ if (typeof check !== "function") return;
442
+ if (!await check.call(provider)) return;
443
+ const tokens = await provider.tokens();
444
+ if (!tokens?.refresh_token) return;
445
+ try {
446
+ await (0, import_auth2.auth)(refreshOnly(provider), { serverUrl, fetchFn });
447
+ } catch (err) {
448
+ log(
449
+ "info",
450
+ err instanceof RefreshOnlyAbort ? `[mcp] ${name}: the stored token could not be refreshed; a new authorization is needed` : `[mcp] ${name}: proactive token refresh failed (${err instanceof Error ? err.message : String(err)}); continuing`
451
+ );
452
+ }
453
+ };
454
+ var openConnection = async (name, cfg, opts, log) => {
455
+ let client;
42
456
  try {
43
457
  if (cfg.headers && cfg.getHeaders) {
44
458
  throw new Error(`MCP server "${name}": specify either headers or getHeaders, not both`);
45
459
  }
46
460
  const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers;
461
+ if (cfg.authProvider) {
462
+ if (headers && "Authorization" in headers) {
463
+ log(
464
+ "warn",
465
+ `[mcp] ${name}: an explicit Authorization header shadows the OAuth token from authProvider`
466
+ );
467
+ }
468
+ assertSecureOAuthUrl(cfg.url, name);
469
+ await refreshIfExpired(cfg.authProvider, cfg.url, cfg.fetch, log, name);
470
+ }
47
471
  const transport = new import_streamableHttp.StreamableHTTPClientTransport(new URL(cfg.url), {
48
- requestInit: headers ? { headers } : void 0
472
+ requestInit: headers ? { headers } : void 0,
473
+ authProvider: cfg.authProvider,
474
+ fetch: cfg.fetch,
475
+ reconnectionOptions: { ...DEFAULT_RECONNECTION, ...cfg.reconnection }
49
476
  });
50
- const client = new import_client.Client({
477
+ transport.onerror = (err) => log("warn", `[mcp] ${name}: transport error - ${err.message}`);
478
+ transport.onclose = () => log("info", `[mcp] ${name}: transport closed`);
479
+ client = new import_client.Client({
51
480
  name: opts.clientName ?? "agent-web",
52
481
  version: opts.clientVersion ?? "0.0.0"
53
482
  });
54
483
  await client.connect(transport);
55
- const listed = await client.listTools();
56
- const mounts = [];
57
- for (const t of listed.tools) {
58
- const prefixed = `${sanitizeName(name)}__${sanitizeName(t.name)}`;
59
- if (prefixed.length > MAX_TOOL_NAME_LEN) {
60
- log(
61
- "warn",
62
- `[mcp] ${name}: tool "${t.name}" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`
63
- );
64
- continue;
65
- }
66
- const description = t.description ?? "";
67
- mounts.push({
68
- key: prefixed,
69
- description,
70
- tool: (0, import_ai.dynamicTool)({
71
- description,
72
- inputSchema: (0, import_ai.jsonSchema)(t.inputSchema),
73
- execute: async (args, options) => {
74
- const res = await client.callTool(
75
- { name: t.name, arguments: args ?? {} },
76
- void 0,
77
- options?.abortSignal ? { signal: options.abortSignal } : void 0
78
- );
79
- const flat = flattenContent(res.content);
80
- if (res.isError) {
81
- throw new Error(typeof flat === "string" ? flat : JSON.stringify(flat));
82
- }
83
- return flat;
84
- }
85
- })
86
- });
484
+ if (opts.onToolsChanged) {
485
+ const notify = opts.onToolsChanged;
486
+ client.setNotificationHandler(import_types.ToolListChangedNotificationSchema, () => notify(name));
87
487
  }
88
- return { name, client, mounts };
488
+ const listed = (await client.listTools()).tools;
489
+ return { name, client, listed };
89
490
  } catch (err) {
491
+ if (client) await client.close().catch(() => {
492
+ });
493
+ const needsAuthorization = err instanceof import_auth2.UnauthorizedError;
90
494
  const message = err instanceof Error ? err.message : String(err);
91
- log("error", `[mcp] ${name}: failed to connect - ${message}`);
92
- return { name, mounts: [], error: message };
495
+ log(
496
+ "error",
497
+ needsAuthorization ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl` : `[mcp] ${name}: failed to connect - ${message}`
498
+ );
499
+ return { name, error: message, needsAuthorization };
93
500
  }
94
501
  };
95
502
  var connectMcpHttp = async (servers, opts = {}) => {
@@ -99,32 +506,85 @@ var connectMcpHttp = async (servers, opts = {}) => {
99
506
  const tools = {};
100
507
  const catalog = [];
101
508
  const results = [];
102
- const settled = await Promise.all(
103
- Object.entries(servers).map(([name, cfg]) => connectOne(name, cfg, opts, log))
104
- );
105
- for (const server of settled) {
106
- if (server.error !== void 0) {
107
- results.push({ name: server.name, connected: false, error: server.error });
108
- continue;
509
+ const mountedKeys = /* @__PURE__ */ new Map();
510
+ const mountTools = (name, client, listed) => {
511
+ const prefix = `${sanitizeName(name)}__`;
512
+ for (const k of mountedKeys.get(name) ?? []) {
513
+ delete tools[k];
514
+ }
515
+ for (let i = catalog.length - 1; i >= 0; i--) {
516
+ if (catalog[i].server === name) catalog.splice(i, 1);
109
517
  }
110
- if (server.client) clients.set(server.name, server.client);
518
+ const keys = [];
111
519
  let mounted = 0;
112
- for (const m of server.mounts) {
113
- if (tools[m.key]) {
114
- log("warn", `[mcp] ${server.name}: duplicate tool name "${m.key}"; keeping the first`);
520
+ for (const t of listed) {
521
+ const prefixed = prefix + sanitizeName(t.name);
522
+ if (prefixed.length > MAX_TOOL_NAME_LEN) {
523
+ log(
524
+ "warn",
525
+ `[mcp] ${name}: tool "${t.name}" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`
526
+ );
527
+ continue;
528
+ }
529
+ const key = uniqueKey(prefixed, tools);
530
+ if (!key) {
531
+ log("warn", `[mcp] ${name}: cannot find a free name for tool "${t.name}"; skipping`);
115
532
  continue;
116
533
  }
117
- tools[m.key] = m.tool;
118
- catalog.push({ name: m.key, description: m.description, server: server.name });
534
+ if (key !== prefixed) {
535
+ log("warn", `[mcp] ${name}: tool name "${prefixed}" already taken; mounted as "${key}"`);
536
+ }
537
+ const description = t.description ?? "";
538
+ tools[key] = (0, import_ai.dynamicTool)({
539
+ description,
540
+ inputSchema: (0, import_ai.jsonSchema)(t.inputSchema),
541
+ execute: async (args, options) => {
542
+ const res = await client.callTool(
543
+ { name: t.name, arguments: args ?? {} },
544
+ void 0,
545
+ options?.abortSignal ? { signal: options.abortSignal } : void 0
546
+ );
547
+ const flat = flattenContent(res.content);
548
+ if (res.isError) {
549
+ throw new Error(typeof flat === "string" ? flat : JSON.stringify(flat));
550
+ }
551
+ return flat;
552
+ }
553
+ });
554
+ catalog.push({ name: key, description, server: name });
555
+ keys.push(key);
119
556
  mounted += 1;
120
557
  }
121
- log("info", `[mcp] ${server.name}: ${mounted} tools mounted`);
558
+ mountedKeys.set(name, keys);
559
+ log("info", `[mcp] ${name}: ${mounted} tools mounted`);
560
+ };
561
+ const connects = await Promise.all(
562
+ Object.entries(servers).map(([name, cfg]) => openConnection(name, cfg, opts, log))
563
+ );
564
+ for (const server of connects) {
565
+ if (!server.client || !server.listed) {
566
+ results.push({
567
+ name: server.name,
568
+ connected: false,
569
+ error: server.error,
570
+ ...server.needsAuthorization ? { needsAuthorization: true } : {}
571
+ });
572
+ continue;
573
+ }
574
+ clients.set(server.name, server.client);
575
+ mountTools(server.name, server.client, server.listed);
122
576
  results.push({ name: server.name, connected: true });
123
577
  }
124
578
  return {
125
579
  tools,
126
580
  catalog,
127
581
  results,
582
+ refreshServer: async (name) => {
583
+ const client = clients.get(name);
584
+ if (!client) throw new Error(`MCP server "${name}" is not connected`);
585
+ const listed = (await client.listTools()).tools;
586
+ mountTools(name, client, listed);
587
+ },
128
588
  close: async () => {
129
589
  await Promise.allSettled([...clients.values()].map((c) => c.close()));
130
590
  }
@@ -132,7 +592,13 @@ var connectMcpHttp = async (servers, opts = {}) => {
132
592
  };
133
593
  // Annotate the CommonJS export names for ESM import in node:
134
594
  0 && (module.exports = {
595
+ BrowserOAuthProvider,
596
+ MemoryOAuthStorage,
597
+ VaultOAuthStorage,
598
+ beginMcpOAuth,
135
599
  connectMcpHttp,
136
- flattenContent
600
+ finishMcpOAuth,
601
+ flattenContent,
602
+ readOAuthCallback
137
603
  });
138
604
  //# sourceMappingURL=mcp.cjs.map