@wrongstack/mcp 0.285.0 → 0.286.0
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-manager.d.ts +65 -0
- package/dist/authorization-manager.d.ts.map +1 -0
- package/dist/authorization.d.ts +128 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/client.d.ts +31 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/content-selection.d.ts +33 -0
- package/dist/content-selection.d.ts.map +1 -0
- package/dist/index.d.ts +13 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2955 -505
- package/dist/index.js.map +4 -4
- package/dist/manage.d.ts +3 -1
- package/dist/manage.d.ts.map +1 -1
- package/dist/manifest-cache.d.ts +15 -0
- package/dist/manifest-cache.d.ts.map +1 -1
- package/dist/operations.d.ts +100 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/protocol.d.ts +91 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/registry.d.ts +73 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/server.d.ts +25 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/token-store.d.ts +56 -0
- package/dist/token-store.d.ts.map +1 -0
- package/dist/transport.d.ts +26 -10
- package/dist/transport.d.ts.map +1 -1
- package/dist/wrap-tool.d.ts +8 -1
- package/dist/wrap-tool.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,742 @@
|
|
|
1
|
+
// src/authorization.ts
|
|
2
|
+
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import * as dns from "node:dns/promises";
|
|
4
|
+
import * as http from "node:http";
|
|
5
|
+
import * as https from "node:https";
|
|
6
|
+
import * as net from "node:net";
|
|
7
|
+
import { isPrivateIPv4, isPrivateIPv6 } from "@wrongstack/core/utils";
|
|
8
|
+
function canonicalMcpResource(rawUrl) {
|
|
9
|
+
let url;
|
|
10
|
+
try {
|
|
11
|
+
url = new URL(rawUrl);
|
|
12
|
+
} catch {
|
|
13
|
+
throw new Error("MCP authorization resource must be an absolute URL");
|
|
14
|
+
}
|
|
15
|
+
if (url.protocol !== "https:" && !isLoopbackHttp(url)) {
|
|
16
|
+
throw new Error("MCP authorization resource must use HTTPS (except loopback development)");
|
|
17
|
+
}
|
|
18
|
+
if (url.username || url.password || url.hash) {
|
|
19
|
+
throw new Error("MCP authorization resource must not contain credentials or a fragment");
|
|
20
|
+
}
|
|
21
|
+
if (url.pathname === "/" && !url.search) return url.origin;
|
|
22
|
+
return url.toString();
|
|
23
|
+
}
|
|
24
|
+
function authorizationHeaderForToken(token, expectedResource, now = Date.now()) {
|
|
25
|
+
if (canonicalMcpResource(token.resource) !== expectedResource) {
|
|
26
|
+
throw new Error("MCP access token resource does not match the target server");
|
|
27
|
+
}
|
|
28
|
+
if (token.expiresAt !== void 0 && token.expiresAt <= now) {
|
|
29
|
+
throw new Error("MCP access token is expired");
|
|
30
|
+
}
|
|
31
|
+
const tokenType = token.tokenType ?? "Bearer";
|
|
32
|
+
if (tokenType.toLowerCase() !== "bearer") {
|
|
33
|
+
throw new Error(`Unsupported MCP OAuth token type "${tokenType}"`);
|
|
34
|
+
}
|
|
35
|
+
if (!token.accessToken || token.accessToken.length > 16384 || /[\r\n]/.test(token.accessToken)) {
|
|
36
|
+
throw new Error("MCP access token is empty, oversized, or contains invalid characters");
|
|
37
|
+
}
|
|
38
|
+
return `Bearer ${token.accessToken}`;
|
|
39
|
+
}
|
|
40
|
+
function parseMcpBearerChallenge(header, resource) {
|
|
41
|
+
const challenge = {
|
|
42
|
+
status: 401,
|
|
43
|
+
resource,
|
|
44
|
+
scopes: [],
|
|
45
|
+
rawScheme: "Bearer"
|
|
46
|
+
};
|
|
47
|
+
if (!header) return challenge;
|
|
48
|
+
const bearer = /(?:^|,)\s*Bearer(?:\s+|$)/i.exec(header);
|
|
49
|
+
if (!bearer) return challenge;
|
|
50
|
+
const parameters = header.slice((bearer.index ?? 0) + bearer[0].length);
|
|
51
|
+
const resourceMetadata = challengeParameter(parameters, "resource_metadata");
|
|
52
|
+
if (resourceMetadata) {
|
|
53
|
+
const metadataUrl = validateMetadataUrl(resourceMetadata);
|
|
54
|
+
if (metadataUrl) challenge.resourceMetadataUrl = metadataUrl;
|
|
55
|
+
}
|
|
56
|
+
const scope = challengeParameter(parameters, "scope");
|
|
57
|
+
if (scope) {
|
|
58
|
+
challenge.scopes = [...new Set(scope.split(/\s+/).filter(Boolean))].slice(0, 64);
|
|
59
|
+
}
|
|
60
|
+
return challenge;
|
|
61
|
+
}
|
|
62
|
+
function protectedResourceMetadataUrls(resource) {
|
|
63
|
+
const url = new URL(canonicalMcpResource(resource));
|
|
64
|
+
const suffix = url.pathname === "/" ? "" : url.pathname;
|
|
65
|
+
const candidates = [
|
|
66
|
+
new URL(`/.well-known/oauth-protected-resource${suffix}`, url.origin).toString(),
|
|
67
|
+
new URL("/.well-known/oauth-protected-resource", url.origin).toString()
|
|
68
|
+
];
|
|
69
|
+
return [...new Set(candidates)];
|
|
70
|
+
}
|
|
71
|
+
function authorizationServerMetadataUrls(issuer) {
|
|
72
|
+
const url = secureOAuthUrl(issuer, "authorization server issuer");
|
|
73
|
+
const suffix = url.pathname === "/" ? "" : url.pathname;
|
|
74
|
+
const candidates = [
|
|
75
|
+
new URL(`/.well-known/oauth-authorization-server${suffix}`, url.origin).toString(),
|
|
76
|
+
new URL(`/.well-known/openid-configuration${suffix}`, url.origin).toString()
|
|
77
|
+
];
|
|
78
|
+
if (suffix) {
|
|
79
|
+
candidates.push(
|
|
80
|
+
new URL(
|
|
81
|
+
`${suffix.replace(/\/$/, "")}/.well-known/openid-configuration`,
|
|
82
|
+
url.origin
|
|
83
|
+
).toString()
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return candidates;
|
|
87
|
+
}
|
|
88
|
+
function parseProtectedResourceMetadata(value, expectedResource) {
|
|
89
|
+
const metadata = record(value, "protected resource metadata");
|
|
90
|
+
const resource = canonicalMcpResource(requiredString(metadata["resource"], "resource"));
|
|
91
|
+
if (resource !== canonicalMcpResource(expectedResource)) {
|
|
92
|
+
throw new Error("MCP protected resource metadata resource does not match the target server");
|
|
93
|
+
}
|
|
94
|
+
const authorizationServers = boundedStringArray(
|
|
95
|
+
metadata["authorization_servers"],
|
|
96
|
+
"authorization_servers",
|
|
97
|
+
8
|
|
98
|
+
).map((issuer) => secureOAuthUrl(issuer, "authorization server issuer").toString());
|
|
99
|
+
if (authorizationServers.length === 0) {
|
|
100
|
+
throw new Error("MCP protected resource metadata must declare an authorization server");
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
resource,
|
|
104
|
+
authorizationServers,
|
|
105
|
+
scopesSupported: optionalStringArray(metadata["scopes_supported"], "scopes_supported", 128)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function parseAuthorizationServerMetadata(value, expectedIssuer) {
|
|
109
|
+
const metadata = record(value, "authorization server metadata");
|
|
110
|
+
const issuer = secureOAuthUrl(requiredString(metadata["issuer"], "issuer"), "issuer").toString();
|
|
111
|
+
if (issuer !== secureOAuthUrl(expectedIssuer, "expected issuer").toString()) {
|
|
112
|
+
throw new Error("MCP authorization metadata issuer mismatch");
|
|
113
|
+
}
|
|
114
|
+
const methods = boundedStringArray(
|
|
115
|
+
metadata["code_challenge_methods_supported"],
|
|
116
|
+
"code_challenge_methods_supported",
|
|
117
|
+
16
|
|
118
|
+
);
|
|
119
|
+
if (!methods.includes("S256")) {
|
|
120
|
+
throw new Error("MCP authorization server does not advertise required PKCE S256 support");
|
|
121
|
+
}
|
|
122
|
+
const registration = optionalString(metadata["registration_endpoint"], "registration_endpoint");
|
|
123
|
+
return {
|
|
124
|
+
issuer,
|
|
125
|
+
authorizationEndpoint: secureOAuthUrl(
|
|
126
|
+
requiredString(metadata["authorization_endpoint"], "authorization_endpoint"),
|
|
127
|
+
"authorization endpoint"
|
|
128
|
+
).toString(),
|
|
129
|
+
tokenEndpoint: secureOAuthUrl(
|
|
130
|
+
requiredString(metadata["token_endpoint"], "token_endpoint"),
|
|
131
|
+
"token endpoint"
|
|
132
|
+
).toString(),
|
|
133
|
+
registrationEndpoint: registration ? secureOAuthUrl(registration, "registration endpoint").toString() : void 0,
|
|
134
|
+
scopesSupported: optionalStringArray(metadata["scopes_supported"], "scopes_supported", 128)
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function validateMcpAuthorizationServerMetadata(value) {
|
|
138
|
+
const metadata = record(value, "stored authorization server metadata");
|
|
139
|
+
const registration = optionalString(metadata["registrationEndpoint"], "registrationEndpoint");
|
|
140
|
+
return {
|
|
141
|
+
issuer: secureOAuthUrl(requiredString(metadata["issuer"], "issuer"), "issuer").toString(),
|
|
142
|
+
authorizationEndpoint: secureOAuthUrl(
|
|
143
|
+
requiredString(metadata["authorizationEndpoint"], "authorizationEndpoint"),
|
|
144
|
+
"authorization endpoint"
|
|
145
|
+
).toString(),
|
|
146
|
+
tokenEndpoint: secureOAuthUrl(
|
|
147
|
+
requiredString(metadata["tokenEndpoint"], "tokenEndpoint"),
|
|
148
|
+
"token endpoint"
|
|
149
|
+
).toString(),
|
|
150
|
+
registrationEndpoint: registration ? secureOAuthUrl(registration, "registration endpoint").toString() : void 0,
|
|
151
|
+
scopesSupported: optionalStringArray(metadata["scopesSupported"], "scopesSupported", 128)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function discoverMcpAuthorization(resource, options = {}) {
|
|
155
|
+
const canonicalResource = canonicalMcpResource(resource);
|
|
156
|
+
const resourceUrl = new URL(canonicalResource);
|
|
157
|
+
const allowedLoopbackHostname = isLoopbackHttp(resourceUrl) ? unbracket(resourceUrl.hostname).toLowerCase() : void 0;
|
|
158
|
+
const fetchJson = options.fetchJson ?? ((url, signal) => requestPinnedJson(url, {
|
|
159
|
+
signal,
|
|
160
|
+
timeoutMs: options.timeoutMs,
|
|
161
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
162
|
+
lookup: options.lookup,
|
|
163
|
+
allowedLoopbackHostname
|
|
164
|
+
}));
|
|
165
|
+
const challenge = parseMcpBearerChallenge(options.challengeHeader ?? null, canonicalResource);
|
|
166
|
+
const resourceCandidates = challenge.resourceMetadataUrl ? [challenge.resourceMetadataUrl] : protectedResourceMetadataUrls(canonicalResource);
|
|
167
|
+
const resourceDiscovery = await discoverFirst(
|
|
168
|
+
resourceCandidates,
|
|
169
|
+
fetchJson,
|
|
170
|
+
options.signal,
|
|
171
|
+
(value) => parseProtectedResourceMetadata(value, canonicalResource),
|
|
172
|
+
"protected resource metadata"
|
|
173
|
+
);
|
|
174
|
+
const issuer = resourceDiscovery.value.authorizationServers[0];
|
|
175
|
+
const authorizationDiscovery = await discoverFirst(
|
|
176
|
+
authorizationServerMetadataUrls(issuer),
|
|
177
|
+
fetchJson,
|
|
178
|
+
options.signal,
|
|
179
|
+
(value) => parseAuthorizationServerMetadata(value, issuer),
|
|
180
|
+
"authorization server metadata"
|
|
181
|
+
);
|
|
182
|
+
return {
|
|
183
|
+
resourceMetadataUrl: resourceDiscovery.url,
|
|
184
|
+
authorizationServerMetadataUrl: authorizationDiscovery.url,
|
|
185
|
+
protectedResource: resourceDiscovery.value,
|
|
186
|
+
authorizationServer: authorizationDiscovery.value
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function createMcpAuthorizationRequest(options) {
|
|
190
|
+
const resource = canonicalMcpResource(options.resource);
|
|
191
|
+
const clientId = boundedCredential(options.clientId, "client id");
|
|
192
|
+
const redirectUri = validateRedirectUri(options.redirectUri);
|
|
193
|
+
const scopes = validateScopes(options.scopes ?? []);
|
|
194
|
+
const codeVerifier = base64Url(randomBytes(32));
|
|
195
|
+
const codeChallenge = base64Url(createHash("sha256").update(codeVerifier).digest());
|
|
196
|
+
const state = base64Url(randomBytes(32));
|
|
197
|
+
const authorizationUrl = secureOAuthUrl(
|
|
198
|
+
options.authorizationServer.authorizationEndpoint,
|
|
199
|
+
"authorization endpoint"
|
|
200
|
+
);
|
|
201
|
+
authorizationUrl.searchParams.set("response_type", "code");
|
|
202
|
+
authorizationUrl.searchParams.set("client_id", clientId);
|
|
203
|
+
authorizationUrl.searchParams.set("redirect_uri", redirectUri);
|
|
204
|
+
authorizationUrl.searchParams.set("state", state);
|
|
205
|
+
authorizationUrl.searchParams.set("code_challenge", codeChallenge);
|
|
206
|
+
authorizationUrl.searchParams.set("code_challenge_method", "S256");
|
|
207
|
+
authorizationUrl.searchParams.set("resource", resource);
|
|
208
|
+
if (scopes.length > 0) authorizationUrl.searchParams.set("scope", scopes.join(" "));
|
|
209
|
+
return {
|
|
210
|
+
authorizationUrl: authorizationUrl.toString(),
|
|
211
|
+
state,
|
|
212
|
+
codeVerifier,
|
|
213
|
+
redirectUri,
|
|
214
|
+
clientId,
|
|
215
|
+
resource
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function parseMcpAuthorizationCallback(callbackUrl, session) {
|
|
219
|
+
let callback;
|
|
220
|
+
try {
|
|
221
|
+
callback = new URL(callbackUrl);
|
|
222
|
+
} catch {
|
|
223
|
+
throw new Error("MCP OAuth callback must be an absolute URL");
|
|
224
|
+
}
|
|
225
|
+
const expected = new URL(validateRedirectUri(session.redirectUri));
|
|
226
|
+
if (callback.protocol !== expected.protocol || callback.hostname !== expected.hostname || callback.port !== expected.port || callback.pathname !== expected.pathname) {
|
|
227
|
+
throw new Error("MCP OAuth callback redirect URI does not match the authorization session");
|
|
228
|
+
}
|
|
229
|
+
const returnedState = callback.searchParams.get("state") ?? "";
|
|
230
|
+
if (!constantTimeEqual(returnedState, session.state)) {
|
|
231
|
+
throw new Error("MCP OAuth callback state mismatch");
|
|
232
|
+
}
|
|
233
|
+
const oauthError = callback.searchParams.get("error");
|
|
234
|
+
if (oauthError)
|
|
235
|
+
throw new Error(`MCP OAuth authorization failed: ${boundedErrorCode(oauthError)}`);
|
|
236
|
+
return boundedCredential(callback.searchParams.get("code") ?? "", "authorization code");
|
|
237
|
+
}
|
|
238
|
+
async function exchangeMcpAuthorizationCode(options) {
|
|
239
|
+
const resource = canonicalMcpResource(options.resource);
|
|
240
|
+
const body = new URLSearchParams({
|
|
241
|
+
grant_type: "authorization_code",
|
|
242
|
+
code: boundedCredential(options.code, "authorization code"),
|
|
243
|
+
client_id: boundedCredential(options.clientId, "client id"),
|
|
244
|
+
redirect_uri: validateRedirectUri(options.redirectUri),
|
|
245
|
+
code_verifier: validateCodeVerifier(options.codeVerifier),
|
|
246
|
+
resource
|
|
247
|
+
}).toString();
|
|
248
|
+
const response = await requestPinnedJson(options.authorizationServer.tokenEndpoint, {
|
|
249
|
+
method: "POST",
|
|
250
|
+
body,
|
|
251
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
252
|
+
signal: options.signal,
|
|
253
|
+
timeoutMs: options.timeoutMs,
|
|
254
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
255
|
+
lookup: options.lookup,
|
|
256
|
+
allowedLoopbackHostname: loopbackHostnameForResource(resource)
|
|
257
|
+
});
|
|
258
|
+
if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
|
|
259
|
+
return parseTokenResponse(response, resource);
|
|
260
|
+
}
|
|
261
|
+
async function refreshMcpAccessToken(options) {
|
|
262
|
+
const resource = canonicalMcpResource(options.resource);
|
|
263
|
+
const previousRefreshToken = boundedCredential(options.refreshToken, "refresh token");
|
|
264
|
+
const body = new URLSearchParams({
|
|
265
|
+
grant_type: "refresh_token",
|
|
266
|
+
refresh_token: previousRefreshToken,
|
|
267
|
+
client_id: boundedCredential(options.clientId, "client id"),
|
|
268
|
+
resource
|
|
269
|
+
}).toString();
|
|
270
|
+
const response = await requestPinnedJson(options.authorizationServer.tokenEndpoint, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
body,
|
|
273
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
274
|
+
signal: options.signal,
|
|
275
|
+
timeoutMs: options.timeoutMs,
|
|
276
|
+
maxResponseBytes: options.maxResponseBytes,
|
|
277
|
+
lookup: options.lookup,
|
|
278
|
+
allowedLoopbackHostname: loopbackHostnameForResource(resource)
|
|
279
|
+
});
|
|
280
|
+
if (response === void 0) throw new Error("MCP OAuth token endpoint returned no response");
|
|
281
|
+
const parsed = parseTokenResponse(response, resource);
|
|
282
|
+
return { ...parsed, refreshToken: parsed.refreshToken ?? previousRefreshToken };
|
|
283
|
+
}
|
|
284
|
+
function challengeParameter(parameters, name) {
|
|
285
|
+
const pattern = new RegExp(
|
|
286
|
+
`(?:^|,)\\s*${name}\\s*=\\s*(?:"((?:\\\\.|[^"\\\\])*)"|([^,\\s]+))`,
|
|
287
|
+
"i"
|
|
288
|
+
);
|
|
289
|
+
const match = pattern.exec(parameters);
|
|
290
|
+
const value = match?.[1] ?? match?.[2];
|
|
291
|
+
return value?.replace(/\\(["\\])/g, "$1");
|
|
292
|
+
}
|
|
293
|
+
function validateMetadataUrl(value) {
|
|
294
|
+
try {
|
|
295
|
+
const url = new URL(value);
|
|
296
|
+
if (url.username || url.password || url.hash) return void 0;
|
|
297
|
+
if (url.protocol !== "https:" && !isLoopbackHttp(url)) return void 0;
|
|
298
|
+
return url.toString();
|
|
299
|
+
} catch {
|
|
300
|
+
return void 0;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function record(value, label) {
|
|
304
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
305
|
+
throw new Error(`MCP ${label} must be an object`);
|
|
306
|
+
}
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
function requiredString(value, field) {
|
|
310
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096) {
|
|
311
|
+
throw new Error(`MCP authorization field "${field}" must be a bounded non-empty string`);
|
|
312
|
+
}
|
|
313
|
+
return value;
|
|
314
|
+
}
|
|
315
|
+
function optionalString(value, field) {
|
|
316
|
+
return value === void 0 ? void 0 : requiredString(value, field);
|
|
317
|
+
}
|
|
318
|
+
function boundedStringArray(value, field, maxItems) {
|
|
319
|
+
if (!Array.isArray(value) || value.length > maxItems) {
|
|
320
|
+
throw new Error(`MCP authorization field "${field}" must be an array of at most ${maxItems}`);
|
|
321
|
+
}
|
|
322
|
+
return [...new Set(value.map((entry) => requiredString(entry, field)))];
|
|
323
|
+
}
|
|
324
|
+
function optionalStringArray(value, field, maxItems) {
|
|
325
|
+
return value === void 0 ? [] : boundedStringArray(value, field, maxItems);
|
|
326
|
+
}
|
|
327
|
+
function secureOAuthUrl(value, label) {
|
|
328
|
+
let url;
|
|
329
|
+
try {
|
|
330
|
+
url = new URL(value);
|
|
331
|
+
} catch {
|
|
332
|
+
throw new Error(`MCP ${label} must be an absolute URL`);
|
|
333
|
+
}
|
|
334
|
+
if (url.protocol !== "https:" && !isLoopbackHttp(url)) {
|
|
335
|
+
throw new Error(`MCP ${label} must use HTTPS (except loopback development)`);
|
|
336
|
+
}
|
|
337
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
338
|
+
throw new Error(`MCP ${label} must not contain credentials, query, or fragment components`);
|
|
339
|
+
}
|
|
340
|
+
if (url.pathname === "/") return new URL(url.origin);
|
|
341
|
+
return url;
|
|
342
|
+
}
|
|
343
|
+
async function discoverFirst(candidates, fetchJson, signal, parse, label) {
|
|
344
|
+
const failures = [];
|
|
345
|
+
for (const candidate of candidates) {
|
|
346
|
+
signal?.throwIfAborted();
|
|
347
|
+
try {
|
|
348
|
+
const value = await fetchJson(candidate, signal);
|
|
349
|
+
if (value === void 0) {
|
|
350
|
+
failures.push(`${candidate}: not found`);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
return { url: candidate, value: parse(value) };
|
|
354
|
+
} catch (error) {
|
|
355
|
+
signal?.throwIfAborted();
|
|
356
|
+
failures.push(`${candidate}: ${error instanceof Error ? error.message : String(error)}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
throw new Error(`MCP ${label} discovery failed (${failures.join("; ")})`);
|
|
360
|
+
}
|
|
361
|
+
async function requestPinnedJson(rawUrl, options) {
|
|
362
|
+
const url = secureOAuthUrl(rawUrl, "discovery URL");
|
|
363
|
+
const target = await resolvePinnedAddress(url, options);
|
|
364
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
365
|
+
const maxBytes = options.maxResponseBytes ?? 64 * 1024;
|
|
366
|
+
options.signal?.throwIfAborted();
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
let settled = false;
|
|
369
|
+
const finish = (error, value) => {
|
|
370
|
+
if (settled) return;
|
|
371
|
+
settled = true;
|
|
372
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
373
|
+
if (error) reject(error);
|
|
374
|
+
else resolve(value);
|
|
375
|
+
};
|
|
376
|
+
const onAbort = () => {
|
|
377
|
+
request3.destroy(options.signal?.reason instanceof Error ? options.signal.reason : void 0);
|
|
378
|
+
};
|
|
379
|
+
const headers = {
|
|
380
|
+
accept: "application/json",
|
|
381
|
+
host: url.host,
|
|
382
|
+
...options.headers
|
|
383
|
+
};
|
|
384
|
+
if (options.body !== void 0) {
|
|
385
|
+
headers["content-length"] = Buffer.byteLength(options.body);
|
|
386
|
+
}
|
|
387
|
+
const requestOptions = {
|
|
388
|
+
host: target.address,
|
|
389
|
+
family: target.family,
|
|
390
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
|
|
391
|
+
method: options.method ?? "GET",
|
|
392
|
+
path: `${url.pathname}${url.search}`,
|
|
393
|
+
headers,
|
|
394
|
+
...url.protocol === "https:" && net.isIP(unbracket(url.hostname)) === 0 ? { servername: unbracket(url.hostname) } : {}
|
|
395
|
+
};
|
|
396
|
+
const requestFn = url.protocol === "https:" ? https.request : http.request;
|
|
397
|
+
const request3 = requestFn(requestOptions, (response) => {
|
|
398
|
+
const status = response.statusCode ?? 0;
|
|
399
|
+
if (status === 404 || status === 410) {
|
|
400
|
+
response.resume();
|
|
401
|
+
finish(void 0, void 0);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (status >= 300 && status < 400) {
|
|
405
|
+
response.resume();
|
|
406
|
+
finish(new Error("MCP OAuth discovery redirects are not allowed"));
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (status < 200 || status >= 300) {
|
|
410
|
+
response.resume();
|
|
411
|
+
finish(new Error(`MCP OAuth discovery HTTP ${status}`));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const contentType = response.headers["content-type"] ?? "";
|
|
415
|
+
if (!/^(?:application\/json|[^;]+\+json)(?:;|$)/i.test(contentType)) {
|
|
416
|
+
response.resume();
|
|
417
|
+
finish(new Error("MCP OAuth discovery response must be JSON"));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const declaredLength = Number(response.headers["content-length"] ?? 0);
|
|
421
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
|
|
422
|
+
response.destroy();
|
|
423
|
+
finish(new Error(`MCP OAuth discovery response exceeds ${maxBytes} bytes`));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const chunks = [];
|
|
427
|
+
let size = 0;
|
|
428
|
+
response.on("data", (chunk) => {
|
|
429
|
+
size += chunk.length;
|
|
430
|
+
if (size > maxBytes) {
|
|
431
|
+
response.destroy();
|
|
432
|
+
finish(new Error(`MCP OAuth discovery response exceeds ${maxBytes} bytes`));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
chunks.push(chunk);
|
|
436
|
+
});
|
|
437
|
+
response.once("end", () => {
|
|
438
|
+
try {
|
|
439
|
+
finish(void 0, JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
440
|
+
} catch {
|
|
441
|
+
finish(new Error("MCP OAuth discovery response is not valid JSON"));
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
response.once("error", (error) => finish(error));
|
|
445
|
+
});
|
|
446
|
+
request3.setTimeout(timeoutMs, () => {
|
|
447
|
+
request3.destroy(new Error(`MCP OAuth discovery timed out after ${timeoutMs}ms`));
|
|
448
|
+
});
|
|
449
|
+
request3.once("error", (error) => finish(error));
|
|
450
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
451
|
+
request3.end(options.body);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async function resolvePinnedAddress(url, options) {
|
|
455
|
+
const hostname = unbracket(url.hostname).toLowerCase();
|
|
456
|
+
const literalFamily = net.isIP(hostname);
|
|
457
|
+
if (literalFamily === 4 || literalFamily === 6) {
|
|
458
|
+
assertDiscoveryAddressAllowed(
|
|
459
|
+
hostname,
|
|
460
|
+
literalFamily,
|
|
461
|
+
hostname,
|
|
462
|
+
options.allowedLoopbackHostname
|
|
463
|
+
);
|
|
464
|
+
return { address: hostname, family: literalFamily };
|
|
465
|
+
}
|
|
466
|
+
const lookup2 = options.lookup ?? ((host) => dns.lookup(host, { all: true }));
|
|
467
|
+
const records = await lookup2(hostname);
|
|
468
|
+
if (records.length === 0)
|
|
469
|
+
throw new Error(`MCP OAuth discovery DNS returned no addresses for ${hostname}`);
|
|
470
|
+
for (const record3 of records) {
|
|
471
|
+
if (record3.family !== 4 && record3.family !== 6) {
|
|
472
|
+
throw new Error("MCP OAuth discovery DNS returned an unsupported address family");
|
|
473
|
+
}
|
|
474
|
+
assertDiscoveryAddressAllowed(
|
|
475
|
+
record3.address,
|
|
476
|
+
record3.family,
|
|
477
|
+
hostname,
|
|
478
|
+
options.allowedLoopbackHostname
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
const selected = records[0];
|
|
482
|
+
return { address: selected.address, family: selected.family };
|
|
483
|
+
}
|
|
484
|
+
function assertDiscoveryAddressAllowed(address, family, hostname, allowedLoopbackHostname) {
|
|
485
|
+
const isPrivate = family === 4 ? isPrivateIPv4(address) : isPrivateIPv6(address);
|
|
486
|
+
if (!isPrivate) return;
|
|
487
|
+
const loopback = family === 4 ? address.startsWith("127.") : address === "::1";
|
|
488
|
+
if (loopback && hostname === allowedLoopbackHostname) return;
|
|
489
|
+
throw new Error(`MCP OAuth discovery blocked private address ${address}`);
|
|
490
|
+
}
|
|
491
|
+
function unbracket(hostname) {
|
|
492
|
+
return hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
493
|
+
}
|
|
494
|
+
function validateRedirectUri(value) {
|
|
495
|
+
let url;
|
|
496
|
+
try {
|
|
497
|
+
url = new URL(value);
|
|
498
|
+
} catch {
|
|
499
|
+
throw new Error("MCP OAuth redirect URI must be an absolute URL");
|
|
500
|
+
}
|
|
501
|
+
if (url.protocol !== "https:" && !isLoopbackHttp(url)) {
|
|
502
|
+
throw new Error("MCP OAuth redirect URI must use HTTPS or loopback HTTP");
|
|
503
|
+
}
|
|
504
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
505
|
+
throw new Error("MCP OAuth redirect URI must not contain credentials, query, or fragment");
|
|
506
|
+
}
|
|
507
|
+
return url.toString();
|
|
508
|
+
}
|
|
509
|
+
function validateScopes(scopes) {
|
|
510
|
+
if (scopes.length > 128) throw new Error("MCP OAuth scope list exceeds 128 entries");
|
|
511
|
+
const normalized = scopes.map((scope) => {
|
|
512
|
+
if (!scope || scope.length > 256 || /\s/.test(scope)) {
|
|
513
|
+
throw new Error("MCP OAuth scopes must be bounded non-empty tokens");
|
|
514
|
+
}
|
|
515
|
+
return scope;
|
|
516
|
+
});
|
|
517
|
+
return [...new Set(normalized)];
|
|
518
|
+
}
|
|
519
|
+
function validateCodeVerifier(value) {
|
|
520
|
+
if (value.length < 43 || value.length > 128 || !/^[A-Za-z0-9._~-]+$/.test(value)) {
|
|
521
|
+
throw new Error("MCP OAuth PKCE code verifier is invalid");
|
|
522
|
+
}
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
function boundedCredential(value, label) {
|
|
526
|
+
if (!value || value.length > 16384 || /[\r\n]/.test(value)) {
|
|
527
|
+
throw new Error(`MCP OAuth ${label} is empty, oversized, or invalid`);
|
|
528
|
+
}
|
|
529
|
+
return value;
|
|
530
|
+
}
|
|
531
|
+
function boundedErrorCode(value) {
|
|
532
|
+
return /^[A-Za-z0-9._-]{1,128}$/.test(value) ? value : "invalid_error";
|
|
533
|
+
}
|
|
534
|
+
function base64Url(value) {
|
|
535
|
+
return Buffer.from(value).toString("base64url");
|
|
536
|
+
}
|
|
537
|
+
function constantTimeEqual(left, right) {
|
|
538
|
+
const leftHash = createHash("sha256").update(left).digest();
|
|
539
|
+
const rightHash = createHash("sha256").update(right).digest();
|
|
540
|
+
return timingSafeEqual(leftHash, rightHash);
|
|
541
|
+
}
|
|
542
|
+
function parseTokenResponse(value, resource) {
|
|
543
|
+
const response = record(value, "token response");
|
|
544
|
+
const accessToken = boundedCredential(
|
|
545
|
+
requiredString(response["access_token"], "access_token"),
|
|
546
|
+
"access token"
|
|
547
|
+
);
|
|
548
|
+
const tokenType = optionalString(response["token_type"], "token_type") ?? "Bearer";
|
|
549
|
+
if (tokenType.toLowerCase() !== "bearer") {
|
|
550
|
+
throw new Error(`Unsupported MCP OAuth token type "${tokenType}"`);
|
|
551
|
+
}
|
|
552
|
+
const expiresIn = response["expires_in"];
|
|
553
|
+
let expiresAt;
|
|
554
|
+
if (expiresIn !== void 0) {
|
|
555
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0 || expiresIn > 31536e3) {
|
|
556
|
+
throw new Error("MCP OAuth expires_in must be between 1 second and 1 year");
|
|
557
|
+
}
|
|
558
|
+
expiresAt = Date.now() + Math.floor(expiresIn * 1e3);
|
|
559
|
+
}
|
|
560
|
+
const refresh = optionalString(response["refresh_token"], "refresh_token");
|
|
561
|
+
const scope = optionalString(response["scope"], "scope");
|
|
562
|
+
const token = {
|
|
563
|
+
accessToken,
|
|
564
|
+
tokenType: "Bearer",
|
|
565
|
+
resource,
|
|
566
|
+
scopes: scope ? validateScopes(scope.split(/\s+/).filter(Boolean)) : [],
|
|
567
|
+
...expiresAt !== void 0 ? { expiresAt } : {},
|
|
568
|
+
...refresh ? { refreshToken: boundedCredential(refresh, "refresh token") } : {}
|
|
569
|
+
};
|
|
570
|
+
authorizationHeaderForToken(token, resource);
|
|
571
|
+
return token;
|
|
572
|
+
}
|
|
573
|
+
function loopbackHostnameForResource(resource) {
|
|
574
|
+
const url = new URL(resource);
|
|
575
|
+
return isLoopbackHttp(url) ? unbracket(url.hostname).toLowerCase() : void 0;
|
|
576
|
+
}
|
|
577
|
+
function isLoopbackHttp(url) {
|
|
578
|
+
if (url.protocol !== "http:") return false;
|
|
579
|
+
return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/authorization-manager.ts
|
|
583
|
+
var DEFAULT_PENDING_TTL_MS = 10 * 6e4;
|
|
584
|
+
var MAX_PENDING_AUTHORIZATIONS = 32;
|
|
585
|
+
var MCPAuthorizationManager = class {
|
|
586
|
+
constructor(options) {
|
|
587
|
+
this.options = options;
|
|
588
|
+
this.pendingTtlMs = options.pendingTtlMs ?? DEFAULT_PENDING_TTL_MS;
|
|
589
|
+
if (!Number.isFinite(this.pendingTtlMs) || this.pendingTtlMs <= 0) {
|
|
590
|
+
throw new Error("MCP authorization pending TTL must be a positive finite number");
|
|
591
|
+
}
|
|
592
|
+
this.discover = options.discover ?? discoverMcpAuthorization;
|
|
593
|
+
this.exchange = options.exchange ?? exchangeMcpAuthorizationCode;
|
|
594
|
+
this.now = options.now ?? Date.now;
|
|
595
|
+
}
|
|
596
|
+
options;
|
|
597
|
+
pending = /* @__PURE__ */ new Map();
|
|
598
|
+
pendingTtlMs;
|
|
599
|
+
discover;
|
|
600
|
+
exchange;
|
|
601
|
+
now;
|
|
602
|
+
async begin(input) {
|
|
603
|
+
const resource = canonicalMcpResource(input.resource);
|
|
604
|
+
const key = authorizationKey(input.serverName, resource);
|
|
605
|
+
this.pruneExpired();
|
|
606
|
+
if (!this.pending.has(key) && this.pending.size >= MAX_PENDING_AUTHORIZATIONS) {
|
|
607
|
+
throw new Error("Too many pending MCP authorization sessions");
|
|
608
|
+
}
|
|
609
|
+
const discovery = await this.discover(resource, {
|
|
610
|
+
challengeHeader: input.challengeHeader,
|
|
611
|
+
signal: input.signal
|
|
612
|
+
});
|
|
613
|
+
const challengeScopes = parseMcpBearerChallenge(input.challengeHeader ?? null, resource).scopes;
|
|
614
|
+
const scopes = input.scopes ? [...input.scopes] : challengeScopes;
|
|
615
|
+
const session = createMcpAuthorizationRequest({
|
|
616
|
+
authorizationServer: discovery.authorizationServer,
|
|
617
|
+
clientId: input.clientId,
|
|
618
|
+
redirectUri: input.redirectUri,
|
|
619
|
+
resource,
|
|
620
|
+
scopes
|
|
621
|
+
});
|
|
622
|
+
const normalizedScopes = new URL(session.authorizationUrl).searchParams.get("scope")?.split(" ").filter(Boolean) ?? [];
|
|
623
|
+
const expiresAt = this.now() + this.pendingTtlMs;
|
|
624
|
+
this.pending.set(key, { session, discovery, scopes: normalizedScopes, expiresAt });
|
|
625
|
+
return {
|
|
626
|
+
serverName: boundedServerName(input.serverName),
|
|
627
|
+
resource,
|
|
628
|
+
authorizationUrl: session.authorizationUrl,
|
|
629
|
+
redirectUri: session.redirectUri,
|
|
630
|
+
scopes: [...normalizedScopes],
|
|
631
|
+
expiresAt
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
async complete(input) {
|
|
635
|
+
const serverName = boundedServerName(input.serverName);
|
|
636
|
+
const resource = canonicalMcpResource(input.resource);
|
|
637
|
+
const key = authorizationKey(serverName, resource);
|
|
638
|
+
this.pruneExpired();
|
|
639
|
+
const pending = this.pending.get(key);
|
|
640
|
+
if (!pending) {
|
|
641
|
+
throw new Error("No live MCP authorization session exists for this server");
|
|
642
|
+
}
|
|
643
|
+
const code = parseMcpAuthorizationCallback(input.callbackUrl, pending.session);
|
|
644
|
+
this.pending.delete(key);
|
|
645
|
+
const tokenSet = await this.exchange({
|
|
646
|
+
authorizationServer: pending.discovery.authorizationServer,
|
|
647
|
+
clientId: pending.session.clientId,
|
|
648
|
+
redirectUri: pending.session.redirectUri,
|
|
649
|
+
resource,
|
|
650
|
+
code,
|
|
651
|
+
codeVerifier: pending.session.codeVerifier,
|
|
652
|
+
signal: input.signal
|
|
653
|
+
});
|
|
654
|
+
const stored = {
|
|
655
|
+
serverName,
|
|
656
|
+
resource,
|
|
657
|
+
clientId: pending.session.clientId,
|
|
658
|
+
authorizationServer: pending.discovery.authorizationServer,
|
|
659
|
+
tokenSet,
|
|
660
|
+
updatedAt: new Date(this.now()).toISOString()
|
|
661
|
+
};
|
|
662
|
+
await this.options.store.save(stored);
|
|
663
|
+
this.emit("authorized", stored);
|
|
664
|
+
return statusFromStored(stored, this.now());
|
|
665
|
+
}
|
|
666
|
+
async status(serverName, resource) {
|
|
667
|
+
const normalizedName = boundedServerName(serverName);
|
|
668
|
+
const normalizedResource = canonicalMcpResource(resource);
|
|
669
|
+
this.pruneExpired();
|
|
670
|
+
const pending = this.pending.get(authorizationKey(normalizedName, normalizedResource));
|
|
671
|
+
if (pending) {
|
|
672
|
+
return {
|
|
673
|
+
serverName: normalizedName,
|
|
674
|
+
resource: normalizedResource,
|
|
675
|
+
state: "pending",
|
|
676
|
+
expiresAt: pending.expiresAt,
|
|
677
|
+
scopes: [...pending.scopes],
|
|
678
|
+
canRefresh: false
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const stored = await this.options.store.load(normalizedName, normalizedResource);
|
|
682
|
+
return stored ? statusFromStored(stored, this.now()) : {
|
|
683
|
+
serverName: normalizedName,
|
|
684
|
+
resource: normalizedResource,
|
|
685
|
+
state: "not_authorized",
|
|
686
|
+
scopes: [],
|
|
687
|
+
canRefresh: false
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
async disconnect(serverName, resource) {
|
|
691
|
+
const normalizedName = boundedServerName(serverName);
|
|
692
|
+
const normalizedResource = canonicalMcpResource(resource);
|
|
693
|
+
this.pending.delete(authorizationKey(normalizedName, normalizedResource));
|
|
694
|
+
const removed = await this.options.store.remove(normalizedName, normalizedResource);
|
|
695
|
+
if (removed) {
|
|
696
|
+
this.options.onStateChange?.({
|
|
697
|
+
serverName: normalizedName,
|
|
698
|
+
state: "removed",
|
|
699
|
+
resource: normalizedResource
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
return removed;
|
|
703
|
+
}
|
|
704
|
+
pruneExpired() {
|
|
705
|
+
const now = this.now();
|
|
706
|
+
for (const [key, value] of this.pending) {
|
|
707
|
+
if (value.expiresAt <= now) this.pending.delete(key);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
emit(state, value) {
|
|
711
|
+
this.options.onStateChange?.({
|
|
712
|
+
serverName: value.serverName,
|
|
713
|
+
state,
|
|
714
|
+
resource: value.resource,
|
|
715
|
+
expiresAt: value.tokenSet.expiresAt,
|
|
716
|
+
scopes: [...value.tokenSet.scopes ?? []]
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
function statusFromStored(value, now) {
|
|
721
|
+
return {
|
|
722
|
+
serverName: value.serverName,
|
|
723
|
+
resource: value.resource,
|
|
724
|
+
state: value.tokenSet.expiresAt !== void 0 && value.tokenSet.expiresAt <= now ? "expired" : "authorized",
|
|
725
|
+
expiresAt: value.tokenSet.expiresAt,
|
|
726
|
+
scopes: [...value.tokenSet.scopes ?? []],
|
|
727
|
+
canRefresh: !!value.tokenSet.refreshToken
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function authorizationKey(serverName, resource) {
|
|
731
|
+
return `${boundedServerName(serverName)}\0${resource}`;
|
|
732
|
+
}
|
|
733
|
+
function boundedServerName(value) {
|
|
734
|
+
if (!value || value.length > 256 || /[\r\n\0]/.test(value)) {
|
|
735
|
+
throw new Error("MCP authorization server name is invalid");
|
|
736
|
+
}
|
|
737
|
+
return value;
|
|
738
|
+
}
|
|
739
|
+
|
|
1
740
|
// src/client.ts
|
|
2
741
|
import { spawn } from "node:child_process";
|
|
3
742
|
import { buildChildEnv } from "@wrongstack/core";
|
|
@@ -47,6 +786,207 @@ var MCP_CONSTANTS = Object.freeze({
|
|
|
47
786
|
REQUEST_LOG_CAP: 1024
|
|
48
787
|
});
|
|
49
788
|
|
|
789
|
+
// src/protocol.ts
|
|
790
|
+
function record2(value, label) {
|
|
791
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
792
|
+
throw new Error(`Malformed MCP ${label}: expected object`);
|
|
793
|
+
}
|
|
794
|
+
return value;
|
|
795
|
+
}
|
|
796
|
+
function requiredString2(value, label) {
|
|
797
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
798
|
+
throw new Error(`Malformed MCP ${label}: expected non-empty string`);
|
|
799
|
+
}
|
|
800
|
+
return value;
|
|
801
|
+
}
|
|
802
|
+
function optionalString2(value, label) {
|
|
803
|
+
if (value === void 0) return void 0;
|
|
804
|
+
if (typeof value !== "string") throw new Error(`Malformed MCP ${label}: expected string`);
|
|
805
|
+
return value;
|
|
806
|
+
}
|
|
807
|
+
function optionalRecord(value, label) {
|
|
808
|
+
if (value === void 0) return void 0;
|
|
809
|
+
return record2(value, label);
|
|
810
|
+
}
|
|
811
|
+
function optionalCursor(value, label) {
|
|
812
|
+
return optionalString2(value, `${label}.nextCursor`);
|
|
813
|
+
}
|
|
814
|
+
function parseServerMetadata(value) {
|
|
815
|
+
const input = record2(value, "initialize result");
|
|
816
|
+
const serverInfo = record2(input["serverInfo"], "initialize.serverInfo");
|
|
817
|
+
const capabilities = record2(input["capabilities"], "initialize.capabilities");
|
|
818
|
+
return {
|
|
819
|
+
protocolVersion: requiredString2(input["protocolVersion"], "initialize.protocolVersion"),
|
|
820
|
+
capabilities,
|
|
821
|
+
serverInfo: {
|
|
822
|
+
name: requiredString2(serverInfo["name"], "initialize.serverInfo.name"),
|
|
823
|
+
version: requiredString2(serverInfo["version"], "initialize.serverInfo.version"),
|
|
824
|
+
title: optionalString2(serverInfo["title"], "initialize.serverInfo.title")
|
|
825
|
+
},
|
|
826
|
+
instructions: optionalString2(input["instructions"], "initialize.instructions")
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function parseResource(value, index) {
|
|
830
|
+
const input = record2(value, `resources/list.resources[${index}]`);
|
|
831
|
+
const size = input["size"];
|
|
832
|
+
if (size !== void 0 && (typeof size !== "number" || !Number.isFinite(size) || size < 0)) {
|
|
833
|
+
throw new Error(`Malformed MCP resources/list.resources[${index}].size`);
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
uri: requiredString2(input["uri"], `resources/list.resources[${index}].uri`),
|
|
837
|
+
name: requiredString2(input["name"], `resources/list.resources[${index}].name`),
|
|
838
|
+
title: optionalString2(input["title"], `resources/list.resources[${index}].title`),
|
|
839
|
+
description: optionalString2(
|
|
840
|
+
input["description"],
|
|
841
|
+
`resources/list.resources[${index}].description`
|
|
842
|
+
),
|
|
843
|
+
mimeType: optionalString2(input["mimeType"], `resources/list.resources[${index}].mimeType`),
|
|
844
|
+
size,
|
|
845
|
+
annotations: optionalRecord(
|
|
846
|
+
input["annotations"],
|
|
847
|
+
`resources/list.resources[${index}].annotations`
|
|
848
|
+
)
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
function parseListResourcesResult(value) {
|
|
852
|
+
const input = record2(value, "resources/list result");
|
|
853
|
+
if (!Array.isArray(input["resources"])) {
|
|
854
|
+
throw new Error("Malformed MCP resources/list result: resources must be an array");
|
|
855
|
+
}
|
|
856
|
+
return {
|
|
857
|
+
resources: input["resources"].map(parseResource),
|
|
858
|
+
nextCursor: optionalCursor(input["nextCursor"], "resources/list")
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function parseListResourceTemplatesResult(value) {
|
|
862
|
+
const input = record2(value, "resources/templates/list result");
|
|
863
|
+
const templates = input["resourceTemplates"];
|
|
864
|
+
if (!Array.isArray(templates)) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
"Malformed MCP resources/templates/list result: resourceTemplates must be an array"
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
return {
|
|
870
|
+
resourceTemplates: templates.map((value2, index) => {
|
|
871
|
+
const template = record2(value2, `resources/templates/list.resourceTemplates[${index}]`);
|
|
872
|
+
return {
|
|
873
|
+
uriTemplate: requiredString2(
|
|
874
|
+
template["uriTemplate"],
|
|
875
|
+
`resources/templates/list.resourceTemplates[${index}].uriTemplate`
|
|
876
|
+
),
|
|
877
|
+
name: requiredString2(
|
|
878
|
+
template["name"],
|
|
879
|
+
`resources/templates/list.resourceTemplates[${index}].name`
|
|
880
|
+
),
|
|
881
|
+
title: optionalString2(
|
|
882
|
+
template["title"],
|
|
883
|
+
`resources/templates/list.resourceTemplates[${index}].title`
|
|
884
|
+
),
|
|
885
|
+
description: optionalString2(
|
|
886
|
+
template["description"],
|
|
887
|
+
`resources/templates/list.resourceTemplates[${index}].description`
|
|
888
|
+
),
|
|
889
|
+
mimeType: optionalString2(
|
|
890
|
+
template["mimeType"],
|
|
891
|
+
`resources/templates/list.resourceTemplates[${index}].mimeType`
|
|
892
|
+
),
|
|
893
|
+
annotations: optionalRecord(
|
|
894
|
+
template["annotations"],
|
|
895
|
+
`resources/templates/list.resourceTemplates[${index}].annotations`
|
|
896
|
+
)
|
|
897
|
+
};
|
|
898
|
+
}),
|
|
899
|
+
nextCursor: optionalCursor(input["nextCursor"], "resources/templates/list")
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
function parseReadResourceResult(value) {
|
|
903
|
+
const input = record2(value, "resources/read result");
|
|
904
|
+
if (!Array.isArray(input["contents"])) {
|
|
905
|
+
throw new Error("Malformed MCP resources/read result: contents must be an array");
|
|
906
|
+
}
|
|
907
|
+
return {
|
|
908
|
+
contents: input["contents"].map((value2, index) => {
|
|
909
|
+
const content = record2(value2, `resources/read.contents[${index}]`);
|
|
910
|
+
const text = optionalString2(content["text"], `resources/read.contents[${index}].text`);
|
|
911
|
+
const blob = optionalString2(content["blob"], `resources/read.contents[${index}].blob`);
|
|
912
|
+
if (text === void 0 && blob === void 0) {
|
|
913
|
+
throw new Error(`Malformed MCP resources/read.contents[${index}]: expected text or blob`);
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
uri: requiredString2(content["uri"], `resources/read.contents[${index}].uri`),
|
|
917
|
+
mimeType: optionalString2(content["mimeType"], `resources/read.contents[${index}].mimeType`),
|
|
918
|
+
text,
|
|
919
|
+
blob
|
|
920
|
+
};
|
|
921
|
+
})
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
function parsePromptArgument(value, promptIndex, argIndex) {
|
|
925
|
+
const input = record2(value, `prompts/list.prompts[${promptIndex}].arguments[${argIndex}]`);
|
|
926
|
+
const required = input["required"];
|
|
927
|
+
if (required !== void 0 && typeof required !== "boolean") {
|
|
928
|
+
throw new Error(
|
|
929
|
+
`Malformed MCP prompts/list.prompts[${promptIndex}].arguments[${argIndex}].required`
|
|
930
|
+
);
|
|
931
|
+
}
|
|
932
|
+
return {
|
|
933
|
+
name: requiredString2(
|
|
934
|
+
input["name"],
|
|
935
|
+
`prompts/list.prompts[${promptIndex}].arguments[${argIndex}].name`
|
|
936
|
+
),
|
|
937
|
+
description: optionalString2(
|
|
938
|
+
input["description"],
|
|
939
|
+
`prompts/list.prompts[${promptIndex}].arguments[${argIndex}].description`
|
|
940
|
+
),
|
|
941
|
+
required
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
function parseListPromptsResult(value) {
|
|
945
|
+
const input = record2(value, "prompts/list result");
|
|
946
|
+
if (!Array.isArray(input["prompts"])) {
|
|
947
|
+
throw new Error("Malformed MCP prompts/list result: prompts must be an array");
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
prompts: input["prompts"].map((value2, index) => {
|
|
951
|
+
const prompt = record2(value2, `prompts/list.prompts[${index}]`);
|
|
952
|
+
const args = prompt["arguments"];
|
|
953
|
+
if (args !== void 0 && !Array.isArray(args)) {
|
|
954
|
+
throw new Error(`Malformed MCP prompts/list.prompts[${index}].arguments`);
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
name: requiredString2(prompt["name"], `prompts/list.prompts[${index}].name`),
|
|
958
|
+
title: optionalString2(prompt["title"], `prompts/list.prompts[${index}].title`),
|
|
959
|
+
description: optionalString2(
|
|
960
|
+
prompt["description"],
|
|
961
|
+
`prompts/list.prompts[${index}].description`
|
|
962
|
+
),
|
|
963
|
+
arguments: args?.map((arg, argIndex) => parsePromptArgument(arg, index, argIndex))
|
|
964
|
+
};
|
|
965
|
+
}),
|
|
966
|
+
nextCursor: optionalCursor(input["nextCursor"], "prompts/list")
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
function parseGetPromptResult(value) {
|
|
970
|
+
const input = record2(value, "prompts/get result");
|
|
971
|
+
if (!Array.isArray(input["messages"])) {
|
|
972
|
+
throw new Error("Malformed MCP prompts/get result: messages must be an array");
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
description: optionalString2(input["description"], "prompts/get.description"),
|
|
976
|
+
messages: input["messages"].map((value2, index) => {
|
|
977
|
+
const message = record2(value2, `prompts/get.messages[${index}]`);
|
|
978
|
+
const role = message["role"];
|
|
979
|
+
if (role !== "user" && role !== "assistant") {
|
|
980
|
+
throw new Error(`Malformed MCP prompts/get.messages[${index}].role`);
|
|
981
|
+
}
|
|
982
|
+
if (message["content"] === void 0) {
|
|
983
|
+
throw new Error(`Malformed MCP prompts/get.messages[${index}].content`);
|
|
984
|
+
}
|
|
985
|
+
return { role, content: message["content"] };
|
|
986
|
+
})
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
50
990
|
// src/tool-schema.ts
|
|
51
991
|
function normalizeMCPTools(value) {
|
|
52
992
|
if (!Array.isArray(value)) return [];
|
|
@@ -75,9 +1015,9 @@ function normalizeMCPTools(value) {
|
|
|
75
1015
|
}
|
|
76
1016
|
|
|
77
1017
|
// src/transport.ts
|
|
78
|
-
import { randomBytes } from "node:crypto";
|
|
79
|
-
import * as
|
|
80
|
-
import * as
|
|
1018
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1019
|
+
import * as https2 from "node:https";
|
|
1020
|
+
import * as net2 from "node:net";
|
|
81
1021
|
import { ConfigError, ToolError } from "@wrongstack/core";
|
|
82
1022
|
function isTlsUnsafeAllowed() {
|
|
83
1023
|
return process.env["WRONGSTACK_UNSAFE_MCP_TLS"] === "1";
|
|
@@ -102,7 +1042,7 @@ function validateTransportUrl(rawUrl) {
|
|
|
102
1042
|
}
|
|
103
1043
|
const hostname = url.hostname;
|
|
104
1044
|
const host = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
|
|
105
|
-
const ipVersion =
|
|
1045
|
+
const ipVersion = net2.isIP(host);
|
|
106
1046
|
if (ipVersion === 4) {
|
|
107
1047
|
const parts = host.split(".").map(Number);
|
|
108
1048
|
if (parts[0] === 169 && parts[1] === 254) {
|
|
@@ -162,7 +1102,11 @@ var SSEReader = class {
|
|
|
162
1102
|
message: `SSE: pending line exceeds ${SSE_READER_MAX_BUFFER} bytes \u2014 upstream is not framing events`,
|
|
163
1103
|
code: "TOOL_EXECUTION_FAILED",
|
|
164
1104
|
toolName: "mcp_transport_sse_reader",
|
|
165
|
-
context: {
|
|
1105
|
+
context: {
|
|
1106
|
+
phase: "feed",
|
|
1107
|
+
bufferLength: this.buffer.length,
|
|
1108
|
+
maxBuffer: SSE_READER_MAX_BUFFER
|
|
1109
|
+
}
|
|
166
1110
|
});
|
|
167
1111
|
}
|
|
168
1112
|
let idx = this.buffer.indexOf("\n");
|
|
@@ -190,7 +1134,11 @@ var SSEReader = class {
|
|
|
190
1134
|
message: `SSE: exceeded ${SSE_READER_MAX_DATA_LINES} data lines per event \u2014 upstream is not sending blank-line delimiters`,
|
|
191
1135
|
code: "TOOL_EXECUTION_FAILED",
|
|
192
1136
|
toolName: "mcp_transport_sse_reader",
|
|
193
|
-
context: {
|
|
1137
|
+
context: {
|
|
1138
|
+
phase: "processLine",
|
|
1139
|
+
dataLineCount: this.dataLines.length,
|
|
1140
|
+
maxDataLines: SSE_READER_MAX_DATA_LINES
|
|
1141
|
+
}
|
|
194
1142
|
});
|
|
195
1143
|
}
|
|
196
1144
|
this.dataLines.push(value);
|
|
@@ -226,13 +1174,25 @@ var SSEReader = class {
|
|
|
226
1174
|
function isJsonRpcResult(v) {
|
|
227
1175
|
if (typeof v !== "object" || v === null) return false;
|
|
228
1176
|
const r = v;
|
|
229
|
-
if (r
|
|
230
|
-
if (r
|
|
231
|
-
|
|
1177
|
+
if (r["jsonrpc"] !== "2.0" || typeof r["id"] !== "number") return false;
|
|
1178
|
+
if (Object.hasOwn(r, "method")) return false;
|
|
1179
|
+
const hasResult = Object.hasOwn(r, "result");
|
|
1180
|
+
const hasError = Object.hasOwn(r, "error");
|
|
1181
|
+
if (hasResult === hasError) return false;
|
|
1182
|
+
if (hasError) {
|
|
1183
|
+
const error = r["error"];
|
|
1184
|
+
return typeof error === "object" && error !== null && typeof error["code"] === "number" && typeof error["message"] === "string";
|
|
232
1185
|
}
|
|
233
|
-
return
|
|
1186
|
+
return true;
|
|
234
1187
|
}
|
|
235
|
-
function
|
|
1188
|
+
function isJsonRpcMethodEnvelope(v) {
|
|
1189
|
+
if (typeof v !== "object" || v === null) return false;
|
|
1190
|
+
const envelope = v;
|
|
1191
|
+
if (envelope["jsonrpc"] !== "2.0" || typeof envelope["method"] !== "string") return false;
|
|
1192
|
+
const id = envelope["id"];
|
|
1193
|
+
return id === void 0 || typeof id === "number" || typeof id === "string";
|
|
1194
|
+
}
|
|
1195
|
+
function extractJsonRpcEnvelopes(text) {
|
|
236
1196
|
const out = [];
|
|
237
1197
|
let dataBuf = [];
|
|
238
1198
|
const flush = () => {
|
|
@@ -242,7 +1202,7 @@ function extractJsonRpcResults(text) {
|
|
|
242
1202
|
if (!joined) return;
|
|
243
1203
|
try {
|
|
244
1204
|
const parsed = JSON.parse(joined);
|
|
245
|
-
if (isJsonRpcResult(parsed)) out.push(parsed);
|
|
1205
|
+
if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
|
|
246
1206
|
} catch {
|
|
247
1207
|
}
|
|
248
1208
|
};
|
|
@@ -266,7 +1226,7 @@ function extractJsonRpcResults(text) {
|
|
|
266
1226
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
267
1227
|
try {
|
|
268
1228
|
const parsed = JSON.parse(trimmed);
|
|
269
|
-
if (isJsonRpcResult(parsed)) out.push(parsed);
|
|
1229
|
+
if (isJsonRpcResult(parsed) || isJsonRpcMethodEnvelope(parsed)) out.push(parsed);
|
|
270
1230
|
} catch {
|
|
271
1231
|
}
|
|
272
1232
|
}
|
|
@@ -274,9 +1234,8 @@ function extractJsonRpcResults(text) {
|
|
|
274
1234
|
flush();
|
|
275
1235
|
return out;
|
|
276
1236
|
}
|
|
277
|
-
function
|
|
278
|
-
|
|
279
|
-
return results.find((r) => r.id === id) ?? results[0];
|
|
1237
|
+
function extractJsonRpcResults(text) {
|
|
1238
|
+
return extractJsonRpcEnvelopes(text).filter(isJsonRpcResult);
|
|
280
1239
|
}
|
|
281
1240
|
function assertMatchingJsonRpcResult(data, expectedId, method) {
|
|
282
1241
|
if (!isJsonRpcResult(data)) {
|
|
@@ -287,7 +1246,7 @@ function assertMatchingJsonRpcResult(data, expectedId, method) {
|
|
|
287
1246
|
context: { method, expectedId, reason: "not-jsonrpc-envelope" }
|
|
288
1247
|
});
|
|
289
1248
|
}
|
|
290
|
-
if (data.id !==
|
|
1249
|
+
if (data.id !== expectedId) {
|
|
291
1250
|
throw new ToolError({
|
|
292
1251
|
message: `Invalid JSON-RPC response: id mismatch for ${method} (expected ${expectedId}, got ${data.id})`,
|
|
293
1252
|
code: "TOOL_EXECUTION_FAILED",
|
|
@@ -295,14 +1254,6 @@ function assertMatchingJsonRpcResult(data, expectedId, method) {
|
|
|
295
1254
|
context: { method, expectedId, actualId: data.id, reason: "id-mismatch" }
|
|
296
1255
|
});
|
|
297
1256
|
}
|
|
298
|
-
if (data.id === void 0 && !method.startsWith("notifications/")) {
|
|
299
|
-
throw new ToolError({
|
|
300
|
-
message: `Invalid JSON-RPC response: missing id for ${method}`,
|
|
301
|
-
code: "TOOL_EXECUTION_FAILED",
|
|
302
|
-
toolName: "mcp_transport_jsonrpc",
|
|
303
|
-
context: { method, expectedId, reason: "missing-id" }
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
1257
|
return data;
|
|
307
1258
|
}
|
|
308
1259
|
function makeAbortError(method) {
|
|
@@ -336,16 +1287,26 @@ var BaseHTTPTransport = class {
|
|
|
336
1287
|
headers;
|
|
337
1288
|
timeout;
|
|
338
1289
|
requestTimeout;
|
|
1290
|
+
name;
|
|
1291
|
+
authorizationProvider;
|
|
1292
|
+
authorizationResource;
|
|
339
1293
|
/** Per-request TLS agent — created once from HttpTransportOptions.tls */
|
|
340
1294
|
tlsAgent;
|
|
341
1295
|
tools = [];
|
|
1296
|
+
serverMetadata;
|
|
342
1297
|
abortController;
|
|
343
1298
|
disconnectHandlers = [];
|
|
344
1299
|
toolsChangedListeners = /* @__PURE__ */ new Set();
|
|
1300
|
+
resourcesChangedListeners = /* @__PURE__ */ new Set();
|
|
1301
|
+
promptsChangedListeners = /* @__PURE__ */ new Set();
|
|
1302
|
+
protocolVersion;
|
|
345
1303
|
constructor(opts, transportName) {
|
|
346
1304
|
validateTransportUrl(opts.url);
|
|
1305
|
+
this.name = opts.name;
|
|
347
1306
|
this.url = opts.url;
|
|
348
1307
|
this.headers = { ...opts.headers };
|
|
1308
|
+
this.authorizationProvider = opts.authorizationProvider;
|
|
1309
|
+
this.authorizationResource = canonicalMcpResource(opts.url);
|
|
349
1310
|
this.timeout = opts.startupTimeoutMs ?? 1e4;
|
|
350
1311
|
this.requestTimeout = opts.requestTimeoutMs ?? 6e4;
|
|
351
1312
|
if (opts.tls) {
|
|
@@ -361,7 +1322,7 @@ var BaseHTTPTransport = class {
|
|
|
361
1322
|
`[mcp:${transportName}] \u26A0\uFE0F TLS verification DISABLED for ${this.url}. Network attacks are possible \u2014 only use on localhost.`
|
|
362
1323
|
);
|
|
363
1324
|
}
|
|
364
|
-
this.tlsAgent = new
|
|
1325
|
+
this.tlsAgent = new https2.Agent({
|
|
365
1326
|
ca: opts.tls.ca,
|
|
366
1327
|
rejectUnauthorized: opts.tls.rejectUnauthorized
|
|
367
1328
|
});
|
|
@@ -370,9 +1331,52 @@ var BaseHTTPTransport = class {
|
|
|
370
1331
|
getState() {
|
|
371
1332
|
return this.state;
|
|
372
1333
|
}
|
|
1334
|
+
async fetchWithAuthorization(input, init, signal) {
|
|
1335
|
+
const context = {
|
|
1336
|
+
serverName: this.name,
|
|
1337
|
+
resource: this.authorizationResource,
|
|
1338
|
+
signal
|
|
1339
|
+
};
|
|
1340
|
+
const send = async () => {
|
|
1341
|
+
signal?.throwIfAborted();
|
|
1342
|
+
const headers = new Headers(init.headers);
|
|
1343
|
+
if (this.protocolVersion) headers.set("MCP-Protocol-Version", this.protocolVersion);
|
|
1344
|
+
const token = await this.authorizationProvider?.getAccessToken(context);
|
|
1345
|
+
signal?.throwIfAborted();
|
|
1346
|
+
if (token) {
|
|
1347
|
+
headers.set(
|
|
1348
|
+
"Authorization",
|
|
1349
|
+
authorizationHeaderForToken(token, this.authorizationResource)
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
return fetch(input, { ...init, headers });
|
|
1353
|
+
};
|
|
1354
|
+
let response = await send();
|
|
1355
|
+
if (response.status !== 401 || !this.authorizationProvider?.handleUnauthorized) {
|
|
1356
|
+
return response;
|
|
1357
|
+
}
|
|
1358
|
+
const challenge = parseMcpBearerChallenge(
|
|
1359
|
+
response.headers.get("www-authenticate"),
|
|
1360
|
+
this.authorizationResource
|
|
1361
|
+
);
|
|
1362
|
+
const retry = await this.authorizationProvider.handleUnauthorized(challenge, context);
|
|
1363
|
+
if (!retry) return response;
|
|
1364
|
+
await response.body?.cancel().catch(() => void 0);
|
|
1365
|
+
response = await send();
|
|
1366
|
+
return response;
|
|
1367
|
+
}
|
|
373
1368
|
listTools() {
|
|
374
1369
|
return [...this.tools];
|
|
375
1370
|
}
|
|
1371
|
+
getServerMetadata() {
|
|
1372
|
+
const metadata = this.serverMetadata;
|
|
1373
|
+
if (!metadata) return void 0;
|
|
1374
|
+
return {
|
|
1375
|
+
...metadata,
|
|
1376
|
+
capabilities: { ...metadata.capabilities },
|
|
1377
|
+
serverInfo: { ...metadata.serverInfo }
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
376
1380
|
onDisconnect(cb) {
|
|
377
1381
|
this.disconnectHandlers.push(cb);
|
|
378
1382
|
return () => {
|
|
@@ -386,7 +1390,15 @@ var BaseHTTPTransport = class {
|
|
|
386
1390
|
this.toolsChangedListeners.delete(cb);
|
|
387
1391
|
};
|
|
388
1392
|
}
|
|
389
|
-
|
|
1393
|
+
onResourcesChanged(cb) {
|
|
1394
|
+
this.resourcesChangedListeners.add(cb);
|
|
1395
|
+
return () => this.resourcesChangedListeners.delete(cb);
|
|
1396
|
+
}
|
|
1397
|
+
onPromptsChanged(cb) {
|
|
1398
|
+
this.promptsChangedListeners.add(cb);
|
|
1399
|
+
return () => this.promptsChangedListeners.delete(cb);
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
390
1402
|
* Fire all disconnect handlers. Subclasses call this when the connection
|
|
391
1403
|
* drops so the registry can schedule reconnects.
|
|
392
1404
|
*/
|
|
@@ -398,6 +1410,22 @@ var BaseHTTPTransport = class {
|
|
|
398
1410
|
}
|
|
399
1411
|
}
|
|
400
1412
|
}
|
|
1413
|
+
notifyResourcesChanged() {
|
|
1414
|
+
for (const cb of this.resourcesChangedListeners) {
|
|
1415
|
+
try {
|
|
1416
|
+
cb();
|
|
1417
|
+
} catch {
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
notifyPromptsChanged() {
|
|
1422
|
+
for (const cb of this.promptsChangedListeners) {
|
|
1423
|
+
try {
|
|
1424
|
+
cb();
|
|
1425
|
+
} catch {
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
401
1429
|
/**
|
|
402
1430
|
* Apply the pinned TLS agent (if configured) to a `RequestInit` object.
|
|
403
1431
|
* Uses `HttpDispatcher` from `@wrongstack/core`'s dispatcher-types shim,
|
|
@@ -444,6 +1472,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
444
1472
|
}
|
|
445
1473
|
async connect() {
|
|
446
1474
|
this.state = "connecting";
|
|
1475
|
+
this.serverMetadata = void 0;
|
|
447
1476
|
this.abortController = new AbortController();
|
|
448
1477
|
const signal = this.abortController.signal;
|
|
449
1478
|
const startupTimer = setTimeout(() => this.abortController?.abort(), this.timeout);
|
|
@@ -454,7 +1483,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
454
1483
|
signal
|
|
455
1484
|
};
|
|
456
1485
|
this.applyTlsAgent(fetchOpts);
|
|
457
|
-
const response = await
|
|
1486
|
+
const response = await this.fetchWithAuthorization(sseUrl, fetchOpts, signal);
|
|
458
1487
|
if (!response.ok) {
|
|
459
1488
|
throw new ToolError({
|
|
460
1489
|
message: `SSE connect HTTP ${response.status}: ${response.statusText}`,
|
|
@@ -478,6 +1507,10 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
478
1507
|
if (msg.method && !msg.id) {
|
|
479
1508
|
if (msg.method === "notifications/tools/list_changed") {
|
|
480
1509
|
void this.handleToolsListChanged();
|
|
1510
|
+
} else if (msg.method === "notifications/resources/list_changed") {
|
|
1511
|
+
this.notifyResourcesChanged();
|
|
1512
|
+
} else if (msg.method === "notifications/prompts/list_changed") {
|
|
1513
|
+
this.notifyPromptsChanged();
|
|
481
1514
|
}
|
|
482
1515
|
}
|
|
483
1516
|
});
|
|
@@ -500,6 +1533,8 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
500
1533
|
context: { transport: "sse", url: this.url }
|
|
501
1534
|
});
|
|
502
1535
|
}
|
|
1536
|
+
this.serverMetadata = parseServerMetadata(initRes.result);
|
|
1537
|
+
this.protocolVersion = this.serverMetadata.protocolVersion;
|
|
503
1538
|
try {
|
|
504
1539
|
await this.httpPost("notifications/initialized", {});
|
|
505
1540
|
} catch {
|
|
@@ -509,11 +1544,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
509
1544
|
this.tools.splice(0, this.tools.length);
|
|
510
1545
|
} else {
|
|
511
1546
|
const result = toolsRes.result;
|
|
512
|
-
this.tools.splice(
|
|
513
|
-
0,
|
|
514
|
-
this.tools.length,
|
|
515
|
-
...normalizeMCPTools(result?.tools)
|
|
516
|
-
);
|
|
1547
|
+
this.tools.splice(0, this.tools.length, ...normalizeMCPTools(result?.tools));
|
|
517
1548
|
}
|
|
518
1549
|
this.state = "connected";
|
|
519
1550
|
clearTimeout(startupTimer);
|
|
@@ -542,7 +1573,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
542
1573
|
buildSSEUrl() {
|
|
543
1574
|
try {
|
|
544
1575
|
const url = new URL(this.url);
|
|
545
|
-
url.searchParams.set("session",
|
|
1576
|
+
url.searchParams.set("session", randomBytes2(16).toString("hex"));
|
|
546
1577
|
return url.toString();
|
|
547
1578
|
} catch {
|
|
548
1579
|
return this.url;
|
|
@@ -565,7 +1596,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
565
1596
|
};
|
|
566
1597
|
this.applyTlsAgent(fetchOpts);
|
|
567
1598
|
try {
|
|
568
|
-
const res = await
|
|
1599
|
+
const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
|
|
569
1600
|
if (!res.ok) {
|
|
570
1601
|
const body2 = await res.text();
|
|
571
1602
|
const cap = MCP_CONSTANTS.REQUEST_LOG_CAP;
|
|
@@ -624,13 +1655,12 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
624
1655
|
};
|
|
625
1656
|
}
|
|
626
1657
|
/** Generic JSON-RPC request — used by MCPClient.request() for SSE transports. */
|
|
627
|
-
async request(method, params, timeoutMs) {
|
|
1658
|
+
async request(method, params, timeoutMs, opts) {
|
|
628
1659
|
const id = this.genId();
|
|
629
1660
|
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
);
|
|
1661
|
+
const external = opts?.signal;
|
|
1662
|
+
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
1663
|
+
const timeoutSignal = createTimeoutSignal(parent, timeoutMs ?? this.requestTimeout);
|
|
634
1664
|
const fetchOpts = {
|
|
635
1665
|
method: "POST",
|
|
636
1666
|
headers: {
|
|
@@ -642,7 +1672,7 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
642
1672
|
};
|
|
643
1673
|
this.applyTlsAgent(fetchOpts);
|
|
644
1674
|
try {
|
|
645
|
-
const res = await
|
|
1675
|
+
const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
|
|
646
1676
|
if (!res.ok) {
|
|
647
1677
|
throw new ToolError({
|
|
648
1678
|
message: `HTTP ${res.status}: ${res.statusText}`,
|
|
@@ -670,6 +1700,16 @@ var SSETransport = class extends BaseHTTPTransport {
|
|
|
670
1700
|
}
|
|
671
1701
|
const result = assertMatchingJsonRpcResult(data, id, method);
|
|
672
1702
|
return { jsonrpc: "2.0", id, result: result.result, error: result.error };
|
|
1703
|
+
} catch (err) {
|
|
1704
|
+
if (external?.aborted && !method.startsWith("notifications/")) {
|
|
1705
|
+
void this.httpPost("notifications/cancelled", {
|
|
1706
|
+
requestId: id,
|
|
1707
|
+
reason: "client aborted"
|
|
1708
|
+
}).catch(() => {
|
|
1709
|
+
});
|
|
1710
|
+
throw makeAbortError(method);
|
|
1711
|
+
}
|
|
1712
|
+
throw err;
|
|
673
1713
|
} finally {
|
|
674
1714
|
timeoutSignal.dispose();
|
|
675
1715
|
}
|
|
@@ -700,8 +1740,45 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
700
1740
|
genId() {
|
|
701
1741
|
return this._nextId++;
|
|
702
1742
|
}
|
|
1743
|
+
consumeResponseText(text, requestId) {
|
|
1744
|
+
const envelopes = extractJsonRpcEnvelopes(text);
|
|
1745
|
+
for (const envelope of envelopes) {
|
|
1746
|
+
if ("method" in envelope && envelope.id === void 0) {
|
|
1747
|
+
this.handleNotification(envelope.method);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
const responses = envelopes.filter(isJsonRpcResult);
|
|
1751
|
+
return responses.find((envelope) => envelope.id === requestId) ?? responses.find((envelope) => envelope.id !== void 0) ?? responses[0];
|
|
1752
|
+
}
|
|
1753
|
+
handleNotification(method) {
|
|
1754
|
+
if (method === "notifications/resources/list_changed") {
|
|
1755
|
+
this.notifyResourcesChanged();
|
|
1756
|
+
} else if (method === "notifications/prompts/list_changed") {
|
|
1757
|
+
this.notifyPromptsChanged();
|
|
1758
|
+
} else if (method === "notifications/tools/list_changed") {
|
|
1759
|
+
void this.refreshTools();
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
async refreshTools() {
|
|
1763
|
+
try {
|
|
1764
|
+
const response = await this.postRaw("tools/list", {});
|
|
1765
|
+
if (response.error) return;
|
|
1766
|
+
const tools = normalizeMCPTools(
|
|
1767
|
+
response.result?.tools
|
|
1768
|
+
);
|
|
1769
|
+
this.tools.splice(0, this.tools.length, ...tools);
|
|
1770
|
+
for (const listener of this.toolsChangedListeners) {
|
|
1771
|
+
try {
|
|
1772
|
+
listener([...tools]);
|
|
1773
|
+
} catch {
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
} catch {
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
703
1779
|
async connect() {
|
|
704
1780
|
this.state = "connecting";
|
|
1781
|
+
this.serverMetadata = void 0;
|
|
705
1782
|
this.abortController = new AbortController();
|
|
706
1783
|
const signal = this.abortController.signal;
|
|
707
1784
|
const startupTimer = setTimeout(() => this.abortController?.abort(), this.timeout);
|
|
@@ -726,7 +1803,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
726
1803
|
signal
|
|
727
1804
|
};
|
|
728
1805
|
this.applyTlsAgent(initFetchOpts);
|
|
729
|
-
const initRes = await
|
|
1806
|
+
const initRes = await this.fetchWithAuthorization(this.url, initFetchOpts, signal);
|
|
730
1807
|
if (!initRes.ok) {
|
|
731
1808
|
throw new Error(`initialize HTTP ${initRes.status}: ${initRes.statusText}`);
|
|
732
1809
|
}
|
|
@@ -745,6 +1822,8 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
745
1822
|
if (data.error) {
|
|
746
1823
|
throw new Error(`initialize failed: ${data.error.message}`);
|
|
747
1824
|
}
|
|
1825
|
+
this.serverMetadata = parseServerMetadata(data.result);
|
|
1826
|
+
this.protocolVersion = this.serverMetadata.protocolVersion;
|
|
748
1827
|
this.sessionId = initRes.headers.get("mcp-session-id") ?? void 0;
|
|
749
1828
|
await this.postRaw("notifications/initialized", {});
|
|
750
1829
|
const toolsRes = await this.postRaw("tools/list", {});
|
|
@@ -782,7 +1861,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
782
1861
|
};
|
|
783
1862
|
this.applyTlsAgent(fetchOpts);
|
|
784
1863
|
try {
|
|
785
|
-
const res = await
|
|
1864
|
+
const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
|
|
786
1865
|
if (!res.ok) {
|
|
787
1866
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
788
1867
|
}
|
|
@@ -790,7 +1869,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
790
1869
|
await res.text().catch(() => void 0);
|
|
791
1870
|
return { jsonrpc: "2.0" };
|
|
792
1871
|
}
|
|
793
|
-
const match =
|
|
1872
|
+
const match = this.consumeResponseText(await res.text(), id);
|
|
794
1873
|
if (match) {
|
|
795
1874
|
return assertMatchingJsonRpcResult(match, id, method);
|
|
796
1875
|
}
|
|
@@ -810,13 +1889,12 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
810
1889
|
}
|
|
811
1890
|
}
|
|
812
1891
|
/** Generic JSON-RPC request — used by MCPClient.request() for SSE/streamable-http transports. */
|
|
813
|
-
async request(method, params, timeoutMs) {
|
|
1892
|
+
async request(method, params, timeoutMs, opts) {
|
|
814
1893
|
const id = this.genId();
|
|
815
1894
|
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
);
|
|
1895
|
+
const external = opts?.signal;
|
|
1896
|
+
const parent = external && this.abortController ? AbortSignal.any([this.abortController.signal, external]) : external ?? this.abortController?.signal;
|
|
1897
|
+
const timeoutSignal = createTimeoutSignal(parent, timeoutMs ?? this.requestTimeout);
|
|
820
1898
|
const fetchOpts = {
|
|
821
1899
|
method: "POST",
|
|
822
1900
|
headers: {
|
|
@@ -829,8 +1907,8 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
829
1907
|
signal: timeoutSignal.signal
|
|
830
1908
|
};
|
|
831
1909
|
this.applyTlsAgent(fetchOpts);
|
|
832
|
-
const res = await fetch(this.url, fetchOpts);
|
|
833
1910
|
try {
|
|
1911
|
+
const res = await this.fetchWithAuthorization(this.url, fetchOpts, timeoutSignal.signal);
|
|
834
1912
|
if (!res.ok) {
|
|
835
1913
|
throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
836
1914
|
}
|
|
@@ -838,7 +1916,7 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
838
1916
|
await res.text().catch(() => void 0);
|
|
839
1917
|
return { jsonrpc: "2.0", id };
|
|
840
1918
|
}
|
|
841
|
-
const parsed =
|
|
1919
|
+
const parsed = this.consumeResponseText(await res.text(), id);
|
|
842
1920
|
if (parsed) {
|
|
843
1921
|
return {
|
|
844
1922
|
jsonrpc: "2.0",
|
|
@@ -848,6 +1926,16 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
848
1926
|
};
|
|
849
1927
|
}
|
|
850
1928
|
throw new Error("Could not parse response as JSON-RPC");
|
|
1929
|
+
} catch (err) {
|
|
1930
|
+
if (external?.aborted && !method.startsWith("notifications/")) {
|
|
1931
|
+
void this.postRaw("notifications/cancelled", {
|
|
1932
|
+
requestId: id,
|
|
1933
|
+
reason: "client aborted"
|
|
1934
|
+
}).catch(() => {
|
|
1935
|
+
});
|
|
1936
|
+
throw makeAbortError(method);
|
|
1937
|
+
}
|
|
1938
|
+
throw err;
|
|
851
1939
|
} finally {
|
|
852
1940
|
timeoutSignal.dispose();
|
|
853
1941
|
}
|
|
@@ -875,6 +1963,18 @@ var StreamableHTTPTransport = class extends BaseHTTPTransport {
|
|
|
875
1963
|
};
|
|
876
1964
|
|
|
877
1965
|
// src/client.ts
|
|
1966
|
+
function isJsonRpcResponse(value) {
|
|
1967
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1968
|
+
const response = value;
|
|
1969
|
+
if (response["jsonrpc"] !== "2.0" || typeof response["id"] !== "number") return false;
|
|
1970
|
+
if (Object.hasOwn(response, "method")) return false;
|
|
1971
|
+
const hasResult = Object.hasOwn(response, "result");
|
|
1972
|
+
const hasError = Object.hasOwn(response, "error");
|
|
1973
|
+
if (hasResult === hasError) return false;
|
|
1974
|
+
if (!hasError) return true;
|
|
1975
|
+
const error = response["error"];
|
|
1976
|
+
return typeof error === "object" && error !== null && typeof error["code"] === "number" && typeof error["message"] === "string";
|
|
1977
|
+
}
|
|
878
1978
|
var MCPClient = class {
|
|
879
1979
|
constructor(opts) {
|
|
880
1980
|
this.opts = opts;
|
|
@@ -891,6 +1991,8 @@ var MCPClient = class {
|
|
|
891
1991
|
pending = /* @__PURE__ */ new Map();
|
|
892
1992
|
rxBuffer = "";
|
|
893
1993
|
_tools = [];
|
|
1994
|
+
/** Server-declared handshake metadata. Populated for stdio in the first protocol slice. */
|
|
1995
|
+
_serverMetadata;
|
|
894
1996
|
/** Cached tool list — survives reconnects so the registry can re-register without re-discovering. */
|
|
895
1997
|
_toolsCache;
|
|
896
1998
|
_drainPending = false;
|
|
@@ -902,11 +2004,22 @@ var MCPClient = class {
|
|
|
902
2004
|
exitListeners = /* @__PURE__ */ new Set();
|
|
903
2005
|
/** Notified when the server announces a tools/list_changed notification. */
|
|
904
2006
|
toolsChangedListeners = /* @__PURE__ */ new Set();
|
|
2007
|
+
resourcesChangedListeners = /* @__PURE__ */ new Set();
|
|
2008
|
+
promptsChangedListeners = /* @__PURE__ */ new Set();
|
|
905
2009
|
/** Notified when an HTTP transport (SSE or streamable-http) disconnects. */
|
|
906
2010
|
disconnectListeners = /* @__PURE__ */ new Set();
|
|
907
2011
|
getState() {
|
|
908
2012
|
return this.state;
|
|
909
2013
|
}
|
|
2014
|
+
getServerMetadata() {
|
|
2015
|
+
const metadata = this._serverMetadata;
|
|
2016
|
+
if (!metadata) return void 0;
|
|
2017
|
+
return {
|
|
2018
|
+
...metadata,
|
|
2019
|
+
capabilities: { ...metadata.capabilities },
|
|
2020
|
+
serverInfo: { ...metadata.serverInfo }
|
|
2021
|
+
};
|
|
2022
|
+
}
|
|
910
2023
|
listTools() {
|
|
911
2024
|
return this._tools.length > 0 ? [...this._tools] : this._toolsCache ? [...this._toolsCache] : [];
|
|
912
2025
|
}
|
|
@@ -936,6 +2049,7 @@ var MCPClient = class {
|
|
|
936
2049
|
}
|
|
937
2050
|
async connect() {
|
|
938
2051
|
this.state = "connecting";
|
|
2052
|
+
this._serverMetadata = void 0;
|
|
939
2053
|
if (this.opts.transport === "stdio") {
|
|
940
2054
|
await this.connectStdio();
|
|
941
2055
|
} else if (this.opts.transport === "sse") {
|
|
@@ -1007,6 +2121,12 @@ var MCPClient = class {
|
|
|
1007
2121
|
this.state = "failed";
|
|
1008
2122
|
throw new Error(`MCP initialize failed: ${initialize.error.message}`);
|
|
1009
2123
|
}
|
|
2124
|
+
try {
|
|
2125
|
+
this._serverMetadata = parseServerMetadata(initialize.result);
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
this.state = "failed";
|
|
2128
|
+
throw new Error(`MCP initialize returned malformed server metadata: ${toErrorMessage(err)}`);
|
|
2129
|
+
}
|
|
1010
2130
|
try {
|
|
1011
2131
|
await this.notify("notifications/initialized", {});
|
|
1012
2132
|
} catch (err) {
|
|
@@ -1034,7 +2154,8 @@ var MCPClient = class {
|
|
|
1034
2154
|
url: this.opts.url,
|
|
1035
2155
|
headers: this.opts.headers,
|
|
1036
2156
|
startupTimeoutMs: this.opts.startupTimeoutMs,
|
|
1037
|
-
requestTimeoutMs: this.opts.requestTimeoutMs
|
|
2157
|
+
requestTimeoutMs: this.opts.requestTimeoutMs,
|
|
2158
|
+
authorizationProvider: this.opts.authorizationProvider
|
|
1038
2159
|
};
|
|
1039
2160
|
this.sseTransport = new SSETransport(httpOpts);
|
|
1040
2161
|
this.sseTransport.onDisconnect(() => {
|
|
@@ -1056,6 +2177,8 @@ var MCPClient = class {
|
|
|
1056
2177
|
}
|
|
1057
2178
|
}
|
|
1058
2179
|
});
|
|
2180
|
+
this.sseTransport.onResourcesChanged(() => this.emitCapabilityChanged("resources"));
|
|
2181
|
+
this.sseTransport.onPromptsChanged(() => this.emitCapabilityChanged("prompts"));
|
|
1059
2182
|
try {
|
|
1060
2183
|
await this.sseTransport.connect();
|
|
1061
2184
|
} catch (err) {
|
|
@@ -1068,6 +2191,7 @@ var MCPClient = class {
|
|
|
1068
2191
|
}
|
|
1069
2192
|
this._tools = this.sseTransport.listTools();
|
|
1070
2193
|
this._toolsCache = this._tools;
|
|
2194
|
+
this._serverMetadata = this.sseTransport.getServerMetadata();
|
|
1071
2195
|
this.state = "connected";
|
|
1072
2196
|
}
|
|
1073
2197
|
async connectStreamableHTTP() {
|
|
@@ -1080,7 +2204,8 @@ var MCPClient = class {
|
|
|
1080
2204
|
url: this.opts.url,
|
|
1081
2205
|
headers: this.opts.headers,
|
|
1082
2206
|
startupTimeoutMs: this.opts.startupTimeoutMs,
|
|
1083
|
-
requestTimeoutMs: this.opts.requestTimeoutMs
|
|
2207
|
+
requestTimeoutMs: this.opts.requestTimeoutMs,
|
|
2208
|
+
authorizationProvider: this.opts.authorizationProvider
|
|
1084
2209
|
};
|
|
1085
2210
|
this.httpTransport = new StreamableHTTPTransport(httpOpts);
|
|
1086
2211
|
this.httpTransport.onDisconnect(() => {
|
|
@@ -1102,6 +2227,8 @@ var MCPClient = class {
|
|
|
1102
2227
|
}
|
|
1103
2228
|
}
|
|
1104
2229
|
});
|
|
2230
|
+
this.httpTransport.onResourcesChanged(() => this.emitCapabilityChanged("resources"));
|
|
2231
|
+
this.httpTransport.onPromptsChanged(() => this.emitCapabilityChanged("prompts"));
|
|
1105
2232
|
try {
|
|
1106
2233
|
await this.httpTransport.connect();
|
|
1107
2234
|
} catch (err) {
|
|
@@ -1114,6 +2241,7 @@ var MCPClient = class {
|
|
|
1114
2241
|
}
|
|
1115
2242
|
this._tools = this.httpTransport.listTools();
|
|
1116
2243
|
this._toolsCache = this._tools;
|
|
2244
|
+
this._serverMetadata = this.httpTransport.getServerMetadata();
|
|
1117
2245
|
this.state = "connected";
|
|
1118
2246
|
}
|
|
1119
2247
|
async callTool(name, input, opts) {
|
|
@@ -1136,6 +2264,79 @@ var MCPClient = class {
|
|
|
1136
2264
|
isError: Boolean(result?.isError)
|
|
1137
2265
|
};
|
|
1138
2266
|
}
|
|
2267
|
+
async listResources(opts = {}) {
|
|
2268
|
+
const params = pageParams(opts.cursor, "resources/list cursor");
|
|
2269
|
+
return this.requestCapability(
|
|
2270
|
+
"resources",
|
|
2271
|
+
"resources/list",
|
|
2272
|
+
params,
|
|
2273
|
+
parseListResourcesResult,
|
|
2274
|
+
opts
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
async listResourceTemplates(opts = {}) {
|
|
2278
|
+
const params = pageParams(opts.cursor, "resources/templates/list cursor");
|
|
2279
|
+
return this.requestCapability(
|
|
2280
|
+
"resources",
|
|
2281
|
+
"resources/templates/list",
|
|
2282
|
+
params,
|
|
2283
|
+
parseListResourceTemplatesResult,
|
|
2284
|
+
opts
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
async readResource(uri, opts = {}) {
|
|
2288
|
+
validateProtocolString(uri, "resource URI");
|
|
2289
|
+
return this.requestCapability(
|
|
2290
|
+
"resources",
|
|
2291
|
+
"resources/read",
|
|
2292
|
+
{ uri },
|
|
2293
|
+
parseReadResourceResult,
|
|
2294
|
+
opts
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
async subscribeResource(uri, opts = {}) {
|
|
2298
|
+
validateProtocolString(uri, "resource URI");
|
|
2299
|
+
this.requireResourceSubscriptions("resources/subscribe");
|
|
2300
|
+
await this.requestCapability(
|
|
2301
|
+
"resources",
|
|
2302
|
+
"resources/subscribe",
|
|
2303
|
+
{ uri },
|
|
2304
|
+
parseEmptyResult,
|
|
2305
|
+
opts
|
|
2306
|
+
);
|
|
2307
|
+
}
|
|
2308
|
+
async unsubscribeResource(uri, opts = {}) {
|
|
2309
|
+
validateProtocolString(uri, "resource URI");
|
|
2310
|
+
this.requireResourceSubscriptions("resources/unsubscribe");
|
|
2311
|
+
await this.requestCapability(
|
|
2312
|
+
"resources",
|
|
2313
|
+
"resources/unsubscribe",
|
|
2314
|
+
{ uri },
|
|
2315
|
+
parseEmptyResult,
|
|
2316
|
+
opts
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
async listPrompts(opts = {}) {
|
|
2320
|
+
const params = pageParams(opts.cursor, "prompts/list cursor");
|
|
2321
|
+
return this.requestCapability("prompts", "prompts/list", params, parseListPromptsResult, opts);
|
|
2322
|
+
}
|
|
2323
|
+
async getPrompt(name, args, opts = {}) {
|
|
2324
|
+
validateProtocolString(name, "prompt name");
|
|
2325
|
+
if (args && Object.keys(args).length > 64) {
|
|
2326
|
+
throw new Error("MCP prompt arguments exceed the limit of 64");
|
|
2327
|
+
}
|
|
2328
|
+
for (const [key, value] of Object.entries(args ?? {})) {
|
|
2329
|
+
validateProtocolString(key, "prompt argument name");
|
|
2330
|
+
validateProtocolString(value, `prompt argument "${key}"`, true);
|
|
2331
|
+
}
|
|
2332
|
+
return this.requestCapability(
|
|
2333
|
+
"prompts",
|
|
2334
|
+
"prompts/get",
|
|
2335
|
+
args === void 0 ? { name } : { name, arguments: args },
|
|
2336
|
+
parseGetPromptResult,
|
|
2337
|
+
opts
|
|
2338
|
+
);
|
|
2339
|
+
}
|
|
1139
2340
|
async close() {
|
|
1140
2341
|
if (this.child) {
|
|
1141
2342
|
const child = this.child;
|
|
@@ -1170,8 +2371,8 @@ var MCPClient = class {
|
|
|
1170
2371
|
this.state = "disconnected";
|
|
1171
2372
|
}
|
|
1172
2373
|
request(method, params, timeoutMs = this.opts.requestTimeoutMs ?? 6e4, opts) {
|
|
1173
|
-
if (this.sseTransport) return this.sseTransport.request(method, params, timeoutMs);
|
|
1174
|
-
if (this.httpTransport) return this.httpTransport.request(method, params, timeoutMs);
|
|
2374
|
+
if (this.sseTransport) return this.sseTransport.request(method, params, timeoutMs, opts);
|
|
2375
|
+
if (this.httpTransport) return this.httpTransport.request(method, params, timeoutMs, opts);
|
|
1175
2376
|
const signal = opts?.signal;
|
|
1176
2377
|
if (signal?.aborted) {
|
|
1177
2378
|
const err = new Error(`MCP "${this.opts.name}" request "${method}" aborted before send`);
|
|
@@ -1238,6 +2439,37 @@ var MCPClient = class {
|
|
|
1238
2439
|
}
|
|
1239
2440
|
});
|
|
1240
2441
|
}
|
|
2442
|
+
async requestCapability(capability, method, params, parse, opts) {
|
|
2443
|
+
if (this.state !== "connected") {
|
|
2444
|
+
throw new Error(`MCP client "${this.opts.name}" not connected (state=${this.state})`);
|
|
2445
|
+
}
|
|
2446
|
+
const metadata = this._serverMetadata;
|
|
2447
|
+
if (!metadata) {
|
|
2448
|
+
throw new Error(
|
|
2449
|
+
`MCP server "${this.opts.name}" capability metadata is unavailable for ${method}`
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
if (!metadata.capabilities[capability]) {
|
|
2453
|
+
throw new Error(
|
|
2454
|
+
`MCP server "${this.opts.name}" does not advertise the ${capability} capability`
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
const response = await this.request(method, params, void 0, opts);
|
|
2458
|
+
if (response.error) {
|
|
2459
|
+
throw new Error(`MCP ${method} failed: ${response.error.message}`);
|
|
2460
|
+
}
|
|
2461
|
+
return parse(response.result);
|
|
2462
|
+
}
|
|
2463
|
+
requireResourceSubscriptions(method) {
|
|
2464
|
+
if (this.state !== "connected") {
|
|
2465
|
+
throw new Error(`MCP client "${this.opts.name}" not connected (state=${this.state})`);
|
|
2466
|
+
}
|
|
2467
|
+
if (this._serverMetadata?.capabilities.resources?.subscribe !== true) {
|
|
2468
|
+
throw new Error(
|
|
2469
|
+
`MCP server "${this.opts.name}" does not advertise resource subscriptions for ${method}`
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
1241
2473
|
/**
|
|
1242
2474
|
* Reject every in-flight {@link request} call. Used when the underlying
|
|
1243
2475
|
* transport dies — without this, callers awaiting `tools/call` over a
|
|
@@ -1322,14 +2554,48 @@ var MCPClient = class {
|
|
|
1322
2554
|
} catch {
|
|
1323
2555
|
return;
|
|
1324
2556
|
}
|
|
1325
|
-
if (msg
|
|
2557
|
+
if (typeof msg !== "object" || msg === null) return;
|
|
2558
|
+
const envelope = msg;
|
|
2559
|
+
if (envelope["jsonrpc"] !== "2.0") return;
|
|
2560
|
+
if (typeof envelope["method"] === "string") {
|
|
2561
|
+
const id = envelope["id"];
|
|
2562
|
+
if (typeof id === "number" || typeof id === "string") {
|
|
2563
|
+
this.handleServerRequest({
|
|
2564
|
+
jsonrpc: "2.0",
|
|
2565
|
+
id,
|
|
2566
|
+
method: envelope["method"],
|
|
2567
|
+
params: envelope["params"]
|
|
2568
|
+
});
|
|
2569
|
+
return;
|
|
2570
|
+
}
|
|
2571
|
+
if (Object.hasOwn(envelope, "id")) return;
|
|
2572
|
+
if (envelope["method"] === "notifications/tools/list_changed") {
|
|
2573
|
+
void this.handleToolsListChanged();
|
|
2574
|
+
} else if (envelope["method"] === "notifications/resources/list_changed") {
|
|
2575
|
+
this.emitCapabilityChanged("resources");
|
|
2576
|
+
} else if (envelope["method"] === "notifications/prompts/list_changed") {
|
|
2577
|
+
this.emitCapabilityChanged("prompts");
|
|
2578
|
+
}
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
if (!isJsonRpcResponse(msg)) return;
|
|
2582
|
+
if (this.pending.has(msg.id)) {
|
|
1326
2583
|
const entry = this.pending.get(msg.id);
|
|
1327
2584
|
this.pending.delete(msg.id);
|
|
1328
2585
|
entry?.resolve(msg);
|
|
1329
|
-
return;
|
|
1330
2586
|
}
|
|
1331
|
-
|
|
1332
|
-
|
|
2587
|
+
}
|
|
2588
|
+
handleServerRequest(request3) {
|
|
2589
|
+
const message = request3.method === "sampling/createMessage" ? "Client sampling is disabled by policy" : `Method not found: ${request3.method}`;
|
|
2590
|
+
const response = {
|
|
2591
|
+
jsonrpc: "2.0",
|
|
2592
|
+
id: request3.id,
|
|
2593
|
+
error: { code: -32601, message }
|
|
2594
|
+
};
|
|
2595
|
+
try {
|
|
2596
|
+
this.child?.stdin?.write(`${JSON.stringify(response)}
|
|
2597
|
+
`);
|
|
2598
|
+
} catch {
|
|
1333
2599
|
}
|
|
1334
2600
|
}
|
|
1335
2601
|
/**
|
|
@@ -1361,138 +2627,761 @@ var MCPClient = class {
|
|
|
1361
2627
|
removeToolsChangedListener(listener) {
|
|
1362
2628
|
this.toolsChangedListeners.delete(listener);
|
|
1363
2629
|
}
|
|
2630
|
+
addResourcesChangedListener(listener) {
|
|
2631
|
+
this.resourcesChangedListeners.add(listener);
|
|
2632
|
+
}
|
|
2633
|
+
removeResourcesChangedListener(listener) {
|
|
2634
|
+
this.resourcesChangedListeners.delete(listener);
|
|
2635
|
+
}
|
|
2636
|
+
addPromptsChangedListener(listener) {
|
|
2637
|
+
this.promptsChangedListeners.add(listener);
|
|
2638
|
+
}
|
|
2639
|
+
removePromptsChangedListener(listener) {
|
|
2640
|
+
this.promptsChangedListeners.delete(listener);
|
|
2641
|
+
}
|
|
2642
|
+
emitCapabilityChanged(capability) {
|
|
2643
|
+
const listeners = capability === "resources" ? this.resourcesChangedListeners : this.promptsChangedListeners;
|
|
2644
|
+
for (const listener of listeners) {
|
|
2645
|
+
try {
|
|
2646
|
+
listener(this.opts.name);
|
|
2647
|
+
} catch {
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
1364
2651
|
};
|
|
1365
2652
|
function quoteWindowsArg(arg) {
|
|
1366
2653
|
if (!/[\s"]/.test(arg)) return arg;
|
|
1367
2654
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
1368
2655
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
if (
|
|
1375
|
-
|
|
1376
|
-
if (schema && typeof schema === "object") {
|
|
1377
|
-
const props = schema.properties;
|
|
1378
|
-
if (props) {
|
|
1379
|
-
for (const key of Object.keys(props)) {
|
|
1380
|
-
if (MUTATING_RE.test(key)) return true;
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
2656
|
+
var MAX_PROTOCOL_INPUT_CHARS = 8192;
|
|
2657
|
+
function validateProtocolString(value, label, allowEmpty = false) {
|
|
2658
|
+
if (typeof value !== "string" || !allowEmpty && value.length === 0) {
|
|
2659
|
+
throw new Error(`MCP ${label} must be ${allowEmpty ? "a string" : "a non-empty string"}`);
|
|
2660
|
+
}
|
|
2661
|
+
if (value.length > MAX_PROTOCOL_INPUT_CHARS) {
|
|
2662
|
+
throw new Error(`MCP ${label} exceeds ${MAX_PROTOCOL_INPUT_CHARS} characters`);
|
|
1383
2663
|
}
|
|
1384
|
-
return false;
|
|
1385
2664
|
}
|
|
1386
|
-
function
|
|
1387
|
-
|
|
2665
|
+
function pageParams(cursor, label) {
|
|
2666
|
+
if (cursor === void 0) return {};
|
|
2667
|
+
validateProtocolString(cursor, label);
|
|
2668
|
+
return { cursor };
|
|
2669
|
+
}
|
|
2670
|
+
function parseEmptyResult(value) {
|
|
2671
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2672
|
+
throw new Error("Malformed MCP empty result: expected object");
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
// src/content-selection.ts
|
|
2677
|
+
var DEFAULT_MCP_INSERTION_MAX_BYTES = 256 * 1024;
|
|
2678
|
+
var DEFAULT_MCP_RESOURCE_SCHEMES = [
|
|
2679
|
+
"file",
|
|
2680
|
+
"git",
|
|
2681
|
+
"http",
|
|
2682
|
+
"https",
|
|
2683
|
+
"mcp",
|
|
2684
|
+
"mem",
|
|
2685
|
+
"repo",
|
|
2686
|
+
"resource"
|
|
2687
|
+
];
|
|
2688
|
+
function prepareResourceInsertion(serverName, requestedUri, result, policy = {}) {
|
|
2689
|
+
requireIdentity(serverName, "server name");
|
|
2690
|
+
validateUri(requestedUri, policy);
|
|
2691
|
+
if (result.contents.length > 64) {
|
|
2692
|
+
throw new Error("MCP resource insertion exceeds the limit of 64 content blocks");
|
|
2693
|
+
}
|
|
2694
|
+
let byteSize = 0;
|
|
2695
|
+
for (const content of result.contents) {
|
|
2696
|
+
validateUri(content.uri, policy);
|
|
2697
|
+
if (content.text !== void 0) byteSize += utf8Bytes(content.text);
|
|
2698
|
+
if (content.blob !== void 0) byteSize += base64DecodedBytes(content.blob);
|
|
2699
|
+
enforceSize(byteSize, policy);
|
|
2700
|
+
}
|
|
1388
2701
|
return {
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
if (res.isError) {
|
|
1400
|
-
throw new Error(stringify(res.content));
|
|
1401
|
-
}
|
|
1402
|
-
return stringify(res.content);
|
|
1403
|
-
}
|
|
2702
|
+
kind: "resource",
|
|
2703
|
+
untrusted: true,
|
|
2704
|
+
byteSize,
|
|
2705
|
+
provenance: {
|
|
2706
|
+
origin: "mcp",
|
|
2707
|
+
serverName,
|
|
2708
|
+
capability: "resource",
|
|
2709
|
+
resourceUri: requestedUri
|
|
2710
|
+
},
|
|
2711
|
+
contents: structuredClone(result.contents)
|
|
1404
2712
|
};
|
|
1405
2713
|
}
|
|
1406
|
-
function
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
const t = item.type;
|
|
1412
|
-
if (t === "text") return item.text ?? "";
|
|
1413
|
-
return JSON.stringify(item);
|
|
1414
|
-
}
|
|
1415
|
-
return String(item);
|
|
1416
|
-
}).join("\n");
|
|
2714
|
+
function preparePromptInsertion(serverName, promptName, args, result, policy = {}) {
|
|
2715
|
+
requireIdentity(serverName, "server name");
|
|
2716
|
+
requireIdentity(promptName, "prompt name");
|
|
2717
|
+
if (result.messages.length > 128) {
|
|
2718
|
+
throw new Error("MCP prompt insertion exceeds the limit of 128 messages");
|
|
1417
2719
|
}
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
2720
|
+
for (const message of result.messages) validateEmbeddedUris(message.content, policy, 0);
|
|
2721
|
+
let serialized;
|
|
2722
|
+
try {
|
|
2723
|
+
serialized = JSON.stringify(result.messages);
|
|
2724
|
+
} catch {
|
|
2725
|
+
throw new Error("MCP prompt insertion contains non-serializable content");
|
|
1423
2726
|
}
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
});
|
|
1441
|
-
return createHash("sha256").update(basis).digest("hex").slice(0, 16);
|
|
2727
|
+
const byteSize = utf8Bytes(serialized);
|
|
2728
|
+
enforceSize(byteSize, policy);
|
|
2729
|
+
return {
|
|
2730
|
+
kind: "prompt",
|
|
2731
|
+
untrusted: true,
|
|
2732
|
+
byteSize,
|
|
2733
|
+
provenance: {
|
|
2734
|
+
origin: "mcp",
|
|
2735
|
+
serverName,
|
|
2736
|
+
capability: "prompt",
|
|
2737
|
+
promptName,
|
|
2738
|
+
promptArgumentNames: Object.keys(args ?? {}).sort()
|
|
2739
|
+
},
|
|
2740
|
+
description: result.description,
|
|
2741
|
+
messages: structuredClone(result.messages)
|
|
2742
|
+
};
|
|
1442
2743
|
}
|
|
1443
|
-
function
|
|
1444
|
-
|
|
1445
|
-
|
|
2744
|
+
function validateEmbeddedUris(value, policy, depth) {
|
|
2745
|
+
if (depth > 32) throw new Error("MCP prompt insertion exceeds the nesting depth limit");
|
|
2746
|
+
if (Array.isArray(value)) {
|
|
2747
|
+
for (const item of value) validateEmbeddedUris(item, policy, depth + 1);
|
|
2748
|
+
return;
|
|
2749
|
+
}
|
|
2750
|
+
if (!value || typeof value !== "object") return;
|
|
2751
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
2752
|
+
if (key === "uri" && typeof nested === "string") validateUri(nested, policy);
|
|
2753
|
+
validateEmbeddedUris(nested, policy, depth + 1);
|
|
2754
|
+
}
|
|
1446
2755
|
}
|
|
1447
|
-
|
|
2756
|
+
function validateUri(uri, policy) {
|
|
2757
|
+
if (uri.length === 0 || uri.length > 8192) {
|
|
2758
|
+
throw new Error("MCP insertion URI must contain 1\u20138192 characters");
|
|
2759
|
+
}
|
|
2760
|
+
let parsed;
|
|
1448
2761
|
try {
|
|
1449
|
-
|
|
1450
|
-
const parsed = JSON.parse(raw);
|
|
1451
|
-
if (parsed.configHash !== configHash || !Array.isArray(parsed.tools)) return null;
|
|
1452
|
-
return parsed.tools;
|
|
2762
|
+
parsed = new URL(uri);
|
|
1453
2763
|
} catch {
|
|
1454
|
-
|
|
2764
|
+
throw new Error("MCP insertion URI must be absolute");
|
|
2765
|
+
}
|
|
2766
|
+
const scheme = parsed.protocol.slice(0, -1).toLowerCase();
|
|
2767
|
+
const allowed = new Set(
|
|
2768
|
+
(policy.allowedUriSchemes ?? DEFAULT_MCP_RESOURCE_SCHEMES).map((value) => value.toLowerCase())
|
|
2769
|
+
);
|
|
2770
|
+
if (!allowed.has(scheme)) {
|
|
2771
|
+
throw new Error(`MCP insertion URI scheme "${scheme}" is not allowed`);
|
|
2772
|
+
}
|
|
2773
|
+
if ((scheme === "http" || scheme === "https") && (parsed.username || parsed.password)) {
|
|
2774
|
+
throw new Error("MCP insertion URI must not contain credentials");
|
|
1455
2775
|
}
|
|
1456
2776
|
}
|
|
1457
|
-
|
|
2777
|
+
function enforceSize(byteSize, policy) {
|
|
2778
|
+
const maxBytes = policy.maxBytes ?? DEFAULT_MCP_INSERTION_MAX_BYTES;
|
|
2779
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
|
|
2780
|
+
throw new Error("MCP insertion maxBytes must be a positive safe integer");
|
|
2781
|
+
}
|
|
2782
|
+
if (byteSize > maxBytes) {
|
|
2783
|
+
throw new Error(`MCP insertion exceeds the ${maxBytes}-byte content limit`);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
function base64DecodedBytes(blob) {
|
|
2787
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(blob)) {
|
|
2788
|
+
throw new Error("MCP resource insertion contains invalid base64 content");
|
|
2789
|
+
}
|
|
2790
|
+
const padding = blob.endsWith("==") ? 2 : blob.endsWith("=") ? 1 : 0;
|
|
2791
|
+
return blob.length / 4 * 3 - padding;
|
|
2792
|
+
}
|
|
2793
|
+
function utf8Bytes(value) {
|
|
2794
|
+
return new TextEncoder().encode(value).byteLength;
|
|
2795
|
+
}
|
|
2796
|
+
function requireIdentity(value, label) {
|
|
2797
|
+
if (value.length === 0 || value.length > 256) {
|
|
2798
|
+
throw new Error(`MCP insertion ${label} must contain 1\u2013256 characters`);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
// src/manage.ts
|
|
2803
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
2804
|
+
import * as fs from "node:fs/promises";
|
|
2805
|
+
async function readConfig(path2) {
|
|
1458
2806
|
try {
|
|
1459
|
-
|
|
1460
|
-
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
1461
|
-
const body = { configHash, tools };
|
|
1462
|
-
const tmp = `${file}.tmp`;
|
|
1463
|
-
await fs.writeFile(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
1464
|
-
await fs.rename(tmp, file);
|
|
2807
|
+
return JSON.parse(await fs.readFile(path2, "utf8"));
|
|
1465
2808
|
} catch {
|
|
2809
|
+
return {};
|
|
1466
2810
|
}
|
|
1467
2811
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
idleTimeoutMs;
|
|
1478
|
-
/** Single shared idle sweep timer (started lazily; unref'd; cleared on stopAll). */
|
|
1479
|
-
idleTimer;
|
|
1480
|
-
constructor(opts) {
|
|
1481
|
-
this.toolRegistry = opts.toolRegistry;
|
|
1482
|
-
this.events = opts.events;
|
|
1483
|
-
this.log = opts.log;
|
|
1484
|
-
this.lazyMode = opts.lazyMode ?? false;
|
|
1485
|
-
this.cacheDir = opts.cacheDir;
|
|
1486
|
-
this.idleTimeoutMs = opts.idleTimeoutMs ?? MCP_CONSTANTS.IDLE.DEFAULT_TIMEOUT_MS;
|
|
2812
|
+
async function writeConfig(path2, cfg) {
|
|
2813
|
+
const raw = JSON.stringify(cfg, null, 2);
|
|
2814
|
+
const tmp = `${path2}.${process.pid}.${randomBytes3(6).toString("hex")}.tmp`;
|
|
2815
|
+
await fs.writeFile(tmp, raw, "utf8");
|
|
2816
|
+
try {
|
|
2817
|
+
await fs.rename(tmp, path2);
|
|
2818
|
+
} catch (err) {
|
|
2819
|
+
await fs.rm(tmp, { force: true }).catch(() => void 0);
|
|
2820
|
+
throw err;
|
|
1487
2821
|
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
2822
|
+
}
|
|
2823
|
+
function isMcpServerRecord(value) {
|
|
2824
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2825
|
+
}
|
|
2826
|
+
async function readServers(configPath) {
|
|
2827
|
+
const full = await readConfig(configPath);
|
|
2828
|
+
const servers = isMcpServerRecord(full.mcpServers) ? { ...full.mcpServers } : {};
|
|
2829
|
+
return { full, servers };
|
|
2830
|
+
}
|
|
2831
|
+
async function persist(configPath, full, servers) {
|
|
2832
|
+
full.mcpServers = servers;
|
|
2833
|
+
await writeConfig(configPath, full);
|
|
2834
|
+
}
|
|
2835
|
+
function normalizeTransport(t) {
|
|
2836
|
+
if (t === "sse") return "sse";
|
|
2837
|
+
if (t === "http" || t === "streamable-http") return "streamable-http";
|
|
2838
|
+
return "stdio";
|
|
2839
|
+
}
|
|
2840
|
+
function buildConfig(input, base) {
|
|
2841
|
+
const cfg = {
|
|
2842
|
+
name: input.name,
|
|
2843
|
+
transport: input.transport ? normalizeTransport(String(input.transport)) : base?.transport ?? "stdio"
|
|
2844
|
+
};
|
|
2845
|
+
const description = input.description ?? base?.description;
|
|
2846
|
+
if (description !== void 0) cfg.description = description;
|
|
2847
|
+
const command = input.command ?? base?.command;
|
|
2848
|
+
if (command !== void 0) cfg.command = command;
|
|
2849
|
+
const args = input.args ?? base?.args;
|
|
2850
|
+
if (args !== void 0) cfg.args = args;
|
|
2851
|
+
const env = input.env ?? base?.env;
|
|
2852
|
+
if (env !== void 0) cfg.env = env;
|
|
2853
|
+
const url = input.url ?? base?.url;
|
|
2854
|
+
if (url !== void 0) cfg.url = url;
|
|
2855
|
+
const headers = input.headers ?? base?.headers;
|
|
2856
|
+
if (headers !== void 0) cfg.headers = headers;
|
|
2857
|
+
const allowedTools = input.allowedTools ?? base?.allowedTools;
|
|
2858
|
+
if (allowedTools !== void 0) cfg.allowedTools = allowedTools;
|
|
2859
|
+
const permission = input.permission ?? base?.permission;
|
|
2860
|
+
if (permission !== void 0) cfg.permission = permission;
|
|
2861
|
+
const enabled = input.enabled ?? base?.enabled;
|
|
2862
|
+
if (enabled !== void 0) cfg.enabled = enabled;
|
|
2863
|
+
const lazy = input.lazy ?? base?.lazy;
|
|
2864
|
+
if (lazy !== void 0) cfg.lazy = lazy;
|
|
2865
|
+
const passthroughEnv = input.passthroughEnv ?? base?.passthroughEnv;
|
|
2866
|
+
if (passthroughEnv !== void 0) cfg.passthroughEnv = passthroughEnv;
|
|
2867
|
+
const health = input.health ?? base?.health;
|
|
2868
|
+
if (health !== void 0) cfg.health = health;
|
|
2869
|
+
return cfg;
|
|
2870
|
+
}
|
|
2871
|
+
function projectServer(name, cfg, registry) {
|
|
2872
|
+
const live = registry.list().find((s) => s.name === name);
|
|
2873
|
+
const info = {
|
|
2874
|
+
name,
|
|
2875
|
+
transport: cfg.transport,
|
|
2876
|
+
enabled: cfg.enabled !== false,
|
|
2877
|
+
status: live ? live.state : "stopped",
|
|
2878
|
+
tools: live?.tools ?? []
|
|
2879
|
+
};
|
|
2880
|
+
if (cfg.description !== void 0) info.description = cfg.description;
|
|
2881
|
+
if (cfg.url !== void 0) info.url = cfg.url;
|
|
2882
|
+
if (cfg.command !== void 0) info.command = cfg.command;
|
|
2883
|
+
if (cfg.lazy !== void 0) info.lazy = cfg.lazy;
|
|
2884
|
+
return info;
|
|
2885
|
+
}
|
|
2886
|
+
function liveState(name, registry) {
|
|
2887
|
+
const live = registry.list().find((s) => s.name === name);
|
|
2888
|
+
return { state: live?.state ?? "stopped", tools: live?.tools ?? [] };
|
|
2889
|
+
}
|
|
2890
|
+
function errMessage(err) {
|
|
2891
|
+
return err instanceof Error ? err.message : String(err);
|
|
2892
|
+
}
|
|
2893
|
+
async function listMcp(deps) {
|
|
2894
|
+
const { servers } = await readServers(deps.configPath);
|
|
2895
|
+
return Object.entries(servers).map(
|
|
2896
|
+
([name, cfg]) => projectServer(name, { ...cfg, name }, deps.registry)
|
|
2897
|
+
);
|
|
2898
|
+
}
|
|
2899
|
+
async function addMcp(input, deps) {
|
|
2900
|
+
if (!input.name) return { ok: false, message: "Server name is required" };
|
|
2901
|
+
const { full, servers } = await readServers(deps.configPath);
|
|
2902
|
+
if (servers[input.name]) {
|
|
2903
|
+
return { ok: false, message: `Server "${input.name}" already exists` };
|
|
2904
|
+
}
|
|
2905
|
+
const preset = deps.presets?.[input.name];
|
|
2906
|
+
const hasExplicitConfig = !!(input.transport || input.command || input.url);
|
|
2907
|
+
const cfg = hasExplicitConfig ? buildConfig(input, preset) : preset ? buildConfig({ ...input, name: input.name }, preset) : buildConfig(input);
|
|
2908
|
+
if (!hasExplicitConfig && !preset) {
|
|
2909
|
+
const known = Object.keys(deps.presets ?? {}).join(", ");
|
|
2910
|
+
return {
|
|
2911
|
+
ok: false,
|
|
2912
|
+
message: known ? `Unknown server "${input.name}". Available presets: ${known}` : `No configuration provided for "${input.name}"`
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
cfg.enabled = input.enabled ?? false;
|
|
2916
|
+
servers[input.name] = cfg;
|
|
2917
|
+
await persist(deps.configPath, full, servers);
|
|
2918
|
+
if (cfg.enabled) {
|
|
2919
|
+
return startServer(input.name, cfg, deps, `Server "${input.name}" added`);
|
|
2920
|
+
}
|
|
2921
|
+
trackDisabled(deps.registry, cfg);
|
|
2922
|
+
return {
|
|
2923
|
+
ok: true,
|
|
2924
|
+
message: `Server "${input.name}" added (disabled)`,
|
|
2925
|
+
server: projectServer(input.name, cfg, deps.registry)
|
|
2926
|
+
};
|
|
2927
|
+
}
|
|
2928
|
+
async function updateMcp(input, deps) {
|
|
2929
|
+
if (!input.name) return { ok: false, message: "Server name is required" };
|
|
2930
|
+
const { full, servers } = await readServers(deps.configPath);
|
|
2931
|
+
const existing = servers[input.name];
|
|
2932
|
+
if (!existing) return { ok: false, message: `Server "${input.name}" not found` };
|
|
2933
|
+
const cfg = buildConfig(input, { ...existing, name: input.name });
|
|
2934
|
+
servers[input.name] = cfg;
|
|
2935
|
+
await persist(deps.configPath, full, servers);
|
|
2936
|
+
if (cfg.enabled !== false) {
|
|
2937
|
+
return startServer(input.name, cfg, deps, `Server "${input.name}" updated`, { restart: true });
|
|
2938
|
+
}
|
|
2939
|
+
await safeStop(input.name, deps);
|
|
2940
|
+
trackDisabled(deps.registry, cfg);
|
|
2941
|
+
return {
|
|
2942
|
+
ok: true,
|
|
2943
|
+
message: `Server "${input.name}" updated`,
|
|
2944
|
+
server: projectServer(input.name, cfg, deps.registry)
|
|
2945
|
+
};
|
|
2946
|
+
}
|
|
2947
|
+
async function removeMcp(name, deps) {
|
|
2948
|
+
if (!name) return { ok: false, message: "Server name is required" };
|
|
2949
|
+
const { full, servers } = await readServers(deps.configPath);
|
|
2950
|
+
if (!servers[name]) return { ok: false, message: `Server "${name}" not found` };
|
|
2951
|
+
await safeStop(name, deps);
|
|
2952
|
+
forgetRegistryState(deps.registry, name);
|
|
2953
|
+
delete servers[name];
|
|
2954
|
+
await persist(deps.configPath, full, servers);
|
|
2955
|
+
return { ok: true, message: `Server "${name}" removed` };
|
|
2956
|
+
}
|
|
2957
|
+
async function enableMcp(name, deps) {
|
|
2958
|
+
if (!name) return { ok: false, message: "Server name is required" };
|
|
2959
|
+
const { full, servers } = await readServers(deps.configPath);
|
|
2960
|
+
const cfg = servers[name];
|
|
2961
|
+
if (!cfg) {
|
|
2962
|
+
return { ok: false, message: `Server "${name}" is not in config. Add it first.` };
|
|
2963
|
+
}
|
|
2964
|
+
cfg.enabled = true;
|
|
2965
|
+
servers[name] = cfg;
|
|
2966
|
+
await persist(deps.configPath, full, servers);
|
|
2967
|
+
return startServer(name, cfg, deps, `Server "${name}" enabled`, { restart: true });
|
|
2968
|
+
}
|
|
2969
|
+
async function disableMcp(name, deps) {
|
|
2970
|
+
if (!name) return { ok: false, message: "Server name is required" };
|
|
2971
|
+
const { full, servers } = await readServers(deps.configPath);
|
|
2972
|
+
const cfg = servers[name];
|
|
2973
|
+
if (!cfg) return { ok: false, message: `Server "${name}" is not in config.` };
|
|
2974
|
+
await safeStop(name, deps);
|
|
2975
|
+
cfg.enabled = false;
|
|
2976
|
+
trackDisabled(deps.registry, { ...cfg, name });
|
|
2977
|
+
servers[name] = cfg;
|
|
2978
|
+
await persist(deps.configPath, full, servers);
|
|
2979
|
+
return {
|
|
2980
|
+
ok: true,
|
|
2981
|
+
message: `Server "${name}" disabled`,
|
|
2982
|
+
server: projectServer(name, cfg, deps.registry)
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
async function restartMcp(name, deps) {
|
|
2986
|
+
if (!name) return { ok: false, message: "Server name is required" };
|
|
2987
|
+
const registered = deps.registry.list().some((s) => s.name === name);
|
|
2988
|
+
if (registered) {
|
|
2989
|
+
try {
|
|
2990
|
+
await deps.registry.restart(name);
|
|
2991
|
+
const { state, tools } = liveState(name, deps.registry);
|
|
2992
|
+
return { ok: true, message: `Server "${name}" restarted`, state, tools };
|
|
2993
|
+
} catch (err) {
|
|
2994
|
+
return { ok: false, message: `Failed to restart "${name}": ${errMessage(err)}` };
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
const { servers } = await readServers(deps.configPath);
|
|
2998
|
+
const cfg = servers[name];
|
|
2999
|
+
if (!cfg) return { ok: false, message: `Server "${name}" is not in config.` };
|
|
3000
|
+
return startServer(name, { ...cfg, name }, deps, `Server "${name}" started`, { restart: true });
|
|
3001
|
+
}
|
|
3002
|
+
async function discoverMcp(name, deps) {
|
|
3003
|
+
if (!name) return { ok: false, message: "Server name is required" };
|
|
3004
|
+
const result = await restartMcp(name, deps);
|
|
3005
|
+
if (!result.ok) return result;
|
|
3006
|
+
const { state, tools } = liveState(name, deps.registry);
|
|
3007
|
+
return {
|
|
3008
|
+
ok: true,
|
|
3009
|
+
message: `Discovered ${tools.length} tool${tools.length === 1 ? "" : "s"} from "${name}"`,
|
|
3010
|
+
state,
|
|
3011
|
+
tools
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
async function startServer(name, cfg, deps, okMessage, opts) {
|
|
3015
|
+
try {
|
|
3016
|
+
const alreadyRegistered = deps.registry.list().some((s) => s.name === name);
|
|
3017
|
+
if (alreadyRegistered && opts?.restart) {
|
|
3018
|
+
await deps.registry.restart(name);
|
|
3019
|
+
} else if (alreadyRegistered) {
|
|
3020
|
+
await deps.registry.restart(name);
|
|
3021
|
+
} else {
|
|
3022
|
+
await deps.registry.start({ ...cfg, enabled: true });
|
|
3023
|
+
}
|
|
3024
|
+
const { state, tools } = liveState(name, deps.registry);
|
|
3025
|
+
return {
|
|
3026
|
+
ok: true,
|
|
3027
|
+
message: okMessage,
|
|
3028
|
+
server: projectServer(name, cfg, deps.registry),
|
|
3029
|
+
state,
|
|
3030
|
+
tools
|
|
3031
|
+
};
|
|
3032
|
+
} catch (err) {
|
|
3033
|
+
const message = errMessage(err);
|
|
3034
|
+
return {
|
|
3035
|
+
ok: true,
|
|
3036
|
+
// config persisted — surface a soft warning, not a hard failure
|
|
3037
|
+
message: `${okMessage} in config, but failed to start: ${message}`,
|
|
3038
|
+
server: projectServer(name, cfg, deps.registry),
|
|
3039
|
+
registryError: message
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
async function safeStop(name, deps) {
|
|
3044
|
+
try {
|
|
3045
|
+
await deps.registry.stop(name);
|
|
3046
|
+
} catch {
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
function trackDisabled(registry, cfg) {
|
|
3050
|
+
if (typeof registry.markDisabled === "function") registry.markDisabled(cfg);
|
|
3051
|
+
}
|
|
3052
|
+
function forgetRegistryState(registry, name) {
|
|
3053
|
+
if (typeof registry.forget === "function") registry.forget(name);
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
// src/manifest-cache.ts
|
|
3057
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3058
|
+
import * as fs2 from "node:fs/promises";
|
|
3059
|
+
import * as path from "node:path";
|
|
3060
|
+
function manifestConfigHash(cfg) {
|
|
3061
|
+
const basis = JSON.stringify({
|
|
3062
|
+
transport: cfg.transport,
|
|
3063
|
+
command: cfg.command ?? null,
|
|
3064
|
+
args: cfg.args ?? null,
|
|
3065
|
+
url: cfg.url ?? null
|
|
3066
|
+
});
|
|
3067
|
+
return createHash2("sha256").update(basis).digest("hex").slice(0, 16);
|
|
3068
|
+
}
|
|
3069
|
+
function manifestFile(cacheDir, name) {
|
|
3070
|
+
const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
3071
|
+
return path.join(cacheDir, "mcp-tools", `${safe}.json`);
|
|
3072
|
+
}
|
|
3073
|
+
async function readManifest(cacheDir, name, configHash) {
|
|
3074
|
+
const manifest = await readCapabilityManifest(cacheDir, name, configHash);
|
|
3075
|
+
return manifest?.tools ?? null;
|
|
3076
|
+
}
|
|
3077
|
+
async function readCapabilityManifest(cacheDir, name, configHash) {
|
|
3078
|
+
try {
|
|
3079
|
+
const raw = await fs2.readFile(manifestFile(cacheDir, name), "utf8");
|
|
3080
|
+
const parsed = JSON.parse(raw);
|
|
3081
|
+
if (parsed.configHash !== configHash || !Array.isArray(parsed.tools)) return null;
|
|
3082
|
+
return {
|
|
3083
|
+
tools: parsed.tools,
|
|
3084
|
+
serverMetadata: parsed.serverMetadata === void 0 ? void 0 : parseServerMetadata(parsed.serverMetadata),
|
|
3085
|
+
resources: parsed.resources === void 0 ? void 0 : parseListResourcesResult({ resources: parsed.resources }).resources,
|
|
3086
|
+
resourceTemplates: parsed.resourceTemplates === void 0 ? void 0 : parseListResourceTemplatesResult({ resourceTemplates: parsed.resourceTemplates }).resourceTemplates,
|
|
3087
|
+
prompts: parsed.prompts === void 0 ? void 0 : parseListPromptsResult({ prompts: parsed.prompts }).prompts
|
|
3088
|
+
};
|
|
3089
|
+
} catch {
|
|
3090
|
+
return null;
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
async function writeManifest(cacheDir, name, configHash, tools) {
|
|
3094
|
+
const previous = await readCapabilityManifest(cacheDir, name, configHash);
|
|
3095
|
+
await writeCapabilityManifest(cacheDir, name, configHash, {
|
|
3096
|
+
...previous,
|
|
3097
|
+
tools
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
async function writeCapabilityManifest(cacheDir, name, configHash, manifest) {
|
|
3101
|
+
try {
|
|
3102
|
+
const file = manifestFile(cacheDir, name);
|
|
3103
|
+
await fs2.mkdir(path.dirname(file), { recursive: true });
|
|
3104
|
+
const body = { version: 2, configHash, ...manifest };
|
|
3105
|
+
const tmp = `${file}.tmp`;
|
|
3106
|
+
await fs2.writeFile(tmp, JSON.stringify(body, null, 2), "utf8");
|
|
3107
|
+
await fs2.rename(tmp, file);
|
|
3108
|
+
} catch {
|
|
3109
|
+
}
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
// src/operations.ts
|
|
3113
|
+
var MCP_OPERATION_LIMITS = Object.freeze({
|
|
3114
|
+
LATENCY_SAMPLES: 128,
|
|
3115
|
+
RECENT_EVENTS: 32,
|
|
3116
|
+
REASON_CHARS: 64
|
|
3117
|
+
});
|
|
3118
|
+
var SAFE_OPERATION_REASONS = /* @__PURE__ */ new Set([
|
|
3119
|
+
"automatic",
|
|
3120
|
+
"complete",
|
|
3121
|
+
"connect-attempt-failed",
|
|
3122
|
+
"connected",
|
|
3123
|
+
"http-disconnect",
|
|
3124
|
+
"http-disconnect-lazy",
|
|
3125
|
+
"idle-timeout",
|
|
3126
|
+
"lazy-demand",
|
|
3127
|
+
"manual",
|
|
3128
|
+
"ok",
|
|
3129
|
+
"process-exit",
|
|
3130
|
+
"process-exit-lazy",
|
|
3131
|
+
"prompt-discovery-failed",
|
|
3132
|
+
"reconnect-exhausted",
|
|
3133
|
+
"resource-discovery-failed",
|
|
3134
|
+
"resource-template-discovery-failed",
|
|
3135
|
+
"started",
|
|
3136
|
+
"tool-call-failed"
|
|
3137
|
+
]);
|
|
3138
|
+
function createMCPServerOperationState() {
|
|
3139
|
+
return {
|
|
3140
|
+
consecutiveFailures: 0,
|
|
3141
|
+
failures: { transport: 0, protocol: 0, tool: 0 },
|
|
3142
|
+
reconnectCount: 0,
|
|
3143
|
+
wakeCount: 0,
|
|
3144
|
+
sleepCount: 0,
|
|
3145
|
+
restartCount: 0,
|
|
3146
|
+
connectionSamples: [],
|
|
3147
|
+
discoverySamples: [],
|
|
3148
|
+
callSamples: [],
|
|
3149
|
+
inFlightCalls: 0,
|
|
3150
|
+
peakInFlightCalls: 0,
|
|
3151
|
+
recentEvents: []
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3154
|
+
function healthStateFor(connectionState, operations, enabled = true) {
|
|
3155
|
+
if (!enabled) return "disabled";
|
|
3156
|
+
if (connectionState === "dormant") return "dormant";
|
|
3157
|
+
if (connectionState === "connecting" || connectionState === "reconnecting" || connectionState === "idle") {
|
|
3158
|
+
return "connecting";
|
|
3159
|
+
}
|
|
3160
|
+
if (connectionState === "failed") return "failed";
|
|
3161
|
+
if (connectionState === "disconnected" || operations.consecutiveFailures > 0) return "degraded";
|
|
3162
|
+
return "healthy";
|
|
3163
|
+
}
|
|
3164
|
+
function evaluateHealthThresholds(operations, thresholds) {
|
|
3165
|
+
if (!thresholds) return [];
|
|
3166
|
+
const checks = [];
|
|
3167
|
+
if (thresholds.connectionLatencyP95Ms !== void 0 && operations.connectionSamples.length > 0) {
|
|
3168
|
+
const value = percentile([...operations.connectionSamples].sort((a, b) => a - b), 0.95);
|
|
3169
|
+
checks.push({
|
|
3170
|
+
name: "connection-latency-p95",
|
|
3171
|
+
passed: value <= thresholds.connectionLatencyP95Ms,
|
|
3172
|
+
value,
|
|
3173
|
+
threshold: thresholds.connectionLatencyP95Ms
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
if (thresholds.discoveryLatencyP95Ms !== void 0 && operations.discoverySamples.length > 0) {
|
|
3177
|
+
const value = percentile([...operations.discoverySamples].sort((a, b) => a - b), 0.95);
|
|
3178
|
+
checks.push({
|
|
3179
|
+
name: "discovery-latency-p95",
|
|
3180
|
+
passed: value <= thresholds.discoveryLatencyP95Ms,
|
|
3181
|
+
value,
|
|
3182
|
+
threshold: thresholds.discoveryLatencyP95Ms
|
|
3183
|
+
});
|
|
3184
|
+
}
|
|
3185
|
+
if (thresholds.callLatencyP95Ms !== void 0 && operations.callSamples.length > 0) {
|
|
3186
|
+
const value = percentile([...operations.callSamples].sort((a, b) => a - b), 0.95);
|
|
3187
|
+
checks.push({
|
|
3188
|
+
name: "call-latency-p95",
|
|
3189
|
+
passed: value <= thresholds.callLatencyP95Ms,
|
|
3190
|
+
value,
|
|
3191
|
+
threshold: thresholds.callLatencyP95Ms
|
|
3192
|
+
});
|
|
3193
|
+
}
|
|
3194
|
+
if (thresholds.inFlightCalls !== void 0) {
|
|
3195
|
+
checks.push({
|
|
3196
|
+
name: "in-flight-calls",
|
|
3197
|
+
passed: operations.peakInFlightCalls <= thresholds.inFlightCalls,
|
|
3198
|
+
value: operations.peakInFlightCalls,
|
|
3199
|
+
threshold: thresholds.inFlightCalls
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
return checks;
|
|
3203
|
+
}
|
|
3204
|
+
function applyHealthThresholds(state, checks) {
|
|
3205
|
+
if (state !== "healthy") return state;
|
|
3206
|
+
return checks.some((c) => !c.passed) ? "degraded" : "healthy";
|
|
3207
|
+
}
|
|
3208
|
+
function summarizeLatency(samples) {
|
|
3209
|
+
if (samples.length === 0) return { count: 0 };
|
|
3210
|
+
const sorted = [...samples].sort((a, b) => a - b);
|
|
3211
|
+
return {
|
|
3212
|
+
count: samples.length,
|
|
3213
|
+
lastMs: samples[samples.length - 1],
|
|
3214
|
+
minMs: sorted[0],
|
|
3215
|
+
maxMs: sorted[sorted.length - 1],
|
|
3216
|
+
p50Ms: percentile(sorted, 0.5),
|
|
3217
|
+
p95Ms: percentile(sorted, 0.95)
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
function pushBounded(target, value, limit) {
|
|
3221
|
+
target.push(value);
|
|
3222
|
+
if (target.length > limit) target.splice(0, target.length - limit);
|
|
3223
|
+
}
|
|
3224
|
+
function safeOperationReason(reason) {
|
|
3225
|
+
const normalized = reason.toLowerCase().replace(/[^a-z0-9_.:-]+/g, "-");
|
|
3226
|
+
const bounded = normalized.slice(0, MCP_OPERATION_LIMITS.REASON_CHARS);
|
|
3227
|
+
return SAFE_OPERATION_REASONS.has(bounded) ? bounded : "other";
|
|
3228
|
+
}
|
|
3229
|
+
function percentile(sorted, ratio) {
|
|
3230
|
+
return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1))];
|
|
3231
|
+
}
|
|
3232
|
+
|
|
3233
|
+
// src/registry.ts
|
|
3234
|
+
import { expectDefined } from "@wrongstack/core";
|
|
3235
|
+
|
|
3236
|
+
// src/wrap-tool.ts
|
|
3237
|
+
import { ToolCapabilities } from "@wrongstack/core";
|
|
3238
|
+
var MUTATING_RE = /create|update|delete|write|send|set|put|post|patch|remove|rename|move/i;
|
|
3239
|
+
function isMutatingTool(mcpTool) {
|
|
3240
|
+
if (MUTATING_RE.test(mcpTool.name)) return true;
|
|
3241
|
+
const schema = mcpTool.inputSchema;
|
|
3242
|
+
if (schema && typeof schema === "object") {
|
|
3243
|
+
const props = schema.properties;
|
|
3244
|
+
if (props) {
|
|
3245
|
+
for (const key of Object.keys(props)) {
|
|
3246
|
+
if (MUTATING_RE.test(key)) return true;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
return false;
|
|
3251
|
+
}
|
|
3252
|
+
function wrapMCPTool(serverName, mcpTool, client, permission = "confirm", observer) {
|
|
3253
|
+
const qualifiedName = `mcp__${serverName}__${mcpTool.name}`;
|
|
3254
|
+
return {
|
|
3255
|
+
name: qualifiedName,
|
|
3256
|
+
description: mcpTool.description ?? `${qualifiedName} (MCP tool)`,
|
|
3257
|
+
usageHint: `Tool provided by MCP server "${serverName}". ${mcpTool.description ?? ""}`,
|
|
3258
|
+
permission,
|
|
3259
|
+
mutating: isMutatingTool(mcpTool),
|
|
3260
|
+
capabilities: [ToolCapabilities.MCP_PROXY],
|
|
3261
|
+
inputSchema: mcpTool.inputSchema ?? { type: "object", properties: {} },
|
|
3262
|
+
async execute(input, _ctx, opts) {
|
|
3263
|
+
const startedAt = Date.now();
|
|
3264
|
+
observer?.onStart();
|
|
3265
|
+
let ok = false;
|
|
3266
|
+
try {
|
|
3267
|
+
const live = typeof client === "function" ? await client() : client;
|
|
3268
|
+
const res = await live.callTool(mcpTool.name, input, { signal: opts.signal });
|
|
3269
|
+
if (res.isError) {
|
|
3270
|
+
throw new Error(stringify(res.content));
|
|
3271
|
+
}
|
|
3272
|
+
ok = true;
|
|
3273
|
+
return stringify(res.content);
|
|
3274
|
+
} finally {
|
|
3275
|
+
observer?.onFinish({ durationMs: Date.now() - startedAt, ok });
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
}
|
|
3280
|
+
function stringify(c) {
|
|
3281
|
+
if (typeof c === "string") return c;
|
|
3282
|
+
if (Array.isArray(c)) {
|
|
3283
|
+
return c.map((item) => {
|
|
3284
|
+
if (item && typeof item === "object") {
|
|
3285
|
+
const t = item.type;
|
|
3286
|
+
if (t === "text") return item.text ?? "";
|
|
3287
|
+
return JSON.stringify(item);
|
|
3288
|
+
}
|
|
3289
|
+
return String(item);
|
|
3290
|
+
}).join("\n");
|
|
3291
|
+
}
|
|
3292
|
+
if (c && typeof c === "object") {
|
|
3293
|
+
if ("text" in c) {
|
|
3294
|
+
return String(c.text);
|
|
3295
|
+
}
|
|
3296
|
+
return JSON.stringify(c);
|
|
3297
|
+
}
|
|
3298
|
+
return String(c ?? "");
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
// src/registry.ts
|
|
3302
|
+
var MCPRegistry = class _MCPRegistry {
|
|
3303
|
+
servers = /* @__PURE__ */ new Map();
|
|
3304
|
+
/** Configured-off servers are tracked without creating a transport/client. */
|
|
3305
|
+
disabledServers = /* @__PURE__ */ new Map();
|
|
3306
|
+
toolRegistry;
|
|
3307
|
+
events;
|
|
3308
|
+
log;
|
|
3309
|
+
lazyMode;
|
|
3310
|
+
cacheDir;
|
|
3311
|
+
idleTimeoutMs;
|
|
3312
|
+
authorizationProviderFactory;
|
|
3313
|
+
authorizationManager;
|
|
3314
|
+
operationListeners = /* @__PURE__ */ new Set();
|
|
3315
|
+
/** Single shared idle sweep timer (started lazily; unref'd; cleared on stopAll). */
|
|
3316
|
+
idleTimer;
|
|
3317
|
+
constructor(opts) {
|
|
3318
|
+
this.toolRegistry = opts.toolRegistry;
|
|
3319
|
+
this.events = opts.events;
|
|
3320
|
+
this.log = opts.log;
|
|
3321
|
+
this.lazyMode = opts.lazyMode ?? false;
|
|
3322
|
+
this.cacheDir = opts.cacheDir;
|
|
3323
|
+
this.idleTimeoutMs = opts.idleTimeoutMs ?? MCP_CONSTANTS.IDLE.DEFAULT_TIMEOUT_MS;
|
|
3324
|
+
this.authorizationProviderFactory = opts.authorizationProviderFactory;
|
|
3325
|
+
this.authorizationManager = opts.authorizationManager;
|
|
3326
|
+
}
|
|
3327
|
+
requireSlot(name) {
|
|
3328
|
+
const slot = this.servers.get(name);
|
|
3329
|
+
if (!slot) throw new Error(`MCP server "${name}" not registered`);
|
|
3330
|
+
return slot;
|
|
3331
|
+
}
|
|
3332
|
+
async beginAuthorization(name, input) {
|
|
3333
|
+
const manager = this.requireAuthorizationManager();
|
|
3334
|
+
const cfg = this.requireHttpServerConfig(name);
|
|
3335
|
+
return manager.begin({
|
|
3336
|
+
serverName: name,
|
|
3337
|
+
resource: cfg.url,
|
|
3338
|
+
...input
|
|
3339
|
+
});
|
|
3340
|
+
}
|
|
3341
|
+
async completeAuthorization(name, callbackUrl, signal) {
|
|
3342
|
+
const manager = this.requireAuthorizationManager();
|
|
3343
|
+
const cfg = this.requireHttpServerConfig(name);
|
|
3344
|
+
return manager.complete({ serverName: name, resource: cfg.url, callbackUrl, signal });
|
|
3345
|
+
}
|
|
3346
|
+
async authorizationStatus(name) {
|
|
3347
|
+
const manager = this.requireAuthorizationManager();
|
|
3348
|
+
const cfg = this.requireHttpServerConfig(name);
|
|
3349
|
+
return manager.status(name, cfg.url);
|
|
3350
|
+
}
|
|
3351
|
+
async disconnectAuthorization(name) {
|
|
3352
|
+
const manager = this.requireAuthorizationManager();
|
|
3353
|
+
const cfg = this.requireHttpServerConfig(name);
|
|
3354
|
+
return manager.disconnect(name, cfg.url);
|
|
3355
|
+
}
|
|
3356
|
+
requireAuthorizationManager() {
|
|
3357
|
+
if (!this.authorizationManager) {
|
|
3358
|
+
throw new Error("MCP authorization management is not configured for this host");
|
|
3359
|
+
}
|
|
3360
|
+
return this.authorizationManager;
|
|
3361
|
+
}
|
|
3362
|
+
requireHttpServerConfig(name) {
|
|
3363
|
+
const cfg = this.servers.get(name)?.cfg ?? this.disabledServers.get(name);
|
|
3364
|
+
if (!cfg) throw new Error(`MCP server "${name}" not registered`);
|
|
3365
|
+
if (cfg.transport === "stdio" || !cfg.url) {
|
|
3366
|
+
throw new Error(`MCP server "${name}" does not use an HTTP transport`);
|
|
3367
|
+
}
|
|
3368
|
+
return cfg;
|
|
3369
|
+
}
|
|
3370
|
+
async start(cfg) {
|
|
3371
|
+
if (cfg.enabled === false) {
|
|
3372
|
+
if (this.servers.has(cfg.name)) {
|
|
3373
|
+
await this.stop(cfg.name);
|
|
3374
|
+
}
|
|
3375
|
+
this.markDisabled(cfg);
|
|
3376
|
+
return;
|
|
3377
|
+
}
|
|
3378
|
+
this.disabledServers.delete(cfg.name);
|
|
3379
|
+
if (this.servers.has(cfg.name)) {
|
|
3380
|
+
throw new Error(
|
|
3381
|
+
`MCP server "${cfg.name}" is already registered \u2014 use restart() to re-cycle a running server`
|
|
3382
|
+
);
|
|
3383
|
+
}
|
|
3384
|
+
const lazy = !!cfg.lazy && !!this.cacheDir;
|
|
1496
3385
|
const slot = {
|
|
1497
3386
|
cfg,
|
|
1498
3387
|
state: "idle",
|
|
@@ -1503,7 +3392,8 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1503
3392
|
reconnectCycles: 0,
|
|
1504
3393
|
lazy,
|
|
1505
3394
|
lastUsed: Date.now(),
|
|
1506
|
-
registeredLazy: false
|
|
3395
|
+
registeredLazy: false,
|
|
3396
|
+
operations: createMCPServerOperationState()
|
|
1507
3397
|
};
|
|
1508
3398
|
this.servers.set(cfg.name, slot);
|
|
1509
3399
|
if (lazy) {
|
|
@@ -1512,6 +3402,16 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1512
3402
|
await this.attemptConnect(slot);
|
|
1513
3403
|
}
|
|
1514
3404
|
}
|
|
3405
|
+
/** Record an intentionally disabled configuration without opening a transport. */
|
|
3406
|
+
markDisabled(cfg) {
|
|
3407
|
+
this.servers.delete(cfg.name);
|
|
3408
|
+
this.disabledServers.set(cfg.name, { ...cfg, enabled: false });
|
|
3409
|
+
}
|
|
3410
|
+
/** Remove residual operational/configuration state after a management delete. */
|
|
3411
|
+
forget(name) {
|
|
3412
|
+
this.servers.delete(name);
|
|
3413
|
+
this.disabledServers.delete(name);
|
|
3414
|
+
}
|
|
1515
3415
|
/**
|
|
1516
3416
|
* Boot a lazy server WITHOUT spawning it. If a tool manifest is cached (from a
|
|
1517
3417
|
* prior connect with matching config), register resolver-backed wrappers and
|
|
@@ -1525,13 +3425,17 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1525
3425
|
return;
|
|
1526
3426
|
}
|
|
1527
3427
|
const hash = manifestConfigHash(slot.cfg);
|
|
1528
|
-
const cached = await
|
|
1529
|
-
if (cached
|
|
1530
|
-
|
|
3428
|
+
const cached = await readCapabilityManifest(cacheDir, slot.cfg.name, hash);
|
|
3429
|
+
if (cached) {
|
|
3430
|
+
slot.serverMetadata = cached.serverMetadata;
|
|
3431
|
+
slot.resources = cached.resources;
|
|
3432
|
+
slot.resourceTemplates = cached.resourceTemplates;
|
|
3433
|
+
slot.prompts = cached.prompts;
|
|
3434
|
+
this.applyTools(slot, cached.tools);
|
|
1531
3435
|
slot.state = "dormant";
|
|
1532
3436
|
this.ensureIdleSweep();
|
|
1533
3437
|
this.log.info(
|
|
1534
|
-
`MCP server "${slot.cfg.name}" registered lazily from cache (${cached.length} tools, dormant)`
|
|
3438
|
+
`MCP server "${slot.cfg.name}" registered lazily from cache (${cached.tools.length} tools, dormant)`
|
|
1535
3439
|
);
|
|
1536
3440
|
return;
|
|
1537
3441
|
}
|
|
@@ -1547,6 +3451,11 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1547
3451
|
slot.lastUsed = Date.now();
|
|
1548
3452
|
if (slot.client && slot.state === "connected") return slot.client;
|
|
1549
3453
|
if (slot.connecting) return slot.connecting;
|
|
3454
|
+
const waking = slot.state === "dormant";
|
|
3455
|
+
if (waking) {
|
|
3456
|
+
slot.operations.wakeCount++;
|
|
3457
|
+
this.recordOperation(slot, "wake", "lazy-demand");
|
|
3458
|
+
}
|
|
1550
3459
|
slot.connecting = (async () => {
|
|
1551
3460
|
try {
|
|
1552
3461
|
slot.attempts = 0;
|
|
@@ -1627,6 +3536,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1627
3536
|
slot.client.removeExitListener(this.onChildExit);
|
|
1628
3537
|
if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
|
|
1629
3538
|
slot.client.removeToolsChangedListener(this.onToolsChanged);
|
|
3539
|
+
this.removeCatalogListeners(slot.client);
|
|
1630
3540
|
await slot.client.close();
|
|
1631
3541
|
slot.client = void 0;
|
|
1632
3542
|
}
|
|
@@ -1635,13 +3545,20 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1635
3545
|
for (const t of slot.toolNames) this.toolRegistry.unregister(t);
|
|
1636
3546
|
slot.toolNames = [];
|
|
1637
3547
|
slot.lazyTools = [];
|
|
3548
|
+
slot.serverMetadata = void 0;
|
|
3549
|
+
slot.resources = void 0;
|
|
3550
|
+
slot.resourceTemplates = void 0;
|
|
3551
|
+
slot.prompts = void 0;
|
|
1638
3552
|
slot.registeredLazy = false;
|
|
1639
3553
|
slot.state = "disconnected";
|
|
3554
|
+
this.recordOperation(slot, "stop", "manual");
|
|
1640
3555
|
this.events.emit("mcp.server.disconnected", { name, reason: "stop" });
|
|
1641
3556
|
}
|
|
1642
3557
|
async restart(name) {
|
|
1643
3558
|
const slot = this.servers.get(name);
|
|
1644
3559
|
if (!slot) throw new Error(`MCP server "${name}" not registered`);
|
|
3560
|
+
slot.operations.restartCount++;
|
|
3561
|
+
this.recordOperation(slot, "restart", "manual");
|
|
1645
3562
|
await this.stop(name);
|
|
1646
3563
|
slot.attempts = 0;
|
|
1647
3564
|
slot.reconnectCycles = 0;
|
|
@@ -1658,6 +3575,131 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1658
3575
|
};
|
|
1659
3576
|
});
|
|
1660
3577
|
}
|
|
3578
|
+
/**
|
|
3579
|
+
* Subscribe to payload-free operational signals. Callers must still avoid
|
|
3580
|
+
* using `serverName` as an unbounded metric label.
|
|
3581
|
+
*/
|
|
3582
|
+
onOperation(listener) {
|
|
3583
|
+
this.operationListeners.add(listener);
|
|
3584
|
+
return () => this.operationListeners.delete(listener);
|
|
3585
|
+
}
|
|
3586
|
+
/** Detailed, defensively-copied operational snapshots for CLI/WebUI/HQ. */
|
|
3587
|
+
operationalHealth() {
|
|
3588
|
+
const active = Array.from(this.servers.values()).map((slot) => {
|
|
3589
|
+
const op = slot.operations;
|
|
3590
|
+
const baseHealth = healthStateFor(slot.state, op, slot.cfg.enabled !== false);
|
|
3591
|
+
const checks = evaluateHealthThresholds(op, slot.cfg.health?.thresholds);
|
|
3592
|
+
return {
|
|
3593
|
+
name: slot.cfg.name,
|
|
3594
|
+
connectionState: slot.state,
|
|
3595
|
+
healthState: applyHealthThresholds(baseHealth, checks),
|
|
3596
|
+
lastSuccessAt: op.lastSuccessAt,
|
|
3597
|
+
lastFailureAt: op.lastFailureAt,
|
|
3598
|
+
lastFailureKind: op.lastFailureKind,
|
|
3599
|
+
lastReason: op.lastReason,
|
|
3600
|
+
consecutiveFailures: op.consecutiveFailures,
|
|
3601
|
+
failures: { ...op.failures },
|
|
3602
|
+
reconnectCount: op.reconnectCount,
|
|
3603
|
+
wakeCount: op.wakeCount,
|
|
3604
|
+
sleepCount: op.sleepCount,
|
|
3605
|
+
restartCount: op.restartCount,
|
|
3606
|
+
connectionLatency: summarizeLatency(op.connectionSamples),
|
|
3607
|
+
discoveryLatency: summarizeLatency(op.discoverySamples),
|
|
3608
|
+
callLatency: summarizeLatency(op.callSamples),
|
|
3609
|
+
inFlightCalls: op.inFlightCalls,
|
|
3610
|
+
peakInFlightCalls: op.peakInFlightCalls,
|
|
3611
|
+
recentEvents: op.recentEvents.map((event) => ({ ...event })),
|
|
3612
|
+
healthChecks: checks
|
|
3613
|
+
};
|
|
3614
|
+
});
|
|
3615
|
+
const disabled = Array.from(this.disabledServers.values()).map((cfg) => {
|
|
3616
|
+
const operations = createMCPServerOperationState();
|
|
3617
|
+
return {
|
|
3618
|
+
name: cfg.name,
|
|
3619
|
+
connectionState: "idle",
|
|
3620
|
+
healthState: "disabled",
|
|
3621
|
+
consecutiveFailures: 0,
|
|
3622
|
+
failures: { ...operations.failures },
|
|
3623
|
+
reconnectCount: 0,
|
|
3624
|
+
wakeCount: 0,
|
|
3625
|
+
sleepCount: 0,
|
|
3626
|
+
restartCount: 0,
|
|
3627
|
+
connectionLatency: summarizeLatency([]),
|
|
3628
|
+
discoveryLatency: summarizeLatency([]),
|
|
3629
|
+
callLatency: summarizeLatency([]),
|
|
3630
|
+
inFlightCalls: 0,
|
|
3631
|
+
peakInFlightCalls: 0,
|
|
3632
|
+
recentEvents: [],
|
|
3633
|
+
healthChecks: []
|
|
3634
|
+
};
|
|
3635
|
+
});
|
|
3636
|
+
return [...active, ...disabled];
|
|
3637
|
+
}
|
|
3638
|
+
getCatalog(name) {
|
|
3639
|
+
const slot = this.servers.get(name);
|
|
3640
|
+
if (!slot) return void 0;
|
|
3641
|
+
return catalogSnapshot(slot);
|
|
3642
|
+
}
|
|
3643
|
+
async listResources(name, opts = {}) {
|
|
3644
|
+
const slot = this.requireSlot(name);
|
|
3645
|
+
if (!opts.refresh && slot.resources) return cloneRecords(slot.resources);
|
|
3646
|
+
const client = await this.ensureConnected(name);
|
|
3647
|
+
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
3648
|
+
slot.resources = await collectPages(
|
|
3649
|
+
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
3650
|
+
(page) => page.resources
|
|
3651
|
+
);
|
|
3652
|
+
await this.persistCapabilityManifest(slot);
|
|
3653
|
+
return cloneRecords(slot.resources);
|
|
3654
|
+
}
|
|
3655
|
+
async listResourceTemplates(name, opts = {}) {
|
|
3656
|
+
const slot = this.requireSlot(name);
|
|
3657
|
+
if (!opts.refresh && slot.resourceTemplates) return cloneRecords(slot.resourceTemplates);
|
|
3658
|
+
const client = await this.ensureConnected(name);
|
|
3659
|
+
if (!client.getServerMetadata()?.capabilities.resources) return [];
|
|
3660
|
+
slot.resourceTemplates = await collectPages(
|
|
3661
|
+
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
3662
|
+
(page) => page.resourceTemplates
|
|
3663
|
+
);
|
|
3664
|
+
await this.persistCapabilityManifest(slot);
|
|
3665
|
+
return cloneRecords(slot.resourceTemplates);
|
|
3666
|
+
}
|
|
3667
|
+
async readResource(name, uri) {
|
|
3668
|
+
return (await this.ensureConnected(name)).readResource(uri);
|
|
3669
|
+
}
|
|
3670
|
+
async selectResourceForInsertion(name, uri, policy) {
|
|
3671
|
+
return prepareResourceInsertion(name, uri, await this.readResource(name, uri), policy);
|
|
3672
|
+
}
|
|
3673
|
+
async subscribeResource(name, uri) {
|
|
3674
|
+
await (await this.ensureConnected(name)).subscribeResource(uri);
|
|
3675
|
+
}
|
|
3676
|
+
async unsubscribeResource(name, uri) {
|
|
3677
|
+
await (await this.ensureConnected(name)).unsubscribeResource(uri);
|
|
3678
|
+
}
|
|
3679
|
+
async listPrompts(name, opts = {}) {
|
|
3680
|
+
const slot = this.requireSlot(name);
|
|
3681
|
+
if (!opts.refresh && slot.prompts) return cloneRecords(slot.prompts);
|
|
3682
|
+
const client = await this.ensureConnected(name);
|
|
3683
|
+
if (!client.getServerMetadata()?.capabilities.prompts) return [];
|
|
3684
|
+
slot.prompts = await collectPages(
|
|
3685
|
+
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
3686
|
+
(page) => page.prompts
|
|
3687
|
+
);
|
|
3688
|
+
await this.persistCapabilityManifest(slot);
|
|
3689
|
+
return cloneRecords(slot.prompts);
|
|
3690
|
+
}
|
|
3691
|
+
async getPrompt(serverName, promptName, args) {
|
|
3692
|
+
return (await this.ensureConnected(serverName)).getPrompt(promptName, args);
|
|
3693
|
+
}
|
|
3694
|
+
async selectPromptForInsertion(serverName, promptName, args, policy) {
|
|
3695
|
+
return preparePromptInsertion(
|
|
3696
|
+
serverName,
|
|
3697
|
+
promptName,
|
|
3698
|
+
args,
|
|
3699
|
+
await this.getPrompt(serverName, promptName, args),
|
|
3700
|
+
policy
|
|
3701
|
+
);
|
|
3702
|
+
}
|
|
1661
3703
|
/**
|
|
1662
3704
|
* Resolve the live tool names for a slot — the registered names in normal
|
|
1663
3705
|
* mode, or the cached lazy-tool names when running in lazy mode (where
|
|
@@ -1679,7 +3721,30 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1679
3721
|
const filtered = tools.filter((t) => !allowed || allowed.includes(t.name));
|
|
1680
3722
|
const clientArg = slot.lazy ? () => this.ensureConnected(slot.cfg.name) : expectDefined(client);
|
|
1681
3723
|
const wrapped = filtered.map(
|
|
1682
|
-
(t) => wrapMCPTool(slot.cfg.name, t, clientArg, slot.cfg.permission ?? "confirm"
|
|
3724
|
+
(t) => wrapMCPTool(slot.cfg.name, t, clientArg, slot.cfg.permission ?? "confirm", {
|
|
3725
|
+
onStart: () => {
|
|
3726
|
+
slot.operations.inFlightCalls++;
|
|
3727
|
+
slot.operations.peakInFlightCalls = Math.max(
|
|
3728
|
+
slot.operations.peakInFlightCalls,
|
|
3729
|
+
slot.operations.inFlightCalls
|
|
3730
|
+
);
|
|
3731
|
+
this.recordOperation(slot, "call", "started", void 0, void 0, false);
|
|
3732
|
+
},
|
|
3733
|
+
onFinish: ({ durationMs, ok }) => {
|
|
3734
|
+
slot.operations.inFlightCalls = Math.max(0, slot.operations.inFlightCalls - 1);
|
|
3735
|
+
pushBounded(
|
|
3736
|
+
slot.operations.callSamples,
|
|
3737
|
+
durationMs,
|
|
3738
|
+
MCP_OPERATION_LIMITS.LATENCY_SAMPLES
|
|
3739
|
+
);
|
|
3740
|
+
if (ok) {
|
|
3741
|
+
this.recordSuccess(slot);
|
|
3742
|
+
this.recordOperation(slot, "call", "ok", void 0, durationMs, false);
|
|
3743
|
+
} else {
|
|
3744
|
+
this.recordFailure(slot, "tool", "tool-call-failed", durationMs);
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
})
|
|
1683
3748
|
);
|
|
1684
3749
|
if (this.lazyMode) {
|
|
1685
3750
|
slot.lazyTools = wrapped;
|
|
@@ -1693,7 +3758,71 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1693
3758
|
this.log.warn(`MCP tool "${tool.name}" not registered`, err);
|
|
1694
3759
|
}
|
|
1695
3760
|
}
|
|
1696
|
-
if (slot.lazy) slot.registeredLazy = true;
|
|
3761
|
+
if (slot.lazy && wrapped.length > 0) slot.registeredLazy = true;
|
|
3762
|
+
}
|
|
3763
|
+
async discoverCapabilities(slot, client) {
|
|
3764
|
+
const startedAt = Date.now();
|
|
3765
|
+
slot.serverMetadata = client.getServerMetadata();
|
|
3766
|
+
const capabilities = slot.serverMetadata?.capabilities;
|
|
3767
|
+
if (capabilities?.resources) {
|
|
3768
|
+
try {
|
|
3769
|
+
slot.resources = await collectPages(
|
|
3770
|
+
(cursor) => client.listResources(cursor ? { cursor } : {}),
|
|
3771
|
+
(page) => page.resources
|
|
3772
|
+
);
|
|
3773
|
+
} catch (err) {
|
|
3774
|
+
slot.resources = void 0;
|
|
3775
|
+
this.recordFailure(slot, "protocol", "resource-discovery-failed");
|
|
3776
|
+
this.log.warn(`MCP server "${slot.cfg.name}" resource discovery failed`, err);
|
|
3777
|
+
}
|
|
3778
|
+
try {
|
|
3779
|
+
slot.resourceTemplates = await collectPages(
|
|
3780
|
+
(cursor) => client.listResourceTemplates(cursor ? { cursor } : {}),
|
|
3781
|
+
(page) => page.resourceTemplates
|
|
3782
|
+
);
|
|
3783
|
+
} catch (err) {
|
|
3784
|
+
slot.resourceTemplates = void 0;
|
|
3785
|
+
this.recordFailure(slot, "protocol", "resource-template-discovery-failed");
|
|
3786
|
+
this.log.warn(`MCP server "${slot.cfg.name}" resource template discovery failed`, err);
|
|
3787
|
+
}
|
|
3788
|
+
} else {
|
|
3789
|
+
slot.resources = void 0;
|
|
3790
|
+
slot.resourceTemplates = void 0;
|
|
3791
|
+
}
|
|
3792
|
+
if (capabilities?.prompts) {
|
|
3793
|
+
try {
|
|
3794
|
+
slot.prompts = await collectPages(
|
|
3795
|
+
(cursor) => client.listPrompts(cursor ? { cursor } : {}),
|
|
3796
|
+
(page) => page.prompts
|
|
3797
|
+
);
|
|
3798
|
+
} catch (err) {
|
|
3799
|
+
slot.prompts = void 0;
|
|
3800
|
+
this.recordFailure(slot, "protocol", "prompt-discovery-failed");
|
|
3801
|
+
this.log.warn(`MCP server "${slot.cfg.name}" prompt discovery failed`, err);
|
|
3802
|
+
}
|
|
3803
|
+
} else {
|
|
3804
|
+
slot.prompts = void 0;
|
|
3805
|
+
}
|
|
3806
|
+
const durationMs = Date.now() - startedAt;
|
|
3807
|
+
pushBounded(slot.operations.discoverySamples, durationMs, MCP_OPERATION_LIMITS.LATENCY_SAMPLES);
|
|
3808
|
+
this.recordOperation(slot, "discover", "complete", void 0, durationMs, false);
|
|
3809
|
+
}
|
|
3810
|
+
async persistCapabilityManifest(slot) {
|
|
3811
|
+
if (!slot.lazy || !this.cacheDir) return;
|
|
3812
|
+
const cacheDir = this.cacheDir;
|
|
3813
|
+
const previous = slot.manifestWrite ?? Promise.resolve();
|
|
3814
|
+
const pending = previous.then(
|
|
3815
|
+
() => writeCapabilityManifest(cacheDir, slot.cfg.name, manifestConfigHash(slot.cfg), {
|
|
3816
|
+
tools: slot.client?.listTools() ?? [],
|
|
3817
|
+
serverMetadata: slot.serverMetadata,
|
|
3818
|
+
resources: slot.resources,
|
|
3819
|
+
resourceTemplates: slot.resourceTemplates,
|
|
3820
|
+
prompts: slot.prompts
|
|
3821
|
+
})
|
|
3822
|
+
);
|
|
3823
|
+
slot.manifestWrite = pending;
|
|
3824
|
+
await pending;
|
|
3825
|
+
if (slot.manifestWrite === pending) slot.manifestWrite = void 0;
|
|
1697
3826
|
}
|
|
1698
3827
|
/** Start the shared idle sweep timer once (unref'd so it never holds the process). */
|
|
1699
3828
|
ensureIdleSweep() {
|
|
@@ -1728,11 +3857,14 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1728
3857
|
slot.client.removeExitListener(this.onChildExit);
|
|
1729
3858
|
if (slot.onDisconnect) slot.client.removeDisconnectListener(slot.onDisconnect);
|
|
1730
3859
|
slot.client.removeToolsChangedListener(this.onToolsChanged);
|
|
3860
|
+
this.removeCatalogListeners(slot.client);
|
|
1731
3861
|
await slot.client.close();
|
|
1732
3862
|
slot.client = void 0;
|
|
1733
3863
|
}
|
|
1734
3864
|
slot.onDisconnect = void 0;
|
|
1735
3865
|
slot.state = "dormant";
|
|
3866
|
+
slot.operations.sleepCount++;
|
|
3867
|
+
this.recordOperation(slot, "sleep", "idle-timeout");
|
|
1736
3868
|
this.log.info(`MCP server "${slot.cfg.name}" idle \u2014 sleeping (tools stay registered)`);
|
|
1737
3869
|
this.events.emit("mcp.server.disconnected", { name: slot.cfg.name, reason: "idle-sleep" });
|
|
1738
3870
|
}
|
|
@@ -1743,7 +3875,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1743
3875
|
* triggering connections.
|
|
1744
3876
|
*/
|
|
1745
3877
|
describe() {
|
|
1746
|
-
|
|
3878
|
+
const active = Array.from(this.servers.values()).map((s) => {
|
|
1747
3879
|
const tools = this.toolNamesForSlot(s);
|
|
1748
3880
|
return {
|
|
1749
3881
|
name: s.cfg.name,
|
|
@@ -1753,6 +3885,14 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1753
3885
|
tools
|
|
1754
3886
|
};
|
|
1755
3887
|
});
|
|
3888
|
+
const disabled = Array.from(this.disabledServers.values()).map((cfg) => ({
|
|
3889
|
+
name: cfg.name,
|
|
3890
|
+
state: "idle",
|
|
3891
|
+
toolCount: 0,
|
|
3892
|
+
enabled: false,
|
|
3893
|
+
tools: []
|
|
3894
|
+
}));
|
|
3895
|
+
return [...active, ...disabled];
|
|
1756
3896
|
}
|
|
1757
3897
|
async stopAll() {
|
|
1758
3898
|
if (this.idleTimer) {
|
|
@@ -1762,6 +3902,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1762
3902
|
for (const name of Array.from(this.servers.keys())) {
|
|
1763
3903
|
await this.stop(name);
|
|
1764
3904
|
}
|
|
3905
|
+
this.disabledServers.clear();
|
|
1765
3906
|
}
|
|
1766
3907
|
/**
|
|
1767
3908
|
* Health check — returns 'ok' for connected servers, the current state otherwise.
|
|
@@ -1792,10 +3933,8 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1792
3933
|
slot.toolNames = [];
|
|
1793
3934
|
slot.registeredLazy = false;
|
|
1794
3935
|
const discovered = slot.client.listTools();
|
|
1795
|
-
if (slot.lazy && this.cacheDir) {
|
|
1796
|
-
void writeManifest(this.cacheDir, slot.cfg.name, manifestConfigHash(slot.cfg), discovered);
|
|
1797
|
-
}
|
|
1798
3936
|
this.applyTools(slot, discovered, slot.client);
|
|
3937
|
+
void this.persistCapabilityManifest(slot);
|
|
1799
3938
|
this.events.emit("mcp.server.connected", {
|
|
1800
3939
|
name: slot.cfg.name,
|
|
1801
3940
|
toolCount: slot.toolNames.length
|
|
@@ -1804,12 +3943,36 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1804
3943
|
`MCP server "${slot.cfg.name}" tools refreshed (${this.toolNamesForSlot(slot).length} active)`
|
|
1805
3944
|
);
|
|
1806
3945
|
};
|
|
3946
|
+
onResourcesChanged = (name) => {
|
|
3947
|
+
const slot = this.servers.get(name);
|
|
3948
|
+
if (!slot) return;
|
|
3949
|
+
slot.resources = void 0;
|
|
3950
|
+
slot.resourceTemplates = void 0;
|
|
3951
|
+
void this.persistCapabilityManifest(slot);
|
|
3952
|
+
this.log.info(`MCP server "${name}" resource catalog invalidated`);
|
|
3953
|
+
};
|
|
3954
|
+
onPromptsChanged = (name) => {
|
|
3955
|
+
const slot = this.servers.get(name);
|
|
3956
|
+
if (!slot) return;
|
|
3957
|
+
slot.prompts = void 0;
|
|
3958
|
+
void this.persistCapabilityManifest(slot);
|
|
3959
|
+
this.log.info(`MCP server "${name}" prompt catalog invalidated`);
|
|
3960
|
+
};
|
|
3961
|
+
addCatalogListeners(client) {
|
|
3962
|
+
client.addResourcesChangedListener(this.onResourcesChanged);
|
|
3963
|
+
client.addPromptsChangedListener(this.onPromptsChanged);
|
|
3964
|
+
}
|
|
3965
|
+
removeCatalogListeners(client) {
|
|
3966
|
+
client.removeResourcesChangedListener(this.onResourcesChanged);
|
|
3967
|
+
client.removePromptsChangedListener(this.onPromptsChanged);
|
|
3968
|
+
}
|
|
1807
3969
|
onChildExit = (name, code, _signal) => {
|
|
1808
3970
|
const slot = this.servers.get(name);
|
|
1809
3971
|
if (!slot) return;
|
|
1810
3972
|
if (slot.lazy) {
|
|
1811
3973
|
slot.client = void 0;
|
|
1812
3974
|
slot.state = "dormant";
|
|
3975
|
+
this.recordFailure(slot, "transport", "process-exit-lazy");
|
|
1813
3976
|
this.events.emit("mcp.server.disconnected", {
|
|
1814
3977
|
name,
|
|
1815
3978
|
reason: `exit:${code ?? "unknown"} (dormant)`
|
|
@@ -1824,7 +3987,12 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1824
3987
|
}
|
|
1825
3988
|
slot.toolNames = [];
|
|
1826
3989
|
slot.lazyTools = [];
|
|
3990
|
+
slot.serverMetadata = void 0;
|
|
3991
|
+
slot.resources = void 0;
|
|
3992
|
+
slot.resourceTemplates = void 0;
|
|
3993
|
+
slot.prompts = void 0;
|
|
1827
3994
|
slot.state = "disconnected";
|
|
3995
|
+
this.recordFailure(slot, "transport", "process-exit");
|
|
1828
3996
|
this.events.emit("mcp.server.disconnected", { name, reason: `exit:${code ?? "unknown"}` });
|
|
1829
3997
|
this.scheduleReconnect(slot);
|
|
1830
3998
|
};
|
|
@@ -1835,6 +4003,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1835
4003
|
if (slot.lazy) {
|
|
1836
4004
|
slot.client = void 0;
|
|
1837
4005
|
slot.state = "dormant";
|
|
4006
|
+
this.recordFailure(slot, "transport", "http-disconnect-lazy");
|
|
1838
4007
|
this.events.emit("mcp.server.disconnected", { name, reason: "http-disconnect (dormant)" });
|
|
1839
4008
|
return;
|
|
1840
4009
|
}
|
|
@@ -1846,7 +4015,12 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1846
4015
|
}
|
|
1847
4016
|
slot.toolNames = [];
|
|
1848
4017
|
slot.lazyTools = [];
|
|
4018
|
+
slot.serverMetadata = void 0;
|
|
4019
|
+
slot.resources = void 0;
|
|
4020
|
+
slot.resourceTemplates = void 0;
|
|
4021
|
+
slot.prompts = void 0;
|
|
1849
4022
|
slot.state = "disconnected";
|
|
4023
|
+
this.recordFailure(slot, "transport", "http-disconnect");
|
|
1850
4024
|
this.events.emit("mcp.server.disconnected", { name, reason: "http-disconnect" });
|
|
1851
4025
|
this.scheduleReconnect(slot);
|
|
1852
4026
|
};
|
|
@@ -1865,6 +4039,7 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1865
4039
|
if (slot.reconnectPending) return;
|
|
1866
4040
|
if (slot.reconnectCycles >= _MCPRegistry.MAX_RECONNECT_CYCLES) {
|
|
1867
4041
|
slot.state = "failed";
|
|
4042
|
+
this.recordFailure(slot, "transport", "reconnect-exhausted");
|
|
1868
4043
|
this.log.error(
|
|
1869
4044
|
`MCP server "${slot.cfg.name}" giving up after ${slot.reconnectCycles} reconnect cycles. Use \`/mcp restart ${slot.cfg.name}\` to retry.`
|
|
1870
4045
|
);
|
|
@@ -1893,345 +4068,196 @@ var MCPRegistry = class _MCPRegistry {
|
|
|
1893
4068
|
async attemptReconnect(slot) {
|
|
1894
4069
|
slot.reconnectPending = false;
|
|
1895
4070
|
slot.reconnectCycles++;
|
|
4071
|
+
slot.operations.reconnectCount++;
|
|
4072
|
+
this.recordOperation(slot, "reconnect", "automatic");
|
|
1896
4073
|
await this.attemptConnect(slot);
|
|
1897
4074
|
}
|
|
1898
|
-
|
|
1899
|
-
const
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
attempt++;
|
|
1903
|
-
slot.state = attempt === 1 ? "connecting" : "reconnecting";
|
|
1904
|
-
slot.attempts = attempt;
|
|
1905
|
-
let client;
|
|
1906
|
-
let boundDisconnect;
|
|
1907
|
-
try {
|
|
1908
|
-
client = new MCPClient({
|
|
1909
|
-
name: slot.cfg.name,
|
|
1910
|
-
transport: slot.cfg.transport,
|
|
1911
|
-
command: slot.cfg.command,
|
|
1912
|
-
args: slot.cfg.args,
|
|
1913
|
-
env: slot.cfg.env,
|
|
1914
|
-
url: slot.cfg.url,
|
|
1915
|
-
headers: slot.cfg.headers,
|
|
1916
|
-
startupTimeoutMs: slot.cfg.startupTimeoutMs,
|
|
1917
|
-
requestTimeoutMs: slot.cfg.requestTimeoutMs,
|
|
1918
|
-
passthroughEnv: slot.cfg.passthroughEnv
|
|
1919
|
-
});
|
|
1920
|
-
if (slot.cfg.transport === "stdio") {
|
|
1921
|
-
client.addExitListener(this.onChildExit);
|
|
1922
|
-
} else {
|
|
1923
|
-
boundDisconnect = () => this.onTransportDisconnect(slot.cfg.name);
|
|
1924
|
-
client.addDisconnectListener(boundDisconnect);
|
|
1925
|
-
}
|
|
1926
|
-
client.addToolsChangedListener(this.onToolsChanged);
|
|
1927
|
-
await client.connect();
|
|
1928
|
-
if (slot.client && slot.client !== client) {
|
|
1929
|
-
const prior = slot.client;
|
|
1930
|
-
const priorDisconnect = slot.onDisconnect;
|
|
1931
|
-
slot.client.removeExitListener(this.onChildExit);
|
|
1932
|
-
if (priorDisconnect) prior.removeDisconnectListener(priorDisconnect);
|
|
1933
|
-
prior.removeToolsChangedListener(this.onToolsChanged);
|
|
1934
|
-
prior.close().catch(() => {
|
|
1935
|
-
});
|
|
1936
|
-
}
|
|
1937
|
-
slot.client = client;
|
|
1938
|
-
slot.onDisconnect = boundDisconnect;
|
|
1939
|
-
const isReconnect = attempt > 1;
|
|
1940
|
-
slot.state = "connected";
|
|
1941
|
-
slot.reconnectCycles = 0;
|
|
1942
|
-
const mc = client;
|
|
1943
|
-
const discovered = mc.listTools();
|
|
1944
|
-
if (slot.lazy && this.cacheDir) {
|
|
1945
|
-
await writeManifest(
|
|
1946
|
-
this.cacheDir,
|
|
1947
|
-
slot.cfg.name,
|
|
1948
|
-
manifestConfigHash(slot.cfg),
|
|
1949
|
-
discovered
|
|
1950
|
-
);
|
|
1951
|
-
}
|
|
1952
|
-
this.applyTools(slot, discovered, mc);
|
|
1953
|
-
slot.lastUsed = Date.now();
|
|
1954
|
-
if (slot.lazy) this.ensureIdleSweep();
|
|
1955
|
-
this.events.emit(isReconnect ? "mcp.server.reconnected" : "mcp.server.connected", {
|
|
1956
|
-
name: slot.cfg.name,
|
|
1957
|
-
toolCount: slot.toolNames.length
|
|
1958
|
-
});
|
|
1959
|
-
return;
|
|
1960
|
-
} catch (err) {
|
|
1961
|
-
this.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
|
|
1962
|
-
if (client) {
|
|
1963
|
-
client.removeExitListener(this.onChildExit);
|
|
1964
|
-
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
1965
|
-
client.removeToolsChangedListener(this.onToolsChanged);
|
|
1966
|
-
await client.close().catch(() => {
|
|
1967
|
-
});
|
|
1968
|
-
}
|
|
1969
|
-
if (attempt >= MAX_ATTEMPTS) {
|
|
1970
|
-
this.log.error(
|
|
1971
|
-
`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS} attempts`,
|
|
1972
|
-
err
|
|
1973
|
-
);
|
|
1974
|
-
slot.state = "failed";
|
|
1975
|
-
slot.client = void 0;
|
|
1976
|
-
if (slot.reconnectTimer) {
|
|
1977
|
-
clearTimeout(slot.reconnectTimer);
|
|
1978
|
-
slot.reconnectTimer = void 0;
|
|
1979
|
-
}
|
|
1980
|
-
slot.reconnectPending = false;
|
|
1981
|
-
this.events.emit("mcp.server.disconnected", {
|
|
1982
|
-
name: slot.cfg.name,
|
|
1983
|
-
reason: err instanceof Error ? err.message : "unknown"
|
|
1984
|
-
});
|
|
1985
|
-
return;
|
|
1986
|
-
}
|
|
1987
|
-
const delay = 500 * 2 ** attempt;
|
|
1988
|
-
await new Promise((r) => setTimeout(r, delay));
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
};
|
|
1993
|
-
|
|
1994
|
-
// src/manage.ts
|
|
1995
|
-
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1996
|
-
import * as fs2 from "node:fs/promises";
|
|
1997
|
-
async function readConfig(path2) {
|
|
1998
|
-
try {
|
|
1999
|
-
return JSON.parse(await fs2.readFile(path2, "utf8"));
|
|
2000
|
-
} catch {
|
|
2001
|
-
return {};
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
async function writeConfig(path2, cfg) {
|
|
2005
|
-
const raw = JSON.stringify(cfg, null, 2);
|
|
2006
|
-
const tmp = `${path2}.${process.pid}.${randomBytes2(6).toString("hex")}.tmp`;
|
|
2007
|
-
await fs2.writeFile(tmp, raw, "utf8");
|
|
2008
|
-
try {
|
|
2009
|
-
await fs2.rename(tmp, path2);
|
|
2010
|
-
} catch (err) {
|
|
2011
|
-
await fs2.rm(tmp, { force: true }).catch(() => void 0);
|
|
2012
|
-
throw err;
|
|
4075
|
+
recordSuccess(slot, resetFailures = true) {
|
|
4076
|
+
const operations = this.operationsFor(slot);
|
|
4077
|
+
operations.lastSuccessAt = Date.now();
|
|
4078
|
+
if (resetFailures) operations.consecutiveFailures = 0;
|
|
2013
4079
|
}
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
async function persist(configPath, full, servers) {
|
|
2024
|
-
full.mcpServers = servers;
|
|
2025
|
-
await writeConfig(configPath, full);
|
|
2026
|
-
}
|
|
2027
|
-
function normalizeTransport(t) {
|
|
2028
|
-
if (t === "sse") return "sse";
|
|
2029
|
-
if (t === "http" || t === "streamable-http") return "streamable-http";
|
|
2030
|
-
return "stdio";
|
|
2031
|
-
}
|
|
2032
|
-
function buildConfig(input, base) {
|
|
2033
|
-
const cfg = {
|
|
2034
|
-
name: input.name,
|
|
2035
|
-
transport: input.transport ? normalizeTransport(String(input.transport)) : base?.transport ?? "stdio"
|
|
2036
|
-
};
|
|
2037
|
-
const description = input.description ?? base?.description;
|
|
2038
|
-
if (description !== void 0) cfg.description = description;
|
|
2039
|
-
const command = input.command ?? base?.command;
|
|
2040
|
-
if (command !== void 0) cfg.command = command;
|
|
2041
|
-
const args = input.args ?? base?.args;
|
|
2042
|
-
if (args !== void 0) cfg.args = args;
|
|
2043
|
-
const env = input.env ?? base?.env;
|
|
2044
|
-
if (env !== void 0) cfg.env = env;
|
|
2045
|
-
const url = input.url ?? base?.url;
|
|
2046
|
-
if (url !== void 0) cfg.url = url;
|
|
2047
|
-
const headers = input.headers ?? base?.headers;
|
|
2048
|
-
if (headers !== void 0) cfg.headers = headers;
|
|
2049
|
-
const allowedTools = input.allowedTools ?? base?.allowedTools;
|
|
2050
|
-
if (allowedTools !== void 0) cfg.allowedTools = allowedTools;
|
|
2051
|
-
const permission = input.permission ?? base?.permission;
|
|
2052
|
-
if (permission !== void 0) cfg.permission = permission;
|
|
2053
|
-
const enabled = input.enabled ?? base?.enabled;
|
|
2054
|
-
if (enabled !== void 0) cfg.enabled = enabled;
|
|
2055
|
-
const lazy = input.lazy ?? base?.lazy;
|
|
2056
|
-
if (lazy !== void 0) cfg.lazy = lazy;
|
|
2057
|
-
const passthroughEnv = input.passthroughEnv ?? base?.passthroughEnv;
|
|
2058
|
-
if (passthroughEnv !== void 0) cfg.passthroughEnv = passthroughEnv;
|
|
2059
|
-
return cfg;
|
|
2060
|
-
}
|
|
2061
|
-
function projectServer(name, cfg, registry) {
|
|
2062
|
-
const live = registry.list().find((s) => s.name === name);
|
|
2063
|
-
const info = {
|
|
2064
|
-
name,
|
|
2065
|
-
transport: cfg.transport,
|
|
2066
|
-
enabled: cfg.enabled !== false,
|
|
2067
|
-
status: live ? live.state : "stopped",
|
|
2068
|
-
tools: live?.tools ?? []
|
|
2069
|
-
};
|
|
2070
|
-
if (cfg.description !== void 0) info.description = cfg.description;
|
|
2071
|
-
if (cfg.url !== void 0) info.url = cfg.url;
|
|
2072
|
-
if (cfg.command !== void 0) info.command = cfg.command;
|
|
2073
|
-
if (cfg.lazy !== void 0) info.lazy = cfg.lazy;
|
|
2074
|
-
return info;
|
|
2075
|
-
}
|
|
2076
|
-
function liveState(name, registry) {
|
|
2077
|
-
const live = registry.list().find((s) => s.name === name);
|
|
2078
|
-
return { state: live?.state ?? "stopped", tools: live?.tools ?? [] };
|
|
2079
|
-
}
|
|
2080
|
-
function errMessage(err) {
|
|
2081
|
-
return err instanceof Error ? err.message : String(err);
|
|
2082
|
-
}
|
|
2083
|
-
async function listMcp(deps) {
|
|
2084
|
-
const { servers } = await readServers(deps.configPath);
|
|
2085
|
-
return Object.entries(servers).map(
|
|
2086
|
-
([name, cfg]) => projectServer(name, { ...cfg, name }, deps.registry)
|
|
2087
|
-
);
|
|
2088
|
-
}
|
|
2089
|
-
async function addMcp(input, deps) {
|
|
2090
|
-
if (!input.name) return { ok: false, message: "Server name is required" };
|
|
2091
|
-
const { full, servers } = await readServers(deps.configPath);
|
|
2092
|
-
if (servers[input.name]) {
|
|
2093
|
-
return { ok: false, message: `Server "${input.name}" already exists` };
|
|
4080
|
+
recordFailure(slot, failureKind, reason, durationMs) {
|
|
4081
|
+
const operations = this.operationsFor(slot);
|
|
4082
|
+
const safeReason = safeOperationReason(reason);
|
|
4083
|
+
operations.lastFailureAt = Date.now();
|
|
4084
|
+
operations.lastFailureKind = failureKind;
|
|
4085
|
+
operations.lastReason = safeReason;
|
|
4086
|
+
operations.consecutiveFailures++;
|
|
4087
|
+
operations.failures[failureKind]++;
|
|
4088
|
+
this.recordOperation(slot, "failure", safeReason, failureKind, durationMs);
|
|
2094
4089
|
}
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
const
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
4090
|
+
recordOperation(slot, kind, reason, failureKind, durationMs, retain = true) {
|
|
4091
|
+
const operations = this.operationsFor(slot);
|
|
4092
|
+
const baseHealth = healthStateFor(slot.state, operations, slot.cfg.enabled !== false);
|
|
4093
|
+
const checks = evaluateHealthThresholds(operations, slot.cfg.health?.thresholds);
|
|
4094
|
+
const event = {
|
|
4095
|
+
serverName: slot.cfg.name,
|
|
4096
|
+
kind,
|
|
4097
|
+
at: Date.now(),
|
|
4098
|
+
connectionState: slot.state,
|
|
4099
|
+
healthState: applyHealthThresholds(baseHealth, checks)
|
|
2103
4100
|
};
|
|
4101
|
+
if (reason !== void 0) event.reason = safeOperationReason(reason);
|
|
4102
|
+
if (failureKind !== void 0) event.failureKind = failureKind;
|
|
4103
|
+
if (durationMs !== void 0) event.durationMs = Math.max(0, Math.round(durationMs));
|
|
4104
|
+
if (retain) {
|
|
4105
|
+
pushBounded(operations.recentEvents, event, MCP_OPERATION_LIMITS.RECENT_EVENTS);
|
|
4106
|
+
}
|
|
4107
|
+
for (const listener of this.operationListeners) {
|
|
4108
|
+
try {
|
|
4109
|
+
listener({ ...event });
|
|
4110
|
+
} catch {
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
2104
4113
|
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
return startServer(input.name, cfg, deps, `Server "${input.name}" added`);
|
|
2110
|
-
}
|
|
2111
|
-
return {
|
|
2112
|
-
ok: true,
|
|
2113
|
-
message: `Server "${input.name}" added (disabled)`,
|
|
2114
|
-
server: projectServer(input.name, cfg, deps.registry)
|
|
2115
|
-
};
|
|
2116
|
-
}
|
|
2117
|
-
async function updateMcp(input, deps) {
|
|
2118
|
-
if (!input.name) return { ok: false, message: "Server name is required" };
|
|
2119
|
-
const { full, servers } = await readServers(deps.configPath);
|
|
2120
|
-
const existing = servers[input.name];
|
|
2121
|
-
if (!existing) return { ok: false, message: `Server "${input.name}" not found` };
|
|
2122
|
-
const cfg = buildConfig(input, { ...existing, name: input.name });
|
|
2123
|
-
servers[input.name] = cfg;
|
|
2124
|
-
await persist(deps.configPath, full, servers);
|
|
2125
|
-
if (cfg.enabled !== false) {
|
|
2126
|
-
return startServer(input.name, cfg, deps, `Server "${input.name}" updated`, { restart: true });
|
|
4114
|
+
/** Keeps private-method unit fixtures from needing to duplicate every slot field. */
|
|
4115
|
+
operationsFor(slot) {
|
|
4116
|
+
if (!slot.operations) slot.operations = createMCPServerOperationState();
|
|
4117
|
+
return slot.operations;
|
|
2127
4118
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
4119
|
+
async attemptConnect(slot) {
|
|
4120
|
+
const MAX_ATTEMPTS = MCP_CONSTANTS.RECONNECT.MAX_ATTEMPTS;
|
|
4121
|
+
let attempt = 0;
|
|
4122
|
+
while (attempt < MAX_ATTEMPTS) {
|
|
4123
|
+
attempt++;
|
|
4124
|
+
const startedAt = Date.now();
|
|
4125
|
+
slot.state = attempt === 1 ? "connecting" : "reconnecting";
|
|
4126
|
+
slot.attempts = attempt;
|
|
4127
|
+
let client;
|
|
4128
|
+
let boundDisconnect;
|
|
4129
|
+
try {
|
|
4130
|
+
client = new MCPClient({
|
|
4131
|
+
name: slot.cfg.name,
|
|
4132
|
+
transport: slot.cfg.transport,
|
|
4133
|
+
command: slot.cfg.command,
|
|
4134
|
+
args: slot.cfg.args,
|
|
4135
|
+
env: slot.cfg.env,
|
|
4136
|
+
url: slot.cfg.url,
|
|
4137
|
+
headers: slot.cfg.headers,
|
|
4138
|
+
startupTimeoutMs: slot.cfg.startupTimeoutMs,
|
|
4139
|
+
requestTimeoutMs: slot.cfg.requestTimeoutMs,
|
|
4140
|
+
passthroughEnv: slot.cfg.passthroughEnv,
|
|
4141
|
+
authorizationProvider: this.authorizationProviderFactory?.(slot.cfg)
|
|
4142
|
+
});
|
|
4143
|
+
if (slot.cfg.transport === "stdio") {
|
|
4144
|
+
client.addExitListener(this.onChildExit);
|
|
4145
|
+
} else {
|
|
4146
|
+
boundDisconnect = () => this.onTransportDisconnect(slot.cfg.name);
|
|
4147
|
+
client.addDisconnectListener(boundDisconnect);
|
|
4148
|
+
}
|
|
4149
|
+
client.addToolsChangedListener(this.onToolsChanged);
|
|
4150
|
+
this.addCatalogListeners(client);
|
|
4151
|
+
await client.connect();
|
|
4152
|
+
if (slot.client && slot.client !== client) {
|
|
4153
|
+
const prior = slot.client;
|
|
4154
|
+
const priorDisconnect = slot.onDisconnect;
|
|
4155
|
+
slot.client.removeExitListener(this.onChildExit);
|
|
4156
|
+
if (priorDisconnect) prior.removeDisconnectListener(priorDisconnect);
|
|
4157
|
+
prior.removeToolsChangedListener(this.onToolsChanged);
|
|
4158
|
+
this.removeCatalogListeners(prior);
|
|
4159
|
+
prior.close().catch(() => {
|
|
4160
|
+
});
|
|
4161
|
+
}
|
|
4162
|
+
slot.client = client;
|
|
4163
|
+
slot.onDisconnect = boundDisconnect;
|
|
4164
|
+
const isReconnect = slot.reconnectCycles > 0 || attempt > 1;
|
|
4165
|
+
slot.state = "connected";
|
|
4166
|
+
slot.reconnectCycles = 0;
|
|
4167
|
+
const mc = client;
|
|
4168
|
+
const discovered = mc.listTools();
|
|
4169
|
+
await this.discoverCapabilities(slot, mc);
|
|
4170
|
+
await this.persistCapabilityManifest(slot);
|
|
4171
|
+
this.applyTools(slot, discovered, mc);
|
|
4172
|
+
const durationMs = Date.now() - startedAt;
|
|
4173
|
+
pushBounded(
|
|
4174
|
+
slot.operations.connectionSamples,
|
|
4175
|
+
durationMs,
|
|
4176
|
+
MCP_OPERATION_LIMITS.LATENCY_SAMPLES
|
|
4177
|
+
);
|
|
4178
|
+
this.recordSuccess(slot, (slot.operations.lastFailureAt ?? 0) < startedAt);
|
|
4179
|
+
this.recordOperation(
|
|
4180
|
+
slot,
|
|
4181
|
+
isReconnect ? "reconnect" : "connect",
|
|
4182
|
+
"connected",
|
|
4183
|
+
void 0,
|
|
4184
|
+
durationMs
|
|
4185
|
+
);
|
|
4186
|
+
slot.lastUsed = Date.now();
|
|
4187
|
+
if (slot.lazy) this.ensureIdleSweep();
|
|
4188
|
+
this.events.emit(isReconnect ? "mcp.server.reconnected" : "mcp.server.connected", {
|
|
4189
|
+
name: slot.cfg.name,
|
|
4190
|
+
toolCount: slot.toolNames.length
|
|
4191
|
+
});
|
|
4192
|
+
return;
|
|
4193
|
+
} catch (err) {
|
|
4194
|
+
this.recordFailure(slot, "transport", "connect-attempt-failed", Date.now() - startedAt);
|
|
4195
|
+
this.log.warn(`MCP server "${slot.cfg.name}" connect attempt ${attempt} failed`, err);
|
|
4196
|
+
if (client) {
|
|
4197
|
+
client.removeExitListener(this.onChildExit);
|
|
4198
|
+
if (boundDisconnect) client.removeDisconnectListener(boundDisconnect);
|
|
4199
|
+
client.removeToolsChangedListener(this.onToolsChanged);
|
|
4200
|
+
this.removeCatalogListeners(client);
|
|
4201
|
+
await client.close().catch(() => {
|
|
4202
|
+
});
|
|
4203
|
+
}
|
|
4204
|
+
if (attempt >= MAX_ATTEMPTS) {
|
|
4205
|
+
this.log.error(
|
|
4206
|
+
`MCP server "${slot.cfg.name}" connect exhausted after ${MAX_ATTEMPTS} attempts`,
|
|
4207
|
+
err
|
|
4208
|
+
);
|
|
4209
|
+
slot.state = "failed";
|
|
4210
|
+
slot.client = void 0;
|
|
4211
|
+
if (slot.reconnectTimer) {
|
|
4212
|
+
clearTimeout(slot.reconnectTimer);
|
|
4213
|
+
slot.reconnectTimer = void 0;
|
|
4214
|
+
}
|
|
4215
|
+
slot.reconnectPending = false;
|
|
4216
|
+
this.events.emit("mcp.server.disconnected", {
|
|
4217
|
+
name: slot.cfg.name,
|
|
4218
|
+
reason: err instanceof Error ? err.message : "unknown"
|
|
4219
|
+
});
|
|
4220
|
+
return;
|
|
4221
|
+
}
|
|
4222
|
+
const delay = 500 * 2 ** attempt;
|
|
4223
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
2150
4226
|
}
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
servers[name] = cfg;
|
|
2164
|
-
await persist(deps.configPath, full, servers);
|
|
2165
|
-
return {
|
|
2166
|
-
ok: true,
|
|
2167
|
-
message: `Server "${name}" disabled`,
|
|
2168
|
-
server: projectServer(name, cfg, deps.registry)
|
|
2169
|
-
};
|
|
2170
|
-
}
|
|
2171
|
-
async function restartMcp(name, deps) {
|
|
2172
|
-
if (!name) return { ok: false, message: "Server name is required" };
|
|
2173
|
-
const registered = deps.registry.list().some((s) => s.name === name);
|
|
2174
|
-
if (registered) {
|
|
2175
|
-
try {
|
|
2176
|
-
await deps.registry.restart(name);
|
|
2177
|
-
const { state, tools } = liveState(name, deps.registry);
|
|
2178
|
-
return { ok: true, message: `Server "${name}" restarted`, state, tools };
|
|
2179
|
-
} catch (err) {
|
|
2180
|
-
return { ok: false, message: `Failed to restart "${name}": ${errMessage(err)}` };
|
|
4227
|
+
};
|
|
4228
|
+
var MAX_CATALOG_PAGES = 100;
|
|
4229
|
+
var MAX_CATALOG_ITEMS = 1e4;
|
|
4230
|
+
async function collectPages(load, select) {
|
|
4231
|
+
const items = [];
|
|
4232
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
4233
|
+
let cursor;
|
|
4234
|
+
for (let pageNumber = 0; pageNumber < MAX_CATALOG_PAGES; pageNumber++) {
|
|
4235
|
+
const page = await load(cursor);
|
|
4236
|
+
items.push(...select(page));
|
|
4237
|
+
if (items.length > MAX_CATALOG_ITEMS) {
|
|
4238
|
+
throw new Error(`MCP catalog exceeds ${MAX_CATALOG_ITEMS} items`);
|
|
2181
4239
|
}
|
|
4240
|
+
const next = page.nextCursor;
|
|
4241
|
+
if (!next) return items;
|
|
4242
|
+
if (seenCursors.has(next)) throw new Error(`MCP catalog repeated cursor "${next}"`);
|
|
4243
|
+
seenCursors.add(next);
|
|
4244
|
+
cursor = next;
|
|
2182
4245
|
}
|
|
2183
|
-
|
|
2184
|
-
const cfg = servers[name];
|
|
2185
|
-
if (!cfg) return { ok: false, message: `Server "${name}" is not in config.` };
|
|
2186
|
-
return startServer(name, { ...cfg, name }, deps, `Server "${name}" started`, { restart: true });
|
|
4246
|
+
throw new Error(`MCP catalog exceeds ${MAX_CATALOG_PAGES} pages`);
|
|
2187
4247
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
const { state, tools } = liveState(name, deps.registry);
|
|
4248
|
+
function cloneRecords(records) {
|
|
4249
|
+
return structuredClone(records);
|
|
4250
|
+
}
|
|
4251
|
+
function catalogSnapshot(slot) {
|
|
2193
4252
|
return {
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
4253
|
+
name: slot.cfg.name,
|
|
4254
|
+
state: slot.state,
|
|
4255
|
+
serverMetadata: slot.serverMetadata ? structuredClone(slot.serverMetadata) : void 0,
|
|
4256
|
+
resources: slot.resources ? cloneRecords(slot.resources) : void 0,
|
|
4257
|
+
resourceTemplates: slot.resourceTemplates ? cloneRecords(slot.resourceTemplates) : void 0,
|
|
4258
|
+
prompts: slot.prompts ? cloneRecords(slot.prompts) : void 0
|
|
2198
4259
|
};
|
|
2199
4260
|
}
|
|
2200
|
-
async function startServer(name, cfg, deps, okMessage, opts) {
|
|
2201
|
-
try {
|
|
2202
|
-
const alreadyRegistered = deps.registry.list().some((s) => s.name === name);
|
|
2203
|
-
if (alreadyRegistered && opts?.restart) {
|
|
2204
|
-
await deps.registry.restart(name);
|
|
2205
|
-
} else if (alreadyRegistered) {
|
|
2206
|
-
await deps.registry.restart(name);
|
|
2207
|
-
} else {
|
|
2208
|
-
await deps.registry.start({ ...cfg, enabled: true });
|
|
2209
|
-
}
|
|
2210
|
-
const { state, tools } = liveState(name, deps.registry);
|
|
2211
|
-
return {
|
|
2212
|
-
ok: true,
|
|
2213
|
-
message: okMessage,
|
|
2214
|
-
server: projectServer(name, cfg, deps.registry),
|
|
2215
|
-
state,
|
|
2216
|
-
tools
|
|
2217
|
-
};
|
|
2218
|
-
} catch (err) {
|
|
2219
|
-
const message = errMessage(err);
|
|
2220
|
-
return {
|
|
2221
|
-
ok: true,
|
|
2222
|
-
// config persisted — surface a soft warning, not a hard failure
|
|
2223
|
-
message: `${okMessage} in config, but failed to start: ${message}`,
|
|
2224
|
-
server: projectServer(name, cfg, deps.registry),
|
|
2225
|
-
registryError: message
|
|
2226
|
-
};
|
|
2227
|
-
}
|
|
2228
|
-
}
|
|
2229
|
-
async function safeStop(name, deps) {
|
|
2230
|
-
try {
|
|
2231
|
-
await deps.registry.stop(name);
|
|
2232
|
-
} catch {
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
4261
|
|
|
2236
4262
|
// src/server.ts
|
|
2237
4263
|
import { createServer } from "node:http";
|
|
@@ -2245,6 +4271,8 @@ var MCPServer = class {
|
|
|
2245
4271
|
host;
|
|
2246
4272
|
serverInfo;
|
|
2247
4273
|
logger;
|
|
4274
|
+
resources;
|
|
4275
|
+
prompts;
|
|
2248
4276
|
constructor(opts) {
|
|
2249
4277
|
this.host = opts.host;
|
|
2250
4278
|
this.serverInfo = opts.serverInfo ?? {
|
|
@@ -2252,6 +4280,8 @@ var MCPServer = class {
|
|
|
2252
4280
|
version: MCP_CONSTANTS.CLIENT_INFO.version
|
|
2253
4281
|
};
|
|
2254
4282
|
this.logger = opts.logger;
|
|
4283
|
+
this.resources = structuredClone(opts.resources ?? []);
|
|
4284
|
+
this.prompts = structuredClone(opts.prompts ?? []);
|
|
2255
4285
|
}
|
|
2256
4286
|
/**
|
|
2257
4287
|
* Handle one raw JSON-RPC line. Returns the response JSON string for
|
|
@@ -2296,7 +4326,11 @@ var MCPServer = class {
|
|
|
2296
4326
|
case "initialize":
|
|
2297
4327
|
return {
|
|
2298
4328
|
protocolVersion: MCP_CONSTANTS.PROTOCOL_VERSION,
|
|
2299
|
-
capabilities: {
|
|
4329
|
+
capabilities: {
|
|
4330
|
+
tools: { listChanged: false },
|
|
4331
|
+
...this.resources.length > 0 ? { resources: { subscribe: false, listChanged: false } } : {},
|
|
4332
|
+
...this.prompts.length > 0 ? { prompts: { listChanged: false } } : {}
|
|
4333
|
+
},
|
|
2300
4334
|
serverInfo: this.serverInfo
|
|
2301
4335
|
};
|
|
2302
4336
|
case "ping":
|
|
@@ -2314,6 +4348,54 @@ var MCPServer = class {
|
|
|
2314
4348
|
const res = await this.host.callTool(p.name, args);
|
|
2315
4349
|
return { content: toContentBlocks(res.content), isError: res.isError };
|
|
2316
4350
|
}
|
|
4351
|
+
case "resources/list": {
|
|
4352
|
+
if (this.resources.length === 0) return METHOD_NOT_FOUND_SENTINEL;
|
|
4353
|
+
const page = paginate(this.resources, params);
|
|
4354
|
+
return {
|
|
4355
|
+
resources: page.items.map(({ contents: _contents, ...resource }) => resource),
|
|
4356
|
+
...page.nextCursor ? { nextCursor: page.nextCursor } : {}
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
case "resources/templates/list":
|
|
4360
|
+
if (this.resources.length === 0) return METHOD_NOT_FOUND_SENTINEL;
|
|
4361
|
+
return { resourceTemplates: [] };
|
|
4362
|
+
case "resources/read": {
|
|
4363
|
+
if (this.resources.length === 0) return METHOD_NOT_FOUND_SENTINEL;
|
|
4364
|
+
const uri = requiredParamString(params, "uri", "resources/read");
|
|
4365
|
+
const resource = this.resources.find((candidate) => candidate.uri === uri);
|
|
4366
|
+
if (!resource) throw new Error(`Resource not found: ${uri}`);
|
|
4367
|
+
return { contents: structuredClone(resource.contents) };
|
|
4368
|
+
}
|
|
4369
|
+
case "prompts/list": {
|
|
4370
|
+
if (this.prompts.length === 0) return METHOD_NOT_FOUND_SENTINEL;
|
|
4371
|
+
const page = paginate(this.prompts, params);
|
|
4372
|
+
return {
|
|
4373
|
+
prompts: page.items.map(
|
|
4374
|
+
({ messages: _messages, template: _template, ...prompt }) => prompt
|
|
4375
|
+
),
|
|
4376
|
+
...page.nextCursor ? { nextCursor: page.nextCursor } : {}
|
|
4377
|
+
};
|
|
4378
|
+
}
|
|
4379
|
+
case "prompts/get": {
|
|
4380
|
+
if (this.prompts.length === 0) return METHOD_NOT_FOUND_SENTINEL;
|
|
4381
|
+
const name = requiredParamString(params, "name", "prompts/get");
|
|
4382
|
+
const prompt = this.prompts.find((candidate) => candidate.name === name);
|
|
4383
|
+
if (!prompt) throw new Error(`Prompt not found: ${name}`);
|
|
4384
|
+
const input = paramsRecord(params);
|
|
4385
|
+
const args = stringRecord(input["arguments"], "prompts/get arguments");
|
|
4386
|
+
for (const argument of prompt.arguments ?? []) {
|
|
4387
|
+
if (argument.required && args[argument.name] === void 0) {
|
|
4388
|
+
throw new Error(`Prompt "${name}" requires argument "${argument.name}"`);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
const messages = prompt.template ? [
|
|
4392
|
+
{
|
|
4393
|
+
role: "user",
|
|
4394
|
+
content: { type: "text", text: renderPromptTemplate(prompt.template, args) }
|
|
4395
|
+
}
|
|
4396
|
+
] : structuredClone(prompt.messages ?? []);
|
|
4397
|
+
return { description: prompt.description, messages };
|
|
4398
|
+
}
|
|
2317
4399
|
default:
|
|
2318
4400
|
return METHOD_NOT_FOUND_SENTINEL;
|
|
2319
4401
|
}
|
|
@@ -2322,6 +4404,53 @@ var MCPServer = class {
|
|
|
2322
4404
|
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
2323
4405
|
}
|
|
2324
4406
|
};
|
|
4407
|
+
var SERVER_PAGE_SIZE = 100;
|
|
4408
|
+
function paginate(items, params) {
|
|
4409
|
+
const cursor = paramsRecord(params)["cursor"];
|
|
4410
|
+
let offset = 0;
|
|
4411
|
+
if (cursor !== void 0) {
|
|
4412
|
+
if (typeof cursor !== "string" || !/^\d+$/.test(cursor)) {
|
|
4413
|
+
throw new Error("MCP pagination cursor must be a non-negative integer string");
|
|
4414
|
+
}
|
|
4415
|
+
offset = Number(cursor);
|
|
4416
|
+
if (!Number.isSafeInteger(offset)) throw new Error("MCP pagination cursor is too large");
|
|
4417
|
+
}
|
|
4418
|
+
const page = items.slice(offset, offset + SERVER_PAGE_SIZE);
|
|
4419
|
+
const next = offset + page.length;
|
|
4420
|
+
return {
|
|
4421
|
+
items: page,
|
|
4422
|
+
...next < items.length ? { nextCursor: String(next) } : {}
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
function paramsRecord(params) {
|
|
4426
|
+
return params && typeof params === "object" && !Array.isArray(params) ? params : {};
|
|
4427
|
+
}
|
|
4428
|
+
function requiredParamString(params, field, method) {
|
|
4429
|
+
const value = paramsRecord(params)[field];
|
|
4430
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
4431
|
+
throw new Error(`${method} requires a non-empty string "${field}"`);
|
|
4432
|
+
}
|
|
4433
|
+
return value;
|
|
4434
|
+
}
|
|
4435
|
+
function stringRecord(value, label) {
|
|
4436
|
+
if (value === void 0) return {};
|
|
4437
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4438
|
+
throw new Error(`${label} must be an object`);
|
|
4439
|
+
}
|
|
4440
|
+
const result = {};
|
|
4441
|
+
for (const [key, item] of Object.entries(value)) {
|
|
4442
|
+
if (typeof item !== "string") throw new Error(`${label}.${key} must be a string`);
|
|
4443
|
+
result[key] = item;
|
|
4444
|
+
}
|
|
4445
|
+
return result;
|
|
4446
|
+
}
|
|
4447
|
+
function renderPromptTemplate(template, args) {
|
|
4448
|
+
return template.replace(/\{\{([A-Za-z_][A-Za-z0-9_.-]*)\}\}/g, (_match, name) => {
|
|
4449
|
+
const value = args[name];
|
|
4450
|
+
if (value === void 0) throw new Error(`Missing prompt template argument "${name}"`);
|
|
4451
|
+
return value;
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
2325
4454
|
var METHOD_NOT_FOUND_SENTINEL = /* @__PURE__ */ Symbol("method-not-found");
|
|
2326
4455
|
function toContentBlocks(content) {
|
|
2327
4456
|
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
@@ -2512,28 +4641,349 @@ async function handleHttpRequest(server, req, res, token, log) {
|
|
|
2512
4641
|
});
|
|
2513
4642
|
});
|
|
2514
4643
|
}
|
|
4644
|
+
|
|
4645
|
+
// src/token-store.ts
|
|
4646
|
+
import * as fs3 from "node:fs/promises";
|
|
4647
|
+
import { atomicWrite, withFileLock } from "@wrongstack/core/utils";
|
|
4648
|
+
var TOKEN_STORE_VERSION = 1;
|
|
4649
|
+
var MAX_STORE_BYTES = 1024 * 1024;
|
|
4650
|
+
var MAX_ENTRIES = 256;
|
|
4651
|
+
var DEFAULT_REFRESH_SKEW_MS = 6e4;
|
|
4652
|
+
var MCPVaultTokenStore = class {
|
|
4653
|
+
constructor(filePath, vault) {
|
|
4654
|
+
this.filePath = filePath;
|
|
4655
|
+
this.vault = vault;
|
|
4656
|
+
}
|
|
4657
|
+
filePath;
|
|
4658
|
+
vault;
|
|
4659
|
+
async load(serverName, resource) {
|
|
4660
|
+
const canonicalResource = canonicalMcpResource(resource);
|
|
4661
|
+
return withFileLock(this.filePath, async () => {
|
|
4662
|
+
const file = await this.readFile();
|
|
4663
|
+
const entry = file.entries.find(
|
|
4664
|
+
(candidate) => candidate.serverName === serverName && candidate.resource === canonicalResource
|
|
4665
|
+
);
|
|
4666
|
+
return entry ? this.decryptEntry(entry) : void 0;
|
|
4667
|
+
});
|
|
4668
|
+
}
|
|
4669
|
+
async save(value) {
|
|
4670
|
+
const normalized = normalizeStoredAuthorization(value);
|
|
4671
|
+
await withFileLock(this.filePath, async () => {
|
|
4672
|
+
const file = await this.readFile();
|
|
4673
|
+
const next = file.entries.filter(
|
|
4674
|
+
(entry) => !(entry.serverName === normalized.serverName && entry.resource === normalized.resource)
|
|
4675
|
+
);
|
|
4676
|
+
next.push(this.encryptEntry(normalized));
|
|
4677
|
+
if (next.length > MAX_ENTRIES)
|
|
4678
|
+
throw new Error(`MCP token store exceeds ${MAX_ENTRIES} entries`);
|
|
4679
|
+
await this.writeFile(next);
|
|
4680
|
+
});
|
|
4681
|
+
}
|
|
4682
|
+
async remove(serverName, resource) {
|
|
4683
|
+
const canonicalResource = canonicalMcpResource(resource);
|
|
4684
|
+
return withFileLock(this.filePath, async () => {
|
|
4685
|
+
const file = await this.readFile();
|
|
4686
|
+
const next = file.entries.filter(
|
|
4687
|
+
(entry) => !(entry.serverName === serverName && entry.resource === canonicalResource)
|
|
4688
|
+
);
|
|
4689
|
+
if (next.length === file.entries.length) return false;
|
|
4690
|
+
await this.writeFile(next);
|
|
4691
|
+
return true;
|
|
4692
|
+
});
|
|
4693
|
+
}
|
|
4694
|
+
async readFile() {
|
|
4695
|
+
let raw;
|
|
4696
|
+
try {
|
|
4697
|
+
const stat2 = await fs3.stat(this.filePath);
|
|
4698
|
+
if (stat2.size > MAX_STORE_BYTES) throw new Error("MCP token store exceeds size limit");
|
|
4699
|
+
raw = await fs3.readFile(this.filePath, "utf8");
|
|
4700
|
+
} catch (error) {
|
|
4701
|
+
if (error.code === "ENOENT") return emptyFile();
|
|
4702
|
+
throw error;
|
|
4703
|
+
}
|
|
4704
|
+
let parsed;
|
|
4705
|
+
try {
|
|
4706
|
+
parsed = JSON.parse(raw);
|
|
4707
|
+
} catch {
|
|
4708
|
+
throw new Error("MCP token store is not valid JSON");
|
|
4709
|
+
}
|
|
4710
|
+
return validateStoreFile(parsed);
|
|
4711
|
+
}
|
|
4712
|
+
async writeFile(entries) {
|
|
4713
|
+
const file = {
|
|
4714
|
+
version: TOKEN_STORE_VERSION,
|
|
4715
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4716
|
+
entries
|
|
4717
|
+
};
|
|
4718
|
+
await atomicWrite(this.filePath, `${JSON.stringify(file, null, 2)}
|
|
4719
|
+
`, { mode: 384 });
|
|
4720
|
+
}
|
|
4721
|
+
encryptEntry(value) {
|
|
4722
|
+
const accessToken = this.vault.encrypt(value.tokenSet.accessToken);
|
|
4723
|
+
const refreshToken = value.tokenSet.refreshToken ? this.vault.encrypt(value.tokenSet.refreshToken) : void 0;
|
|
4724
|
+
if (!this.vault.isEncrypted(accessToken) || refreshToken && !this.vault.isEncrypted(refreshToken)) {
|
|
4725
|
+
throw new Error("MCP token store requires an encrypting SecretVault");
|
|
4726
|
+
}
|
|
4727
|
+
return {
|
|
4728
|
+
serverName: value.serverName,
|
|
4729
|
+
resource: value.resource,
|
|
4730
|
+
clientId: value.clientId,
|
|
4731
|
+
authorizationServer: value.authorizationServer,
|
|
4732
|
+
accessToken,
|
|
4733
|
+
refreshToken,
|
|
4734
|
+
tokenType: value.tokenSet.tokenType ?? "Bearer",
|
|
4735
|
+
expiresAt: value.tokenSet.expiresAt,
|
|
4736
|
+
scopes: [...value.tokenSet.scopes ?? []],
|
|
4737
|
+
updatedAt: value.updatedAt
|
|
4738
|
+
};
|
|
4739
|
+
}
|
|
4740
|
+
decryptEntry(entry) {
|
|
4741
|
+
if (!this.vault.isEncrypted(entry.accessToken) || entry.refreshToken !== void 0 && !this.vault.isEncrypted(entry.refreshToken)) {
|
|
4742
|
+
throw new Error("MCP token store contains an unencrypted token");
|
|
4743
|
+
}
|
|
4744
|
+
const value = {
|
|
4745
|
+
serverName: entry.serverName,
|
|
4746
|
+
resource: entry.resource,
|
|
4747
|
+
clientId: entry.clientId,
|
|
4748
|
+
authorizationServer: entry.authorizationServer,
|
|
4749
|
+
tokenSet: {
|
|
4750
|
+
accessToken: this.vault.decrypt(entry.accessToken),
|
|
4751
|
+
refreshToken: entry.refreshToken ? this.vault.decrypt(entry.refreshToken) : void 0,
|
|
4752
|
+
tokenType: entry.tokenType,
|
|
4753
|
+
resource: entry.resource,
|
|
4754
|
+
expiresAt: entry.expiresAt,
|
|
4755
|
+
scopes: [...entry.scopes]
|
|
4756
|
+
},
|
|
4757
|
+
updatedAt: entry.updatedAt
|
|
4758
|
+
};
|
|
4759
|
+
return normalizeStoredAuthorization(value);
|
|
4760
|
+
}
|
|
4761
|
+
};
|
|
4762
|
+
var MCPRefreshingAuthorizationProvider = class {
|
|
4763
|
+
constructor(options) {
|
|
4764
|
+
this.options = options;
|
|
4765
|
+
this.resource = canonicalMcpResource(options.resource);
|
|
4766
|
+
this.refreshSkewMs = options.refreshSkewMs ?? DEFAULT_REFRESH_SKEW_MS;
|
|
4767
|
+
}
|
|
4768
|
+
options;
|
|
4769
|
+
refreshPromise;
|
|
4770
|
+
resource;
|
|
4771
|
+
refreshSkewMs;
|
|
4772
|
+
async getAccessToken(context) {
|
|
4773
|
+
this.assertContext(context);
|
|
4774
|
+
let state = await this.options.store.load(this.options.serverName, this.resource);
|
|
4775
|
+
if (!state) return void 0;
|
|
4776
|
+
if (state.tokenSet.expiresAt !== void 0 && state.tokenSet.expiresAt <= Date.now() + this.refreshSkewMs) {
|
|
4777
|
+
state = await this.refresh(state, context.signal);
|
|
4778
|
+
}
|
|
4779
|
+
if (!state) return void 0;
|
|
4780
|
+
if (state.tokenSet.expiresAt !== void 0 && state.tokenSet.expiresAt <= Date.now()) {
|
|
4781
|
+
this.emit("reauth_required", state);
|
|
4782
|
+
return void 0;
|
|
4783
|
+
}
|
|
4784
|
+
authorizationHeaderForToken(state.tokenSet, this.resource);
|
|
4785
|
+
return { ...state.tokenSet, scopes: [...state.tokenSet.scopes ?? []] };
|
|
4786
|
+
}
|
|
4787
|
+
async handleUnauthorized(challenge, context) {
|
|
4788
|
+
this.assertContext(context);
|
|
4789
|
+
if (challenge.resource !== this.resource) return false;
|
|
4790
|
+
const state = await this.options.store.load(this.options.serverName, this.resource);
|
|
4791
|
+
if (!state?.tokenSet.refreshToken) {
|
|
4792
|
+
if (state) this.emit("reauth_required", state);
|
|
4793
|
+
return false;
|
|
4794
|
+
}
|
|
4795
|
+
return await this.refresh(state, context.signal) !== void 0;
|
|
4796
|
+
}
|
|
4797
|
+
refresh(state, signal) {
|
|
4798
|
+
if (this.refreshPromise) return this.refreshPromise;
|
|
4799
|
+
this.refreshPromise = this.refreshInner(state, signal).finally(() => {
|
|
4800
|
+
this.refreshPromise = void 0;
|
|
4801
|
+
});
|
|
4802
|
+
return this.refreshPromise;
|
|
4803
|
+
}
|
|
4804
|
+
async refreshInner(state, signal) {
|
|
4805
|
+
const refreshToken = state.tokenSet.refreshToken;
|
|
4806
|
+
if (!refreshToken) {
|
|
4807
|
+
this.emit("reauth_required", state);
|
|
4808
|
+
return void 0;
|
|
4809
|
+
}
|
|
4810
|
+
const tokenSet = await refreshMcpAccessToken({
|
|
4811
|
+
authorizationServer: state.authorizationServer,
|
|
4812
|
+
clientId: state.clientId,
|
|
4813
|
+
resource: state.resource,
|
|
4814
|
+
refreshToken,
|
|
4815
|
+
signal
|
|
4816
|
+
});
|
|
4817
|
+
const next = normalizeStoredAuthorization({
|
|
4818
|
+
...state,
|
|
4819
|
+
tokenSet,
|
|
4820
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4821
|
+
});
|
|
4822
|
+
await this.options.store.save(next);
|
|
4823
|
+
this.emit("refreshed", next);
|
|
4824
|
+
return next;
|
|
4825
|
+
}
|
|
4826
|
+
assertContext(context) {
|
|
4827
|
+
if (context.serverName !== this.options.serverName || context.resource !== this.resource) {
|
|
4828
|
+
throw new Error("MCP authorization provider context does not match its server/resource");
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
emit(state, value) {
|
|
4832
|
+
this.options.onStateChange?.({
|
|
4833
|
+
serverName: value.serverName,
|
|
4834
|
+
state,
|
|
4835
|
+
resource: value.resource,
|
|
4836
|
+
expiresAt: value.tokenSet.expiresAt,
|
|
4837
|
+
scopes: [...value.tokenSet.scopes ?? []]
|
|
4838
|
+
});
|
|
4839
|
+
}
|
|
4840
|
+
};
|
|
4841
|
+
function createVaultBackedMcpAuthorizationProviderFactory(options) {
|
|
4842
|
+
const providers = /* @__PURE__ */ new Map();
|
|
4843
|
+
return (server) => {
|
|
4844
|
+
if (server.transport === "stdio" || !server.url) return void 0;
|
|
4845
|
+
const resource = canonicalMcpResource(server.url);
|
|
4846
|
+
const key = `${server.name}\0${resource}`;
|
|
4847
|
+
let provider = providers.get(key);
|
|
4848
|
+
if (!provider) {
|
|
4849
|
+
provider = new MCPRefreshingAuthorizationProvider({
|
|
4850
|
+
serverName: server.name,
|
|
4851
|
+
resource,
|
|
4852
|
+
store: options.store,
|
|
4853
|
+
refreshSkewMs: options.refreshSkewMs,
|
|
4854
|
+
onStateChange: options.onStateChange
|
|
4855
|
+
});
|
|
4856
|
+
providers.set(key, provider);
|
|
4857
|
+
}
|
|
4858
|
+
return provider;
|
|
4859
|
+
};
|
|
4860
|
+
}
|
|
4861
|
+
function emptyFile() {
|
|
4862
|
+
return { version: TOKEN_STORE_VERSION, updatedAt: (/* @__PURE__ */ new Date(0)).toISOString(), entries: [] };
|
|
4863
|
+
}
|
|
4864
|
+
function validateStoreFile(value) {
|
|
4865
|
+
if (!isRecord(value) || value["version"] !== TOKEN_STORE_VERSION || !Array.isArray(value["entries"])) {
|
|
4866
|
+
throw new Error("MCP token store has an unsupported or malformed structure");
|
|
4867
|
+
}
|
|
4868
|
+
if (value["entries"].length > MAX_ENTRIES)
|
|
4869
|
+
throw new Error("MCP token store has too many entries");
|
|
4870
|
+
return {
|
|
4871
|
+
version: TOKEN_STORE_VERSION,
|
|
4872
|
+
updatedAt: boundedString(value["updatedAt"], "updatedAt", 128),
|
|
4873
|
+
entries: value["entries"].map(validateEncryptedEntry)
|
|
4874
|
+
};
|
|
4875
|
+
}
|
|
4876
|
+
function validateEncryptedEntry(value) {
|
|
4877
|
+
if (!isRecord(value)) throw new Error("MCP token store entry must be an object");
|
|
4878
|
+
const resource = canonicalMcpResource(boundedString(value["resource"], "resource", 4096));
|
|
4879
|
+
const authorizationServer = validateMcpAuthorizationServerMetadata(value["authorizationServer"]);
|
|
4880
|
+
const scopes = stringArray(value["scopes"], "scopes", 128);
|
|
4881
|
+
const expiresAt = value["expiresAt"];
|
|
4882
|
+
if (expiresAt !== void 0 && (typeof expiresAt !== "number" || !Number.isFinite(expiresAt))) {
|
|
4883
|
+
throw new Error("MCP token store expiresAt must be a finite number");
|
|
4884
|
+
}
|
|
4885
|
+
return {
|
|
4886
|
+
serverName: boundedString(value["serverName"], "serverName", 256),
|
|
4887
|
+
resource,
|
|
4888
|
+
clientId: boundedString(value["clientId"], "clientId", 4096),
|
|
4889
|
+
authorizationServer,
|
|
4890
|
+
accessToken: boundedString(value["accessToken"], "accessToken", 32768),
|
|
4891
|
+
refreshToken: value["refreshToken"] === void 0 ? void 0 : boundedString(value["refreshToken"], "refreshToken", 32768),
|
|
4892
|
+
tokenType: boundedString(value["tokenType"], "tokenType", 64),
|
|
4893
|
+
expiresAt,
|
|
4894
|
+
scopes,
|
|
4895
|
+
updatedAt: boundedString(value["updatedAt"], "updatedAt", 128)
|
|
4896
|
+
};
|
|
4897
|
+
}
|
|
4898
|
+
function normalizeStoredAuthorization(value) {
|
|
4899
|
+
const serverName = boundedString(value.serverName, "serverName", 256);
|
|
4900
|
+
const resource = canonicalMcpResource(value.resource);
|
|
4901
|
+
const authorizationServer = validateMcpAuthorizationServerMetadata(value.authorizationServer);
|
|
4902
|
+
if (canonicalMcpResource(value.tokenSet.resource) !== resource) {
|
|
4903
|
+
throw new Error("MCP token resource mismatch");
|
|
4904
|
+
}
|
|
4905
|
+
const tokenSet = {
|
|
4906
|
+
...value.tokenSet,
|
|
4907
|
+
resource,
|
|
4908
|
+
scopes: stringArray(value.tokenSet.scopes ?? [], "scopes", 128)
|
|
4909
|
+
};
|
|
4910
|
+
authorizationHeaderForToken({ ...tokenSet, expiresAt: void 0 }, resource);
|
|
4911
|
+
return {
|
|
4912
|
+
serverName,
|
|
4913
|
+
resource,
|
|
4914
|
+
clientId: boundedString(value.clientId, "clientId", 4096),
|
|
4915
|
+
authorizationServer,
|
|
4916
|
+
tokenSet,
|
|
4917
|
+
updatedAt: boundedString(value.updatedAt, "updatedAt", 128)
|
|
4918
|
+
};
|
|
4919
|
+
}
|
|
4920
|
+
function isRecord(value) {
|
|
4921
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4922
|
+
}
|
|
4923
|
+
function boundedString(value, field, maxLength) {
|
|
4924
|
+
if (typeof value !== "string" || value.length === 0 || value.length > maxLength || /[\r\n]/.test(value)) {
|
|
4925
|
+
throw new Error(`MCP token store field "${field}" is invalid`);
|
|
4926
|
+
}
|
|
4927
|
+
return value;
|
|
4928
|
+
}
|
|
4929
|
+
function stringArray(value, field, maxItems) {
|
|
4930
|
+
if (!Array.isArray(value) || value.length > maxItems) {
|
|
4931
|
+
throw new Error(`MCP token store field "${field}" must be a bounded array`);
|
|
4932
|
+
}
|
|
4933
|
+
return [...new Set(value.map((entry) => boundedString(entry, field, 256)))];
|
|
4934
|
+
}
|
|
2515
4935
|
export {
|
|
4936
|
+
DEFAULT_MCP_INSERTION_MAX_BYTES,
|
|
4937
|
+
DEFAULT_MCP_RESOURCE_SCHEMES,
|
|
4938
|
+
MCPAuthorizationManager,
|
|
2516
4939
|
MCPClient,
|
|
4940
|
+
MCPRefreshingAuthorizationProvider,
|
|
2517
4941
|
MCPRegistry,
|
|
2518
4942
|
MCPServer,
|
|
4943
|
+
MCPVaultTokenStore,
|
|
2519
4944
|
MCP_CONSTANTS,
|
|
4945
|
+
MCP_OPERATION_LIMITS,
|
|
2520
4946
|
SSEReader,
|
|
2521
4947
|
SSETransport,
|
|
2522
4948
|
StreamableHTTPTransport,
|
|
2523
4949
|
addMcp,
|
|
4950
|
+
authorizationHeaderForToken,
|
|
4951
|
+
authorizationServerMetadataUrls,
|
|
4952
|
+
canonicalMcpResource,
|
|
4953
|
+
createMcpAuthorizationRequest,
|
|
4954
|
+
createVaultBackedMcpAuthorizationProviderFactory,
|
|
2524
4955
|
disableMcp,
|
|
2525
4956
|
discoverMcp,
|
|
4957
|
+
discoverMcpAuthorization,
|
|
2526
4958
|
enableMcp,
|
|
4959
|
+
exchangeMcpAuthorizationCode,
|
|
2527
4960
|
listMcp,
|
|
2528
4961
|
manifestConfigHash,
|
|
4962
|
+
parseAuthorizationServerMetadata,
|
|
4963
|
+
parseGetPromptResult,
|
|
4964
|
+
parseListPromptsResult,
|
|
4965
|
+
parseListResourceTemplatesResult,
|
|
4966
|
+
parseListResourcesResult,
|
|
4967
|
+
parseMcpAuthorizationCallback,
|
|
4968
|
+
parseMcpBearerChallenge,
|
|
4969
|
+
parseProtectedResourceMetadata,
|
|
4970
|
+
parseReadResourceResult,
|
|
4971
|
+
parseServerMetadata,
|
|
4972
|
+
preparePromptInsertion,
|
|
4973
|
+
prepareResourceInsertion,
|
|
4974
|
+
protectedResourceMetadataUrls,
|
|
4975
|
+
readCapabilityManifest,
|
|
2529
4976
|
readManifest,
|
|
4977
|
+
refreshMcpAccessToken,
|
|
2530
4978
|
removeMcp,
|
|
2531
4979
|
restartMcp,
|
|
2532
4980
|
serveHttp,
|
|
2533
4981
|
serveStdio,
|
|
2534
4982
|
toContentBlocks,
|
|
2535
4983
|
updateMcp,
|
|
4984
|
+
validateMcpAuthorizationServerMetadata,
|
|
2536
4985
|
wrapMCPTool,
|
|
4986
|
+
writeCapabilityManifest,
|
|
2537
4987
|
writeManifest
|
|
2538
4988
|
};
|
|
2539
4989
|
//# sourceMappingURL=index.js.map
|