@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/README.md +49 -0
- package/dist/agent-web.js +95 -88
- package/dist/agent-web.js.map +1 -1
- package/dist/chunk-H7W6MBK2.js +125 -0
- package/dist/chunk-H7W6MBK2.js.map +1 -0
- package/dist/index.d.cts +3 -35
- package/dist/index.d.ts +3 -35
- package/dist/index.js +10 -114
- package/dist/index.js.map +1 -1
- package/dist/mcp.cjs +520 -54
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.d.cts +197 -6
- package/dist/mcp.d.ts +197 -6
- package/dist/mcp.js +407 -54
- package/dist/mcp.js.map +1 -1
- package/dist/vault-BHlMmKXM.d.cts +35 -0
- package/dist/vault-BHlMmKXM.d.ts +35 -0
- package/package.json +13 -13
package/dist/mcp.js
CHANGED
|
@@ -1,9 +1,265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IndexedDBVault
|
|
3
|
+
} from "./chunk-H7W6MBK2.js";
|
|
4
|
+
|
|
1
5
|
// src/mcp/http.ts
|
|
6
|
+
import {
|
|
7
|
+
auth as auth2,
|
|
8
|
+
UnauthorizedError
|
|
9
|
+
} from "@modelcontextprotocol/sdk/client/auth.js";
|
|
2
10
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
StreamableHTTPClientTransport
|
|
13
|
+
} from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
14
|
+
import { ToolListChangedNotificationSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
15
|
import { dynamicTool, jsonSchema } from "ai";
|
|
16
|
+
|
|
17
|
+
// src/mcp/oauth.ts
|
|
18
|
+
import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
19
|
+
var VaultOAuthStorage = class {
|
|
20
|
+
vault;
|
|
21
|
+
constructor(opts = {}) {
|
|
22
|
+
this.vault = new IndexedDBVault(opts);
|
|
23
|
+
}
|
|
24
|
+
get(key) {
|
|
25
|
+
return this.vault.getSecret(key);
|
|
26
|
+
}
|
|
27
|
+
set(key, value) {
|
|
28
|
+
return this.vault.setSecret(key, value);
|
|
29
|
+
}
|
|
30
|
+
delete(key) {
|
|
31
|
+
return this.vault.deleteSecret(key);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
var MemoryOAuthStorage = class {
|
|
35
|
+
map = /* @__PURE__ */ new Map();
|
|
36
|
+
async get(key) {
|
|
37
|
+
return this.map.get(key);
|
|
38
|
+
}
|
|
39
|
+
async set(key, value) {
|
|
40
|
+
this.map.set(key, value);
|
|
41
|
+
}
|
|
42
|
+
async delete(key) {
|
|
43
|
+
this.map.delete(key);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
var DEFAULT_SKEW_SECONDS = 30;
|
|
47
|
+
var namespaceFor = (serverUrl) => {
|
|
48
|
+
const u = new URL(serverUrl);
|
|
49
|
+
return `mcp-oauth:${u.origin}${u.pathname.replace(/\/+$/, "")}${u.search}`;
|
|
50
|
+
};
|
|
51
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]", "::1"]);
|
|
52
|
+
var assertSecureOAuthUrl = (url, label = "MCP server") => {
|
|
53
|
+
let parsed;
|
|
54
|
+
try {
|
|
55
|
+
parsed = new URL(url);
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error(`${label}: "${url}" is not a valid URL`);
|
|
58
|
+
}
|
|
59
|
+
if (parsed.protocol === "https:" || LOOPBACK_HOSTS.has(parsed.hostname)) return;
|
|
60
|
+
throw new Error(
|
|
61
|
+
`${label}: MCP OAuth requires https (or a loopback host) - refusing to send tokens to "${url}"`
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
var randomToken = () => {
|
|
65
|
+
const bytes = new Uint8Array(32);
|
|
66
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
67
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
68
|
+
};
|
|
69
|
+
var BrowserOAuthProvider = class {
|
|
70
|
+
/** Set when the SDK asks for an authorization redirect; read it in your UI. */
|
|
71
|
+
authorizationUrl;
|
|
72
|
+
serverUrl;
|
|
73
|
+
storage;
|
|
74
|
+
ns;
|
|
75
|
+
_redirectUrl;
|
|
76
|
+
_clientMetadata;
|
|
77
|
+
skewMs;
|
|
78
|
+
onRedirect;
|
|
79
|
+
constructor(opts) {
|
|
80
|
+
assertSecureOAuthUrl(opts.serverUrl);
|
|
81
|
+
this.serverUrl = opts.serverUrl;
|
|
82
|
+
this.storage = opts.storage ?? new VaultOAuthStorage();
|
|
83
|
+
this.ns = namespaceFor(opts.serverUrl);
|
|
84
|
+
this._redirectUrl = String(opts.redirectUrl);
|
|
85
|
+
this.skewMs = (opts.expirySkewSeconds ?? DEFAULT_SKEW_SECONDS) * 1e3;
|
|
86
|
+
this.onRedirect = opts.onRedirect;
|
|
87
|
+
this._clientMetadata = {
|
|
88
|
+
client_name: opts.clientName ?? "agent-web",
|
|
89
|
+
redirect_uris: [this._redirectUrl],
|
|
90
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
91
|
+
response_types: ["code"],
|
|
92
|
+
// Public client: a browser cannot hold a client_secret. PKCE is what
|
|
93
|
+
// actually protects the code exchange.
|
|
94
|
+
token_endpoint_auth_method: "none",
|
|
95
|
+
...opts.scope ? { scope: opts.scope } : {},
|
|
96
|
+
...opts.clientMetadata
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
get redirectUrl() {
|
|
100
|
+
return this._redirectUrl;
|
|
101
|
+
}
|
|
102
|
+
get clientMetadata() {
|
|
103
|
+
return this._clientMetadata;
|
|
104
|
+
}
|
|
105
|
+
key(suffix) {
|
|
106
|
+
return `${this.ns}:${suffix}`;
|
|
107
|
+
}
|
|
108
|
+
async readJSON(suffix) {
|
|
109
|
+
try {
|
|
110
|
+
const raw = await this.storage.get(this.key(suffix));
|
|
111
|
+
if (raw === void 0) return void 0;
|
|
112
|
+
return JSON.parse(raw);
|
|
113
|
+
} catch {
|
|
114
|
+
await this.storage.delete(this.key(suffix)).catch(() => {
|
|
115
|
+
});
|
|
116
|
+
return void 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async state() {
|
|
120
|
+
const value = randomToken();
|
|
121
|
+
await this.storage.set(this.key("state"), value);
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* CSRF check for the redirect back. Single-use: the stored value is dropped
|
|
126
|
+
* whether or not it matched, so a replayed callback can't pass twice.
|
|
127
|
+
*/
|
|
128
|
+
async verifyState(returned) {
|
|
129
|
+
const expected = await this.storage.get(this.key("state"));
|
|
130
|
+
await this.storage.delete(this.key("state"));
|
|
131
|
+
return expected !== void 0 && returned !== void 0 && expected === returned;
|
|
132
|
+
}
|
|
133
|
+
async clientInformation() {
|
|
134
|
+
return this.readJSON("client");
|
|
135
|
+
}
|
|
136
|
+
async saveClientInformation(info) {
|
|
137
|
+
await this.storage.set(this.key("client"), JSON.stringify(info));
|
|
138
|
+
}
|
|
139
|
+
async tokens() {
|
|
140
|
+
return (await this.readJSON("tokens"))?.tokens;
|
|
141
|
+
}
|
|
142
|
+
async saveTokens(tokens) {
|
|
143
|
+
const stored = {
|
|
144
|
+
tokens,
|
|
145
|
+
expiresAt: typeof tokens.expires_in === "number" ? Date.now() + tokens.expires_in * 1e3 : void 0
|
|
146
|
+
};
|
|
147
|
+
await this.storage.set(this.key("tokens"), JSON.stringify(stored));
|
|
148
|
+
}
|
|
149
|
+
async saveCodeVerifier(verifier) {
|
|
150
|
+
await this.storage.set(this.key("verifier"), verifier);
|
|
151
|
+
}
|
|
152
|
+
async codeVerifier() {
|
|
153
|
+
const v = await this.storage.get(this.key("verifier"));
|
|
154
|
+
if (!v) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
"No PKCE code verifier stored: the authorization was started in a different browser profile, or storage was cleared mid-flow"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return v;
|
|
160
|
+
}
|
|
161
|
+
async redirectToAuthorization(url) {
|
|
162
|
+
this.authorizationUrl = url;
|
|
163
|
+
await this.onRedirect?.(url);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Called by the SDK when the server rejects what we hold: `'tokens'` after an
|
|
167
|
+
* `invalid_grant` (dead refresh token), `'client'` / `'all'` after an
|
|
168
|
+
* `invalid_client` (the AS forgot our dynamic registration). Clearing lets
|
|
169
|
+
* the SDK re-register and re-authorize without the user clearing site data.
|
|
170
|
+
*/
|
|
171
|
+
async invalidateCredentials(scope) {
|
|
172
|
+
const targets = scope === "all" ? ["tokens", "client", "verifier", "state"] : [scope];
|
|
173
|
+
await Promise.all(targets.map((t) => this.storage.delete(this.key(t))));
|
|
174
|
+
}
|
|
175
|
+
/** True when there is no access token, or it expires within the skew window. */
|
|
176
|
+
async isAccessTokenExpired() {
|
|
177
|
+
const stored = await this.readJSON("tokens");
|
|
178
|
+
if (!stored?.tokens?.access_token) return true;
|
|
179
|
+
if (stored.expiresAt === void 0) return false;
|
|
180
|
+
return Date.now() + this.skewMs >= stored.expiresAt;
|
|
181
|
+
}
|
|
182
|
+
/** True once an authorization has completed for this server. */
|
|
183
|
+
async isAuthorized() {
|
|
184
|
+
return await this.tokens() !== void 0;
|
|
185
|
+
}
|
|
186
|
+
/** Forget tokens, registration, verifier and state for this server. */
|
|
187
|
+
async reset() {
|
|
188
|
+
this.authorizationUrl = void 0;
|
|
189
|
+
await this.invalidateCredentials("all");
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var readOAuthCallback = (input) => {
|
|
193
|
+
const href = input ? String(input) : typeof globalThis.location !== "undefined" ? globalThis.location.href : void 0;
|
|
194
|
+
if (!href) return void 0;
|
|
195
|
+
let url;
|
|
196
|
+
try {
|
|
197
|
+
url = new URL(href);
|
|
198
|
+
} catch {
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
const params = new URLSearchParams(url.search);
|
|
202
|
+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
203
|
+
const q = hash.indexOf("?");
|
|
204
|
+
if (q >= 0) {
|
|
205
|
+
for (const [k, v] of new URLSearchParams(hash.slice(q + 1))) {
|
|
206
|
+
if (!params.has(k)) params.set(k, v);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const code = params.get("code") ?? void 0;
|
|
210
|
+
const error = params.get("error") ?? void 0;
|
|
211
|
+
if (!code && !error) return void 0;
|
|
212
|
+
return {
|
|
213
|
+
code,
|
|
214
|
+
state: params.get("state") ?? void 0,
|
|
215
|
+
error,
|
|
216
|
+
errorDescription: params.get("error_description") ?? void 0
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
var finishMcpOAuth = async (provider, callback, opts = {}) => {
|
|
220
|
+
if (callback.error) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Authorization denied by the server: ${callback.error}${callback.errorDescription ? ` - ${callback.errorDescription}` : ""}`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (!callback.code) {
|
|
226
|
+
throw new Error("Authorization callback carried no code");
|
|
227
|
+
}
|
|
228
|
+
if (!await provider.verifyState(callback.state)) {
|
|
229
|
+
throw new Error("Authorization state mismatch; refusing to exchange the code (possible CSRF)");
|
|
230
|
+
}
|
|
231
|
+
const result = await auth(provider, {
|
|
232
|
+
serverUrl: provider.serverUrl,
|
|
233
|
+
authorizationCode: callback.code,
|
|
234
|
+
fetchFn: opts.fetch
|
|
235
|
+
});
|
|
236
|
+
if (result !== "AUTHORIZED") {
|
|
237
|
+
throw new Error(`Token exchange did not complete (SDK returned "${result}")`);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
var beginMcpOAuth = async (provider, opts = {}) => {
|
|
241
|
+
provider.authorizationUrl = void 0;
|
|
242
|
+
return auth(provider, { serverUrl: provider.serverUrl, fetchFn: opts.fetch });
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
// src/mcp/http.ts
|
|
5
246
|
var MAX_TOOL_NAME_LEN = 64;
|
|
247
|
+
var DEFAULT_RECONNECTION = {
|
|
248
|
+
maxReconnectionDelay: 3e4,
|
|
249
|
+
initialReconnectionDelay: 1e3,
|
|
250
|
+
reconnectionDelayGrowFactor: 1.5,
|
|
251
|
+
maxRetries: 5
|
|
252
|
+
};
|
|
6
253
|
var sanitizeName = (s) => s.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
254
|
+
var uniqueKey = (base, taken) => {
|
|
255
|
+
if (!Object.hasOwn(taken, base)) return base;
|
|
256
|
+
for (let n = 2; n < 100; n++) {
|
|
257
|
+
const suffix = `_${n}`;
|
|
258
|
+
const candidate = base.slice(0, MAX_TOOL_NAME_LEN - suffix.length) + suffix;
|
|
259
|
+
if (!Object.hasOwn(taken, candidate)) return candidate;
|
|
260
|
+
}
|
|
261
|
+
return "";
|
|
262
|
+
};
|
|
7
263
|
var flattenContent = (content) => {
|
|
8
264
|
if (!Array.isArray(content)) return content;
|
|
9
265
|
const parts = content;
|
|
@@ -11,58 +267,96 @@ var flattenContent = (content) => {
|
|
|
11
267
|
if (allText) return parts.map((p) => p.text).join("\n");
|
|
12
268
|
return parts.map((p) => p?.type === "text" && typeof p.text === "string" ? p.text : p);
|
|
13
269
|
};
|
|
14
|
-
var
|
|
270
|
+
var RefreshOnlyAbort = class extends Error {
|
|
271
|
+
};
|
|
272
|
+
var refreshOnly = (p) => ({
|
|
273
|
+
// Must stay truthy: `auth()` reads a missing redirectUrl as "non-interactive
|
|
274
|
+
// grant" and would try to fetch a token without one.
|
|
275
|
+
get redirectUrl() {
|
|
276
|
+
return p.redirectUrl;
|
|
277
|
+
},
|
|
278
|
+
get clientMetadata() {
|
|
279
|
+
return p.clientMetadata;
|
|
280
|
+
},
|
|
281
|
+
clientInformation: () => p.clientInformation(),
|
|
282
|
+
saveClientInformation: p.saveClientInformation?.bind(p),
|
|
283
|
+
tokens: () => p.tokens(),
|
|
284
|
+
saveTokens: (t) => p.saveTokens(t),
|
|
285
|
+
invalidateCredentials: p.invalidateCredentials?.bind(p),
|
|
286
|
+
addClientAuthentication: p.addClientAuthentication?.bind(p),
|
|
287
|
+
validateResourceURL: p.validateResourceURL?.bind(p),
|
|
288
|
+
codeVerifier: () => p.codeVerifier(),
|
|
289
|
+
state: () => {
|
|
290
|
+
throw new RefreshOnlyAbort();
|
|
291
|
+
},
|
|
292
|
+
saveCodeVerifier: () => {
|
|
293
|
+
throw new RefreshOnlyAbort();
|
|
294
|
+
},
|
|
295
|
+
redirectToAuthorization: () => {
|
|
296
|
+
throw new RefreshOnlyAbort();
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
var refreshIfExpired = async (provider, serverUrl, fetchFn, log, name) => {
|
|
300
|
+
const check = provider.isAccessTokenExpired;
|
|
301
|
+
if (typeof check !== "function") return;
|
|
302
|
+
if (!await check.call(provider)) return;
|
|
303
|
+
const tokens = await provider.tokens();
|
|
304
|
+
if (!tokens?.refresh_token) return;
|
|
305
|
+
try {
|
|
306
|
+
await auth2(refreshOnly(provider), { serverUrl, fetchFn });
|
|
307
|
+
} catch (err) {
|
|
308
|
+
log(
|
|
309
|
+
"info",
|
|
310
|
+
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`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
var openConnection = async (name, cfg, opts, log) => {
|
|
315
|
+
let client;
|
|
15
316
|
try {
|
|
16
317
|
if (cfg.headers && cfg.getHeaders) {
|
|
17
318
|
throw new Error(`MCP server "${name}": specify either headers or getHeaders, not both`);
|
|
18
319
|
}
|
|
19
320
|
const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers;
|
|
321
|
+
if (cfg.authProvider) {
|
|
322
|
+
if (headers && "Authorization" in headers) {
|
|
323
|
+
log(
|
|
324
|
+
"warn",
|
|
325
|
+
`[mcp] ${name}: an explicit Authorization header shadows the OAuth token from authProvider`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
assertSecureOAuthUrl(cfg.url, name);
|
|
329
|
+
await refreshIfExpired(cfg.authProvider, cfg.url, cfg.fetch, log, name);
|
|
330
|
+
}
|
|
20
331
|
const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {
|
|
21
|
-
requestInit: headers ? { headers } : void 0
|
|
332
|
+
requestInit: headers ? { headers } : void 0,
|
|
333
|
+
authProvider: cfg.authProvider,
|
|
334
|
+
fetch: cfg.fetch,
|
|
335
|
+
reconnectionOptions: { ...DEFAULT_RECONNECTION, ...cfg.reconnection }
|
|
22
336
|
});
|
|
23
|
-
|
|
337
|
+
transport.onerror = (err) => log("warn", `[mcp] ${name}: transport error - ${err.message}`);
|
|
338
|
+
transport.onclose = () => log("info", `[mcp] ${name}: transport closed`);
|
|
339
|
+
client = new Client({
|
|
24
340
|
name: opts.clientName ?? "agent-web",
|
|
25
341
|
version: opts.clientVersion ?? "0.0.0"
|
|
26
342
|
});
|
|
27
343
|
await client.connect(transport);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const prefixed = `${sanitizeName(name)}__${sanitizeName(t.name)}`;
|
|
32
|
-
if (prefixed.length > MAX_TOOL_NAME_LEN) {
|
|
33
|
-
log(
|
|
34
|
-
"warn",
|
|
35
|
-
`[mcp] ${name}: tool "${t.name}" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`
|
|
36
|
-
);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
const description = t.description ?? "";
|
|
40
|
-
mounts.push({
|
|
41
|
-
key: prefixed,
|
|
42
|
-
description,
|
|
43
|
-
tool: dynamicTool({
|
|
44
|
-
description,
|
|
45
|
-
inputSchema: jsonSchema(t.inputSchema),
|
|
46
|
-
execute: async (args, options) => {
|
|
47
|
-
const res = await client.callTool(
|
|
48
|
-
{ name: t.name, arguments: args ?? {} },
|
|
49
|
-
void 0,
|
|
50
|
-
options?.abortSignal ? { signal: options.abortSignal } : void 0
|
|
51
|
-
);
|
|
52
|
-
const flat = flattenContent(res.content);
|
|
53
|
-
if (res.isError) {
|
|
54
|
-
throw new Error(typeof flat === "string" ? flat : JSON.stringify(flat));
|
|
55
|
-
}
|
|
56
|
-
return flat;
|
|
57
|
-
}
|
|
58
|
-
})
|
|
59
|
-
});
|
|
344
|
+
if (opts.onToolsChanged) {
|
|
345
|
+
const notify = opts.onToolsChanged;
|
|
346
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, () => notify(name));
|
|
60
347
|
}
|
|
61
|
-
|
|
348
|
+
const listed = (await client.listTools()).tools;
|
|
349
|
+
return { name, client, listed };
|
|
62
350
|
} catch (err) {
|
|
351
|
+
if (client) await client.close().catch(() => {
|
|
352
|
+
});
|
|
353
|
+
const needsAuthorization = err instanceof UnauthorizedError;
|
|
63
354
|
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
-
log(
|
|
65
|
-
|
|
355
|
+
log(
|
|
356
|
+
"error",
|
|
357
|
+
needsAuthorization ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl` : `[mcp] ${name}: failed to connect - ${message}`
|
|
358
|
+
);
|
|
359
|
+
return { name, error: message, needsAuthorization };
|
|
66
360
|
}
|
|
67
361
|
};
|
|
68
362
|
var connectMcpHttp = async (servers, opts = {}) => {
|
|
@@ -72,39 +366,98 @@ var connectMcpHttp = async (servers, opts = {}) => {
|
|
|
72
366
|
const tools = {};
|
|
73
367
|
const catalog = [];
|
|
74
368
|
const results = [];
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
results.push({ name: server.name, connected: false, error: server.error });
|
|
81
|
-
continue;
|
|
369
|
+
const mountedKeys = /* @__PURE__ */ new Map();
|
|
370
|
+
const mountTools = (name, client, listed) => {
|
|
371
|
+
const prefix = `${sanitizeName(name)}__`;
|
|
372
|
+
for (const k of mountedKeys.get(name) ?? []) {
|
|
373
|
+
delete tools[k];
|
|
82
374
|
}
|
|
83
|
-
|
|
375
|
+
for (let i = catalog.length - 1; i >= 0; i--) {
|
|
376
|
+
if (catalog[i].server === name) catalog.splice(i, 1);
|
|
377
|
+
}
|
|
378
|
+
const keys = [];
|
|
84
379
|
let mounted = 0;
|
|
85
|
-
for (const
|
|
86
|
-
|
|
87
|
-
|
|
380
|
+
for (const t of listed) {
|
|
381
|
+
const prefixed = prefix + sanitizeName(t.name);
|
|
382
|
+
if (prefixed.length > MAX_TOOL_NAME_LEN) {
|
|
383
|
+
log(
|
|
384
|
+
"warn",
|
|
385
|
+
`[mcp] ${name}: tool "${t.name}" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`
|
|
386
|
+
);
|
|
88
387
|
continue;
|
|
89
388
|
}
|
|
90
|
-
|
|
91
|
-
|
|
389
|
+
const key = uniqueKey(prefixed, tools);
|
|
390
|
+
if (!key) {
|
|
391
|
+
log("warn", `[mcp] ${name}: cannot find a free name for tool "${t.name}"; skipping`);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (key !== prefixed) {
|
|
395
|
+
log("warn", `[mcp] ${name}: tool name "${prefixed}" already taken; mounted as "${key}"`);
|
|
396
|
+
}
|
|
397
|
+
const description = t.description ?? "";
|
|
398
|
+
tools[key] = dynamicTool({
|
|
399
|
+
description,
|
|
400
|
+
inputSchema: jsonSchema(t.inputSchema),
|
|
401
|
+
execute: async (args, options) => {
|
|
402
|
+
const res = await client.callTool(
|
|
403
|
+
{ name: t.name, arguments: args ?? {} },
|
|
404
|
+
void 0,
|
|
405
|
+
options?.abortSignal ? { signal: options.abortSignal } : void 0
|
|
406
|
+
);
|
|
407
|
+
const flat = flattenContent(res.content);
|
|
408
|
+
if (res.isError) {
|
|
409
|
+
throw new Error(typeof flat === "string" ? flat : JSON.stringify(flat));
|
|
410
|
+
}
|
|
411
|
+
return flat;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
catalog.push({ name: key, description, server: name });
|
|
415
|
+
keys.push(key);
|
|
92
416
|
mounted += 1;
|
|
93
417
|
}
|
|
94
|
-
|
|
418
|
+
mountedKeys.set(name, keys);
|
|
419
|
+
log("info", `[mcp] ${name}: ${mounted} tools mounted`);
|
|
420
|
+
};
|
|
421
|
+
const connects = await Promise.all(
|
|
422
|
+
Object.entries(servers).map(([name, cfg]) => openConnection(name, cfg, opts, log))
|
|
423
|
+
);
|
|
424
|
+
for (const server of connects) {
|
|
425
|
+
if (!server.client || !server.listed) {
|
|
426
|
+
results.push({
|
|
427
|
+
name: server.name,
|
|
428
|
+
connected: false,
|
|
429
|
+
error: server.error,
|
|
430
|
+
...server.needsAuthorization ? { needsAuthorization: true } : {}
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
clients.set(server.name, server.client);
|
|
435
|
+
mountTools(server.name, server.client, server.listed);
|
|
95
436
|
results.push({ name: server.name, connected: true });
|
|
96
437
|
}
|
|
97
438
|
return {
|
|
98
439
|
tools,
|
|
99
440
|
catalog,
|
|
100
441
|
results,
|
|
442
|
+
refreshServer: async (name) => {
|
|
443
|
+
const client = clients.get(name);
|
|
444
|
+
if (!client) throw new Error(`MCP server "${name}" is not connected`);
|
|
445
|
+
const listed = (await client.listTools()).tools;
|
|
446
|
+
mountTools(name, client, listed);
|
|
447
|
+
},
|
|
101
448
|
close: async () => {
|
|
102
449
|
await Promise.allSettled([...clients.values()].map((c) => c.close()));
|
|
103
450
|
}
|
|
104
451
|
};
|
|
105
452
|
};
|
|
106
453
|
export {
|
|
454
|
+
BrowserOAuthProvider,
|
|
455
|
+
MemoryOAuthStorage,
|
|
456
|
+
VaultOAuthStorage,
|
|
457
|
+
beginMcpOAuth,
|
|
107
458
|
connectMcpHttp,
|
|
108
|
-
|
|
459
|
+
finishMcpOAuth,
|
|
460
|
+
flattenContent,
|
|
461
|
+
readOAuthCallback
|
|
109
462
|
};
|
|
110
463
|
//# sourceMappingURL=mcp.js.map
|
package/dist/mcp.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/mcp/http.ts"],"sourcesContent":["import { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: { name: string; connected: boolean; error?: string }[]\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n// MCP names may contain characters providers reject (dots, slashes, spaces);\n// map them into the allowed alphabet. callTool still uses the ORIGINAL name.\nconst sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\ninterface ServerMount {\n name: string\n client?: Client\n mounts: { key: string; description: string; tool: ToolSet[string] }[]\n error?: string\n}\n\nconst connectOne = async (\n name: string,\n cfg: McpHttpServerConfig,\n opts: ConnectMcpOptions,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n): Promise<ServerMount> => {\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n })\n const client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n\n const listed = await client.listTools()\n const mounts: ServerMount['mounts'] = []\n for (const t of listed.tools) {\n const prefixed = `${sanitizeName(name)}__${sanitizeName(t.name)}`\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const description = t.description ?? ''\n mounts.push({\n key: prefixed,\n description,\n tool: dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n const flat = flattenContent(res.content)\n // An MCP failure is a NORMAL response with isError set — surface it\n // as a thrown error so both tool paths record a failed call instead\n // of feeding the error text to the model as a success.\n if ((res as { isError?: boolean }).isError) {\n throw new Error(typeof flat === 'string' ? flat : JSON.stringify(flat))\n }\n return flat\n },\n }),\n })\n }\n return { name, client, mounts }\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n log('error', `[mcp] ${name}: failed to connect - ${message}`)\n return { name, mounts: [], error: message }\n }\n}\n\n/**\n * Connect to one or more HTTP MCP servers and return their tools + catalogue.\n * Servers connect concurrently; tools, catalogue, and results merge in the\n * caller's declaration order, so the outcome is deterministic.\n */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: ConnectedMcp['results'] = []\n\n const settled = await Promise.all(\n Object.entries(servers).map(([name, cfg]) => connectOne(name, cfg, opts, log)),\n )\n\n for (const server of settled) {\n if (server.error !== undefined) {\n results.push({ name: server.name, connected: false, error: server.error })\n continue\n }\n if (server.client) clients.set(server.name, server.client)\n let mounted = 0\n for (const m of server.mounts) {\n if (tools[m.key]) {\n log('warn', `[mcp] ${server.name}: duplicate tool name \"${m.key}\"; keeping the first`)\n continue\n }\n tools[m.key] = m.tool\n catalog.push({ name: m.key, description: m.description, server: server.name })\n mounted += 1\n }\n log('info', `[mcp] ${server.name}: ${mounted} tools mounted`)\n results.push({ name: server.name, connected: true })\n }\n\n return {\n tools,\n catalog,\n results,\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/mcp/http.ts","../src/mcp/oauth.ts"],"sourcesContent":["import {\n auth,\n UnauthorizedError,\n type OAuthClientProvider,\n} from '@modelcontextprotocol/sdk/client/auth.js'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport {\n StreamableHTTPClientTransport,\n type StreamableHTTPReconnectionOptions,\n} from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\nimport { assertSecureOAuthUrl } from './oauth.js'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n *\n * Three ways to authenticate, in ascending order of capability:\n * 1. `headers` — a static token you already hold.\n * 2. `getHeaders` — resolved once per connect, for tokens you mint yourself.\n * 3. `authProvider` — full OAuth 2.1: the SDK discovers the authorization\n * server, dynamically registers the client (DCR), runs PKCE, and — the\n * part 1 and 2 cannot do — transparently REFRESHES the access token when\n * the server answers 401 mid-run. See `./oauth`.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n /**\n * OAuth client provider (see `./oauth` for a browser implementation). The\n * transport sends its access token, refreshes on 401 and retries the request.\n * Do NOT also set an `Authorization` entry in `headers`: request headers are\n * merged last and would shadow the OAuth token.\n */\n authProvider?: OAuthClientProvider\n /** Custom fetch for every request: proxying, instrumentation, retries. */\n fetch?: FetchLike\n /** Overrides for the SSE reconnect backoff. Merged over the defaults below. */\n reconnection?: Partial<StreamableHTTPReconnectionOptions>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface McpServerResult {\n name: string\n connected: boolean\n error?: string\n /**\n * The server demands OAuth and we have no usable token. When the config\n * carried a `BrowserOAuthProvider`, its `authorizationUrl` now holds the page\n * to send the user to; reconnect once they are back.\n */\n needsAuthorization?: boolean\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: McpServerResult[]\n /**\n * Re-list one server's tools and rewrite its entries in `tools`/`catalog`.\n * Both are mutated in place, so anything holding a reference (including a\n * live agent config) sees the new set. Throws if the server isn't connected.\n */\n refreshServer: (name: string) => Promise<void>\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n /**\n * Fires when a server pushes `notifications/tools/list_changed`. Call\n * `refreshServer(name)` from a point where no run is reading `tools` — this\n * connector deliberately does not refresh behind a running agent's back.\n */\n onToolsChanged?: (server: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n// Browsers drop long-lived SSE streams on tab-throttling and flaky networks far\n// more often than a server does; the SDK's default (2 retries) gives up too\n// early for an agent that may idle between steps.\nconst DEFAULT_RECONNECTION: StreamableHTTPReconnectionOptions = {\n maxReconnectionDelay: 30_000,\n initialReconnectionDelay: 1_000,\n reconnectionDelayGrowFactor: 1.5,\n maxRetries: 5,\n}\n\n// MCP names may contain characters providers reject (dots, slashes, spaces);\n// map them into the allowed alphabet. callTool still uses the ORIGINAL name.\nconst sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')\n\n/**\n * Sanitizing can map two distinct MCP names onto one key (\"a.b\" and \"a-b\" both\n * become \"a_b\"). Suffix instead of dropping: a tool the model can't see is a\n * silent capability loss, and the disambiguated name still resolves back to the\n * original on dispatch.\n */\nconst uniqueKey = (base: string, taken: ToolSet): string => {\n // hasOwn, not truthiness: a tool named so that the key lands on \"__proto__\"\n // would otherwise read as already taken through the prototype chain.\n if (!Object.hasOwn(taken, base)) return base\n for (let n = 2; n < 100; n++) {\n const suffix = `_${n}`\n const candidate = base.slice(0, MAX_TOOL_NAME_LEN - suffix.length) + suffix\n if (!Object.hasOwn(taken, candidate)) return candidate\n }\n return ''\n}\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\ninterface McpToolDescriptor {\n name: string\n description?: string\n inputSchema: unknown\n}\n\ninterface ServerConnect {\n name: string\n client?: Client\n listed?: McpToolDescriptor[]\n error?: string\n needsAuthorization?: boolean\n}\n\n/** Thrown by the refresh-only view when the SDK reaches for a redirect. */\nclass RefreshOnlyAbort extends Error {}\n\n/**\n * A view of the provider that can renew a token but can never start an\n * interactive authorization.\n *\n * `auth()` does not have a refresh-only mode: when a refresh fails for a reason\n * it deems recoverable (a 5xx from the token endpoint, a network blip) it falls\n * through to a brand-new authorization — issuing a state and a PKCE verifier,\n * overwriting the stored pair, and calling `redirectToAuthorization`. The\n * transport then does its own `auth()` on the first 401, overwriting them\n * again, so the user comes back holding a code the second pair can't verify.\n * Blocking the three redirect-path methods aborts the escalation before\n * anything is overwritten; the transport's 401 handling stays the single owner\n * of interactive authorization.\n */\nconst refreshOnly = (p: OAuthClientProvider): OAuthClientProvider => ({\n // Must stay truthy: `auth()` reads a missing redirectUrl as \"non-interactive\n // grant\" and would try to fetch a token without one.\n get redirectUrl() {\n return p.redirectUrl\n },\n get clientMetadata() {\n return p.clientMetadata\n },\n clientInformation: () => p.clientInformation(),\n saveClientInformation: p.saveClientInformation?.bind(p),\n tokens: () => p.tokens(),\n saveTokens: (t) => p.saveTokens(t),\n invalidateCredentials: p.invalidateCredentials?.bind(p),\n addClientAuthentication: p.addClientAuthentication?.bind(p),\n validateResourceURL: p.validateResourceURL?.bind(p),\n codeVerifier: () => p.codeVerifier(),\n state: () => {\n throw new RefreshOnlyAbort()\n },\n saveCodeVerifier: () => {\n throw new RefreshOnlyAbort()\n },\n redirectToAuthorization: () => {\n throw new RefreshOnlyAbort()\n },\n})\n\n/**\n * An access token that is already expired produces a guaranteed 401 on the\n * first request. When the provider tells us so AND we hold a refresh token,\n * renew before connecting: one round-trip saved, and a run that starts on a\n * stale token doesn't surface a spurious auth error to the model.\n *\n * Best-effort by design — anything that goes wrong here is left to the\n * transport's own 401 handling, which is the path that knows how to ask the\n * user for a new authorization.\n */\nconst refreshIfExpired = async (\n provider: OAuthClientProvider,\n serverUrl: string,\n fetchFn: FetchLike | undefined,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n name: string,\n): Promise<void> => {\n const check = (provider as { isAccessTokenExpired?: () => boolean | Promise<boolean> })\n .isAccessTokenExpired\n if (typeof check !== 'function') return\n if (!(await check.call(provider))) return\n const tokens = await provider.tokens()\n // Nothing to refresh with: let the 401 path ask for a new authorization.\n if (!tokens?.refresh_token) return\n try {\n await auth(refreshOnly(provider), { serverUrl, fetchFn })\n } catch (err) {\n log(\n 'info',\n err instanceof RefreshOnlyAbort\n ? `[mcp] ${name}: the stored token could not be refreshed; a new authorization is needed`\n : `[mcp] ${name}: proactive token refresh failed (${\n err instanceof Error ? err.message : String(err)\n }); continuing`,\n )\n }\n}\n\nconst openConnection = async (\n name: string,\n cfg: McpHttpServerConfig,\n opts: ConnectMcpOptions,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n): Promise<ServerConnect> => {\n let client: Client | undefined\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n if (cfg.authProvider) {\n if (headers && 'Authorization' in headers) {\n log(\n 'warn',\n `[mcp] ${name}: an explicit Authorization header shadows the OAuth token from authProvider`,\n )\n }\n assertSecureOAuthUrl(cfg.url, name)\n await refreshIfExpired(cfg.authProvider, cfg.url, cfg.fetch, log, name)\n }\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n authProvider: cfg.authProvider,\n fetch: cfg.fetch,\n reconnectionOptions: { ...DEFAULT_RECONNECTION, ...cfg.reconnection },\n })\n // Without these, a dropped SSE stream or a transport-level protocol error\n // is swallowed and the agent just stops getting results.\n transport.onerror = (err) => log('warn', `[mcp] ${name}: transport error - ${err.message}`)\n // info, not warn: an ordinary close() ends here too, and a routine\n // teardown logged as a warning trains people to ignore warnings.\n transport.onclose = () => log('info', `[mcp] ${name}: transport closed`)\n\n client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n\n // Subscribe BEFORE the first list call: a server that mutates its tool set\n // during init would otherwise lose the notification in the window between\n // connect() and listTools().\n if (opts.onToolsChanged) {\n const notify = opts.onToolsChanged\n client.setNotificationHandler(ToolListChangedNotificationSchema, () => notify(name))\n }\n\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n return { name, client, listed }\n } catch (err) {\n // The client may be live even though we ended up here (listTools() failing\n // after a successful connect). Close it, or the SSE stream leaks for the\n // lifetime of the tab.\n if (client) await client.close().catch(() => {})\n const needsAuthorization = err instanceof UnauthorizedError\n const message = err instanceof Error ? err.message : String(err)\n log(\n 'error',\n needsAuthorization\n ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl`\n : `[mcp] ${name}: failed to connect - ${message}`,\n )\n return { name, error: message, needsAuthorization }\n }\n}\n\n/**\n * Connect to one or more HTTP MCP servers and return their tools + catalogue.\n * Servers connect concurrently; tools, catalogue, and results merge in the\n * caller's declaration order, so the outcome is deterministic.\n */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: McpServerResult[] = []\n // server name → the tool keys it currently owns, so a refresh can remove\n // exactly its own entries.\n const mountedKeys = new Map<string, string[]>()\n\n const mountTools = (name: string, client: Client, listed: McpToolDescriptor[]): void => {\n const prefix = `${sanitizeName(name)}__`\n // Drop this server's existing entries first, mutating in place so external\n // references (an agent's live ToolSet) stay valid. We delete the keys this\n // server actually mounted rather than everything matching its prefix:\n // sanitizing can make two server names share a prefix (\"docs.v1\" and\n // \"docs/v1\"), and one server's prefix can nest inside another's (\"a\" and\n // \"a__b\") — either way a prefix sweep would silently unmount a DIFFERENT\n // server's tools.\n for (const k of mountedKeys.get(name) ?? []) {\n delete tools[k]\n }\n for (let i = catalog.length - 1; i >= 0; i--) {\n if (catalog[i].server === name) catalog.splice(i, 1)\n }\n const keys: string[] = []\n\n let mounted = 0\n for (const t of listed) {\n const prefixed = prefix + sanitizeName(t.name)\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const key = uniqueKey(prefixed, tools)\n if (!key) {\n log('warn', `[mcp] ${name}: cannot find a free name for tool \"${t.name}\"; skipping`)\n continue\n }\n if (key !== prefixed) {\n log('warn', `[mcp] ${name}: tool name \"${prefixed}\" already taken; mounted as \"${key}\"`)\n }\n const description = t.description ?? ''\n tools[key] = dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n const flat = flattenContent(res.content)\n // An MCP failure is a NORMAL response with isError set — surface it\n // as a thrown error so both tool paths record a failed call instead\n // of feeding the error text to the model as a success.\n if ((res as { isError?: boolean }).isError) {\n throw new Error(typeof flat === 'string' ? flat : JSON.stringify(flat))\n }\n return flat\n },\n })\n catalog.push({ name: key, description, server: name })\n keys.push(key)\n mounted += 1\n }\n mountedKeys.set(name, keys)\n log('info', `[mcp] ${name}: ${mounted} tools mounted`)\n }\n\n const connects = await Promise.all(\n Object.entries(servers).map(([name, cfg]) => openConnection(name, cfg, opts, log)),\n )\n\n for (const server of connects) {\n if (!server.client || !server.listed) {\n results.push({\n name: server.name,\n connected: false,\n error: server.error,\n ...(server.needsAuthorization ? { needsAuthorization: true } : {}),\n })\n continue\n }\n clients.set(server.name, server.client)\n mountTools(server.name, server.client, server.listed)\n results.push({ name: server.name, connected: true })\n }\n\n return {\n tools,\n catalog,\n results,\n refreshServer: async (name: string) => {\n const client = clients.get(name)\n if (!client) throw new Error(`MCP server \"${name}\" is not connected`)\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n mountTools(name, client, listed)\n },\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n","import { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'\nimport type {\n OAuthClientInformationMixed,\n OAuthClientMetadata,\n OAuthTokens,\n} from '@modelcontextprotocol/sdk/shared/auth.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { IndexedDBVault, type VaultOptions } from '../secrets/vault.js'\n\n/**\n * Browser-side OAuth 2.1 for remote MCP servers: Dynamic Client Registration\n * (RFC 7591), PKCE authorization-code flow, and refresh-token rotation.\n *\n * The heavy lifting lives in the MCP SDK — `auth()` discovers the protected\n * resource (RFC 9728), registers the client if we have no client_id yet,\n * exchanges the code, and refreshes expired tokens; `StreamableHTTPClient-\n * Transport` retries any 401 through it. This module supplies the missing\n * half: WHERE the tokens, the registration, and the PKCE verifier are kept,\n * and HOW the user is sent to the authorization server.\n *\n * ── WHY NO AUTOMATIC NAVIGATION ──\n * `redirectToAuthorization` deliberately does NOT navigate by default. The SDK\n * can call it from deep inside a tool call, and throwing the user out of a\n * running agent would discard in-flight state. Instead the URL is recorded on\n * `authorizationUrl`, `connectMcpHttp` reports `needsAuthorization: true`, and\n * the app decides when to send the user there (a button, a popup, a redirect).\n * Pass `onRedirect` to opt into eager navigation.\n */\n\n/** Where an OAuth provider keeps its tokens / registration / PKCE verifier. */\nexport interface McpOAuthStorage {\n get(key: string): Promise<string | undefined>\n set(key: string, value: string): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/**\n * Default storage: the same encrypted IndexedDB vault that holds provider API\n * keys (AES-GCM, non-extractable key). Read the threat model in\n * `secrets/vault.ts` — this protects tokens at rest, not against active XSS.\n */\nexport class VaultOAuthStorage implements McpOAuthStorage {\n private readonly vault: IndexedDBVault\n\n constructor(opts: VaultOptions = {}) {\n this.vault = new IndexedDBVault(opts)\n }\n\n get(key: string): Promise<string | undefined> {\n return this.vault.getSecret(key)\n }\n set(key: string, value: string): Promise<void> {\n return this.vault.setSecret(key, value)\n }\n delete(key: string): Promise<void> {\n return this.vault.deleteSecret(key)\n }\n}\n\n/** Non-persistent storage for tests / SSR. Tokens die with the tab. */\nexport class MemoryOAuthStorage implements McpOAuthStorage {\n private readonly map = new Map<string, string>()\n\n async get(key: string): Promise<string | undefined> {\n return this.map.get(key)\n }\n async set(key: string, value: string): Promise<void> {\n this.map.set(key, value)\n }\n async delete(key: string): Promise<void> {\n this.map.delete(key)\n }\n}\n\nexport interface BrowserOAuthProviderOptions {\n /** The MCP endpoint this provider authorizes against (the resource server). */\n serverUrl: string\n /**\n * Where the authorization server sends the user back. Must be a page of your\n * app that calls `readOAuthCallback()` + `finishMcpOAuth()`. Registered with\n * the AS via DCR, so it has to match byte-for-byte on the way back.\n */\n redirectUrl: string | URL\n /** `client_name` submitted during dynamic registration. */\n clientName?: string\n /** Fallback scope when the server's metadata advertises none. */\n scope?: string\n /** Merged over the defaults; use it for `software_id`, `contacts`, … */\n clientMetadata?: Partial<OAuthClientMetadata>\n /** Defaults to the encrypted IndexedDB vault. */\n storage?: McpOAuthStorage\n /** Treat an access token as expired this many seconds early. Default 30. */\n expirySkewSeconds?: number\n /** Called when the SDK wants the user at the authorization server. */\n onRedirect?: (url: URL) => void | Promise<void>\n}\n\ninterface StoredTokens {\n tokens: OAuthTokens\n /** Absolute ms timestamp derived from `expires_in` at save time. */\n expiresAt?: number\n}\n\nconst DEFAULT_SKEW_SECONDS = 30\n\n// Namespacing by origin+path+query keeps two MCP servers on the same host\n// (/mcp/docs vs /mcp/crm, or ?tenant=a vs ?tenant=b) from sharing a\n// registration or a token — one tenant's bearer must never be sent to another.\nconst namespaceFor = (serverUrl: string): string => {\n const u = new URL(serverUrl)\n return `mcp-oauth:${u.origin}${u.pathname.replace(/\\/+$/, '')}${u.search}`\n}\n\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])\n\n/**\n * OAuth over plaintext hands the bearer token to anyone on the path, so only\n * https — or a loopback host, where there is no network to sniff — is allowed.\n * Parsed rather than pattern-matched: `http://localhost.attacker.example/` is a\n * registrable host that a prefix match would wave through.\n */\nexport const assertSecureOAuthUrl = (url: string, label = 'MCP server'): void => {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`${label}: \"${url}\" is not a valid URL`)\n }\n if (parsed.protocol === 'https:' || LOOPBACK_HOSTS.has(parsed.hostname)) return\n throw new Error(\n `${label}: MCP OAuth requires https (or a loopback host) - refusing to send tokens to \"${url}\"`,\n )\n}\n\nconst randomToken = (): string => {\n const bytes = new Uint8Array(32)\n globalThis.crypto.getRandomValues(bytes)\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/**\n * An `OAuthClientProvider` (MCP SDK) that persists everything it is handed and\n * never navigates behind your back. One instance per MCP server.\n */\nexport class BrowserOAuthProvider implements OAuthClientProvider {\n /** Set when the SDK asks for an authorization redirect; read it in your UI. */\n authorizationUrl?: URL\n\n readonly serverUrl: string\n\n private readonly storage: McpOAuthStorage\n private readonly ns: string\n private readonly _redirectUrl: string\n private readonly _clientMetadata: OAuthClientMetadata\n private readonly skewMs: number\n private readonly onRedirect?: (url: URL) => void | Promise<void>\n\n constructor(opts: BrowserOAuthProviderOptions) {\n assertSecureOAuthUrl(opts.serverUrl)\n this.serverUrl = opts.serverUrl\n this.storage = opts.storage ?? new VaultOAuthStorage()\n this.ns = namespaceFor(opts.serverUrl)\n this._redirectUrl = String(opts.redirectUrl)\n this.skewMs = (opts.expirySkewSeconds ?? DEFAULT_SKEW_SECONDS) * 1000\n this.onRedirect = opts.onRedirect\n this._clientMetadata = {\n client_name: opts.clientName ?? 'agent-web',\n redirect_uris: [this._redirectUrl],\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n // Public client: a browser cannot hold a client_secret. PKCE is what\n // actually protects the code exchange.\n token_endpoint_auth_method: 'none',\n ...(opts.scope ? { scope: opts.scope } : {}),\n ...opts.clientMetadata,\n } as OAuthClientMetadata\n }\n\n get redirectUrl(): string {\n return this._redirectUrl\n }\n\n get clientMetadata(): OAuthClientMetadata {\n return this._clientMetadata\n }\n\n private key(suffix: string): string {\n return `${this.ns}:${suffix}`\n }\n\n private async readJSON<T>(suffix: string): Promise<T | undefined> {\n // The read itself is inside the guard on purpose: the default storage\n // decrypts, and an AES-GCM blob written under a key that no longer exists\n // (a partially cleared IndexedDB, a rotated vault key) throws. Treating\n // that as \"no value\" lets the flow re-authorize instead of every call\n // rejecting with an opaque OperationError forever.\n try {\n const raw = await this.storage.get(this.key(suffix))\n if (raw === undefined) return undefined\n return JSON.parse(raw) as T\n } catch {\n await this.storage.delete(this.key(suffix)).catch(() => {})\n return undefined\n }\n }\n\n async state(): Promise<string> {\n const value = randomToken()\n await this.storage.set(this.key('state'), value)\n return value\n }\n\n /**\n * CSRF check for the redirect back. Single-use: the stored value is dropped\n * whether or not it matched, so a replayed callback can't pass twice.\n */\n async verifyState(returned: string | undefined): Promise<boolean> {\n const expected = await this.storage.get(this.key('state'))\n await this.storage.delete(this.key('state'))\n return expected !== undefined && returned !== undefined && expected === returned\n }\n\n async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {\n return this.readJSON<OAuthClientInformationMixed>('client')\n }\n\n async saveClientInformation(info: OAuthClientInformationMixed): Promise<void> {\n await this.storage.set(this.key('client'), JSON.stringify(info))\n }\n\n async tokens(): Promise<OAuthTokens | undefined> {\n return (await this.readJSON<StoredTokens>('tokens'))?.tokens\n }\n\n async saveTokens(tokens: OAuthTokens): Promise<void> {\n const stored: StoredTokens = {\n tokens,\n expiresAt:\n typeof tokens.expires_in === 'number' ? Date.now() + tokens.expires_in * 1000 : undefined,\n }\n await this.storage.set(this.key('tokens'), JSON.stringify(stored))\n }\n\n async saveCodeVerifier(verifier: string): Promise<void> {\n await this.storage.set(this.key('verifier'), verifier)\n }\n\n async codeVerifier(): Promise<string> {\n const v = await this.storage.get(this.key('verifier'))\n if (!v) {\n throw new Error(\n 'No PKCE code verifier stored: the authorization was started in a different browser profile, or storage was cleared mid-flow',\n )\n }\n return v\n }\n\n async redirectToAuthorization(url: URL): Promise<void> {\n this.authorizationUrl = url\n await this.onRedirect?.(url)\n }\n\n /**\n * Called by the SDK when the server rejects what we hold: `'tokens'` after an\n * `invalid_grant` (dead refresh token), `'client'` / `'all'` after an\n * `invalid_client` (the AS forgot our dynamic registration). Clearing lets\n * the SDK re-register and re-authorize without the user clearing site data.\n */\n async invalidateCredentials(\n scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery',\n ): Promise<void> {\n const targets = scope === 'all' ? ['tokens', 'client', 'verifier', 'state'] : [scope]\n await Promise.all(targets.map((t) => this.storage.delete(this.key(t))))\n }\n\n /** True when there is no access token, or it expires within the skew window. */\n async isAccessTokenExpired(): Promise<boolean> {\n const stored = await this.readJSON<StoredTokens>('tokens')\n if (!stored?.tokens?.access_token) return true\n // No expires_in means the AS didn't say; assume it is still good and let a\n // 401 drive the refresh.\n if (stored.expiresAt === undefined) return false\n return Date.now() + this.skewMs >= stored.expiresAt\n }\n\n /** True once an authorization has completed for this server. */\n async isAuthorized(): Promise<boolean> {\n return (await this.tokens()) !== undefined\n }\n\n /** Forget tokens, registration, verifier and state for this server. */\n async reset(): Promise<void> {\n this.authorizationUrl = undefined\n await this.invalidateCredentials('all')\n }\n}\n\nexport interface McpOAuthRequestOptions {\n /**\n * Custom fetch for the discovery / registration / token requests. Pass the\n * same one you give the connector so a proxy or an instrumentation wrapper\n * covers the OAuth traffic too.\n */\n fetch?: FetchLike\n}\n\nexport interface McpOAuthCallback {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\n/**\n * Pull the authorization-code parameters out of a redirect URL. Reads the query\n * string and, for hash-routed apps, the fragment. Returns undefined when the\n * URL carries neither a `code` nor an `error`, so it is safe to call on every\n * page load.\n */\nexport const readOAuthCallback = (input?: string | URL): McpOAuthCallback | undefined => {\n const href = input\n ? String(input)\n : typeof globalThis.location !== 'undefined'\n ? globalThis.location.href\n : undefined\n if (!href) return undefined\n let url: URL\n try {\n url = new URL(href)\n } catch {\n return undefined\n }\n const params = new URLSearchParams(url.search)\n const hash = url.hash.startsWith('#') ? url.hash.slice(1) : url.hash\n // Hash-routed apps get \"#/callback?code=…\"; take whatever follows the \"?\".\n const q = hash.indexOf('?')\n if (q >= 0) {\n for (const [k, v] of new URLSearchParams(hash.slice(q + 1))) {\n if (!params.has(k)) params.set(k, v)\n }\n }\n const code = params.get('code') ?? undefined\n const error = params.get('error') ?? undefined\n if (!code && !error) return undefined\n return {\n code,\n state: params.get('state') ?? undefined,\n error,\n errorDescription: params.get('error_description') ?? undefined,\n }\n}\n\n/**\n * Finish the flow after the authorization server redirected back: verify the\n * `state`, exchange the code (PKCE) and persist the tokens. Throws when the AS\n * returned an error, when `state` doesn't match what we issued, or when the\n * exchange fails — never resolves on a half-finished authorization.\n */\nexport const finishMcpOAuth = async (\n provider: BrowserOAuthProvider,\n callback: McpOAuthCallback,\n opts: McpOAuthRequestOptions = {},\n): Promise<void> => {\n if (callback.error) {\n throw new Error(\n `Authorization denied by the server: ${callback.error}${\n callback.errorDescription ? ` - ${callback.errorDescription}` : ''\n }`,\n )\n }\n if (!callback.code) {\n throw new Error('Authorization callback carried no code')\n }\n if (!(await provider.verifyState(callback.state))) {\n throw new Error('Authorization state mismatch; refusing to exchange the code (possible CSRF)')\n }\n const result = await auth(provider, {\n serverUrl: provider.serverUrl,\n authorizationCode: callback.code,\n fetchFn: opts.fetch,\n })\n if (result !== 'AUTHORIZED') {\n throw new Error(`Token exchange did not complete (SDK returned \"${result}\")`)\n }\n}\n\n/**\n * Kick off (or silently renew) an authorization. Returns `'AUTHORIZED'` when a\n * usable token is in hand — including the case where a refresh token was\n * silently exchanged — or `'REDIRECT'` when the user has to visit\n * `provider.authorizationUrl`.\n */\nexport const beginMcpOAuth = async (\n provider: BrowserOAuthProvider,\n opts: McpOAuthRequestOptions = {},\n): Promise<'AUTHORIZED' | 'REDIRECT'> => {\n provider.authorizationUrl = undefined\n return auth(provider, { serverUrl: provider.serverUrl, fetchFn: opts.fetch })\n}\n"],"mappings":"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","names":["auth","auth"]}
|