@humain/terminal 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/bundle/chunks/{chunk-6TW5AYGH.js → chunk-Z4RA2T5Q.js} +252 -40
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +30 -9
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +27 -0
- package/dist/humain/mcp-adapter/vendor/README.md +13 -13
- package/dist/humain/mcp-adapter/vendor/commands.ts +58 -21
- package/dist/humain/mcp-adapter/vendor/config.ts +18 -4
- package/dist/humain/mcp-adapter/vendor/direct-tools.ts +26 -6
- package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +4 -0
- package/dist/humain/mcp-adapter/vendor/dist/config.js +13 -4
- package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +11 -3
- package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/error-signal.ts +15 -4
- package/dist/humain/mcp-adapter/vendor/errors.ts +141 -0
- package/dist/humain/mcp-adapter/vendor/host-html-template.ts +58 -5
- package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +1017 -160
- package/dist/humain/mcp-adapter/vendor/index.ts +2 -3
- package/dist/humain/mcp-adapter/vendor/init.ts +5 -0
- package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +30 -9
- package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +66 -28
- package/dist/humain/mcp-adapter/vendor/mcp-status.ts +2 -0
- package/dist/humain/mcp-adapter/vendor/package.json +3 -2
- package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +66 -13
- package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +217 -0
- package/dist/humain/mcp-adapter/vendor/server-manager.ts +337 -6
- package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +1 -0
- package/dist/humain/mcp-adapter/vendor/types.ts +18 -3
- package/dist/humain/mcp-adapter/vendor/ui-resource-handler.ts +18 -2
- package/dist/humain/mcp-adapter/vendor/ui-server.ts +179 -54
- package/dist/humain/mcp-adapter/vendor/ui-session.ts +28 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/humain/mcp-adapter/UPSTREAM.md +1 -1
- package/src/humain/mcp-adapter/upstream.json +4 -4
- 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
|
|
939
|
-
//
|
|
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
|
-
?
|
|
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
|
-
|
|
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;
|
|
@@ -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
|
-
*
|
|
568
|
-
*
|
|
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 {
|
|
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) =>
|
|
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, {
|