@humain/terminal 0.0.14 → 0.0.15

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 (41) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/bundle/chunks/{chunk-6TW5AYGH.js → chunk-QMTWJZBT.js} +251 -39
  3. package/dist/bundle/cli.js +1 -1
  4. package/dist/bundle/index.js +1 -1
  5. package/dist/bundle/rpc-entry.js +1 -1
  6. package/dist/core/tools/subagent.d.ts.map +1 -1
  7. package/dist/core/tools/subagent.js +30 -9
  8. package/dist/core/tools/subagent.js.map +1 -1
  9. package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +27 -0
  10. package/dist/humain/mcp-adapter/vendor/README.md +13 -13
  11. package/dist/humain/mcp-adapter/vendor/commands.ts +58 -21
  12. package/dist/humain/mcp-adapter/vendor/config.ts +18 -4
  13. package/dist/humain/mcp-adapter/vendor/direct-tools.ts +26 -6
  14. package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +4 -0
  15. package/dist/humain/mcp-adapter/vendor/dist/config.js +13 -4
  16. package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
  17. package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +11 -3
  18. package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
  19. package/dist/humain/mcp-adapter/vendor/error-signal.ts +15 -4
  20. package/dist/humain/mcp-adapter/vendor/errors.ts +141 -0
  21. package/dist/humain/mcp-adapter/vendor/host-html-template.ts +58 -5
  22. package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +1017 -160
  23. package/dist/humain/mcp-adapter/vendor/index.ts +2 -3
  24. package/dist/humain/mcp-adapter/vendor/init.ts +5 -0
  25. package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +30 -9
  26. package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +66 -28
  27. package/dist/humain/mcp-adapter/vendor/mcp-status.ts +2 -0
  28. package/dist/humain/mcp-adapter/vendor/package.json +3 -2
  29. package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +66 -13
  30. package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +217 -0
  31. package/dist/humain/mcp-adapter/vendor/server-manager.ts +337 -6
  32. package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +1 -0
  33. package/dist/humain/mcp-adapter/vendor/types.ts +18 -3
  34. package/dist/humain/mcp-adapter/vendor/ui-resource-handler.ts +18 -2
  35. package/dist/humain/mcp-adapter/vendor/ui-server.ts +179 -54
  36. package/dist/humain/mcp-adapter/vendor/ui-session.ts +28 -8
  37. package/npm-shrinkwrap.json +2 -2
  38. package/package.json +1 -1
  39. package/src/humain/mcp-adapter/UPSTREAM.md +1 -1
  40. package/src/humain/mcp-adapter/upstream.json +4 -4
  41. package/src/humain/mcp-adapter/vendor/CHANGELOG.md +27 -0
@@ -11,6 +11,8 @@ import {
11
11
  type GetPromptResult,
12
12
  type ListToolsResult,
13
13
  type ReadResourceResult,
14
+ type McpSubscription,
15
+ type SubscriptionFilter,
14
16
  type CacheableRequestOptions,
15
17
  type RequestOptions,
16
18
  type UrlElicitationRequiredError,
@@ -28,6 +30,7 @@ import {
28
30
  type ServerStreamResultPatchNotification,
29
31
  type Transport,
30
32
  type McpTraceSettings,
33
+ type McpListenState,
31
34
  SERVER_STREAM_RESULT_PATCH_METHOD,
32
35
  serverStreamResultPatchNotificationSchema,
33
36
  } from "./types.ts";
@@ -37,7 +40,7 @@ import { logger } from "./logger.ts";
37
40
  import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
38
41
  import { McpOAuthProvider } from "./mcp-oauth-provider.ts";
39
42
  import { extractOAuthConfig, supportsOAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
40
- import { invalidateAuthEntryCache, type AuthStorageOptions } from "./mcp-auth.ts";
43
+ import { inspectAuthForUrl, invalidateAuthEntryCache, type AuthStorageOptions } from "./mcp-auth.ts";
41
44
  import { getBearerTokenForUrl } from "./mcp-bearer-store.ts";
42
45
  import { registerSamplingHandler, type ServerSamplingConfig } from "./sampling-handler.ts";
43
46
  import {
@@ -72,6 +75,7 @@ const abortCleanupPromises = new WeakMap<object, Promise<void>>();
72
75
  type HttpAuthProviderState =
73
76
  | { status: "disabled" }
74
77
  | { status: "implicit-deferred" }
78
+ | { status: "implicit-stored"; provider: McpOAuthProvider }
75
79
  | { status: "explicit"; provider: McpOAuthProvider }
76
80
  | { status: "implicit-challenged"; provider: McpOAuthProvider };
77
81
 
@@ -142,6 +146,18 @@ export interface ServerConnection {
142
146
  lastUsedAt: number;
143
147
  inFlight: number;
144
148
  status: "connected" | "closed" | "needs-auth";
149
+ /** Catalog subscription health, tracked independently from transport health. */
150
+ listenState: McpListenState;
151
+ listenSubscription?: McpSubscription;
152
+ /** Last requested filter; the server's honored filter may be a subset. */
153
+ listenFilter?: SubscriptionFilter;
154
+ /** True when recovery has an active listen but could not confirm every catalog list. */
155
+ listenCatalogStale?: boolean;
156
+ listenPromise?: Promise<void>;
157
+ listenRetryAfter?: number;
158
+ listenStopped?: boolean;
159
+ recentResourceUris?: Map<string, number>;
160
+ resourceReadRefreshUris?: Set<string>;
145
161
  /** True once this needs-auth episode discarded the cached credential. */
146
162
  credentialsInvalidated?: boolean;
147
163
  }
@@ -149,6 +165,8 @@ export interface ServerConnection {
149
165
 
150
166
  type UiStreamListener = (serverName: string, notification: ServerStreamResultPatchNotification["params"]) => void;
151
167
  type MetadataListChangedListener = (serverName: string, reason: string) => void;
168
+ type ListenStateChangedListener = (serverName: string, state: McpListenState) => void;
169
+ type ResourceUpdatedListener = (serverName: string, uri: string) => void;
152
170
 
153
171
  export type ToolRefreshResult = "updated" | "unchanged" | "superseded" | "refresh-timeout";
154
172
 
@@ -156,6 +174,9 @@ type ToolListCacheHints = Partial<Pick<ListToolsResult, "ttlMs" | "cacheScope">>
156
174
  type ToolListResult = { tools: McpTool[]; hints?: ToolListCacheHints };
157
175
 
158
176
  const KEEP_ALIVE_REFRESH_TIMEOUT_MS = 5_000;
177
+ const LISTEN_RETRY_DELAY_MS = 5_000;
178
+ const RECENT_RESOURCE_TTL_MS = 10 * 60_000;
179
+ const MAX_RESOURCE_SUBSCRIPTIONS = 32;
159
180
 
160
181
  export function isTransientHttpConnectError(error: unknown): boolean {
161
182
  let current: unknown = error;
@@ -173,6 +194,12 @@ export class McpServerManager {
173
194
  private uiStreamListeners = new Map<string, UiStreamListener>();
174
195
  private samplingConfig: ServerSamplingConfig | undefined;
175
196
  private metadataListChangedListener: MetadataListChangedListener | undefined;
197
+ private listenStateChangedListener: ListenStateChangedListener | undefined;
198
+ private resourceUpdatedListeners = new Map<string, {
199
+ serverName: string;
200
+ uri: string;
201
+ listener: ResourceUpdatedListener;
202
+ }>();
176
203
  private pendingMetadataPublications = new Map<
177
204
  string,
178
205
  { connection: ServerConnection; reason: string }
@@ -201,6 +228,10 @@ export class McpServerManager {
201
228
  this.metadataListChangedListener = listener;
202
229
  }
203
230
 
231
+ setListenStateChangedListener(listener: ListenStateChangedListener | undefined): void {
232
+ this.listenStateChangedListener = listener;
233
+ }
234
+
204
235
  publishMetadataChanged(
205
236
  name: string,
206
237
  expectedConnection: ServerConnection,
@@ -313,6 +344,10 @@ export class McpServerManager {
313
344
  throw new Error(`MCP connection for ${name} was closed while connecting`);
314
345
  }
315
346
  this.connections.set(name, connection);
347
+ this.watchListenSubscription(name, connection, connection.listenSubscription);
348
+ if ([...this.resourceUpdatedListeners.values()].some(registration => registration.serverName === name)) {
349
+ void this.ensureListen(name, connection);
350
+ }
316
351
  return connection;
317
352
  } finally {
318
353
  if (this.connectPromises.get(name) === promise) this.connectPromises.delete(name);
@@ -373,6 +408,8 @@ export class McpServerManager {
373
408
  return "superseded";
374
409
  }
375
410
 
411
+ await this.ensureListen(name, expectedConnection);
412
+
376
413
  const requestOptions = this.buildRequestOptions(expectedConnection.definition, signal);
377
414
  const timeout = Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS);
378
415
  const healthOptions = {
@@ -457,6 +494,250 @@ export class McpServerManager {
457
494
  }
458
495
  }
459
496
 
497
+ private setListenState(name: string, connection: ServerConnection, state: McpListenState): void {
498
+ if (connection.listenState === state) return;
499
+ connection.listenState = state;
500
+ if (this.connections.get(name) === connection) {
501
+ try {
502
+ this.listenStateChangedListener?.(name, state);
503
+ } catch {
504
+ // Status consumers must not interrupt listen lifecycle recovery.
505
+ }
506
+ }
507
+ }
508
+
509
+ private catalogListenFilter(connection: ServerConnection): SubscriptionFilter {
510
+ const capabilities = connection.client.getServerCapabilities?.();
511
+ return {
512
+ ...(capabilities?.tools?.listChanged ? { toolsListChanged: true } : {}),
513
+ ...(capabilities?.prompts?.listChanged ? { promptsListChanged: true } : {}),
514
+ ...(capabilities?.resources?.listChanged ? { resourcesListChanged: true } : {}),
515
+ };
516
+ }
517
+
518
+ private currentListenFilter(name: string, connection: ServerConnection): SubscriptionFilter {
519
+ const now = Date.now();
520
+ const recent = connection.recentResourceUris;
521
+ if (recent) {
522
+ for (const [uri, touchedAt] of recent) {
523
+ if (now - touchedAt > RECENT_RESOURCE_TTL_MS) recent.delete(uri);
524
+ }
525
+ }
526
+ const openResourceUris = [...new Set(
527
+ [...this.resourceUpdatedListeners.values()]
528
+ .filter(registration => registration.serverName === name)
529
+ .map(registration => registration.uri),
530
+ )].slice(-MAX_RESOURCE_SUBSCRIPTIONS);
531
+ const openSet = new Set(openResourceUris);
532
+ const recentSlots = MAX_RESOURCE_SUBSCRIPTIONS - openResourceUris.length;
533
+ const recentResourceUris = recentSlots > 0
534
+ ? [...(recent?.keys() ?? [])].filter(uri => !openSet.has(uri)).slice(-recentSlots)
535
+ : [];
536
+ const resourceSubscriptions = [...openResourceUris, ...recentResourceUris];
537
+ return {
538
+ ...this.catalogListenFilter(connection),
539
+ ...(resourceSubscriptions.length > 0 ? { resourceSubscriptions } : {}),
540
+ };
541
+ }
542
+
543
+ private watchListenSubscription(
544
+ name: string,
545
+ connection: ServerConnection,
546
+ subscription: McpSubscription | undefined,
547
+ ): void {
548
+ if (!subscription) return;
549
+ void subscription.closed.then(cause => {
550
+ if (
551
+ this.stopped ||
552
+ this.connections.get(name) !== connection ||
553
+ connection.status !== "connected" ||
554
+ connection.listenSubscription !== subscription
555
+ ) return;
556
+ if (cause === "remote") {
557
+ const staleUris = connection.listenFilter?.resourceSubscriptions ?? [];
558
+ if (staleUris.length > 0) connection.resourceReadRefreshUris = new Set(staleUris);
559
+ connection.listenCatalogStale = true;
560
+ connection.listenRetryAfter = Date.now();
561
+ this.setListenState(name, connection, "dropped");
562
+ } else if (cause === "graceful" || connection.listenState !== "re-establishing") {
563
+ connection.listenStopped = true;
564
+ this.setListenState(name, connection, "not-listening");
565
+ }
566
+ });
567
+ }
568
+
569
+ /** Quietly repairs a modern catalog listen at an existing activity boundary. */
570
+ async ensureListen(name: string, expectedConnection: ServerConnection): Promise<void> {
571
+ if (
572
+ this.stopped ||
573
+ this.connections.get(name) !== expectedConnection ||
574
+ expectedConnection.status !== "connected" ||
575
+ expectedConnection.listenStopped ||
576
+ expectedConnection.client.getProtocolEra?.() !== "modern"
577
+ ) return;
578
+
579
+ const filter = this.currentListenFilter(name, expectedConnection);
580
+ if (Object.keys(filter).length === 0) {
581
+ this.setListenState(name, expectedConnection, "not-listening");
582
+ return;
583
+ }
584
+ if (expectedConnection.listenPromise) {
585
+ await expectedConnection.listenPromise;
586
+ return this.ensureListen(name, expectedConnection);
587
+ }
588
+ if ((expectedConnection.listenRetryAfter ?? 0) > Date.now()) return;
589
+
590
+ const sameFilter = isDeepStrictEqual(expectedConnection.listenFilter, filter);
591
+ if (expectedConnection.listenState === "active" && sameFilter) {
592
+ if (!expectedConnection.listenCatalogStale) return;
593
+ const attempt = (async () => {
594
+ this.setListenState(name, expectedConnection, "re-establishing");
595
+ const confirmed = await this.reconcileCatalogAfterListen(name, expectedConnection, this.buildRequestOptions(expectedConnection.definition));
596
+ if (!confirmed) expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
597
+ this.setListenState(name, expectedConnection, "active");
598
+ })().finally(() => {
599
+ if (expectedConnection.listenPromise === attempt) delete expectedConnection.listenPromise;
600
+ });
601
+ expectedConnection.listenPromise = attempt;
602
+ return attempt;
603
+ }
604
+
605
+ const attempt = (async () => {
606
+ const recoverDroppedListen = expectedConnection.listenState === "dropped";
607
+ this.setListenState(name, expectedConnection, "re-establishing");
608
+ const previous = expectedConnection.listenSubscription;
609
+ if (previous) await previous.close().catch(() => {});
610
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return;
611
+ try {
612
+ const requestOptions = this.buildRequestOptions(expectedConnection.definition);
613
+ const subscription = await expectedConnection.client.listen(
614
+ filter,
615
+ {
616
+ ...requestOptions,
617
+ timeout: Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS),
618
+ },
619
+ );
620
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") {
621
+ await subscription.close().catch(() => {});
622
+ return;
623
+ }
624
+ expectedConnection.listenSubscription = subscription;
625
+ expectedConnection.listenFilter = filter;
626
+ delete expectedConnection.listenStopped;
627
+ delete expectedConnection.listenRetryAfter;
628
+ this.watchListenSubscription(name, expectedConnection, subscription);
629
+ const confirmed = recoverDroppedListen
630
+ ? await this.reconcileCatalogAfterListen(name, expectedConnection, requestOptions)
631
+ : true;
632
+ if (!confirmed) expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
633
+ this.setListenState(name, expectedConnection, "active");
634
+ } catch (error) {
635
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return;
636
+ if (expectedConnection.listenSubscription) await expectedConnection.listenSubscription.close().catch(() => {});
637
+ expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
638
+ this.setListenState(name, expectedConnection, "dropped");
639
+ logger.debug(`MCP: catalog listen repair failed for ${name}: ${error instanceof Error ? error.message : String(error)}`);
640
+ }
641
+ })().finally(() => {
642
+ if (expectedConnection.listenPromise === attempt) delete expectedConnection.listenPromise;
643
+ });
644
+ expectedConnection.listenPromise = attempt;
645
+ return attempt;
646
+ }
647
+
648
+ private async reconcileCatalogAfterListen(
649
+ name: string,
650
+ expectedConnection: ServerConnection,
651
+ requestOptions?: CacheableRequestOptions,
652
+ ): Promise<boolean> {
653
+ const timeout = Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS);
654
+ const refreshSignal = combineAbortSignals(requestOptions?.signal, AbortSignal.timeout(timeout));
655
+ const refreshOptions: CacheableRequestOptions = {
656
+ ...requestOptions,
657
+ timeout,
658
+ cacheMode: "refresh",
659
+ ...(refreshSignal ? { signal: refreshSignal } : {}),
660
+ };
661
+ const [toolResult, resources, promptResult] = await Promise.allSettled([
662
+ this.fetchAllTools(expectedConnection.client, refreshOptions),
663
+ this.fetchAllResources(expectedConnection.client, refreshOptions, true),
664
+ this.fetchAllPrompts(expectedConnection.client, refreshOptions),
665
+ ]);
666
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return false;
667
+
668
+ const nextTools = toolResult.status === "fulfilled" ? toolResult.value : undefined;
669
+ const nextResources = resources.status === "fulfilled" ? resources.value : undefined;
670
+ const nextPrompts = promptResult.status === "fulfilled" && !promptResult.value.failed ? promptResult.value : undefined;
671
+ const confirmed = nextTools !== undefined && nextResources !== undefined && nextPrompts !== undefined;
672
+ const changed = (nextTools !== undefined && (
673
+ !isDeepStrictEqual(expectedConnection.tools, nextTools.tools) ||
674
+ !isDeepStrictEqual(expectedConnection.toolListHints, nextTools.hints)
675
+ )) ||
676
+ (nextResources !== undefined && !isDeepStrictEqual(expectedConnection.resources, nextResources)) ||
677
+ (nextPrompts !== undefined && (
678
+ !isDeepStrictEqual(expectedConnection.prompts, nextPrompts.prompts) ||
679
+ expectedConnection.promptDiscoveryFailed !== false
680
+ ));
681
+ if (!changed) {
682
+ this.retryPendingMetadataPublication(name, expectedConnection);
683
+ if (confirmed) delete expectedConnection.listenCatalogStale;
684
+ else expectedConnection.listenCatalogStale = true;
685
+ return confirmed;
686
+ }
687
+
688
+ if (nextTools !== undefined) {
689
+ expectedConnection.tools = nextTools.tools;
690
+ expectedConnection.toolListHints = nextTools.hints;
691
+ expectedConnection.toolsRevision = (expectedConnection.toolsRevision ?? 0) + 1;
692
+ }
693
+ if (nextResources !== undefined) expectedConnection.resources = nextResources;
694
+ if (nextPrompts !== undefined) {
695
+ expectedConnection.prompts = nextPrompts.prompts;
696
+ expectedConnection.promptDiscoveryFailed = false;
697
+ }
698
+ if (confirmed) delete expectedConnection.listenCatalogStale;
699
+ else expectedConnection.listenCatalogStale = true;
700
+ this.metadataListChangedListener?.(name, "listen-recovered");
701
+ this.pendingMetadataPublications.delete(name);
702
+ return confirmed;
703
+ }
704
+
705
+ async prepareResourceUse(name: string, uri: string, expectedConnection: ServerConnection): Promise<boolean> {
706
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return false;
707
+ const refreshRead = expectedConnection.listenState === "dropped" ||
708
+ expectedConnection.listenState === "re-establishing" ||
709
+ expectedConnection.resourceReadRefreshUris?.has(uri) === true;
710
+ const recent = expectedConnection.recentResourceUris ?? new Map<string, number>();
711
+ recent.delete(uri);
712
+ recent.set(uri, Date.now());
713
+ while (recent.size > MAX_RESOURCE_SUBSCRIPTIONS) {
714
+ const oldest = recent.keys().next().value as string | undefined;
715
+ if (oldest === undefined) break;
716
+ recent.delete(oldest);
717
+ }
718
+ expectedConnection.recentResourceUris = recent;
719
+ await this.ensureListen(name, expectedConnection);
720
+ expectedConnection.resourceReadRefreshUris?.delete(uri);
721
+ return refreshRead;
722
+ }
723
+
724
+ registerResourceUpdatedListener(
725
+ token: string,
726
+ serverName: string,
727
+ uri: string,
728
+ listener: ResourceUpdatedListener,
729
+ ): void {
730
+ this.removeResourceUpdatedListener(token);
731
+ this.resourceUpdatedListeners.set(token, { serverName, uri, listener });
732
+ const connection = this.connections.get(serverName);
733
+ if (!connection || connection.status !== "connected") return;
734
+ void this.prepareResourceUse(serverName, uri, connection);
735
+ }
736
+
737
+ removeResourceUpdatedListener(token: string): void {
738
+ this.resourceUpdatedListeners.delete(token);
739
+ }
740
+
460
741
  private async doReconnect(
461
742
  name: string,
462
743
  definition: ServerDefinition,
@@ -571,6 +852,7 @@ export class McpServerManager {
571
852
  lastUsedAt: Date.now(),
572
853
  inFlight: 0,
573
854
  status: "needs-auth",
855
+ listenState: "disconnected",
574
856
  credentialsInvalidated: invalidated,
575
857
  };
576
858
  }
@@ -594,6 +876,8 @@ export class McpServerManager {
594
876
  this.attachAdapterNotificationHandlers(name, client);
595
877
 
596
878
  const instructions = client.getInstructions?.();
879
+ const protocolEra = client.getProtocolEra?.();
880
+ const autoOpenedSubscription = client.autoOpenedSubscription;
597
881
  const connection: ServerConnection = {
598
882
  client,
599
883
  transport,
@@ -606,7 +890,16 @@ export class McpServerManager {
606
890
  lastUsedAt: Date.now(),
607
891
  inFlight: 0,
608
892
  status: "connected",
893
+ listenState: protocolEra === "modern"
894
+ ? autoOpenedSubscription
895
+ ? "active"
896
+ : "not-listening"
897
+ : "legacy",
898
+ ...(autoOpenedSubscription ? {
899
+ listenSubscription: autoOpenedSubscription,
900
+ } : {}),
609
901
  };
902
+ if (autoOpenedSubscription) connection.listenFilter = this.catalogListenFilter(connection);
610
903
 
611
904
  // Reflect the SDK's own close signal in connection status, guarded by
612
905
  // identity so a stale connection's late close can never clobber a fresh
@@ -664,6 +957,7 @@ export class McpServerManager {
664
957
  lastUsedAt: Date.now(),
665
958
  inFlight: 0,
666
959
  status: "needs-auth",
960
+ listenState: "disconnected",
667
961
  credentialsInvalidated: invalidated,
668
962
  };
669
963
  }
@@ -935,11 +1229,26 @@ export class McpServerManager {
935
1229
  this.oauthRuntime?.signal,
936
1230
  );
937
1231
 
938
- // Explicit OAuth checks secure storage immediately. Implicit OAuth defers
939
- // provider construction until the server proves authentication is needed.
1232
+ // Explicit OAuth checks secure storage immediately. Implicit OAuth keeps
1233
+ // anonymous servers provider-free unless URL-bound credentials are already
1234
+ // stored, so an unavailable credential store does not break anonymous use.
1235
+ let implicitStoredAuth: ReturnType<typeof inspectAuthForUrl> | undefined;
1236
+ if (definition.auth === undefined && supportsOAuth(definition)) {
1237
+ try {
1238
+ implicitStoredAuth = inspectAuthForUrl(serverName, serverUrl, this.authStorageOptions);
1239
+ } catch {
1240
+ // Implicit preflight is opportunistic; malformed records must not block
1241
+ // an otherwise anonymous-capable server from connecting.
1242
+ }
1243
+ }
1244
+ const hasImplicitStoredTokens = implicitStoredAuth?.status === "present"
1245
+ && implicitStoredAuth.entry.tokens !== undefined;
1246
+ if (hasImplicitStoredTokens) invalidateAuthEntryCache(serverName);
940
1247
  let authState: HttpAuthProviderState = supportsOAuth(definition)
941
1248
  ? definition.auth === undefined
942
- ? { status: "implicit-deferred" }
1249
+ ? hasImplicitStoredTokens
1250
+ ? { status: "implicit-stored", provider: createAuthProvider() }
1251
+ : { status: "implicit-deferred" }
943
1252
  : { status: "explicit", provider: createAuthProvider() }
944
1253
  : { status: "disabled" };
945
1254
 
@@ -1082,7 +1391,7 @@ export class McpServerManager {
1082
1391
  }
1083
1392
  }
1084
1393
 
1085
- private async fetchAllResources(client: Client, requestOptions?: RequestOptions): Promise<McpResource[]> {
1394
+ private async fetchAllResources(client: Client, requestOptions?: RequestOptions, strict = false): Promise<McpResource[]> {
1086
1395
  const capabilities = client.getServerCapabilities?.();
1087
1396
  if (!capabilities?.resources) return [];
1088
1397
 
@@ -1102,12 +1411,27 @@ export class McpServerManager {
1102
1411
  throwIfAborted(requestOptions.signal);
1103
1412
  }
1104
1413
  if (isUnauthorizedHttpError(error)) throw error;
1414
+ if (strict) throw error;
1105
1415
  // The server advertises resources but the listing failed
1106
1416
  return [];
1107
1417
  }
1108
1418
  }
1109
1419
 
1110
1420
  private attachAdapterNotificationHandlers(serverName: string, client: Client): void {
1421
+ client.setNotificationHandler("notifications/resources/updated", notification => {
1422
+ const uri = notification.params.uri;
1423
+ const connection = this.connections.get(serverName);
1424
+ if (!connection || connection.client !== client || connection.status !== "connected") return;
1425
+ for (const registration of this.resourceUpdatedListeners.values()) {
1426
+ if (registration.serverName === serverName && registration.uri === uri) {
1427
+ try {
1428
+ registration.listener(serverName, uri);
1429
+ } catch {
1430
+ // One UI listener must not block resource invalidation for others.
1431
+ }
1432
+ }
1433
+ }
1434
+ });
1111
1435
  client.setNotificationHandler(
1112
1436
  SERVER_STREAM_RESULT_PATCH_METHOD,
1113
1437
  { params: serverStreamResultPatchNotificationSchema.shape.params },
@@ -1140,6 +1464,7 @@ export class McpServerManager {
1140
1464
  try {
1141
1465
  this.touch(name);
1142
1466
  this.incrementInFlight(name);
1467
+ await this.ensureListen(name, connection);
1143
1468
  return await connection.client.getPrompt(
1144
1469
  { name: promptName, ...(args ? { arguments: args } : {}) },
1145
1470
  this.getRequestOptions(name, signal),
@@ -1162,7 +1487,12 @@ export class McpServerManager {
1162
1487
  try {
1163
1488
  this.touch(name);
1164
1489
  this.incrementInFlight(name);
1165
- return await connection.client.readResource({ uri }, this.getRequestOptions(name, signal));
1490
+ const refreshRead = await this.prepareResourceUse(name, uri, connection);
1491
+ const requestOptions = this.getRequestOptions(name, signal);
1492
+ return await connection.client.readResource(
1493
+ { uri },
1494
+ refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions,
1495
+ );
1166
1496
  } finally {
1167
1497
  this.decrementInFlight(name);
1168
1498
  this.touch(name);
@@ -1235,6 +1565,7 @@ export class McpServerManager {
1235
1565
  .flatMap(result => result.status === "rejected" ? [result.reason] : [])
1236
1566
  .filter(error => this.containsCleanupFailure(error));
1237
1567
  this.uiStreamListeners.clear();
1568
+ this.resourceUpdatedListeners.clear();
1238
1569
  this.acceptedUrlElicitations.clear();
1239
1570
  this.pendingMetadataPublications.clear();
1240
1571
  this.samplingConfig = undefined;
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  name: mcp-scripting
3
3
  description: Write mcpScript JavaScript for discovering, inspecting, and calling MCP tools.
4
+ disable-model-invocation: true
4
5
  ---
5
6
 
6
7
  # MCP scripting
@@ -26,6 +26,14 @@ export type McpServerRuntimeStatus =
26
26
  | "not-connected"
27
27
  | "disabled";
28
28
 
29
+ export type McpListenState =
30
+ | "active"
31
+ | "dropped"
32
+ | "re-establishing"
33
+ | "legacy"
34
+ | "not-listening"
35
+ | "disconnected";
36
+
29
37
  export interface McpServerStatusSnapshot {
30
38
  readonly name: string;
31
39
  readonly status: McpServerRuntimeStatus;
@@ -33,6 +41,8 @@ export interface McpServerStatusSnapshot {
33
41
  readonly resourceCount?: number;
34
42
  readonly failedAgoSeconds?: number;
35
43
  readonly disabled: boolean;
44
+ readonly listenState: McpListenState;
45
+ readonly catalogStale?: boolean;
36
46
  }
37
47
 
38
48
  export interface McpStatusSnapshot {
@@ -173,6 +183,9 @@ export type UiDisplayMode = "inline" | "fullscreen" | "pip";
173
183
  export interface UiServerHandle {
174
184
  url: string;
175
185
  port: number;
186
+ /** URL of the second-origin MCP Apps sandbox proxy. */
187
+ proxyUrl: string;
188
+ proxyPort: number;
176
189
  sessionToken: string;
177
190
  serverName: string;
178
191
  toolName: string;
@@ -183,6 +196,7 @@ export interface UiServerHandle {
183
196
  sendToolResult: (result: CallToolResult) => void;
184
197
  sendResultPatch: (result: CallToolResult) => void;
185
198
  sendToolCancelled: (reason: string) => void;
199
+ sendResourceUpdated: (uri: string) => void;
186
200
  sendHostContext: (context: UiHostContext) => void;
187
201
  /** Get accumulated messages from this session */
188
202
  getSessionMessages: () => UiSessionMessages;
@@ -564,9 +578,8 @@ export interface McpSettings {
564
578
  approveTools?: boolean | string[];
565
579
  disableProxyTool?: boolean;
566
580
  /** Freeze direct-tool registration after the initial sync. Automatic metadata updates
567
- * (reconnects, lazy-connect, tool-list-changed) won't rebuild the system prompt,
568
- * preserving the prompt-cache prefix. The agent rediscovers explicitly via
569
- * mcp({ connect: "server" }). Default: false. */
581
+ * and explicit reconnects won't rebuild the system prompt, preserving the
582
+ * prompt-cache prefix. Proxy/search/cache metadata still refreshes. Default: false. */
570
583
  freezeDirectTools?: boolean;
571
584
  autoAuth?: boolean;
572
585
  sampling?: boolean;
@@ -710,6 +723,8 @@ export interface McpPanelCallbacks {
710
723
 
711
724
  export interface McpPanelResult {
712
725
  changes: Map<string, true | string[] | false>;
726
+ /** Servers whose disabled flag changed during the panel session (name → new disabled state). */
727
+ disabledChanges: Map<string, boolean>;
713
728
  cancelled: boolean;
714
729
  }
715
730
 
@@ -1,6 +1,11 @@
1
1
  import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
2
2
  import { UrlElicitationRequiredError, type ReadResourceResult } from "@modelcontextprotocol/client";
3
- import { ResourceFetchError, ResourceParseError } from "./errors.ts";
3
+ import {
4
+ getInputRequiredNeedsUiDetails,
5
+ InputRequiredNeedsUiError,
6
+ ResourceFetchError,
7
+ ResourceParseError,
8
+ } from "./errors.ts";
4
9
  import { logger } from "./logger.ts";
5
10
  import { SessionRecoveryAuthRequiredError, withSessionRecovery, type SessionRecoveryDeps } from "./session-recovery.ts";
6
11
  import type { McpServerManager } from "./server-manager.ts";
@@ -52,7 +57,14 @@ export class UiResourceHandler {
52
57
  ...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),
53
58
  },
54
59
  serverName,
55
- (connection) => connection.client.readResource({ uri }, this.manager.getRequestOptions(serverName, options.signal)),
60
+ async (connection) => {
61
+ const refreshRead = await this.manager.prepareResourceUse?.(serverName, uri, connection);
62
+ const requestOptions = this.manager.getRequestOptions(serverName, options.signal);
63
+ return connection.client.readResource(
64
+ { uri },
65
+ refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions,
66
+ );
67
+ },
56
68
  );
57
69
  } finally {
58
70
  this.manager.decrementInFlight(serverName);
@@ -63,6 +75,10 @@ export class UiResourceHandler {
63
75
  }
64
76
  } catch (error) {
65
77
  if (error instanceof UrlElicitationRequiredError || error instanceof SessionRecoveryAuthRequiredError) throw error;
78
+ const inputRequired = getInputRequiredNeedsUiDetails(error, { server: serverName, resourceUri: uri });
79
+ if (inputRequired) {
80
+ throw new InputRequiredNeedsUiError(inputRequired, error instanceof Error ? error : undefined);
81
+ }
66
82
  const message = error instanceof Error ? error.message : String(error);
67
83
  log.error("Failed to read resource", error instanceof Error ? error : undefined);
68
84
  throw new ResourceFetchError(uri, message, {