@herbertgao/pi-extensions 2026.9.11 → 2026.9.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +21 -4
- package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
- package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
- package/node_modules/@narumitw/pi-btw/package.json +1 -1
- package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
- package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
- package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
- package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
- package/node_modules/pi-mcp-adapter/README.md +75 -7
- package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
- package/node_modules/pi-mcp-adapter/cli.js +56 -5
- package/node_modules/pi-mcp-adapter/commands.ts +96 -5
- package/node_modules/pi-mcp-adapter/config.ts +73 -6
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
- package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
- package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
- package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
- package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
- package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
- package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
- package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
- package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
- package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
- package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
- package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
- package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
- package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
- package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
- package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
- package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
- package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
- package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
- package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
- package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
- package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
- package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
- package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
- package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
- package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
- package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
- package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
- package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
- package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
- package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
- package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
- package/node_modules/pi-mcp-adapter/index.ts +129 -6
- package/node_modules/pi-mcp-adapter/init.ts +6 -19
- package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
- package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
- package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
- package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
- package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
- package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
- package/node_modules/pi-mcp-adapter/package.json +17 -9
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
- package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
- package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
- package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
- package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
- package/node_modules/pi-mcp-adapter/state.ts +3 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
- package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
- package/node_modules/pi-mcp-adapter/types.ts +53 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
- package/node_modules/pi-mcp-adapter/utils.ts +16 -0
- package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
- package/node_modules/pi-multi-account/README.md +38 -11
- package/node_modules/pi-multi-account/index.ts +371 -96
- package/node_modules/pi-multi-account/package.json +5 -4
- package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
- package/node_modules/pi-multi-account/usage.ts +31 -2
- package/node_modules/pi-typesafe/README.md +3 -1
- package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
- package/node_modules/pi-typesafe/dist/auth.js +13 -7
- package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
- package/node_modules/pi-typesafe/dist/backends.js +33 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
- package/node_modules/pi-typesafe/dist/client.js +63 -39
- package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
- package/node_modules/pi-typesafe/dist/credentials.js +15 -8
- package/node_modules/pi-typesafe/dist/extension.js +4 -1
- package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
- package/node_modules/pi-typesafe/dist/login.js +17 -8
- package/node_modules/pi-typesafe/dist/schema.js +19 -13
- package/node_modules/pi-typesafe/package.json +1 -1
- package/package.json +11 -10
|
@@ -52,12 +52,15 @@ import {
|
|
|
52
52
|
type OAuthAuthority,
|
|
53
53
|
} from "./mcp-auth.ts";
|
|
54
54
|
import { getBearerTokenForUrl } from "./mcp-bearer-store.ts";
|
|
55
|
-
import { registerSamplingHandler, type ServerSamplingConfig } from "./sampling-handler.ts";
|
|
55
|
+
import { handleSamplingRequest, registerSamplingHandler, type ServerSamplingConfig } from "./sampling-handler.ts";
|
|
56
56
|
import {
|
|
57
|
+
handleElicitationRequest,
|
|
57
58
|
handleUrlElicitation,
|
|
58
59
|
registerElicitationHandler,
|
|
59
60
|
type ServerElicitationConfig,
|
|
60
61
|
} from "./elicitation-handler.ts";
|
|
62
|
+
import { attachTaskSession, type RawRequestChannel } from "./mcp-tasks.ts";
|
|
63
|
+
import type { TaskEnabledSession } from "@modelcontextprotocol/ext-tasks/client";
|
|
61
64
|
import {
|
|
62
65
|
interpolateEnvVars,
|
|
63
66
|
resolveBearerToken,
|
|
@@ -79,6 +82,7 @@ import {
|
|
|
79
82
|
import { createOAuthFetch, resolveOAuthHeaders } from "./mcp-auth-fetch.ts";
|
|
80
83
|
import { createRequestHeadersCommandFetch } from "./request-headers-command.ts";
|
|
81
84
|
import { createCaFetch, validateCaFile } from "./http-ca.ts";
|
|
85
|
+
import { BearerCommandResolver } from "./bearer-command-resolver.ts";
|
|
82
86
|
|
|
83
87
|
const MAX_CAPTURED_STDERR_BYTES = 8 * 1024;
|
|
84
88
|
const MAX_CAPTURED_STDERR_LINES = 3;
|
|
@@ -120,8 +124,12 @@ function localNetworkFailureCodes(error: unknown, seen = new Set<object>()): str
|
|
|
120
124
|
return [...new Set(codes)];
|
|
121
125
|
}
|
|
122
126
|
|
|
123
|
-
function isUnauthorizedHttpError(error: unknown): boolean {
|
|
124
|
-
return error instanceof UnauthorizedError
|
|
127
|
+
export function isUnauthorizedHttpError(error: unknown): boolean {
|
|
128
|
+
return error instanceof UnauthorizedError
|
|
129
|
+
|| (error instanceof SdkHttpError && error.status === 401)
|
|
130
|
+
// Some pinned-SDK transport paths emit this plain Error when bearer
|
|
131
|
+
// request headers are used without an OAuth authProvider.
|
|
132
|
+
|| (error instanceof Error && /^Error POSTing to endpoint \(HTTP 401\):/.test(error.message));
|
|
125
133
|
}
|
|
126
134
|
|
|
127
135
|
function shouldFallbackToSse(error: unknown, definition: ServerDefinition): boolean {
|
|
@@ -191,6 +199,10 @@ export interface ServerConnection {
|
|
|
191
199
|
status: "connected" | "closed" | "needs-auth";
|
|
192
200
|
/** Catalog subscription health, tracked independently from transport health. */
|
|
193
201
|
listenState: McpListenState;
|
|
202
|
+
/** ext-tasks requester session, attached when `tasks: true` and the server advertises the extension. */
|
|
203
|
+
taskSession?: TaskEnabledSession;
|
|
204
|
+
/** Raw JSON-RPC channel backing the task session on 2026-07-28 connections. */
|
|
205
|
+
taskChannel?: RawRequestChannel;
|
|
194
206
|
listenSubscription?: McpSubscription;
|
|
195
207
|
/** Last requested filter; the server's honored filter may be a subset. */
|
|
196
208
|
listenFilter?: SubscriptionFilter;
|
|
@@ -231,11 +243,31 @@ export function isTransientHttpConnectError(error: unknown): boolean {
|
|
|
231
243
|
return false;
|
|
232
244
|
}
|
|
233
245
|
|
|
246
|
+
/** Wrap a FetchLike so each request re-resolves the bearer token via the resolver. */
|
|
247
|
+
function createBearerCommandFetch(
|
|
248
|
+
resolver: BearerCommandResolver,
|
|
249
|
+
delegate: FetchLike | undefined,
|
|
250
|
+
): FetchLike {
|
|
251
|
+
const innerFetch = delegate
|
|
252
|
+
? (input: URL | RequestInfo, init?: RequestInit) => delegate(input as URL, init)
|
|
253
|
+
: (input: URL | RequestInfo, init?: RequestInit) => globalThis.fetch(input, init);
|
|
254
|
+
return async (input, init) => {
|
|
255
|
+
const request = new Request(input, init);
|
|
256
|
+
const token = await resolver.resolve(request.signal);
|
|
257
|
+
const headers = new Headers(request.headers);
|
|
258
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
259
|
+
// Composed runtime fetches accept Request despite the SDK's narrower type.
|
|
260
|
+
return innerFetch(new Request(request, { headers }));
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
234
264
|
export class McpServerManager {
|
|
235
265
|
private connections = new Map<string, ServerConnection>();
|
|
236
266
|
private connectPromises = new Map<string, Promise<ServerConnection>>();
|
|
237
267
|
private connectOAuthAuthorities = new Map<string, OAuthAuthority>();
|
|
238
268
|
private reconnectPromises = new Map<string, Promise<ServerConnection>>();
|
|
269
|
+
private reconnectOAuthAuthorities = new Map<string, OAuthAuthority>();
|
|
270
|
+
private reconnectAttempts = new Map<string, AbortController>();
|
|
239
271
|
private uiStreamListeners = new Map<string, UiStreamListener>();
|
|
240
272
|
private samplingConfig: ServerSamplingConfig | undefined;
|
|
241
273
|
private metadataListChangedListener: MetadataListChangedListener | undefined;
|
|
@@ -349,6 +381,36 @@ export class McpServerManager {
|
|
|
349
381
|
};
|
|
350
382
|
}
|
|
351
383
|
|
|
384
|
+
private arbitrateConnectionAttempt(
|
|
385
|
+
name: string,
|
|
386
|
+
oauthAuthority: OAuthAuthority | undefined,
|
|
387
|
+
promises: Map<string, Promise<ServerConnection>>,
|
|
388
|
+
authorities: Map<string, OAuthAuthority>,
|
|
389
|
+
attempts: Map<string, AbortController>,
|
|
390
|
+
): Promise<ServerConnection> | undefined {
|
|
391
|
+
const existingPromise = promises.get(name);
|
|
392
|
+
if (!existingPromise) return undefined;
|
|
393
|
+
|
|
394
|
+
const existingAuthority = authorities.get(name);
|
|
395
|
+
let reusable = oauthAuthority === undefined && existingAuthority === undefined;
|
|
396
|
+
if (oauthAuthority && existingAuthority) {
|
|
397
|
+
try {
|
|
398
|
+
existingAuthority();
|
|
399
|
+
reusable = true;
|
|
400
|
+
} catch {
|
|
401
|
+
reusable = false;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (reusable) return existingPromise;
|
|
405
|
+
|
|
406
|
+
const replacedAttempt = attempts.get(name);
|
|
407
|
+
replacedAttempt?.abort(new Error(`MCP connection ${name} was replaced`));
|
|
408
|
+
if (attempts.get(name) === replacedAttempt) attempts.delete(name);
|
|
409
|
+
if (promises.get(name) === existingPromise) promises.delete(name);
|
|
410
|
+
if (authorities.get(name) === existingAuthority) authorities.delete(name);
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
|
|
352
414
|
async connect(name: string, definition: ServerDefinition, signal?: AbortSignal): Promise<ServerConnection> {
|
|
353
415
|
validateCaFile(definition);
|
|
354
416
|
if (isServerDisabled(definition)) throw new Error(`MCP server "${name}" is disabled`);
|
|
@@ -363,27 +425,14 @@ export class McpServerManager {
|
|
|
363
425
|
throwIfAborted(ownedSignal);
|
|
364
426
|
|
|
365
427
|
// Dedupe concurrent connection attempts.
|
|
366
|
-
const existingConnect = this.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
} catch {
|
|
375
|
-
reusable = false;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (reusable) return abortable(existingConnect, ownedSignal);
|
|
379
|
-
const replacedAttempt = this.connectAttempts.get(name);
|
|
380
|
-
replacedAttempt?.abort(new Error(`MCP connection ${name} was replaced`));
|
|
381
|
-
if (this.connectAttempts.get(name) === replacedAttempt) this.connectAttempts.delete(name);
|
|
382
|
-
if (this.connectPromises.get(name) === existingConnect) this.connectPromises.delete(name);
|
|
383
|
-
if (this.connectOAuthAuthorities.get(name) === existingAuthority) {
|
|
384
|
-
this.connectOAuthAuthorities.delete(name);
|
|
385
|
-
}
|
|
386
|
-
}
|
|
428
|
+
const existingConnect = this.arbitrateConnectionAttempt(
|
|
429
|
+
name,
|
|
430
|
+
oauthAuthority,
|
|
431
|
+
this.connectPromises,
|
|
432
|
+
this.connectOAuthAuthorities,
|
|
433
|
+
this.connectAttempts,
|
|
434
|
+
);
|
|
435
|
+
if (existingConnect) return abortable(existingConnect, ownedSignal);
|
|
387
436
|
|
|
388
437
|
const existing = this.connections.get(name);
|
|
389
438
|
if (existing?.status === "connected") {
|
|
@@ -391,6 +440,18 @@ export class McpServerManager {
|
|
|
391
440
|
return existing;
|
|
392
441
|
}
|
|
393
442
|
|
|
443
|
+
// A needs-auth route can be retried through either public path. Share an
|
|
444
|
+
// authority-compatible reconnect candidate instead of creating a second
|
|
445
|
+
// owner for the same server.
|
|
446
|
+
const existingReconnect = this.arbitrateConnectionAttempt(
|
|
447
|
+
name,
|
|
448
|
+
oauthAuthority,
|
|
449
|
+
this.reconnectPromises,
|
|
450
|
+
this.reconnectOAuthAuthorities,
|
|
451
|
+
this.reconnectAttempts,
|
|
452
|
+
);
|
|
453
|
+
if (existingReconnect) return abortable(existingReconnect, ownedSignal);
|
|
454
|
+
|
|
394
455
|
const credentialsInvalidated = existing?.status === "needs-auth"
|
|
395
456
|
&& existing.credentialsInvalidated === true;
|
|
396
457
|
const generation = this.closeGenerations.get(name) ?? 0;
|
|
@@ -449,21 +510,63 @@ export class McpServerManager {
|
|
|
449
510
|
staleConnection: ServerConnection,
|
|
450
511
|
signal?: AbortSignal,
|
|
451
512
|
): Promise<ServerConnection> {
|
|
513
|
+
validateCaFile(definition);
|
|
452
514
|
if (isServerDisabled(definition)) throw new Error(`MCP server "${name}" is disabled`);
|
|
453
515
|
if (this.stopped) throw new Error("MCP server manager is closed");
|
|
454
516
|
const ownedSignal = combineAbortSignals(this.runtimeSignal, signal);
|
|
455
517
|
throwIfAborted(ownedSignal);
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
518
|
+
const oauthAuthority = definition.url && supportsOAuth(definition)
|
|
519
|
+
? captureOAuthAuthority(name, false)
|
|
520
|
+
: undefined;
|
|
521
|
+
// connect() and reconnect() are interchangeable retry entry points for a
|
|
522
|
+
// published needs-auth route. They must not create competing OAuth owners.
|
|
523
|
+
const existingConnect = this.arbitrateConnectionAttempt(
|
|
524
|
+
name,
|
|
525
|
+
oauthAuthority,
|
|
526
|
+
this.connectPromises,
|
|
527
|
+
this.connectOAuthAuthorities,
|
|
528
|
+
this.connectAttempts,
|
|
529
|
+
);
|
|
530
|
+
if (existingConnect) return abortable(existingConnect, ownedSignal);
|
|
531
|
+
const inFlight = this.arbitrateConnectionAttempt(
|
|
532
|
+
name,
|
|
533
|
+
oauthAuthority,
|
|
534
|
+
this.reconnectPromises,
|
|
535
|
+
this.reconnectOAuthAuthorities,
|
|
536
|
+
this.reconnectAttempts,
|
|
537
|
+
);
|
|
538
|
+
if (inFlight) return abortable(inFlight, ownedSignal);
|
|
460
539
|
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
540
|
+
const attemptController = new AbortController();
|
|
541
|
+
const attemptSignal = combineAbortSignals(ownedSignal, attemptController.signal);
|
|
542
|
+
const promise = this.doReconnect(
|
|
543
|
+
name,
|
|
544
|
+
definition,
|
|
545
|
+
staleConnection,
|
|
546
|
+
attemptSignal,
|
|
547
|
+
oauthAuthority,
|
|
548
|
+
attemptController,
|
|
549
|
+
)
|
|
550
|
+
.catch(error => {
|
|
551
|
+
if (attemptController.signal.aborted && !this.containsCleanupFailure(error)) {
|
|
552
|
+
throw new Error(`MCP connection for ${name} was closed while reconnecting`, { cause: error });
|
|
553
|
+
}
|
|
554
|
+
throw error;
|
|
555
|
+
})
|
|
556
|
+
.finally(() => {
|
|
557
|
+
if (this.reconnectPromises.get(name) === promise) {
|
|
558
|
+
this.reconnectPromises.delete(name);
|
|
559
|
+
}
|
|
560
|
+
if (this.reconnectAttempts.get(name) === attemptController) {
|
|
561
|
+
this.reconnectAttempts.delete(name);
|
|
562
|
+
}
|
|
563
|
+
if (this.reconnectOAuthAuthorities.get(name) === oauthAuthority) {
|
|
564
|
+
this.reconnectOAuthAuthorities.delete(name);
|
|
565
|
+
}
|
|
566
|
+
});
|
|
466
567
|
this.reconnectPromises.set(name, promise);
|
|
568
|
+
if (oauthAuthority) this.reconnectOAuthAuthorities.set(name, oauthAuthority);
|
|
569
|
+
this.reconnectAttempts.set(name, attemptController);
|
|
467
570
|
return abortable(promise, ownedSignal);
|
|
468
571
|
}
|
|
469
572
|
|
|
@@ -828,6 +931,8 @@ export class McpServerManager {
|
|
|
828
931
|
definition: ServerDefinition,
|
|
829
932
|
staleConnection: ServerConnection,
|
|
830
933
|
signal?: AbortSignal,
|
|
934
|
+
oauthAuthority?: OAuthAuthority,
|
|
935
|
+
attemptOwner?: AbortController,
|
|
831
936
|
): Promise<ServerConnection> {
|
|
832
937
|
throwIfAborted(signal);
|
|
833
938
|
const current = this.connections.get(name);
|
|
@@ -839,10 +944,68 @@ export class McpServerManager {
|
|
|
839
944
|
return current ?? this.connect(name, definition, signal);
|
|
840
945
|
}
|
|
841
946
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
947
|
+
// Build the replacement privately. The current route remains usable until
|
|
948
|
+
// the candidate has completed its handshake and metadata discovery. A
|
|
949
|
+
// failed candidate therefore cannot remove or close the last good route.
|
|
950
|
+
const generation = this.closeGenerations.get(name) ?? 0;
|
|
951
|
+
const credentialsInvalidated = staleConnection.status === "needs-auth"
|
|
952
|
+
&& staleConnection.credentialsInvalidated === true;
|
|
953
|
+
const candidateAttempt = this.createConnection(
|
|
954
|
+
name,
|
|
955
|
+
definition,
|
|
956
|
+
signal,
|
|
957
|
+
signal,
|
|
958
|
+
credentialsInvalidated,
|
|
959
|
+
oauthAuthority,
|
|
960
|
+
attemptOwner,
|
|
961
|
+
);
|
|
962
|
+
const fresh = definition.url
|
|
963
|
+
? await candidateAttempt.catch(async error => { throw await this.enrichHttpConnectionError(definition, error); })
|
|
964
|
+
: await candidateAttempt;
|
|
965
|
+
|
|
966
|
+
// An explicit close or a concurrent replacement supersedes this candidate.
|
|
967
|
+
// Dispose only the candidate we own; never close whichever route is now
|
|
968
|
+
// registered under the shared server name.
|
|
969
|
+
if (signal?.aborted
|
|
970
|
+
|| this.reconnectAttempts.get(name) !== attemptOwner
|
|
971
|
+
|| (this.closeGenerations.get(name) ?? 0) !== generation) {
|
|
972
|
+
await this.disposeConnection(fresh);
|
|
973
|
+
throwIfAborted(signal);
|
|
974
|
+
throw new Error(`MCP connection for ${name} was closed while reconnecting`);
|
|
975
|
+
}
|
|
976
|
+
const routeAtPublish = this.connections.get(name);
|
|
977
|
+
if (routeAtPublish !== staleConnection) {
|
|
978
|
+
await this.disposeConnection(fresh);
|
|
979
|
+
return routeAtPublish ?? this.connect(name, definition, signal);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
fresh.inFlight = Math.max(fresh.inFlight, staleConnection.inFlight);
|
|
983
|
+
this.connections.set(name, fresh);
|
|
984
|
+
this.watchListenSubscription(name, fresh, fresh.listenSubscription);
|
|
985
|
+
if ([...this.resourceUpdatedListeners.values()].some(registration => registration.serverName === name)) {
|
|
986
|
+
void this.ensureListen(name, fresh);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Publish first so calls route to the fresh image while the exact displaced
|
|
990
|
+
// child is terminated and reaped. Late close callbacks are identity-guarded.
|
|
991
|
+
staleConnection.status = "closed";
|
|
992
|
+
try {
|
|
993
|
+
await this.disposeConnection(staleConnection);
|
|
994
|
+
} catch (error) {
|
|
995
|
+
const failures = error instanceof AggregateError ? error.errors : [error];
|
|
996
|
+
const details = failures
|
|
997
|
+
.map(failure => failure instanceof Error ? failure.message : String(failure))
|
|
998
|
+
.join("; ");
|
|
999
|
+
logger.debug(`MCP: stale connection cleanup failed for ${name}: ${details}`);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Publication is not permission to return a route that close() disposed
|
|
1003
|
+
// while stale cleanup was pending.
|
|
1004
|
+
if ((this.closeGenerations.get(name) ?? 0) !== generation
|
|
1005
|
+
|| this.connections.get(name) !== fresh
|
|
1006
|
+
|| fresh.status === "closed") {
|
|
1007
|
+
throw new Error(`MCP connection for ${name} was closed while reconnecting`);
|
|
1008
|
+
}
|
|
846
1009
|
return fresh;
|
|
847
1010
|
}
|
|
848
1011
|
|
|
@@ -1024,6 +1187,50 @@ export class McpServerManager {
|
|
|
1024
1187
|
connection.prompts = promptResult.prompts;
|
|
1025
1188
|
connection.promptDiscoveryFailed = promptResult.failed;
|
|
1026
1189
|
|
|
1190
|
+
if (definition.tasks !== false) {
|
|
1191
|
+
try {
|
|
1192
|
+
const attachment = await attachTaskSession({
|
|
1193
|
+
client,
|
|
1194
|
+
serverName: name,
|
|
1195
|
+
definition,
|
|
1196
|
+
transport,
|
|
1197
|
+
requestTimeoutMs: this.getResolvedRequestTimeoutMs(definition),
|
|
1198
|
+
clientInfo: { name: `pi-mcp-${name}`, version: "1.0.0" },
|
|
1199
|
+
clientCapabilities: this.buildClientCapabilities(),
|
|
1200
|
+
...(this.elicitationConfig
|
|
1201
|
+
? {
|
|
1202
|
+
onElicitation: (request: Parameters<typeof handleElicitationRequest>[1], signal?: AbortSignal) =>
|
|
1203
|
+
handleElicitationRequest({
|
|
1204
|
+
...this.elicitationConfig!,
|
|
1205
|
+
serverName: name,
|
|
1206
|
+
onUrlAccepted: elicitationId => this.rememberUrlElicitation(name, elicitationId),
|
|
1207
|
+
}, request, signal),
|
|
1208
|
+
}
|
|
1209
|
+
: {}),
|
|
1210
|
+
...(this.samplingConfig
|
|
1211
|
+
? {
|
|
1212
|
+
onSampling: (request: Parameters<typeof handleSamplingRequest>[1], signal?: AbortSignal) =>
|
|
1213
|
+
handleSamplingRequest({
|
|
1214
|
+
...this.samplingConfig!,
|
|
1215
|
+
serverName: name,
|
|
1216
|
+
// Cancelling the task should also stop the local sampling run.
|
|
1217
|
+
getSignal: () => combineAbortSignals(this.samplingConfig!.getSignal(), signal),
|
|
1218
|
+
}, request),
|
|
1219
|
+
}
|
|
1220
|
+
: {}),
|
|
1221
|
+
});
|
|
1222
|
+
if (attachment && connection.status === "connected") {
|
|
1223
|
+
connection.taskSession = attachment.session;
|
|
1224
|
+
connection.taskChannel = attachment.channel;
|
|
1225
|
+
} else if (attachment) {
|
|
1226
|
+
attachment.channel.rejectAll("MCP connection closed");
|
|
1227
|
+
await attachment.session.close().catch(() => {});
|
|
1228
|
+
}
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
logger.debug(`Task session attach failed for ${name}: ${String(error)}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1027
1234
|
return connection;
|
|
1028
1235
|
} catch (error) {
|
|
1029
1236
|
// If connectClientWithAbort closed the transport, await that exact close.
|
|
@@ -1317,14 +1524,24 @@ export class McpServerManager {
|
|
|
1317
1524
|
const commandBearer = definition.bearerToken?.startsWith("!") && !definition.bearerToken.startsWith("!!")
|
|
1318
1525
|
? definition.bearerToken
|
|
1319
1526
|
: undefined;
|
|
1527
|
+
// Command-backed bearers resolve eagerly, then refresh through a TTL cache.
|
|
1528
|
+
let bearerCommandResolver: BearerCommandResolver | undefined;
|
|
1320
1529
|
if (definition.auth === "bearer") {
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1530
|
+
if (commandBearer) {
|
|
1531
|
+
bearerCommandResolver = new BearerCommandResolver(
|
|
1532
|
+
commandBearer,
|
|
1533
|
+
`MCP server "${serverName}" HTTP bearer token`,
|
|
1534
|
+
);
|
|
1535
|
+
// Eager resolve so a broken command surfaces at connect time, not at
|
|
1536
|
+
// the first tool call.
|
|
1537
|
+
await bearerCommandResolver.resolve(signal);
|
|
1538
|
+
} else {
|
|
1539
|
+
const token = resolveBearerToken(definition)
|
|
1324
1540
|
?? (definition.bearerToken === undefined && definition.bearerTokenEnv === undefined && definition.bearerTokenStore === true
|
|
1325
1541
|
? getBearerTokenForUrl(serverName, serverUrl)
|
|
1326
1542
|
: undefined);
|
|
1327
|
-
|
|
1543
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
1544
|
+
}
|
|
1328
1545
|
}
|
|
1329
1546
|
|
|
1330
1547
|
if (hasCommandHeader || commandBearer) {
|
|
@@ -1342,8 +1559,10 @@ export class McpServerManager {
|
|
|
1342
1559
|
let caFetch = createCaFetch(definition);
|
|
1343
1560
|
try {
|
|
1344
1561
|
const createAuthProvider = (): McpOAuthProvider => {
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1562
|
+
const currentConnectAttempt = this.connectAttempts.get(serverName);
|
|
1563
|
+
const currentReconnectAttempt = this.reconnectAttempts.get(serverName);
|
|
1564
|
+
if ((currentConnectAttempt && currentConnectAttempt !== attemptOwner)
|
|
1565
|
+
|| (currentReconnectAttempt && currentReconnectAttempt !== attemptOwner)) {
|
|
1347
1566
|
throw new Error(`MCP connection for ${serverName} was replaced while connecting`);
|
|
1348
1567
|
}
|
|
1349
1568
|
if (!oauthAuthority) throw new Error(`Missing OAuth authority for ${serverName}`);
|
|
@@ -1390,13 +1609,17 @@ export class McpServerManager {
|
|
|
1390
1609
|
const commandFetch = definition.requestHeadersCommand
|
|
1391
1610
|
? createRequestHeadersCommandFetch(definition.requestHeadersCommand, caFetch?.fetch)
|
|
1392
1611
|
: caFetch?.fetch;
|
|
1612
|
+
// requestHeadersCommand stays last in the header chain.
|
|
1613
|
+
const bearerFetch = bearerCommandResolver
|
|
1614
|
+
? createBearerCommandFetch(bearerCommandResolver, commandFetch)
|
|
1615
|
+
: commandFetch;
|
|
1393
1616
|
const requestFetch = oauthEnabled
|
|
1394
1617
|
? createOAuthFetch(serverUrl, () => serviceHeaders, this.oauthRuntime?.signal, {
|
|
1395
1618
|
// MCP streams outlive individual auth requests; retain SDK request deadlines.
|
|
1396
1619
|
timeout: false,
|
|
1397
|
-
...(
|
|
1620
|
+
...(bearerFetch ? { delegate: bearerFetch } : {}),
|
|
1398
1621
|
})
|
|
1399
|
-
:
|
|
1622
|
+
: bearerFetch;
|
|
1400
1623
|
const attempt = async (
|
|
1401
1624
|
kind: "streamable-http" | "sse",
|
|
1402
1625
|
): Promise<
|
|
@@ -1682,8 +1905,10 @@ export class McpServerManager {
|
|
|
1682
1905
|
async close(name: string): Promise<void> {
|
|
1683
1906
|
this.closeGenerations.set(name, (this.closeGenerations.get(name) ?? 0) + 1);
|
|
1684
1907
|
this.connectAttempts.get(name)?.abort(new Error(`MCP connection ${name} was closed`));
|
|
1908
|
+
this.reconnectAttempts.get(name)?.abort(new Error(`MCP connection ${name} was closed`));
|
|
1685
1909
|
this.pendingMetadataPublications.delete(name);
|
|
1686
1910
|
|
|
1911
|
+
const pendingReconnect = this.reconnectPromises.get(name);
|
|
1687
1912
|
const connection = this.connections.get(name);
|
|
1688
1913
|
if (!connection) {
|
|
1689
1914
|
const pendingClose = this.closePromises.get(name);
|
|
@@ -1692,9 +1917,10 @@ export class McpServerManager {
|
|
|
1692
1917
|
return;
|
|
1693
1918
|
}
|
|
1694
1919
|
const pendingConnect = this.connectPromises.get(name);
|
|
1695
|
-
|
|
1920
|
+
for (const pending of [pendingConnect, pendingReconnect]) {
|
|
1921
|
+
if (!pending) continue;
|
|
1696
1922
|
try {
|
|
1697
|
-
await
|
|
1923
|
+
await pending;
|
|
1698
1924
|
} catch (error) {
|
|
1699
1925
|
if (this.containsCleanupFailure(error)) throw error;
|
|
1700
1926
|
}
|
|
@@ -1711,11 +1937,23 @@ export class McpServerManager {
|
|
|
1711
1937
|
if (this.closePromises.get(name) === closing) this.closePromises.delete(name);
|
|
1712
1938
|
});
|
|
1713
1939
|
this.closePromises.set(name, closing);
|
|
1714
|
-
|
|
1940
|
+
await closing;
|
|
1941
|
+
if (pendingReconnect) {
|
|
1942
|
+
try {
|
|
1943
|
+
await pendingReconnect;
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (this.containsCleanupFailure(error)) throw error;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1715
1948
|
}
|
|
1716
1949
|
|
|
1717
1950
|
private async disposeConnection(connection: ServerConnection): Promise<void> {
|
|
1718
1951
|
const results = await Promise.allSettled([
|
|
1952
|
+
// Release ext-tasks state first so its SDK adapter detaches before close.
|
|
1953
|
+
Promise.resolve().then(async () => {
|
|
1954
|
+
connection.taskChannel?.rejectAll("MCP connection closed");
|
|
1955
|
+
await connection.taskSession?.close();
|
|
1956
|
+
}).catch(() => {}),
|
|
1719
1957
|
// Only client.close() is needed; the client owns the transport and will close it internally.
|
|
1720
1958
|
Promise.resolve().then(() => connection.client.close()),
|
|
1721
1959
|
this.traceWriter?.flush() ?? Promise.resolve(),
|
|
@@ -1726,13 +1964,18 @@ export class McpServerManager {
|
|
|
1726
1964
|
|
|
1727
1965
|
async closeAll(): Promise<void> {
|
|
1728
1966
|
this.stopped = true;
|
|
1729
|
-
const names = new Set([
|
|
1967
|
+
const names = new Set([
|
|
1968
|
+
...this.connections.keys(),
|
|
1969
|
+
...this.connectPromises.keys(),
|
|
1970
|
+
...this.reconnectPromises.keys(),
|
|
1971
|
+
]);
|
|
1730
1972
|
for (const name of names) {
|
|
1731
1973
|
this.closeGenerations.set(name, (this.closeGenerations.get(name) ?? 0) + 1);
|
|
1732
1974
|
this.connectAttempts.get(name)?.abort(new Error(`MCP connection ${name} was closed`));
|
|
1975
|
+
this.reconnectAttempts.get(name)?.abort(new Error(`MCP connection ${name} was closed`));
|
|
1733
1976
|
}
|
|
1734
1977
|
|
|
1735
|
-
const pendingConnects = [...this.connectPromises.values()];
|
|
1978
|
+
const pendingConnects = [...this.connectPromises.values(), ...this.reconnectPromises.values()];
|
|
1736
1979
|
const currentNames = [...this.connections.keys()];
|
|
1737
1980
|
const pendingResults = await Promise.allSettled(pendingConnects);
|
|
1738
1981
|
const results = await Promise.allSettled(currentNames.map(name => this.close(name)));
|
|
@@ -160,12 +160,26 @@ export function rememberToolApproval(
|
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** Bind broad consent to this configured server, not a replacement with the same name. */
|
|
164
|
+
export function getServerApprovalIdentity(state: McpExtensionState, serverName: string) {
|
|
165
|
+
if (!Object.hasOwn(state.config.mcpServers, serverName)) return undefined;
|
|
166
|
+
const definition = state.config.mcpServers[serverName];
|
|
167
|
+
if (!definition) return undefined;
|
|
168
|
+
return {
|
|
169
|
+
definition,
|
|
170
|
+
hash: createHash("sha256").update(stableStringify(definition)).digest("hex"),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
163
174
|
export function restoreSessionApprovalState(
|
|
164
175
|
state: McpExtensionState,
|
|
165
176
|
branchEntries: readonly unknown[],
|
|
166
177
|
): void {
|
|
167
178
|
const approvedToolCalls = state.approvedToolCalls ??= new Map<string, true>();
|
|
168
179
|
approvedToolCalls.clear();
|
|
180
|
+
// Replace the map so an approval dialog opened on the old branch cannot grant
|
|
181
|
+
// server-wide authority after navigation, resume, or session replacement.
|
|
182
|
+
state.approvedServers = new Map();
|
|
169
183
|
state.consentManager.clear();
|
|
170
184
|
|
|
171
185
|
for (const entry of branchEntries) {
|
|
@@ -36,6 +36,8 @@ When advertised by the server, `outputSchema` is the original JSON Schema and `o
|
|
|
36
36
|
|
|
37
37
|
Calls resolve to `{ ok: true, data }` or `{ ok: false, error }`; handle failed calls instead of expecting them to stop the script. `emit(value)` adds user-visible output before the final `return` value. `console` output is captured too.
|
|
38
38
|
|
|
39
|
+
With `settings.jev.scriptEvaluation` enabled, call `jev.evaluate({ state, questions, sources })`; declare every MCP server represented in state. The host conservatively taints the whole script with every server-attributed call result or error, so every declared or observed source must remain enabled and allowed for later direct evaluations and semantic searches. Semantic search requires `searchMode: "semantic"`. Direct and semantic attempts share count, UTF-8 request-byte, deadline, and provider-reported token budgets. Scores do not bypass `tools.call` approval. Action loops must use fresh observations, positive step/time/evaluation budgets, validated operations and targets, and stop on uncertainty, staleness, no progress, no match, or missing information. Do not retry a possibly side-effectful action.
|
|
40
|
+
|
|
39
41
|
On success, `data` may still be the raw MCP `CallToolResult` envelope rather than the domain payload. Check `data.structuredContent` for the fields your script expects; if they are absent, inspect text blocks in `data.content` too (some servers emit newline-delimited JSON). If neither shape is understood, return or emit the envelope for inspection instead of coercing it to `[]` or `{}`.
|
|
40
42
|
|
|
41
43
|
`tools` is a non-enumerable proxy: `Object.keys(tools)` throws. Always use `tools.search` for discovery. When a known flat path is a valid identifier, direct calls such as `tools.github_search_issues(args)` are supported; use bracket syntax for hyphenated names: `tools["server_tool-name"](args)`. `search`, `call`, `describe`, and promise/serialization names (`then`, `catch`, `finally`, `toJSON`, `toString`, `valueOf`) are reserved on the proxy; if a flat path collides with one, call it via `tools.call("exact-path", args)`.
|
|
@@ -3,7 +3,7 @@ import type { ConsentManager } from "./consent-manager.ts";
|
|
|
3
3
|
import type { McpLifecycleManager } from "./lifecycle.ts";
|
|
4
4
|
import type { McpServerManager } from "./server-manager.ts";
|
|
5
5
|
import type { AuthStorageOptions } from "./mcp-auth.ts";
|
|
6
|
-
import type { ToolMetadata, PromptMetadata, McpConfig, UiSessionMessages, UiStreamSummary, McpStatusEventBus, UiServerHandle } from "./types.ts";
|
|
6
|
+
import type { ServerDefinition, ToolMetadata, PromptMetadata, McpConfig, UiSessionMessages, UiStreamSummary, McpStatusEventBus, UiServerHandle } from "./types.ts";
|
|
7
7
|
import type { UiResourceHandler } from "./ui-resource-handler.ts";
|
|
8
8
|
import type { McpRuntimeOwner } from "./runtime-owner.ts";
|
|
9
9
|
import type { McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
@@ -51,6 +51,8 @@ export interface McpExtensionState {
|
|
|
51
51
|
failureMessages: Map<string, string>;
|
|
52
52
|
/** Session-only approvals keyed by server, tool definition, and arguments. */
|
|
53
53
|
approvedToolCalls: Map<string, true>;
|
|
54
|
+
/** Runtime-only server grants. Never persisted or restored from session entries. */
|
|
55
|
+
approvedServers?: Map<string, { definition: ServerDefinition; hash: string }>;
|
|
54
56
|
/** Optional active-session sink for approval decisions. */
|
|
55
57
|
persistSessionApproval?: SessionApprovalWriter;
|
|
56
58
|
/** Session manager used to reject stale session-tree contexts. */
|
|
@@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { abortable } from "./abort.ts";
|
|
3
3
|
import { combineAbortSignals } from "./runtime-owner.ts";
|
|
4
4
|
import type { McpExtensionState } from "./state.ts";
|
|
5
|
-
import { getToolApprovalIdentity, rememberToolApproval } from "./session-approvals.ts";
|
|
5
|
+
import { getServerApprovalIdentity, getToolApprovalIdentity, rememberToolApproval } from "./session-approvals.ts";
|
|
6
6
|
import {
|
|
7
7
|
getToolNameCandidates,
|
|
8
8
|
matchesToolPattern,
|
|
@@ -137,6 +137,9 @@ export async function ensureToolCallApproved(
|
|
|
137
137
|
origin: McpToolApprovalOrigin = toolMeta.resourceUri ? "resource" : "proxy",
|
|
138
138
|
approvalMetadata?: ReadonlyMap<string, readonly ToolMetadata[]>,
|
|
139
139
|
): Promise<ToolCallApprovalResult> {
|
|
140
|
+
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
141
|
+
const approvedServers = state.approvedServers ??= new Map();
|
|
142
|
+
const serverIdentity = getServerApprovalIdentity(state, serverName);
|
|
140
143
|
const brokerDecision = await requestBrokerApproval(state, serverName, toolMeta, args, origin, signal);
|
|
141
144
|
if (brokerDecision === "allow_once") return { ok: true };
|
|
142
145
|
if (brokerDecision === "allow_for_session") {
|
|
@@ -145,6 +148,15 @@ export async function ensureToolCallApproved(
|
|
|
145
148
|
}
|
|
146
149
|
if (brokerDecision === "deny") return { ok: false, reason: "denied" };
|
|
147
150
|
|
|
151
|
+
const serverGrant = approvedServers.get(serverName);
|
|
152
|
+
const liveServerIdentity = getServerApprovalIdentity(state, serverName);
|
|
153
|
+
if (serverGrant && state.approvedServers === approvedServers
|
|
154
|
+
&& serverGrant.definition === liveServerIdentity?.definition && serverGrant.hash === liveServerIdentity?.hash) {
|
|
155
|
+
ownedSignal?.throwIfAborted();
|
|
156
|
+
return { ok: true };
|
|
157
|
+
}
|
|
158
|
+
approvedServers.delete(serverName);
|
|
159
|
+
|
|
148
160
|
const { cacheKey } = getToolApprovalIdentity(serverName, toolMeta, args);
|
|
149
161
|
const approvedToolCalls = state.approvedToolCalls ??= new Map<string, true>();
|
|
150
162
|
if (approvedToolCalls.has(cacheKey)) {
|
|
@@ -163,15 +175,25 @@ export async function ensureToolCallApproved(
|
|
|
163
175
|
const sanitized = sanitizeTerminalText(json);
|
|
164
176
|
const preview = sanitized.length > 500 ? `${sanitized.slice(0, 500)}...` : sanitized;
|
|
165
177
|
const title = `MCP: ${sanitizeTerminalText(serverName)} wants to run ${sanitizeTerminalText(toolMeta.originalName)}`;
|
|
166
|
-
const
|
|
178
|
+
const serverScope = "Allow server for this session permits all tools and arguments on this server until reload or session/branch change. Other security and UI consent checks still apply.";
|
|
167
179
|
const decision = await abortable(
|
|
168
180
|
state.ui.select(
|
|
169
|
-
`${title}\n\nArguments:\n${preview}`,
|
|
170
|
-
["Allow once", "Allow for session", "Deny"],
|
|
181
|
+
`${title}\n\nArguments:\n${preview}\n\n${serverScope}`,
|
|
182
|
+
["Allow once", "Allow for session", "Allow server for this session", "Deny"],
|
|
171
183
|
),
|
|
172
184
|
ownedSignal,
|
|
173
185
|
);
|
|
174
186
|
|
|
187
|
+
if (decision === "Allow server for this session") {
|
|
188
|
+
ownedSignal?.throwIfAborted();
|
|
189
|
+
const currentIdentity = getServerApprovalIdentity(state, serverName);
|
|
190
|
+
if (!serverIdentity || state.approvedServers !== approvedServers
|
|
191
|
+
|| currentIdentity?.definition !== serverIdentity.definition || currentIdentity.hash !== serverIdentity.hash) {
|
|
192
|
+
return { ok: false, reason: "denied" };
|
|
193
|
+
}
|
|
194
|
+
approvedServers.set(serverName, serverIdentity);
|
|
195
|
+
return { ok: true };
|
|
196
|
+
}
|
|
175
197
|
if (decision === "Allow once") {
|
|
176
198
|
return { ok: true };
|
|
177
199
|
}
|