@modelcontextprotocol/inspector 2.0.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 +67 -10
- package/clients/cli/build/index.js +436 -72
- package/clients/tui/build/index.js +2846 -669
- package/clients/web/build/index.js +135 -13
- package/clients/web/dist/assets/index-BrCIxo3Q.js +242 -0
- package/clients/web/dist/assets/prism-light-CWdjx3zH.js +6 -0
- package/clients/web/dist/index.html +1 -1
- package/clients/web/static/sandbox_proxy.html +237 -0
- package/package.json +15 -16
- package/clients/web/dist/assets/index-wIMJi5MN.js +0 -300
- package/clients/web/dist/assets/prism-light-CVa8c8_p.js +0 -6
|
@@ -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
|
}
|
|
@@ -536,9 +596,13 @@ function loadMcpServersConfig(configPath, writable) {
|
|
|
536
596
|
return { ...config, mcpServers: normalizedServers };
|
|
537
597
|
} catch (error) {
|
|
538
598
|
if (error instanceof Error) {
|
|
539
|
-
throw new Error(`Error loading configuration: ${error.message}
|
|
599
|
+
throw new Error(`Error loading configuration: ${error.message}`, {
|
|
600
|
+
cause: error
|
|
601
|
+
});
|
|
540
602
|
}
|
|
541
|
-
throw new Error("Error loading configuration: Unknown error"
|
|
603
|
+
throw new Error("Error loading configuration: Unknown error", {
|
|
604
|
+
cause: error
|
|
605
|
+
});
|
|
542
606
|
}
|
|
543
607
|
}
|
|
544
608
|
function loadServerFromConfig(configPath, serverName, writable) {
|
|
@@ -1067,7 +1131,9 @@ function parseHttpUrl(value, label) {
|
|
|
1067
1131
|
return new URL(trimmed);
|
|
1068
1132
|
} catch (err) {
|
|
1069
1133
|
const detail = err instanceof Error ? err.message : String(err);
|
|
1070
|
-
throw new Error(`Invalid ${label}: "${trimmed}" (${detail})
|
|
1134
|
+
throw new Error(`Invalid ${label}: "${trimmed}" (${detail})`, {
|
|
1135
|
+
cause: err
|
|
1136
|
+
});
|
|
1071
1137
|
}
|
|
1072
1138
|
}
|
|
1073
1139
|
var parseOAuthCallbackParams = (location) => {
|
|
@@ -2100,6 +2166,7 @@ import { Client } from "@modelcontextprotocol/client";
|
|
|
2100
2166
|
|
|
2101
2167
|
// ../../core/json/xMcpHeader.ts
|
|
2102
2168
|
var X_MCP_HEADER_KEY = "x-mcp-header";
|
|
2169
|
+
var MCP_PARAM_HEADER_PREFIX = "Mcp-Param-";
|
|
2103
2170
|
var RFC9110_TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
2104
2171
|
var PERMITTED_X_MCP_HEADER_TYPES = /* @__PURE__ */ new Set([
|
|
2105
2172
|
"string",
|
|
@@ -2189,6 +2256,66 @@ function scanXMcpHeaderDeclarations(inputSchema) {
|
|
|
2189
2256
|
const fault = visit(inputSchema, [], true);
|
|
2190
2257
|
return fault === void 0 ? { valid: true, declarations } : { valid: false, reason: fault };
|
|
2191
2258
|
}
|
|
2259
|
+
var BASE64_SENTINEL_PREFIX = "=?base64?";
|
|
2260
|
+
var BASE64_SENTINEL_SUFFIX = "?=";
|
|
2261
|
+
function mcpParamPrimitiveToString(value) {
|
|
2262
|
+
if (typeof value === "string") return value;
|
|
2263
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
2264
|
+
if (typeof value === "number") {
|
|
2265
|
+
if (!Number.isFinite(value)) return void 0;
|
|
2266
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
2267
|
+
return void 0;
|
|
2268
|
+
}
|
|
2269
|
+
return String(value);
|
|
2270
|
+
}
|
|
2271
|
+
return void 0;
|
|
2272
|
+
}
|
|
2273
|
+
function needsBase64(s2) {
|
|
2274
|
+
if (s2.length === 0) return true;
|
|
2275
|
+
if (s2.startsWith(BASE64_SENTINEL_PREFIX) && s2.endsWith(BASE64_SENTINEL_SUFFIX)) {
|
|
2276
|
+
return true;
|
|
2277
|
+
}
|
|
2278
|
+
if (s2 !== s2.trim()) return true;
|
|
2279
|
+
for (let i2 = 0; i2 < s2.length; i2++) {
|
|
2280
|
+
const c = s2.codePointAt(i2);
|
|
2281
|
+
if (c === 9 || c >= 32 && c <= 126) continue;
|
|
2282
|
+
return true;
|
|
2283
|
+
}
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
function utf8ToBase64(s2) {
|
|
2287
|
+
const bytes = new TextEncoder().encode(s2);
|
|
2288
|
+
let bin = "";
|
|
2289
|
+
for (const b2 of bytes) bin += String.fromCodePoint(b2);
|
|
2290
|
+
return btoa(bin);
|
|
2291
|
+
}
|
|
2292
|
+
function encodeMcpParamValue(value) {
|
|
2293
|
+
return needsBase64(value) ? `${BASE64_SENTINEL_PREFIX}${utf8ToBase64(value)}${BASE64_SENTINEL_SUFFIX}` : value;
|
|
2294
|
+
}
|
|
2295
|
+
function valueAtPath(root, path2) {
|
|
2296
|
+
let node = root;
|
|
2297
|
+
for (const key of path2) {
|
|
2298
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
2299
|
+
node = node[key];
|
|
2300
|
+
}
|
|
2301
|
+
return node;
|
|
2302
|
+
}
|
|
2303
|
+
function buildMcpParamHeaders(declarations, args) {
|
|
2304
|
+
const out = {};
|
|
2305
|
+
for (const decl of declarations) {
|
|
2306
|
+
const raw = valueAtPath(args, decl.path);
|
|
2307
|
+
if (raw === void 0 || raw === null) continue;
|
|
2308
|
+
const stringValue = mcpParamPrimitiveToString(raw);
|
|
2309
|
+
if (stringValue === void 0) continue;
|
|
2310
|
+
out[`${MCP_PARAM_HEADER_PREFIX}${decl.headerName}`] = encodeMcpParamValue(stringValue);
|
|
2311
|
+
}
|
|
2312
|
+
return out;
|
|
2313
|
+
}
|
|
2314
|
+
function mcpParamHeadersForTool(tool, args) {
|
|
2315
|
+
const scan = scanXMcpHeaderDeclarations(tool.inputSchema);
|
|
2316
|
+
if (!scan.valid || scan.declarations.length === 0) return {};
|
|
2317
|
+
return buildMcpParamHeaders(scan.declarations, args);
|
|
2318
|
+
}
|
|
2192
2319
|
|
|
2193
2320
|
// ../../core/mcp/messageTrackingTransport.ts
|
|
2194
2321
|
var MessageTrackingTransport = class {
|
|
@@ -3285,7 +3412,9 @@ function getAuthorizationServerUrl(serverUrl, resourceMetadata) {
|
|
|
3285
3412
|
return new URL("/", serverUrl);
|
|
3286
3413
|
} catch (err) {
|
|
3287
3414
|
const detail = err instanceof Error ? err.message : String(err);
|
|
3288
|
-
throw new Error(`Invalid MCP server URL: "${serverUrl}" (${detail})
|
|
3415
|
+
throw new Error(`Invalid MCP server URL: "${serverUrl}" (${detail})`, {
|
|
3416
|
+
cause: err
|
|
3417
|
+
});
|
|
3289
3418
|
}
|
|
3290
3419
|
}
|
|
3291
3420
|
|
|
@@ -4560,6 +4689,7 @@ async function* parseSSE(reader) {
|
|
|
4560
4689
|
}
|
|
4561
4690
|
var RemoteClientTransport = class {
|
|
4562
4691
|
_sessionId = void 0;
|
|
4692
|
+
_protocolVersion = void 0;
|
|
4563
4693
|
eventStreamReader = null;
|
|
4564
4694
|
eventStreamAbort = null;
|
|
4565
4695
|
eventStreamConsumeTask = null;
|
|
@@ -4582,6 +4712,21 @@ var RemoteClientTransport = class {
|
|
|
4582
4712
|
getRemoteBackendSessionId() {
|
|
4583
4713
|
return this._sessionId;
|
|
4584
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
|
+
}
|
|
4585
4730
|
/**
|
|
4586
4731
|
* Reattach to an existing remote backend session after a full-page OAuth
|
|
4587
4732
|
* redirect. Opens the SSE event stream without POST /connect.
|
|
@@ -4948,6 +5093,14 @@ var RemoteClientTransport = class {
|
|
|
4948
5093
|
message,
|
|
4949
5094
|
...options?.relatedRequestId != null && {
|
|
4950
5095
|
relatedRequestId: options.relatedRequestId
|
|
5096
|
+
},
|
|
5097
|
+
// Forward per-send `Mcp-Param-*` headers (SEP-2243) for the backend to
|
|
5098
|
+
// apply to the upstream send; the browser can't set them cross-origin.
|
|
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
|
|
4951
5104
|
}
|
|
4952
5105
|
};
|
|
4953
5106
|
const res = await this.fetchFn(`${this.baseUrl}/api/mcp/send`, {
|
|
@@ -5029,6 +5182,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5029
5182
|
outputValidator = null;
|
|
5030
5183
|
transport = null;
|
|
5031
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();
|
|
5032
5192
|
/** True when the cached transport was built with an OAuth authProvider attached. */
|
|
5033
5193
|
transportHasAuthProvider = false;
|
|
5034
5194
|
/** Dedupes concurrent ambient auth challenges (reason + scopes). */
|
|
@@ -5139,7 +5299,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5139
5299
|
// `McpSubscription` whose filter's `resourceSubscriptions` mirrors
|
|
5140
5300
|
// `subscribedResources`; mutating the set re-lists (close old, open new), and
|
|
5141
5301
|
// an unexpected `"remote"` close re-lists (reconnect-by-re-listen — the stream
|
|
5142
|
-
// 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).
|
|
5143
5306
|
modernSubscription = null;
|
|
5144
5307
|
// Monotonic guard so a stale re-list/reconnect (whose `listen()` or `closed`
|
|
5145
5308
|
// resolves after a newer refresh already started) can detect it lost the race
|
|
@@ -5362,6 +5525,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5362
5525
|
createMessageTrackingCallbacks() {
|
|
5363
5526
|
return {
|
|
5364
5527
|
trackRequest: (message, origin) => {
|
|
5528
|
+
if (origin === "client") {
|
|
5529
|
+
this.outboundRequestMethods.set(message.id, message.method);
|
|
5530
|
+
}
|
|
5365
5531
|
const entry = {
|
|
5366
5532
|
id: crypto.randomUUID(),
|
|
5367
5533
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -5372,6 +5538,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5372
5538
|
this.dispatchTypedEvent("message", entry);
|
|
5373
5539
|
},
|
|
5374
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
|
+
}
|
|
5375
5549
|
const entry = {
|
|
5376
5550
|
id: crypto.randomUUID(),
|
|
5377
5551
|
timestamp: /* @__PURE__ */ new Date(),
|
|
@@ -5929,6 +6103,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
5929
6103
|
this.clearReceiverTasks();
|
|
5930
6104
|
this.resetSubscriptionStream();
|
|
5931
6105
|
this.cancelledTaskIds.clear();
|
|
6106
|
+
this.outboundRequestMethods.clear();
|
|
6107
|
+
this.lastAnsweredRequestByMethod.clear();
|
|
5932
6108
|
for (const [, controller] of this.taskInputAbortControllers) {
|
|
5933
6109
|
controller.abort(new Error("Connection ended"));
|
|
5934
6110
|
}
|
|
@@ -6152,7 +6328,6 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
6152
6328
|
this.status = "connected";
|
|
6153
6329
|
this.dispatchTypedEvent("statusChange", this.status);
|
|
6154
6330
|
await this.fetchServerInfo();
|
|
6155
|
-
this.dispatchTypedEvent("connect");
|
|
6156
6331
|
if (this.initialLoggingLevel && this.capabilities?.logging) {
|
|
6157
6332
|
await this.client.setLoggingLevel(
|
|
6158
6333
|
this.initialLoggingLevel,
|
|
@@ -6231,6 +6406,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
6231
6406
|
);
|
|
6232
6407
|
}
|
|
6233
6408
|
}
|
|
6409
|
+
await this.openModernListenStreamOnConnect();
|
|
6410
|
+
if (this.status === "connected" && !this.disconnecting) {
|
|
6411
|
+
this.dispatchTypedEvent("connect");
|
|
6412
|
+
}
|
|
6234
6413
|
} catch (error) {
|
|
6235
6414
|
if (!isConnectAuthRecoveryError(error)) {
|
|
6236
6415
|
this.status = "error";
|
|
@@ -7042,7 +7221,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7042
7221
|
this.getCacheableRequestOptions(options?.cacheMode)
|
|
7043
7222
|
)
|
|
7044
7223
|
);
|
|
7045
|
-
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
|
+
);
|
|
7046
7229
|
});
|
|
7047
7230
|
return { tools: [...response.tools] };
|
|
7048
7231
|
}
|
|
@@ -7055,6 +7238,26 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7055
7238
|
excludesInvalidXMcpHeaderTools() {
|
|
7056
7239
|
return this.isModernEra() && this.getServerType() !== "stdio";
|
|
7057
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
|
+
}
|
|
7058
7261
|
/** The current SEP-2243 excluded-tools set (empty on legacy/stdio). */
|
|
7059
7262
|
getExcludedTools() {
|
|
7060
7263
|
return this.excludedTools;
|
|
@@ -7198,6 +7401,43 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7198
7401
|
}
|
|
7199
7402
|
}
|
|
7200
7403
|
}
|
|
7404
|
+
/**
|
|
7405
|
+
* Coerce the string-valued entries of a tool's arguments to the types its
|
|
7406
|
+
* `inputSchema` declares (the Tools form hands everything over as text).
|
|
7407
|
+
* Shared by the two `tools/call` entry points — {@link attemptToolCall} and
|
|
7408
|
+
* {@link callToolStream} — so both put the SAME arguments on the wire.
|
|
7409
|
+
*/
|
|
7410
|
+
convertStringToolArgs(tool, args) {
|
|
7411
|
+
const stringArgs = {};
|
|
7412
|
+
for (const [key, value] of Object.entries(args)) {
|
|
7413
|
+
if (typeof value === "string") {
|
|
7414
|
+
stringArgs[key] = value;
|
|
7415
|
+
}
|
|
7416
|
+
}
|
|
7417
|
+
if (Object.keys(stringArgs).length === 0) return args;
|
|
7418
|
+
return { ...args, ...convertToolParameters(tool, stringArgs) };
|
|
7419
|
+
}
|
|
7420
|
+
/**
|
|
7421
|
+
* SEP-2243: mirror `x-mcp-header`-annotated arguments into `Mcp-Param-*`
|
|
7422
|
+
* headers on a modern connection. The SDK only does this inside
|
|
7423
|
+
* `client.callTool()` (and skips it in the browser), but we route
|
|
7424
|
+
* `tools/call` through `client.request()` for manual MRTR driving (#1704), so
|
|
7425
|
+
* we mirror ourselves. `Protocol.request` forwards `headers` (preserved
|
|
7426
|
+
* across MRTR retry legs) to the transport, and the remote transport relays
|
|
7427
|
+
* them to the backend's upstream send — issued server-side, where the browser
|
|
7428
|
+
* skip doesn't apply. No-op on legacy/stdio (no annotations).
|
|
7429
|
+
*
|
|
7430
|
+
* Applied by BOTH `tools/call` entry points: a plain call
|
|
7431
|
+
* ({@link attemptToolCall}) and a task-augmented one
|
|
7432
|
+
* ({@link callToolStream}) — a strict modern server rejects either with
|
|
7433
|
+
* `-32020` when the mirrored header is missing.
|
|
7434
|
+
*/
|
|
7435
|
+
applyMirroredParamHeaders(tool, convertedArgs, requestOptions) {
|
|
7436
|
+
if (this.protocolEra !== "modern") return;
|
|
7437
|
+
const paramHeaders = mcpParamHeadersForTool(tool, convertedArgs);
|
|
7438
|
+
if (Object.keys(paramHeaders).length === 0) return;
|
|
7439
|
+
requestOptions.headers = { ...requestOptions.headers, ...paramHeaders };
|
|
7440
|
+
}
|
|
7201
7441
|
/**
|
|
7202
7442
|
* Run a single tools/call attempt: convert args, issue the request, validate,
|
|
7203
7443
|
* and return a successful {@link ToolCallInvocation}. Throws on any error
|
|
@@ -7217,17 +7457,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7217
7457
|
if (!client) {
|
|
7218
7458
|
throw new Error("Client is not connected");
|
|
7219
7459
|
}
|
|
7220
|
-
|
|
7221
|
-
const stringArgs = {};
|
|
7222
|
-
for (const [key, value] of Object.entries(args)) {
|
|
7223
|
-
if (typeof value === "string") {
|
|
7224
|
-
stringArgs[key] = value;
|
|
7225
|
-
}
|
|
7226
|
-
}
|
|
7227
|
-
if (Object.keys(stringArgs).length > 0) {
|
|
7228
|
-
const convertedStringArgs = convertToolParameters(tool, stringArgs);
|
|
7229
|
-
convertedArgs = { ...args, ...convertedStringArgs };
|
|
7230
|
-
}
|
|
7460
|
+
const convertedArgs = this.convertStringToolArgs(tool, args);
|
|
7231
7461
|
const callMetadata = generalMetadata || toolSpecificMetadata ? { ...generalMetadata || {}, ...toolSpecificMetadata || {} } : void 0;
|
|
7232
7462
|
const timestamp = /* @__PURE__ */ new Date();
|
|
7233
7463
|
const metadata = this.mergeMeta(callMetadata);
|
|
@@ -7243,6 +7473,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7243
7473
|
metadata?.progressToken,
|
|
7244
7474
|
signal
|
|
7245
7475
|
);
|
|
7476
|
+
this.applyMirroredParamHeaders(tool, convertedArgs, requestOptions);
|
|
7246
7477
|
const rawResult = await this.invokeMcpClient(
|
|
7247
7478
|
() => this.requestWithInputRequired(
|
|
7248
7479
|
"tools/call",
|
|
@@ -7646,17 +7877,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7646
7877
|
throw new Error("Client is not connected");
|
|
7647
7878
|
}
|
|
7648
7879
|
try {
|
|
7649
|
-
|
|
7650
|
-
const stringArgs = {};
|
|
7651
|
-
for (const [key, value] of Object.entries(args)) {
|
|
7652
|
-
if (typeof value === "string") {
|
|
7653
|
-
stringArgs[key] = value;
|
|
7654
|
-
}
|
|
7655
|
-
}
|
|
7656
|
-
if (Object.keys(stringArgs).length > 0) {
|
|
7657
|
-
const convertedStringArgs = convertToolParameters(tool, stringArgs);
|
|
7658
|
-
convertedArgs = { ...args, ...convertedStringArgs };
|
|
7659
|
-
}
|
|
7880
|
+
const convertedArgs = this.convertStringToolArgs(tool, args);
|
|
7660
7881
|
const callMetadata = generalMetadata || toolSpecificMetadata ? { ...generalMetadata || {}, ...toolSpecificMetadata || {} } : void 0;
|
|
7661
7882
|
const timestamp = /* @__PURE__ */ new Date();
|
|
7662
7883
|
const metadata = this.mergeMeta(callMetadata);
|
|
@@ -7674,6 +7895,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7674
7895
|
let taskId;
|
|
7675
7896
|
let error;
|
|
7676
7897
|
const requestOptions = this.getRequestOptions(metadata?.progressToken);
|
|
7898
|
+
this.applyMirroredParamHeaders(tool, convertedArgs, requestOptions);
|
|
7677
7899
|
if (this.progress) {
|
|
7678
7900
|
const innerOnProgress = requestOptions.onprogress;
|
|
7679
7901
|
requestOptions.onprogress = (progress) => {
|
|
@@ -7779,7 +8001,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7779
8001
|
);
|
|
7780
8002
|
} catch (resultError) {
|
|
7781
8003
|
throw new Error(
|
|
7782
|
-
`Tool call did not return a result: ${resultError instanceof Error ? resultError.message : String(resultError)}
|
|
8004
|
+
`Tool call did not return a result: ${resultError instanceof Error ? resultError.message : String(resultError)}`,
|
|
8005
|
+
{ cause: resultError }
|
|
7783
8006
|
);
|
|
7784
8007
|
}
|
|
7785
8008
|
}
|
|
@@ -7922,7 +8145,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7922
8145
|
expandedUri = uriTemplate2.expand(params);
|
|
7923
8146
|
} catch (error) {
|
|
7924
8147
|
throw new Error(
|
|
7925
|
-
`Failed to expand URI template "${uriTemplate}": ${error instanceof Error ? error.message : String(error)}
|
|
8148
|
+
`Failed to expand URI template "${uriTemplate}": ${error instanceof Error ? error.message : String(error)}`,
|
|
8149
|
+
{ cause: error }
|
|
7926
8150
|
);
|
|
7927
8151
|
}
|
|
7928
8152
|
const readInvocation = await this.readResource(expandedUri, metadata);
|
|
@@ -8120,7 +8344,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8120
8344
|
return { values: [] };
|
|
8121
8345
|
}
|
|
8122
8346
|
throw new Error(
|
|
8123
|
-
`Failed to get completions: ${error instanceof Error ? error.message : String(error)}
|
|
8347
|
+
`Failed to get completions: ${error instanceof Error ? error.message : String(error)}`,
|
|
8348
|
+
{ cause: error }
|
|
8124
8349
|
);
|
|
8125
8350
|
}
|
|
8126
8351
|
}
|
|
@@ -8300,9 +8525,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8300
8525
|
* opted-in notification type (SEP §7.4).
|
|
8301
8526
|
*/
|
|
8302
8527
|
buildSubscriptionFilter() {
|
|
8303
|
-
const filter = {
|
|
8304
|
-
|
|
8305
|
-
|
|
8528
|
+
const filter = {};
|
|
8529
|
+
if (this.subscribedResources.size > 0) {
|
|
8530
|
+
filter.resourceSubscriptions = Array.from(this.subscribedResources);
|
|
8531
|
+
}
|
|
8306
8532
|
if (this.listChangedNotifications.tools && this.capabilities?.tools?.listChanged) {
|
|
8307
8533
|
filter.toolsListChanged = true;
|
|
8308
8534
|
}
|
|
@@ -8314,6 +8540,74 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8314
8540
|
}
|
|
8315
8541
|
return filter;
|
|
8316
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
|
+
}
|
|
8317
8611
|
/** Cancel a pending reconnect re-listen, if any (#1630). */
|
|
8318
8612
|
clearModernReconnectTimer() {
|
|
8319
8613
|
if (this.modernReconnectTimer !== void 0) {
|
|
@@ -8323,9 +8617,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8323
8617
|
}
|
|
8324
8618
|
/**
|
|
8325
8619
|
* (Re-)establish the modern `subscriptions/listen` stream to match the current
|
|
8326
|
-
*
|
|
8327
|
-
*
|
|
8328
|
-
*
|
|
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).
|
|
8329
8624
|
*
|
|
8330
8625
|
* `modernListenGeneration` guards against races — if a newer refresh starts
|
|
8331
8626
|
* while this one awaits its acknowledgement, the just-opened stream is
|
|
@@ -8343,7 +8638,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8343
8638
|
if (previous) {
|
|
8344
8639
|
await closeSubscriptionBestEffort(previous);
|
|
8345
8640
|
}
|
|
8346
|
-
if (this.
|
|
8641
|
+
if (!this.wantsModernStream()) {
|
|
8347
8642
|
this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
|
|
8348
8643
|
return;
|
|
8349
8644
|
}
|
|
@@ -8358,7 +8653,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8358
8653
|
this.modernSubscription = subscription;
|
|
8359
8654
|
this.modernReconnectAttempts = 0;
|
|
8360
8655
|
this.setModernStreamState({
|
|
8361
|
-
active:
|
|
8656
|
+
active: this.modernStreamActive(),
|
|
8362
8657
|
status: "acknowledged",
|
|
8363
8658
|
honoredUris: subscription.honoredFilter.resourceSubscriptions ?? []
|
|
8364
8659
|
});
|
|
@@ -8389,10 +8684,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8389
8684
|
return;
|
|
8390
8685
|
}
|
|
8391
8686
|
this.modernSubscription = null;
|
|
8392
|
-
const shouldReconnect = reason === "remote" && !isTerminalStatus(this.status) && this.
|
|
8687
|
+
const shouldReconnect = reason === "remote" && !isTerminalStatus(this.status) && this.wantsModernStream();
|
|
8393
8688
|
if (!shouldReconnect) {
|
|
8394
8689
|
this.setModernStreamState({
|
|
8395
|
-
active: this.
|
|
8690
|
+
active: this.modernStreamActive(),
|
|
8396
8691
|
status: "ended",
|
|
8397
8692
|
honoredUris: []
|
|
8398
8693
|
});
|
|
@@ -8406,10 +8701,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8406
8701
|
* refresh owns the state as well as the filter — so both call sites gate on
|
|
8407
8702
|
* the generation first.
|
|
8408
8703
|
*
|
|
8409
|
-
* The
|
|
8410
|
-
*
|
|
8411
|
-
*
|
|
8412
|
-
*
|
|
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
|
|
8413
8709
|
* or a reconnect that failed, and neither happened here. Left alone, the state
|
|
8414
8710
|
* keeps whatever the last success (or the optimistic `"connecting"`) wrote — a
|
|
8415
8711
|
* badge that will never change over subscriptions the server may never have
|
|
@@ -8417,17 +8713,17 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8417
8713
|
* Subscribe early-returns on the URI already being in the set).
|
|
8418
8714
|
*
|
|
8419
8715
|
* So it reconnects rather than settling for an honest-but-dead `"ended"`:
|
|
8420
|
-
* every other route to "stream gone,
|
|
8716
|
+
* every other route to "stream gone, filter live" either expects the close or
|
|
8421
8717
|
* has exhausted the retry cap, and this is the one that has made no attempt
|
|
8422
8718
|
* at all. `scheduleModernReconnect` fits as-is — a user-initiated refresh
|
|
8423
8719
|
* already reset `modernReconnectAttempts`, so it starts at the base delay; the
|
|
8424
|
-
* timer bails on a terminal status or an emptied
|
|
8720
|
+
* timer bails on a terminal status or an emptied filter; and past the cap
|
|
8425
8721
|
* `onModernReconnectFailed` lands on the same `"ended"` badge. The state
|
|
8426
8722
|
* therefore becomes true or ends after a real attempt. The caller still sees
|
|
8427
8723
|
* its error either way — the retry is about the subscriptions, not the call.
|
|
8428
8724
|
*/
|
|
8429
8725
|
reconcileModernStreamStateAfterFailedRefresh() {
|
|
8430
|
-
if (this.
|
|
8726
|
+
if (!this.wantsModernStream()) {
|
|
8431
8727
|
this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
|
|
8432
8728
|
return;
|
|
8433
8729
|
}
|
|
@@ -8441,7 +8737,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8441
8737
|
*/
|
|
8442
8738
|
scheduleModernReconnect() {
|
|
8443
8739
|
this.setModernStreamState({
|
|
8444
|
-
active:
|
|
8740
|
+
active: this.modernStreamActive(),
|
|
8445
8741
|
status: "reconnecting",
|
|
8446
8742
|
honoredUris: []
|
|
8447
8743
|
});
|
|
@@ -8452,7 +8748,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8452
8748
|
this.clearModernReconnectTimer();
|
|
8453
8749
|
this.modernReconnectTimer = setTimeout(() => {
|
|
8454
8750
|
this.modernReconnectTimer = void 0;
|
|
8455
|
-
if (isTerminalStatus(this.status) || this.
|
|
8751
|
+
if (isTerminalStatus(this.status) || !this.wantsModernStream()) {
|
|
8456
8752
|
return;
|
|
8457
8753
|
}
|
|
8458
8754
|
this.refreshModernSubscription(true).catch(
|
|
@@ -8467,9 +8763,9 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8467
8763
|
*/
|
|
8468
8764
|
onModernReconnectFailed() {
|
|
8469
8765
|
this.modernReconnectAttempts += 1;
|
|
8470
|
-
if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || this.
|
|
8766
|
+
if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || !this.wantsModernStream()) {
|
|
8471
8767
|
this.setModernStreamState({
|
|
8472
|
-
active: this.
|
|
8768
|
+
active: this.modernStreamActive(),
|
|
8473
8769
|
status: "ended",
|
|
8474
8770
|
honoredUris: []
|
|
8475
8771
|
});
|
|
@@ -8522,7 +8818,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8522
8818
|
this.dispatchSubscriptionsChange();
|
|
8523
8819
|
} catch (error) {
|
|
8524
8820
|
throw new Error(
|
|
8525
|
-
`Failed to subscribe to resource: ${error instanceof Error ? error.message : String(error)}
|
|
8821
|
+
`Failed to subscribe to resource: ${error instanceof Error ? error.message : String(error)}`,
|
|
8822
|
+
{ cause: error }
|
|
8526
8823
|
);
|
|
8527
8824
|
}
|
|
8528
8825
|
}
|
|
@@ -8566,7 +8863,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8566
8863
|
}
|
|
8567
8864
|
} catch (error) {
|
|
8568
8865
|
throw new Error(
|
|
8569
|
-
`Failed to unsubscribe from resource: ${error instanceof Error ? error.message : String(error)}
|
|
8866
|
+
`Failed to unsubscribe from resource: ${error instanceof Error ? error.message : String(error)}`,
|
|
8867
|
+
{ cause: error }
|
|
8570
8868
|
);
|
|
8571
8869
|
}
|
|
8572
8870
|
}
|
|
@@ -9113,13 +9411,22 @@ async function consumeMethodOutcome(outcome, args) {
|
|
|
9113
9411
|
}
|
|
9114
9412
|
|
|
9115
9413
|
// ../../core/mcp/state/managedListState.ts
|
|
9414
|
+
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
|
|
9116
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
|
+
}
|
|
9117
9419
|
var ManagedListState = class extends TypedEventTarget {
|
|
9118
9420
|
items = [];
|
|
9119
9421
|
client = null;
|
|
9120
9422
|
unsubscribe = null;
|
|
9121
9423
|
_metadata = void 0;
|
|
9122
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;
|
|
9123
9430
|
config;
|
|
9124
9431
|
// Debounce a burst of `list_changed` notifications into a single
|
|
9125
9432
|
// refresh (or one indicator light) once it settles.
|
|
@@ -9138,7 +9445,8 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9138
9445
|
if (this.config.deferWhenPaginated && this.client?.getServerSettings()?.paginatedLists) {
|
|
9139
9446
|
return;
|
|
9140
9447
|
}
|
|
9141
|
-
void this.refresh()
|
|
9448
|
+
void this.refresh().catch(() => {
|
|
9449
|
+
});
|
|
9142
9450
|
};
|
|
9143
9451
|
const onListChanged = () => {
|
|
9144
9452
|
if (this.listChangedTimer !== null) clearTimeout(this.listChangedTimer);
|
|
@@ -9156,6 +9464,7 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9156
9464
|
this.items = [];
|
|
9157
9465
|
this.dispatchChange();
|
|
9158
9466
|
this.setListChanged(false);
|
|
9467
|
+
this.setError(null);
|
|
9159
9468
|
}
|
|
9160
9469
|
};
|
|
9161
9470
|
this.client.addEventListener("connect", onConnect);
|
|
@@ -9197,7 +9506,8 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9197
9506
|
const settings = this.client?.getServerSettings();
|
|
9198
9507
|
const skipAggregate = this.config.deferWhenPaginated && settings?.paginatedLists;
|
|
9199
9508
|
if (!skipAggregate && settings?.autoRefreshOnListChanged) {
|
|
9200
|
-
await this.refresh(void 0, "refresh")
|
|
9509
|
+
await this.refresh(void 0, "refresh").catch(() => {
|
|
9510
|
+
});
|
|
9201
9511
|
} else if (this.config.supportsIndicator) {
|
|
9202
9512
|
this.setListChanged(true);
|
|
9203
9513
|
}
|
|
@@ -9239,6 +9549,18 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9239
9549
|
this.listChanged = value;
|
|
9240
9550
|
this.emit("listChangedChange", value);
|
|
9241
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
|
+
}
|
|
9242
9564
|
setMetadata(metadata) {
|
|
9243
9565
|
this._metadata = metadata;
|
|
9244
9566
|
}
|
|
@@ -9247,9 +9569,29 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9247
9569
|
* this fetch: `undefined` (the connect-time load) uses the default `'use'`;
|
|
9248
9570
|
* a user-initiated or auto refresh passes `'refresh'` to force a
|
|
9249
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).
|
|
9250
9578
|
*/
|
|
9251
9579
|
async refresh(metadata, cacheMode) {
|
|
9252
|
-
|
|
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);
|
|
9253
9595
|
if (next === null) return this.getItems();
|
|
9254
9596
|
this.applyItems(next);
|
|
9255
9597
|
return this.getItems();
|
|
@@ -9288,6 +9630,7 @@ var ManagedListState = class extends TypedEventTarget {
|
|
|
9288
9630
|
var ManagedToolsState = class extends ManagedListState {
|
|
9289
9631
|
constructor(client, debounceMs = DEFAULT_LIST_CHANGED_DEBOUNCE_MS) {
|
|
9290
9632
|
super(client, {
|
|
9633
|
+
listMethod: "tools/list",
|
|
9291
9634
|
changeEvent: "toolsChange",
|
|
9292
9635
|
listChangedEvent: "toolsListChanged",
|
|
9293
9636
|
capabilityKey: "tools",
|
|
@@ -9349,6 +9692,20 @@ var MessageLogState = class extends TypedEventTarget {
|
|
|
9349
9692
|
}
|
|
9350
9693
|
pushEntry(entry);
|
|
9351
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
|
+
};
|
|
9352
9709
|
const onStatusChange = () => {
|
|
9353
9710
|
if (isTerminalStatus(this.client?.getStatus())) {
|
|
9354
9711
|
this.messages = [];
|
|
@@ -9357,10 +9714,12 @@ var MessageLogState = class extends TypedEventTarget {
|
|
|
9357
9714
|
}
|
|
9358
9715
|
};
|
|
9359
9716
|
this.client.addEventListener("message", onMessage);
|
|
9717
|
+
this.client.addEventListener("responseRejected", onResponseRejected);
|
|
9360
9718
|
this.client.addEventListener("statusChange", onStatusChange);
|
|
9361
9719
|
this.unsubscribe = () => {
|
|
9362
9720
|
if (this.client) {
|
|
9363
9721
|
this.client.removeEventListener("message", onMessage);
|
|
9722
|
+
this.client.removeEventListener("responseRejected", onResponseRejected);
|
|
9364
9723
|
this.client.removeEventListener("statusChange", onStatusChange);
|
|
9365
9724
|
}
|
|
9366
9725
|
this.client = null;
|
|
@@ -9406,6 +9765,7 @@ var MessageLogState = class extends TypedEventTarget {
|
|
|
9406
9765
|
var ManagedResourcesState = class extends ManagedListState {
|
|
9407
9766
|
constructor(client, debounceMs = DEFAULT_LIST_CHANGED_DEBOUNCE_MS) {
|
|
9408
9767
|
super(client, {
|
|
9768
|
+
listMethod: "resources/list",
|
|
9409
9769
|
changeEvent: "resourcesChange",
|
|
9410
9770
|
listChangedEvent: "resourcesListChanged",
|
|
9411
9771
|
capabilityKey: "resources",
|
|
@@ -9427,6 +9787,7 @@ var ManagedResourcesState = class extends ManagedListState {
|
|
|
9427
9787
|
var ManagedResourceTemplatesState = class extends ManagedListState {
|
|
9428
9788
|
constructor(client, debounceMs = DEFAULT_LIST_CHANGED_DEBOUNCE_MS) {
|
|
9429
9789
|
super(client, {
|
|
9790
|
+
listMethod: "resources/templates/list",
|
|
9430
9791
|
changeEvent: "resourceTemplatesChange",
|
|
9431
9792
|
listChangedEvent: "resourceTemplatesListChanged",
|
|
9432
9793
|
// Templates are gated on the broader `resources` capability.
|
|
@@ -9454,6 +9815,7 @@ var ManagedResourceTemplatesState = class extends ManagedListState {
|
|
|
9454
9815
|
var ManagedPromptsState = class extends ManagedListState {
|
|
9455
9816
|
constructor(client, debounceMs = DEFAULT_LIST_CHANGED_DEBOUNCE_MS) {
|
|
9456
9817
|
super(client, {
|
|
9818
|
+
listMethod: "prompts/list",
|
|
9457
9819
|
changeEvent: "promptsChange",
|
|
9458
9820
|
listChangedEvent: "promptsListChanged",
|
|
9459
9821
|
capabilityKey: "prompts",
|
|
@@ -11212,8 +11574,8 @@ var C = Q.object({ method: Q.literal("ui/resource-teardown"), params: Q.object({
|
|
|
11212
11574
|
var S = Q.record(Q.string(), Q.unknown());
|
|
11213
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.") });
|
|
11214
11576
|
var q = Q.object({ method: Q.literal("ui/notifications/request-teardown"), params: Q.object({}).optional() });
|
|
11215
|
-
var t = Q.object({ experimental: Q.
|
|
11216
|
-
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.") });
|
|
11217
11579
|
var y = Q.object({ method: Q.literal("ui/notifications/initialized"), params: Q.object({}).optional() });
|
|
11218
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.
|
|
11219
11581
|
|
|
@@ -11567,7 +11929,8 @@ async function runMethod(inspectorClient, args) {
|
|
|
11567
11929
|
roots = parsed;
|
|
11568
11930
|
} catch (e2) {
|
|
11569
11931
|
throw new Error(
|
|
11570
|
-
`--roots-json is invalid: ${e2 instanceof Error ? e2.message : String(e2)}
|
|
11932
|
+
`--roots-json is invalid: ${e2 instanceof Error ? e2.message : String(e2)}`,
|
|
11933
|
+
{ cause: e2 }
|
|
11571
11934
|
);
|
|
11572
11935
|
}
|
|
11573
11936
|
await inspectorClient.setRoots(roots);
|
|
@@ -11846,7 +12209,7 @@ function parseRunnerOAuthCallbackUrl(cliCallbackUrl) {
|
|
|
11846
12209
|
url = new URL(raw);
|
|
11847
12210
|
} catch (err) {
|
|
11848
12211
|
const reason = err?.message ?? String(err);
|
|
11849
|
-
throw new Error(`Invalid OAuth callback URL: ${reason}
|
|
12212
|
+
throw new Error(`Invalid OAuth callback URL: ${reason}`, { cause: err });
|
|
11850
12213
|
}
|
|
11851
12214
|
if (url.protocol !== "http:") {
|
|
11852
12215
|
throw new Error("OAuth callback URL must use http scheme");
|
|
@@ -12810,7 +13173,7 @@ async function parseArgs(argv) {
|
|
|
12810
13173
|
const scriptArgs = rawArgs.slice(2);
|
|
12811
13174
|
const dashDashIndex = scriptArgs.indexOf("--");
|
|
12812
13175
|
let targetArgs = [];
|
|
12813
|
-
let optionArgs
|
|
13176
|
+
let optionArgs;
|
|
12814
13177
|
if (dashDashIndex >= 0) {
|
|
12815
13178
|
targetArgs = scriptArgs.slice(0, dashDashIndex);
|
|
12816
13179
|
optionArgs = scriptArgs.slice(dashDashIndex + 1);
|
|
@@ -13120,7 +13483,8 @@ async function parseArgs(argv) {
|
|
|
13120
13483
|
parsed = JSON.parse(options.toolArgsJson);
|
|
13121
13484
|
} catch (e2) {
|
|
13122
13485
|
throw new Error(
|
|
13123
|
-
`--tool-args-json is not valid JSON: ${e2.message}
|
|
13486
|
+
`--tool-args-json is not valid JSON: ${e2 instanceof Error ? e2.message : String(e2)}`,
|
|
13487
|
+
{ cause: e2 }
|
|
13124
13488
|
);
|
|
13125
13489
|
}
|
|
13126
13490
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|