@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.
Files changed (192) hide show
  1. package/README.md +4 -4
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  5. package/node_modules/@herbertgao/pi-subagents/package.json +1 -1
  6. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +5 -4
  7. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +39 -16
  8. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  9. package/node_modules/@narumitw/pi-btw/README.md +21 -4
  10. package/node_modules/@narumitw/pi-btw/dist/index.ts +1668 -958
  11. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +4 -4
  12. package/node_modules/@narumitw/pi-btw/docs/workflows.md +10 -2
  13. package/node_modules/@narumitw/pi-btw/package.json +1 -1
  14. package/node_modules/@narumitw/pi-btw/src/btw.ts +17 -79
  15. package/node_modules/@narumitw/pi-btw/src/conversation-context.ts +74 -0
  16. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +196 -7
  17. package/node_modules/@narumitw/pi-btw/src/main-thread-updates.ts +40 -0
  18. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  19. package/node_modules/@narumitw/pi-btw/src/settings.ts +49 -0
  20. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +9 -1
  21. package/node_modules/@narumitw/pi-btw/src/workspace-layout.ts +559 -0
  22. package/node_modules/pi-mcp-adapter/CHANGELOG.md +62 -0
  23. package/node_modules/pi-mcp-adapter/README.md +75 -7
  24. package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +193 -0
  25. package/node_modules/pi-mcp-adapter/cli.js +56 -5
  26. package/node_modules/pi-mcp-adapter/commands.ts +96 -5
  27. package/node_modules/pi-mcp-adapter/config.ts +73 -6
  28. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +22 -28
  29. package/node_modules/pi-mcp-adapter/direct-tools.ts +13 -5
  30. package/node_modules/pi-mcp-adapter/dist/abort.d.ts +2 -0
  31. package/node_modules/pi-mcp-adapter/dist/abort.js +36 -0
  32. package/node_modules/pi-mcp-adapter/dist/abort.js.map +1 -0
  33. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +6 -0
  34. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +194 -0
  35. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +1 -0
  36. package/node_modules/pi-mcp-adapter/dist/config.d.ts +5 -0
  37. package/node_modules/pi-mcp-adapter/dist/config.js +75 -6
  38. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  39. package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +18 -0
  40. package/node_modules/pi-mcp-adapter/dist/consent-manager.js +88 -0
  41. package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +1 -0
  42. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +17 -0
  43. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +316 -0
  44. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +1 -0
  45. package/node_modules/pi-mcp-adapter/dist/errors.d.ts +131 -0
  46. package/node_modules/pi-mcp-adapter/dist/errors.js +278 -0
  47. package/node_modules/pi-mcp-adapter/dist/errors.js.map +1 -0
  48. package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +8 -0
  49. package/node_modules/pi-mcp-adapter/dist/http-ca.js +88 -0
  50. package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +1 -0
  51. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +15 -0
  52. package/node_modules/pi-mcp-adapter/dist/jev-client.js +340 -0
  53. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +1 -0
  54. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +77 -0
  55. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +2 -0
  56. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +1 -0
  57. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +22 -0
  58. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +85 -0
  59. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +1 -0
  60. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +2 -0
  61. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +55 -0
  62. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -0
  63. package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +56 -0
  64. package/node_modules/pi-mcp-adapter/dist/lifecycle.js +410 -0
  65. package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +1 -0
  66. package/node_modules/pi-mcp-adapter/dist/logger.d.ts +51 -0
  67. package/node_modules/pi-mcp-adapter/dist/logger.js +131 -0
  68. package/node_modules/pi-mcp-adapter/dist/logger.js.map +1 -0
  69. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +26 -0
  70. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +103 -0
  71. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +1 -0
  72. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +118 -0
  73. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +1098 -0
  74. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +1 -0
  75. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +168 -0
  76. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +1117 -0
  77. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -0
  78. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +7 -140
  79. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -1
  80. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +53 -0
  81. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +441 -0
  82. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +1 -0
  83. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +148 -0
  84. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +689 -0
  85. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +1 -0
  86. package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +6 -0
  87. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +166 -0
  88. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +1 -0
  89. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +102 -0
  90. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +369 -0
  91. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +1 -0
  92. package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +95 -0
  93. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +242 -0
  94. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +1 -0
  95. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +2 -11
  96. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  97. package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +6 -0
  98. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +505 -0
  99. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +1 -0
  100. package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +10 -0
  101. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +312 -0
  102. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +1 -0
  103. package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +13 -0
  104. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +97 -0
  105. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +1 -0
  106. package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +17 -0
  107. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +203 -0
  108. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +1 -0
  109. package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +11 -0
  110. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +172 -0
  111. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +1 -0
  112. package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +157 -0
  113. package/node_modules/pi-mcp-adapter/dist/server-manager.js +1743 -0
  114. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -0
  115. package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +34 -0
  116. package/node_modules/pi-mcp-adapter/dist/session-approvals.js +140 -0
  117. package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +1 -0
  118. package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +26 -0
  119. package/node_modules/pi-mcp-adapter/dist/session-recovery.js +142 -0
  120. package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +1 -0
  121. package/node_modules/pi-mcp-adapter/dist/state.d.ts +73 -0
  122. package/node_modules/pi-mcp-adapter/dist/state.js +2 -0
  123. package/node_modules/pi-mcp-adapter/dist/state.js.map +1 -0
  124. package/node_modules/pi-mcp-adapter/dist/types.d.ts +36 -0
  125. package/node_modules/pi-mcp-adapter/dist/types.js +18 -0
  126. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  127. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +17 -0
  128. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +219 -0
  129. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +1 -0
  130. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +15 -0
  131. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +85 -0
  132. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +1 -0
  133. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +1 -0
  134. package/node_modules/pi-mcp-adapter/dist/utils.js +13 -0
  135. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  136. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +29 -19
  137. package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +85 -0
  138. package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +34 -0
  139. package/node_modules/pi-mcp-adapter/index.ts +129 -6
  140. package/node_modules/pi-mcp-adapter/init.ts +6 -19
  141. package/node_modules/pi-mcp-adapter/jev-client.ts +273 -0
  142. package/node_modules/pi-mcp-adapter/jev-contracts.ts +53 -0
  143. package/node_modules/pi-mcp-adapter/jev-key-store.ts +79 -0
  144. package/node_modules/pi-mcp-adapter/lifecycle.ts +14 -3
  145. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +7 -142
  146. package/node_modules/pi-mcp-adapter/mcp-code.ts +119 -11
  147. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +4 -0
  148. package/node_modules/pi-mcp-adapter/mcp-references.ts +5 -3
  149. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +5 -0
  150. package/node_modules/pi-mcp-adapter/mcp-tasks.ts +467 -0
  151. package/node_modules/pi-mcp-adapter/metadata-cache.ts +2 -13
  152. package/node_modules/pi-mcp-adapter/namespace-tools.ts +1 -1
  153. package/node_modules/pi-mcp-adapter/package.json +17 -9
  154. package/node_modules/pi-mcp-adapter/proxy-modes.ts +319 -166
  155. package/node_modules/pi-mcp-adapter/request-headers-command.ts +6 -3
  156. package/node_modules/pi-mcp-adapter/search-ranking.ts +38 -7
  157. package/node_modules/pi-mcp-adapter/secure-keyring.ts +170 -0
  158. package/node_modules/pi-mcp-adapter/semantic-search.ts +186 -0
  159. package/node_modules/pi-mcp-adapter/server-manager.ts +293 -50
  160. package/node_modules/pi-mcp-adapter/session-approvals.ts +14 -0
  161. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +2 -0
  162. package/node_modules/pi-mcp-adapter/state.ts +3 -1
  163. package/node_modules/pi-mcp-adapter/tool-approval.ts +26 -4
  164. package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -14
  165. package/node_modules/pi-mcp-adapter/tool-registrar.ts +8 -6
  166. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +4 -1
  167. package/node_modules/pi-mcp-adapter/types.ts +53 -0
  168. package/node_modules/pi-mcp-adapter/ui-server.ts +17 -9
  169. package/node_modules/pi-mcp-adapter/utils.ts +16 -0
  170. package/node_modules/pi-multi-account/CHANGELOG.md +23 -0
  171. package/node_modules/pi-multi-account/README.md +38 -11
  172. package/node_modules/pi-multi-account/index.ts +371 -96
  173. package/node_modules/pi-multi-account/package.json +5 -4
  174. package/node_modules/pi-multi-account/provider-payload-stream.ts +36 -28
  175. package/node_modules/pi-multi-account/usage.ts +31 -2
  176. package/node_modules/pi-typesafe/README.md +3 -1
  177. package/node_modules/pi-typesafe/dist/auth.d.ts +10 -3
  178. package/node_modules/pi-typesafe/dist/auth.js +13 -7
  179. package/node_modules/pi-typesafe/dist/backends.d.ts +31 -0
  180. package/node_modules/pi-typesafe/dist/backends.js +33 -0
  181. package/node_modules/pi-typesafe/dist/client.d.ts +3 -9
  182. package/node_modules/pi-typesafe/dist/client.js +63 -39
  183. package/node_modules/pi-typesafe/dist/credentials.d.ts +11 -5
  184. package/node_modules/pi-typesafe/dist/credentials.js +15 -8
  185. package/node_modules/pi-typesafe/dist/extension.js +4 -1
  186. package/node_modules/pi-typesafe/dist/index.d.ts +1 -1
  187. package/node_modules/pi-typesafe/dist/index.js +1 -1
  188. package/node_modules/pi-typesafe/dist/login.d.ts +13 -7
  189. package/node_modules/pi-typesafe/dist/login.js +17 -8
  190. package/node_modules/pi-typesafe/dist/schema.js +19 -13
  191. package/node_modules/pi-typesafe/package.json +1 -1
  192. 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 || (error instanceof SdkHttpError && error.status === 401);
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.connectPromises.get(name);
367
- if (existingConnect) {
368
- const existingAuthority = this.connectOAuthAuthorities.get(name);
369
- let reusable = oauthAuthority === undefined && existingAuthority === undefined;
370
- if (oauthAuthority && existingAuthority) {
371
- try {
372
- existingAuthority();
373
- reusable = true;
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 inFlight = this.reconnectPromises.get(name);
457
- if (inFlight) {
458
- return abortable(inFlight, ownedSignal);
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 promise = this.doReconnect(name, definition, staleConnection, ownedSignal).finally(() => {
462
- if (this.reconnectPromises.get(name) === promise) {
463
- this.reconnectPromises.delete(name);
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
- const staleInFlight = staleConnection.inFlight;
843
- await this.close(name);
844
- const fresh = await this.connect(name, definition, signal);
845
- fresh.inFlight = Math.max(fresh.inFlight, staleInFlight);
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
- const token = commandBearer
1322
- ? resolveCommandSecret(commandBearer, `MCP server "${serverName}" HTTP bearer token`)
1323
- : resolveBearerToken(definition)
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
- if (token) headers["Authorization"] = `Bearer ${token}`;
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 currentAttempt = this.connectAttempts.get(serverName);
1346
- if (currentAttempt && currentAttempt !== attemptOwner) {
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
- ...(commandFetch ? { delegate: commandFetch } : {}),
1620
+ ...(bearerFetch ? { delegate: bearerFetch } : {}),
1398
1621
  })
1399
- : commandFetch;
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
- if (pendingConnect) {
1920
+ for (const pending of [pendingConnect, pendingReconnect]) {
1921
+ if (!pending) continue;
1696
1922
  try {
1697
- await pendingConnect;
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
- return closing;
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([...this.connections.keys(), ...this.connectPromises.keys()]);
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 ownedSignal = combineAbortSignals(state.owner?.signal, signal);
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
  }