@modelcontextprotocol/inspector 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -16
- package/clients/cli/build/index.js +304 -38
- package/clients/tui/build/index.js +2737 -656
- package/clients/web/build/index.js +112 -9
- package/clients/web/dist/assets/index-BrCIxo3Q.js +242 -0
- package/clients/web/dist/assets/{prism-light-CCFFzO_n.js → prism-light-CWdjx3zH.js} +1 -1
- package/clients/web/dist/index.html +1 -1
- package/clients/web/static/sandbox_proxy.html +237 -0
- package/package.json +13 -14
- package/clients/web/dist/assets/index-CyPh2pEE.js +0 -300
|
@@ -83,9 +83,6 @@ function withConnectTimeout(settings, connectionTimeout) {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
// ../../core/auth/node/secret-store.ts
|
|
87
|
-
import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring";
|
|
88
|
-
|
|
89
86
|
// ../../core/auth/secret-fields.ts
|
|
90
87
|
var SECRET_FIELD_OAUTH_CLIENT_SECRET = "oauth-client-secret";
|
|
91
88
|
var SECRET_FIELD_IDP_CLIENT_SECRET = "idp-client-secret";
|
|
@@ -93,6 +90,41 @@ var envSecretField = (envKey) => `env:${envKey}`;
|
|
|
93
90
|
|
|
94
91
|
// ../../core/auth/node/secret-store.ts
|
|
95
92
|
var SERVICE_NAME = "mcp-inspector";
|
|
93
|
+
var keyringLoad;
|
|
94
|
+
var KeyringModuleShapeError = class extends Error {
|
|
95
|
+
constructor(detail, options) {
|
|
96
|
+
super(`@napi-rs/keyring loaded but ${detail}`, options);
|
|
97
|
+
this.name = "KeyringModuleShapeError";
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var checkKeyringShape = (mod) => {
|
|
101
|
+
try {
|
|
102
|
+
if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
|
|
103
|
+
return { ok: true, mod };
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
err: new KeyringModuleShapeError(
|
|
108
|
+
"did not expose AsyncEntry / findCredentialsAsync"
|
|
109
|
+
)
|
|
110
|
+
};
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return {
|
|
113
|
+
ok: false,
|
|
114
|
+
err: new KeyringModuleShapeError(
|
|
115
|
+
`its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
|
|
116
|
+
{ cause: err }
|
|
117
|
+
)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var loadKeyring = () => {
|
|
122
|
+
keyringLoad ??= import("@napi-rs/keyring").then(
|
|
123
|
+
checkKeyringShape,
|
|
124
|
+
(err) => ({ ok: false, err })
|
|
125
|
+
);
|
|
126
|
+
return keyringLoad;
|
|
127
|
+
};
|
|
96
128
|
function parseAccount(account) {
|
|
97
129
|
const idx = account.indexOf(":");
|
|
98
130
|
if (idx <= 0 || idx === account.length - 1) return null;
|
|
@@ -104,16 +136,31 @@ function parseAccount(account) {
|
|
|
104
136
|
var buildAccount = (serverId, field) => `${serverId}:${field}`;
|
|
105
137
|
var KeychainUnavailableError = class extends Error {
|
|
106
138
|
constructor(cause) {
|
|
139
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
107
140
|
super(
|
|
108
|
-
`OS keychain is not available.
|
|
141
|
+
`OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
|
|
109
142
|
);
|
|
110
143
|
this.name = "KeychainUnavailableError";
|
|
111
144
|
}
|
|
112
145
|
};
|
|
146
|
+
var hintFor = (cause, message) => {
|
|
147
|
+
if (cause instanceof KeyringModuleShapeError) {
|
|
148
|
+
return `The @napi-rs/keyring package loaded but does not expose the API this build expects \u2014 most likely a version or packaging mismatch; reinstall the Inspector, and report this if it persists.`;
|
|
149
|
+
}
|
|
150
|
+
if (message.includes("Cannot find native binding")) {
|
|
151
|
+
return `The @napi-rs/keyring platform package for this OS is missing or unavailable \u2014 reinstall the Inspector (for npx, clear the npx cache under your npm cache directory first).`;
|
|
152
|
+
}
|
|
153
|
+
return `On Linux, install libsecret / gnome-keyring.`;
|
|
154
|
+
};
|
|
113
155
|
var KeyringSecretStore = class {
|
|
114
156
|
async get(serverId, field) {
|
|
115
|
-
const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
|
|
116
157
|
try {
|
|
158
|
+
const keyring = await loadKeyring();
|
|
159
|
+
if (!keyring.ok) return null;
|
|
160
|
+
const entry = new keyring.mod.AsyncEntry(
|
|
161
|
+
SERVICE_NAME,
|
|
162
|
+
buildAccount(serverId, field)
|
|
163
|
+
);
|
|
117
164
|
const v2 = await entry.getPassword();
|
|
118
165
|
return v2 ?? null;
|
|
119
166
|
} catch {
|
|
@@ -121,16 +168,27 @@ var KeyringSecretStore = class {
|
|
|
121
168
|
}
|
|
122
169
|
}
|
|
123
170
|
async set(serverId, field, value) {
|
|
124
|
-
const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
|
|
125
171
|
try {
|
|
172
|
+
const keyring = await loadKeyring();
|
|
173
|
+
if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
|
|
174
|
+
const entry = new keyring.mod.AsyncEntry(
|
|
175
|
+
SERVICE_NAME,
|
|
176
|
+
buildAccount(serverId, field)
|
|
177
|
+
);
|
|
126
178
|
await entry.setPassword(value);
|
|
127
179
|
} catch (err) {
|
|
180
|
+
if (err instanceof KeychainUnavailableError) throw err;
|
|
128
181
|
throw new KeychainUnavailableError(err);
|
|
129
182
|
}
|
|
130
183
|
}
|
|
131
184
|
async delete(serverId, field) {
|
|
132
|
-
const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
|
|
133
185
|
try {
|
|
186
|
+
const keyring = await loadKeyring();
|
|
187
|
+
if (!keyring.ok) return;
|
|
188
|
+
const entry = new keyring.mod.AsyncEntry(
|
|
189
|
+
SERVICE_NAME,
|
|
190
|
+
buildAccount(serverId, field)
|
|
191
|
+
);
|
|
134
192
|
await entry.deleteCredential();
|
|
135
193
|
} catch {
|
|
136
194
|
}
|
|
@@ -138,7 +196,9 @@ var KeyringSecretStore = class {
|
|
|
138
196
|
async deleteAllForServer(serverId) {
|
|
139
197
|
let creds;
|
|
140
198
|
try {
|
|
141
|
-
|
|
199
|
+
const keyring = await loadKeyring();
|
|
200
|
+
if (!keyring.ok) return;
|
|
201
|
+
creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
|
|
142
202
|
} catch {
|
|
143
203
|
return;
|
|
144
204
|
}
|
|
@@ -4629,6 +4689,7 @@ async function* parseSSE(reader) {
|
|
|
4629
4689
|
}
|
|
4630
4690
|
var RemoteClientTransport = class {
|
|
4631
4691
|
_sessionId = void 0;
|
|
4692
|
+
_protocolVersion = void 0;
|
|
4632
4693
|
eventStreamReader = null;
|
|
4633
4694
|
eventStreamAbort = null;
|
|
4634
4695
|
eventStreamConsumeTask = null;
|
|
@@ -4651,6 +4712,21 @@ var RemoteClientTransport = class {
|
|
|
4651
4712
|
getRemoteBackendSessionId() {
|
|
4652
4713
|
return this._sessionId;
|
|
4653
4714
|
}
|
|
4715
|
+
/**
|
|
4716
|
+
* The SDK Client calls this with the negotiated protocol version as soon as
|
|
4717
|
+
* `initialize` resolves — before it sends `notifications/initialized` — so an
|
|
4718
|
+
* HTTP transport can stamp `Mcp-Protocol-Version` on every later request.
|
|
4719
|
+
*
|
|
4720
|
+
* Here the real upstream transport lives on the backend, so we can only
|
|
4721
|
+
* record the version and forward it on the next `/api/mcp/send`, which
|
|
4722
|
+
* applies it to the upstream transport *before* sending. Because
|
|
4723
|
+
* `notifications/initialized` is that next send, it and everything after it
|
|
4724
|
+
* (including the standalone SSE GET and the session DELETE, both issued by
|
|
4725
|
+
* the upstream transport itself) carry the header (#1935).
|
|
4726
|
+
*/
|
|
4727
|
+
setProtocolVersion(version) {
|
|
4728
|
+
this._protocolVersion = version;
|
|
4729
|
+
}
|
|
4654
4730
|
/**
|
|
4655
4731
|
* Reattach to an existing remote backend session after a full-page OAuth
|
|
4656
4732
|
* redirect. Opens the SSE event stream without POST /connect.
|
|
@@ -5020,7 +5096,12 @@ var RemoteClientTransport = class {
|
|
|
5020
5096
|
},
|
|
5021
5097
|
// Forward per-send `Mcp-Param-*` headers (SEP-2243) for the backend to
|
|
5022
5098
|
// apply to the upstream send; the browser can't set them cross-origin.
|
|
5023
|
-
...options?.headers != null && { headers: options.headers }
|
|
5099
|
+
...options?.headers != null && { headers: options.headers },
|
|
5100
|
+
// Forward the negotiated protocol version so the backend's transport can
|
|
5101
|
+
// stamp `Mcp-Protocol-Version` on this and every later request (#1935).
|
|
5102
|
+
...this._protocolVersion !== void 0 && {
|
|
5103
|
+
protocolVersion: this._protocolVersion
|
|
5104
|
+
}
|
|
5024
5105
|
};
|
|
5025
5106
|
const res = await this.fetchFn(`${this.baseUrl}/api/mcp/send`, {
|
|
5026
5107
|
method: "POST",
|
|
@@ -5101,6 +5182,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5101
5182
|
outputValidator = null;
|
|
5102
5183
|
transport = null;
|
|
5103
5184
|
baseTransport = null;
|
|
5185
|
+
// Correlation for `markResponseRejected` (#1953): the method of each
|
|
5186
|
+
// outbound request still awaiting a response, and — once one is answered —
|
|
5187
|
+
// the id of the most recently answered request per method. Entries are
|
|
5188
|
+
// dropped as responses arrive, so this holds at most one id per method
|
|
5189
|
+
// rather than growing with the session.
|
|
5190
|
+
outboundRequestMethods = /* @__PURE__ */ new Map();
|
|
5191
|
+
lastAnsweredRequestByMethod = /* @__PURE__ */ new Map();
|
|
5104
5192
|
/** True when the cached transport was built with an OAuth authProvider attached. */
|
|
5105
5193
|
transportHasAuthProvider = false;
|
|
5106
5194
|
/** Dedupes concurrent ambient auth challenges (reason + scopes). */
|
|
@@ -5211,7 +5299,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5211
5299
|
// `McpSubscription` whose filter's `resourceSubscriptions` mirrors
|
|
5212
5300
|
// `subscribedResources`; mutating the set re-lists (close old, open new), and
|
|
5213
5301
|
// an unexpected `"remote"` close re-lists (reconnect-by-re-listen — the stream
|
|
5214
|
-
// is not resumable). `null`
|
|
5302
|
+
// is not resumable). `null` on the legacy era, and on the modern era whenever
|
|
5303
|
+
// the filter is empty — which is *not* the same as "no URI subscribed": the
|
|
5304
|
+
// stream also carries the list-change opt-ins, so a tools-only server opens it
|
|
5305
|
+
// with no subscriptions at all (#1920).
|
|
5215
5306
|
modernSubscription = null;
|
|
5216
5307
|
// Monotonic guard so a stale re-list/reconnect (whose `listen()` or `closed`
|
|
5217
5308
|
// resolves after a newer refresh already started) can detect it lost the race
|
|
@@ -5434,6 +5525,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5434
5525
|
createMessageTrackingCallbacks() {
|
|
5435
5526
|
return {
|
|
5436
5527
|
trackRequest: (message, origin) => {
|
|
5528
|
+
if (origin === "client") {
|
|
5529
|
+
this.outboundRequestMethods.set(message.id, message.method);
|
|
5530
|
+
}
|
|
5437
5531
|
const entry = {
|
|
5438
5532
|
id: crypto.randomUUID(),
|
|
5439
5533
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -5444,6 +5538,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5444
5538
|
this.dispatchTypedEvent("message", entry);
|
|
5445
5539
|
},
|
|
5446
5540
|
trackResponse: (message, origin) => {
|
|
5541
|
+
const responseId = message.id;
|
|
5542
|
+
if (origin === "server" && responseId !== void 0) {
|
|
5543
|
+
const method = this.outboundRequestMethods.get(responseId);
|
|
5544
|
+
this.outboundRequestMethods.delete(responseId);
|
|
5545
|
+
if (method !== void 0) {
|
|
5546
|
+
this.lastAnsweredRequestByMethod.set(method, responseId);
|
|
5547
|
+
}
|
|
5548
|
+
}
|
|
5447
5549
|
const entry = {
|
|
5448
5550
|
id: crypto.randomUUID(),
|
|
5449
5551
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -6001,6 +6103,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
6001
6103
|
this.clearReceiverTasks();
|
|
6002
6104
|
this.resetSubscriptionStream();
|
|
6003
6105
|
this.cancelledTaskIds.clear();
|
|
6106
|
+
this.outboundRequestMethods.clear();
|
|
6107
|
+
this.lastAnsweredRequestByMethod.clear();
|
|
6004
6108
|
for (const [, controller] of this.taskInputAbortControllers) {
|
|
6005
6109
|
controller.abort(new Error("Connection ended"));
|
|
6006
6110
|
}
|
|
@@ -6224,7 +6328,6 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
6224
6328
|
this.status = "connected";
|
|
6225
6329
|
this.dispatchTypedEvent("statusChange", this.status);
|
|
6226
6330
|
await this.fetchServerInfo();
|
|
6227
|
-
this.dispatchTypedEvent("connect");
|
|
6228
6331
|
if (this.initialLoggingLevel && this.capabilities?.logging) {
|
|
6229
6332
|
await this.client.setLoggingLevel(
|
|
6230
6333
|
this.initialLoggingLevel,
|
|
@@ -6303,6 +6406,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
6303
6406
|
);
|
|
6304
6407
|
}
|
|
6305
6408
|
}
|
|
6409
|
+
await this.openModernListenStreamOnConnect();
|
|
6410
|
+
if (this.status === "connected" && !this.disconnecting) {
|
|
6411
|
+
this.dispatchTypedEvent("connect");
|
|
6412
|
+
}
|
|
6306
6413
|
} catch (error) {
|
|
6307
6414
|
if (!isConnectAuthRecoveryError(error)) {
|
|
6308
6415
|
this.status = "error";
|
|
@@ -7114,7 +7221,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7114
7221
|
this.getCacheableRequestOptions(options?.cacheMode)
|
|
7115
7222
|
)
|
|
7116
7223
|
);
|
|
7117
|
-
await this.refreshExcludedTools(options?.metadata).catch(() => {
|
|
7224
|
+
await this.refreshExcludedTools(options?.metadata).catch((err) => {
|
|
7225
|
+
this.logger.warn(
|
|
7226
|
+
{ err },
|
|
7227
|
+
"Excluded-tools walk failed; the SEP-2243 excluded list may be incomplete"
|
|
7228
|
+
);
|
|
7118
7229
|
});
|
|
7119
7230
|
return { tools: [...response.tools] };
|
|
7120
7231
|
}
|
|
@@ -7127,6 +7238,26 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7127
7238
|
excludesInvalidXMcpHeaderTools() {
|
|
7128
7239
|
return this.isModernEra() && this.getServerType() !== "stdio";
|
|
7129
7240
|
}
|
|
7241
|
+
/**
|
|
7242
|
+
* Mark the response that most recently answered `method` as rejected by the
|
|
7243
|
+
* client, so its Protocol entry shows why instead of rendering as a clean
|
|
7244
|
+
* success (#1953).
|
|
7245
|
+
*
|
|
7246
|
+
* The SDK gives no request id with a decode failure — `SdkError` carries the
|
|
7247
|
+
* method and nothing else — so the id is recovered by correlation: the last
|
|
7248
|
+
* response received for that method. That is exact rather than approximate,
|
|
7249
|
+
* because the SDK rejects synchronously while decoding the response (inside
|
|
7250
|
+
* the transport's `onmessage`) and the caller's `catch` runs in the very next
|
|
7251
|
+
* microtask. Delivering another response for the same method in that window
|
|
7252
|
+
* would take a macrotask (a socket read), which cannot interleave there.
|
|
7253
|
+
*
|
|
7254
|
+
* A no-op when nothing has answered `method` this session.
|
|
7255
|
+
*/
|
|
7256
|
+
markResponseRejected(method, reason) {
|
|
7257
|
+
const id = this.lastAnsweredRequestByMethod.get(method);
|
|
7258
|
+
if (id === void 0) return;
|
|
7259
|
+
this.dispatchTypedEvent("responseRejected", { id, reason });
|
|
7260
|
+
}
|
|
7130
7261
|
/** The current SEP-2243 excluded-tools set (empty on legacy/stdio). */
|
|
7131
7262
|
getExcludedTools() {
|
|
7132
7263
|
return this.excludedTools;
|
|
@@ -8394,9 +8525,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8394
8525
|
* opted-in notification type (SEP §7.4).
|
|
8395
8526
|
*/
|
|
8396
8527
|
buildSubscriptionFilter() {
|
|
8397
|
-
const filter = {
|
|
8398
|
-
|
|
8399
|
-
|
|
8528
|
+
const filter = {};
|
|
8529
|
+
if (this.subscribedResources.size > 0) {
|
|
8530
|
+
filter.resourceSubscriptions = Array.from(this.subscribedResources);
|
|
8531
|
+
}
|
|
8400
8532
|
if (this.listChangedNotifications.tools && this.capabilities?.tools?.listChanged) {
|
|
8401
8533
|
filter.toolsListChanged = true;
|
|
8402
8534
|
}
|
|
@@ -8408,6 +8540,74 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8408
8540
|
}
|
|
8409
8541
|
return filter;
|
|
8410
8542
|
}
|
|
8543
|
+
/**
|
|
8544
|
+
* Whether the modern listen stream should be open: the built filter carries
|
|
8545
|
+
* something to listen for (#1920). Before #1920 this was "at least one URI is
|
|
8546
|
+
* subscribed", which made the stream unreachable on a server with no resources
|
|
8547
|
+
* — a tools-only server advertising `tools.listChanged` had no way to open it,
|
|
8548
|
+
* so `notifications/tools/list_changed` could never arrive. The filter already
|
|
8549
|
+
* modelled the list-change opt-ins; only the trigger was narrower than the
|
|
8550
|
+
* filter it built. This matches the SDK's own `ClientOptions.listChanged`
|
|
8551
|
+
* auto-open, which opens whenever the effective (config ∩ capability)
|
|
8552
|
+
* intersection is non-empty.
|
|
8553
|
+
*
|
|
8554
|
+
* Note this is deliberately *not* the same predicate as the stream state's
|
|
8555
|
+
* `active` — see `modernStreamActive()`.
|
|
8556
|
+
*/
|
|
8557
|
+
wantsModernStream() {
|
|
8558
|
+
const filter = this.buildSubscriptionFilter();
|
|
8559
|
+
return (filter.resourceSubscriptions?.length ?? 0) > 0 || filter.toolsListChanged === true || filter.resourcesListChanged === true || filter.promptsListChanged === true;
|
|
8560
|
+
}
|
|
8561
|
+
/**
|
|
8562
|
+
* Whether the stream state reports `active` — i.e. whether the *Subscriptions*
|
|
8563
|
+
* UI has a stream to describe. That is a narrower question than
|
|
8564
|
+
* `wantsModernStream()`: `ResourceSubscriptionStreamState` drives the
|
|
8565
|
+
* Subscriptions section's badge, so a stream open purely for list-change
|
|
8566
|
+
* notifications (no subscribed URI) has nothing to report there and stays
|
|
8567
|
+
* `active: false` (#1920). Keeping the two apart also preserves the invariant
|
|
8568
|
+
* the rest of this file is written against — an empty subscribed set is never
|
|
8569
|
+
* announced alongside an `active` stream.
|
|
8570
|
+
*/
|
|
8571
|
+
modernStreamActive() {
|
|
8572
|
+
return this.subscribedResources.size > 0;
|
|
8573
|
+
}
|
|
8574
|
+
/**
|
|
8575
|
+
* Open the modern listen stream at the end of a successful connect, when the
|
|
8576
|
+
* filter is non-empty (#1920). Only the list-change opt-ins can make it
|
|
8577
|
+
* non-empty here — the subscribed set is emptied by `resetSessionState()` on
|
|
8578
|
+
* the way in — so this is exactly the "server advertises a listChanged the
|
|
8579
|
+
* Inspector wants" case that had no trigger before.
|
|
8580
|
+
*
|
|
8581
|
+
* A failure is not allowed to fail the connect: the handshake succeeded and
|
|
8582
|
+
* every request-scoped feature works without this stream. It is reported the
|
|
8583
|
+
* way a lost stream is — hand it to the reconnect machinery, which retries
|
|
8584
|
+
* with backoff and settles on `"ended"` past the cap.
|
|
8585
|
+
*
|
|
8586
|
+
* Gated on the same generation test as `subscribeToResource` — see the long
|
|
8587
|
+
* comment there. The `connect` event has deliberately *not* been dispatched
|
|
8588
|
+
* yet (that is the point of running here), so a list-state consumer is not the
|
|
8589
|
+
* risk; what is, is anything else that bumps the generation while this
|
|
8590
|
+
* `listen()` is in flight. `statusChange` has already fired, and any
|
|
8591
|
+
* concurrent call on this instance qualifies: a `subscribeToResource` from a
|
|
8592
|
+
* caller restoring subscriptions, or a `disconnect()` — whose
|
|
8593
|
+
* `resetSubscriptionStream` bumps the generation too, making a reconcile here
|
|
8594
|
+
* arm a reconnect for a session that is already gone.
|
|
8595
|
+
*/
|
|
8596
|
+
async openModernListenStreamOnConnect() {
|
|
8597
|
+
if (!this.isModernEra() || !this.wantsModernStream()) return;
|
|
8598
|
+
const generationBefore = this.modernListenGeneration;
|
|
8599
|
+
try {
|
|
8600
|
+
await this.refreshModernSubscription();
|
|
8601
|
+
} catch (error) {
|
|
8602
|
+
this.logger.error(
|
|
8603
|
+
{ error },
|
|
8604
|
+
"Failed to open the modern subscriptions/listen stream on connect"
|
|
8605
|
+
);
|
|
8606
|
+
if (this.modernListenGeneration === generationBefore + 1) {
|
|
8607
|
+
this.reconcileModernStreamStateAfterFailedRefresh();
|
|
8608
|
+
}
|
|
8609
|
+
}
|
|
8610
|
+
}
|
|
8411
8611
|
/** Cancel a pending reconnect re-listen, if any (#1630). */
|
|
8412
8612
|
clearModernReconnectTimer() {
|
|
8413
8613
|
if (this.modernReconnectTimer !== void 0) {
|
|
@@ -8417,9 +8617,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8417
8617
|
}
|
|
8418
8618
|
/**
|
|
8419
8619
|
* (Re-)establish the modern `subscriptions/listen` stream to match the current
|
|
8420
|
-
*
|
|
8421
|
-
*
|
|
8422
|
-
*
|
|
8620
|
+
* filter (#1630). Because the stream is not resumable, every filter change
|
|
8621
|
+
* re-lists: the existing stream is closed and a fresh `listen()` opened. With
|
|
8622
|
+
* an empty filter — no subscribed URIs *and* no enabled list-change opt-in the
|
|
8623
|
+
* server advertises — the stream is left closed (#1920).
|
|
8423
8624
|
*
|
|
8424
8625
|
* `modernListenGeneration` guards against races — if a newer refresh starts
|
|
8425
8626
|
* while this one awaits its acknowledgement, the just-opened stream is
|
|
@@ -8437,7 +8638,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8437
8638
|
if (previous) {
|
|
8438
8639
|
await closeSubscriptionBestEffort(previous);
|
|
8439
8640
|
}
|
|
8440
|
-
if (this.
|
|
8641
|
+
if (!this.wantsModernStream()) {
|
|
8441
8642
|
this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
|
|
8442
8643
|
return;
|
|
8443
8644
|
}
|
|
@@ -8452,7 +8653,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8452
8653
|
this.modernSubscription = subscription;
|
|
8453
8654
|
this.modernReconnectAttempts = 0;
|
|
8454
8655
|
this.setModernStreamState({
|
|
8455
|
-
active:
|
|
8656
|
+
active: this.modernStreamActive(),
|
|
8456
8657
|
status: "acknowledged",
|
|
8457
8658
|
honoredUris: subscription.honoredFilter.resourceSubscriptions ?? []
|
|
8458
8659
|
});
|
|
@@ -8483,10 +8684,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8483
8684
|
return;
|
|
8484
8685
|
}
|
|
8485
8686
|
this.modernSubscription = null;
|
|
8486
|
-
const shouldReconnect = reason === "remote" && !isTerminalStatus(this.status) && this.
|
|
8687
|
+
const shouldReconnect = reason === "remote" && !isTerminalStatus(this.status) && this.wantsModernStream();
|
|
8487
8688
|
if (!shouldReconnect) {
|
|
8488
8689
|
this.setModernStreamState({
|
|
8489
|
-
active: this.
|
|
8690
|
+
active: this.modernStreamActive(),
|
|
8490
8691
|
status: "ended",
|
|
8491
8692
|
honoredUris: []
|
|
8492
8693
|
});
|
|
@@ -8500,10 +8701,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8500
8701
|
* refresh owns the state as well as the filter — so both call sites gate on
|
|
8501
8702
|
* the generation first.
|
|
8502
8703
|
*
|
|
8503
|
-
* The
|
|
8504
|
-
*
|
|
8505
|
-
*
|
|
8506
|
-
*
|
|
8704
|
+
* The two branches are the empty and non-empty *filter* (#1920) — which is
|
|
8705
|
+
* "nothing subscribed" only when no list-change opt-in is live. The empty case
|
|
8706
|
+
* is the ordinary one: nothing to listen for, no stream, inactive. The
|
|
8707
|
+
* non-empty one exists because a failed re-listen leaves `modernSubscription`
|
|
8708
|
+
* null with the filter still wanting a stream, and nothing else will notice: the reconnect machinery is reachable only from a stream that closed
|
|
8507
8709
|
* or a reconnect that failed, and neither happened here. Left alone, the state
|
|
8508
8710
|
* keeps whatever the last success (or the optimistic `"connecting"`) wrote — a
|
|
8509
8711
|
* badge that will never change over subscriptions the server may never have
|
|
@@ -8511,17 +8713,17 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8511
8713
|
* Subscribe early-returns on the URI already being in the set).
|
|
8512
8714
|
*
|
|
8513
8715
|
* So it reconnects rather than settling for an honest-but-dead `"ended"`:
|
|
8514
|
-
* every other route to "stream gone,
|
|
8716
|
+
* every other route to "stream gone, filter live" either expects the close or
|
|
8515
8717
|
* has exhausted the retry cap, and this is the one that has made no attempt
|
|
8516
8718
|
* at all. `scheduleModernReconnect` fits as-is — a user-initiated refresh
|
|
8517
8719
|
* already reset `modernReconnectAttempts`, so it starts at the base delay; the
|
|
8518
|
-
* timer bails on a terminal status or an emptied
|
|
8720
|
+
* timer bails on a terminal status or an emptied filter; and past the cap
|
|
8519
8721
|
* `onModernReconnectFailed` lands on the same `"ended"` badge. The state
|
|
8520
8722
|
* therefore becomes true or ends after a real attempt. The caller still sees
|
|
8521
8723
|
* its error either way — the retry is about the subscriptions, not the call.
|
|
8522
8724
|
*/
|
|
8523
8725
|
reconcileModernStreamStateAfterFailedRefresh() {
|
|
8524
|
-
if (this.
|
|
8726
|
+
if (!this.wantsModernStream()) {
|
|
8525
8727
|
this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
|
|
8526
8728
|
return;
|
|
8527
8729
|
}
|
|
@@ -8535,7 +8737,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8535
8737
|
*/
|
|
8536
8738
|
scheduleModernReconnect() {
|
|
8537
8739
|
this.setModernStreamState({
|
|
8538
|
-
active:
|
|
8740
|
+
active: this.modernStreamActive(),
|
|
8539
8741
|
status: "reconnecting",
|
|
8540
8742
|
honoredUris: []
|
|
8541
8743
|
});
|
|
@@ -8546,7 +8748,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8546
8748
|
this.clearModernReconnectTimer();
|
|
8547
8749
|
this.modernReconnectTimer = setTimeout(() => {
|
|
8548
8750
|
this.modernReconnectTimer = void 0;
|
|
8549
|
-
if (isTerminalStatus(this.status) || this.
|
|
8751
|
+
if (isTerminalStatus(this.status) || !this.wantsModernStream()) {
|
|
8550
8752
|
return;
|
|
8551
8753
|
}
|
|
8552
8754
|
this.refreshModernSubscription(true).catch(
|
|
@@ -8561,9 +8763,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8561
8763
|
*/
|
|
8562
8764
|
onModernReconnectFailed() {
|
|
8563
8765
|
this.modernReconnectAttempts += 1;
|
|
8564
|
-
if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || this.
|
|
8766
|
+
if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || !this.wantsModernStream()) {
|
|
8565
8767
|
this.setModernStreamState({
|
|
8566
|
-
active: this.
|
|
8768
|
+
active: this.modernStreamActive(),
|
|
8567
8769
|
status: "ended",
|
|
8568
8770
|
honoredUris: []
|
|
8569
8771
|
});
|
|
@@ -9209,13 +9411,22 @@ async function consumeMethodOutcome(outcome, args) {
|
|
|
9209
9411
|
}
|
|
9210
9412
|
|
|
9211
9413
|
// ../../core/mcp/state/managedListState.ts
|
|
9414
|
+
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
|
|
9212
9415
|
var DEFAULT_LIST_CHANGED_DEBOUNCE_MS = 250;
|
|
9416
|
+
function isClientDecodeRejection(err) {
|
|
9417
|
+
return SdkError.isInstance(err) && (err.code === SdkErrorCode.InvalidResult || err.code === SdkErrorCode.UnsupportedResultType);
|
|
9418
|
+
}
|
|
9213
9419
|
var ManagedListState = class extends TypedEventTarget {
|
|
9214
9420
|
items = [];
|
|
9215
9421
|
client = null;
|
|
9216
9422
|
unsubscribe = null;
|
|
9217
9423
|
_metadata = void 0;
|
|
9218
9424
|
listChanged = false;
|
|
9425
|
+
// The last fetch's failure, kept as observable state so a load that fails
|
|
9426
|
+
// (a transport error, or a result the SDK codec rejects as invalid) is
|
|
9427
|
+
// rendered by the list panel instead of vanishing into an unhandled
|
|
9428
|
+
// rejection — which read as "this server has no tools" (#1953).
|
|
9429
|
+
error = null;
|
|
9219
9430
|
config;
|
|
9220
9431
|
// Debounce a burst of `list_changed` notifications into a single
|
|
9221
9432
|
// refresh (or one indicator light) once it settles.
|
|
@@ -9234,7 +9445,8 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9234
9445
|
if (this.config.deferWhenPaginated && this.client?.getServerSettings()?.paginatedLists) {
|
|
9235
9446
|
return;
|
|
9236
9447
|
}
|
|
9237
|
-
void this.refresh()
|
|
9448
|
+
void this.refresh().catch(() => {
|
|
9449
|
+
});
|
|
9238
9450
|
};
|
|
9239
9451
|
const onListChanged = () => {
|
|
9240
9452
|
if (this.listChangedTimer !== null) clearTimeout(this.listChangedTimer);
|
|
@@ -9252,6 +9464,7 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9252
9464
|
this.items = [];
|
|
9253
9465
|
this.dispatchChange();
|
|
9254
9466
|
this.setListChanged(false);
|
|
9467
|
+
this.setError(null);
|
|
9255
9468
|
}
|
|
9256
9469
|
};
|
|
9257
9470
|
this.client.addEventListener("connect", onConnect);
|
|
@@ -9293,7 +9506,8 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9293
9506
|
const settings = this.client?.getServerSettings();
|
|
9294
9507
|
const skipAggregate = this.config.deferWhenPaginated && settings?.paginatedLists;
|
|
9295
9508
|
if (!skipAggregate && settings?.autoRefreshOnListChanged) {
|
|
9296
|
-
await this.refresh(void 0, "refresh")
|
|
9509
|
+
await this.refresh(void 0, "refresh").catch(() => {
|
|
9510
|
+
});
|
|
9297
9511
|
} else if (this.config.supportsIndicator) {
|
|
9298
9512
|
this.setListChanged(true);
|
|
9299
9513
|
}
|
|
@@ -9335,6 +9549,18 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9335
9549
|
this.listChanged = value;
|
|
9336
9550
|
this.emit("listChangedChange", value);
|
|
9337
9551
|
}
|
|
9552
|
+
/** The last fetch's failure, or `null` when the last fetch succeeded. */
|
|
9553
|
+
getError() {
|
|
9554
|
+
return this.error;
|
|
9555
|
+
}
|
|
9556
|
+
// Compared by identity rather than message: two distinct failures with the
|
|
9557
|
+
// same text are still two events, and a re-render on a repeat failure is
|
|
9558
|
+
// cheap next to silently coalescing them.
|
|
9559
|
+
setError(value) {
|
|
9560
|
+
if (this.error === value) return;
|
|
9561
|
+
this.error = value;
|
|
9562
|
+
this.emit("errorChange", value);
|
|
9563
|
+
}
|
|
9338
9564
|
setMetadata(metadata) {
|
|
9339
9565
|
this._metadata = metadata;
|
|
9340
9566
|
}
|
|
@@ -9343,9 +9569,29 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9343
9569
|
* this fetch: `undefined` (the connect-time load) uses the default `'use'`;
|
|
9344
9570
|
* a user-initiated or auto refresh passes `'refresh'` to force a
|
|
9345
9571
|
* cache-bypassing round trip and re-store the fresh aggregate.
|
|
9572
|
+
*
|
|
9573
|
+
* A failure is recorded as observable state (`getError`) AND re-thrown: the
|
|
9574
|
+
* state drives the panel's error rendering, while the rejection is what the
|
|
9575
|
+
* caller's auth-recovery wrapper keys off to detect a 401 and start a
|
|
9576
|
+
* re-authorization. Callers with nobody to await them (the connect-time load,
|
|
9577
|
+
* the `list_changed` auto-refresh) catch it explicitly (#1953).
|
|
9346
9578
|
*/
|
|
9347
9579
|
async refresh(metadata, cacheMode) {
|
|
9348
|
-
|
|
9580
|
+
let next;
|
|
9581
|
+
try {
|
|
9582
|
+
next = await this.fetchItems(metadata, cacheMode);
|
|
9583
|
+
} catch (err) {
|
|
9584
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
9585
|
+
this.setError(error);
|
|
9586
|
+
if (isClientDecodeRejection(err)) {
|
|
9587
|
+
this.client?.markResponseRejected?.(
|
|
9588
|
+
this.config.listMethod,
|
|
9589
|
+
error.message
|
|
9590
|
+
);
|
|
9591
|
+
}
|
|
9592
|
+
throw err;
|
|
9593
|
+
}
|
|
9594
|
+
this.setError(null);
|
|
9349
9595
|
if (next === null) return this.getItems();
|
|
9350
9596
|
this.applyItems(next);
|
|
9351
9597
|
return this.getItems();
|
|
@@ -9384,6 +9630,7 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9384
9630
|
var ManagedToolsState = class extends ManagedListState {
|
|
9385
9631
|
constructor(client, debounceMs = DEFAULT_LIST_CHANGED_DEBOUNCE_MS) {
|
|
9386
9632
|
super(client, {
|
|
9633
|
+
listMethod: "tools/list",
|
|
9387
9634
|
changeEvent: "toolsChange",
|
|
9388
9635
|
listChangedEvent: "toolsListChanged",
|
|
9389
9636
|
capabilityKey: "tools",
|
|
@@ -9445,6 +9692,20 @@ var MessageLogState = class extends TypedEventTarget {
|
|
|
9445
9692
|
}
|
|
9446
9693
|
pushEntry(entry);
|
|
9447
9694
|
};
|
|
9695
|
+
const onResponseRejected = (event) => {
|
|
9696
|
+
const { id, reason } = event.detail;
|
|
9697
|
+
for (let i2 = this.messages.length - 1; i2 >= 0; i2--) {
|
|
9698
|
+
const entry = this.messages[i2];
|
|
9699
|
+
const entryId = entry.message.id;
|
|
9700
|
+
if (entryId !== id) continue;
|
|
9701
|
+
if (entry.direction === "notification") continue;
|
|
9702
|
+
if (entry.direction === "request" && !entry.response) continue;
|
|
9703
|
+
entry.clientError = reason;
|
|
9704
|
+
this.dispatchTypedEvent("message", entry);
|
|
9705
|
+
this.dispatchTypedEvent("messagesChange", this.getMessages());
|
|
9706
|
+
return;
|
|
9707
|
+
}
|
|
9708
|
+
};
|
|
9448
9709
|
const onStatusChange = () => {
|
|
9449
9710
|
if (isTerminalStatus(this.client?.getStatus())) {
|
|
9450
9711
|
this.messages = [];
|
|
@@ -9453,10 +9714,12 @@ var MessageLogState = class extends TypedEventTarget {
|
|
|
9453
9714
|
}
|
|
9454
9715
|
};
|
|
9455
9716
|
this.client.addEventListener("message", onMessage);
|
|
9717
|
+
this.client.addEventListener("responseRejected", onResponseRejected);
|
|
9456
9718
|
this.client.addEventListener("statusChange", onStatusChange);
|
|
9457
9719
|
this.unsubscribe = () => {
|
|
9458
9720
|
if (this.client) {
|
|
9459
9721
|
this.client.removeEventListener("message", onMessage);
|
|
9722
|
+
this.client.removeEventListener("responseRejected", onResponseRejected);
|
|
9460
9723
|
this.client.removeEventListener("statusChange", onStatusChange);
|
|
9461
9724
|
}
|
|
9462
9725
|
this.client = null;
|
|
@@ -9502,6 +9765,7 @@ var MessageLogState = class extends TypedEventTarget {
|
|
|
9502
9765
|
var ManagedResourcesState = class extends ManagedListState {
|
|
9503
9766
|
constructor(client, debounceMs = DEFAULT_LIST_CHANGED_DEBOUNCE_MS) {
|
|
9504
9767
|
super(client, {
|
|
9768
|
+
listMethod: "resources/list",
|
|
9505
9769
|
changeEvent: "resourcesChange",
|
|
9506
9770
|
listChangedEvent: "resourcesListChanged",
|
|
9507
9771
|
capabilityKey: "resources",
|
|
@@ -9523,6 +9787,7 @@ var ManagedResourcesState = class extends ManagedListState {
|
|
|
9523
9787
|
var ManagedResourceTemplatesState = class extends ManagedListState {
|
|
9524
9788
|
constructor(client, debounceMs = DEFAULT_LIST_CHANGED_DEBOUNCE_MS) {
|
|
9525
9789
|
super(client, {
|
|
9790
|
+
listMethod: "resources/templates/list",
|
|
9526
9791
|
changeEvent: "resourceTemplatesChange",
|
|
9527
9792
|
listChangedEvent: "resourceTemplatesListChanged",
|
|
9528
9793
|
// Templates are gated on the broader `resources` capability.
|
|
@@ -9550,6 +9815,7 @@ var ManagedResourceTemplatesState = class extends ManagedListState {
|
|
|
9550
9815
|
var ManagedPromptsState = class extends ManagedListState {
|
|
9551
9816
|
constructor(client, debounceMs = DEFAULT_LIST_CHANGED_DEBOUNCE_MS) {
|
|
9552
9817
|
super(client, {
|
|
9818
|
+
listMethod: "prompts/list",
|
|
9553
9819
|
changeEvent: "promptsChange",
|
|
9554
9820
|
listChangedEvent: "promptsListChanged",
|
|
9555
9821
|
capabilityKey: "prompts",
|
|
@@ -11308,8 +11574,8 @@ var C = Q.object({ method: Q.literal("ui/resource-teardown"), params: Q.object({
|
|
|
11308
11574
|
var S = Q.record(Q.string(), Q.unknown());
|
|
11309
11575
|
var F = Q.object({ text: Q.object({}).optional().describe("Host supports text content blocks."), image: Q.object({}).optional().describe("Host supports image content blocks."), audio: Q.object({}).optional().describe("Host supports audio content blocks."), resource: Q.object({}).optional().describe("Host supports resource content blocks."), resourceLink: Q.object({}).optional().describe("Host supports resource link content blocks."), structuredContent: Q.object({}).optional().describe("Host supports structured content.") });
|
|
11310
11576
|
var q = Q.object({ method: Q.literal("ui/notifications/request-teardown"), params: Q.object({}).optional() });
|
|
11311
|
-
var t = Q.object({ experimental: Q.
|
|
11312
|
-
var a = Q.object({ experimental: Q.
|
|
11577
|
+
var t = Q.object({ experimental: Q.record(Q.string(), Q.record(Q.string(), Q.any()).describe("Experimental features keyed by identifier.")).optional().describe("Experimental features keyed by identifier."), openLinks: Q.object({}).optional().describe("Host supports opening external URLs."), downloadFile: Q.object({}).optional().describe("Host supports file downloads via ui/download-file."), serverTools: Q.object({ listChanged: Q.boolean().optional().describe("Host supports tools/list_changed notifications.") }).optional().describe("Host can proxy tool calls to the MCP server."), serverResources: Q.object({ listChanged: Q.boolean().optional().describe("Host supports resources/list_changed notifications.") }).optional().describe("Host can proxy resource reads to the MCP server."), logging: Q.object({}).optional().describe("Host accepts log messages."), sandbox: Q.object({ permissions: z4.optional().describe("Permissions granted by the host (camera, microphone, geolocation)."), csp: O.optional().describe("CSP domains approved by the host.") }).optional().describe("Sandbox configuration applied by the host."), updateModelContext: F.optional().describe("Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns."), message: F.optional().describe("Host supports receiving content messages (ui/message) from the view."), sampling: Q.object({ tools: Q.object({}).optional().describe("Host supports tool use via `tools` and `toolChoice` parameters.") }).optional().describe("Host supports LLM sampling (sampling/createMessage) from the view.\nMirrors the MCP `ClientCapabilities.sampling` shape so hosts can pass it through.") });
|
|
11578
|
+
var a = Q.object({ experimental: Q.record(Q.string(), Q.record(Q.string(), Q.any()).describe("Experimental features keyed by identifier.")).optional().describe("Experimental features keyed by identifier."), tools: Q.object({ listChanged: Q.boolean().optional().describe("App supports tools/list_changed notifications.") }).optional().describe("App exposes MCP-style tools that the host can call."), availableDisplayModes: Q.array(B).optional().describe("Display modes the app supports.") });
|
|
11313
11579
|
var y = Q.object({ method: Q.literal("ui/notifications/initialized"), params: Q.object({}).optional() });
|
|
11314
11580
|
var TQ = Q.object({ csp: O.optional().describe("Content Security Policy configuration for UI resources."), permissions: z4.optional().describe("Sandbox permissions requested by the UI resource."), domain: Q.string().optional().describe(`Dedicated origin for view sandbox.
|
|
11315
11581
|
|