@wrongstack/mcp 1.0.24 → 1.0.27
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/authorization-callback-server.d.ts +24 -0
- package/dist/authorization-discovery.d.ts +96 -0
- package/dist/authorization-manager.d.ts +46 -1
- package/dist/authorization-registration.d.ts +39 -0
- package/dist/authorization.d.ts +6 -61
- package/dist/client-http-connection.d.ts +22 -0
- package/dist/client-types.d.ts +8 -0
- package/dist/client.d.ts +24 -3
- package/dist/config-env.d.ts +18 -0
- package/dist/constants.d.ts +26 -5
- package/dist/contracts.d.ts +21 -0
- package/dist/elicitation-form.d.ts +26 -0
- package/dist/elicitation.d.ts +151 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1988 -656
- package/dist/manage.d.ts +4 -0
- package/dist/protocol.d.ts +10 -0
- package/dist/registry-authorization.d.ts +3 -2
- package/dist/registry-connect-loop.d.ts +1 -0
- package/dist/registry-slots.d.ts +6 -0
- package/dist/registry-types.d.ts +6 -0
- package/dist/registry.d.ts +12 -1
- package/dist/structured-result.d.ts +7 -0
- package/dist/token-store.d.ts +5 -1
- package/dist/tool-schema.d.ts +14 -1
- package/dist/transport-base.d.ts +26 -4
- package/dist/transport-streamable.d.ts +14 -1
- package/dist/wrap-tool.d.ts +4 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// src/authorization.ts
|
|
2
2
|
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
// src/authorization-discovery.ts
|
|
3
5
|
import * as dns from "node:dns/promises";
|
|
4
6
|
import * as http from "node:http";
|
|
5
7
|
import * as https from "node:https";
|
|
@@ -29,22 +31,6 @@ function canonicalMcpResource(rawUrl) {
|
|
|
29
31
|
if (url.pathname === "/" && !url.search) return url.origin;
|
|
30
32
|
return url.toString();
|
|
31
33
|
}
|
|
32
|
-
function authorizationHeaderForToken(token, expectedResource, now = Date.now()) {
|
|
33
|
-
if (canonicalMcpResource(token.resource) !== expectedResource) {
|
|
34
|
-
throw new Error("MCP access token resource does not match the target server");
|
|
35
|
-
}
|
|
36
|
-
if (token.expiresAt !== void 0 && token.expiresAt <= now) {
|
|
37
|
-
throw new Error("MCP access token is expired");
|
|
38
|
-
}
|
|
39
|
-
const tokenType = token.tokenType ?? "Bearer";
|
|
40
|
-
if (tokenType.toLowerCase() !== "bearer") {
|
|
41
|
-
throw new Error(`Unsupported MCP OAuth token type "${tokenType}"`);
|
|
42
|
-
}
|
|
43
|
-
if (!token.accessToken || token.accessToken.length > 16384 || /[\r\n]/.test(token.accessToken)) {
|
|
44
|
-
throw new Error("MCP access token is empty, oversized, or contains invalid characters");
|
|
45
|
-
}
|
|
46
|
-
return `Bearer ${token.accessToken}`;
|
|
47
|
-
}
|
|
48
34
|
function parseMcpBearerChallenge(header, resource) {
|
|
49
35
|
const challenge = {
|
|
50
36
|
status: 401,
|
|
@@ -154,36 +140,6 @@ function parseAuthorizationServerMetadata(value, expectedIssuer) {
|
|
|
154
140
|
scopesSupported: optionalStringArray(metadata["scopes_supported"], "scopes_supported", 128)
|
|
155
141
|
};
|
|
156
142
|
}
|
|
157
|
-
function validateMcpAuthorizationServerMetadata(value) {
|
|
158
|
-
const metadata = record(value, "stored authorization server metadata");
|
|
159
|
-
const registration = optionalString(metadata["registrationEndpoint"], "registrationEndpoint");
|
|
160
|
-
const issuer = secureOAuthUrl(requiredString(metadata["issuer"], "issuer"), "issuer").toString();
|
|
161
|
-
const authorizationEndpoint = secureOAuthUrl(
|
|
162
|
-
requiredString(metadata["authorizationEndpoint"], "authorizationEndpoint"),
|
|
163
|
-
"authorization endpoint"
|
|
164
|
-
).toString();
|
|
165
|
-
const tokenEndpoint = secureOAuthUrl(
|
|
166
|
-
requiredString(metadata["tokenEndpoint"], "tokenEndpoint"),
|
|
167
|
-
"token endpoint"
|
|
168
|
-
).toString();
|
|
169
|
-
const issuerParameterSupported = optionalBoolean(
|
|
170
|
-
metadata["authorizationResponseIssuerParameterSupported"],
|
|
171
|
-
"authorizationResponseIssuerParameterSupported"
|
|
172
|
-
);
|
|
173
|
-
if (issuerParameterSupported !== true && (new URL(authorizationEndpoint).origin !== new URL(issuer).origin || new URL(tokenEndpoint).origin !== new URL(issuer).origin)) {
|
|
174
|
-
throw new Error(
|
|
175
|
-
"MCP authorization server with cross-origin endpoints must support the authorization response issuer parameter"
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
issuer,
|
|
180
|
-
authorizationEndpoint,
|
|
181
|
-
tokenEndpoint,
|
|
182
|
-
registrationEndpoint: registration ? secureOAuthUrl(registration, "registration endpoint").toString() : void 0,
|
|
183
|
-
authorizationResponseIssuerParameterSupported: issuerParameterSupported,
|
|
184
|
-
scopesSupported: optionalStringArray(metadata["scopesSupported"], "scopesSupported", 128)
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
143
|
async function discoverMcpAuthorization(resource, options = {}) {
|
|
188
144
|
const canonicalResource = canonicalMcpResource(resource);
|
|
189
145
|
const resourceUrl = new URL(canonicalResource);
|
|
@@ -219,124 +175,6 @@ async function discoverMcpAuthorization(resource, options = {}) {
|
|
|
219
175
|
authorizationServer: authorizationDiscovery.value
|
|
220
176
|
};
|
|
221
177
|
}
|
|
222
|
-
function createMcpAuthorizationRequest(options) {
|
|
223
|
-
const resource = canonicalMcpResource(options.resource);
|
|
224
|
-
const clientId = boundedCredential(options.clientId, "client id");
|
|
225
|
-
const redirectUri = validateRedirectUri(options.redirectUri);
|
|
226
|
-
const scopes = validateScopes(options.scopes ?? []);
|
|
227
|
-
const codeVerifier = base64Url(randomBytes(32));
|
|
228
|
-
const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
|
|
229
|
-
const state = base64Url(randomBytes(32));
|
|
230
|
-
const authorizationUrl = secureOAuthUrl(
|
|
231
|
-
options.authorizationServer.authorizationEndpoint,
|
|
232
|
-
"authorization endpoint"
|
|
233
|
-
);
|
|
234
|
-
authorizationUrl.searchParams.set("response_type", "code");
|
|
235
|
-
authorizationUrl.searchParams.set("client_id", clientId);
|
|
236
|
-
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
|
237
|
-
authorizationUrl.searchParams.set("state", state);
|
|
238
|
-
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
|
|
239
|
-
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
240
|
-
authorizationUrl.searchParams.set("resource", resource);
|
|
241
|
-
if (scopes.length > 0) authorizationUrl.searchParams.set("scope", scopes.join(" "));
|
|
242
|
-
return {
|
|
243
|
-
authorizationUrl: authorizationUrl.toString(),
|
|
244
|
-
state,
|
|
245
|
-
codeVerifier,
|
|
246
|
-
redirectUri,
|
|
247
|
-
clientId,
|
|
248
|
-
resource,
|
|
249
|
-
issuer: options.authorizationServer.issuer,
|
|
250
|
-
requireIssuerParameter: options.authorizationServer.authorizationResponseIssuerParameterSupported === true
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
function parseMcpAuthorizationCallback(callbackUrl, session) {
|
|
254
|
-
let callback;
|
|
255
|
-
try {
|
|
256
|
-
callback = new URL(callbackUrl);
|
|
257
|
-
} catch {
|
|
258
|
-
throw new Error("MCP OAuth callback must be an absolute URL");
|
|
259
|
-
}
|
|
260
|
-
const expected = new URL(validateRedirectUri(session.redirectUri));
|
|
261
|
-
if (callback.protocol !== expected.protocol || callback.hostname !== expected.hostname || callback.port !== expected.port || callback.pathname !== expected.pathname) {
|
|
262
|
-
throw new Error("MCP OAuth callback redirect URI does not match the authorization session");
|
|
263
|
-
}
|
|
264
|
-
const returnedStates = callback.searchParams.getAll("state");
|
|
265
|
-
if (returnedStates.length !== 1) {
|
|
266
|
-
throw new Error("MCP OAuth callback must contain exactly one state parameter");
|
|
267
|
-
}
|
|
268
|
-
const returnedState = returnedStates[0];
|
|
269
|
-
if (!constantTimeEqual(returnedState, session.state)) {
|
|
270
|
-
throw new Error("MCP OAuth callback state mismatch");
|
|
271
|
-
}
|
|
272
|
-
const returnedIssuers = callback.searchParams.getAll("iss");
|
|
273
|
-
if (session.requireIssuerParameter && returnedIssuers.length !== 1) {
|
|
274
|
-
throw new Error("MCP OAuth callback must contain exactly one issuer parameter");
|
|
275
|
-
}
|
|
276
|
-
if (returnedIssuers.length > 0 && returnedIssuers.some((issuer) => issuer !== session.issuer)) {
|
|
277
|
-
throw new Error("MCP OAuth callback issuer mismatch");
|
|
278
|
-
}
|
|
279
|
-
const oauthErrors = callback.searchParams.getAll("error");
|
|
280
|
-
const codes = callback.searchParams.getAll("code");
|
|
281
|
-
if (oauthErrors.length > 1 || codes.length > 1 || oauthErrors.length === 1 && codes.length > 0) {
|
|
282
|
-
throw new Error("MCP OAuth callback contains ambiguous response parameters");
|
|
283
|
-
}
|
|
284
|
-
if (oauthErrors.length === 1) {
|
|
285
|
-
throw new Error(`MCP OAuth authorization failed: ${boundedErrorCode(oauthErrors[0])}`);
|
|
286
|
-
}
|
|
287
|
-
if (codes.length !== 1) {
|
|
288
|
-
throw new Error("MCP OAuth callback must contain exactly one authorization code");
|
|
289
|
-
}
|
|
290
|
-
return boundedCredential(codes[0], "authorization code");
|
|
291
|
-
}
|
|
292
|
-
async function exchangeMcpAuthorizationCode(options) {
|
|
293
|
-
const resource = canonicalMcpResource(options.resource);
|
|
294
|
-
const body = new URLSearchParams({
|
|
295
|
-
grant_type: "authorization_code",
|
|
296
|
-
code: boundedCredential(options.code, "authorization code"),
|
|
297
|
-
client_id: boundedCredential(options.clientId, "client id"),
|
|
298
|
-
redirect_uri: validateRedirectUri(options.redirectUri),
|
|
299
|
-
code_verifier: validateCodeVerifier(options.codeVerifier),
|
|
300
|
-
resource
|
|
301
|
-
}).toString();
|
|
302
|
-
const response = await requestPinnedJson(options.authorizationServer.tokenEndpoint, {
|
|
303
|
-
method: "POST",
|
|
304
|
-
body,
|
|
305
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
306
|
-
signal: options.signal,
|
|
307
|
-
timeoutMs: options.timeoutMs,
|
|
308
|
-
maxResponseBytes: options.maxResponseBytes,
|
|
309
|
-
lookup: options.lookup,
|
|
310
|
-
allowedLoopbackHostname: loopbackHostnameForResource(resource),
|
|
311
|
-
label: "token endpoint"
|
|
312
|
-
});
|
|
313
|
-
if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
|
|
314
|
-
return parseTokenResponse(response, resource);
|
|
315
|
-
}
|
|
316
|
-
async function refreshMcpAccessToken(options) {
|
|
317
|
-
const resource = canonicalMcpResource(options.resource);
|
|
318
|
-
const previousRefreshToken = boundedCredential(options.refreshToken, "refresh token");
|
|
319
|
-
const body = new URLSearchParams({
|
|
320
|
-
grant_type: "refresh_token",
|
|
321
|
-
refresh_token: previousRefreshToken,
|
|
322
|
-
client_id: boundedCredential(options.clientId, "client id"),
|
|
323
|
-
resource
|
|
324
|
-
}).toString();
|
|
325
|
-
const response = await requestPinnedJson(options.authorizationServer.tokenEndpoint, {
|
|
326
|
-
method: "POST",
|
|
327
|
-
body,
|
|
328
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
329
|
-
signal: options.signal,
|
|
330
|
-
timeoutMs: options.timeoutMs,
|
|
331
|
-
maxResponseBytes: options.maxResponseBytes,
|
|
332
|
-
lookup: options.lookup,
|
|
333
|
-
allowedLoopbackHostname: loopbackHostnameForResource(resource),
|
|
334
|
-
label: "token endpoint"
|
|
335
|
-
});
|
|
336
|
-
if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
|
|
337
|
-
const parsed = parseTokenResponse(response, resource);
|
|
338
|
-
return { ...parsed, refreshToken: parsed.refreshToken ?? previousRefreshToken };
|
|
339
|
-
}
|
|
340
178
|
function challengeParameter(parameters, name) {
|
|
341
179
|
const pattern = new RegExp(
|
|
342
180
|
`(?:^|,)\\s*${name}\\s*=\\s*(?:"((?:\\\\.|[^"\\\\])*)"|([^,\\s]+))`,
|
|
@@ -356,9 +194,9 @@ function validateMetadataUrl(value) {
|
|
|
356
194
|
return void 0;
|
|
357
195
|
}
|
|
358
196
|
}
|
|
359
|
-
function record(value,
|
|
197
|
+
function record(value, label2) {
|
|
360
198
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
361
|
-
throw new Error(`MCP ${
|
|
199
|
+
throw new Error(`MCP ${label2} must be an object`);
|
|
362
200
|
}
|
|
363
201
|
return value;
|
|
364
202
|
}
|
|
@@ -387,23 +225,23 @@ function boundedStringArray(value, field, maxItems) {
|
|
|
387
225
|
function optionalStringArray(value, field, maxItems) {
|
|
388
226
|
return value === void 0 ? [] : boundedStringArray(value, field, maxItems);
|
|
389
227
|
}
|
|
390
|
-
function secureOAuthUrl(value,
|
|
228
|
+
function secureOAuthUrl(value, label2) {
|
|
391
229
|
let url;
|
|
392
230
|
try {
|
|
393
231
|
url = new URL(value);
|
|
394
232
|
} catch {
|
|
395
|
-
throw new Error(`MCP ${
|
|
233
|
+
throw new Error(`MCP ${label2} must be an absolute URL`);
|
|
396
234
|
}
|
|
397
235
|
if (url.protocol !== "https:" && !isLoopbackHttp(url)) {
|
|
398
|
-
throw new Error(`MCP ${
|
|
236
|
+
throw new Error(`MCP ${label2} must use HTTPS (except loopback development)`);
|
|
399
237
|
}
|
|
400
238
|
if (url.username || url.password || url.search || url.hash) {
|
|
401
|
-
throw new Error(`MCP ${
|
|
239
|
+
throw new Error(`MCP ${label2} must not contain credentials, query, or fragment components`);
|
|
402
240
|
}
|
|
403
241
|
if (url.pathname === "/") return new URL(url.origin);
|
|
404
242
|
return url;
|
|
405
243
|
}
|
|
406
|
-
async function discoverFirst(candidates, fetchJson, signal, parse,
|
|
244
|
+
async function discoverFirst(candidates, fetchJson, signal, parse, label2) {
|
|
407
245
|
const failures = [];
|
|
408
246
|
for (const candidate of candidates) {
|
|
409
247
|
signal?.throwIfAborted();
|
|
@@ -419,11 +257,11 @@ async function discoverFirst(candidates, fetchJson, signal, parse, label) {
|
|
|
419
257
|
failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
420
258
|
}
|
|
421
259
|
}
|
|
422
|
-
throw new Error(`MCP ${
|
|
260
|
+
throw new Error(`MCP ${label2} discovery failed (${failures.join("; ")})`);
|
|
423
261
|
}
|
|
424
262
|
async function requestPinnedJson(rawUrl, options) {
|
|
425
|
-
const
|
|
426
|
-
const url = secureOAuthUrl(rawUrl, `${
|
|
263
|
+
const label2 = options.label ?? "discovery";
|
|
264
|
+
const url = secureOAuthUrl(rawUrl, `${label2} URL`);
|
|
427
265
|
const target = await resolvePinnedAddress(url, options);
|
|
428
266
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
429
267
|
const maxBytes = options.maxResponseBytes ?? 64 * 1024;
|
|
@@ -467,12 +305,12 @@ async function requestPinnedJson(rawUrl, options) {
|
|
|
467
305
|
}
|
|
468
306
|
if (status >= 300 && status < 400) {
|
|
469
307
|
response.resume();
|
|
470
|
-
finish(new Error(`MCP OAuth ${
|
|
308
|
+
finish(new Error(`MCP OAuth ${label2} redirects are not allowed`));
|
|
471
309
|
return;
|
|
472
310
|
}
|
|
473
311
|
if (status < 200 || status >= 300) {
|
|
474
312
|
response.resume();
|
|
475
|
-
finish(new MCPOAuthHttpError(`MCP OAuth ${
|
|
313
|
+
finish(new MCPOAuthHttpError(`MCP OAuth ${label2} HTTP ${status}`, status));
|
|
476
314
|
return;
|
|
477
315
|
}
|
|
478
316
|
const contentType = response.headers["content-type"] ?? "";
|
|
@@ -516,21 +354,21 @@ async function requestPinnedJson(rawUrl, options) {
|
|
|
516
354
|
});
|
|
517
355
|
}
|
|
518
356
|
async function resolvePinnedAddress(url, options) {
|
|
519
|
-
const
|
|
520
|
-
const literalFamily = net.isIP(
|
|
357
|
+
const hostname2 = unbracket(url.hostname).toLowerCase();
|
|
358
|
+
const literalFamily = net.isIP(hostname2);
|
|
521
359
|
if (literalFamily === 4 || literalFamily === 6) {
|
|
522
360
|
assertDiscoveryAddressAllowed(
|
|
523
|
-
|
|
361
|
+
hostname2,
|
|
524
362
|
literalFamily,
|
|
525
|
-
|
|
363
|
+
hostname2,
|
|
526
364
|
options.allowedLoopbackHostname
|
|
527
365
|
);
|
|
528
|
-
return { address:
|
|
366
|
+
return { address: hostname2, family: literalFamily };
|
|
529
367
|
}
|
|
530
368
|
const lookup3 = options.lookup ?? ((host) => dns.lookup(host, { all: true }));
|
|
531
|
-
const records = await lookup3(
|
|
369
|
+
const records = await lookup3(hostname2);
|
|
532
370
|
if (records.length === 0)
|
|
533
|
-
throw new Error(`MCP OAuth discovery DNS returned no addresses for ${
|
|
371
|
+
throw new Error(`MCP OAuth discovery DNS returned no addresses for ${hostname2}`);
|
|
534
372
|
for (const record3 of records) {
|
|
535
373
|
if (record3.family !== 4 && record3.family !== 6) {
|
|
536
374
|
throw new Error("MCP OAuth discovery DNS returned an unsupported address family");
|
|
@@ -538,22 +376,194 @@ async function resolvePinnedAddress(url, options) {
|
|
|
538
376
|
assertDiscoveryAddressAllowed(
|
|
539
377
|
record3.address,
|
|
540
378
|
record3.family,
|
|
541
|
-
|
|
379
|
+
hostname2,
|
|
542
380
|
options.allowedLoopbackHostname
|
|
543
381
|
);
|
|
544
382
|
}
|
|
545
383
|
const selected = records[0];
|
|
546
384
|
return { address: selected.address, family: selected.family };
|
|
547
385
|
}
|
|
548
|
-
function assertDiscoveryAddressAllowed(address, family,
|
|
386
|
+
function assertDiscoveryAddressAllowed(address, family, hostname2, allowedLoopbackHostname) {
|
|
549
387
|
const isPrivate = family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
|
|
550
388
|
if (!isPrivate) return;
|
|
551
389
|
const loopback = family === 4 ? address.startsWith("127.") : address === "::1";
|
|
552
|
-
if (loopback &&
|
|
390
|
+
if (loopback && hostname2 === allowedLoopbackHostname) return;
|
|
553
391
|
throw new Error(`MCP OAuth discovery blocked private address ${address}`);
|
|
554
392
|
}
|
|
555
|
-
function unbracket(
|
|
556
|
-
return
|
|
393
|
+
function unbracket(hostname2) {
|
|
394
|
+
return hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
395
|
+
}
|
|
396
|
+
function isLoopbackHttp(url) {
|
|
397
|
+
if (url.protocol !== "http:") return false;
|
|
398
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/authorization.ts
|
|
402
|
+
function authorizationHeaderForToken(token, expectedResource, now = Date.now()) {
|
|
403
|
+
if (canonicalMcpResource(token.resource) !== expectedResource) {
|
|
404
|
+
throw new Error("MCP access token resource does not match the target server");
|
|
405
|
+
}
|
|
406
|
+
if (token.expiresAt !== void 0 && token.expiresAt <= now) {
|
|
407
|
+
throw new Error("MCP access token is expired");
|
|
408
|
+
}
|
|
409
|
+
const tokenType = token.tokenType ?? "Bearer";
|
|
410
|
+
if (tokenType.toLowerCase() !== "bearer") {
|
|
411
|
+
throw new Error(`Unsupported MCP OAuth token type "${tokenType}"`);
|
|
412
|
+
}
|
|
413
|
+
if (!token.accessToken || token.accessToken.length > 16384 || /[\r\n]/.test(token.accessToken)) {
|
|
414
|
+
throw new Error("MCP access token is empty, oversized, or contains invalid characters");
|
|
415
|
+
}
|
|
416
|
+
return `Bearer ${token.accessToken}`;
|
|
417
|
+
}
|
|
418
|
+
function validateMcpAuthorizationServerMetadata(value) {
|
|
419
|
+
const metadata = record(value, "stored authorization server metadata");
|
|
420
|
+
const registration = optionalString(metadata["registrationEndpoint"], "registrationEndpoint");
|
|
421
|
+
const issuer = secureOAuthUrl(requiredString(metadata["issuer"], "issuer"), "issuer").toString();
|
|
422
|
+
const authorizationEndpoint = secureOAuthUrl(
|
|
423
|
+
requiredString(metadata["authorizationEndpoint"], "authorizationEndpoint"),
|
|
424
|
+
"authorization endpoint"
|
|
425
|
+
).toString();
|
|
426
|
+
const tokenEndpoint = secureOAuthUrl(
|
|
427
|
+
requiredString(metadata["tokenEndpoint"], "tokenEndpoint"),
|
|
428
|
+
"token endpoint"
|
|
429
|
+
).toString();
|
|
430
|
+
const issuerParameterSupported = optionalBoolean(
|
|
431
|
+
metadata["authorizationResponseIssuerParameterSupported"],
|
|
432
|
+
"authorizationResponseIssuerParameterSupported"
|
|
433
|
+
);
|
|
434
|
+
if (issuerParameterSupported !== true && (new URL(authorizationEndpoint).origin !== new URL(issuer).origin || new URL(tokenEndpoint).origin !== new URL(issuer).origin)) {
|
|
435
|
+
throw new Error(
|
|
436
|
+
"MCP authorization server with cross-origin endpoints must support the authorization response issuer parameter"
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
issuer,
|
|
441
|
+
authorizationEndpoint,
|
|
442
|
+
tokenEndpoint,
|
|
443
|
+
registrationEndpoint: registration ? secureOAuthUrl(registration, "registration endpoint").toString() : void 0,
|
|
444
|
+
authorizationResponseIssuerParameterSupported: issuerParameterSupported,
|
|
445
|
+
scopesSupported: optionalStringArray(metadata["scopesSupported"], "scopesSupported", 128)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function createMcpAuthorizationRequest(options) {
|
|
449
|
+
const resource = canonicalMcpResource(options.resource);
|
|
450
|
+
const clientId = boundedCredential(options.clientId, "client id");
|
|
451
|
+
const redirectUri = validateRedirectUri(options.redirectUri);
|
|
452
|
+
const scopes = validateScopes(options.scopes ?? []);
|
|
453
|
+
const codeVerifier = base64Url(randomBytes(32));
|
|
454
|
+
const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
|
|
455
|
+
const state = base64Url(randomBytes(32));
|
|
456
|
+
const authorizationUrl = secureOAuthUrl(
|
|
457
|
+
options.authorizationServer.authorizationEndpoint,
|
|
458
|
+
"authorization endpoint"
|
|
459
|
+
);
|
|
460
|
+
authorizationUrl.searchParams.set("response_type", "code");
|
|
461
|
+
authorizationUrl.searchParams.set("client_id", clientId);
|
|
462
|
+
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
|
463
|
+
authorizationUrl.searchParams.set("state", state);
|
|
464
|
+
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
|
|
465
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
466
|
+
authorizationUrl.searchParams.set("resource", resource);
|
|
467
|
+
if (scopes.length > 0) authorizationUrl.searchParams.set("scope", scopes.join(" "));
|
|
468
|
+
return {
|
|
469
|
+
authorizationUrl: authorizationUrl.toString(),
|
|
470
|
+
state,
|
|
471
|
+
codeVerifier,
|
|
472
|
+
redirectUri,
|
|
473
|
+
clientId,
|
|
474
|
+
resource,
|
|
475
|
+
issuer: options.authorizationServer.issuer,
|
|
476
|
+
requireIssuerParameter: options.authorizationServer.authorizationResponseIssuerParameterSupported === true
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
function parseMcpAuthorizationCallback(callbackUrl, session) {
|
|
480
|
+
let callback;
|
|
481
|
+
try {
|
|
482
|
+
callback = new URL(callbackUrl);
|
|
483
|
+
} catch {
|
|
484
|
+
throw new Error("MCP OAuth callback must be an absolute URL");
|
|
485
|
+
}
|
|
486
|
+
const expected = new URL(validateRedirectUri(session.redirectUri));
|
|
487
|
+
if (callback.protocol !== expected.protocol || callback.hostname !== expected.hostname || callback.port !== expected.port || callback.pathname !== expected.pathname) {
|
|
488
|
+
throw new Error("MCP OAuth callback redirect URI does not match the authorization session");
|
|
489
|
+
}
|
|
490
|
+
const returnedStates = callback.searchParams.getAll("state");
|
|
491
|
+
if (returnedStates.length !== 1) {
|
|
492
|
+
throw new Error("MCP OAuth callback must contain exactly one state parameter");
|
|
493
|
+
}
|
|
494
|
+
const returnedState = returnedStates[0];
|
|
495
|
+
if (!constantTimeEqual(returnedState, session.state)) {
|
|
496
|
+
throw new Error("MCP OAuth callback state mismatch");
|
|
497
|
+
}
|
|
498
|
+
const returnedIssuers = callback.searchParams.getAll("iss");
|
|
499
|
+
if (session.requireIssuerParameter && returnedIssuers.length !== 1) {
|
|
500
|
+
throw new Error("MCP OAuth callback must contain exactly one issuer parameter");
|
|
501
|
+
}
|
|
502
|
+
if (returnedIssuers.length > 0 && returnedIssuers.some((issuer) => issuer !== session.issuer)) {
|
|
503
|
+
throw new Error("MCP OAuth callback issuer mismatch");
|
|
504
|
+
}
|
|
505
|
+
const oauthErrors = callback.searchParams.getAll("error");
|
|
506
|
+
const codes = callback.searchParams.getAll("code");
|
|
507
|
+
if (oauthErrors.length > 1 || codes.length > 1 || oauthErrors.length === 1 && codes.length > 0) {
|
|
508
|
+
throw new Error("MCP OAuth callback contains ambiguous response parameters");
|
|
509
|
+
}
|
|
510
|
+
if (oauthErrors.length === 1) {
|
|
511
|
+
throw new Error(`MCP OAuth authorization failed: ${boundedErrorCode(oauthErrors[0])}`);
|
|
512
|
+
}
|
|
513
|
+
if (codes.length !== 1) {
|
|
514
|
+
throw new Error("MCP OAuth callback must contain exactly one authorization code");
|
|
515
|
+
}
|
|
516
|
+
return boundedCredential(codes[0], "authorization code");
|
|
517
|
+
}
|
|
518
|
+
async function exchangeMcpAuthorizationCode(options) {
|
|
519
|
+
const resource = canonicalMcpResource(options.resource);
|
|
520
|
+
const body = new URLSearchParams({
|
|
521
|
+
grant_type: "authorization_code",
|
|
522
|
+
code: boundedCredential(options.code, "authorization code"),
|
|
523
|
+
client_id: boundedCredential(options.clientId, "client id"),
|
|
524
|
+
redirect_uri: validateRedirectUri(options.redirectUri),
|
|
525
|
+
code_verifier: validateCodeVerifier(options.codeVerifier),
|
|
526
|
+
resource,
|
|
527
|
+
...options.clientSecret ? { client_secret: boundedCredential(options.clientSecret, "client secret") } : {}
|
|
528
|
+
}).toString();
|
|
529
|
+
const response = await requestPinnedJson(options.authorizationServer.tokenEndpoint, {
|
|
530
|
+
method: "POST",
|
|
531
|
+
body,
|
|
532
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
533
|
+
signal: options.signal,
|
|
534
|
+
timeoutMs: options.timeoutMs,
|
|
535
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
536
|
+
lookup: options.lookup,
|
|
537
|
+
allowedLoopbackHostname: loopbackHostnameForResource(resource),
|
|
538
|
+
label: "token endpoint"
|
|
539
|
+
});
|
|
540
|
+
if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
|
|
541
|
+
return parseTokenResponse(response, resource);
|
|
542
|
+
}
|
|
543
|
+
async function refreshMcpAccessToken(options) {
|
|
544
|
+
const resource = canonicalMcpResource(options.resource);
|
|
545
|
+
const previousRefreshToken = boundedCredential(options.refreshToken, "refresh token");
|
|
546
|
+
const body = new URLSearchParams({
|
|
547
|
+
grant_type: "refresh_token",
|
|
548
|
+
refresh_token: previousRefreshToken,
|
|
549
|
+
client_id: boundedCredential(options.clientId, "client id"),
|
|
550
|
+
resource,
|
|
551
|
+
...options.clientSecret ? { client_secret: boundedCredential(options.clientSecret, "client secret") } : {}
|
|
552
|
+
}).toString();
|
|
553
|
+
const response = await requestPinnedJson(options.authorizationServer.tokenEndpoint, {
|
|
554
|
+
method: "POST",
|
|
555
|
+
body,
|
|
556
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
557
|
+
signal: options.signal,
|
|
558
|
+
timeoutMs: options.timeoutMs,
|
|
559
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
560
|
+
lookup: options.lookup,
|
|
561
|
+
allowedLoopbackHostname: loopbackHostnameForResource(resource),
|
|
562
|
+
label: "token endpoint"
|
|
563
|
+
});
|
|
564
|
+
if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
|
|
565
|
+
const parsed = parseTokenResponse(response, resource);
|
|
566
|
+
return { ...parsed, refreshToken: parsed.refreshToken ?? previousRefreshToken };
|
|
557
567
|
}
|
|
558
568
|
function validateRedirectUri(value) {
|
|
559
569
|
let url;
|
|
@@ -586,9 +596,9 @@ function validateCodeVerifier(value) {
|
|
|
586
596
|
}
|
|
587
597
|
return value;
|
|
588
598
|
}
|
|
589
|
-
function boundedCredential(value,
|
|
599
|
+
function boundedCredential(value, label2) {
|
|
590
600
|
if (!value || value.length > 16384 || /[\r\n]/.test(value)) {
|
|
591
|
-
throw new Error(`MCP OAuth ${
|
|
601
|
+
throw new Error(`MCP OAuth ${label2} is empty, oversized, or invalid`);
|
|
592
602
|
}
|
|
593
603
|
return value;
|
|
594
604
|
}
|
|
@@ -638,14 +648,213 @@ function loopbackHostnameForResource(resource) {
|
|
|
638
648
|
const url = new URL(resource);
|
|
639
649
|
return isLoopbackHttp(url) ? unbracket(url.hostname).toLowerCase() : void 0;
|
|
640
650
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
651
|
+
|
|
652
|
+
// src/authorization-callback-server.ts
|
|
653
|
+
import * as http2 from "node:http";
|
|
654
|
+
var DEFAULT_PATH = "/callback";
|
|
655
|
+
var DEFAULT_TIMEOUT_MS = 5 * 6e4;
|
|
656
|
+
var MAX_REQUESTS = 16;
|
|
657
|
+
async function startMcpOAuthCallbackServer(options = {}) {
|
|
658
|
+
const path2 = normalizePath(options.path ?? DEFAULT_PATH);
|
|
659
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
660
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
661
|
+
throw new Error("MCP OAuth callback timeout must be a positive finite number");
|
|
662
|
+
}
|
|
663
|
+
options.signal?.throwIfAborted();
|
|
664
|
+
let settle;
|
|
665
|
+
let fail;
|
|
666
|
+
let finished = false;
|
|
667
|
+
let requests = 0;
|
|
668
|
+
const callback = new Promise((resolve, reject) => {
|
|
669
|
+
settle = resolve;
|
|
670
|
+
fail = reject;
|
|
671
|
+
});
|
|
672
|
+
callback.catch(() => void 0);
|
|
673
|
+
const server = http2.createServer((req, res) => {
|
|
674
|
+
if (finished || ++requests > MAX_REQUESTS) {
|
|
675
|
+
res.writeHead(429).end();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const requestUrl = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
679
|
+
if (req.method !== "GET" || requestUrl.pathname !== path2) {
|
|
680
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" }).end("Not found");
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
finished = true;
|
|
684
|
+
res.writeHead(200, {
|
|
685
|
+
"content-type": "text/html; charset=utf-8",
|
|
686
|
+
// The URL carries a one-shot authorization code; keep it out of caches
|
|
687
|
+
// and out of the next site the user visits.
|
|
688
|
+
"cache-control": "no-store",
|
|
689
|
+
"referrer-policy": "no-referrer"
|
|
690
|
+
}).end(SUCCESS_PAGE);
|
|
691
|
+
const address2 = server.address();
|
|
692
|
+
const port = address2?.port ?? 0;
|
|
693
|
+
settle?.(`http://127.0.0.1:${port}${req.url ?? path2}`);
|
|
694
|
+
});
|
|
695
|
+
server.on("error", (error) => {
|
|
696
|
+
finished = true;
|
|
697
|
+
fail?.(error);
|
|
698
|
+
});
|
|
699
|
+
await new Promise((resolve, reject) => {
|
|
700
|
+
server.once("error", reject);
|
|
701
|
+
server.listen(options.port ?? 0, "127.0.0.1", () => {
|
|
702
|
+
server.removeListener("error", reject);
|
|
703
|
+
resolve();
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
const address = server.address();
|
|
707
|
+
if (!address) {
|
|
708
|
+
server.close();
|
|
709
|
+
throw new Error("MCP OAuth callback listener failed to bind a loopback port");
|
|
710
|
+
}
|
|
711
|
+
const redirectUri = `http://127.0.0.1:${address.port}${path2}`;
|
|
712
|
+
const timer = setTimeout(() => {
|
|
713
|
+
finished = true;
|
|
714
|
+
fail?.(new Error(`MCP OAuth callback was not received within ${timeoutMs}ms`));
|
|
715
|
+
close();
|
|
716
|
+
}, timeoutMs);
|
|
717
|
+
timer.unref?.();
|
|
718
|
+
const onAbort = () => {
|
|
719
|
+
finished = true;
|
|
720
|
+
fail?.(
|
|
721
|
+
options.signal?.reason instanceof Error ? options.signal.reason : new Error("MCP OAuth callback wait was aborted")
|
|
722
|
+
);
|
|
723
|
+
close();
|
|
724
|
+
};
|
|
725
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
726
|
+
function close() {
|
|
727
|
+
clearTimeout(timer);
|
|
728
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
729
|
+
server.closeAllConnections?.();
|
|
730
|
+
server.close(() => void 0);
|
|
731
|
+
}
|
|
732
|
+
return {
|
|
733
|
+
redirectUri,
|
|
734
|
+
waitForCallback: () => callback.finally(close),
|
|
735
|
+
close
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function normalizePath(value) {
|
|
739
|
+
if (!value.startsWith("/") || value.length > 256 || /[?#\s]/.test(value)) {
|
|
740
|
+
throw new Error("MCP OAuth callback path must be a bounded absolute path without query");
|
|
741
|
+
}
|
|
742
|
+
return value;
|
|
743
|
+
}
|
|
744
|
+
var SUCCESS_PAGE = `<!doctype html>
|
|
745
|
+
<html lang="en"><head><meta charset="utf-8"><title>Authorized</title></head>
|
|
746
|
+
<body style="font-family:system-ui,sans-serif;padding:3rem;text-align:center">
|
|
747
|
+
<h1>Authorization complete</h1>
|
|
748
|
+
<p>You can close this tab and return to WrongStack.</p>
|
|
749
|
+
</body></html>
|
|
750
|
+
`;
|
|
751
|
+
|
|
752
|
+
// src/authorization-registration.ts
|
|
753
|
+
var DEFAULT_CLIENT_NAME = "WrongStack";
|
|
754
|
+
var MAX_REDIRECT_URIS = 8;
|
|
755
|
+
async function registerMcpOAuthClient(options) {
|
|
756
|
+
const endpoint = options.authorizationServer.registrationEndpoint;
|
|
757
|
+
if (!endpoint) {
|
|
758
|
+
throw new Error(
|
|
759
|
+
"MCP authorization server does not support dynamic client registration; pass an explicit client id"
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
const redirectUris = validateRedirectUris(options.redirectUris);
|
|
763
|
+
const scopes = options.scopes?.filter(Boolean) ?? [];
|
|
764
|
+
const body = JSON.stringify({
|
|
765
|
+
client_name: boundedClientName(options.clientName ?? DEFAULT_CLIENT_NAME),
|
|
766
|
+
redirect_uris: redirectUris,
|
|
767
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
768
|
+
response_types: ["code"],
|
|
769
|
+
// A CLI is a public client: it cannot hold a secret that the user could not
|
|
770
|
+
// also read. Asking for `none` keeps the PKCE-only exchange we implement.
|
|
771
|
+
token_endpoint_auth_method: "none",
|
|
772
|
+
application_type: "native",
|
|
773
|
+
...options.clientUri ? { client_uri: options.clientUri } : {},
|
|
774
|
+
...scopes.length > 0 ? { scope: scopes.join(" ") } : {}
|
|
775
|
+
});
|
|
776
|
+
const request3 = options.requestJson ?? requestPinnedJson;
|
|
777
|
+
const response = await request3(endpoint, {
|
|
778
|
+
method: "POST",
|
|
779
|
+
body,
|
|
780
|
+
headers: { "content-type": "application/json" },
|
|
781
|
+
signal: options.signal,
|
|
782
|
+
timeoutMs: options.timeoutMs,
|
|
783
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
784
|
+
lookup: options.lookup,
|
|
785
|
+
allowedLoopbackHostname: loopbackHostnameForResource2(options.resource),
|
|
786
|
+
label: "client registration endpoint"
|
|
787
|
+
});
|
|
788
|
+
if (response === void 0) {
|
|
789
|
+
throw new Error("MCP OAuth client registration endpoint returned no response");
|
|
790
|
+
}
|
|
791
|
+
return parseRegistrationResponse(response, options.authorizationServer.issuer);
|
|
792
|
+
}
|
|
793
|
+
function parseRegistrationResponse(value, issuer) {
|
|
794
|
+
const metadata = record(value, "client registration response");
|
|
795
|
+
const clientId = requiredString(metadata["client_id"], "client_id");
|
|
796
|
+
if (/[\r\n]/.test(clientId)) {
|
|
797
|
+
throw new Error("MCP OAuth client registration returned an invalid client_id");
|
|
798
|
+
}
|
|
799
|
+
const clientSecret = optionalString(metadata["client_secret"], "client_secret");
|
|
800
|
+
if (clientSecret !== void 0 && /[\r\n]/.test(clientSecret)) {
|
|
801
|
+
throw new Error("MCP OAuth client registration returned an invalid client_secret");
|
|
802
|
+
}
|
|
803
|
+
const expiresAt = metadata["client_secret_expires_at"];
|
|
804
|
+
if (clientSecret !== void 0 && typeof expiresAt === "number" && expiresAt !== 0) {
|
|
805
|
+
if (!Number.isFinite(expiresAt) || expiresAt * 1e3 <= Date.now()) {
|
|
806
|
+
throw new Error("MCP OAuth client registration returned an already-expired client secret");
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
clientId,
|
|
811
|
+
...clientSecret !== void 0 ? { clientSecret } : {},
|
|
812
|
+
issuer,
|
|
813
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function validateRedirectUris(values) {
|
|
817
|
+
if (values.length === 0 || values.length > MAX_REDIRECT_URIS) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`MCP OAuth client registration requires between 1 and ${MAX_REDIRECT_URIS} redirect URIs`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
return values.map((value) => {
|
|
823
|
+
let url;
|
|
824
|
+
try {
|
|
825
|
+
url = new URL(value);
|
|
826
|
+
} catch {
|
|
827
|
+
throw new Error("MCP OAuth redirect URI must be an absolute URL");
|
|
828
|
+
}
|
|
829
|
+
if (url.protocol !== "https:" && !isLoopbackHttp(url)) {
|
|
830
|
+
throw new Error("MCP OAuth redirect URI must use HTTPS or loopback HTTP");
|
|
831
|
+
}
|
|
832
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
833
|
+
throw new Error("MCP OAuth redirect URI must not contain credentials, query, or fragment");
|
|
834
|
+
}
|
|
835
|
+
return url.toString();
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
function boundedClientName(value) {
|
|
839
|
+
if (!value || value.length > 128 || /[\r\n]/.test(value)) {
|
|
840
|
+
throw new Error("MCP OAuth client name must be a bounded single-line string");
|
|
841
|
+
}
|
|
842
|
+
return value;
|
|
843
|
+
}
|
|
844
|
+
function loopbackHostnameForResource2(resource) {
|
|
845
|
+
if (!resource) return void 0;
|
|
846
|
+
try {
|
|
847
|
+
const url = new URL(resource);
|
|
848
|
+
return isLoopbackHttp(url) ? unbracket(url.hostname).toLowerCase() : void 0;
|
|
849
|
+
} catch {
|
|
850
|
+
return void 0;
|
|
851
|
+
}
|
|
644
852
|
}
|
|
645
853
|
|
|
646
854
|
// src/authorization-manager.ts
|
|
647
855
|
var DEFAULT_PENDING_TTL_MS = 10 * 6e4;
|
|
648
856
|
var MAX_PENDING_AUTHORIZATIONS = 32;
|
|
857
|
+
var MAX_CACHED_REGISTRATIONS = 32;
|
|
649
858
|
var MCPAuthorizationManager = class {
|
|
650
859
|
constructor(options) {
|
|
651
860
|
this.options = options;
|
|
@@ -655,14 +864,18 @@ var MCPAuthorizationManager = class {
|
|
|
655
864
|
}
|
|
656
865
|
this.discover = options.discover ?? discoverMcpAuthorization;
|
|
657
866
|
this.exchange = options.exchange ?? exchangeMcpAuthorizationCode;
|
|
867
|
+
this.register = options.register ?? registerMcpOAuthClient;
|
|
658
868
|
this.now = options.now ?? Date.now;
|
|
659
869
|
}
|
|
660
870
|
options;
|
|
661
871
|
pending = /* @__PURE__ */ new Map();
|
|
662
872
|
starting = /* @__PURE__ */ new Map();
|
|
873
|
+
/** Registrations reused across servers that share one authorization server. */
|
|
874
|
+
registrations = /* @__PURE__ */ new Map();
|
|
663
875
|
pendingTtlMs;
|
|
664
876
|
discover;
|
|
665
877
|
exchange;
|
|
878
|
+
register;
|
|
666
879
|
now;
|
|
667
880
|
async begin(input) {
|
|
668
881
|
const resource = canonicalMcpResource(input.resource);
|
|
@@ -689,23 +902,42 @@ var MCPAuthorizationManager = class {
|
|
|
689
902
|
resource
|
|
690
903
|
).scopes;
|
|
691
904
|
const scopes = input.scopes ? [...input.scopes] : challengeScopes;
|
|
905
|
+
const identity = await this.resolveClientIdentity({
|
|
906
|
+
serverName: input.serverName,
|
|
907
|
+
resource,
|
|
908
|
+
discovery,
|
|
909
|
+
redirectUri: input.redirectUri,
|
|
910
|
+
explicitClientId: input.clientId,
|
|
911
|
+
scopes,
|
|
912
|
+
signal: input.signal
|
|
913
|
+
});
|
|
914
|
+
if (this.starting.get(key) !== attempt) {
|
|
915
|
+
throw new Error("MCP authorization discovery was cancelled");
|
|
916
|
+
}
|
|
692
917
|
const session = createMcpAuthorizationRequest({
|
|
693
918
|
authorizationServer: discovery.authorizationServer,
|
|
694
|
-
clientId:
|
|
919
|
+
clientId: identity.clientId,
|
|
695
920
|
redirectUri: input.redirectUri,
|
|
696
921
|
resource,
|
|
697
922
|
scopes
|
|
698
923
|
});
|
|
699
924
|
const normalizedScopes = new URL(session.authorizationUrl).searchParams.get("scope")?.split(" ").filter(Boolean) ?? [];
|
|
700
925
|
const expiresAt = this.now() + this.pendingTtlMs;
|
|
701
|
-
this.pending.set(key, {
|
|
926
|
+
this.pending.set(key, {
|
|
927
|
+
session,
|
|
928
|
+
discovery,
|
|
929
|
+
scopes: normalizedScopes,
|
|
930
|
+
expiresAt,
|
|
931
|
+
clientSecret: identity.clientSecret
|
|
932
|
+
});
|
|
702
933
|
return {
|
|
703
934
|
serverName: boundedServerName(input.serverName),
|
|
704
935
|
resource,
|
|
705
936
|
authorizationUrl: session.authorizationUrl,
|
|
706
937
|
redirectUri: session.redirectUri,
|
|
707
938
|
scopes: [...normalizedScopes],
|
|
708
|
-
expiresAt
|
|
939
|
+
expiresAt,
|
|
940
|
+
clientIdSource: identity.source
|
|
709
941
|
};
|
|
710
942
|
} finally {
|
|
711
943
|
if (this.starting.get(key) === attempt) this.starting.delete(key);
|
|
@@ -725,23 +957,106 @@ var MCPAuthorizationManager = class {
|
|
|
725
957
|
const tokenSet = await this.exchange({
|
|
726
958
|
authorizationServer: pending.discovery.authorizationServer,
|
|
727
959
|
clientId: pending.session.clientId,
|
|
960
|
+
clientSecret: pending.clientSecret,
|
|
728
961
|
redirectUri: pending.session.redirectUri,
|
|
729
962
|
resource,
|
|
730
963
|
code,
|
|
731
964
|
codeVerifier: pending.session.codeVerifier,
|
|
732
965
|
signal: input.signal
|
|
733
966
|
});
|
|
734
|
-
const stored = {
|
|
735
|
-
serverName,
|
|
736
|
-
resource,
|
|
737
|
-
clientId: pending.session.clientId,
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
967
|
+
const stored = {
|
|
968
|
+
serverName,
|
|
969
|
+
resource,
|
|
970
|
+
clientId: pending.session.clientId,
|
|
971
|
+
...pending.clientSecret ? { clientSecret: pending.clientSecret } : {},
|
|
972
|
+
authorizationServer: pending.discovery.authorizationServer,
|
|
973
|
+
tokenSet,
|
|
974
|
+
updatedAt: new Date(this.now()).toISOString()
|
|
975
|
+
};
|
|
976
|
+
await this.options.store.save(stored);
|
|
977
|
+
this.emit("authorized", stored);
|
|
978
|
+
return statusFromStored(stored, this.now());
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Bind a loopback receiver and start the flow, returning as soon as the URL
|
|
982
|
+
* exists so a surface can show it without blocking. Completion is awaited
|
|
983
|
+
* separately — a slash command returns its message immediately and reports
|
|
984
|
+
* the outcome through `onStateChange`. The two-step `begin`/`complete` pair
|
|
985
|
+
* stays for surfaces that cannot host a listener.
|
|
986
|
+
*/
|
|
987
|
+
async beginLogin(input) {
|
|
988
|
+
const callbackOptions = {
|
|
989
|
+
port: input.port,
|
|
990
|
+
timeoutMs: input.timeoutMs,
|
|
991
|
+
signal: input.signal
|
|
992
|
+
};
|
|
993
|
+
const listener = await startMcpOAuthCallbackServer(callbackOptions);
|
|
994
|
+
let started;
|
|
995
|
+
try {
|
|
996
|
+
started = await this.begin({
|
|
997
|
+
serverName: input.serverName,
|
|
998
|
+
resource: input.resource,
|
|
999
|
+
clientId: input.clientId,
|
|
1000
|
+
redirectUri: listener.redirectUri,
|
|
1001
|
+
scopes: input.scopes,
|
|
1002
|
+
challengeHeader: input.challengeHeader,
|
|
1003
|
+
signal: input.signal
|
|
1004
|
+
});
|
|
1005
|
+
} catch (error) {
|
|
1006
|
+
listener.close();
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
const completion = listener.waitForCallback().then(
|
|
1010
|
+
(callbackUrl) => this.complete({
|
|
1011
|
+
serverName: input.serverName,
|
|
1012
|
+
resource: input.resource,
|
|
1013
|
+
callbackUrl,
|
|
1014
|
+
signal: input.signal
|
|
1015
|
+
})
|
|
1016
|
+
).finally(() => listener.close());
|
|
1017
|
+
return { started, completion, cancel: () => listener.close() };
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Identity precedence: an explicit id, then the one already stored for this
|
|
1021
|
+
* server (so re-authorizing does not orphan a registration), then the
|
|
1022
|
+
* per-issuer cache, then a fresh dynamic registration.
|
|
1023
|
+
*/
|
|
1024
|
+
async resolveClientIdentity(input) {
|
|
1025
|
+
if (input.explicitClientId) {
|
|
1026
|
+
return { clientId: input.explicitClientId, source: "explicit" };
|
|
1027
|
+
}
|
|
1028
|
+
const issuer = normalizeIssuer(input.discovery.authorizationServer.issuer);
|
|
1029
|
+
const stored = await this.options.store.load(boundedServerName(input.serverName), input.resource).catch(() => void 0);
|
|
1030
|
+
if (stored && normalizeIssuer(stored.authorizationServer.issuer) === issuer) {
|
|
1031
|
+
return {
|
|
1032
|
+
clientId: stored.clientId,
|
|
1033
|
+
clientSecret: stored.clientSecret,
|
|
1034
|
+
source: "stored"
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
const registrationKey = `${issuer}\0${input.redirectUri}`;
|
|
1038
|
+
const cached = this.registrations.get(registrationKey);
|
|
1039
|
+
if (cached) {
|
|
1040
|
+
return { clientId: cached.clientId, clientSecret: cached.clientSecret, source: "registered" };
|
|
1041
|
+
}
|
|
1042
|
+
const registration = await this.register({
|
|
1043
|
+
authorizationServer: input.discovery.authorizationServer,
|
|
1044
|
+
redirectUris: [input.redirectUri],
|
|
1045
|
+
clientName: this.options.clientName,
|
|
1046
|
+
scopes: input.scopes,
|
|
1047
|
+
resource: input.resource,
|
|
1048
|
+
signal: input.signal
|
|
1049
|
+
});
|
|
1050
|
+
if (this.registrations.size >= MAX_CACHED_REGISTRATIONS) {
|
|
1051
|
+
const oldest = this.registrations.keys().next();
|
|
1052
|
+
if (!oldest.done) this.registrations.delete(oldest.value);
|
|
1053
|
+
}
|
|
1054
|
+
this.registrations.set(registrationKey, registration);
|
|
1055
|
+
return {
|
|
1056
|
+
clientId: registration.clientId,
|
|
1057
|
+
clientSecret: registration.clientSecret,
|
|
1058
|
+
source: "registered"
|
|
741
1059
|
};
|
|
742
|
-
await this.options.store.save(stored);
|
|
743
|
-
this.emit("authorized", stored);
|
|
744
|
-
return statusFromStored(stored, this.now());
|
|
745
1060
|
}
|
|
746
1061
|
async status(serverName, resource) {
|
|
747
1062
|
const normalizedName = boundedServerName(serverName);
|
|
@@ -816,6 +1131,9 @@ function statusFromStored(value, now) {
|
|
|
816
1131
|
canRefresh: !!value.tokenSet.refreshToken
|
|
817
1132
|
};
|
|
818
1133
|
}
|
|
1134
|
+
function normalizeIssuer(value) {
|
|
1135
|
+
return secureOAuthUrl(value, "authorization server issuer").toString();
|
|
1136
|
+
}
|
|
819
1137
|
function authorizationKey(serverName, resource) {
|
|
820
1138
|
return `${boundedServerName(serverName)}\0${resource}`;
|
|
821
1139
|
}
|
|
@@ -833,17 +1151,17 @@ import { buildChildEnv as buildChildEnv2, buildWin32CmdShimInvocation, toErrorMe
|
|
|
833
1151
|
|
|
834
1152
|
// src/client-protocol-helpers.ts
|
|
835
1153
|
var MAX_PROTOCOL_INPUT_CHARS = 8192;
|
|
836
|
-
function validateProtocolString(value,
|
|
1154
|
+
function validateProtocolString(value, label2, allowEmpty = false) {
|
|
837
1155
|
if (typeof value !== "string" || !allowEmpty && value.length === 0) {
|
|
838
|
-
throw new Error(`MCP ${
|
|
1156
|
+
throw new Error(`MCP ${label2} must be ${allowEmpty ? "a string" : "a non-empty string"}`);
|
|
839
1157
|
}
|
|
840
1158
|
if (value.length > MAX_PROTOCOL_INPUT_CHARS) {
|
|
841
|
-
throw new Error(`MCP ${
|
|
1159
|
+
throw new Error(`MCP ${label2} exceeds ${MAX_PROTOCOL_INPUT_CHARS} characters`);
|
|
842
1160
|
}
|
|
843
1161
|
}
|
|
844
|
-
function pageParams(cursor,
|
|
1162
|
+
function pageParams(cursor, label2) {
|
|
845
1163
|
if (cursor === void 0) return {};
|
|
846
|
-
validateProtocolString(cursor,
|
|
1164
|
+
validateProtocolString(cursor, label2);
|
|
847
1165
|
return { cursor };
|
|
848
1166
|
}
|
|
849
1167
|
function parseEmptyResult(value) {
|
|
@@ -853,29 +1171,29 @@ function parseEmptyResult(value) {
|
|
|
853
1171
|
}
|
|
854
1172
|
|
|
855
1173
|
// src/protocol.ts
|
|
856
|
-
function record2(value,
|
|
1174
|
+
function record2(value, label2) {
|
|
857
1175
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
858
|
-
throw new Error(`Malformed MCP ${
|
|
1176
|
+
throw new Error(`Malformed MCP ${label2}: expected object`);
|
|
859
1177
|
}
|
|
860
1178
|
return value;
|
|
861
1179
|
}
|
|
862
|
-
function requiredString2(value,
|
|
1180
|
+
function requiredString2(value, label2) {
|
|
863
1181
|
if (typeof value !== "string" || value.length === 0) {
|
|
864
|
-
throw new Error(`Malformed MCP ${
|
|
1182
|
+
throw new Error(`Malformed MCP ${label2}: expected non-empty string`);
|
|
865
1183
|
}
|
|
866
1184
|
return value;
|
|
867
1185
|
}
|
|
868
|
-
function optionalString2(value,
|
|
1186
|
+
function optionalString2(value, label2) {
|
|
869
1187
|
if (value === void 0) return void 0;
|
|
870
|
-
if (typeof value !== "string") throw new Error(`Malformed MCP ${
|
|
1188
|
+
if (typeof value !== "string") throw new Error(`Malformed MCP ${label2}: expected string`);
|
|
871
1189
|
return value;
|
|
872
1190
|
}
|
|
873
|
-
function optionalRecord(value,
|
|
1191
|
+
function optionalRecord(value, label2) {
|
|
874
1192
|
if (value === void 0) return void 0;
|
|
875
|
-
return record2(value,
|
|
1193
|
+
return record2(value, label2);
|
|
876
1194
|
}
|
|
877
|
-
function optionalCursor(value,
|
|
878
|
-
return optionalString2(value, `${
|
|
1195
|
+
function optionalCursor(value, label2) {
|
|
1196
|
+
return optionalString2(value, `${label2}.nextCursor`);
|
|
879
1197
|
}
|
|
880
1198
|
function parseServerMetadata(value) {
|
|
881
1199
|
const input = record2(value, "initialize result");
|
|
@@ -1052,6 +1370,13 @@ function parseGetPromptResult(value) {
|
|
|
1052
1370
|
})
|
|
1053
1371
|
};
|
|
1054
1372
|
}
|
|
1373
|
+
function resourceUpdatedUri(params) {
|
|
1374
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) return void 0;
|
|
1375
|
+
const uri = params.uri;
|
|
1376
|
+
if (typeof uri !== "string" || uri.length === 0 || uri.length > 4096) return void 0;
|
|
1377
|
+
if (/[\r\n]/.test(uri)) return void 0;
|
|
1378
|
+
return uri;
|
|
1379
|
+
}
|
|
1055
1380
|
|
|
1056
1381
|
// src/client-capabilities.ts
|
|
1057
1382
|
var MCPCapabilityClient = class {
|
|
@@ -1188,116 +1513,6 @@ function forceKillTree(child) {
|
|
|
1188
1513
|
}
|
|
1189
1514
|
}
|
|
1190
1515
|
|
|
1191
|
-
// src/constants.ts
|
|
1192
|
-
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2024-11-05"]);
|
|
1193
|
-
function negotiateProtocolVersion(requested) {
|
|
1194
|
-
if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
|
|
1195
|
-
return requested;
|
|
1196
|
-
}
|
|
1197
|
-
return MCP_CONSTANTS.PROTOCOL_VERSION;
|
|
1198
|
-
}
|
|
1199
|
-
var MCP_CONSTANTS = Object.freeze({
|
|
1200
|
-
/** MCP protocol version advertised during handshake: the newest we implement. */
|
|
1201
|
-
PROTOCOL_VERSION: SUPPORTED_PROTOCOL_VERSIONS[0],
|
|
1202
|
-
/** Identity announced to MCP servers during `initialize`. */
|
|
1203
|
-
CLIENT_INFO: Object.freeze({
|
|
1204
|
-
name: "wrongstack",
|
|
1205
|
-
version: "0.1.10"
|
|
1206
|
-
}),
|
|
1207
|
-
/** Reconnection behaviour when a transport disconnects. */
|
|
1208
|
-
RECONNECT: Object.freeze({
|
|
1209
|
-
/** Max full reconnect cycles before the slot is marked `failed`. */
|
|
1210
|
-
MAX_CYCLES: 5,
|
|
1211
|
-
/** Base delay between cycles (exponential backoff applied on top). */
|
|
1212
|
-
BASE_DELAY_MS: 1e3,
|
|
1213
|
-
/** Jitter factor applied to the backoff (0 = no jitter, 1 = full). */
|
|
1214
|
-
JITTER_FACTOR: 0.2,
|
|
1215
|
-
/** Max connection attempts within a single cycle. */
|
|
1216
|
-
MAX_ATTEMPTS: 3,
|
|
1217
|
-
/** Base multiplier for the exponential backoff formula (`delay = BASE * multiplier^attempt`). */
|
|
1218
|
-
BACKOFF_MULTIPLIER: 2
|
|
1219
|
-
}),
|
|
1220
|
-
/** Timing for graceful / forced disconnect. */
|
|
1221
|
-
DISCONNECT: Object.freeze({
|
|
1222
|
-
/** Ms to wait for in-flight requests to complete before force-closing. */
|
|
1223
|
-
GRACEFUL_MS: 800,
|
|
1224
|
-
/** Ms after which the force disconnect is triggered. */
|
|
1225
|
-
FORCE_TIMEOUT_MS: 1200
|
|
1226
|
-
}),
|
|
1227
|
-
/** Lazy-connect idle lifecycle. */
|
|
1228
|
-
IDLE: Object.freeze({
|
|
1229
|
-
/** Default ms a lazy server stays connected with no tool calls before auto-sleep. */
|
|
1230
|
-
DEFAULT_TIMEOUT_MS: 3e5,
|
|
1231
|
-
/** How often the idle sweep runs (kept well below the timeout). */
|
|
1232
|
-
SWEEP_INTERVAL_MS: 3e4
|
|
1233
|
-
}),
|
|
1234
|
-
/** JSON-RPC response timeout for outstanding requests. */
|
|
1235
|
-
RESPONSE_TIMEOUT_MS: 500,
|
|
1236
|
-
/** Max buffer size for the SSE reader. */
|
|
1237
|
-
SSE_READER_MAX_BUFFER: 256 * 1024,
|
|
1238
|
-
/** Max characters logged from a request body. */
|
|
1239
|
-
REQUEST_LOG_CAP: 1024
|
|
1240
|
-
});
|
|
1241
|
-
|
|
1242
|
-
// src/tool-schema.ts
|
|
1243
|
-
var MAX_TOOL_PAGES = 100;
|
|
1244
|
-
var MAX_TOOLS = 1e4;
|
|
1245
|
-
async function listAllTools(requestPage) {
|
|
1246
|
-
const tools = [];
|
|
1247
|
-
const seenCursors = /* @__PURE__ */ new Set();
|
|
1248
|
-
let cursor;
|
|
1249
|
-
for (let page = 0; page < MAX_TOOL_PAGES; page++) {
|
|
1250
|
-
let response;
|
|
1251
|
-
try {
|
|
1252
|
-
response = await requestPage(cursor ? { cursor } : {});
|
|
1253
|
-
} catch (err) {
|
|
1254
|
-
if (page === 0) throw err;
|
|
1255
|
-
break;
|
|
1256
|
-
}
|
|
1257
|
-
if (response.error) {
|
|
1258
|
-
if (page === 0) return null;
|
|
1259
|
-
break;
|
|
1260
|
-
}
|
|
1261
|
-
const result = response.result;
|
|
1262
|
-
tools.push(...normalizeMCPTools(result?.tools));
|
|
1263
|
-
const next = result?.nextCursor;
|
|
1264
|
-
if (typeof next !== "string" || next.length === 0) break;
|
|
1265
|
-
if (seenCursors.has(next) || tools.length >= MAX_TOOLS) break;
|
|
1266
|
-
seenCursors.add(next);
|
|
1267
|
-
cursor = next;
|
|
1268
|
-
}
|
|
1269
|
-
return tools.slice(0, MAX_TOOLS);
|
|
1270
|
-
}
|
|
1271
|
-
function normalizeMCPTools(value) {
|
|
1272
|
-
if (!Array.isArray(value)) return [];
|
|
1273
|
-
const tools = [];
|
|
1274
|
-
for (const raw of value) {
|
|
1275
|
-
if (!raw || typeof raw !== "object") continue;
|
|
1276
|
-
const t = raw;
|
|
1277
|
-
if (typeof t.name !== "string") continue;
|
|
1278
|
-
const name = t.name.trim();
|
|
1279
|
-
if (name.length === 0) continue;
|
|
1280
|
-
const inputSchema = t.inputSchema && typeof t.inputSchema === "object" && !Array.isArray(t.inputSchema) ? t.inputSchema : { type: "object", properties: {} };
|
|
1281
|
-
if (!t.inputSchema || typeof t.inputSchema !== "object" || Array.isArray(t.inputSchema)) {
|
|
1282
|
-
console.warn(
|
|
1283
|
-
JSON.stringify({
|
|
1284
|
-
level: "warn",
|
|
1285
|
-
event: "mcp.tool_schema_invalid",
|
|
1286
|
-
tool: name,
|
|
1287
|
-
message: "no/invalid inputSchema \u2014 defaulting to empty object",
|
|
1288
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1289
|
-
})
|
|
1290
|
-
);
|
|
1291
|
-
}
|
|
1292
|
-
tools.push({
|
|
1293
|
-
name,
|
|
1294
|
-
...typeof t.description === "string" ? { description: t.description } : {},
|
|
1295
|
-
inputSchema
|
|
1296
|
-
});
|
|
1297
|
-
}
|
|
1298
|
-
return tools;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
1516
|
// src/sse-reader.ts
|
|
1302
1517
|
import { ToolError } from "@wrongstack/core/types";
|
|
1303
1518
|
var SSE_READER_MAX_BUFFER = 256 * 1024;
|
|
@@ -1328,27 +1543,8 @@ var SSEReader = class {
|
|
|
1328
1543
|
};
|
|
1329
1544
|
}
|
|
1330
1545
|
feed(chunk) {
|
|
1331
|
-
|
|
1332
|
-
throw new ToolError({
|
|
1333
|
-
message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
|
|
1334
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
1335
|
-
toolName: "mcp_transport_sse_reader",
|
|
1336
|
-
context: { phase: "feed", chunkLength: chunk.length, maxBuffer: SSE_READER_MAX_BUFFER }
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1546
|
+
const carried = this.buffer.length;
|
|
1339
1547
|
this.buffer += chunk;
|
|
1340
|
-
if (this.buffer.length > SSE_READER_MAX_BUFFER) {
|
|
1341
|
-
throw new ToolError({
|
|
1342
|
-
message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
|
|
1343
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
1344
|
-
toolName: "mcp_transport_sse_reader",
|
|
1345
|
-
context: {
|
|
1346
|
-
phase: "feed",
|
|
1347
|
-
bufferLength: this.buffer.length,
|
|
1348
|
-
maxBuffer: SSE_READER_MAX_BUFFER
|
|
1349
|
-
}
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1352
1548
|
let start = 0;
|
|
1353
1549
|
let idx = this.buffer.indexOf("\n", start);
|
|
1354
1550
|
while (idx !== -1) {
|
|
@@ -1359,6 +1555,25 @@ var SSEReader = class {
|
|
|
1359
1555
|
idx = this.buffer.indexOf("\n", start);
|
|
1360
1556
|
}
|
|
1361
1557
|
if (start > 0) this.buffer = this.buffer.slice(start);
|
|
1558
|
+
if (this.buffer.length <= SSE_READER_MAX_BUFFER) return;
|
|
1559
|
+
if (start >= carried) {
|
|
1560
|
+
throw new ToolError({
|
|
1561
|
+
message: `SSE: chunk size ${chunk.length} exceeds max buffer ${SSE_READER_MAX_BUFFER} \u2014 refusing to accumulate`,
|
|
1562
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1563
|
+
toolName: "mcp_transport_sse_reader",
|
|
1564
|
+
context: { phase: "feed", chunkLength: chunk.length, maxBuffer: SSE_READER_MAX_BUFFER }
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
throw new ToolError({
|
|
1568
|
+
message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
|
|
1569
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
1570
|
+
toolName: "mcp_transport_sse_reader",
|
|
1571
|
+
context: {
|
|
1572
|
+
phase: "feed",
|
|
1573
|
+
bufferLength: this.buffer.length,
|
|
1574
|
+
maxBuffer: SSE_READER_MAX_BUFFER
|
|
1575
|
+
}
|
|
1576
|
+
});
|
|
1362
1577
|
}
|
|
1363
1578
|
processLine(line) {
|
|
1364
1579
|
if (line === "") {
|
|
@@ -1385,53 +1600,434 @@ var SSEReader = class {
|
|
|
1385
1600
|
}
|
|
1386
1601
|
});
|
|
1387
1602
|
}
|
|
1388
|
-
this.dataLines.push(value);
|
|
1603
|
+
this.dataLines.push(value);
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
flush() {
|
|
1607
|
+
const eventName = this.eventName;
|
|
1608
|
+
this.eventName = "";
|
|
1609
|
+
if (this.dataLines.length === 0) {
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
const data = this.dataLines.join("\n").trim();
|
|
1613
|
+
this.dataLines = [];
|
|
1614
|
+
if (!data) return;
|
|
1615
|
+
if (eventName === "endpoint") {
|
|
1616
|
+
for (const cb of this.endpointListeners) {
|
|
1617
|
+
try {
|
|
1618
|
+
cb(data);
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
try {
|
|
1625
|
+
const parsed = JSON.parse(data);
|
|
1626
|
+
this.dispatch(parsed);
|
|
1627
|
+
} catch {
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
dispatch(msg) {
|
|
1631
|
+
for (const cb of this.listeners) {
|
|
1632
|
+
try {
|
|
1633
|
+
cb(msg);
|
|
1634
|
+
} catch {
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
reset() {
|
|
1639
|
+
this.buffer = "";
|
|
1640
|
+
this.dataLines = [];
|
|
1641
|
+
this.eventName = "";
|
|
1642
|
+
this.listeners = [];
|
|
1643
|
+
this.endpointListeners = [];
|
|
1644
|
+
}
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
// src/transport-base.ts
|
|
1648
|
+
import * as https2 from "node:https";
|
|
1649
|
+
import { ConfigError as ConfigError2 } from "@wrongstack/core/types";
|
|
1650
|
+
|
|
1651
|
+
// src/elicitation.ts
|
|
1652
|
+
import { sanitizeTerminalText } from "@wrongstack/core/utils";
|
|
1653
|
+
var ELICITATION_MAX_WAIT_MS = 10 * 6e4;
|
|
1654
|
+
var MAX_FIELDS = 32;
|
|
1655
|
+
var MAX_MESSAGE_CHARS = 4e3;
|
|
1656
|
+
var MAX_OPTIONS = 100;
|
|
1657
|
+
function isRecord(v) {
|
|
1658
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1659
|
+
}
|
|
1660
|
+
function optionalString3(v) {
|
|
1661
|
+
return typeof v === "string" && v.trim() ? v : void 0;
|
|
1662
|
+
}
|
|
1663
|
+
function displayText(v) {
|
|
1664
|
+
return sanitizeTerminalText(v);
|
|
1665
|
+
}
|
|
1666
|
+
function optionalDisplayString(v) {
|
|
1667
|
+
const text = optionalString3(v);
|
|
1668
|
+
return text === void 0 ? void 0 : optionalString3(displayText(text));
|
|
1669
|
+
}
|
|
1670
|
+
function optionalNumber(v) {
|
|
1671
|
+
return typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
1672
|
+
}
|
|
1673
|
+
function optionalCount(v) {
|
|
1674
|
+
return typeof v === "number" && Number.isInteger(v) && v >= 0 ? v : void 0;
|
|
1675
|
+
}
|
|
1676
|
+
function parseOptions(schema) {
|
|
1677
|
+
const values = schema["enum"];
|
|
1678
|
+
if (Array.isArray(values)) {
|
|
1679
|
+
if (!values.every((v) => typeof v === "string")) return void 0;
|
|
1680
|
+
const names = Array.isArray(schema["enumNames"]) ? schema["enumNames"] : [];
|
|
1681
|
+
return values.map((value, i) => ({
|
|
1682
|
+
value,
|
|
1683
|
+
label: displayText(typeof names[i] === "string" && names[i] ? names[i] : value)
|
|
1684
|
+
}));
|
|
1685
|
+
}
|
|
1686
|
+
const titled = Array.isArray(schema["oneOf"]) ? schema["oneOf"] : Array.isArray(schema["anyOf"]) ? schema["anyOf"] : void 0;
|
|
1687
|
+
if (!titled) return void 0;
|
|
1688
|
+
const options = [];
|
|
1689
|
+
for (const entry of titled) {
|
|
1690
|
+
if (!isRecord(entry) || typeof entry["const"] !== "string") return void 0;
|
|
1691
|
+
options.push({
|
|
1692
|
+
value: entry["const"],
|
|
1693
|
+
label: optionalDisplayString(entry["title"]) ?? displayText(entry["const"])
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
return options;
|
|
1697
|
+
}
|
|
1698
|
+
function parseField(name, schema, required) {
|
|
1699
|
+
if (!isRecord(schema)) return { ok: false, error: `property "${name}" is not a schema object` };
|
|
1700
|
+
const base = {
|
|
1701
|
+
name,
|
|
1702
|
+
title: optionalDisplayString(schema["title"]),
|
|
1703
|
+
description: optionalDisplayString(schema["description"]),
|
|
1704
|
+
required
|
|
1705
|
+
};
|
|
1706
|
+
const type = schema["type"];
|
|
1707
|
+
if (type === "array") {
|
|
1708
|
+
const items = schema["items"];
|
|
1709
|
+
const options = isRecord(items) ? parseOptions(items) : void 0;
|
|
1710
|
+
if (!options || options.length === 0 || options.length > MAX_OPTIONS) {
|
|
1711
|
+
return { ok: false, error: `property "${name}": arrays must be a list of enum values` };
|
|
1712
|
+
}
|
|
1713
|
+
const defaults = Array.isArray(schema["default"]) ? schema["default"].filter((v) => typeof v === "string") : void 0;
|
|
1714
|
+
return {
|
|
1715
|
+
ok: true,
|
|
1716
|
+
value: {
|
|
1717
|
+
...base,
|
|
1718
|
+
kind: "multi-enum",
|
|
1719
|
+
options,
|
|
1720
|
+
minItems: optionalCount(schema["minItems"]),
|
|
1721
|
+
maxItems: optionalCount(schema["maxItems"]),
|
|
1722
|
+
default: defaults
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
if (type === "string") {
|
|
1727
|
+
const options = parseOptions(schema);
|
|
1728
|
+
if (options) {
|
|
1729
|
+
if (options.length === 0 || options.length > MAX_OPTIONS) {
|
|
1730
|
+
return { ok: false, error: `property "${name}": enum needs 1-${MAX_OPTIONS} values` };
|
|
1731
|
+
}
|
|
1732
|
+
return {
|
|
1733
|
+
ok: true,
|
|
1734
|
+
value: { ...base, kind: "enum", options, default: optionalString3(schema["default"]) }
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
return {
|
|
1738
|
+
ok: true,
|
|
1739
|
+
value: {
|
|
1740
|
+
...base,
|
|
1741
|
+
kind: "string",
|
|
1742
|
+
minLength: optionalCount(schema["minLength"]),
|
|
1743
|
+
maxLength: optionalCount(schema["maxLength"]),
|
|
1744
|
+
format: optionalString3(schema["format"]),
|
|
1745
|
+
default: typeof schema["default"] === "string" ? schema["default"] : void 0
|
|
1746
|
+
}
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
if (type === "number" || type === "integer") {
|
|
1750
|
+
return {
|
|
1751
|
+
ok: true,
|
|
1752
|
+
value: {
|
|
1753
|
+
...base,
|
|
1754
|
+
kind: type,
|
|
1755
|
+
minimum: optionalNumber(schema["minimum"]),
|
|
1756
|
+
maximum: optionalNumber(schema["maximum"]),
|
|
1757
|
+
default: optionalNumber(schema["default"])
|
|
1758
|
+
}
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
if (type === "boolean") {
|
|
1762
|
+
return {
|
|
1763
|
+
ok: true,
|
|
1764
|
+
value: {
|
|
1765
|
+
...base,
|
|
1766
|
+
kind: "boolean",
|
|
1767
|
+
default: typeof schema["default"] === "boolean" ? schema["default"] : void 0
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
return {
|
|
1772
|
+
ok: false,
|
|
1773
|
+
error: `property "${name}" has unsupported type ${JSON.stringify(type)} \u2014 only flat primitive fields can be elicited`
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
function parseUrlElicitation(params) {
|
|
1777
|
+
if (!isRecord(params)) return { ok: false, error: "params must be an object" };
|
|
1778
|
+
const message = params["message"];
|
|
1779
|
+
if (typeof message !== "string" || !message.trim()) {
|
|
1780
|
+
return { ok: false, error: "message is required" };
|
|
1781
|
+
}
|
|
1782
|
+
const elicitationId = params["elicitationId"];
|
|
1783
|
+
if (typeof elicitationId !== "string" || !elicitationId) {
|
|
1784
|
+
return { ok: false, error: "elicitationId is required in URL mode" };
|
|
1785
|
+
}
|
|
1786
|
+
const raw = params["url"];
|
|
1787
|
+
let url;
|
|
1788
|
+
try {
|
|
1789
|
+
url = new URL(typeof raw === "string" ? raw : "");
|
|
1790
|
+
} catch {
|
|
1791
|
+
return { ok: false, error: "url must be a valid URL" };
|
|
1792
|
+
}
|
|
1793
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
1794
|
+
return { ok: false, error: `url must be http(s), not ${url.protocol}` };
|
|
1795
|
+
}
|
|
1796
|
+
return {
|
|
1797
|
+
ok: true,
|
|
1798
|
+
value: {
|
|
1799
|
+
mode: "url",
|
|
1800
|
+
message: displayText(message).slice(0, MAX_MESSAGE_CHARS),
|
|
1801
|
+
url: url.href,
|
|
1802
|
+
elicitationId
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
function parseElicitationParams(params) {
|
|
1807
|
+
if (!isRecord(params)) return { ok: false, error: "params must be an object" };
|
|
1808
|
+
const message = params["message"];
|
|
1809
|
+
if (typeof message !== "string" || !message.trim()) {
|
|
1810
|
+
return { ok: false, error: "message is required" };
|
|
1811
|
+
}
|
|
1812
|
+
if (params["mode"] === "url") return parseUrlElicitation(params);
|
|
1813
|
+
if (params["mode"] !== void 0 && params["mode"] !== "form") {
|
|
1814
|
+
return { ok: false, error: `unsupported mode ${JSON.stringify(params["mode"])}` };
|
|
1815
|
+
}
|
|
1816
|
+
const schema = params["requestedSchema"];
|
|
1817
|
+
if (!isRecord(schema) || schema["type"] !== "object" || !isRecord(schema["properties"])) {
|
|
1818
|
+
return { ok: false, error: "requestedSchema must be an object schema with properties" };
|
|
1819
|
+
}
|
|
1820
|
+
const required = new Set(
|
|
1821
|
+
Array.isArray(schema["required"]) ? schema["required"].filter((v) => typeof v === "string") : []
|
|
1822
|
+
);
|
|
1823
|
+
const entries = Object.entries(schema["properties"]);
|
|
1824
|
+
if (entries.length > MAX_FIELDS) {
|
|
1825
|
+
return { ok: false, error: `requestedSchema has more than ${MAX_FIELDS} properties` };
|
|
1826
|
+
}
|
|
1827
|
+
const fields = [];
|
|
1828
|
+
for (const [name, property] of entries) {
|
|
1829
|
+
const parsed = parseField(name, property, required.has(name));
|
|
1830
|
+
if (!parsed.ok) return parsed;
|
|
1831
|
+
fields.push(parsed.value);
|
|
1832
|
+
}
|
|
1833
|
+
return { ok: true, value: { message: displayText(message).slice(0, MAX_MESSAGE_CHARS), fields } };
|
|
1834
|
+
}
|
|
1835
|
+
function label(field) {
|
|
1836
|
+
return field.title ?? displayText(field.name);
|
|
1837
|
+
}
|
|
1838
|
+
function checkValue(field, raw) {
|
|
1839
|
+
const name = label(field);
|
|
1840
|
+
switch (field.kind) {
|
|
1841
|
+
case "string": {
|
|
1842
|
+
if (typeof raw !== "string") return { ok: false, error: `${name} must be text` };
|
|
1843
|
+
if (field.minLength !== void 0 && raw.length < field.minLength) {
|
|
1844
|
+
return { ok: false, error: `${name} needs at least ${field.minLength} characters` };
|
|
1845
|
+
}
|
|
1846
|
+
if (field.maxLength !== void 0 && raw.length > field.maxLength) {
|
|
1847
|
+
return { ok: false, error: `${name} allows at most ${field.maxLength} characters` };
|
|
1848
|
+
}
|
|
1849
|
+
return { ok: true, value: raw };
|
|
1850
|
+
}
|
|
1851
|
+
case "number":
|
|
1852
|
+
case "integer": {
|
|
1853
|
+
const n = typeof raw === "string" && raw.trim() ? Number(raw.trim()) : raw;
|
|
1854
|
+
if (typeof n !== "number" || !Number.isFinite(n)) {
|
|
1855
|
+
return { ok: false, error: `${name} must be a number` };
|
|
1856
|
+
}
|
|
1857
|
+
if (field.kind === "integer" && !Number.isInteger(n)) {
|
|
1858
|
+
return { ok: false, error: `${name} must be a whole number` };
|
|
1859
|
+
}
|
|
1860
|
+
if (field.minimum !== void 0 && n < field.minimum) {
|
|
1861
|
+
return { ok: false, error: `${name} must be at least ${field.minimum}` };
|
|
1862
|
+
}
|
|
1863
|
+
if (field.maximum !== void 0 && n > field.maximum) {
|
|
1864
|
+
return { ok: false, error: `${name} must be at most ${field.maximum}` };
|
|
1865
|
+
}
|
|
1866
|
+
return { ok: true, value: n };
|
|
1867
|
+
}
|
|
1868
|
+
case "boolean":
|
|
1869
|
+
return typeof raw === "boolean" ? { ok: true, value: raw } : { ok: false, error: `${name} must be yes or no` };
|
|
1870
|
+
case "enum":
|
|
1871
|
+
return typeof raw === "string" && field.options.some((o) => o.value === raw) ? { ok: true, value: raw } : { ok: false, error: `${name} must be one of the offered choices` };
|
|
1872
|
+
case "multi-enum": {
|
|
1873
|
+
if (!Array.isArray(raw) || !raw.every((v) => field.options.some((o) => o.value === v))) {
|
|
1874
|
+
return { ok: false, error: `${name} must be picked from the offered choices` };
|
|
1875
|
+
}
|
|
1876
|
+
const picked = [...new Set(raw)];
|
|
1877
|
+
if (field.minItems !== void 0 && picked.length < field.minItems) {
|
|
1878
|
+
return { ok: false, error: `${name} needs at least ${field.minItems} choices` };
|
|
1879
|
+
}
|
|
1880
|
+
if (field.maxItems !== void 0 && picked.length > field.maxItems) {
|
|
1881
|
+
return { ok: false, error: `${name} allows at most ${field.maxItems} choices` };
|
|
1882
|
+
}
|
|
1883
|
+
return { ok: true, value: picked };
|
|
1389
1884
|
}
|
|
1390
1885
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1886
|
+
}
|
|
1887
|
+
function validateElicitationContent(fields, content) {
|
|
1888
|
+
const out = {};
|
|
1889
|
+
for (const field of fields) {
|
|
1890
|
+
const raw = content[field.name];
|
|
1891
|
+
if (Array.isArray(raw) && raw.length === 0 && field.kind === "multi-enum" && field.minItems === 0) {
|
|
1892
|
+
out[field.name] = [];
|
|
1893
|
+
continue;
|
|
1396
1894
|
}
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1895
|
+
if (raw === void 0 || raw === null || raw === "" || Array.isArray(raw) && raw.length === 0) {
|
|
1896
|
+
if (field.required) return { ok: false, error: `${label(field)} is required` };
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
const checked = checkValue(field, raw);
|
|
1900
|
+
if (!checked.ok) return checked;
|
|
1901
|
+
out[field.name] = checked.value;
|
|
1902
|
+
}
|
|
1903
|
+
return { ok: true, value: out };
|
|
1904
|
+
}
|
|
1905
|
+
var ServerRequestResponder = class {
|
|
1906
|
+
constructor(elicitation) {
|
|
1907
|
+
this.elicitation = elicitation;
|
|
1908
|
+
}
|
|
1909
|
+
elicitation;
|
|
1910
|
+
pending = /* @__PURE__ */ new Map();
|
|
1911
|
+
/** Client capabilities to declare in `initialize` — only what can be answered. */
|
|
1912
|
+
capabilities() {
|
|
1913
|
+
return this.elicitation ? { elicitation: { form: {}, url: {} } } : {};
|
|
1914
|
+
}
|
|
1915
|
+
/** True while an elicitation waits on the user; request timeouts hold meanwhile. */
|
|
1916
|
+
get awaitingUser() {
|
|
1917
|
+
return this.pending.size > 0;
|
|
1918
|
+
}
|
|
1919
|
+
async answer(request3) {
|
|
1920
|
+
const { id, method } = request3;
|
|
1921
|
+
const error = (code, message) => ({
|
|
1922
|
+
jsonrpc: "2.0",
|
|
1923
|
+
id,
|
|
1924
|
+
error: { code, message }
|
|
1925
|
+
});
|
|
1926
|
+
if (method === "ping") return { jsonrpc: "2.0", id, result: {} };
|
|
1927
|
+
if (method === "sampling/createMessage") {
|
|
1928
|
+
return error(-32601, "Client sampling is disabled by policy");
|
|
1929
|
+
}
|
|
1930
|
+
if (method !== "elicitation/create" || !this.elicitation) {
|
|
1931
|
+
return error(-32601, `Method not found: ${method}`);
|
|
1408
1932
|
}
|
|
1933
|
+
const form = parseElicitationParams(request3.params);
|
|
1934
|
+
if (!form.ok) return error(-32602, `Invalid elicitation request: ${form.error}`);
|
|
1935
|
+
if (this.pending.size > 0) {
|
|
1936
|
+
return error(-32603, "Another elicitation from this server is still waiting on the user");
|
|
1937
|
+
}
|
|
1938
|
+
const controller = new AbortController();
|
|
1939
|
+
this.pending.set(id, controller);
|
|
1940
|
+
const deadline = setTimeout(
|
|
1941
|
+
() => controller.abort(new Error("elicitation timed out")),
|
|
1942
|
+
ELICITATION_MAX_WAIT_MS
|
|
1943
|
+
);
|
|
1944
|
+
deadline.unref?.();
|
|
1409
1945
|
try {
|
|
1410
|
-
const
|
|
1411
|
-
this.
|
|
1946
|
+
const prompt = form.value;
|
|
1947
|
+
const result = await this.elicitation({ ...prompt, signal: controller.signal });
|
|
1948
|
+
if (controller.signal.aborted) return { jsonrpc: "2.0", id, result: { action: "cancel" } };
|
|
1949
|
+
if (result.action !== "accept")
|
|
1950
|
+
return { jsonrpc: "2.0", id, result: { action: result.action } };
|
|
1951
|
+
if (prompt.mode === "url") return { jsonrpc: "2.0", id, result: { action: "accept" } };
|
|
1952
|
+
const content = validateElicitationContent(prompt.fields, result.content);
|
|
1953
|
+
if (!content.ok) return error(-32603, `Elicitation answer rejected: ${content.error}`);
|
|
1954
|
+
return { jsonrpc: "2.0", id, result: { action: "accept", content: content.value } };
|
|
1412
1955
|
} catch {
|
|
1956
|
+
return { jsonrpc: "2.0", id, result: { action: "cancel" } };
|
|
1957
|
+
} finally {
|
|
1958
|
+
clearTimeout(deadline);
|
|
1959
|
+
this.pending.delete(id);
|
|
1413
1960
|
}
|
|
1414
1961
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1962
|
+
/**
|
|
1963
|
+
* Pages a tool call's `-32042` (URL elicitation required) error names, put
|
|
1964
|
+
* to the user one at a time. Returns what the user chose for each; nothing
|
|
1965
|
+
* is opened without their consent, and without a handler every one is
|
|
1966
|
+
* `cancel`.
|
|
1967
|
+
*/
|
|
1968
|
+
async presentUrl(elicitations, signal) {
|
|
1969
|
+
const outcomes = [];
|
|
1970
|
+
for (const elicitation of elicitations) {
|
|
1971
|
+
let action = "cancel";
|
|
1972
|
+
if (this.elicitation && !signal.aborted) {
|
|
1973
|
+
try {
|
|
1974
|
+
action = (await this.elicitation({ ...elicitation, signal })).action;
|
|
1975
|
+
} catch {
|
|
1976
|
+
action = "cancel";
|
|
1977
|
+
}
|
|
1420
1978
|
}
|
|
1979
|
+
outcomes.push({ elicitation, action });
|
|
1421
1980
|
}
|
|
1981
|
+
return outcomes;
|
|
1422
1982
|
}
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
this.
|
|
1983
|
+
/** `notifications/cancelled` from the server: stop waiting for that answer. */
|
|
1984
|
+
cancel(params) {
|
|
1985
|
+
if (!isRecord(params)) return;
|
|
1986
|
+
const requestId = params["requestId"];
|
|
1987
|
+
if (typeof requestId !== "number" && typeof requestId !== "string") return;
|
|
1988
|
+
this.pending.get(requestId)?.abort(new Error("cancelled by server"));
|
|
1989
|
+
}
|
|
1990
|
+
/** The connection is gone: abandon every open form. */
|
|
1991
|
+
dispose() {
|
|
1992
|
+
for (const controller of this.pending.values())
|
|
1993
|
+
controller.abort(new Error("connection closed"));
|
|
1994
|
+
this.pending.clear();
|
|
1429
1995
|
}
|
|
1430
1996
|
};
|
|
1431
1997
|
|
|
1432
|
-
// src/
|
|
1433
|
-
import
|
|
1434
|
-
|
|
1998
|
+
// src/read-body.ts
|
|
1999
|
+
import { ToolError as ToolError2 } from "@wrongstack/core/types";
|
|
2000
|
+
var MAX_MCP_HTTP_BODY_BYTES = 16 * 1024 * 1024;
|
|
2001
|
+
async function readBodyCapped(res, maxBytes = MAX_MCP_HTTP_BODY_BYTES) {
|
|
2002
|
+
const body = res.body;
|
|
2003
|
+
if (!body || typeof body.getReader !== "function") {
|
|
2004
|
+
return await res.text();
|
|
2005
|
+
}
|
|
2006
|
+
const reader = body.getReader();
|
|
2007
|
+
const chunks = [];
|
|
2008
|
+
let total = 0;
|
|
2009
|
+
try {
|
|
2010
|
+
for (; ; ) {
|
|
2011
|
+
const { done, value } = await reader.read();
|
|
2012
|
+
if (done) break;
|
|
2013
|
+
if (!value) continue;
|
|
2014
|
+
total += value.byteLength;
|
|
2015
|
+
if (total > maxBytes) {
|
|
2016
|
+
await reader.cancel().catch(() => void 0);
|
|
2017
|
+
throw new ToolError2({
|
|
2018
|
+
message: `MCP response body exceeded ${maxBytes} bytes \u2014 refusing to buffer`,
|
|
2019
|
+
code: "TOOL_EXECUTION_FAILED",
|
|
2020
|
+
toolName: "mcp_transport",
|
|
2021
|
+
context: { maxBytes, received: total }
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
chunks.push(value);
|
|
2025
|
+
}
|
|
2026
|
+
} finally {
|
|
2027
|
+
reader.releaseLock?.();
|
|
2028
|
+
}
|
|
2029
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
2030
|
+
}
|
|
1435
2031
|
|
|
1436
2032
|
// src/transport-security.ts
|
|
1437
2033
|
import * as dns2 from "node:dns/promises";
|
|
@@ -1459,16 +2055,16 @@ function validateTransportUrl(rawUrl) {
|
|
|
1459
2055
|
context: { field: "url", rawUrl, protocol: url.protocol }
|
|
1460
2056
|
});
|
|
1461
2057
|
}
|
|
1462
|
-
const
|
|
1463
|
-
const host =
|
|
2058
|
+
const hostname2 = url.hostname;
|
|
2059
|
+
const host = hostname2.startsWith("[") && hostname2.endsWith("]") ? hostname2.slice(1, -1) : hostname2;
|
|
1464
2060
|
const ipVersion = net2.isIP(host);
|
|
1465
2061
|
if (ipVersion !== 0) {
|
|
1466
2062
|
const classification = classifyTransportAddress(host, ipVersion);
|
|
1467
2063
|
if (classification === "blocked") {
|
|
1468
2064
|
throw new ConfigError({
|
|
1469
|
-
message: `MCP transport: blocked link-local/IMDS address "${
|
|
2065
|
+
message: `MCP transport: blocked link-local/IMDS address "${hostname2}" \u2014 likely not a valid MCP server`,
|
|
1470
2066
|
code: "CONFIG_INVALID",
|
|
1471
|
-
context: { field: "url", rawUrl, hostname }
|
|
2067
|
+
context: { field: "url", rawUrl, hostname: hostname2 }
|
|
1472
2068
|
});
|
|
1473
2069
|
}
|
|
1474
2070
|
}
|
|
@@ -1476,9 +2072,9 @@ function validateTransportUrl(rawUrl) {
|
|
|
1476
2072
|
const isLoopback = host === "localhost" || ipVersion !== 0 && classifyTransportAddress(host, ipVersion) === "loopback";
|
|
1477
2073
|
if (!isLoopback) {
|
|
1478
2074
|
throw new ConfigError({
|
|
1479
|
-
message: `MCP transport: http:// is only allowed for loopback addresses; use https:// for "${
|
|
2075
|
+
message: `MCP transport: http:// is only allowed for loopback addresses; use https:// for "${hostname2}"`,
|
|
1480
2076
|
code: "CONFIG_INVALID",
|
|
1481
|
-
context: { field: "url", rawUrl, hostname, protocol: url.protocol }
|
|
2077
|
+
context: { field: "url", rawUrl, hostname: hostname2, protocol: url.protocol }
|
|
1482
2078
|
});
|
|
1483
2079
|
}
|
|
1484
2080
|
}
|
|
@@ -1489,6 +2085,11 @@ if (ALLOW_MCP_PRIVATE_NETWORKS && !process.env["CI"]) {
|
|
|
1489
2085
|
"[WrongStack] WARNING: WRONGSTACK_MCP_ALLOW_PRIVATE=1 is active \u2014\n MCP HTTP transports may dial private/LAN addresses (10.x, 192.168.x,\n 172.16-31.x, ULA) when a server config sets allowPrivateNetworks. Link-local/IMDS targets stay blocked."
|
|
1490
2086
|
);
|
|
1491
2087
|
}
|
|
2088
|
+
function isImdsIpv6Literal(v6) {
|
|
2089
|
+
const groups = expandIPv6(v6);
|
|
2090
|
+
if (groups === null) return false;
|
|
2091
|
+
return groups[0] === 64768 && groups[1] === 3778 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 0 && groups[6] === 0 && groups[7] === 596;
|
|
2092
|
+
}
|
|
1492
2093
|
function classifyTransportAddress(address, family) {
|
|
1493
2094
|
if (family === 4) {
|
|
1494
2095
|
const v4 = address.toLowerCase();
|
|
@@ -1499,7 +2100,7 @@ function classifyTransportAddress(address, family) {
|
|
|
1499
2100
|
if (family === 6) {
|
|
1500
2101
|
const v6 = address.toLowerCase();
|
|
1501
2102
|
if (v6 === "::1") return "loopback";
|
|
1502
|
-
if (/^fe[89ab]/.test(v6) || v6
|
|
2103
|
+
if (/^fe[89ab]/.test(v6) || isImdsIpv6Literal(v6)) return "blocked";
|
|
1503
2104
|
if (v6.startsWith("::ffff:")) {
|
|
1504
2105
|
const tail = v6.slice("::ffff:".length);
|
|
1505
2106
|
if (tail.includes(".")) return classifyTransportAddress(tail, 4);
|
|
@@ -1525,30 +2126,30 @@ function classifyTransportAddress(address, family) {
|
|
|
1525
2126
|
}
|
|
1526
2127
|
return "blocked";
|
|
1527
2128
|
}
|
|
1528
|
-
function assertTransportAddressAllowed(address, family,
|
|
2129
|
+
function assertTransportAddressAllowed(address, family, hostname2, allowPrivateNetworks) {
|
|
1529
2130
|
const classification = classifyTransportAddress(address, family);
|
|
1530
2131
|
if (classification === "blocked") {
|
|
1531
2132
|
throw new ConfigError({
|
|
1532
|
-
message: `MCP transport: resolved address "${address}" for "${
|
|
2133
|
+
message: `MCP transport: resolved address "${address}" for "${hostname2}" is link-local/IMDS \u2014 never a valid MCP target`,
|
|
1533
2134
|
code: "CONFIG_INVALID",
|
|
1534
|
-
context: { hostname, address }
|
|
2135
|
+
context: { hostname: hostname2, address }
|
|
1535
2136
|
});
|
|
1536
2137
|
}
|
|
1537
2138
|
if (classification === "private" && !allowPrivateNetworks) {
|
|
1538
2139
|
throw new ConfigError({
|
|
1539
|
-
message: `MCP transport: "${
|
|
2140
|
+
message: `MCP transport: "${hostname2}" resolved to private address ${address}. Private/LAN targets are blocked by default; if this server really runs on your private network, set allowPrivateNetworks: true on its config (or WRONGSTACK_MCP_ALLOW_PRIVATE=1 globally).`,
|
|
1540
2141
|
code: "CONFIG_INVALID",
|
|
1541
|
-
context: { hostname, address }
|
|
2142
|
+
context: { hostname: hostname2, address }
|
|
1542
2143
|
});
|
|
1543
2144
|
}
|
|
1544
2145
|
}
|
|
1545
2146
|
function transportPinnedLookup(options) {
|
|
1546
2147
|
const lookup3 = options.lookup ?? (async (host) => dns2.lookup(host, { all: true }));
|
|
1547
|
-
return (
|
|
1548
|
-
lookup3(
|
|
2148
|
+
return (hostname2, connectOptions, callback) => {
|
|
2149
|
+
lookup3(hostname2).then((records) => {
|
|
1549
2150
|
if (records.length === 0) {
|
|
1550
2151
|
callback(
|
|
1551
|
-
Object.assign(new Error(`MCP transport: no addresses for "${
|
|
2152
|
+
Object.assign(new Error(`MCP transport: no addresses for "${hostname2}"`), {
|
|
1552
2153
|
code: "ENOTFOUND"
|
|
1553
2154
|
})
|
|
1554
2155
|
);
|
|
@@ -1558,7 +2159,7 @@ function transportPinnedLookup(options) {
|
|
|
1558
2159
|
assertTransportAddressAllowed(
|
|
1559
2160
|
record3.address,
|
|
1560
2161
|
record3.family,
|
|
1561
|
-
|
|
2162
|
+
hostname2,
|
|
1562
2163
|
options.allowPrivateNetworks
|
|
1563
2164
|
);
|
|
1564
2165
|
}
|
|
@@ -1598,6 +2199,14 @@ if (!pinnedAgentsCleanupRegistered) {
|
|
|
1598
2199
|
pinnedAgents.clear();
|
|
1599
2200
|
});
|
|
1600
2201
|
}
|
|
2202
|
+
function urlOriginForLog(raw) {
|
|
2203
|
+
try {
|
|
2204
|
+
const url = new URL(raw);
|
|
2205
|
+
return `${url.protocol}//${url.host}`;
|
|
2206
|
+
} catch {
|
|
2207
|
+
return "<invalid url>";
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
1601
2210
|
function makeAbortError(method) {
|
|
1602
2211
|
const err = new Error(`MCP request "${method}" aborted by client`);
|
|
1603
2212
|
err.name = "AbortError";
|
|
@@ -1606,7 +2215,7 @@ function makeAbortError(method) {
|
|
|
1606
2215
|
function nextJsonRpcId(id) {
|
|
1607
2216
|
return id >= Number.MAX_SAFE_INTEGER ? 1 : id + 1;
|
|
1608
2217
|
}
|
|
1609
|
-
function createTimeoutSignal(parent, timeoutMs) {
|
|
2218
|
+
function createTimeoutSignal(parent, timeoutMs, hold) {
|
|
1610
2219
|
const ctrl = new AbortController();
|
|
1611
2220
|
const onAbort = () => ctrl.abort(parent?.reason);
|
|
1612
2221
|
if (parent?.aborted) {
|
|
@@ -1614,10 +2223,15 @@ function createTimeoutSignal(parent, timeoutMs) {
|
|
|
1614
2223
|
} else {
|
|
1615
2224
|
parent?.addEventListener("abort", onAbort, { once: true });
|
|
1616
2225
|
}
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
2226
|
+
const onTimeout = () => {
|
|
2227
|
+
if (hold?.()) {
|
|
2228
|
+
timer = setTimeout(onTimeout, timeoutMs);
|
|
2229
|
+
timer.unref?.();
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
ctrl.abort(new Error(`MCP HTTP request timed out after ${timeoutMs}ms`));
|
|
2233
|
+
};
|
|
2234
|
+
let timer = setTimeout(onTimeout, timeoutMs);
|
|
1621
2235
|
timer.unref?.();
|
|
1622
2236
|
return {
|
|
1623
2237
|
signal: ctrl.signal,
|
|
@@ -1648,9 +2262,12 @@ var BaseHTTPTransport = class {
|
|
|
1648
2262
|
disconnectHandlers = [];
|
|
1649
2263
|
toolsChangedListeners = /* @__PURE__ */ new Set();
|
|
1650
2264
|
resourcesChangedListeners = /* @__PURE__ */ new Set();
|
|
2265
|
+
resourceUpdatedListeners = /* @__PURE__ */ new Set();
|
|
1651
2266
|
promptsChangedListeners = /* @__PURE__ */ new Set();
|
|
1652
2267
|
protocolVersion;
|
|
2268
|
+
serverRequests;
|
|
1653
2269
|
constructor(opts, transportName) {
|
|
2270
|
+
this.serverRequests = opts.serverRequests ?? new ServerRequestResponder();
|
|
1654
2271
|
validateTransportUrl(opts.url);
|
|
1655
2272
|
this.name = opts.name;
|
|
1656
2273
|
this.url = opts.url;
|
|
@@ -1661,15 +2278,16 @@ var BaseHTTPTransport = class {
|
|
|
1661
2278
|
this.requestTimeout = opts.requestTimeoutMs ?? 6e4;
|
|
1662
2279
|
if (opts.tls) {
|
|
1663
2280
|
if (opts.tls.rejectUnauthorized === false) {
|
|
2281
|
+
const where = urlOriginForLog(this.url);
|
|
1664
2282
|
if (!isTlsUnsafeAllowed()) {
|
|
1665
2283
|
throw new ConfigError2({
|
|
1666
|
-
message: `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${
|
|
2284
|
+
message: `[mcp:${transportName}] TLS verification disabled \u2014 set WRONGSTACK_UNSAFE_MCP_TLS=1 to allow. Rejecting insecure configuration for ${where}.`,
|
|
1667
2285
|
code: "CONFIG_INVALID",
|
|
1668
|
-
context: { field: "tls.rejectUnauthorized", transportName, url:
|
|
2286
|
+
context: { field: "tls.rejectUnauthorized", transportName, url: where }
|
|
1669
2287
|
});
|
|
1670
2288
|
}
|
|
1671
2289
|
console.error(
|
|
1672
|
-
`[mcp:${transportName}] \u26A0\uFE0F TLS verification DISABLED for ${
|
|
2290
|
+
`[mcp:${transportName}] \u26A0\uFE0F TLS verification DISABLED for ${where}. Network attacks are possible \u2014 only use on localhost.`
|
|
1673
2291
|
);
|
|
1674
2292
|
}
|
|
1675
2293
|
this.tlsAgent = new https2.Agent({
|
|
@@ -1774,6 +2392,10 @@ var BaseHTTPTransport = class {
|
|
|
1774
2392
|
this.promptsChangedListeners.add(cb);
|
|
1775
2393
|
return () => this.promptsChangedListeners.delete(cb);
|
|
1776
2394
|
}
|
|
2395
|
+
onResourceUpdated(cb) {
|
|
2396
|
+
this.resourceUpdatedListeners.add(cb);
|
|
2397
|
+
return () => this.resourceUpdatedListeners.delete(cb);
|
|
2398
|
+
}
|
|
1777
2399
|
/**
|
|
1778
2400
|
* Fire all disconnect handlers. Subclasses call this when the connection
|
|
1779
2401
|
* drops so the registry can schedule reconnects.
|
|
@@ -1802,6 +2424,14 @@ var BaseHTTPTransport = class {
|
|
|
1802
2424
|
}
|
|
1803
2425
|
}
|
|
1804
2426
|
}
|
|
2427
|
+
notifyResourceUpdated(uri) {
|
|
2428
|
+
for (const cb of this.resourceUpdatedListeners) {
|
|
2429
|
+
try {
|
|
2430
|
+
cb(uri);
|
|
2431
|
+
} catch {
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
1805
2435
|
async dispatcherFetch() {
|
|
1806
2436
|
if (globalThis.fetch !== nativeGlobalFetch) return globalThis.fetch;
|
|
1807
2437
|
const undici = await ensureUndici();
|
|
@@ -1853,10 +2483,40 @@ var BaseHTTPTransport = class {
|
|
|
1853
2483
|
fetchOpts.dispatcher = this.tlsAgent;
|
|
1854
2484
|
}
|
|
1855
2485
|
}
|
|
2486
|
+
/**
|
|
2487
|
+
* Request timeout for a call. It holds while the server waits on the user's
|
|
2488
|
+
* elicitation answer — that wait is the user typing, not the server stalling.
|
|
2489
|
+
*/
|
|
2490
|
+
requestTimeoutSignal(parent, timeoutMs) {
|
|
2491
|
+
return createTimeoutSignal(parent, timeoutMs, () => this.serverRequests.awaitingUser);
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Answer a server→client request. Over HTTP the reply is POSTed back like
|
|
2495
|
+
* any client message; the server acknowledges it with 202. A failed reply is
|
|
2496
|
+
* not retried — the server times its request out on its side.
|
|
2497
|
+
*/
|
|
2498
|
+
async replyToServer(request3, url, headers) {
|
|
2499
|
+
const response = await this.serverRequests.answer(request3);
|
|
2500
|
+
const timeoutSignal = createTimeoutSignal(this.abortController?.signal, this.requestTimeout);
|
|
2501
|
+
const init = {
|
|
2502
|
+
method: "POST",
|
|
2503
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
2504
|
+
body: JSON.stringify(response),
|
|
2505
|
+
signal: timeoutSignal.signal
|
|
2506
|
+
};
|
|
2507
|
+
this.applyTlsAgent(init);
|
|
2508
|
+
try {
|
|
2509
|
+
const res = await this.fetchWithAuthorization(url, init, timeoutSignal.signal);
|
|
2510
|
+
await readBodyCapped(res).catch(() => void 0);
|
|
2511
|
+
} catch {
|
|
2512
|
+
} finally {
|
|
2513
|
+
timeoutSignal.dispose();
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
1856
2516
|
};
|
|
1857
2517
|
|
|
1858
2518
|
// src/transport-jsonrpc.ts
|
|
1859
|
-
import { ToolError as
|
|
2519
|
+
import { ToolError as ToolError3 } from "@wrongstack/core/types";
|
|
1860
2520
|
function encodeJsonRpcMessage(id, method, params) {
|
|
1861
2521
|
return JSON.stringify(
|
|
1862
2522
|
method.startsWith("notifications/") ? { jsonrpc: "2.0", method, params } : { jsonrpc: "2.0", id, method, params }
|
|
@@ -1930,7 +2590,7 @@ function extractJsonRpcResults(text) {
|
|
|
1930
2590
|
}
|
|
1931
2591
|
function assertMatchingJsonRpcResult(data, expectedId, method) {
|
|
1932
2592
|
if (!isJsonRpcResult(data)) {
|
|
1933
|
-
throw new
|
|
2593
|
+
throw new ToolError3({
|
|
1934
2594
|
message: "Invalid JSON-RPC response: not a JSON-RPC 2.0 envelope",
|
|
1935
2595
|
code: "TOOL_EXECUTION_FAILED",
|
|
1936
2596
|
toolName: "mcp_transport_jsonrpc",
|
|
@@ -1938,7 +2598,7 @@ function assertMatchingJsonRpcResult(data, expectedId, method) {
|
|
|
1938
2598
|
});
|
|
1939
2599
|
}
|
|
1940
2600
|
if (data.id !== expectedId) {
|
|
1941
|
-
throw new
|
|
2601
|
+
throw new ToolError3({
|
|
1942
2602
|
message: `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`,
|
|
1943
2603
|
code: "TOOL_EXECUTION_FAILED",
|
|
1944
2604
|
toolName: "mcp_transport_jsonrpc",
|
|
@@ -1952,38 +2612,147 @@ function assertMatchingJsonRpcResult(data, expectedId, method) {
|
|
|
1952
2612
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1953
2613
|
import { ToolError as ToolError4 } from "@wrongstack/core/types";
|
|
1954
2614
|
|
|
1955
|
-
// src/
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
if (!body || typeof body.getReader !== "function") {
|
|
1961
|
-
return await res.text();
|
|
2615
|
+
// src/constants.ts
|
|
2616
|
+
var SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2024-11-05"]);
|
|
2617
|
+
function negotiateProtocolVersion(requested) {
|
|
2618
|
+
if (typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)) {
|
|
2619
|
+
return requested;
|
|
1962
2620
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2621
|
+
return MCP_CONSTANTS.PROTOCOL_VERSION;
|
|
2622
|
+
}
|
|
2623
|
+
var MCP_CONSTANTS = Object.freeze({
|
|
2624
|
+
/** MCP protocol version advertised during handshake: the newest we implement. */
|
|
2625
|
+
PROTOCOL_VERSION: SUPPORTED_PROTOCOL_VERSIONS[0],
|
|
2626
|
+
/** Identity announced to MCP servers during `initialize`. */
|
|
2627
|
+
CLIENT_INFO: Object.freeze({
|
|
2628
|
+
name: "wrongstack",
|
|
2629
|
+
version: "0.1.10"
|
|
2630
|
+
}),
|
|
2631
|
+
/** Reconnection behaviour when a transport disconnects. */
|
|
2632
|
+
RECONNECT: Object.freeze({
|
|
2633
|
+
/** Max full reconnect cycles before the slot is marked `failed`. */
|
|
2634
|
+
MAX_CYCLES: 5,
|
|
2635
|
+
/** Base delay between cycles (exponential backoff applied on top). */
|
|
2636
|
+
BASE_DELAY_MS: 1e3,
|
|
2637
|
+
/** Jitter factor applied to the backoff (0 = no jitter, 1 = full). */
|
|
2638
|
+
JITTER_FACTOR: 0.2,
|
|
2639
|
+
/** Max connection attempts within a single cycle. */
|
|
2640
|
+
MAX_ATTEMPTS: 3,
|
|
2641
|
+
/** Base multiplier for the exponential backoff formula (`delay = BASE * multiplier^attempt`). */
|
|
2642
|
+
BACKOFF_MULTIPLIER: 2
|
|
2643
|
+
}),
|
|
2644
|
+
/** Timing for graceful / forced disconnect. */
|
|
2645
|
+
DISCONNECT: Object.freeze({
|
|
2646
|
+
/** Ms to wait for in-flight requests to complete before force-closing. */
|
|
2647
|
+
GRACEFUL_MS: 800,
|
|
2648
|
+
/** Ms after which the force disconnect is triggered. */
|
|
2649
|
+
FORCE_TIMEOUT_MS: 1200
|
|
2650
|
+
}),
|
|
2651
|
+
/** Lazy-connect idle lifecycle. */
|
|
2652
|
+
IDLE: Object.freeze({
|
|
2653
|
+
/** Default ms a lazy server stays connected with no tool calls before auto-sleep. */
|
|
2654
|
+
DEFAULT_TIMEOUT_MS: 3e5,
|
|
2655
|
+
/** How often the idle sweep runs (kept well below the timeout). */
|
|
2656
|
+
SWEEP_INTERVAL_MS: 3e4
|
|
2657
|
+
}),
|
|
2658
|
+
/** JSON-RPC response timeout for outstanding requests. */
|
|
2659
|
+
RESPONSE_TIMEOUT_MS: 500,
|
|
2660
|
+
/** Max buffer size for the SSE reader. */
|
|
2661
|
+
SSE_READER_MAX_BUFFER: 256 * 1024,
|
|
2662
|
+
/** Max characters logged from a request body. */
|
|
2663
|
+
REQUEST_LOG_CAP: 1024
|
|
2664
|
+
});
|
|
2665
|
+
|
|
2666
|
+
// src/tool-schema.ts
|
|
2667
|
+
var URL_ELICITATION_REQUIRED = -32042;
|
|
2668
|
+
function isPlainObject(value) {
|
|
2669
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2670
|
+
}
|
|
2671
|
+
function toToolCallResult(response) {
|
|
2672
|
+
if (response.error) {
|
|
2673
|
+
const pages = urlElicitationsOf(response.error);
|
|
2674
|
+
return {
|
|
2675
|
+
content: response.error.message,
|
|
2676
|
+
isError: true,
|
|
2677
|
+
...pages.length > 0 ? { urlElicitations: pages } : {}
|
|
2678
|
+
};
|
|
2679
|
+
}
|
|
2680
|
+
const result = isPlainObject(response.result) ? response.result : {};
|
|
2681
|
+
const structured = result["structuredContent"];
|
|
2682
|
+
return {
|
|
2683
|
+
content: result["content"] ?? "",
|
|
2684
|
+
isError: Boolean(result["isError"]),
|
|
2685
|
+
...isPlainObject(structured) ? { structuredContent: structured } : {}
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
function urlElicitationsOf(error) {
|
|
2689
|
+
if (error.code !== URL_ELICITATION_REQUIRED || !isPlainObject(error.data)) return [];
|
|
2690
|
+
const listed = error.data["elicitations"];
|
|
2691
|
+
if (!Array.isArray(listed)) return [];
|
|
2692
|
+
const pages = [];
|
|
2693
|
+
for (const entry of listed) {
|
|
2694
|
+
const parsed = parseUrlElicitation(entry);
|
|
2695
|
+
if (parsed.ok) pages.push(parsed.value);
|
|
2696
|
+
}
|
|
2697
|
+
return pages;
|
|
2698
|
+
}
|
|
2699
|
+
var MAX_TOOL_PAGES = 100;
|
|
2700
|
+
var MAX_TOOLS = 1e4;
|
|
2701
|
+
async function listAllTools(requestPage) {
|
|
2702
|
+
const tools = [];
|
|
2703
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
2704
|
+
let cursor;
|
|
2705
|
+
for (let page = 0; page < MAX_TOOL_PAGES; page++) {
|
|
2706
|
+
let response;
|
|
2707
|
+
try {
|
|
2708
|
+
response = await requestPage(cursor ? { cursor } : {});
|
|
2709
|
+
} catch (err) {
|
|
2710
|
+
if (page === 0) throw err;
|
|
2711
|
+
break;
|
|
2712
|
+
}
|
|
2713
|
+
if (response.error) {
|
|
2714
|
+
if (page === 0) return null;
|
|
2715
|
+
break;
|
|
2716
|
+
}
|
|
2717
|
+
const result = response.result;
|
|
2718
|
+
tools.push(...normalizeMCPTools(result?.tools));
|
|
2719
|
+
const next = result?.nextCursor;
|
|
2720
|
+
if (typeof next !== "string" || next.length === 0) break;
|
|
2721
|
+
if (seenCursors.has(next) || tools.length >= MAX_TOOLS) break;
|
|
2722
|
+
seenCursors.add(next);
|
|
2723
|
+
cursor = next;
|
|
2724
|
+
}
|
|
2725
|
+
return tools.slice(0, MAX_TOOLS);
|
|
2726
|
+
}
|
|
2727
|
+
function normalizeMCPTools(value) {
|
|
2728
|
+
if (!Array.isArray(value)) return [];
|
|
2729
|
+
const tools = [];
|
|
2730
|
+
for (const raw of value) {
|
|
2731
|
+
if (!raw || typeof raw !== "object") continue;
|
|
2732
|
+
const t = raw;
|
|
2733
|
+
if (typeof t.name !== "string") continue;
|
|
2734
|
+
const name = t.name.trim();
|
|
2735
|
+
if (name.length === 0) continue;
|
|
2736
|
+
const inputSchema = t.inputSchema && typeof t.inputSchema === "object" && !Array.isArray(t.inputSchema) ? t.inputSchema : { type: "object", properties: {} };
|
|
2737
|
+
if (!t.inputSchema || typeof t.inputSchema !== "object" || Array.isArray(t.inputSchema)) {
|
|
2738
|
+
console.warn(
|
|
2739
|
+
JSON.stringify({
|
|
2740
|
+
level: "warn",
|
|
2741
|
+
event: "mcp.tool_schema_invalid",
|
|
2742
|
+
tool: name,
|
|
2743
|
+
message: "no/invalid inputSchema \u2014 defaulting to empty object",
|
|
2744
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2745
|
+
})
|
|
2746
|
+
);
|
|
1982
2747
|
}
|
|
1983
|
-
|
|
1984
|
-
|
|
2748
|
+
tools.push({
|
|
2749
|
+
name,
|
|
2750
|
+
...typeof t.description === "string" ? { description: t.description } : {},
|
|
2751
|
+
inputSchema,
|
|
2752
|
+
...isPlainObject(t.outputSchema) ? { outputSchema: t.outputSchema } : {}
|
|
2753
|
+
});
|
|
1985
2754
|
}
|
|
1986
|
-
return
|
|
2755
|
+
return tools;
|
|
1987
2756
|
}
|
|
1988
2757
|
|
|
1989
2758
|
// src/transport-sse.ts
|
|
@@ -2069,13 +2838,27 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2069
2838
|
});
|
|
2070
2839
|
sseReader.onMessage((msg) => {
|
|
2071
2840
|
this.streamSignal?.();
|
|
2072
|
-
|
|
2073
|
-
|
|
2841
|
+
const id = msg.id;
|
|
2842
|
+
if (msg.method && (typeof id === "number" || typeof id === "string")) {
|
|
2843
|
+
void this.replyToServer(
|
|
2844
|
+
{ id, method: msg.method, params: msg.params },
|
|
2845
|
+
this.endpointUrl ?? this.url,
|
|
2846
|
+
this.headers
|
|
2847
|
+
);
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
if (msg.method) {
|
|
2851
|
+
if (msg.method === "notifications/cancelled") {
|
|
2852
|
+
this.serverRequests.cancel(msg.params);
|
|
2853
|
+
} else if (msg.method === "notifications/tools/list_changed") {
|
|
2074
2854
|
void this.handleToolsListChanged();
|
|
2075
2855
|
} else if (msg.method === "notifications/resources/list_changed") {
|
|
2076
2856
|
this.notifyResourcesChanged();
|
|
2077
2857
|
} else if (msg.method === "notifications/prompts/list_changed") {
|
|
2078
2858
|
this.notifyPromptsChanged();
|
|
2859
|
+
} else if (msg.method === "notifications/resources/updated") {
|
|
2860
|
+
const uri = resourceUpdatedUri(msg.params);
|
|
2861
|
+
if (uri) this.notifyResourceUpdated(uri);
|
|
2079
2862
|
}
|
|
2080
2863
|
return;
|
|
2081
2864
|
}
|
|
@@ -2096,9 +2879,9 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2096
2879
|
await this.waitForStream(streamReady, signal);
|
|
2097
2880
|
const initRes = await this.httpPost("initialize", {
|
|
2098
2881
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
2099
|
-
// Client capabilities
|
|
2882
|
+
// Client capabilities: elicitation when the host can ask its user.
|
|
2100
2883
|
// `tools` is a SERVER capability and never belonged here.
|
|
2101
|
-
capabilities:
|
|
2884
|
+
capabilities: this.serverRequests.capabilities(),
|
|
2102
2885
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
2103
2886
|
});
|
|
2104
2887
|
if (initRes.error) {
|
|
@@ -2226,7 +3009,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2226
3009
|
const body = encodeJsonRpcMessage(id, method, params);
|
|
2227
3010
|
const external = opts?.signal;
|
|
2228
3011
|
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
2229
|
-
const timeoutSignal =
|
|
3012
|
+
const timeoutSignal = this.requestTimeoutSignal(parent, timeoutMs);
|
|
2230
3013
|
const streamed = isNotification ? void 0 : this.awaitStreamResponse(id, timeoutSignal.signal);
|
|
2231
3014
|
const fetchOpts = {
|
|
2232
3015
|
method: "POST",
|
|
@@ -2315,14 +3098,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
2315
3098
|
});
|
|
2316
3099
|
}
|
|
2317
3100
|
const res = await this.httpPost("tools/call", { name, arguments: input }, opts);
|
|
2318
|
-
|
|
2319
|
-
return { content: res.error.message, isError: true };
|
|
2320
|
-
}
|
|
2321
|
-
const result = res.result;
|
|
2322
|
-
return {
|
|
2323
|
-
content: result?.content ?? "",
|
|
2324
|
-
isError: Boolean(result?.isError)
|
|
2325
|
-
};
|
|
3101
|
+
return toToolCallResult(res);
|
|
2326
3102
|
}
|
|
2327
3103
|
/** Generic JSON-RPC request — used by MCPClient.request() for SSE transports. */
|
|
2328
3104
|
async request(method, params, timeoutMs, opts) {
|
|
@@ -2379,23 +3155,89 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2379
3155
|
this._nextId = nextJsonRpcId(id);
|
|
2380
3156
|
return id;
|
|
2381
3157
|
}
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
3158
|
+
/**
|
|
3159
|
+
* Read a POST's reply. A `text/event-stream` reply is consumed event by
|
|
3160
|
+
* event: the server may put notifications and its OWN requests (elicitation)
|
|
3161
|
+
* ahead of our response and then wait for our answer to them, so buffering
|
|
3162
|
+
* the whole body first would deadlock the call.
|
|
3163
|
+
*/
|
|
3164
|
+
async readResponse(res, requestId) {
|
|
3165
|
+
const streamed = (res.headers?.get("content-type") ?? "").includes("text/event-stream");
|
|
3166
|
+
const body = res.body;
|
|
3167
|
+
if (!streamed || !body || typeof body.getReader !== "function") {
|
|
3168
|
+
return this.takeEnvelopes(await readBodyCapped(res), requestId);
|
|
3169
|
+
}
|
|
3170
|
+
const reader = body.getReader();
|
|
3171
|
+
const decoder = new TextDecoder();
|
|
3172
|
+
const others = [];
|
|
3173
|
+
let pending = "";
|
|
3174
|
+
let total = 0;
|
|
3175
|
+
try {
|
|
3176
|
+
for (; ; ) {
|
|
3177
|
+
const { done, value } = await reader.read();
|
|
3178
|
+
if (done) break;
|
|
3179
|
+
total += value.byteLength;
|
|
3180
|
+
if (total > MAX_MCP_HTTP_BODY_BYTES) {
|
|
3181
|
+
throw new Error(`MCP response stream exceeded ${MAX_MCP_HTTP_BODY_BYTES} bytes`);
|
|
3182
|
+
}
|
|
3183
|
+
pending += decoder.decode(value, { stream: true });
|
|
3184
|
+
const cut = completeEventsEnd(pending);
|
|
3185
|
+
if (cut === 0) continue;
|
|
3186
|
+
const match = this.takeEnvelopes(pending.slice(0, cut), requestId, others);
|
|
3187
|
+
pending = pending.slice(cut);
|
|
3188
|
+
if (match) {
|
|
3189
|
+
await reader.cancel().catch(() => void 0);
|
|
3190
|
+
return match;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
pending += decoder.decode();
|
|
3194
|
+
return this.takeEnvelopes(pending, requestId, others) ?? others[0];
|
|
3195
|
+
} finally {
|
|
3196
|
+
reader.releaseLock?.();
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
/**
|
|
3200
|
+
* Dispatch the notifications and server requests in `text`, and return the
|
|
3201
|
+
* response to `requestId` — falling back to the first other response, which
|
|
3202
|
+
* the caller's id check then rejects.
|
|
3203
|
+
*/
|
|
3204
|
+
takeEnvelopes(text, requestId, others = []) {
|
|
3205
|
+
let match;
|
|
3206
|
+
for (const envelope of extractJsonRpcEnvelopes(text)) {
|
|
3207
|
+
if (isJsonRpcResult(envelope)) {
|
|
3208
|
+
if (envelope.id === requestId) match ??= envelope;
|
|
3209
|
+
else others.push(envelope);
|
|
3210
|
+
} else if (envelope.id !== void 0) {
|
|
3211
|
+
void this.replyToServer(
|
|
3212
|
+
{ id: envelope.id, method: envelope.method, params: envelope.params },
|
|
3213
|
+
this.url,
|
|
3214
|
+
this.sessionHeaders()
|
|
3215
|
+
);
|
|
3216
|
+
} else {
|
|
3217
|
+
this.handleNotification(envelope.method, envelope.params);
|
|
2387
3218
|
}
|
|
2388
3219
|
}
|
|
2389
|
-
|
|
2390
|
-
|
|
3220
|
+
return match ?? others[0];
|
|
3221
|
+
}
|
|
3222
|
+
sessionHeaders() {
|
|
3223
|
+
return {
|
|
3224
|
+
Accept: "application/json, text/event-stream",
|
|
3225
|
+
...this.sessionId ? { "Mcp-Session-Id": this.sessionId } : {},
|
|
3226
|
+
...this.headers
|
|
3227
|
+
};
|
|
2391
3228
|
}
|
|
2392
|
-
handleNotification(method) {
|
|
2393
|
-
if (method === "notifications/
|
|
3229
|
+
handleNotification(method, params) {
|
|
3230
|
+
if (method === "notifications/cancelled") {
|
|
3231
|
+
this.serverRequests.cancel(params);
|
|
3232
|
+
} else if (method === "notifications/resources/list_changed") {
|
|
2394
3233
|
this.notifyResourcesChanged();
|
|
2395
3234
|
} else if (method === "notifications/prompts/list_changed") {
|
|
2396
3235
|
this.notifyPromptsChanged();
|
|
2397
3236
|
} else if (method === "notifications/tools/list_changed") {
|
|
2398
3237
|
void this.refreshTools();
|
|
3238
|
+
} else if (method === "notifications/resources/updated") {
|
|
3239
|
+
const uri = resourceUpdatedUri(params);
|
|
3240
|
+
if (uri) this.notifyResourceUpdated(uri);
|
|
2399
3241
|
}
|
|
2400
3242
|
}
|
|
2401
3243
|
async refreshTools() {
|
|
@@ -2434,8 +3276,9 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2434
3276
|
method: "initialize",
|
|
2435
3277
|
params: {
|
|
2436
3278
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
2437
|
-
// Client capabilities
|
|
2438
|
-
|
|
3279
|
+
// Client capabilities: elicitation when the host can ask its user
|
|
3280
|
+
// (`tools` is a server capability and never belongs here).
|
|
3281
|
+
capabilities: this.serverRequests.capabilities(),
|
|
2439
3282
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
2440
3283
|
}
|
|
2441
3284
|
}),
|
|
@@ -2481,7 +3324,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2481
3324
|
const body = encodeJsonRpcMessage(id, method, params);
|
|
2482
3325
|
const external = opts?.signal;
|
|
2483
3326
|
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
2484
|
-
const timeoutSignal =
|
|
3327
|
+
const timeoutSignal = this.requestTimeoutSignal(parent, this.requestTimeout);
|
|
2485
3328
|
const fetchOpts = {
|
|
2486
3329
|
method: "POST",
|
|
2487
3330
|
headers: {
|
|
@@ -2506,7 +3349,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2506
3349
|
await readBodyCapped(res).catch(() => void 0);
|
|
2507
3350
|
return { jsonrpc: "2.0", id };
|
|
2508
3351
|
}
|
|
2509
|
-
const match = this.
|
|
3352
|
+
const match = await this.readResponse(res, id);
|
|
2510
3353
|
if (match) {
|
|
2511
3354
|
return assertMatchingJsonRpcResult(match, id, method);
|
|
2512
3355
|
}
|
|
@@ -2531,7 +3374,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2531
3374
|
const body = encodeJsonRpcMessage(id, method, params);
|
|
2532
3375
|
const external = opts?.signal;
|
|
2533
3376
|
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
2534
|
-
const timeoutSignal =
|
|
3377
|
+
const timeoutSignal = this.requestTimeoutSignal(parent, timeoutMs ?? this.requestTimeout);
|
|
2535
3378
|
const fetchOpts = {
|
|
2536
3379
|
method: "POST",
|
|
2537
3380
|
headers: {
|
|
@@ -2556,7 +3399,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2556
3399
|
await readBodyCapped(res).catch(() => void 0);
|
|
2557
3400
|
return { jsonrpc: "2.0", id };
|
|
2558
3401
|
}
|
|
2559
|
-
const parsed = this.
|
|
3402
|
+
const parsed = await this.readResponse(res, id);
|
|
2560
3403
|
if (parsed) {
|
|
2561
3404
|
const matched = assertMatchingJsonRpcResult(parsed, id, method);
|
|
2562
3405
|
return {
|
|
@@ -2586,14 +3429,7 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2586
3429
|
throw new Error(`streamable-http transport not connected (state=${this.state})`);
|
|
2587
3430
|
}
|
|
2588
3431
|
const res = await this.postRaw("tools/call", { name, arguments: input }, opts);
|
|
2589
|
-
|
|
2590
|
-
return { content: res.error.message, isError: true };
|
|
2591
|
-
}
|
|
2592
|
-
const result = res.result;
|
|
2593
|
-
return {
|
|
2594
|
-
content: result?.content ?? "",
|
|
2595
|
-
isError: Boolean(result?.isError)
|
|
2596
|
-
};
|
|
3432
|
+
return toToolCallResult(res);
|
|
2597
3433
|
}
|
|
2598
3434
|
async close() {
|
|
2599
3435
|
this.releasePinnedDispatcher();
|
|
@@ -2619,11 +3455,125 @@ var StreamableHTTPTransport = class _StreamableHTTPTransport extends BaseHTTPTra
|
|
|
2619
3455
|
}
|
|
2620
3456
|
}
|
|
2621
3457
|
};
|
|
3458
|
+
function completeEventsEnd(text) {
|
|
3459
|
+
const lf = text.lastIndexOf("\n\n");
|
|
3460
|
+
const crlf = text.lastIndexOf("\r\n\r\n");
|
|
3461
|
+
return Math.max(lf === -1 ? 0 : lf + 2, crlf === -1 ? 0 : crlf + 4);
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
// src/client-http-connection.ts
|
|
3465
|
+
async function connectSSE(host) {
|
|
3466
|
+
if (!host.opts.url) {
|
|
3467
|
+
host.state = "failed";
|
|
3468
|
+
throw new Error('MCP SSE transport requires "url"');
|
|
3469
|
+
}
|
|
3470
|
+
const httpOpts = {
|
|
3471
|
+
name: host.opts.name,
|
|
3472
|
+
url: host.opts.url,
|
|
3473
|
+
headers: resolveHttpBearerHeaders(host.opts),
|
|
3474
|
+
startupTimeoutMs: host.opts.startupTimeoutMs,
|
|
3475
|
+
requestTimeoutMs: host.opts.requestTimeoutMs,
|
|
3476
|
+
authorizationProvider: host.opts.authorizationProvider,
|
|
3477
|
+
allowPrivateNetworks: host.opts.allowPrivateNetworks,
|
|
3478
|
+
serverRequests: host.serverRequests
|
|
3479
|
+
};
|
|
3480
|
+
host.sseTransport = new SSETransport(httpOpts);
|
|
3481
|
+
host.sseTransport.onDisconnect(() => {
|
|
3482
|
+
host.state = "disconnected";
|
|
3483
|
+
for (const cb of host.disconnectListeners) {
|
|
3484
|
+
try {
|
|
3485
|
+
cb();
|
|
3486
|
+
} catch {
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
});
|
|
3490
|
+
host.sseTransport.onToolsChanged((tools) => {
|
|
3491
|
+
host._tools = tools;
|
|
3492
|
+
host._toolsCache = tools;
|
|
3493
|
+
for (const cb of host.toolsChangedListeners) {
|
|
3494
|
+
try {
|
|
3495
|
+
cb(host.opts.name, tools);
|
|
3496
|
+
} catch {
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
});
|
|
3500
|
+
host.sseTransport.onResourcesChanged(() => host.emitCapabilityChanged("resources"));
|
|
3501
|
+
host.sseTransport.onResourceUpdated((uri) => host.emitResourceUpdated(uri));
|
|
3502
|
+
host.sseTransport.onPromptsChanged(() => host.emitCapabilityChanged("prompts"));
|
|
3503
|
+
try {
|
|
3504
|
+
await host.sseTransport.connect();
|
|
3505
|
+
} catch (err) {
|
|
3506
|
+
const t = host.sseTransport;
|
|
3507
|
+
host.sseTransport = void 0;
|
|
3508
|
+
await t.close().catch(() => {
|
|
3509
|
+
});
|
|
3510
|
+
host.state = "failed";
|
|
3511
|
+
throw err;
|
|
3512
|
+
}
|
|
3513
|
+
host._tools = host.sseTransport.listTools();
|
|
3514
|
+
host._toolsCache = host._tools;
|
|
3515
|
+
host._serverMetadata = host.sseTransport.getServerMetadata();
|
|
3516
|
+
host.state = "connected";
|
|
3517
|
+
}
|
|
3518
|
+
async function connectStreamableHTTP(host) {
|
|
3519
|
+
if (!host.opts.url) {
|
|
3520
|
+
host.state = "failed";
|
|
3521
|
+
throw new Error('MCP streamable-http transport requires "url"');
|
|
3522
|
+
}
|
|
3523
|
+
const httpOpts = {
|
|
3524
|
+
name: host.opts.name,
|
|
3525
|
+
url: host.opts.url,
|
|
3526
|
+
headers: resolveHttpBearerHeaders(host.opts),
|
|
3527
|
+
startupTimeoutMs: host.opts.startupTimeoutMs,
|
|
3528
|
+
requestTimeoutMs: host.opts.requestTimeoutMs,
|
|
3529
|
+
authorizationProvider: host.opts.authorizationProvider,
|
|
3530
|
+
allowPrivateNetworks: host.opts.allowPrivateNetworks,
|
|
3531
|
+
serverRequests: host.serverRequests
|
|
3532
|
+
};
|
|
3533
|
+
host.httpTransport = new StreamableHTTPTransport(httpOpts);
|
|
3534
|
+
host.httpTransport.onDisconnect(() => {
|
|
3535
|
+
host.state = "disconnected";
|
|
3536
|
+
for (const cb of host.disconnectListeners) {
|
|
3537
|
+
try {
|
|
3538
|
+
cb();
|
|
3539
|
+
} catch {
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
});
|
|
3543
|
+
host.httpTransport.onToolsChanged((tools) => {
|
|
3544
|
+
host._tools = tools;
|
|
3545
|
+
host._toolsCache = tools;
|
|
3546
|
+
for (const cb of host.toolsChangedListeners) {
|
|
3547
|
+
try {
|
|
3548
|
+
cb(host.opts.name, tools);
|
|
3549
|
+
} catch {
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
});
|
|
3553
|
+
host.httpTransport.onResourcesChanged(() => host.emitCapabilityChanged("resources"));
|
|
3554
|
+
host.httpTransport.onResourceUpdated((uri) => host.emitResourceUpdated(uri));
|
|
3555
|
+
host.httpTransport.onPromptsChanged(() => host.emitCapabilityChanged("prompts"));
|
|
3556
|
+
try {
|
|
3557
|
+
await host.httpTransport.connect();
|
|
3558
|
+
} catch (err) {
|
|
3559
|
+
const t = host.httpTransport;
|
|
3560
|
+
host.httpTransport = void 0;
|
|
3561
|
+
await t.close().catch(() => {
|
|
3562
|
+
});
|
|
3563
|
+
host.state = "failed";
|
|
3564
|
+
throw err;
|
|
3565
|
+
}
|
|
3566
|
+
host._tools = host.httpTransport.listTools();
|
|
3567
|
+
host._toolsCache = host._tools;
|
|
3568
|
+
host._serverMetadata = host.httpTransport.getServerMetadata();
|
|
3569
|
+
host.state = "connected";
|
|
3570
|
+
}
|
|
2622
3571
|
|
|
2623
3572
|
// src/client.ts
|
|
2624
3573
|
var MCPClient = class _MCPClient {
|
|
2625
3574
|
constructor(opts) {
|
|
2626
3575
|
this.opts = opts;
|
|
3576
|
+
this.serverRequests = new ServerRequestResponder(opts.elicitation);
|
|
2627
3577
|
}
|
|
2628
3578
|
opts;
|
|
2629
3579
|
capabilityClient = new MCPCapabilityClient(
|
|
@@ -2648,7 +3598,19 @@ var MCPClient = class _MCPClient {
|
|
|
2648
3598
|
* (stdio child exit, `close()`) so callers don't hang forever.
|
|
2649
3599
|
*/
|
|
2650
3600
|
pending = /* @__PURE__ */ new Map();
|
|
2651
|
-
|
|
3601
|
+
/**
|
|
3602
|
+
* The unterminated tail of stdout, as the chunks it arrived in; joined only
|
|
3603
|
+
* when its newline arrives. Kept as one string (`+=` then `indexOf`), every
|
|
3604
|
+
* chunk flattened and re-scanned everything buffered so far — O(n²) for a
|
|
3605
|
+
* multi-MiB tools/call result.
|
|
3606
|
+
*/
|
|
3607
|
+
rxParts = [];
|
|
3608
|
+
get rxBuffer() {
|
|
3609
|
+
return this.rxParts.join("");
|
|
3610
|
+
}
|
|
3611
|
+
set rxBuffer(value) {
|
|
3612
|
+
this.rxParts = value ? [value] : [];
|
|
3613
|
+
}
|
|
2652
3614
|
rxBufferBytes = 0;
|
|
2653
3615
|
/**
|
|
2654
3616
|
* Incremental UTF-8 decoder for the stdio rx path. A pipe read boundary can
|
|
@@ -2676,8 +3638,11 @@ var MCPClient = class _MCPClient {
|
|
|
2676
3638
|
toolsChangedListeners = /* @__PURE__ */ new Set();
|
|
2677
3639
|
resourcesChangedListeners = /* @__PURE__ */ new Set();
|
|
2678
3640
|
promptsChangedListeners = /* @__PURE__ */ new Set();
|
|
3641
|
+
resourceUpdatedListeners = /* @__PURE__ */ new Set();
|
|
2679
3642
|
/** Notified when an HTTP transport (SSE or streamable-http) disconnects. */
|
|
2680
3643
|
disconnectListeners = /* @__PURE__ */ new Set();
|
|
3644
|
+
/** Answers server→client requests (ping, elicitation) on every transport. */
|
|
3645
|
+
serverRequests;
|
|
2681
3646
|
getState() {
|
|
2682
3647
|
return this.state;
|
|
2683
3648
|
}
|
|
@@ -2819,9 +3784,9 @@ var MCPClient = class _MCPClient {
|
|
|
2819
3784
|
"initialize",
|
|
2820
3785
|
{
|
|
2821
3786
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
2822
|
-
// Client capabilities
|
|
3787
|
+
// Client capabilities: elicitation when the host can ask its user.
|
|
2823
3788
|
// `tools` is a SERVER capability and never belonged here.
|
|
2824
|
-
capabilities:
|
|
3789
|
+
capabilities: this.serverRequests.capabilities(),
|
|
2825
3790
|
clientInfo: MCP_CONSTANTS.CLIENT_INFO
|
|
2826
3791
|
},
|
|
2827
3792
|
this.opts.startupTimeoutMs ?? 1e4
|
|
@@ -2848,106 +3813,10 @@ var MCPClient = class _MCPClient {
|
|
|
2848
3813
|
this.state = "connected";
|
|
2849
3814
|
}
|
|
2850
3815
|
async connectSSE() {
|
|
2851
|
-
|
|
2852
|
-
this.state = "failed";
|
|
2853
|
-
throw new Error('MCP SSE transport requires "url"');
|
|
2854
|
-
}
|
|
2855
|
-
const httpOpts = {
|
|
2856
|
-
name: this.opts.name,
|
|
2857
|
-
url: this.opts.url,
|
|
2858
|
-
headers: resolveHttpBearerHeaders(this.opts),
|
|
2859
|
-
startupTimeoutMs: this.opts.startupTimeoutMs,
|
|
2860
|
-
requestTimeoutMs: this.opts.requestTimeoutMs,
|
|
2861
|
-
authorizationProvider: this.opts.authorizationProvider,
|
|
2862
|
-
allowPrivateNetworks: this.opts.allowPrivateNetworks
|
|
2863
|
-
};
|
|
2864
|
-
this.sseTransport = new SSETransport(httpOpts);
|
|
2865
|
-
this.sseTransport.onDisconnect(() => {
|
|
2866
|
-
this.state = "disconnected";
|
|
2867
|
-
for (const cb of this.disconnectListeners) {
|
|
2868
|
-
try {
|
|
2869
|
-
cb();
|
|
2870
|
-
} catch {
|
|
2871
|
-
}
|
|
2872
|
-
}
|
|
2873
|
-
});
|
|
2874
|
-
this.sseTransport.onToolsChanged((tools) => {
|
|
2875
|
-
this._tools = tools;
|
|
2876
|
-
this._toolsCache = tools;
|
|
2877
|
-
for (const cb of this.toolsChangedListeners) {
|
|
2878
|
-
try {
|
|
2879
|
-
cb(this.opts.name, tools);
|
|
2880
|
-
} catch {
|
|
2881
|
-
}
|
|
2882
|
-
}
|
|
2883
|
-
});
|
|
2884
|
-
this.sseTransport.onResourcesChanged(() => this.emitCapabilityChanged("resources"));
|
|
2885
|
-
this.sseTransport.onPromptsChanged(() => this.emitCapabilityChanged("prompts"));
|
|
2886
|
-
try {
|
|
2887
|
-
await this.sseTransport.connect();
|
|
2888
|
-
} catch (err) {
|
|
2889
|
-
const t = this.sseTransport;
|
|
2890
|
-
this.sseTransport = void 0;
|
|
2891
|
-
await t.close().catch(() => {
|
|
2892
|
-
});
|
|
2893
|
-
this.state = "failed";
|
|
2894
|
-
throw err;
|
|
2895
|
-
}
|
|
2896
|
-
this._tools = this.sseTransport.listTools();
|
|
2897
|
-
this._toolsCache = this._tools;
|
|
2898
|
-
this._serverMetadata = this.sseTransport.getServerMetadata();
|
|
2899
|
-
this.state = "connected";
|
|
3816
|
+
return connectSSE(this.clientHttpConnectionHost());
|
|
2900
3817
|
}
|
|
2901
3818
|
async connectStreamableHTTP() {
|
|
2902
|
-
|
|
2903
|
-
this.state = "failed";
|
|
2904
|
-
throw new Error('MCP streamable-http transport requires "url"');
|
|
2905
|
-
}
|
|
2906
|
-
const httpOpts = {
|
|
2907
|
-
name: this.opts.name,
|
|
2908
|
-
url: this.opts.url,
|
|
2909
|
-
headers: resolveHttpBearerHeaders(this.opts),
|
|
2910
|
-
startupTimeoutMs: this.opts.startupTimeoutMs,
|
|
2911
|
-
requestTimeoutMs: this.opts.requestTimeoutMs,
|
|
2912
|
-
authorizationProvider: this.opts.authorizationProvider,
|
|
2913
|
-
allowPrivateNetworks: this.opts.allowPrivateNetworks
|
|
2914
|
-
};
|
|
2915
|
-
this.httpTransport = new StreamableHTTPTransport(httpOpts);
|
|
2916
|
-
this.httpTransport.onDisconnect(() => {
|
|
2917
|
-
this.state = "disconnected";
|
|
2918
|
-
for (const cb of this.disconnectListeners) {
|
|
2919
|
-
try {
|
|
2920
|
-
cb();
|
|
2921
|
-
} catch {
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
});
|
|
2925
|
-
this.httpTransport.onToolsChanged((tools) => {
|
|
2926
|
-
this._tools = tools;
|
|
2927
|
-
this._toolsCache = tools;
|
|
2928
|
-
for (const cb of this.toolsChangedListeners) {
|
|
2929
|
-
try {
|
|
2930
|
-
cb(this.opts.name, tools);
|
|
2931
|
-
} catch {
|
|
2932
|
-
}
|
|
2933
|
-
}
|
|
2934
|
-
});
|
|
2935
|
-
this.httpTransport.onResourcesChanged(() => this.emitCapabilityChanged("resources"));
|
|
2936
|
-
this.httpTransport.onPromptsChanged(() => this.emitCapabilityChanged("prompts"));
|
|
2937
|
-
try {
|
|
2938
|
-
await this.httpTransport.connect();
|
|
2939
|
-
} catch (err) {
|
|
2940
|
-
const t = this.httpTransport;
|
|
2941
|
-
this.httpTransport = void 0;
|
|
2942
|
-
await t.close().catch(() => {
|
|
2943
|
-
});
|
|
2944
|
-
this.state = "failed";
|
|
2945
|
-
throw err;
|
|
2946
|
-
}
|
|
2947
|
-
this._tools = this.httpTransport.listTools();
|
|
2948
|
-
this._toolsCache = this._tools;
|
|
2949
|
-
this._serverMetadata = this.httpTransport.getServerMetadata();
|
|
2950
|
-
this.state = "connected";
|
|
3819
|
+
return connectStreamableHTTP(this.clientHttpConnectionHost());
|
|
2951
3820
|
}
|
|
2952
3821
|
async callTool(name, input, opts) {
|
|
2953
3822
|
if (this.state !== "connected") {
|
|
@@ -2960,14 +3829,14 @@ var MCPClient = class _MCPClient {
|
|
|
2960
3829
|
return this.httpTransport.callTool(name, input, opts);
|
|
2961
3830
|
}
|
|
2962
3831
|
const res = await this.request("tools/call", { name, arguments: input }, void 0, opts);
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
3832
|
+
return toToolCallResult(res);
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Put the pages of a `-32042` tool-call error to the user (consent first,
|
|
3836
|
+
* nothing opened without it), on the run whose call got the error.
|
|
3837
|
+
*/
|
|
3838
|
+
presentUrlElicitations(elicitations, signal) {
|
|
3839
|
+
return this.serverRequests.presentUrl(elicitations, signal);
|
|
2971
3840
|
}
|
|
2972
3841
|
async listResources(opts = {}) {
|
|
2973
3842
|
return this.capabilityClient.listResources(opts);
|
|
@@ -3041,6 +3910,7 @@ var MCPClient = class _MCPClient {
|
|
|
3041
3910
|
this.child = void 0;
|
|
3042
3911
|
}
|
|
3043
3912
|
this.failPending(`MCP "${this.opts.name}" closed`);
|
|
3913
|
+
this.serverRequests.dispose();
|
|
3044
3914
|
await Promise.allSettled([this.sseTransport?.close(), this.httpTransport?.close()]);
|
|
3045
3915
|
this.state = "disconnected";
|
|
3046
3916
|
}
|
|
@@ -3074,26 +3944,31 @@ var MCPClient = class _MCPClient {
|
|
|
3074
3944
|
const detach = () => {
|
|
3075
3945
|
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
3076
3946
|
};
|
|
3077
|
-
const
|
|
3947
|
+
const onTimeout = () => {
|
|
3948
|
+
if (this.serverRequests.awaitingUser) {
|
|
3949
|
+
entry.timer = setTimeout(onTimeout, timeoutMs);
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3078
3952
|
this.pending.delete(id);
|
|
3079
3953
|
detach();
|
|
3080
3954
|
reject(
|
|
3081
3955
|
new Error(`MCP "${this.opts.name}" request "${method}" timed out after ${timeoutMs}ms`)
|
|
3082
3956
|
);
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3957
|
+
};
|
|
3958
|
+
const entry = {
|
|
3085
3959
|
resolve: (res) => {
|
|
3086
|
-
clearTimeout(timer);
|
|
3960
|
+
clearTimeout(entry.timer);
|
|
3087
3961
|
detach();
|
|
3088
3962
|
resolve(res);
|
|
3089
3963
|
},
|
|
3090
3964
|
reject: (err) => {
|
|
3091
|
-
clearTimeout(timer);
|
|
3965
|
+
clearTimeout(entry.timer);
|
|
3092
3966
|
detach();
|
|
3093
3967
|
reject(err);
|
|
3094
3968
|
},
|
|
3095
|
-
timer
|
|
3096
|
-
}
|
|
3969
|
+
timer: setTimeout(onTimeout, timeoutMs)
|
|
3970
|
+
};
|
|
3971
|
+
this.pending.set(id, entry);
|
|
3097
3972
|
const stdin = this.child?.stdin;
|
|
3098
3973
|
if (!stdin || stdin.destroyed) {
|
|
3099
3974
|
const pending = this.pending.get(id);
|
|
@@ -3217,11 +4092,10 @@ var MCPClient = class _MCPClient {
|
|
|
3217
4092
|
}
|
|
3218
4093
|
}
|
|
3219
4094
|
onData(s) {
|
|
3220
|
-
this.rxBuffer += s;
|
|
3221
4095
|
this.rxBufferBytes += Buffer.byteLength(s, "utf8");
|
|
3222
4096
|
if (this.rxBufferBytes > _MCPClient.MAX_RX_BUFFER_BYTES) {
|
|
3223
4097
|
const truncated = this.rxBufferBytes;
|
|
3224
|
-
this.
|
|
4098
|
+
this.rxParts = [];
|
|
3225
4099
|
this.rxBufferBytes = 0;
|
|
3226
4100
|
this.failPending(
|
|
3227
4101
|
`MCP "${this.opts.name}" rx buffer overflow (${truncated} bytes without a newline) \u2014 closing connection`
|
|
@@ -3230,17 +4104,22 @@ var MCPClient = class _MCPClient {
|
|
|
3230
4104
|
return;
|
|
3231
4105
|
}
|
|
3232
4106
|
let start = 0;
|
|
3233
|
-
let idx =
|
|
4107
|
+
let idx = s.indexOf("\n");
|
|
4108
|
+
if (idx === -1) {
|
|
4109
|
+
this.rxParts.push(s);
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
3234
4112
|
while (idx !== -1) {
|
|
3235
|
-
const
|
|
4113
|
+
const head = this.rxParts.length > 0 ? this.rxParts.join("") : "";
|
|
4114
|
+
this.rxParts = [];
|
|
4115
|
+
const line = (head + s.slice(start, idx)).trim();
|
|
3236
4116
|
start = idx + 1;
|
|
3237
4117
|
if (line) this.onLine(line);
|
|
3238
|
-
idx =
|
|
3239
|
-
}
|
|
3240
|
-
if (start > 0) {
|
|
3241
|
-
this.rxBufferBytes -= Buffer.byteLength(this.rxBuffer.slice(0, start), "utf8");
|
|
3242
|
-
this.rxBuffer = this.rxBuffer.slice(start);
|
|
4118
|
+
idx = s.indexOf("\n", start);
|
|
3243
4119
|
}
|
|
4120
|
+
const tail = s.slice(start);
|
|
4121
|
+
this.rxBufferBytes = Buffer.byteLength(tail, "utf8");
|
|
4122
|
+
if (tail) this.rxParts.push(tail);
|
|
3244
4123
|
}
|
|
3245
4124
|
onLine(line) {
|
|
3246
4125
|
let msg;
|
|
@@ -3255,7 +4134,7 @@ var MCPClient = class _MCPClient {
|
|
|
3255
4134
|
if (typeof envelope["method"] === "string") {
|
|
3256
4135
|
const id = envelope["id"];
|
|
3257
4136
|
if (typeof id === "number" || typeof id === "string") {
|
|
3258
|
-
this.handleServerRequest({
|
|
4137
|
+
void this.handleServerRequest({
|
|
3259
4138
|
jsonrpc: "2.0",
|
|
3260
4139
|
id,
|
|
3261
4140
|
method: envelope["method"],
|
|
@@ -3264,12 +4143,17 @@ var MCPClient = class _MCPClient {
|
|
|
3264
4143
|
return;
|
|
3265
4144
|
}
|
|
3266
4145
|
if (Object.hasOwn(envelope, "id")) return;
|
|
3267
|
-
if (envelope["method"] === "notifications/
|
|
4146
|
+
if (envelope["method"] === "notifications/cancelled") {
|
|
4147
|
+
this.serverRequests.cancel(envelope["params"]);
|
|
4148
|
+
} else if (envelope["method"] === "notifications/tools/list_changed") {
|
|
3268
4149
|
void this.handleToolsListChanged();
|
|
3269
4150
|
} else if (envelope["method"] === "notifications/resources/list_changed") {
|
|
3270
4151
|
this.emitCapabilityChanged("resources");
|
|
3271
4152
|
} else if (envelope["method"] === "notifications/prompts/list_changed") {
|
|
3272
4153
|
this.emitCapabilityChanged("prompts");
|
|
4154
|
+
} else if (envelope["method"] === "notifications/resources/updated") {
|
|
4155
|
+
const uri = resourceUpdatedUri(envelope["params"]);
|
|
4156
|
+
if (uri) this.emitResourceUpdated(uri);
|
|
3273
4157
|
}
|
|
3274
4158
|
return;
|
|
3275
4159
|
}
|
|
@@ -3281,15 +4165,8 @@ var MCPClient = class _MCPClient {
|
|
|
3281
4165
|
entry?.resolve(response);
|
|
3282
4166
|
}
|
|
3283
4167
|
}
|
|
3284
|
-
handleServerRequest(request3) {
|
|
3285
|
-
const response =
|
|
3286
|
-
jsonrpc: "2.0",
|
|
3287
|
-
id: request3.id,
|
|
3288
|
-
error: {
|
|
3289
|
-
code: -32601,
|
|
3290
|
-
message: request3.method === "sampling/createMessage" ? "Client sampling is disabled by policy" : `Method not found: ${request3.method}`
|
|
3291
|
-
}
|
|
3292
|
-
};
|
|
4168
|
+
async handleServerRequest(request3) {
|
|
4169
|
+
const response = await this.serverRequests.answer(request3);
|
|
3293
4170
|
try {
|
|
3294
4171
|
this.child?.stdin?.write(`${JSON.stringify(response)}
|
|
3295
4172
|
`);
|
|
@@ -3329,12 +4206,26 @@ var MCPClient = class _MCPClient {
|
|
|
3329
4206
|
removeResourcesChangedListener(listener) {
|
|
3330
4207
|
this.resourcesChangedListeners.delete(listener);
|
|
3331
4208
|
}
|
|
4209
|
+
addResourceUpdatedListener(listener) {
|
|
4210
|
+
this.resourceUpdatedListeners.add(listener);
|
|
4211
|
+
}
|
|
4212
|
+
removeResourceUpdatedListener(listener) {
|
|
4213
|
+
this.resourceUpdatedListeners.delete(listener);
|
|
4214
|
+
}
|
|
3332
4215
|
addPromptsChangedListener(listener) {
|
|
3333
4216
|
this.promptsChangedListeners.add(listener);
|
|
3334
4217
|
}
|
|
3335
4218
|
removePromptsChangedListener(listener) {
|
|
3336
4219
|
this.promptsChangedListeners.delete(listener);
|
|
3337
4220
|
}
|
|
4221
|
+
emitResourceUpdated(uri) {
|
|
4222
|
+
for (const listener of this.resourceUpdatedListeners) {
|
|
4223
|
+
try {
|
|
4224
|
+
listener(this.opts.name, uri);
|
|
4225
|
+
} catch {
|
|
4226
|
+
}
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
3338
4229
|
emitCapabilityChanged(capability) {
|
|
3339
4230
|
const listeners = capability === "resources" ? this.resourcesChangedListeners : this.promptsChangedListeners;
|
|
3340
4231
|
for (const listener of listeners) {
|
|
@@ -3344,8 +4235,99 @@ var MCPClient = class _MCPClient {
|
|
|
3344
4235
|
}
|
|
3345
4236
|
}
|
|
3346
4237
|
}
|
|
4238
|
+
clientHttpConnectionHost() {
|
|
4239
|
+
const self = this;
|
|
4240
|
+
return {
|
|
4241
|
+
get opts() {
|
|
4242
|
+
return self.opts;
|
|
4243
|
+
},
|
|
4244
|
+
get state() {
|
|
4245
|
+
return self.state;
|
|
4246
|
+
},
|
|
4247
|
+
set state(value) {
|
|
4248
|
+
self.state = value;
|
|
4249
|
+
},
|
|
4250
|
+
get sseTransport() {
|
|
4251
|
+
return self.sseTransport;
|
|
4252
|
+
},
|
|
4253
|
+
set sseTransport(value) {
|
|
4254
|
+
self.sseTransport = value;
|
|
4255
|
+
},
|
|
4256
|
+
disconnectListeners: this.disconnectListeners,
|
|
4257
|
+
get _tools() {
|
|
4258
|
+
return self._tools;
|
|
4259
|
+
},
|
|
4260
|
+
set _tools(value) {
|
|
4261
|
+
self._tools = value;
|
|
4262
|
+
},
|
|
4263
|
+
get _toolsCache() {
|
|
4264
|
+
return self._toolsCache;
|
|
4265
|
+
},
|
|
4266
|
+
set _toolsCache(value) {
|
|
4267
|
+
self._toolsCache = value;
|
|
4268
|
+
},
|
|
4269
|
+
toolsChangedListeners: this.toolsChangedListeners,
|
|
4270
|
+
emitCapabilityChanged: (...args) => this.emitCapabilityChanged(...args),
|
|
4271
|
+
emitResourceUpdated: (uri) => this.emitResourceUpdated(uri),
|
|
4272
|
+
get _serverMetadata() {
|
|
4273
|
+
return self._serverMetadata;
|
|
4274
|
+
},
|
|
4275
|
+
set _serverMetadata(value) {
|
|
4276
|
+
self._serverMetadata = value;
|
|
4277
|
+
},
|
|
4278
|
+
get httpTransport() {
|
|
4279
|
+
return self.httpTransport;
|
|
4280
|
+
},
|
|
4281
|
+
set httpTransport(value) {
|
|
4282
|
+
self.httpTransport = value;
|
|
4283
|
+
},
|
|
4284
|
+
serverRequests: this.serverRequests
|
|
4285
|
+
};
|
|
4286
|
+
}
|
|
3347
4287
|
};
|
|
3348
4288
|
|
|
4289
|
+
// src/config-env.ts
|
|
4290
|
+
var PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
4291
|
+
function mcpEnvPlaceholders(cfg) {
|
|
4292
|
+
const names = /* @__PURE__ */ new Set();
|
|
4293
|
+
const scan = (value) => {
|
|
4294
|
+
if (!value) return;
|
|
4295
|
+
for (const match of value.matchAll(PLACEHOLDER)) if (match[1]) names.add(match[1]);
|
|
4296
|
+
};
|
|
4297
|
+
scan(cfg.command);
|
|
4298
|
+
scan(cfg.url);
|
|
4299
|
+
for (const arg of cfg.args ?? []) scan(arg);
|
|
4300
|
+
for (const value of Object.values(cfg.env ?? {})) scan(value);
|
|
4301
|
+
for (const value of Object.values(cfg.headers ?? {})) scan(value);
|
|
4302
|
+
return [...names].sort();
|
|
4303
|
+
}
|
|
4304
|
+
function expandMcpEnvPlaceholders(cfg, env = process.env) {
|
|
4305
|
+
const missing = /* @__PURE__ */ new Set();
|
|
4306
|
+
const expand = (value) => value.replace(PLACEHOLDER, (_match, name, fallback) => {
|
|
4307
|
+
const resolved = env[name];
|
|
4308
|
+
if (resolved !== void 0 && resolved !== "") return resolved;
|
|
4309
|
+
if (fallback !== void 0) return fallback;
|
|
4310
|
+
missing.add(name);
|
|
4311
|
+
return "";
|
|
4312
|
+
});
|
|
4313
|
+
const mapValues = (record3) => record3 ? Object.fromEntries(Object.entries(record3).map(([key, value]) => [key, expand(value)])) : void 0;
|
|
4314
|
+
const expanded = {
|
|
4315
|
+
command: cfg.command === void 0 ? void 0 : expand(cfg.command),
|
|
4316
|
+
args: cfg.args?.map(expand),
|
|
4317
|
+
env: mapValues(cfg.env),
|
|
4318
|
+
url: cfg.url === void 0 ? void 0 : expand(cfg.url),
|
|
4319
|
+
headers: mapValues(cfg.headers)
|
|
4320
|
+
};
|
|
4321
|
+
if (missing.size > 0) {
|
|
4322
|
+
throw new Error(
|
|
4323
|
+
`MCP server "${cfg.name}" needs environment variable${missing.size === 1 ? "" : "s"} ${[
|
|
4324
|
+
...missing
|
|
4325
|
+
].join(", ")} (referenced as \${\u2026} in its config)`
|
|
4326
|
+
);
|
|
4327
|
+
}
|
|
4328
|
+
return expanded;
|
|
4329
|
+
}
|
|
4330
|
+
|
|
3349
4331
|
// src/content-selection.ts
|
|
3350
4332
|
var DEFAULT_MCP_INSERTION_MAX_BYTES = 256 * 1024;
|
|
3351
4333
|
var DEFAULT_MCP_RESOURCE_SCHEMES = [
|
|
@@ -3466,11 +4448,225 @@ function base64DecodedBytes(blob) {
|
|
|
3466
4448
|
function utf8Bytes(value) {
|
|
3467
4449
|
return new TextEncoder().encode(value).byteLength;
|
|
3468
4450
|
}
|
|
3469
|
-
function requireIdentity(value,
|
|
4451
|
+
function requireIdentity(value, label2) {
|
|
3470
4452
|
if (value.length === 0 || value.length > 256) {
|
|
3471
|
-
throw new Error(`MCP insertion ${
|
|
4453
|
+
throw new Error(`MCP insertion ${label2} must contain 1\u2013256 characters`);
|
|
4454
|
+
}
|
|
4455
|
+
}
|
|
4456
|
+
|
|
4457
|
+
// src/elicitation-form.ts
|
|
4458
|
+
import { randomUUID } from "node:crypto";
|
|
4459
|
+
import { hostname } from "node:os";
|
|
4460
|
+
import { sanitizeTerminalText as sanitizeTerminalText2 } from "@wrongstack/core/utils";
|
|
4461
|
+
var MAX_ATTEMPTS = 3;
|
|
4462
|
+
var CONFIRM_QUESTION = "confirm";
|
|
4463
|
+
var URL_QUESTION = "url";
|
|
4464
|
+
function questionId(index) {
|
|
4465
|
+
return `f${index}`;
|
|
4466
|
+
}
|
|
4467
|
+
function optionId(index) {
|
|
4468
|
+
return `o${index}`;
|
|
4469
|
+
}
|
|
4470
|
+
function bounds(min, max, unit = "") {
|
|
4471
|
+
if (min !== void 0 && max !== void 0) return `Between ${min} and ${max}${unit}.`;
|
|
4472
|
+
if (min !== void 0) return `At least ${min}${unit}.`;
|
|
4473
|
+
if (max !== void 0) return `At most ${max}${unit}.`;
|
|
4474
|
+
return void 0;
|
|
4475
|
+
}
|
|
4476
|
+
function describe(field) {
|
|
4477
|
+
const notes = [field.description];
|
|
4478
|
+
if (field.kind === "string") {
|
|
4479
|
+
notes.push(field.format ? `Format: ${field.format}.` : void 0);
|
|
4480
|
+
notes.push(bounds(field.minLength, field.maxLength, " characters"));
|
|
4481
|
+
} else if (field.kind === "number" || field.kind === "integer") {
|
|
4482
|
+
notes.push(field.kind === "integer" ? "A whole number." : "A number.");
|
|
4483
|
+
notes.push(bounds(field.minimum, field.maximum));
|
|
4484
|
+
} else if (field.kind === "multi-enum") {
|
|
4485
|
+
notes.push(bounds(field.minItems, field.maxItems, " choices"));
|
|
4486
|
+
}
|
|
4487
|
+
const text = notes.filter(Boolean).join(" ");
|
|
4488
|
+
return text || void 0;
|
|
4489
|
+
}
|
|
4490
|
+
function toQuestion(field, index) {
|
|
4491
|
+
const base = {
|
|
4492
|
+
id: questionId(index),
|
|
4493
|
+
// Titles arrive sanitized from the parser; a bare field name does not.
|
|
4494
|
+
prompt: field.title ?? sanitizeTerminalText2(field.name),
|
|
4495
|
+
description: describe(field),
|
|
4496
|
+
required: field.required
|
|
4497
|
+
};
|
|
4498
|
+
switch (field.kind) {
|
|
4499
|
+
case "string":
|
|
4500
|
+
return { ...base, kind: "text", recommendedText: field.default, placeholder: field.format };
|
|
4501
|
+
case "number":
|
|
4502
|
+
case "integer":
|
|
4503
|
+
return {
|
|
4504
|
+
...base,
|
|
4505
|
+
kind: "text",
|
|
4506
|
+
recommendedText: field.default === void 0 ? void 0 : String(field.default)
|
|
4507
|
+
};
|
|
4508
|
+
case "boolean":
|
|
4509
|
+
return {
|
|
4510
|
+
...base,
|
|
4511
|
+
kind: "single_select",
|
|
4512
|
+
options: [
|
|
4513
|
+
{ id: "yes", label: "Yes" },
|
|
4514
|
+
{ id: "no", label: "No" }
|
|
4515
|
+
],
|
|
4516
|
+
recommendedOptionIds: field.default === void 0 ? void 0 : [field.default ? "yes" : "no"]
|
|
4517
|
+
};
|
|
4518
|
+
case "enum":
|
|
4519
|
+
case "multi-enum": {
|
|
4520
|
+
const defaults = field.kind === "enum" ? [field.default] : field.default ?? [];
|
|
4521
|
+
const recommended = field.options.map((option, i) => defaults.includes(option.value) ? optionId(i) : void 0).filter((id) => id !== void 0);
|
|
4522
|
+
return {
|
|
4523
|
+
...base,
|
|
4524
|
+
kind: field.kind === "enum" ? "single_select" : "multi_select",
|
|
4525
|
+
options: field.options.map((option, i) => ({ id: optionId(i), label: option.label })),
|
|
4526
|
+
recommendedOptionIds: recommended.length > 0 ? recommended : void 0
|
|
4527
|
+
};
|
|
4528
|
+
}
|
|
3472
4529
|
}
|
|
3473
4530
|
}
|
|
4531
|
+
function pageWarnings(url) {
|
|
4532
|
+
const warnings = [];
|
|
4533
|
+
if (url.hostname.split(".").some((label2) => label2.startsWith("xn--"))) {
|
|
4534
|
+
warnings.push(
|
|
4535
|
+
"The address uses international characters (punycode); it may imitate another site."
|
|
4536
|
+
);
|
|
4537
|
+
}
|
|
4538
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
4539
|
+
if (url.protocol === "http:" && !local) {
|
|
4540
|
+
warnings.push("The page is not served over HTTPS.");
|
|
4541
|
+
}
|
|
4542
|
+
if (url.username || url.password) warnings.push("The address carries a user name or password.");
|
|
4543
|
+
return warnings;
|
|
4544
|
+
}
|
|
4545
|
+
function toPageRequest(request3, canOpen) {
|
|
4546
|
+
const url = new URL(request3.url);
|
|
4547
|
+
const description = [
|
|
4548
|
+
request3.message,
|
|
4549
|
+
`Page: ${request3.url}`,
|
|
4550
|
+
`Site: ${url.host}`,
|
|
4551
|
+
...pageWarnings(url).map((warning) => `Warning: ${warning}`),
|
|
4552
|
+
"What you do on that page goes to the server directly, not through WrongStack."
|
|
4553
|
+
].join("\n\n");
|
|
4554
|
+
return {
|
|
4555
|
+
id: `mcp-elicitation-${randomUUID()}`,
|
|
4556
|
+
title: `MCP server "${request3.server}" asks you to open ${url.host}`,
|
|
4557
|
+
description,
|
|
4558
|
+
submitLabel: "Continue",
|
|
4559
|
+
tabs: [
|
|
4560
|
+
{
|
|
4561
|
+
id: "page",
|
|
4562
|
+
label: request3.server,
|
|
4563
|
+
questions: [
|
|
4564
|
+
{
|
|
4565
|
+
id: URL_QUESTION,
|
|
4566
|
+
prompt: `Open ${url.host}?`,
|
|
4567
|
+
kind: "single_select",
|
|
4568
|
+
required: true,
|
|
4569
|
+
options: [
|
|
4570
|
+
// Named: with `wstack remote` this process runs on another machine.
|
|
4571
|
+
...canOpen ? [{ id: "open", label: `Open it in the browser on ${hostname()}` }] : [],
|
|
4572
|
+
{ id: "self", label: "I will open it myself" },
|
|
4573
|
+
{ id: "decline", label: "Decline" }
|
|
4574
|
+
]
|
|
4575
|
+
}
|
|
4576
|
+
]
|
|
4577
|
+
}
|
|
4578
|
+
]
|
|
4579
|
+
};
|
|
4580
|
+
}
|
|
4581
|
+
function toUserInputRequest(request3, problem) {
|
|
4582
|
+
const questions = request3.fields.length > 0 ? request3.fields.map(toQuestion) : [
|
|
4583
|
+
{
|
|
4584
|
+
id: CONFIRM_QUESTION,
|
|
4585
|
+
prompt: "Go ahead?",
|
|
4586
|
+
kind: "single_select",
|
|
4587
|
+
required: true,
|
|
4588
|
+
options: [
|
|
4589
|
+
{ id: "yes", label: "Yes" },
|
|
4590
|
+
{ id: "no", label: "No" }
|
|
4591
|
+
]
|
|
4592
|
+
}
|
|
4593
|
+
];
|
|
4594
|
+
const description = [
|
|
4595
|
+
request3.message,
|
|
4596
|
+
`Only share what you are comfortable sending to "${request3.server}".`,
|
|
4597
|
+
problem ? `The last answer was not accepted: ${problem}.` : void 0
|
|
4598
|
+
].filter(Boolean).join("\n\n");
|
|
4599
|
+
return {
|
|
4600
|
+
id: `mcp-elicitation-${randomUUID()}`,
|
|
4601
|
+
title: `MCP server "${request3.server}" is asking for input`,
|
|
4602
|
+
description,
|
|
4603
|
+
submitLabel: `Send to ${request3.server}`,
|
|
4604
|
+
tabs: [{ id: "form", label: request3.server, questions }]
|
|
4605
|
+
};
|
|
4606
|
+
}
|
|
4607
|
+
function answerValue(field, answer) {
|
|
4608
|
+
if (!answer || answer.delegated) return void 0;
|
|
4609
|
+
switch (field.kind) {
|
|
4610
|
+
case "string":
|
|
4611
|
+
return answer.text?.trim() ? answer.text : void 0;
|
|
4612
|
+
case "number":
|
|
4613
|
+
case "integer":
|
|
4614
|
+
return answer.text?.trim() || void 0;
|
|
4615
|
+
case "boolean": {
|
|
4616
|
+
const picked = answer.selectedOptionIds[0];
|
|
4617
|
+
return picked === "yes" ? true : picked === "no" ? false : void 0;
|
|
4618
|
+
}
|
|
4619
|
+
case "enum":
|
|
4620
|
+
case "multi-enum": {
|
|
4621
|
+
const values = answer.selectedOptionIds.map((id) => field.options[Number(id.slice(1))]?.value).filter((v) => v !== void 0);
|
|
4622
|
+
if (field.kind === "enum") return values[0];
|
|
4623
|
+
return values;
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
async function elicitViaUserInput(request3, fallback, options = {}) {
|
|
4628
|
+
const target = request3.requester ?? fallback;
|
|
4629
|
+
if (!target) return { action: "cancel" };
|
|
4630
|
+
if (request3.mode === "url") {
|
|
4631
|
+
if (request3.signal.aborted) return { action: "cancel" };
|
|
4632
|
+
const response = await target.requestUserInput(
|
|
4633
|
+
toPageRequest(request3, options.openUrl !== void 0),
|
|
4634
|
+
request3.signal
|
|
4635
|
+
);
|
|
4636
|
+
if (!response || request3.signal.aborted) return { action: "cancel" };
|
|
4637
|
+
if (response.status !== "submitted") return { action: "decline" };
|
|
4638
|
+
const choice = response.answers.find((a) => a.questionId === URL_QUESTION)?.selectedOptionIds[0];
|
|
4639
|
+
if (choice === "open" && options.openUrl) {
|
|
4640
|
+
options.openUrl(request3.url);
|
|
4641
|
+
return { action: "accept", content: {} };
|
|
4642
|
+
}
|
|
4643
|
+
if (choice === "self") return { action: "accept", content: {} };
|
|
4644
|
+
return { action: "decline" };
|
|
4645
|
+
}
|
|
4646
|
+
const form = request3;
|
|
4647
|
+
let problem;
|
|
4648
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
4649
|
+
if (request3.signal.aborted) return { action: "cancel" };
|
|
4650
|
+
const response = await target.requestUserInput(
|
|
4651
|
+
toUserInputRequest(form, problem),
|
|
4652
|
+
request3.signal
|
|
4653
|
+
);
|
|
4654
|
+
if (!response || request3.signal.aborted) return { action: "cancel" };
|
|
4655
|
+
if (response.status !== "submitted") return { action: "decline" };
|
|
4656
|
+
const byId = new Map(response.answers.map((answer) => [answer.questionId, answer]));
|
|
4657
|
+
if (form.fields.length === 0) {
|
|
4658
|
+
return byId.get(CONFIRM_QUESTION)?.selectedOptionIds[0] === "yes" ? { action: "accept", content: {} } : { action: "decline" };
|
|
4659
|
+
}
|
|
4660
|
+
const content = {};
|
|
4661
|
+
form.fields.forEach((field, i) => {
|
|
4662
|
+
content[field.name] = answerValue(field, byId.get(questionId(i)));
|
|
4663
|
+
});
|
|
4664
|
+
const checked = validateElicitationContent(form.fields, content);
|
|
4665
|
+
if (checked.ok) return { action: "accept", content: checked.value };
|
|
4666
|
+
problem = checked.error;
|
|
4667
|
+
}
|
|
4668
|
+
return { action: "cancel" };
|
|
4669
|
+
}
|
|
3474
4670
|
|
|
3475
4671
|
// src/manage.ts
|
|
3476
4672
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
@@ -3572,6 +4768,10 @@ function buildConfig(input, base) {
|
|
|
3572
4768
|
if (allowedTools !== void 0) cfg.allowedTools = allowedTools;
|
|
3573
4769
|
const permission = input.permission ?? base?.permission;
|
|
3574
4770
|
if (permission !== void 0) cfg.permission = permission;
|
|
4771
|
+
const startupTimeoutMs = input.startupTimeoutMs ?? base?.startupTimeoutMs;
|
|
4772
|
+
if (startupTimeoutMs !== void 0) cfg.startupTimeoutMs = startupTimeoutMs;
|
|
4773
|
+
const requestTimeoutMs = input.requestTimeoutMs ?? base?.requestTimeoutMs;
|
|
4774
|
+
if (requestTimeoutMs !== void 0) cfg.requestTimeoutMs = requestTimeoutMs;
|
|
3575
4775
|
const allowPrivateNetworks = input.allowPrivateNetworks ?? base?.allowPrivateNetworks;
|
|
3576
4776
|
if (allowPrivateNetworks !== void 0) cfg.allowPrivateNetworks = allowPrivateNetworks;
|
|
3577
4777
|
const enabled = input.enabled ?? base?.enabled;
|
|
@@ -3708,11 +4908,11 @@ async function disableMcp(name, deps) {
|
|
|
3708
4908
|
}
|
|
3709
4909
|
const cfg = servers[name];
|
|
3710
4910
|
if (!cfg) return { ok: false, message: `Server "${name}" is not in config.` };
|
|
3711
|
-
await safeStop(name, deps);
|
|
3712
4911
|
cfg.enabled = false;
|
|
3713
|
-
trackDisabled(deps.registry, { ...cfg, name });
|
|
3714
4912
|
servers[name] = cfg;
|
|
3715
4913
|
await persist(deps.configPath, full, servers);
|
|
4914
|
+
trackDisabled(deps.registry, { ...cfg, name });
|
|
4915
|
+
await safeStop(name, deps);
|
|
3716
4916
|
return {
|
|
3717
4917
|
ok: true,
|
|
3718
4918
|
message: `Server "${name}" disabled`,
|
|
@@ -4023,6 +5223,13 @@ function requireHttpServerConfig(servers, disabledServers, name) {
|
|
|
4023
5223
|
}
|
|
4024
5224
|
return cfg;
|
|
4025
5225
|
}
|
|
5226
|
+
async function loginRegistryAuthorization(manager, cfg, name, input) {
|
|
5227
|
+
return requireAuthorizationManager(manager).beginLogin({
|
|
5228
|
+
serverName: name,
|
|
5229
|
+
resource: cfg.url,
|
|
5230
|
+
...input
|
|
5231
|
+
});
|
|
5232
|
+
}
|
|
4026
5233
|
async function beginRegistryAuthorization(manager, cfg, name, input) {
|
|
4027
5234
|
return requireAuthorizationManager(manager).begin({
|
|
4028
5235
|
serverName: name,
|
|
@@ -4080,6 +5287,56 @@ import { expectDefined } from "@wrongstack/core/utils";
|
|
|
4080
5287
|
// src/wrap-tool.ts
|
|
4081
5288
|
import { ToolCapabilities } from "@wrongstack/core/security";
|
|
4082
5289
|
import { mcpQualifiedToolName } from "@wrongstack/core/utils";
|
|
5290
|
+
|
|
5291
|
+
// src/structured-result.ts
|
|
5292
|
+
import { validateAgainstSchema } from "@wrongstack/core/utils";
|
|
5293
|
+
function canonical(value) {
|
|
5294
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
5295
|
+
if (value && typeof value === "object") {
|
|
5296
|
+
const entries = Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`);
|
|
5297
|
+
return `{${entries.join(",")}}`;
|
|
5298
|
+
}
|
|
5299
|
+
return JSON.stringify(value) ?? "null";
|
|
5300
|
+
}
|
|
5301
|
+
function textBlocks(content) {
|
|
5302
|
+
if (typeof content === "string") return [content];
|
|
5303
|
+
if (!Array.isArray(content)) return [];
|
|
5304
|
+
return content.flatMap(
|
|
5305
|
+
(block) => block && typeof block === "object" && block.type === "text" ? [String(block.text ?? "")] : []
|
|
5306
|
+
);
|
|
5307
|
+
}
|
|
5308
|
+
function alreadyInText(content, structured) {
|
|
5309
|
+
const wanted = canonical(structured);
|
|
5310
|
+
return textBlocks(content).some((text) => {
|
|
5311
|
+
const trimmed = text.trim();
|
|
5312
|
+
if (!trimmed.startsWith("{")) return false;
|
|
5313
|
+
try {
|
|
5314
|
+
return canonical(JSON.parse(trimmed)) === wanted;
|
|
5315
|
+
} catch {
|
|
5316
|
+
return false;
|
|
5317
|
+
}
|
|
5318
|
+
});
|
|
5319
|
+
}
|
|
5320
|
+
function renderStructuredResult(rendered, content, structured, outputSchema) {
|
|
5321
|
+
if (!structured) return rendered;
|
|
5322
|
+
const parts = [rendered];
|
|
5323
|
+
if (!alreadyInText(content, structured)) {
|
|
5324
|
+
parts.push(`Structured result:
|
|
5325
|
+
${JSON.stringify(structured, null, 2)}`);
|
|
5326
|
+
}
|
|
5327
|
+
if (outputSchema) {
|
|
5328
|
+
const check = validateAgainstSchema(structured, outputSchema);
|
|
5329
|
+
if (!check.ok) {
|
|
5330
|
+
const problems = check.errors.slice(0, 3).map((e) => `${e.path || "(root)"}: ${e.message}`).join("; ");
|
|
5331
|
+
parts.push(
|
|
5332
|
+
`[The structured result does not match the tool's declared output schema \u2014 ${problems}]`
|
|
5333
|
+
);
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
return parts.filter((part) => part.length > 0).join("\n\n");
|
|
5337
|
+
}
|
|
5338
|
+
|
|
5339
|
+
// src/wrap-tool.ts
|
|
4083
5340
|
var MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;
|
|
4084
5341
|
function isMutatingTool(mcpTool) {
|
|
4085
5342
|
if (MUTATING_RE.test(mcpTool.name)) return true;
|
|
@@ -4106,24 +5363,49 @@ function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observ
|
|
|
4106
5363
|
inputSchema: mcpTool.inputSchema ?? { type: "object", properties: {} },
|
|
4107
5364
|
async execute(input, ctx, opts) {
|
|
4108
5365
|
const startedAt = Date.now();
|
|
4109
|
-
observer?.onStart();
|
|
5366
|
+
observer?.onStart(ctx);
|
|
4110
5367
|
let ok = false;
|
|
4111
5368
|
try {
|
|
4112
5369
|
const live = typeof client === "function" ? await client() : client;
|
|
4113
5370
|
const signal = opts?.signal ?? ctx?.signal;
|
|
4114
5371
|
const res = await live.callTool(mcpTool.name, input, signal ? { signal } : void 0);
|
|
5372
|
+
if (res.urlElicitations && res.urlElicitations.length > 0) {
|
|
5373
|
+
const outcomes = await live.presentUrlElicitations(
|
|
5374
|
+
res.urlElicitations,
|
|
5375
|
+
signal ?? new AbortController().signal
|
|
5376
|
+
);
|
|
5377
|
+
throw new Error(describeUrlOutcomes(serverName, qualifiedName, outcomes));
|
|
5378
|
+
}
|
|
4115
5379
|
if (res.isError) {
|
|
4116
5380
|
const errText = stringify(res.content);
|
|
4117
5381
|
throw new Error(errText || `MCP tool "${qualifiedName}" failed`);
|
|
4118
5382
|
}
|
|
4119
5383
|
ok = true;
|
|
4120
|
-
return
|
|
5384
|
+
return renderStructuredResult(
|
|
5385
|
+
stringify(res.content),
|
|
5386
|
+
res.content,
|
|
5387
|
+
res.structuredContent,
|
|
5388
|
+
mcpTool.outputSchema
|
|
5389
|
+
);
|
|
4121
5390
|
} finally {
|
|
4122
|
-
observer?.onFinish({ durationMs: Date.now() - startedAt, ok });
|
|
5391
|
+
observer?.onFinish({ durationMs: Date.now() - startedAt, ok }, ctx);
|
|
4123
5392
|
}
|
|
4124
5393
|
}
|
|
4125
5394
|
};
|
|
4126
5395
|
}
|
|
5396
|
+
function describeUrlOutcomes(serverName, toolName, outcomes) {
|
|
5397
|
+
const lines = outcomes.map(({ elicitation, action }) => {
|
|
5398
|
+
const site = new URL(elicitation.url).host;
|
|
5399
|
+
const choice = action === "accept" ? "the user agreed to open it" : action === "decline" ? "the user declined" : "no answer";
|
|
5400
|
+
return `- ${site}: ${elicitation.message} (${choice})`;
|
|
5401
|
+
});
|
|
5402
|
+
const agreed = outcomes.length > 0 && outcomes.every((o) => o.action === "accept");
|
|
5403
|
+
return [
|
|
5404
|
+
`MCP server "${serverName}" needs the user to finish a step in the browser before "${toolName}" can run:`,
|
|
5405
|
+
...lines,
|
|
5406
|
+
agreed ? "Ask the user to tell you when they have finished, then call the tool again." : "The user did not agree to every page, so do not call the tool again unless they ask."
|
|
5407
|
+
].join("\n");
|
|
5408
|
+
}
|
|
4127
5409
|
function renderContentBlock(item) {
|
|
4128
5410
|
const type = item["type"];
|
|
4129
5411
|
if (type === "text") return typeof item["text"] === "string" ? item["text"] : "";
|
|
@@ -4181,7 +5463,8 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
4181
5463
|
const clientArg = slot.lazy ? () => ctx.ensureConnected(slot.cfg.name) : expectDefined(client);
|
|
4182
5464
|
const wrapped = filtered.map(
|
|
4183
5465
|
(t) => wrapMCPTool(slot.cfg.name, t, clientArg, slot.cfg.permission ?? "confirm", {
|
|
4184
|
-
onStart: () => {
|
|
5466
|
+
onStart: (caller) => {
|
|
5467
|
+
if (caller) slot.activeCallers = [...slot.activeCallers ?? [], caller];
|
|
4185
5468
|
slot.operations.inFlightCalls++;
|
|
4186
5469
|
slot.operations.peakInFlightCalls = Math.max(
|
|
4187
5470
|
slot.operations.peakInFlightCalls,
|
|
@@ -4189,7 +5472,9 @@ function applySlotTools(ctx, slot, tools, client) {
|
|
|
4189
5472
|
);
|
|
4190
5473
|
ctx.recordOperation(slot, "call", "started", void 0, void 0, false);
|
|
4191
5474
|
},
|
|
4192
|
-
onFinish: ({ durationMs, ok }) => {
|
|
5475
|
+
onFinish: ({ durationMs, ok }, caller) => {
|
|
5476
|
+
const at = caller ? slot.activeCallers?.lastIndexOf(caller) ?? -1 : -1;
|
|
5477
|
+
if (at >= 0) slot.activeCallers?.splice(at, 1);
|
|
4193
5478
|
slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
|
|
4194
5479
|
slot.lastUsed = Date.now();
|
|
4195
5480
|
pushBounded(slot.operations.callSamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
@@ -4289,9 +5574,9 @@ async function persistSlotCapabilityManifest(cacheDir, slot) {
|
|
|
4289
5574
|
if (slot.manifestWrite === pending) slot.manifestWrite = void 0;
|
|
4290
5575
|
}
|
|
4291
5576
|
async function attemptConnectSlot(ctx, slot) {
|
|
4292
|
-
const
|
|
5577
|
+
const MAX_ATTEMPTS2 = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
|
|
4293
5578
|
let attempt = 0;
|
|
4294
|
-
while (attempt <
|
|
5579
|
+
while (attempt < MAX_ATTEMPTS2) {
|
|
4295
5580
|
if (ctx.servers.get(slot.cfg.name) !== slot) {
|
|
4296
5581
|
return;
|
|
4297
5582
|
}
|
|
@@ -4305,18 +5590,17 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
4305
5590
|
client = new MCPClient({
|
|
4306
5591
|
name: slot.cfg.name,
|
|
4307
5592
|
transport: slot.cfg.transport,
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
url: slot.cfg.url,
|
|
4312
|
-
headers: slot.cfg.headers,
|
|
5593
|
+
// `${VAR}` placeholders resolve here, per connect — never into the
|
|
5594
|
+
// stored config (see config-env.ts).
|
|
5595
|
+
...expandMcpEnvPlaceholders(slot.cfg),
|
|
4313
5596
|
bearerTokenEnv: slot.cfg.bearerTokenEnv,
|
|
4314
5597
|
startupTimeoutMs: slot.cfg.startupTimeoutMs,
|
|
4315
5598
|
requestTimeoutMs: slot.cfg.requestTimeoutMs,
|
|
4316
5599
|
cwd: ctx.cwd,
|
|
4317
5600
|
allowPrivateNetworks: slot.cfg.allowPrivateNetworks,
|
|
4318
5601
|
passthroughEnv: slot.cfg.passthroughEnv,
|
|
4319
|
-
authorizationProvider: ctx.authorizationProviderFactory?.(slot.cfg)
|
|
5602
|
+
authorizationProvider: ctx.authorizationProviderFactory?.(slot.cfg),
|
|
5603
|
+
elicitation: slotElicitation(ctx, slot)
|
|
4320
5604
|
});
|
|
4321
5605
|
if (slot.cfg.transport === "stdio") {
|
|
4322
5606
|
client.addExitListener(ctx.onChildExit);
|
|
@@ -4389,9 +5673,9 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
4389
5673
|
await client.close().catch(() => {
|
|
4390
5674
|
});
|
|
4391
5675
|
}
|
|
4392
|
-
if (attempt >=
|
|
5676
|
+
if (attempt >= MAX_ATTEMPTS2) {
|
|
4393
5677
|
ctx.log.error(
|
|
4394
|
-
`MCP server "${slot.cfg.name}" connect exhausted after ${
|
|
5678
|
+
`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS2} attempts`,
|
|
4395
5679
|
err
|
|
4396
5680
|
);
|
|
4397
5681
|
slot.state = "failed";
|
|
@@ -4403,7 +5687,8 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
4403
5687
|
slot.reconnectPending = false;
|
|
4404
5688
|
ctx.events.emit("mcp.server.disconnected", {
|
|
4405
5689
|
name: slot.cfg.name,
|
|
4406
|
-
reason: err instanceof Error ? err.message : "unknown"
|
|
5690
|
+
reason: err instanceof Error ? err.message : "unknown",
|
|
5691
|
+
terminal: true
|
|
4407
5692
|
});
|
|
4408
5693
|
return;
|
|
4409
5694
|
}
|
|
@@ -4415,6 +5700,11 @@ async function attemptConnectSlot(ctx, slot) {
|
|
|
4415
5700
|
}
|
|
4416
5701
|
}
|
|
4417
5702
|
}
|
|
5703
|
+
function slotElicitation(ctx, slot) {
|
|
5704
|
+
const handler = ctx.elicitationHandler;
|
|
5705
|
+
if (!handler) return void 0;
|
|
5706
|
+
return (form) => handler({ ...form, server: slot.cfg.name, requester: slot.activeCallers?.at(-1) });
|
|
5707
|
+
}
|
|
4418
5708
|
|
|
4419
5709
|
// src/registry-disconnect.ts
|
|
4420
5710
|
function resetDisconnectedSlotTools(slot, toolRegistry) {
|
|
@@ -4536,6 +5826,7 @@ async function sleepIdleSlot(ctx, slot) {
|
|
|
4536
5826
|
ctx.log.warn(`MCP server "${slot.cfg.name}" error during idle sleep close`, err);
|
|
4537
5827
|
}
|
|
4538
5828
|
}
|
|
5829
|
+
if (slot.client || slot.state !== "dormant") return;
|
|
4539
5830
|
slot.onDisconnect = void 0;
|
|
4540
5831
|
slot.operations.sleepCount++;
|
|
4541
5832
|
ctx.recordOperation(slot, "sleep", "idle-timeout");
|
|
@@ -4618,7 +5909,8 @@ function scheduleRegistryReconnect({
|
|
|
4618
5909
|
);
|
|
4619
5910
|
events.emit("mcp.server.disconnected", {
|
|
4620
5911
|
name: slot.cfg.name,
|
|
4621
|
-
reason: `reconnect-exhausted:${slot.reconnectCycles}
|
|
5912
|
+
reason: `reconnect-exhausted:${slot.reconnectCycles}`,
|
|
5913
|
+
terminal: true
|
|
4622
5914
|
});
|
|
4623
5915
|
return;
|
|
4624
5916
|
}
|
|
@@ -4651,7 +5943,10 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4651
5943
|
idleTimeoutMs;
|
|
4652
5944
|
authorizationProviderFactory;
|
|
4653
5945
|
authorizationManager;
|
|
5946
|
+
elicitationHandler;
|
|
4654
5947
|
operationListeners = /* @__PURE__ */ new Set();
|
|
5948
|
+
/** Starts still in flight, for {@link whenStarted}. */
|
|
5949
|
+
startups = /* @__PURE__ */ new Set();
|
|
4655
5950
|
/** Single shared idle sweep timer (started lazily; unref'd; cleared on stopAll). */
|
|
4656
5951
|
idleTimer;
|
|
4657
5952
|
constructor(opts) {
|
|
@@ -4664,12 +5959,17 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4664
5959
|
this.idleTimeoutMs = opts.idleTimeoutMs ?? MCP_CONSTANTS.IDLE.DEFAULT_TIMEOUT_MS;
|
|
4665
5960
|
this.authorizationProviderFactory = opts.authorizationProviderFactory;
|
|
4666
5961
|
this.authorizationManager = opts.authorizationManager;
|
|
5962
|
+
this.elicitationHandler = opts.elicitationHandler;
|
|
4667
5963
|
}
|
|
4668
5964
|
requireSlot(name) {
|
|
4669
5965
|
const slot = this.servers.get(name);
|
|
4670
5966
|
if (!slot) throw new Error(`MCP server "${name}" not registered`);
|
|
4671
5967
|
return slot;
|
|
4672
5968
|
}
|
|
5969
|
+
async loginAuthorization(name, input) {
|
|
5970
|
+
const cfg = this.requireHttpServerConfig(name);
|
|
5971
|
+
return loginRegistryAuthorization(this.authorizationManager, cfg, name, input);
|
|
5972
|
+
}
|
|
4673
5973
|
async beginAuthorization(name, input) {
|
|
4674
5974
|
const cfg = this.requireHttpServerConfig(name);
|
|
4675
5975
|
return beginRegistryAuthorization(this.authorizationManager, cfg, name, input);
|
|
@@ -4720,11 +6020,24 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4720
6020
|
operations: createMCPServerOperationState()
|
|
4721
6021
|
};
|
|
4722
6022
|
this.servers.set(cfg.name, slot);
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
6023
|
+
const startup = lazy ? this.startLazy(slot) : this.singleFlightConnect(slot);
|
|
6024
|
+
const settled = startup.then(
|
|
6025
|
+
() => {
|
|
6026
|
+
},
|
|
6027
|
+
() => {
|
|
6028
|
+
}
|
|
6029
|
+
);
|
|
6030
|
+
this.startups.add(settled);
|
|
6031
|
+
void settled.finally(() => this.startups.delete(settled));
|
|
6032
|
+
await startup;
|
|
6033
|
+
}
|
|
6034
|
+
/**
|
|
6035
|
+
* Resolves once every start already in flight has connected or failed. A
|
|
6036
|
+
* host that starts its servers in the background and then runs a single
|
|
6037
|
+
* turn waits here, so the turn's tool list includes theirs.
|
|
6038
|
+
*/
|
|
6039
|
+
async whenStarted() {
|
|
6040
|
+
while (this.startups.size > 0) await Promise.all([...this.startups]);
|
|
4728
6041
|
}
|
|
4729
6042
|
/** Record an intentionally disabled configuration without opening a transport. */
|
|
4730
6043
|
markDisabled(cfg) {
|
|
@@ -4876,10 +6189,13 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4876
6189
|
slot.reconnectTimer = void 0;
|
|
4877
6190
|
}
|
|
4878
6191
|
slot.state = "disconnected";
|
|
4879
|
-
|
|
6192
|
+
while (slot.client) {
|
|
4880
6193
|
const client = slot.client;
|
|
6194
|
+
slot.client = void 0;
|
|
6195
|
+
const handler = slot.onDisconnect;
|
|
6196
|
+
slot.onDisconnect = void 0;
|
|
4881
6197
|
client.removeExitListener?.(this.onChildExit);
|
|
4882
|
-
if (
|
|
6198
|
+
if (handler) client.removeDisconnectListener?.(handler);
|
|
4883
6199
|
client.removeToolsChangedListener?.(this.onToolsChanged);
|
|
4884
6200
|
this.removeCatalogListeners(client);
|
|
4885
6201
|
try {
|
|
@@ -4887,9 +6203,8 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
4887
6203
|
} catch (err) {
|
|
4888
6204
|
this.log.warn(`MCP server "${name}" error during stop close`, err);
|
|
4889
6205
|
}
|
|
4890
|
-
slot.
|
|
6206
|
+
slot.state = "disconnected";
|
|
4891
6207
|
}
|
|
4892
|
-
slot.onDisconnect = void 0;
|
|
4893
6208
|
slot.connecting = void 0;
|
|
4894
6209
|
resetDisconnectedSlotTools(slot, this.toolRegistry);
|
|
4895
6210
|
slot.registeredLazy = false;
|
|
@@ -5065,6 +6380,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
5065
6380
|
cacheDir: this.cacheDir,
|
|
5066
6381
|
cwd: this.cwd,
|
|
5067
6382
|
authorizationProviderFactory: this.authorizationProviderFactory,
|
|
6383
|
+
elicitationHandler: this.elicitationHandler,
|
|
5068
6384
|
operationListeners: this.operationListeners,
|
|
5069
6385
|
ensureConnected: (name) => this.ensureConnected(name),
|
|
5070
6386
|
recordOperation: (slot, kind, reason, failureKind, durationMs, retain) => this.recordOperation(slot, kind, reason, failureKind, durationMs, retain),
|
|
@@ -5190,6 +6506,10 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
5190
6506
|
void this.persistCapabilityManifest(slot);
|
|
5191
6507
|
this.log.info(`MCP server "${name}" resource catalog invalidated`);
|
|
5192
6508
|
};
|
|
6509
|
+
onResourceUpdated = (name, uri) => {
|
|
6510
|
+
if (!this.servers.has(name)) return;
|
|
6511
|
+
this.events.emit("mcp.resource.updated", { name, uri });
|
|
6512
|
+
};
|
|
5193
6513
|
onPromptsChanged = (name) => {
|
|
5194
6514
|
const slot = this.servers.get(name);
|
|
5195
6515
|
if (!slot) return;
|
|
@@ -5200,10 +6520,12 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
5200
6520
|
addCatalogListeners(client) {
|
|
5201
6521
|
client.addResourcesChangedListener(this.onResourcesChanged);
|
|
5202
6522
|
client.addPromptsChangedListener(this.onPromptsChanged);
|
|
6523
|
+
client.addResourceUpdatedListener?.(this.onResourceUpdated);
|
|
5203
6524
|
}
|
|
5204
6525
|
removeCatalogListeners(client) {
|
|
5205
6526
|
client.removeResourcesChangedListener?.(this.onResourcesChanged);
|
|
5206
6527
|
client.removePromptsChangedListener?.(this.onPromptsChanged);
|
|
6528
|
+
client.removeResourceUpdatedListener?.(this.onResourceUpdated);
|
|
5207
6529
|
}
|
|
5208
6530
|
onChildExit = (name, code, _signal) => {
|
|
5209
6531
|
const slot = this.servers.get(name);
|
|
@@ -5288,8 +6610,8 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
5288
6610
|
|
|
5289
6611
|
// src/server.ts
|
|
5290
6612
|
import { timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
5291
|
-
import { createServer } from "node:http";
|
|
5292
|
-
import { expectDefined as expectDefined3, toErrorMessage as toErrorMessage2, validateAgainstSchema } from "@wrongstack/core/utils";
|
|
6613
|
+
import { createServer as createServer2 } from "node:http";
|
|
6614
|
+
import { expectDefined as expectDefined3, toErrorMessage as toErrorMessage2, validateAgainstSchema as validateAgainstSchema2 } from "@wrongstack/core/utils";
|
|
5293
6615
|
var PARSE_ERROR = -32700;
|
|
5294
6616
|
var INVALID_REQUEST = -32600;
|
|
5295
6617
|
var METHOD_NOT_FOUND = -32601;
|
|
@@ -5482,7 +6804,7 @@ var MCPServer = class {
|
|
|
5482
6804
|
}
|
|
5483
6805
|
const schema = tools.find((tool) => tool.name === name)?.inputSchema;
|
|
5484
6806
|
if (!schema || typeof schema !== "object") return;
|
|
5485
|
-
const result =
|
|
6807
|
+
const result = validateAgainstSchema2(
|
|
5486
6808
|
args,
|
|
5487
6809
|
schema
|
|
5488
6810
|
);
|
|
@@ -5527,14 +6849,14 @@ function requiredParamString(params, field, method) {
|
|
|
5527
6849
|
}
|
|
5528
6850
|
return value;
|
|
5529
6851
|
}
|
|
5530
|
-
function stringRecord(value,
|
|
6852
|
+
function stringRecord(value, label2) {
|
|
5531
6853
|
if (value === void 0) return {};
|
|
5532
6854
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
5533
|
-
throw new Error(`${
|
|
6855
|
+
throw new Error(`${label2} must be an object`);
|
|
5534
6856
|
}
|
|
5535
6857
|
const result = {};
|
|
5536
6858
|
for (const [key, item] of Object.entries(value)) {
|
|
5537
|
-
if (typeof item !== "string") throw new Error(`${
|
|
6859
|
+
if (typeof item !== "string") throw new Error(`${label2}.${key} must be a string`);
|
|
5538
6860
|
result[key] = item;
|
|
5539
6861
|
}
|
|
5540
6862
|
return result;
|
|
@@ -5716,7 +7038,7 @@ function serveHttp(server, opts = {}) {
|
|
|
5716
7038
|
)
|
|
5717
7039
|
);
|
|
5718
7040
|
}
|
|
5719
|
-
const httpServer =
|
|
7041
|
+
const httpServer = createServer2((req, res) => {
|
|
5720
7042
|
void handleHttpRequest(server, req, res, token, log, host);
|
|
5721
7043
|
});
|
|
5722
7044
|
return new Promise((resolve, reject) => {
|
|
@@ -5792,8 +7114,8 @@ async function handleHttpRequest(server, req, res, token, log, boundHost) {
|
|
|
5792
7114
|
});
|
|
5793
7115
|
});
|
|
5794
7116
|
}
|
|
5795
|
-
function isLoopbackHostname(
|
|
5796
|
-
const bare =
|
|
7117
|
+
function isLoopbackHostname(hostname2) {
|
|
7118
|
+
const bare = hostname2.toLowerCase().replace(/^\[(.*)\]$/, "$1");
|
|
5797
7119
|
return bare === "localhost" || bare === "::1" || bare === "127.0.0.1" || bare.startsWith("127.");
|
|
5798
7120
|
}
|
|
5799
7121
|
function originIsAcceptable(req, boundHost) {
|
|
@@ -5920,13 +7242,15 @@ var MCPVaultTokenStore = class {
|
|
|
5920
7242
|
encryptEntry(value) {
|
|
5921
7243
|
const accessToken = this.vault.encrypt(value.tokenSet.accessToken);
|
|
5922
7244
|
const refreshToken = value.tokenSet.refreshToken ? this.vault.encrypt(value.tokenSet.refreshToken) : void 0;
|
|
5923
|
-
|
|
7245
|
+
const clientSecret = value.clientSecret ? this.vault.encrypt(value.clientSecret) : void 0;
|
|
7246
|
+
if (!this.vault.isEncrypted(accessToken) || refreshToken && !this.vault.isEncrypted(refreshToken) || clientSecret && !this.vault.isEncrypted(clientSecret)) {
|
|
5924
7247
|
throw new Error("MCP token store requires an encrypting SecretVault");
|
|
5925
7248
|
}
|
|
5926
7249
|
return {
|
|
5927
7250
|
serverName: value.serverName,
|
|
5928
7251
|
resource: value.resource,
|
|
5929
7252
|
clientId: value.clientId,
|
|
7253
|
+
clientSecret,
|
|
5930
7254
|
authorizationServer: value.authorizationServer,
|
|
5931
7255
|
accessToken,
|
|
5932
7256
|
refreshToken,
|
|
@@ -5937,13 +7261,14 @@ var MCPVaultTokenStore = class {
|
|
|
5937
7261
|
};
|
|
5938
7262
|
}
|
|
5939
7263
|
decryptEntry(entry) {
|
|
5940
|
-
if (!this.vault.isEncrypted(entry.accessToken) || entry.refreshToken !== void 0 && !this.vault.isEncrypted(entry.refreshToken)) {
|
|
7264
|
+
if (!this.vault.isEncrypted(entry.accessToken) || entry.refreshToken !== void 0 && !this.vault.isEncrypted(entry.refreshToken) || entry.clientSecret !== void 0 && !this.vault.isEncrypted(entry.clientSecret)) {
|
|
5941
7265
|
throw new Error("MCP token store contains an unencrypted token");
|
|
5942
7266
|
}
|
|
5943
7267
|
const value = {
|
|
5944
7268
|
serverName: entry.serverName,
|
|
5945
7269
|
resource: entry.resource,
|
|
5946
7270
|
clientId: entry.clientId,
|
|
7271
|
+
...entry.clientSecret !== void 0 ? { clientSecret: this.vault.decrypt(entry.clientSecret) } : {},
|
|
5947
7272
|
authorizationServer: entry.authorizationServer,
|
|
5948
7273
|
tokenSet: {
|
|
5949
7274
|
accessToken: this.vault.decrypt(entry.accessToken),
|
|
@@ -6014,6 +7339,7 @@ var MCPRefreshingAuthorizationProvider = class {
|
|
|
6014
7339
|
tokenSet = await refreshMcpAccessToken({
|
|
6015
7340
|
authorizationServer: state.authorizationServer,
|
|
6016
7341
|
clientId: state.clientId,
|
|
7342
|
+
clientSecret: state.clientSecret,
|
|
6017
7343
|
resource: state.resource,
|
|
6018
7344
|
refreshToken
|
|
6019
7345
|
});
|
|
@@ -6087,7 +7413,7 @@ function emptyFile() {
|
|
|
6087
7413
|
return { version: TOKEN_STORE_VERSION, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(), entries: [] };
|
|
6088
7414
|
}
|
|
6089
7415
|
function validateStoreFile(value) {
|
|
6090
|
-
if (!
|
|
7416
|
+
if (!isRecord2(value) || value["version"] !== TOKEN_STORE_VERSION || !Array.isArray(value["entries"])) {
|
|
6091
7417
|
throw new Error("MCP token store has an unsupported or malformed structure");
|
|
6092
7418
|
}
|
|
6093
7419
|
if (value["entries"].length > MAX_ENTRIES)
|
|
@@ -6099,7 +7425,7 @@ function validateStoreFile(value) {
|
|
|
6099
7425
|
};
|
|
6100
7426
|
}
|
|
6101
7427
|
function validateEncryptedEntry(value) {
|
|
6102
|
-
if (!
|
|
7428
|
+
if (!isRecord2(value)) throw new Error("MCP token store entry must be an object");
|
|
6103
7429
|
const resource = canonicalMcpResource(boundedString(value["resource"], "resource", 4096));
|
|
6104
7430
|
const authorizationServer = validateMcpAuthorizationServerMetadata(value["authorizationServer"]);
|
|
6105
7431
|
const scopes = stringArray(value["scopes"], "scopes", 128);
|
|
@@ -6111,6 +7437,7 @@ function validateEncryptedEntry(value) {
|
|
|
6111
7437
|
serverName: boundedString(value["serverName"], "serverName", 256),
|
|
6112
7438
|
resource,
|
|
6113
7439
|
clientId: boundedString(value["clientId"], "clientId", 4096),
|
|
7440
|
+
clientSecret: value["clientSecret"] === void 0 ? void 0 : boundedString(value["clientSecret"], "clientSecret", 32768),
|
|
6114
7441
|
authorizationServer,
|
|
6115
7442
|
accessToken: boundedString(value["accessToken"], "accessToken", 32768),
|
|
6116
7443
|
refreshToken: value["refreshToken"] === void 0 ? void 0 : boundedString(value["refreshToken"], "refreshToken", 32768),
|
|
@@ -6137,12 +7464,13 @@ function normalizeStoredAuthorization(value) {
|
|
|
6137
7464
|
serverName,
|
|
6138
7465
|
resource,
|
|
6139
7466
|
clientId: boundedString(value.clientId, "clientId", 4096),
|
|
7467
|
+
...value.clientSecret ? { clientSecret: boundedString(value.clientSecret, "clientSecret", 4096) } : {},
|
|
6140
7468
|
authorizationServer,
|
|
6141
7469
|
tokenSet,
|
|
6142
7470
|
updatedAt: boundedString(value.updatedAt, "updatedAt", 128)
|
|
6143
7471
|
};
|
|
6144
7472
|
}
|
|
6145
|
-
function
|
|
7473
|
+
function isRecord2(value) {
|
|
6146
7474
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6147
7475
|
}
|
|
6148
7476
|
function boundedString(value, field, maxLength) {
|
|
@@ -6182,10 +7510,12 @@ export {
|
|
|
6182
7510
|
disableMcp,
|
|
6183
7511
|
discoverMcp,
|
|
6184
7512
|
discoverMcpAuthorization,
|
|
7513
|
+
elicitViaUserInput,
|
|
6185
7514
|
enableMcp,
|
|
6186
7515
|
exchangeMcpAuthorizationCode,
|
|
6187
7516
|
listMcp,
|
|
6188
7517
|
manifestConfigHash,
|
|
7518
|
+
mcpEnvPlaceholders,
|
|
6189
7519
|
negotiateProtocolVersion,
|
|
6190
7520
|
parseAuthorizationServerMetadata,
|
|
6191
7521
|
parseGetPromptResult,
|
|
@@ -6203,10 +7533,12 @@ export {
|
|
|
6203
7533
|
readCapabilityManifest,
|
|
6204
7534
|
readManifest,
|
|
6205
7535
|
refreshMcpAccessToken,
|
|
7536
|
+
registerMcpOAuthClient,
|
|
6206
7537
|
removeMcp,
|
|
6207
7538
|
restartMcp,
|
|
6208
7539
|
serveHttp,
|
|
6209
7540
|
serveStdio,
|
|
7541
|
+
startMcpOAuthCallbackServer,
|
|
6210
7542
|
toContentBlocks,
|
|
6211
7543
|
updateMcp,
|
|
6212
7544
|
validateMcpAuthorizationServerMetadata,
|