@fitchmultz/pi-mcp-adapter 8.0.6 → 8.0.8
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/CHANGELOG.md +142 -8
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.js +58 -33
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +28 -2
- package/skills/mcp-scripting/SKILL.md +6 -6
package/dist/server-manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { clientOperations, drainClientCallbacks, drainClientOperations } from "./client-operations.js";
|
|
1
|
+
import { clientOperations, drainClientCallbacks, drainClientOperations, } from "./client-operations.js";
|
|
2
2
|
import { isServerDisabled, isNonInteractiveOAuth, validateServerProtocolConfig, } from "./types.js";
|
|
3
3
|
import { resolveNpxBinary } from "./npx-resolver.js";
|
|
4
4
|
import { logger } from "./logger.js";
|
|
@@ -7,7 +7,7 @@ import { getAuthBaseDir } from "./mcp-auth.js";
|
|
|
7
7
|
import { ADAPTER_VERSION, normalizeRequestTimeoutMs, resolveBearerToken, resolveCommandSecret, resolveCommandSecretsRecord, resolveConfigPath, resolveServerUrl, } from "./utils.js";
|
|
8
8
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
9
9
|
import { combineAbortSignals } from "./runtime-owner.js";
|
|
10
|
-
import { createMcpTraceWriter, isMcpTraceEnabled,
|
|
10
|
+
import { createMcpTraceWriter, isMcpTraceEnabled, traceTransportKind, wrapTransportWithMcpTrace, } from "./mcp-trace.js";
|
|
11
11
|
const MAX_CAPTURED_STDERR_BYTES = 8 * 1024;
|
|
12
12
|
const MAX_CAPTURED_STDERR_LINES = 3;
|
|
13
13
|
const abortCleanupPromises = new WeakMap();
|
|
@@ -18,9 +18,7 @@ function boundedStderrChunk(chunk) {
|
|
|
18
18
|
}
|
|
19
19
|
// Limit string conversion before encoding; Buffer.from(largeString) would
|
|
20
20
|
// otherwise allocate the entire stderr event before applying the cap.
|
|
21
|
-
const suffix = chunk.length > MAX_CAPTURED_STDERR_BYTES
|
|
22
|
-
? chunk.slice(-MAX_CAPTURED_STDERR_BYTES)
|
|
23
|
-
: chunk;
|
|
21
|
+
const suffix = chunk.length > MAX_CAPTURED_STDERR_BYTES ? chunk.slice(-MAX_CAPTURED_STDERR_BYTES) : chunk;
|
|
24
22
|
const bytes = Buffer.from(suffix, "utf8");
|
|
25
23
|
return bytes.byteLength > MAX_CAPTURED_STDERR_BYTES
|
|
26
24
|
? Buffer.from(bytes.subarray(bytes.byteLength - MAX_CAPTURED_STDERR_BYTES))
|
|
@@ -28,10 +26,12 @@ function boundedStderrChunk(chunk) {
|
|
|
28
26
|
}
|
|
29
27
|
function appendStderrTail(tail, chunk) {
|
|
30
28
|
const bytes = boundedStderrChunk(chunk);
|
|
31
|
-
if (bytes.length === 0)
|
|
29
|
+
if (bytes.length === 0) {
|
|
32
30
|
return tail;
|
|
33
|
-
|
|
31
|
+
}
|
|
32
|
+
if (tail.length === 0) {
|
|
34
33
|
return bytes;
|
|
34
|
+
}
|
|
35
35
|
const combined = Buffer.concat([tail, bytes]);
|
|
36
36
|
return combined.length > MAX_CAPTURED_STDERR_BYTES
|
|
37
37
|
? Buffer.from(combined.subarray(combined.length - MAX_CAPTURED_STDERR_BYTES))
|
|
@@ -62,8 +62,9 @@ export class McpServerManager {
|
|
|
62
62
|
traceWriter;
|
|
63
63
|
stopped = false;
|
|
64
64
|
releaseClientWhenSettled(client) {
|
|
65
|
-
if (this.ownedClients.has(client))
|
|
65
|
+
if (this.ownedClients.has(client)) {
|
|
66
66
|
void drainClientOperations(client).then(() => this.ownedClients.delete(client));
|
|
67
|
+
}
|
|
67
68
|
}
|
|
68
69
|
/** Own native send/auth tails even after a public request has been cancelled. */
|
|
69
70
|
ownTransportSends(transport, client) {
|
|
@@ -123,23 +124,28 @@ export class McpServerManager {
|
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
126
|
async connect(name, definition, signal) {
|
|
126
|
-
if (isServerDisabled(definition))
|
|
127
|
+
if (isServerDisabled(definition)) {
|
|
127
128
|
throw new Error(`MCP server "${name}" is disabled`);
|
|
128
|
-
|
|
129
|
+
}
|
|
130
|
+
if (this.stopped) {
|
|
129
131
|
throw new Error("MCP server manager is closed");
|
|
132
|
+
}
|
|
130
133
|
const ownedSignal = combineAbortSignals(this.runtimeSignal, signal);
|
|
131
134
|
throwIfAborted(ownedSignal);
|
|
132
135
|
const closing = this.closePromises.get(name);
|
|
133
|
-
if (closing)
|
|
136
|
+
if (closing) {
|
|
134
137
|
await abortable(closing, ownedSignal);
|
|
138
|
+
}
|
|
135
139
|
throwIfAborted(ownedSignal);
|
|
136
|
-
if (this.stopped)
|
|
140
|
+
if (this.stopped) {
|
|
137
141
|
throw new Error("MCP server manager is closed");
|
|
142
|
+
}
|
|
138
143
|
const wait = (promise, attempt) => {
|
|
139
144
|
attempt.waiters++;
|
|
140
145
|
return abortable(promise, ownedSignal).finally(() => {
|
|
141
|
-
if (--attempt.waiters === 0 && ownedSignal?.aborted)
|
|
146
|
+
if (--attempt.waiters === 0 && ownedSignal?.aborted) {
|
|
142
147
|
attempt.controller.abort(ownedSignal.reason);
|
|
148
|
+
}
|
|
143
149
|
});
|
|
144
150
|
};
|
|
145
151
|
// Dedupe concurrent connection attempts.
|
|
@@ -153,8 +159,9 @@ export class McpServerManager {
|
|
|
153
159
|
}
|
|
154
160
|
catch (error) {
|
|
155
161
|
throwIfAborted(ownedSignal);
|
|
156
|
-
if (this.containsCleanupFailure(error))
|
|
162
|
+
if (this.containsCleanupFailure(error)) {
|
|
157
163
|
throw error;
|
|
164
|
+
}
|
|
158
165
|
}
|
|
159
166
|
return this.connect(name, definition, signal);
|
|
160
167
|
}
|
|
@@ -168,8 +175,11 @@ export class McpServerManager {
|
|
|
168
175
|
const generation = this.closeGenerations.get(name) ?? 0;
|
|
169
176
|
const attempt = { controller: new AbortController(), waiters: 0 };
|
|
170
177
|
const attemptSignal = combineAbortSignals(this.runtimeSignal, attempt.controller.signal);
|
|
171
|
-
const promise = this.createConnection(name, definition, attemptSignal, ownedSignal ? attemptSignal : undefined)
|
|
172
|
-
|
|
178
|
+
const promise = this.createConnection(name, definition, attemptSignal, ownedSignal ? attemptSignal : undefined)
|
|
179
|
+
.then(async (connection) => {
|
|
180
|
+
if (connection.status === "closed" ||
|
|
181
|
+
attempt.controller.signal.aborted ||
|
|
182
|
+
(this.closeGenerations.get(name) ?? 0) !== generation) {
|
|
173
183
|
await this.disposeConnection(connection);
|
|
174
184
|
throwIfAborted(attemptSignal);
|
|
175
185
|
throw new Error(`MCP connection for ${name} was closed while connecting`);
|
|
@@ -177,11 +187,14 @@ export class McpServerManager {
|
|
|
177
187
|
connection.inFlight = this.connections.get(name)?.inFlight ?? 0;
|
|
178
188
|
this.connections.set(name, connection);
|
|
179
189
|
return connection;
|
|
180
|
-
})
|
|
181
|
-
|
|
190
|
+
})
|
|
191
|
+
.finally(() => {
|
|
192
|
+
if (this.connectPromises.get(name) === promise) {
|
|
182
193
|
this.connectPromises.delete(name);
|
|
183
|
-
|
|
194
|
+
}
|
|
195
|
+
if (this.connectAttempts.get(name) === attempt) {
|
|
184
196
|
this.connectAttempts.delete(name);
|
|
197
|
+
}
|
|
185
198
|
});
|
|
186
199
|
this.connectPromises.set(name, promise);
|
|
187
200
|
this.connectAttempts.set(name, attempt);
|
|
@@ -197,10 +210,12 @@ export class McpServerManager {
|
|
|
197
210
|
* connection, that fresh connection is returned untouched.
|
|
198
211
|
*/
|
|
199
212
|
async reconnect(name, definition, staleConnection, signal) {
|
|
200
|
-
if (isServerDisabled(definition))
|
|
213
|
+
if (isServerDisabled(definition)) {
|
|
201
214
|
throw new Error(`MCP server "${name}" is disabled`);
|
|
202
|
-
|
|
215
|
+
}
|
|
216
|
+
if (this.stopped) {
|
|
203
217
|
throw new Error("MCP server manager is closed");
|
|
218
|
+
}
|
|
204
219
|
const ownedSignal = combineAbortSignals(this.runtimeSignal, signal);
|
|
205
220
|
throwIfAborted(ownedSignal);
|
|
206
221
|
const inFlight = this.reconnectPromises.get(name);
|
|
@@ -222,8 +237,9 @@ export class McpServerManager {
|
|
|
222
237
|
// longer holds the connection we were asked to replace, someone else
|
|
223
238
|
// already reconnected (or connected) first.
|
|
224
239
|
if (current !== staleConnection) {
|
|
225
|
-
if (!current)
|
|
240
|
+
if (!current) {
|
|
226
241
|
throw new Error(`Server "${name}" is not connected`);
|
|
242
|
+
}
|
|
227
243
|
return current;
|
|
228
244
|
}
|
|
229
245
|
this.retire(name, staleConnection);
|
|
@@ -231,28 +247,34 @@ export class McpServerManager {
|
|
|
231
247
|
}
|
|
232
248
|
/** Retire only the expected current client; accepted native work drains before close. */
|
|
233
249
|
retire(name, connection) {
|
|
234
|
-
if (this.connections.get(name) !== connection)
|
|
250
|
+
if (this.connections.get(name) !== connection) {
|
|
235
251
|
return;
|
|
252
|
+
}
|
|
236
253
|
connection.status = "closed";
|
|
237
254
|
let retired = this.retiredConnections.get(name);
|
|
238
|
-
if (!retired)
|
|
239
|
-
this.retiredConnections.set(name, retired = new Set());
|
|
240
|
-
|
|
255
|
+
if (!retired) {
|
|
256
|
+
this.retiredConnections.set(name, (retired = new Set()));
|
|
257
|
+
}
|
|
258
|
+
if (retired.has(connection)) {
|
|
241
259
|
return;
|
|
260
|
+
}
|
|
242
261
|
retired.add(connection);
|
|
243
262
|
void (async () => {
|
|
244
263
|
for (;;) {
|
|
245
264
|
const pending = [...(clientOperations.get(connection.client) ?? [])]
|
|
246
|
-
.filter(([, kind]) => kind !== "subscription")
|
|
247
|
-
|
|
265
|
+
.filter(([, kind]) => kind !== "subscription")
|
|
266
|
+
.map(([operation]) => operation);
|
|
267
|
+
if (!pending.length) {
|
|
248
268
|
break;
|
|
269
|
+
}
|
|
249
270
|
await Promise.allSettled(pending);
|
|
250
271
|
}
|
|
251
272
|
await this.disposeConnection(connection);
|
|
252
273
|
retired.delete(connection);
|
|
253
|
-
if (retired.size === 0 && this.retiredConnections.get(name) === retired)
|
|
274
|
+
if (retired.size === 0 && this.retiredConnections.get(name) === retired) {
|
|
254
275
|
this.retiredConnections.delete(name);
|
|
255
|
-
|
|
276
|
+
}
|
|
277
|
+
})().catch((error) => {
|
|
256
278
|
// Keep failed retirement owned so explicit shutdown also reports it.
|
|
257
279
|
logger.debug(`MCP retired connection cleanup failed for ${name}: ${String(error)}`);
|
|
258
280
|
});
|
|
@@ -261,7 +283,7 @@ export class McpServerManager {
|
|
|
261
283
|
throwIfAborted(signal);
|
|
262
284
|
validateServerProtocolConfig(definition);
|
|
263
285
|
const runtime = await import("./connection-runtime.js");
|
|
264
|
-
const { StdioClientTransport, SdkHttpError, UnixSocketClientTransport, trackToolTransport, getOAuthRequest, isOAuthChallenge } = runtime;
|
|
286
|
+
const { StdioClientTransport, SdkHttpError, UnixSocketClientTransport, trackToolTransport, getOAuthRequest, isOAuthChallenge, } = runtime;
|
|
265
287
|
throwIfAborted(signal);
|
|
266
288
|
let client = this.createClient(name, definition, runtime);
|
|
267
289
|
const tracingEnabled = isMcpTraceEnabled(definition, this.traceSettings);
|
|
@@ -269,17 +291,19 @@ export class McpServerManager {
|
|
|
269
291
|
? (this.traceWriter ??= createMcpTraceWriter(this.defaultCwd, this.traceSettings ?? {}))
|
|
270
292
|
: undefined;
|
|
271
293
|
const traceObserver = traceWriter
|
|
272
|
-
? { record: event => traceWriter.write(event) }
|
|
294
|
+
? { record: (event) => traceWriter.write(event) }
|
|
273
295
|
: undefined;
|
|
274
296
|
let transport;
|
|
275
|
-
let implicitOAuth = Boolean(definition.url &&
|
|
276
|
-
|
|
297
|
+
let implicitOAuth = Boolean(definition.url &&
|
|
298
|
+
supportsOAuth(definition) &&
|
|
299
|
+
getOAuthRequest(name, resolveServerUrl(definition), getAuthBaseDir(this.authStorageOptions), this.oauthRuntime));
|
|
277
300
|
let oauthProvider = false;
|
|
278
301
|
let authError;
|
|
279
|
-
const onAuthChallenge = (error) => {
|
|
302
|
+
const onAuthChallenge = (error) => {
|
|
303
|
+
authError = error;
|
|
304
|
+
};
|
|
280
305
|
let stderrTail = Buffer.alloc(0);
|
|
281
|
-
const configuredTransports = [definition.command, definition.url, definition.socket]
|
|
282
|
-
.filter(value => typeof value === "string" && value.length > 0);
|
|
306
|
+
const configuredTransports = [definition.command, definition.url, definition.socket].filter((value) => typeof value === "string" && value.length > 0);
|
|
283
307
|
if (configuredTransports.length !== 1) {
|
|
284
308
|
throw new Error(`Server ${name} must configure exactly one of command, url, or socket`);
|
|
285
309
|
}
|
|
@@ -319,8 +343,9 @@ export class McpServerManager {
|
|
|
319
343
|
else {
|
|
320
344
|
transport = new UnixSocketClientTransport(resolveConfigPath(definition.socket));
|
|
321
345
|
}
|
|
322
|
-
if (!definition.url)
|
|
346
|
+
if (!definition.url) {
|
|
323
347
|
trackToolTransport(transport);
|
|
348
|
+
}
|
|
324
349
|
if (traceObserver) {
|
|
325
350
|
const traceTransportKindValue = traceTransportKind(definition, transport);
|
|
326
351
|
transport = wrapTransportWithMcpTrace(transport, name, traceTransportKindValue, traceObserver);
|
|
@@ -337,16 +362,21 @@ export class McpServerManager {
|
|
|
337
362
|
}
|
|
338
363
|
catch (error) {
|
|
339
364
|
// Only handshake failures establish that the HTTP endpoint is unsupported.
|
|
340
|
-
if (!definition.url ||
|
|
341
|
-
||
|
|
365
|
+
if (!definition.url ||
|
|
366
|
+
signal?.aborted ||
|
|
367
|
+
legacySse ||
|
|
368
|
+
!(error instanceof SdkHttpError) ||
|
|
369
|
+
![404, 405, 406, 415].includes(error.status)) {
|
|
342
370
|
throw error;
|
|
371
|
+
}
|
|
343
372
|
legacySse = true;
|
|
344
373
|
await client.close();
|
|
345
374
|
this.releaseClientWhenSettled(client);
|
|
346
375
|
client = this.createClient(name, { ...definition, protocolVersion: "legacy" }, runtime);
|
|
347
376
|
({ transport, oauthProvider } = this.createHttpTransport(runtime, definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
|
|
348
|
-
if (traceObserver)
|
|
377
|
+
if (traceObserver) {
|
|
349
378
|
transport = wrapTransportWithMcpTrace(transport, name, traceTransportKind(definition, transport), traceObserver);
|
|
379
|
+
}
|
|
350
380
|
continue;
|
|
351
381
|
}
|
|
352
382
|
this.attachAdapterNotificationHandlers(name, client, runtime.serverStreamResultPatchNotificationSchema);
|
|
@@ -376,11 +406,13 @@ export class McpServerManager {
|
|
|
376
406
|
];
|
|
377
407
|
const [toolResult] = await Promise.allSettled(catalogs);
|
|
378
408
|
throwIfAborted(signal);
|
|
379
|
-
if (toolResult.status === "rejected" && !isOAuthChallenge(toolResult.reason))
|
|
409
|
+
if (toolResult.status === "rejected" && !isOAuthChallenge(toolResult.reason)) {
|
|
380
410
|
throw toolResult.reason;
|
|
411
|
+
}
|
|
381
412
|
// Optional catalogs catch their errors; the native observer still sees auth challenges.
|
|
382
|
-
if (authError)
|
|
413
|
+
if (authError) {
|
|
383
414
|
throw authError;
|
|
415
|
+
}
|
|
384
416
|
const [tools, resources, promptResult] = await Promise.all(catalogs);
|
|
385
417
|
connection.tools = tools;
|
|
386
418
|
connection.resources = resources;
|
|
@@ -389,16 +421,22 @@ export class McpServerManager {
|
|
|
389
421
|
return connection;
|
|
390
422
|
}
|
|
391
423
|
catch (error) {
|
|
392
|
-
if (!definition.url ||
|
|
393
|
-
|
|
424
|
+
if (!definition.url ||
|
|
425
|
+
signal?.aborted ||
|
|
426
|
+
implicitOAuth ||
|
|
427
|
+
definition.auth !== undefined ||
|
|
428
|
+
!supportsOAuth(definition) ||
|
|
429
|
+
!isOAuthChallenge(error)) {
|
|
394
430
|
throw error;
|
|
431
|
+
}
|
|
395
432
|
implicitOAuth = true;
|
|
396
433
|
await client.close();
|
|
397
434
|
this.releaseClientWhenSettled(client);
|
|
398
435
|
client = this.createClient(name, legacySse ? { ...definition, protocolVersion: "legacy" } : definition, runtime);
|
|
399
436
|
({ transport, oauthProvider } = this.createHttpTransport(runtime, definition, name, client, legacySse, implicitOAuth, onAuthChallenge));
|
|
400
|
-
if (traceObserver)
|
|
437
|
+
if (traceObserver) {
|
|
401
438
|
transport = wrapTransportWithMcpTrace(transport, name, traceTransportKind(definition, transport), traceObserver);
|
|
439
|
+
}
|
|
402
440
|
}
|
|
403
441
|
}
|
|
404
442
|
}
|
|
@@ -410,12 +448,10 @@ export class McpServerManager {
|
|
|
410
448
|
const abortCleanupFailed = error instanceof AggregateError && error.message === "MCP connection abort cleanup failed";
|
|
411
449
|
const cleanupResults = abortCleanupFailed
|
|
412
450
|
? []
|
|
413
|
-
: await Promise.allSettled([
|
|
414
|
-
abortCleanup ?? Promise.resolve().then(() => client.close()),
|
|
415
|
-
]);
|
|
451
|
+
: await Promise.allSettled([abortCleanup ?? Promise.resolve().then(() => client.close())]);
|
|
416
452
|
await drainClientCallbacks(client);
|
|
417
453
|
this.releaseClientWhenSettled(client);
|
|
418
|
-
const cleanupFailures = cleanupResults.flatMap(result => result.status === "rejected" ? [result.reason] : []);
|
|
454
|
+
const cleanupFailures = cleanupResults.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
419
455
|
let reportedError = error;
|
|
420
456
|
if (cleanupFailures.length > 0) {
|
|
421
457
|
reportedError = new AggregateError([error, ...cleanupFailures], "MCP connection setup failed");
|
|
@@ -440,7 +476,10 @@ export class McpServerManager {
|
|
|
440
476
|
}
|
|
441
477
|
if (stderrTail.length > 0) {
|
|
442
478
|
const stderrText = stderrTail.toString("utf8").trim();
|
|
443
|
-
const lines = stderrText
|
|
479
|
+
const lines = stderrText
|
|
480
|
+
.split(/\r?\n/)
|
|
481
|
+
.map((line) => line.trim())
|
|
482
|
+
.filter(Boolean);
|
|
444
483
|
if (lines.length > 0) {
|
|
445
484
|
const baseMessage = reportedError instanceof Error ? reportedError.message : String(reportedError);
|
|
446
485
|
const detail = lines.slice(-MAX_CAPTURED_STDERR_LINES).join(" — ");
|
|
@@ -481,8 +520,14 @@ export class McpServerManager {
|
|
|
481
520
|
buildClientCapabilities(definition) {
|
|
482
521
|
return {
|
|
483
522
|
...(supportsOAuth(definition) && isNonInteractiveOAuth(definition.oauth)
|
|
484
|
-
? {
|
|
485
|
-
|
|
523
|
+
? {
|
|
524
|
+
extensions: {
|
|
525
|
+
[definition.oauth && definition.oauth.crossAppAccess
|
|
526
|
+
? "io.modelcontextprotocol/enterprise-managed-authorization"
|
|
527
|
+
: "io.modelcontextprotocol/oauth-client-credentials"]: {},
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
: {}),
|
|
486
531
|
...(this.samplingConfig ? { sampling: {} } : {}),
|
|
487
532
|
...(this.elicitationConfig
|
|
488
533
|
? {
|
|
@@ -499,7 +544,9 @@ export class McpServerManager {
|
|
|
499
544
|
const capabilities = this.buildClientCapabilities(definition);
|
|
500
545
|
let client;
|
|
501
546
|
client = new ManagedClient({ name: `pi-mcp-${serverName}`, version: ADAPTER_VERSION }, {
|
|
502
|
-
versionNegotiation: {
|
|
547
|
+
versionNegotiation: {
|
|
548
|
+
mode: definition.protocolVersion ?? (definition.url ? "auto" : "legacy"),
|
|
549
|
+
},
|
|
503
550
|
// ponytail: reject catalogs over 1024 pages; raise this native cap if a real catalog needs more.
|
|
504
551
|
listMaxPages: 1024,
|
|
505
552
|
...(Object.keys(capabilities).length > 0 ? { capabilities } : {}),
|
|
@@ -524,23 +571,26 @@ export class McpServerManager {
|
|
|
524
571
|
client.serverName = serverName;
|
|
525
572
|
this.ownedClients.add(client);
|
|
526
573
|
if (this.samplingConfig) {
|
|
527
|
-
registerSamplingHandler(client, { ...this.samplingConfig, serverName }, run => client.ownCallback("sampling callback", run));
|
|
574
|
+
registerSamplingHandler(client, { ...this.samplingConfig, serverName }, (run) => client.ownCallback("sampling callback", run));
|
|
528
575
|
}
|
|
529
576
|
if (this.elicitationConfig) {
|
|
530
577
|
registerElicitationHandler(client, {
|
|
531
578
|
...this.elicitationConfig,
|
|
532
579
|
serverName,
|
|
533
|
-
onUrlAccepted: elicitationId => this.rememberUrlElicitation(serverName, elicitationId),
|
|
534
|
-
}, run => client.ownCallback("elicitation callback", run));
|
|
580
|
+
onUrlAccepted: (elicitationId) => this.rememberUrlElicitation(serverName, elicitationId),
|
|
581
|
+
}, (run) => client.ownCallback("elicitation callback", run));
|
|
535
582
|
if (this.elicitationConfig.allowUrl) {
|
|
536
|
-
client.setNotificationHandler("notifications/elicitation/complete", notification => {
|
|
537
|
-
if (this.runtimeSignal?.aborted)
|
|
583
|
+
client.setNotificationHandler("notifications/elicitation/complete", (notification) => {
|
|
584
|
+
if (this.runtimeSignal?.aborted) {
|
|
538
585
|
return;
|
|
586
|
+
}
|
|
539
587
|
const accepted = this.acceptedUrlElicitations.get(serverName);
|
|
540
|
-
if (!accepted?.delete(notification.params.elicitationId))
|
|
588
|
+
if (!accepted?.delete(notification.params.elicitationId)) {
|
|
541
589
|
return;
|
|
542
|
-
|
|
590
|
+
}
|
|
591
|
+
if (accepted.size === 0) {
|
|
543
592
|
this.acceptedUrlElicitations.delete(serverName);
|
|
593
|
+
}
|
|
544
594
|
this.elicitationConfig?.ui.notify(`MCP browser interaction for ${serverName} completed. You can retry the tool now.`, "info");
|
|
545
595
|
});
|
|
546
596
|
}
|
|
@@ -552,11 +602,13 @@ export class McpServerManager {
|
|
|
552
602
|
logger.debug(`MCP: tools/list_changed refresh failed for ${serverName}: ${error.message}`);
|
|
553
603
|
return;
|
|
554
604
|
}
|
|
555
|
-
if (!tools)
|
|
605
|
+
if (!tools) {
|
|
556
606
|
return;
|
|
607
|
+
}
|
|
557
608
|
const connection = this.connections.get(serverName);
|
|
558
|
-
if (!connection || connection.client !== client || connection.status !== "connected")
|
|
609
|
+
if (!connection || connection.client !== client || connection.status !== "connected") {
|
|
559
610
|
return;
|
|
611
|
+
}
|
|
560
612
|
connection.tools = tools;
|
|
561
613
|
this.metadataListChangedListener?.(serverName, "tools-list-changed");
|
|
562
614
|
}
|
|
@@ -565,11 +617,13 @@ export class McpServerManager {
|
|
|
565
617
|
logger.debug(`MCP: prompts/list_changed refresh failed for ${serverName}: ${error.message}`);
|
|
566
618
|
return;
|
|
567
619
|
}
|
|
568
|
-
if (!prompts)
|
|
620
|
+
if (!prompts) {
|
|
569
621
|
return;
|
|
622
|
+
}
|
|
570
623
|
const connection = this.connections.get(serverName);
|
|
571
|
-
if (!connection || connection.client !== client || connection.status !== "connected")
|
|
624
|
+
if (!connection || connection.client !== client || connection.status !== "connected") {
|
|
572
625
|
return;
|
|
626
|
+
}
|
|
573
627
|
connection.prompts = prompts;
|
|
574
628
|
connection.promptDiscoveryFailed = false;
|
|
575
629
|
this.metadataListChangedListener?.(serverName, "prompts-list-changed");
|
|
@@ -579,35 +633,41 @@ export class McpServerManager {
|
|
|
579
633
|
logger.debug(`MCP: resources/list_changed refresh failed for ${serverName}: ${error.message}`);
|
|
580
634
|
return;
|
|
581
635
|
}
|
|
582
|
-
if (!resources)
|
|
636
|
+
if (!resources) {
|
|
583
637
|
return;
|
|
638
|
+
}
|
|
584
639
|
const connection = this.connections.get(serverName);
|
|
585
|
-
if (!connection || connection.client !== client || connection.status !== "connected")
|
|
640
|
+
if (!connection || connection.client !== client || connection.status !== "connected") {
|
|
586
641
|
return;
|
|
642
|
+
}
|
|
587
643
|
connection.resources = resources;
|
|
588
644
|
this.metadataListChangedListener?.(serverName, "resources-list-changed");
|
|
589
645
|
}
|
|
590
646
|
async handleUrlElicitationRequired(serverName, error) {
|
|
591
|
-
if (this.runtimeSignal?.aborted || !this.elicitationConfig?.allowUrl)
|
|
647
|
+
if (this.runtimeSignal?.aborted || !this.elicitationConfig?.allowUrl) {
|
|
592
648
|
return "cancel";
|
|
649
|
+
}
|
|
593
650
|
const { handleUrlElicitation } = await import("./elicitation-handler.js");
|
|
594
651
|
// The import yields; recheck cancellation and URL support before prompting.
|
|
595
|
-
if (this.runtimeSignal?.aborted || !this.elicitationConfig?.allowUrl)
|
|
652
|
+
if (this.runtimeSignal?.aborted || !this.elicitationConfig?.allowUrl) {
|
|
596
653
|
return "cancel";
|
|
654
|
+
}
|
|
597
655
|
for (const params of error.elicitations) {
|
|
598
656
|
const result = await handleUrlElicitation({
|
|
599
657
|
...this.elicitationConfig,
|
|
600
658
|
serverName,
|
|
601
|
-
onUrlAccepted: elicitationId => this.rememberUrlElicitation(serverName, elicitationId),
|
|
659
|
+
onUrlAccepted: (elicitationId) => this.rememberUrlElicitation(serverName, elicitationId),
|
|
602
660
|
}, params);
|
|
603
|
-
if (result.action !== "accept")
|
|
661
|
+
if (result.action !== "accept") {
|
|
604
662
|
return result.action;
|
|
663
|
+
}
|
|
605
664
|
}
|
|
606
665
|
return "accept";
|
|
607
666
|
}
|
|
608
667
|
rememberUrlElicitation(serverName, elicitationId) {
|
|
609
|
-
if (this.runtimeSignal?.aborted)
|
|
668
|
+
if (this.runtimeSignal?.aborted) {
|
|
610
669
|
return;
|
|
670
|
+
}
|
|
611
671
|
let accepted = this.acceptedUrlElicitations.get(serverName);
|
|
612
672
|
if (!accepted) {
|
|
613
673
|
accepted = new Set();
|
|
@@ -622,13 +682,14 @@ export class McpServerManager {
|
|
|
622
682
|
const storageBase = getAuthBaseDir(this.authStorageOptions);
|
|
623
683
|
const runtime = this.oauthRuntime;
|
|
624
684
|
const generation = this.closeGenerations.get(serverName) ?? 0;
|
|
625
|
-
const active = () => !this.stopped &&
|
|
626
|
-
|
|
685
|
+
const active = () => !this.stopped &&
|
|
686
|
+
!this.runtimeSignal?.aborted &&
|
|
687
|
+
!runtime?.signal.aborted &&
|
|
688
|
+
(this.closeGenerations.get(serverName) ?? 0) === generation;
|
|
627
689
|
let issuer;
|
|
628
690
|
// Resolve secret commands only for this connection attempt, without mutating config.
|
|
629
|
-
const hasCommandHeader = Object.values(definition.headers ?? {})
|
|
630
|
-
|
|
631
|
-
const headers = resolveCommandSecretsRecord(definition.headers, key => `MCP server "${serverName}" HTTP header "${key}"`) ?? {};
|
|
691
|
+
const hasCommandHeader = Object.values(definition.headers ?? {}).some((value) => value.startsWith("!") && !value.startsWith("!!"));
|
|
692
|
+
const headers = resolveCommandSecretsRecord(definition.headers, (key) => `MCP server "${serverName}" HTTP header "${key}"`) ?? {};
|
|
632
693
|
// For bearer auth, add the token to headers BEFORE creating requestInit
|
|
633
694
|
const commandBearer = definition.bearerToken?.startsWith("!") && !definition.bearerToken.startsWith("!!")
|
|
634
695
|
? definition.bearerToken
|
|
@@ -637,8 +698,9 @@ export class McpServerManager {
|
|
|
637
698
|
const token = commandBearer
|
|
638
699
|
? resolveCommandSecret(commandBearer, `MCP server "${serverName}" HTTP bearer token`)
|
|
639
700
|
: resolveBearerToken(definition);
|
|
640
|
-
if (token)
|
|
701
|
+
if (token) {
|
|
641
702
|
headers["Authorization"] = `Bearer ${token}`;
|
|
703
|
+
}
|
|
642
704
|
}
|
|
643
705
|
if (hasCommandHeader || commandBearer) {
|
|
644
706
|
try {
|
|
@@ -654,37 +716,45 @@ export class McpServerManager {
|
|
|
654
716
|
onRedirect: async (_authUrl) => {
|
|
655
717
|
// URL is captured by startAuth, no need to log
|
|
656
718
|
},
|
|
657
|
-
onDiscoveryState: state => {
|
|
719
|
+
onDiscoveryState: (state) => {
|
|
658
720
|
issuer = state.authorizationServerMetadata?.issuer ?? state.authorizationServerUrl;
|
|
659
|
-
if (active())
|
|
721
|
+
if (active()) {
|
|
660
722
|
bindOAuthRequestIssuer(serverName, serverUrl, storageBase, issuer, runtime);
|
|
723
|
+
}
|
|
661
724
|
},
|
|
662
725
|
}, this.authStorageOptions, this.oauthRuntime?.signal);
|
|
663
726
|
const authProvider = supportsOAuth(definition) && (definition.auth === "oauth" || implicitOAuth)
|
|
664
|
-
? createAuthProvider()
|
|
727
|
+
? createAuthProvider()
|
|
728
|
+
: undefined;
|
|
665
729
|
client.oauthProvider = authProvider;
|
|
666
730
|
let transportClosed = false;
|
|
667
731
|
// The SDK launches legacy GET bootstrap/reconnect outside send(). Fence every
|
|
668
732
|
// provider entry and own its ACTUAL promise (including signing/storage after
|
|
669
733
|
// cancellation). Keep the OAuthClientProvider shape: native 401/403 behavior,
|
|
670
734
|
// issuer validation and noninteractive grants remain SDK-owned.
|
|
671
|
-
const ownedProvider = authProvider &&
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
return
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
735
|
+
const ownedProvider = authProvider &&
|
|
736
|
+
new Proxy(authProvider, {
|
|
737
|
+
get: (provider, key) => {
|
|
738
|
+
const value = Reflect.get(provider, key, provider);
|
|
739
|
+
if (typeof value !== "function") {
|
|
740
|
+
return value;
|
|
741
|
+
}
|
|
742
|
+
return (...args) => client.ownNative(async () => {
|
|
743
|
+
provider.lifetimeSignal.throwIfAborted();
|
|
744
|
+
return Reflect.apply(value, provider, args);
|
|
745
|
+
});
|
|
746
|
+
},
|
|
747
|
+
});
|
|
682
748
|
const fetch = (input, init) => client.ownNative(async () => {
|
|
683
|
-
if (transportClosed)
|
|
749
|
+
if (transportClosed) {
|
|
684
750
|
throw new Error("MCP transport is closed");
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
751
|
+
}
|
|
752
|
+
const response = await trackToolTransportFailure(input, authProvider
|
|
753
|
+
? {
|
|
754
|
+
...init,
|
|
755
|
+
signal: combineAbortSignals(init?.signal ?? undefined, init?.signal ? authProvider.lifetimeSignal : authProvider.signal),
|
|
756
|
+
}
|
|
757
|
+
: init);
|
|
688
758
|
// Fetch resolves at headers. Native auth/discovery subsequently consumes
|
|
689
759
|
// text/JSON bodies; keep those real reads owned too. Never count the idle
|
|
690
760
|
// SSE body as a perpetual request: its messages use the ingress fence.
|
|
@@ -700,43 +770,55 @@ export class McpServerManager {
|
|
|
700
770
|
// provider calls. Noninteractive transports retain OAuthClientProvider so
|
|
701
771
|
// the SDK's 403 scope-union/retry policy is unchanged; all their asynchronous
|
|
702
772
|
// effects are provider calls (including JWT signing) and fetch/body reads.
|
|
703
|
-
const transportAuth = ownedProvider
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
773
|
+
const transportAuth = ownedProvider && !isNonInteractiveOAuth(definition.oauth) && !legacySse
|
|
774
|
+
? {
|
|
775
|
+
// Preserve transport configuration introspection; auth() uses this same provider.
|
|
776
|
+
get redirectUrl() {
|
|
777
|
+
return ownedProvider.redirectUrl;
|
|
778
|
+
},
|
|
779
|
+
get clientMetadata() {
|
|
780
|
+
return ownedProvider.clientMetadata;
|
|
781
|
+
},
|
|
782
|
+
token: () => client.ownNative(async () => (await ownedProvider.tokens())?.access_token),
|
|
783
|
+
onUnauthorized: (context) => client.ownNative(async () => {
|
|
784
|
+
const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(context.response);
|
|
785
|
+
const result = await auth(ownedProvider, {
|
|
786
|
+
serverUrl: context.serverUrl,
|
|
787
|
+
fetchFn: context.fetchFn,
|
|
788
|
+
...(resourceMetadataUrl !== undefined ? { resourceMetadataUrl } : {}),
|
|
789
|
+
...(scope !== undefined ? { scope } : {}),
|
|
790
|
+
skipIssuerMetadataValidation: definition.oauth !== false &&
|
|
791
|
+
definition.oauth?.skipIssuerMetadataValidation === true,
|
|
792
|
+
});
|
|
793
|
+
if (result !== "AUTHORIZED") {
|
|
794
|
+
throw new UnauthorizedError();
|
|
795
|
+
}
|
|
796
|
+
}),
|
|
797
|
+
}
|
|
798
|
+
: ownedProvider;
|
|
721
799
|
const options = {
|
|
722
800
|
...(requestInit !== undefined ? { requestInit } : {}),
|
|
723
801
|
...(transportAuth !== undefined ? { authProvider: transportAuth } : {}),
|
|
724
802
|
skipIssuerMetadataValidation: definition.oauth !== false && definition.oauth?.skipIssuerMetadataValidation === true,
|
|
725
803
|
fetch,
|
|
726
804
|
};
|
|
727
|
-
const transport = legacySse
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
805
|
+
const transport = legacySse
|
|
806
|
+
? new SSEClientTransport(url, options)
|
|
807
|
+
: new StreamableHTTPClientTransport(url, {
|
|
808
|
+
...options,
|
|
809
|
+
reconnectionScheduler: (reconnect, delay) => {
|
|
810
|
+
const timer = setTimeout(() => {
|
|
811
|
+
if (transportClosed) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
reconnect();
|
|
815
|
+
}, delay);
|
|
816
|
+
return () => clearTimeout(timer);
|
|
817
|
+
},
|
|
818
|
+
...(supportsOAuth(definition) && !isNonInteractiveOAuth(definition.oauth)
|
|
819
|
+
? { onInsufficientScope: "throw" }
|
|
820
|
+
: {}),
|
|
821
|
+
});
|
|
740
822
|
if (authProvider) {
|
|
741
823
|
const send = transport.send.bind(transport);
|
|
742
824
|
transport.send = async (message, options) => authProvider.runWithSignal(options?.requestSignal, () => send(message, options));
|
|
@@ -750,16 +832,21 @@ export class McpServerManager {
|
|
|
750
832
|
};
|
|
751
833
|
if (!legacySse && supportsOAuth(definition)) {
|
|
752
834
|
const previous = transport.onerror;
|
|
753
|
-
transport.onerror = error => {
|
|
835
|
+
transport.onerror = (error) => {
|
|
754
836
|
previous?.(error);
|
|
755
|
-
if (!active() ||
|
|
837
|
+
if (!active() ||
|
|
838
|
+
isServerDisabled(definition) ||
|
|
839
|
+
!supportsOAuth(definition) ||
|
|
840
|
+
!isOAuthChallenge(error)) {
|
|
756
841
|
return;
|
|
842
|
+
}
|
|
757
843
|
recordOAuthChallenge(serverName, serverUrl, storageBase, error, runtime);
|
|
758
844
|
// A known issuer was validated by the native discovery hook, never inferred from the challenge.
|
|
759
845
|
if (issuer) {
|
|
760
846
|
const request = getOAuthRequest(serverName, serverUrl, storageBase, runtime);
|
|
761
|
-
if (request && !request.issuer)
|
|
847
|
+
if (request && !request.issuer) {
|
|
762
848
|
request.issuer = issuer;
|
|
849
|
+
}
|
|
763
850
|
}
|
|
764
851
|
onAuthChallenge?.(error);
|
|
765
852
|
};
|
|
@@ -772,15 +859,20 @@ export class McpServerManager {
|
|
|
772
859
|
}
|
|
773
860
|
async fetchAllPrompts(client, requestOptions) {
|
|
774
861
|
const capabilities = client.getServerCapabilities?.();
|
|
775
|
-
if (!capabilities?.prompts)
|
|
862
|
+
if (!capabilities?.prompts) {
|
|
776
863
|
return { prompts: [], failed: false };
|
|
864
|
+
}
|
|
777
865
|
try {
|
|
778
|
-
const { prompts } = await client.listPrompts(undefined, {
|
|
866
|
+
const { prompts } = await client.listPrompts(undefined, {
|
|
867
|
+
...requestOptions,
|
|
868
|
+
cacheMode: "refresh",
|
|
869
|
+
});
|
|
779
870
|
return { prompts, failed: false };
|
|
780
871
|
}
|
|
781
872
|
catch (error) {
|
|
782
|
-
if (requestOptions?.signal?.aborted)
|
|
873
|
+
if (requestOptions?.signal?.aborted) {
|
|
783
874
|
throwIfAborted(requestOptions.signal);
|
|
875
|
+
}
|
|
784
876
|
const message = error instanceof Error ? error.message : String(error);
|
|
785
877
|
logger.debug(`MCP: prompts/list failed: ${message}`);
|
|
786
878
|
return { prompts: [], failed: true };
|
|
@@ -788,10 +880,12 @@ export class McpServerManager {
|
|
|
788
880
|
}
|
|
789
881
|
async fetchAllResources(client, requestOptions) {
|
|
790
882
|
const capabilities = client.getServerCapabilities?.();
|
|
791
|
-
if (!capabilities?.resources)
|
|
883
|
+
if (!capabilities?.resources) {
|
|
792
884
|
return [];
|
|
885
|
+
}
|
|
793
886
|
try {
|
|
794
|
-
return (await client.listResources(undefined, { ...requestOptions, cacheMode: "refresh" }))
|
|
887
|
+
return (await client.listResources(undefined, { ...requestOptions, cacheMode: "refresh" }))
|
|
888
|
+
.resources;
|
|
795
889
|
}
|
|
796
890
|
catch {
|
|
797
891
|
if (requestOptions?.signal?.aborted) {
|
|
@@ -802,7 +896,7 @@ export class McpServerManager {
|
|
|
802
896
|
}
|
|
803
897
|
}
|
|
804
898
|
attachAdapterNotificationHandlers(serverName, client, schema) {
|
|
805
|
-
client.setNotificationHandler(schema.shape.method.value, { params: schema.shape.params }, params => {
|
|
899
|
+
client.setNotificationHandler(schema.shape.method.value, { params: schema.shape.params }, (params) => {
|
|
806
900
|
this.uiStreamListeners.get(params.streamToken)?.(serverName, params);
|
|
807
901
|
});
|
|
808
902
|
}
|
|
@@ -847,106 +941,125 @@ export class McpServerManager {
|
|
|
847
941
|
}
|
|
848
942
|
async close(name) {
|
|
849
943
|
this.closeGenerations.set(name, (this.closeGenerations.get(name) ?? 0) + 1);
|
|
850
|
-
this.connectAttempts
|
|
944
|
+
this.connectAttempts
|
|
945
|
+
.get(name)
|
|
946
|
+
?.controller.abort(new Error(`MCP connection ${name} was closed`));
|
|
851
947
|
const pendingClose = this.closePromises.get(name);
|
|
852
|
-
if (pendingClose)
|
|
948
|
+
if (pendingClose) {
|
|
853
949
|
return pendingClose;
|
|
950
|
+
}
|
|
854
951
|
const owned = new Set(this.retiredConnections.get(name));
|
|
855
952
|
const connection = this.connections.get(name);
|
|
856
|
-
if (connection)
|
|
953
|
+
if (connection) {
|
|
857
954
|
owned.add(connection);
|
|
955
|
+
}
|
|
858
956
|
// Keep failed cleanup/callback tails owned even after removal from the active connection map.
|
|
859
|
-
if (owned.size)
|
|
957
|
+
if (owned.size) {
|
|
860
958
|
this.retiredConnections.set(name, owned);
|
|
959
|
+
}
|
|
861
960
|
// Remove the placeholder before awaiting cleanup; stale callers cannot resurrect it.
|
|
862
961
|
this.connections.delete(name);
|
|
863
962
|
this.acceptedUrlElicitations.delete(name);
|
|
864
|
-
for (const item of owned)
|
|
963
|
+
for (const item of owned) {
|
|
865
964
|
item.status = "closed";
|
|
965
|
+
}
|
|
866
966
|
const pendingConnect = this.connectPromises.get(name);
|
|
867
967
|
const closing = (async () => {
|
|
868
968
|
const results = await Promise.allSettled([
|
|
869
|
-
...[...owned].map(item => this.disposeConnection(item)),
|
|
870
|
-
pendingConnect?.catch(error => {
|
|
871
|
-
if (this.containsCleanupFailure(error))
|
|
969
|
+
...[...owned].map((item) => this.disposeConnection(item)),
|
|
970
|
+
pendingConnect?.catch((error) => {
|
|
971
|
+
if (this.containsCleanupFailure(error)) {
|
|
872
972
|
throw error;
|
|
973
|
+
}
|
|
873
974
|
}),
|
|
874
975
|
]);
|
|
875
|
-
const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
|
|
876
|
-
if (failures.length > 0)
|
|
976
|
+
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
977
|
+
if (failures.length > 0) {
|
|
877
978
|
throw new AggregateError(failures, "MCP connection cleanup failed");
|
|
979
|
+
}
|
|
878
980
|
this.retiredConnections.delete(name);
|
|
879
981
|
})().finally(() => {
|
|
880
|
-
if (this.closePromises.get(name) === closing)
|
|
982
|
+
if (this.closePromises.get(name) === closing) {
|
|
881
983
|
this.closePromises.delete(name);
|
|
984
|
+
}
|
|
882
985
|
});
|
|
883
986
|
this.closePromises.set(name, closing);
|
|
884
987
|
return closing;
|
|
885
988
|
}
|
|
886
989
|
disposeConnection(connection) {
|
|
887
990
|
const existing = this.disposePromises.get(connection);
|
|
888
|
-
if (existing)
|
|
991
|
+
if (existing) {
|
|
889
992
|
return existing;
|
|
993
|
+
}
|
|
890
994
|
const closing = (async () => {
|
|
891
995
|
const results = await Promise.allSettled([
|
|
892
996
|
// The SDK client owns its transport.
|
|
893
997
|
Promise.resolve().then(() => connection.client.close()),
|
|
894
998
|
this.traceWriter?.flush() ?? Promise.resolve(),
|
|
895
999
|
]);
|
|
896
|
-
const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
|
|
1000
|
+
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
897
1001
|
// Keep ordinary per-server cancellation prompt, but retain native send/auth tails for
|
|
898
1002
|
// readiness and final shutdown. Inbound handler promises still drain before disposal.
|
|
899
1003
|
await drainClientCallbacks(connection.client);
|
|
900
|
-
if (failures.length > 0)
|
|
1004
|
+
if (failures.length > 0) {
|
|
901
1005
|
throw new AggregateError(failures, "MCP connection cleanup failed");
|
|
1006
|
+
}
|
|
902
1007
|
this.releaseClientWhenSettled(connection.client);
|
|
903
1008
|
})();
|
|
904
1009
|
this.disposePromises.set(connection, closing);
|
|
905
1010
|
void closing.catch(() => {
|
|
906
1011
|
// Native close retains its transport until onclose. Retry only on an
|
|
907
1012
|
// explicit later close, retaining the connection/operations veto meanwhile.
|
|
908
|
-
if (connection.client.transport === connection.transport &&
|
|
1013
|
+
if (connection.client.transport === connection.transport &&
|
|
1014
|
+
this.disposePromises.get(connection) === closing) {
|
|
909
1015
|
this.disposePromises.delete(connection);
|
|
1016
|
+
}
|
|
910
1017
|
});
|
|
911
1018
|
return closing;
|
|
912
1019
|
}
|
|
913
1020
|
async closeAll() {
|
|
914
1021
|
this.stopped = true;
|
|
915
1022
|
const names = new Set([
|
|
916
|
-
...this.connections.keys(),
|
|
917
|
-
...this.
|
|
1023
|
+
...this.connections.keys(),
|
|
1024
|
+
...this.retiredConnections.keys(),
|
|
1025
|
+
...this.connectPromises.keys(),
|
|
1026
|
+
...this.closePromises.keys(),
|
|
918
1027
|
]);
|
|
919
|
-
const results = await Promise.allSettled([...names].map(name => this.close(name)));
|
|
920
|
-
const failures = results.flatMap(result => result.status === "rejected" ? [result.reason] : []);
|
|
1028
|
+
const results = await Promise.allSettled([...names].map((name) => this.close(name)));
|
|
1029
|
+
const failures = results.flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
921
1030
|
this.uiStreamListeners.clear();
|
|
922
1031
|
this.acceptedUrlElicitations.clear();
|
|
923
1032
|
this.samplingConfig = undefined;
|
|
924
1033
|
this.elicitationConfig = undefined;
|
|
925
|
-
await Promise.all([...this.ownedClients].map(client => drainClientOperations(client)));
|
|
1034
|
+
await Promise.all([...this.ownedClients].map((client) => drainClientOperations(client)));
|
|
926
1035
|
try {
|
|
927
1036
|
await this.traceWriter?.flush();
|
|
928
1037
|
}
|
|
929
1038
|
catch (error) {
|
|
930
1039
|
failures.push(error);
|
|
931
1040
|
}
|
|
932
|
-
if (failures.length > 0)
|
|
1041
|
+
if (failures.length > 0) {
|
|
933
1042
|
throw new AggregateError(failures, "MCP manager cleanup failed");
|
|
1043
|
+
}
|
|
934
1044
|
}
|
|
935
1045
|
containsCleanupFailure(error) {
|
|
936
1046
|
const pending = [error];
|
|
937
1047
|
const seen = new Set();
|
|
938
1048
|
while (pending.length > 0) {
|
|
939
1049
|
const current = pending.pop();
|
|
940
|
-
if (!(current instanceof Error) || seen.has(current))
|
|
1050
|
+
if (!(current instanceof Error) || seen.has(current)) {
|
|
941
1051
|
continue;
|
|
1052
|
+
}
|
|
942
1053
|
seen.add(current);
|
|
943
1054
|
if (current instanceof AggregateError) {
|
|
944
|
-
if (/cleanup failed|setup failed/.test(current.message))
|
|
1055
|
+
if (/cleanup failed|setup failed/.test(current.message)) {
|
|
945
1056
|
return true;
|
|
1057
|
+
}
|
|
946
1058
|
pending.push(...current.errors);
|
|
947
1059
|
}
|
|
948
|
-
if (current.cause !== undefined)
|
|
1060
|
+
if (current.cause !== undefined) {
|
|
949
1061
|
pending.push(current.cause);
|
|
1062
|
+
}
|
|
950
1063
|
}
|
|
951
1064
|
return false;
|
|
952
1065
|
}
|
|
@@ -976,15 +1089,18 @@ export class McpServerManager {
|
|
|
976
1089
|
}
|
|
977
1090
|
isIdle(name, timeoutMs) {
|
|
978
1091
|
const connection = this.connections.get(name);
|
|
979
|
-
if (!connection || connection.status !== "connected")
|
|
1092
|
+
if (!connection || connection.status !== "connected") {
|
|
980
1093
|
return false;
|
|
981
|
-
|
|
1094
|
+
}
|
|
1095
|
+
if (connection.inFlight > 0) {
|
|
982
1096
|
return false;
|
|
1097
|
+
}
|
|
983
1098
|
// Background subscriptions keep their existing idle policy, but accepted requests and
|
|
984
1099
|
// callbacks (including cancellation tails) must not be closed as idle while unfinished.
|
|
985
|
-
if ([...(clientOperations.get(connection.client)?.values() ?? [])].some(kind => kind !== "subscription"))
|
|
1100
|
+
if ([...(clientOperations.get(connection.client)?.values() ?? [])].some((kind) => kind !== "subscription")) {
|
|
986
1101
|
return false;
|
|
987
|
-
|
|
1102
|
+
}
|
|
1103
|
+
return Date.now() - connection.lastUsedAt > timeoutMs;
|
|
988
1104
|
}
|
|
989
1105
|
}
|
|
990
1106
|
/**
|
|
@@ -993,9 +1109,10 @@ export class McpServerManager {
|
|
|
993
1109
|
function resolveEnv(env, serverName) {
|
|
994
1110
|
const resolved = {};
|
|
995
1111
|
for (const [key, value] of Object.entries(process.env)) {
|
|
996
|
-
if (value !== undefined)
|
|
1112
|
+
if (value !== undefined) {
|
|
997
1113
|
resolved[key] = value;
|
|
1114
|
+
}
|
|
998
1115
|
}
|
|
999
|
-
const overrides = resolveCommandSecretsRecord(env, key => `MCP server "${serverName}" stdio env "${key}"`);
|
|
1116
|
+
const overrides = resolveCommandSecretsRecord(env, (key) => `MCP server "${serverName}" stdio env "${key}"`);
|
|
1000
1117
|
return overrides ? { ...resolved, ...overrides } : resolved;
|
|
1001
1118
|
}
|