@modelcontextprotocol/inspector 2.2.0 → 2.3.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.
@@ -233,6 +233,60 @@ function cleanRoots(roots) {
233
233
  function envRecordToPairs(env2) {
234
234
  return env2 ? Object.entries(env2).map(([key, value]) => ({ key, value })) : [];
235
235
  }
236
+ function envPairsToRecord(pairs) {
237
+ const out = {};
238
+ for (const { key, value } of pairs) {
239
+ if (key.trim() === "") continue;
240
+ out[key] = value;
241
+ }
242
+ return out;
243
+ }
244
+ function cleanAuthorizationParams(params) {
245
+ if (params === void 0) return void 0;
246
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
247
+ console.warn(
248
+ "Ignoring `oauth.authorizationParams`: expected an object of string values, got",
249
+ Array.isArray(params) ? "array" : typeof params
250
+ );
251
+ return void 0;
252
+ }
253
+ const out = {};
254
+ for (const [key, value] of Object.entries(params)) {
255
+ if (typeof value !== "string") {
256
+ console.warn(
257
+ `Dropping \`oauth.authorizationParams.${key}\`: expected a string value, got ${typeof value}.`
258
+ );
259
+ continue;
260
+ }
261
+ if (key.trim() === "") continue;
262
+ out[key] = value;
263
+ }
264
+ return Object.keys(out).length > 0 ? out : void 0;
265
+ }
266
+ function oauthAuthorizationParamsFromSettings(settings) {
267
+ if (!settings.oauthAuthorizationParams) return void 0;
268
+ const record = envPairsToRecord(settings.oauthAuthorizationParams);
269
+ return Object.keys(record).length > 0 ? record : void 0;
270
+ }
271
+ function cleanEndpointOverride(value, field) {
272
+ if (value === void 0) return void 0;
273
+ if (typeof value !== "string") {
274
+ console.warn(
275
+ `Ignoring \`oauth.${field}\`: expected a string, got ${typeof value}.`
276
+ );
277
+ return void 0;
278
+ }
279
+ return value.trim() || void 0;
280
+ }
281
+ function oauthEndpointOverridesFromSettings(settings) {
282
+ const authorizationUrl = settings.oauthAuthorizationUrl?.trim();
283
+ const tokenUrl = settings.oauthTokenUrl?.trim();
284
+ if (!authorizationUrl && !tokenUrl) return void 0;
285
+ return {
286
+ ...authorizationUrl && { authorizationUrl },
287
+ ...tokenUrl && { tokenUrl }
288
+ };
289
+ }
236
290
  function storedFieldsToInspectorSettings(stored) {
237
291
  const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
238
292
  if (!hasAny) return void 0;
@@ -270,6 +324,19 @@ function storedFieldsToInspectorSettings(stored) {
270
324
  if (stored.oauth?.clientSecret)
271
325
  settings.oauthClientSecret = stored.oauth.clientSecret;
272
326
  if (stored.oauth?.scopes) settings.oauthScopes = stored.oauth.scopes;
327
+ const authorizationParams = cleanAuthorizationParams(
328
+ stored.oauth?.authorizationParams
329
+ );
330
+ if (authorizationParams) {
331
+ settings.oauthAuthorizationParams = envRecordToPairs(authorizationParams);
332
+ }
333
+ const authorizationUrl = cleanEndpointOverride(
334
+ stored.oauth?.authorizationUrl,
335
+ "authorizationUrl"
336
+ );
337
+ if (authorizationUrl) settings.oauthAuthorizationUrl = authorizationUrl;
338
+ const tokenUrl = cleanEndpointOverride(stored.oauth?.tokenUrl, "tokenUrl");
339
+ if (tokenUrl) settings.oauthTokenUrl = tokenUrl;
273
340
  if (stored.oauth?.onInsufficientScope) {
274
341
  settings.oauthOnInsufficientScope = stored.oauth.onInsufficientScope;
275
342
  }
@@ -1369,7 +1436,7 @@ function withProxyDispatcher(baseFetch) {
1369
1436
  if (readProxyEnv() === void 0) return baseFetch;
1370
1437
  let dispatcherPromise;
1371
1438
  const getDispatcher = () => {
1372
- dispatcherPromise ??= import("./undici-HXPKCIY3.js").then(
1439
+ dispatcherPromise ??= import("./undici-KNXAVD2F.js").then(
1373
1440
  ({ EnvHttpProxyAgent }) => new EnvHttpProxyAgent(),
1374
1441
  (cause) => {
1375
1442
  throw new Error(
@@ -1713,7 +1780,9 @@ function isOAuthCapableServerConfig(config) {
1713
1780
  function buildRunnerClientAuthOptions(clientConfig, savedSettings, cliOverrides) {
1714
1781
  const activeIdp = getActiveEnterpriseManagedAuthIdp(clientConfig);
1715
1782
  const activeCimdUrl = getActiveCimdClientMetadataUrl(clientConfig);
1716
- const oauthFromServer = savedSettings && (savedSettings.oauthClientId || savedSettings.oauthClientSecret || savedSettings.oauthScopes || savedSettings.enterpriseManaged) ? {
1783
+ const serverAuthorizationParams = savedSettings ? oauthAuthorizationParamsFromSettings(savedSettings) : void 0;
1784
+ const serverEndpointOverrides = savedSettings ? oauthEndpointOverridesFromSettings(savedSettings) : void 0;
1785
+ const oauthFromServer = savedSettings && (savedSettings.oauthClientId || savedSettings.oauthClientSecret || savedSettings.oauthScopes || serverAuthorizationParams || serverEndpointOverrides || savedSettings.enterpriseManaged) ? {
1717
1786
  ...savedSettings.oauthClientId && {
1718
1787
  clientId: savedSettings.oauthClientId
1719
1788
  },
@@ -1723,6 +1792,10 @@ function buildRunnerClientAuthOptions(clientConfig, savedSettings, cliOverrides)
1723
1792
  ...savedSettings.oauthScopes && {
1724
1793
  scope: savedSettings.oauthScopes
1725
1794
  },
1795
+ ...serverAuthorizationParams && {
1796
+ authorizationParams: serverAuthorizationParams
1797
+ },
1798
+ ...serverEndpointOverrides,
1726
1799
  ...savedSettings.enterpriseManaged && {
1727
1800
  enterpriseManaged: true
1728
1801
  }
@@ -2220,7 +2293,10 @@ import {
2220
2293
  ListToolsResultSchema,
2221
2294
  ListResourcesResultSchema,
2222
2295
  ListResourceTemplatesResultSchema,
2223
- ListPromptsResultSchema
2296
+ ListPromptsResultSchema,
2297
+ ResourceSchema,
2298
+ ResourceTemplateSchema,
2299
+ PromptSchema
2224
2300
  } from "@modelcontextprotocol/core";
2225
2301
  import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
2226
2302
 
@@ -2240,16 +2316,367 @@ function validateToolOutput(provider, tool, result) {
2240
2316
  }
2241
2317
  }
2242
2318
 
2319
+ // ../../core/mcp/listSalvage.ts
2320
+ import { z as z3 } from "zod/v4";
2321
+ import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
2322
+ import { ToolSchema } from "@modelcontextprotocol/core";
2323
+ var LIST_MAX_PAGES = 64;
2324
+ function listPaginationExceeded(method) {
2325
+ return new Error(
2326
+ `Salvage walk for ${method} exceeded ${LIST_MAX_PAGES} pages without the server's pagination converging`
2327
+ );
2328
+ }
2329
+ function isClientDecodeRejection(err) {
2330
+ return SdkError.isInstance(err) && (err.code === SdkErrorCode.InvalidResult || err.code === SdkErrorCode.UnsupportedResultType);
2331
+ }
2332
+ function isSalvageableRejection(err) {
2333
+ return SdkError.isInstance(err) && err.code === SdkErrorCode.InvalidResult;
2334
+ }
2335
+ function lenientListPageSchema(resultSchema, itemsKey) {
2336
+ return resultSchema.extend({ [itemsKey]: z3.array(z3.unknown()) });
2337
+ }
2338
+ function nextCursorOf(page) {
2339
+ if (typeof page !== "object" || page === null) return void 0;
2340
+ const value = page.nextCursor;
2341
+ return typeof value === "string" ? value : void 0;
2342
+ }
2343
+ var ModernResultEnvelopeSchema = z3.looseObject({
2344
+ resultType: z3.literal("complete"),
2345
+ // A non-negative INTEGER, matching the codec: `ttlMs: -1` or `0.5` is an
2346
+ // envelope violation the strict path rejects, and accepting it here on the
2347
+ // strength of an unrelated bad entry would hide it.
2348
+ ttlMs: z3.int().min(0),
2349
+ cacheScope: z3.enum(["public", "private"])
2350
+ });
2351
+ function rawItemsOf(page, itemsKey) {
2352
+ if (typeof page !== "object" || page === null) return void 0;
2353
+ const value = page[itemsKey];
2354
+ return Array.isArray(value) ? value : void 0;
2355
+ }
2356
+ function labelForRawItem(value) {
2357
+ if (typeof value !== "object" || value === null) return void 0;
2358
+ const record = value;
2359
+ for (const key of ["name", "uriTemplate", "uri", "title"]) {
2360
+ const candidate = record[key];
2361
+ if (typeof candidate === "string" && candidate.length > 0) return candidate;
2362
+ }
2363
+ return void 0;
2364
+ }
2365
+ function describeIssues(error) {
2366
+ const [issue] = error.issues;
2367
+ if (!issue) return "did not match the expected shape";
2368
+ const path3 = issue.path.join(".");
2369
+ const detail = expectedDetail(issue);
2370
+ const message = detail ? `${issue.message} (${detail})` : issue.message;
2371
+ return path3.length > 0 ? `${path3}: ${message}` : message;
2372
+ }
2373
+ function expectedDetail(issue) {
2374
+ if (!("expected" in issue) || typeof issue.expected !== "string") {
2375
+ return void 0;
2376
+ }
2377
+ const detail = `expected ${issue.expected}`;
2378
+ return issue.message.includes(detail) ? void 0 : detail;
2379
+ }
2380
+ function salvageListItems({
2381
+ method,
2382
+ items,
2383
+ schema,
2384
+ startIndex = 0
2385
+ }) {
2386
+ const valid = [];
2387
+ const malformed = [];
2388
+ items.forEach((item, offset) => {
2389
+ const parsed = schema.safeParse(item);
2390
+ if (parsed.success) {
2391
+ valid.push(parsed.data);
2392
+ return;
2393
+ }
2394
+ const label = labelForRawItem(item);
2395
+ malformed.push({
2396
+ method,
2397
+ index: startIndex + offset,
2398
+ ...label !== void 0 && { label },
2399
+ reason: describeIssues(parsed.error)
2400
+ });
2401
+ });
2402
+ return { valid, malformed };
2403
+ }
2404
+ function summarizeMalformed(malformed) {
2405
+ const [first] = malformed;
2406
+ if (!first) return "";
2407
+ const where = first.label ?? `index ${first.index}`;
2408
+ const rest = malformed.length > 1 ? ` (+${malformed.length - 1} more)` : "";
2409
+ return `Dropped ${malformed.length} malformed ${malformed.length === 1 ? "entry" : "entries"} \u2014 ${where}: ${first.reason}${rest}`;
2410
+ }
2411
+ function toolItemSchemaForEra(isModern) {
2412
+ return isModern ? eraToolItemSchema(ModernToolWireSchema, stripExecution) : eraToolItemSchema(LegacyToolWireSchema, (tool) => tool);
2413
+ }
2414
+ var LegacyToolWireSchema = ToolSchema.extend({
2415
+ outputSchema: ToolSchema.shape.inputSchema.optional()
2416
+ });
2417
+ var ModernToolWireSchema = ToolSchema.extend({
2418
+ inputSchema: z3.looseObject({
2419
+ type: z3.literal("object"),
2420
+ $schema: z3.string().optional()
2421
+ })
2422
+ });
2423
+ function stripExecution(tool) {
2424
+ if (tool.execution === void 0) return tool;
2425
+ const { execution: _execution, ...rest } = tool;
2426
+ return rest;
2427
+ }
2428
+ function eraToolItemSchema(wire, normalize) {
2429
+ return wire.transform((parsed) => normalize(parsed));
2430
+ }
2431
+
2243
2432
  // ../../core/mcp/taskNotificationSchemas.ts
2244
2433
  import { NotificationSchema } from "@modelcontextprotocol/core";
2245
- import { z as z3 } from "zod/v4";
2434
+ import { z as z4 } from "zod/v4";
2246
2435
  var TasksListChangedNotificationSchema = NotificationSchema.extend({
2247
- method: z3.literal("notifications/tasks/list_changed"),
2248
- params: z3.record(z3.string(), z3.unknown()).optional()
2436
+ method: z4.literal("notifications/tasks/list_changed"),
2437
+ params: z4.record(z4.string(), z4.unknown()).optional()
2249
2438
  });
2250
2439
 
2251
- // ../../core/mcp/inspectorClient.ts
2440
+ // ../../core/mcp/uriTemplate.ts
2252
2441
  import { UriTemplate } from "@modelcontextprotocol/client";
2442
+ var OPERATORS = ["+", "#", ".", "/", ";", "?", "&"];
2443
+ var OMITTABLE_OPERATORS = /* @__PURE__ */ new Set(["#", ".", "/", ";", "?", "&"]);
2444
+ var NAMED_OPERATORS = /* @__PURE__ */ new Set([";", "?", "&"]);
2445
+ var VARCHAR = String.raw`(?:[A-Za-z0-9_]|%[0-9A-Fa-f]{2})`;
2446
+ var TOLERATED_VARCHAR = String.raw`[-~]`;
2447
+ function varnameGrammar(varchar) {
2448
+ return `${varchar}(?:\\.?${varchar})*`;
2449
+ }
2450
+ var MODIFIER = String.raw`(?:\*|:([1-9][0-9]{0,3}))?`;
2451
+ var CONFORMING_VARSPEC = new RegExp(
2452
+ `^(${varnameGrammar(VARCHAR)})${MODIFIER}$`
2453
+ );
2454
+ var TOLERATED_VARSPEC = new RegExp(
2455
+ `^(${varnameGrammar(`(?:${VARCHAR}|${TOLERATED_VARCHAR})`)})${MODIFIER}$`
2456
+ );
2457
+ function parseVarSpec(raw) {
2458
+ const conforming = CONFORMING_VARSPEC.exec(raw);
2459
+ const match = conforming ?? TOLERATED_VARSPEC.exec(raw);
2460
+ if (match === null) return "invalid";
2461
+ const [, name, maxLength] = match;
2462
+ return {
2463
+ name,
2464
+ ...maxLength === void 0 ? {} : { maxLength: +maxLength },
2465
+ ...conforming === null ? { conforming: false } : {}
2466
+ };
2467
+ }
2468
+ function parseUriTemplate(uriTemplate) {
2469
+ const parts = [];
2470
+ let literal = "";
2471
+ let i = 0;
2472
+ while (i < uriTemplate.length) {
2473
+ if (uriTemplate[i] !== "{") {
2474
+ literal += uriTemplate[i];
2475
+ i += 1;
2476
+ continue;
2477
+ }
2478
+ const end = uriTemplate.indexOf("}", i);
2479
+ if (end === -1) {
2480
+ literal += uriTemplate.slice(i);
2481
+ break;
2482
+ }
2483
+ if (literal) {
2484
+ parts.push({ kind: "literal", text: literal });
2485
+ literal = "";
2486
+ }
2487
+ const body = uriTemplate.slice(i + 1, end);
2488
+ const operator = OPERATORS.find((op) => body.startsWith(op)) ?? "";
2489
+ const parsed = body.slice(operator.length).split(",").map(parseVarSpec);
2490
+ const varspecs = parsed.filter(
2491
+ (spec) => spec !== "invalid"
2492
+ );
2493
+ parts.push({
2494
+ kind: "expression",
2495
+ source: uriTemplate.slice(i, end + 1),
2496
+ operator,
2497
+ varspecs,
2498
+ names: varspecs.map((spec) => spec.name),
2499
+ invalid: parsed.includes("invalid")
2500
+ });
2501
+ i = end + 1;
2502
+ }
2503
+ if (literal) parts.push({ kind: "literal", text: literal });
2504
+ return parts;
2505
+ }
2506
+ function templateVariables(uriTemplate) {
2507
+ const byName = /* @__PURE__ */ new Map();
2508
+ for (const part of parseUriTemplate(uriTemplate)) {
2509
+ if (part.kind !== "expression") continue;
2510
+ const required = !OMITTABLE_OPERATORS.has(part.operator);
2511
+ for (const spec of part.varspecs) {
2512
+ const { name } = spec;
2513
+ const conforming = spec.conforming !== false;
2514
+ const existing = byName.get(name);
2515
+ if (existing) {
2516
+ existing.required = existing.required || required;
2517
+ existing.conforming = existing.conforming && conforming;
2518
+ } else {
2519
+ byName.set(name, {
2520
+ name,
2521
+ operator: part.operator,
2522
+ required,
2523
+ conforming
2524
+ });
2525
+ }
2526
+ }
2527
+ }
2528
+ return [...byName.values()];
2529
+ }
2530
+ function requiredGroups(uriTemplate) {
2531
+ const groups = [];
2532
+ for (const part of parseUriTemplate(uriTemplate)) {
2533
+ if (part.kind !== "expression") continue;
2534
+ if (OMITTABLE_OPERATORS.has(part.operator)) continue;
2535
+ if (part.names.length === 0) continue;
2536
+ groups.push([...new Set(part.names)]);
2537
+ }
2538
+ return groups;
2539
+ }
2540
+ function unmetRequiredGroups(groups, values) {
2541
+ return groups.filter(
2542
+ (names) => !names.some((name) => (readValue(values, name) ?? "").length > 0)
2543
+ );
2544
+ }
2545
+ function readValue(values, name) {
2546
+ return Object.hasOwn(values, name) ? values[name] : void 0;
2547
+ }
2548
+ function definedValues(values) {
2549
+ return Object.fromEntries(
2550
+ Object.entries(values).filter(([, value]) => value.length > 0)
2551
+ );
2552
+ }
2553
+ var ALLOW_RESERVED = /[^A-Za-z0-9\-._~:/?#[\]@!$&'()*+,;=]/gu;
2554
+ function encodeLiteral(text) {
2555
+ return text.split(/([{}])/g).map(
2556
+ (chunk, index) => index % 2 === 1 ? chunk : encodeAllowReserved(chunk)
2557
+ ).join("");
2558
+ }
2559
+ function encodeAllowReserved(value) {
2560
+ return value.split(/(%[0-9A-Fa-f]{2})/g).map(
2561
+ (chunk, index) => index % 2 === 1 ? chunk : chunk.replace(ALLOW_RESERVED, (char) => encodeURIComponent(char))
2562
+ ).join("");
2563
+ }
2564
+ var FORCE_ENCODED = {
2565
+ "!": "%21",
2566
+ "'": "%27",
2567
+ "(": "%28",
2568
+ ")": "%29",
2569
+ "*": "%2A"
2570
+ };
2571
+ function encodeUnreserved(value) {
2572
+ return encodeURIComponent(value).replace(
2573
+ /[!'()*]/g,
2574
+ (char) => FORCE_ENCODED[char]
2575
+ );
2576
+ }
2577
+ function allowsReserved(operator) {
2578
+ return operator === "+" || operator === "#";
2579
+ }
2580
+ function encodeValue(value, operator) {
2581
+ return allowsReserved(operator) ? encodeAllowReserved(value) : encodeUnreserved(value);
2582
+ }
2583
+ var CONTINUATION = String.raw`%[89AB][0-9A-F]`;
2584
+ var UTF8_SEQUENCE = [
2585
+ String.raw`(?:%F0%(?:9[0-9A-F]|[AB][0-9A-F])|%F[1-3]${CONTINUATION}|%F4%8[0-9A-F])${CONTINUATION}${CONTINUATION}`,
2586
+ String.raw`(?:%E0%[AB][0-9A-F]|%E[1-9ABCEF]${CONTINUATION}|%ED%[89][0-9A-F])${CONTINUATION}`,
2587
+ String.raw`%(?:C[2-9A-F]|D[0-9A-F])${CONTINUATION}`
2588
+ ].join("|");
2589
+ var PREFIX_UNIT = new RegExp(`${UTF8_SEQUENCE}|%[0-9A-F]{2}|[\\s\\S]`, "giu");
2590
+ function prefixUnits(value) {
2591
+ return value.match(PREFIX_UNIT) ?? [];
2592
+ }
2593
+ function renderValue(value, spec, operator) {
2594
+ const truncated = spec.maxLength === void 0 ? value : prefixUnits(value).slice(0, spec.maxLength).join("");
2595
+ return encodeValue(truncated, operator);
2596
+ }
2597
+ function expandTemplateExpression(part, values) {
2598
+ const present = part.varspecs.flatMap((spec) => {
2599
+ const value = readValue(values, spec.name);
2600
+ return value === void 0 ? [] : [{ spec, value }];
2601
+ });
2602
+ if (present.length === 0) return "";
2603
+ const { operator } = part;
2604
+ if (NAMED_OPERATORS.has(operator)) {
2605
+ const pairs = present.map(({ spec, value }) => {
2606
+ const rendered2 = renderValue(value, spec, operator);
2607
+ return operator === ";" && rendered2.length === 0 ? spec.name : `${spec.name}=${rendered2}`;
2608
+ });
2609
+ return operator === ";" ? `;${pairs.join(";")}` : `${operator}${pairs.join("&")}`;
2610
+ }
2611
+ const rendered = present.map(
2612
+ ({ spec, value }) => renderValue(value, spec, operator)
2613
+ );
2614
+ switch (operator) {
2615
+ case "#":
2616
+ return `#${rendered.join(",")}`;
2617
+ case ".":
2618
+ return `.${rendered.join(".")}`;
2619
+ case "/":
2620
+ return `/${rendered.join("/")}`;
2621
+ // "" and "+" — a bare comma-joined list, no prefix.
2622
+ default:
2623
+ return rendered.join(",");
2624
+ }
2625
+ }
2626
+ function expandParts(parts, values) {
2627
+ return parts.map(
2628
+ (part) => part.kind === "literal" ? encodeLiteral(part.text) : expandTemplateExpression(part, values)
2629
+ ).join("");
2630
+ }
2631
+ function expandUriTemplateStrict(uriTemplate, values) {
2632
+ const sdk = sdkTemplateError(uriTemplate);
2633
+ if (sdk) throw new Error(sdk);
2634
+ const parts = parseUriTemplate(uriTemplate);
2635
+ const invalid = partsError(uriTemplate, parts);
2636
+ if (invalid) throw new Error(invalid);
2637
+ const tooLong = valueLengthError(values, declaredNames(parts));
2638
+ if (tooLong) throw new Error(tooLong);
2639
+ return expandParts(parts, values);
2640
+ }
2641
+ function sdkTemplateError(uriTemplate) {
2642
+ try {
2643
+ new UriTemplate(uriTemplate);
2644
+ return null;
2645
+ } catch (error) {
2646
+ return error instanceof Error ? error.message : String(error);
2647
+ }
2648
+ }
2649
+ function partsError(uriTemplate, parts) {
2650
+ const stray = parts.find(
2651
+ (part) => part.kind === "literal" && /[{}]/.test(part.text)
2652
+ );
2653
+ if (stray) {
2654
+ return `Unmatched brace in "${uriTemplate}" \u2014 an expression must be a complete {\u2026}.`;
2655
+ }
2656
+ const bad = parts.find((part) => part.kind === "expression" && part.invalid);
2657
+ if (bad) {
2658
+ return `Invalid RFC 6570 varspec in "${uriTemplate}": ${bad.kind === "expression" ? bad.source : ""} \u2014 each varspec must name a variable, optionally with an explode (*) or a 1-9999 prefix modifier.`;
2659
+ }
2660
+ return null;
2661
+ }
2662
+ var MAX_VALUE_LENGTH = 1e6;
2663
+ function valueLengthError(values, names) {
2664
+ const considered = names === void 0 ? Object.keys(values) : [...new Set(names)];
2665
+ for (const name of considered) {
2666
+ const value = readValue(values, name);
2667
+ if (value !== void 0 && value.length > MAX_VALUE_LENGTH) {
2668
+ return `The value for "${name}" exceeds the ${MAX_VALUE_LENGTH}-character limit.`;
2669
+ }
2670
+ }
2671
+ return null;
2672
+ }
2673
+ function declaredNames(parts) {
2674
+ return [
2675
+ ...new Set(
2676
+ parts.flatMap((part) => part.kind === "expression" ? part.names : [])
2677
+ )
2678
+ ];
2679
+ }
2253
2680
 
2254
2681
  // ../../core/mcp/typedEventTarget.ts
2255
2682
  var TypedEventGeneric = class extends CustomEvent {
@@ -2516,6 +2943,136 @@ var ToolCallCancelledError = class extends Error {
2516
2943
  }
2517
2944
  };
2518
2945
 
2946
+ // ../../core/auth/endpointOverrides.ts
2947
+ var OVERRIDE_FIELDS = {
2948
+ authorizationUrl: "authorization_endpoint",
2949
+ tokenUrl: "token_endpoint"
2950
+ };
2951
+ function oauthEndpointUrlError(value) {
2952
+ const trimmed = value.trim();
2953
+ if (trimmed === "") return void 0;
2954
+ let parsed;
2955
+ try {
2956
+ parsed = new URL(trimmed);
2957
+ } catch {
2958
+ return `"${trimmed}" is not an absolute URL.`;
2959
+ }
2960
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2961
+ return `"${trimmed}" is not an http(s) URL.`;
2962
+ }
2963
+ if (parsed.username || parsed.password) {
2964
+ return "URL must not contain a username or password.";
2965
+ }
2966
+ return void 0;
2967
+ }
2968
+ function normalizeEndpointUrl(value, field) {
2969
+ if (value === void 0) return void 0;
2970
+ const trimmed = value.trim();
2971
+ if (trimmed === "") return void 0;
2972
+ const error = oauthEndpointUrlError(trimmed);
2973
+ if (error) {
2974
+ console.warn(`[oauth] Ignoring \`${field}\`: ${error}`);
2975
+ return void 0;
2976
+ }
2977
+ return trimmed;
2978
+ }
2979
+ function normalizeOAuthEndpointOverrides(overrides) {
2980
+ if (!overrides) return void 0;
2981
+ const authorizationUrl = normalizeEndpointUrl(
2982
+ overrides.authorizationUrl,
2983
+ "authorizationUrl"
2984
+ );
2985
+ const tokenUrl = normalizeEndpointUrl(overrides.tokenUrl, "tokenUrl");
2986
+ if (!authorizationUrl && !tokenUrl) return void 0;
2987
+ return {
2988
+ ...authorizationUrl && { authorizationUrl },
2989
+ ...tokenUrl && { tokenUrl }
2990
+ };
2991
+ }
2992
+ function isAuthorizationServerMetadata(value) {
2993
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2994
+ return false;
2995
+ }
2996
+ const doc = value;
2997
+ if (typeof doc.issuer !== "string") return false;
2998
+ return typeof doc.authorization_endpoint === "string" || typeof doc.token_endpoint === "string";
2999
+ }
3000
+ function applyOAuthEndpointOverrides(metadata, overrides) {
3001
+ const patched = { ...metadata };
3002
+ for (const [key, field] of Object.entries(OVERRIDE_FIELDS)) {
3003
+ const override = overrides[key];
3004
+ if (override) patched[field] = override;
3005
+ }
3006
+ return patched;
3007
+ }
3008
+ var AS_METADATA_WELL_KNOWN_PATHS = [
3009
+ "/.well-known/oauth-authorization-server",
3010
+ "/.well-known/openid-configuration"
3011
+ ];
3012
+ function requestUrlOf(input) {
3013
+ if (typeof input === "string") return input;
3014
+ if (input instanceof URL) return input.href;
3015
+ return input.url;
3016
+ }
3017
+ function isAuthorizationServerMetadataRequest(input) {
3018
+ const raw = requestUrlOf(input);
3019
+ let pathname;
3020
+ try {
3021
+ pathname = new URL(raw).pathname;
3022
+ } catch {
3023
+ pathname = raw;
3024
+ }
3025
+ return AS_METADATA_WELL_KNOWN_PATHS.some((path3) => pathname.includes(path3));
3026
+ }
3027
+ function isJsonDocumentResponse(contentType) {
3028
+ if (!contentType) return false;
3029
+ const mediaType = contentType.split(";")[0].trim().toLowerCase();
3030
+ return mediaType === "application/json" || mediaType.endsWith("+json");
3031
+ }
3032
+ function responseWithBody(response, body) {
3033
+ const headers = new Headers(response.headers);
3034
+ headers.delete("content-length");
3035
+ headers.delete("content-encoding");
3036
+ return new Response(body, {
3037
+ status: response.status,
3038
+ statusText: response.statusText,
3039
+ headers
3040
+ });
3041
+ }
3042
+ function withOAuthEndpointOverrides(fetchFn, getOverrides) {
3043
+ let lastKey;
3044
+ let lastNormalized;
3045
+ const resolveOverrides = () => {
3046
+ const raw = getOverrides();
3047
+ const key = JSON.stringify([raw?.authorizationUrl, raw?.tokenUrl]);
3048
+ if (key !== lastKey) {
3049
+ lastKey = key;
3050
+ lastNormalized = normalizeOAuthEndpointOverrides(raw);
3051
+ }
3052
+ return lastNormalized;
3053
+ };
3054
+ return async (input, init) => {
3055
+ const response = await fetchFn(input, init);
3056
+ const overrides = resolveOverrides();
3057
+ if (!overrides) return response;
3058
+ if (!isAuthorizationServerMetadataRequest(input)) return response;
3059
+ if (!response.ok) return response;
3060
+ if (!isJsonDocumentResponse(response.headers.get("content-type")))
3061
+ return response;
3062
+ let parsed;
3063
+ try {
3064
+ parsed = await response.clone().json();
3065
+ } catch {
3066
+ return response;
3067
+ }
3068
+ if (!isAuthorizationServerMetadata(parsed)) return response;
3069
+ return responseWithBody(
3070
+ response,
3071
+ JSON.stringify(applyOAuthEndpointOverrides(parsed, overrides))
3072
+ );
3073
+ };
3074
+ }
3075
+
2519
3076
  // ../../core/logging/logger.ts
2520
3077
  var noop = () => {
2521
3078
  };
@@ -2535,6 +3092,41 @@ function createSilentLogger() {
2535
3092
  }
2536
3093
  var silentLogger = createSilentLogger();
2537
3094
 
3095
+ // ../../core/auth/authorizationParams.ts
3096
+ var RESERVED_AUTHORIZATION_PARAMS = [
3097
+ "client_id",
3098
+ "code_challenge",
3099
+ "code_challenge_method",
3100
+ "redirect_uri",
3101
+ "resource",
3102
+ "response_type",
3103
+ "scope",
3104
+ "state"
3105
+ ];
3106
+ var RESERVED = new Set(RESERVED_AUTHORIZATION_PARAMS);
3107
+ function isReservedAuthorizationParam(key) {
3108
+ return RESERVED.has(key.trim().toLowerCase());
3109
+ }
3110
+ function applyAuthorizationParams(authorizationUrl, params) {
3111
+ if (!params) return authorizationUrl;
3112
+ const applicable = Object.entries(params).filter(([key]) => {
3113
+ if (key.trim() === "") return false;
3114
+ if (isReservedAuthorizationParam(key)) {
3115
+ console.warn(
3116
+ `[oauth] Ignoring reserved authorization parameter "${key.trim()}" \u2014 it is set by the authorization flow.`
3117
+ );
3118
+ return false;
3119
+ }
3120
+ return true;
3121
+ });
3122
+ if (applicable.length === 0) return authorizationUrl;
3123
+ const merged = new URL(authorizationUrl.href);
3124
+ for (const [key, value] of applicable) {
3125
+ merged.searchParams.set(key.trim(), value);
3126
+ }
3127
+ return merged;
3128
+ }
3129
+
2538
3130
  // ../../core/auth/providers.ts
2539
3131
  var MutableRedirectUrlProvider = class {
2540
3132
  redirectUrl = "";
@@ -2570,12 +3162,15 @@ var BaseOAuthClientProvider = class {
2570
3162
  redirectUrlProvider;
2571
3163
  navigation;
2572
3164
  clientMetadataUrl;
3165
+ /** Custom authorization-request parameters (#2018). Authorize URL only. */
3166
+ authorizationParams;
2573
3167
  constructor(serverUrl, oauthConfig) {
2574
3168
  this.serverUrl = serverUrl;
2575
3169
  this.storage = oauthConfig.storage;
2576
3170
  this.redirectUrlProvider = oauthConfig.redirectUrlProvider;
2577
3171
  this.navigation = oauthConfig.navigation;
2578
3172
  this.clientMetadataUrl = oauthConfig.clientMetadataUrl;
3173
+ this.authorizationParams = oauthConfig.authorizationParams;
2579
3174
  }
2580
3175
  /**
2581
3176
  * Load persisted scope into {@link cachedScope} before SDK `auth()` (which
@@ -2682,6 +3277,27 @@ var BaseOAuthClientProvider = class {
2682
3277
  });
2683
3278
  }
2684
3279
  redirectToAuthorization(authorizationUrl) {
3280
+ this.performAuthorizationRedirect(
3281
+ applyAuthorizationParams(authorizationUrl, this.authorizationParams)
3282
+ );
3283
+ }
3284
+ /**
3285
+ * Redirect to an authorize URL belonging to a **different** authorization
3286
+ * server than the one this provider was configured for, so the per-server
3287
+ * custom parameters (#2018) MUST NOT be applied.
3288
+ *
3289
+ * The only caller is the enterprise-managed (EMA) transport provider, which
3290
+ * discards the resource AS URL and sends the user to the enterprise IdP
3291
+ * instead. Parameters a user configured for their MCP server's authorization
3292
+ * server — a `kc_idp_hint`, an `audience` — are meaningless and potentially
3293
+ * flow-breaking on the IdP's, and appending them there would leak per-server
3294
+ * config across an authorization-server boundary.
3295
+ */
3296
+ redirectToExternalAuthorization(authorizationUrl) {
3297
+ this.performAuthorizationRedirect(authorizationUrl);
3298
+ }
3299
+ /** Capture, announce, and navigate to a finished authorize URL. */
3300
+ performAuthorizationRedirect(authorizationUrl) {
2685
3301
  this.capturedAuthUrl = authorizationUrl;
2686
3302
  if (!this.suppressAuthorizationNavigation) {
2687
3303
  if (this.eventTarget) {
@@ -3549,7 +4165,7 @@ var EmaTransportOAuthProvider = class {
3549
4165
  async redirectToAuthorization(_authorizationUrl) {
3550
4166
  const idpAuthorizationUrl = await startEmaIdpAuthorization(this.emaConfig);
3551
4167
  this.inner.clearCapturedAuthUrl();
3552
- this.inner.redirectToAuthorization(idpAuthorizationUrl);
4168
+ this.inner.redirectToExternalAuthorization(idpAuthorizationUrl);
3553
4169
  }
3554
4170
  saveCodeVerifier(codeVerifier) {
3555
4171
  return this.inner.saveCodeVerifier(codeVerifier);
@@ -3684,6 +4300,30 @@ var OAuthManager = class {
3684
4300
  getServerUrl() {
3685
4301
  return this.params.getServerUrl();
3686
4302
  }
4303
+ /**
4304
+ * The per-server authorization/token endpoint overrides, read live so a
4305
+ * `setOAuthConfig` between requests takes effect without rebuilding the
4306
+ * client's fetch. `InspectorClient` reads this from inside its fetch wrapper.
4307
+ * (#1906)
4308
+ *
4309
+ * Returns nothing under enterprise-managed authorization, for the same reason
4310
+ * `redirectToExternalAuthorization` skips the custom authorization parameters
4311
+ * (#2018): the EMA leg authorizes against the enterprise IdP, a *different*
4312
+ * authorization server, and its OIDC discovery runs through this same fetch
4313
+ * (`emaFlow` → `idpOidc`). Rewriting that document would point the IdP login —
4314
+ * or the IdP code/refresh-token exchange — at the resource server's
4315
+ * authorization server, which is both wrong and a way to leak an IdP
4316
+ * credential across an authorization-server boundary.
4317
+ */
4318
+ getEndpointOverrides() {
4319
+ if (this.isEnterpriseManaged()) return void 0;
4320
+ const { authorizationUrl, tokenUrl } = this.oauthConfig;
4321
+ if (!authorizationUrl && !tokenUrl) return void 0;
4322
+ return {
4323
+ ...authorizationUrl && { authorizationUrl },
4324
+ ...tokenUrl && { tokenUrl }
4325
+ };
4326
+ }
3687
4327
  /**
3688
4328
  * Return the configured OAuth storage, throwing a clear error if it is
3689
4329
  * absent. Used on the EMA re-mint/persist paths, which are only reached for
@@ -3707,7 +4347,12 @@ var OAuthManager = class {
3707
4347
  storage: this.oauthConfig.storage,
3708
4348
  redirectUrlProvider: this.oauthConfig.redirectUrlProvider,
3709
4349
  navigation: this.oauthConfig.navigation,
3710
- clientMetadataUrl: this.oauthConfig.clientMetadataUrl
4350
+ clientMetadataUrl: this.oauthConfig.clientMetadataUrl,
4351
+ // #2018: per-server custom authorization-request parameters. Carried on
4352
+ // the provider (not in OAuth storage like `scope`) — they are pure config
4353
+ // read from mcp.json, and nothing in the flow needs to persist or union
4354
+ // them.
4355
+ authorizationParams: this.oauthConfig.authorizationParams
3711
4356
  });
3712
4357
  provider.setEventTarget(this.params.getEventTarget());
3713
4358
  await provider.prepareForAuth();
@@ -4933,6 +5578,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
4933
5578
  // annotations (SEP-2243), recomputed on every aggregate tools refresh and
4934
5579
  // surfaced so the Tools tab can show why a tool vanished (#1632).
4935
5580
  excludedTools = [];
5581
+ // Entries the Inspector dropped from a list result because they failed the
5582
+ // spec schema for that primitive, keyed by list method. Populated only by the
5583
+ // salvage fallback in `listAllSalvaging` — empty against a conforming server
5584
+ // (#1909).
5585
+ malformedListItems = /* @__PURE__ */ new Map();
4936
5586
  // The capabilities this Inspector client advertises to the server during the
4937
5587
  // initialize handshake. Built once in setupClient() and snapshotted here so
4938
5588
  // UI surfaces (Server Info modal) can display them without poking at the
@@ -5111,6 +5761,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
5111
5761
  resources: options.listChangedNotifications?.resources ?? true,
5112
5762
  prompts: options.listChangedNotifications?.prompts ?? true
5113
5763
  };
5764
+ this.fetchFn = withOAuthEndpointOverrides(
5765
+ this.fetchFn ?? fetch,
5766
+ () => this.oauthManager?.getEndpointOverrides()
5767
+ );
5114
5768
  this.effectiveAuthFetch = this.buildEffectiveAuthFetch();
5115
5769
  this.sessionId = options.sessionId;
5116
5770
  if (options.oauth || options.environment.oauth) {
@@ -5138,6 +5792,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
5138
5792
  });
5139
5793
  }
5140
5794
  const clientOptions = {
5795
+ // Bound the SDK's auto-aggregate with the SAME constant the #1909 salvage
5796
+ // re-walks use, so the strict path and its fallback cannot drift apart.
5797
+ // This is the SDK's own default today; setting it explicitly is what makes
5798
+ // the shared value a fact rather than a copied guess.
5799
+ listMaxPages: LIST_MAX_PAGES,
5141
5800
  // Per-server protocol era (SEP §7.8), threaded from config via
5142
5801
  // `eraToVersionNegotiation` and defaulted to `{ mode: "legacy" }` in the
5143
5802
  // constructor. "legacy" keeps the wire byte-identical to a 2025 client;
@@ -5807,6 +6466,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
5807
6466
  this.cancelledTaskIds.clear();
5808
6467
  this.outboundRequestMethods.clear();
5809
6468
  this.lastAnsweredRequestByMethod.clear();
6469
+ if (this.malformedListItems.size > 0) {
6470
+ this.malformedListItems.clear();
6471
+ this.dispatchTypedEvent("malformedListItemsChange", []);
6472
+ }
6473
+ if (this.excludedTools.length > 0) {
6474
+ this.excludedTools = [];
6475
+ this.dispatchTypedEvent("excludedToolsChange", []);
6476
+ }
5810
6477
  for (const [, controller] of this.taskInputAbortControllers) {
5811
6478
  controller.abort(new Error("Connection ended"));
5812
6479
  }
@@ -6030,7 +6697,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6030
6697
  this.status = "connected";
6031
6698
  this.dispatchTypedEvent("statusChange", this.status);
6032
6699
  await this.fetchServerInfo();
6033
- if (this.initialLoggingLevel && this.capabilities?.logging) {
6700
+ if (this.initialLoggingLevel && this.capabilities?.logging && this.protocolEra !== "modern") {
6034
6701
  await this.client.setLoggingLevel(
6035
6702
  this.initialLoggingLevel,
6036
6703
  this.getRequestOptions()
@@ -6175,6 +6842,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6175
6842
  this.protocolEra = void 0;
6176
6843
  this.discoverResult = void 0;
6177
6844
  this.excludedTools = [];
6845
+ this.malformedListItems.clear();
6178
6846
  this.modernLogLevel = void 0;
6179
6847
  this.dispatchTypedEvent("pendingSamplesChange", this.pendingSamples);
6180
6848
  this.dispatchTypedEvent(
@@ -6188,6 +6856,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6188
6856
  this.dispatchTypedEvent("protocolEraChange", this.protocolEra);
6189
6857
  this.dispatchTypedEvent("discoverResultChange", this.discoverResult);
6190
6858
  this.dispatchTypedEvent("excludedToolsChange", this.excludedTools);
6859
+ this.dispatchTypedEvent("malformedListItemsChange", []);
6191
6860
  }
6192
6861
  /**
6193
6862
  * Returns a client proxy for use by AppRenderer / @mcp-ui. Delegates to the
@@ -6281,7 +6950,6 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6281
6950
  * `clientCapabilities` carrying the tasks extension). We reproduce it here.
6282
6951
  */
6283
6952
  withModernTaskEnvelope(params) {
6284
- const existingMeta = params._meta ?? {};
6285
6953
  const clientCapabilities = {
6286
6954
  ...this.clientCapabilities,
6287
6955
  // Force-stamp the tasks extension regardless of what the client
@@ -6293,6 +6961,26 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6293
6961
  [TASKS_EXTENSION_KEY]: {}
6294
6962
  }
6295
6963
  };
6964
+ return this.withModernEnvelope(params, clientCapabilities);
6965
+ }
6966
+ /**
6967
+ * Stamp the `_meta` envelope every raw-wire request needs on the modern leg:
6968
+ * the negotiated protocol version, the client identity, and the client
6969
+ * capabilities.
6970
+ *
6971
+ * Split out of {@link withModernTaskEnvelope} so the list-salvage re-walk
6972
+ * (#1909) can reuse it WITHOUT that method's force-stamped tasks extension —
6973
+ * a `tools/list` has no business claiming an extension the user may have
6974
+ * deliberately turned off (#1738).
6975
+ *
6976
+ * The NEGOTIATED protocol version is used so the envelope agrees with the
6977
+ * `MCP-Protocol-Version` header the transport stamps from the same source — a
6978
+ * future modern-family revision would negotiate a different string, and the
6979
+ * two must not disagree. The raw channel only runs on a connected modern
6980
+ * session, so this is always set; the constant is a defensive fallback.
6981
+ */
6982
+ withModernEnvelope(params, clientCapabilities = this.clientCapabilities) {
6983
+ const existingMeta = params._meta ?? {};
6296
6984
  const protocolVersion = this.getProtocolVersion() ?? MODERN_PROTOCOL_VERSION2;
6297
6985
  return {
6298
6986
  ...params,
@@ -6889,7 +7577,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6889
7577
  const effectiveMeta = this.mergeMeta(metadata);
6890
7578
  const params = {
6891
7579
  ...effectiveMeta ? { _meta: effectiveMeta } : {},
6892
- ...cursor ? { cursor } : {}
7580
+ // `!== undefined`, not truthiness: "" is a valid cursor. Dropping it asks
7581
+ // for page one again — and `listToolsForScan` pairs this call with a
7582
+ // fallback that already preserves it, so the two legs of one scan page
7583
+ // would otherwise disagree about which page they fetched.
7584
+ ...cursor !== void 0 && { cursor }
6893
7585
  };
6894
7586
  const response = await this.invokeMcpClient(
6895
7587
  () => this.client.request(
@@ -6917,19 +7609,29 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6917
7609
  if (!this.client) {
6918
7610
  throw new Error("Client is not connected");
6919
7611
  }
6920
- const response = await this.invokeMcpClient(
6921
- () => this.client.listTools(
6922
- this.aggregateListParams(options?.metadata),
6923
- this.getCacheableRequestOptions(options?.cacheMode)
6924
- )
6925
- );
7612
+ const tools = await this.listAllSalvaging({
7613
+ method: "tools/list",
7614
+ itemsKey: "tools",
7615
+ resultSchema: ListToolsResultSchema,
7616
+ // Era-aware: the neutral `ToolSchema` is more permissive than the
7617
+ // negotiated era's wire schema — see `toolItemSchemaForEra`.
7618
+ itemSchema: toolItemSchemaForEra(this.isModernEra()),
7619
+ metadata: options?.metadata,
7620
+ finalize: (salvaged) => this.excludeInvalidXMcpHeaderTools(salvaged),
7621
+ aggregate: async () => [
7622
+ ...(await this.client.listTools(
7623
+ this.aggregateListParams(options?.metadata),
7624
+ this.getCacheableRequestOptions(options?.cacheMode)
7625
+ )).tools
7626
+ ]
7627
+ });
6926
7628
  await this.refreshExcludedTools(options?.metadata).catch((err) => {
6927
7629
  this.logger.warn(
6928
7630
  { err },
6929
7631
  "Excluded-tools walk failed; the SEP-2243 excluded list may be incomplete"
6930
7632
  );
6931
7633
  });
6932
- return { tools: [...response.tools] };
7634
+ return { tools };
6933
7635
  }
6934
7636
  /**
6935
7637
  * Whether this connection excludes tools with invalid `x-mcp-header`
@@ -6960,6 +7662,202 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6960
7662
  if (id === void 0) return;
6961
7663
  this.dispatchTypedEvent("responseRejected", { id, reason });
6962
7664
  }
7665
+ /**
7666
+ * Put the method's response correlation back where it was before a salvage
7667
+ * re-walk, so a caller that goes on to mark the failure still marks the
7668
+ * ORIGINAL rejected response.
7669
+ *
7670
+ * The re-walk answers the same method, so on any rethrow path the map points
7671
+ * at a lenient page that succeeded. `ManagedListState.refresh()` catches the
7672
+ * rethrown error and calls `markResponseRejected(method, ...)`, which would
7673
+ * then stamp "Rejected by the Inspector" onto that page and leave the real
7674
+ * one rendering clean — the same misattribution the capture inside
7675
+ * `salvageList` exists to prevent, just one frame further out. Deleting when
7676
+ * nothing had answered before makes the outer mark a no-op rather than a lie.
7677
+ */
7678
+ restoreResponseCorrelation(method, id) {
7679
+ if (id === void 0) this.lastAnsweredRequestByMethod.delete(method);
7680
+ else this.lastAnsweredRequestByMethod.set(method, id);
7681
+ }
7682
+ /** Every entry dropped from a list result as malformed, across methods (#1909). */
7683
+ getMalformedListItems() {
7684
+ return [...this.malformedListItems.values()].flat();
7685
+ }
7686
+ /**
7687
+ * Record (or clear) the malformed entries for one list method and notify.
7688
+ *
7689
+ * A no-op when the method had none and still has none, so the common case —
7690
+ * every conforming refresh, of which there are many — costs no event and no
7691
+ * re-render.
7692
+ */
7693
+ setMalformedListItems(method, malformed) {
7694
+ const previous = this.malformedListItems.get(method) ?? [];
7695
+ if (previous.length === 0 && malformed.length === 0) return;
7696
+ if (malformed.length === 0) this.malformedListItems.delete(method);
7697
+ else this.malformedListItems.set(method, malformed);
7698
+ this.dispatchTypedEvent(
7699
+ "malformedListItemsChange",
7700
+ this.getMalformedListItems()
7701
+ );
7702
+ }
7703
+ /**
7704
+ * Run a list aggregate, falling back to a per-item salvage when the SDK
7705
+ * rejects the result it received (#1909).
7706
+ *
7707
+ * The strict aggregate runs first and unchanged, so a conforming server pays
7708
+ * nothing: no extra request, no extra parse, and the SDK's response cache
7709
+ * still serves it. Only an `InvalidResult` rejection — a response in hand
7710
+ * that failed validation — reaches {@link salvageList}; every other failure
7711
+ * (transport drop, timeout, server-sent error) rethrows untouched, because
7712
+ * re-listing would not produce anything to salvage.
7713
+ */
7714
+ async listAllSalvaging({
7715
+ method,
7716
+ itemsKey,
7717
+ itemSchema,
7718
+ resultSchema,
7719
+ aggregate,
7720
+ metadata,
7721
+ finalize
7722
+ }) {
7723
+ try {
7724
+ const items = await this.invokeMcpClient(aggregate, { method });
7725
+ this.setMalformedListItems(method, []);
7726
+ return items;
7727
+ } catch (err) {
7728
+ if (!isSalvageableRejection(err)) throw err;
7729
+ const salvaged = await this.salvageList({
7730
+ method,
7731
+ itemsKey,
7732
+ itemSchema,
7733
+ resultSchema,
7734
+ metadata,
7735
+ err
7736
+ });
7737
+ return finalize ? finalize(salvaged) : salvaged;
7738
+ }
7739
+ }
7740
+ /**
7741
+ * Drop the tools the SDK would have excluded from `tools/list` for invalid
7742
+ * `x-mcp-header` annotations (SEP-2243).
7743
+ *
7744
+ * The strict aggregate is filtered by the SDK before we see it, so the
7745
+ * salvage path has to reapply the same rule itself: otherwise one ordinary
7746
+ * malformed tool anywhere in the list would trip the fallback and quietly
7747
+ * readmit every invalid-header tool alongside it — turning a rendering fix
7748
+ * into a spec violation. The dropped tools are still reported through
7749
+ * `refreshExcludedTools`, which lists them with their reason.
7750
+ */
7751
+ excludeInvalidXMcpHeaderTools(tools) {
7752
+ if (!this.excludesInvalidXMcpHeaderTools()) return tools;
7753
+ return tools.filter(
7754
+ (tool) => scanXMcpHeaderDeclarations(tool.inputSchema).valid
7755
+ );
7756
+ }
7757
+ /**
7758
+ * Re-walk a list method with pagination validated but entries left raw, then
7759
+ * validate each entry on its own — keeping the good ones and reporting the
7760
+ * rest (#1909).
7761
+ *
7762
+ * Two deliberate properties:
7763
+ *
7764
+ * - **The original error wins when nothing is salvageable.** If every entry
7765
+ * validates here, the strict rejection was about something else (a
7766
+ * top-level field, an era codec decision), so there is no per-item story to
7767
+ * tell and swallowing it would hide a real failure. Rethrow the original.
7768
+ * - **A salvaged response is still a rejected response.** The Protocol entry
7769
+ * is marked (#1953) with what was dropped, so the wire truth survives even
7770
+ * though the list now renders.
7771
+ *
7772
+ * This walk is deliberately un-cached (it uses the raw per-page path), which
7773
+ * is fine: it only runs against a server that just returned a non-conforming
7774
+ * result.
7775
+ */
7776
+ async salvageList({
7777
+ method,
7778
+ itemsKey,
7779
+ itemSchema,
7780
+ resultSchema,
7781
+ metadata,
7782
+ err
7783
+ }) {
7784
+ const pageSchema = lenientListPageSchema(resultSchema, itemsKey);
7785
+ const rejectedResponseId = this.lastAnsweredRequestByMethod.get(method);
7786
+ const valid = [];
7787
+ const malformed = [];
7788
+ const seenCursors = /* @__PURE__ */ new Set();
7789
+ let cursor;
7790
+ let pages = 0;
7791
+ try {
7792
+ do {
7793
+ if (pages >= LIST_MAX_PAGES) throw listPaginationExceeded(method);
7794
+ pages += 1;
7795
+ const effectiveMeta = this.mergeMeta(metadata);
7796
+ const params = {
7797
+ ...effectiveMeta ? { _meta: effectiveMeta } : {},
7798
+ // `!== undefined`, not truthiness: "" is a valid cursor, and dropping it
7799
+ // would re-request page one and duplicate its entries until the
7800
+ // repeated-cursor guard below stopped the walk.
7801
+ ...cursor !== void 0 && { cursor }
7802
+ };
7803
+ const page = await this.invokeMcpClient(
7804
+ () => this.isModernEra() ? this.rawWireRequest(
7805
+ method,
7806
+ this.withModernEnvelope(params),
7807
+ pageSchema
7808
+ ) : this.client.request(
7809
+ { method, params },
7810
+ pageSchema,
7811
+ this.getRequestOptions(metadata?.progressToken)
7812
+ ),
7813
+ { method }
7814
+ );
7815
+ if (this.isModernEra() && !ModernResultEnvelopeSchema.safeParse(page).success) {
7816
+ throw err;
7817
+ }
7818
+ const items = rawItemsOf(page, itemsKey);
7819
+ if (items === void 0) throw err;
7820
+ const salvaged = salvageListItems({
7821
+ method,
7822
+ items,
7823
+ schema: itemSchema,
7824
+ startIndex: valid.length + malformed.length
7825
+ });
7826
+ valid.push(...salvaged.valid);
7827
+ malformed.push(...salvaged.malformed);
7828
+ cursor = nextCursorOf(page);
7829
+ if (cursor !== void 0) {
7830
+ if (seenCursors.has(cursor)) break;
7831
+ seenCursors.add(cursor);
7832
+ }
7833
+ } while (cursor !== void 0);
7834
+ } catch (walkErr) {
7835
+ if (walkErr !== err) {
7836
+ this.logger.warn(
7837
+ { err: walkErr, method },
7838
+ "Salvage re-walk failed; surfacing the original list error"
7839
+ );
7840
+ }
7841
+ this.restoreResponseCorrelation(method, rejectedResponseId);
7842
+ throw err;
7843
+ }
7844
+ if (malformed.length === 0) {
7845
+ this.restoreResponseCorrelation(method, rejectedResponseId);
7846
+ throw err;
7847
+ }
7848
+ if (rejectedResponseId !== void 0) {
7849
+ this.dispatchTypedEvent("responseRejected", {
7850
+ id: rejectedResponseId,
7851
+ reason: summarizeMalformed(malformed)
7852
+ });
7853
+ }
7854
+ this.setMalformedListItems(method, malformed);
7855
+ this.logger.warn(
7856
+ { method, malformed },
7857
+ "Dropped malformed entries from a list result; the rest of the list was kept"
7858
+ );
7859
+ return valid;
7860
+ }
6963
7861
  /** The current SEP-2243 excluded-tools set (empty on legacy/stdio). */
6964
7862
  getExcludedTools() {
6965
7863
  return this.excludedTools;
@@ -6978,8 +7876,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6978
7876
  if (this.excludesInvalidXMcpHeaderTools()) {
6979
7877
  const seenCursors = /* @__PURE__ */ new Set();
6980
7878
  let cursor;
7879
+ let pages = 0;
6981
7880
  do {
6982
- const page = await this.listTools(cursor, metadata);
7881
+ if (pages >= LIST_MAX_PAGES) throw listPaginationExceeded("tools/list");
7882
+ pages += 1;
7883
+ const page = await this.listToolsForScan(cursor, metadata);
6983
7884
  for (const tool of page.tools) {
6984
7885
  const scan = scanXMcpHeaderDeclarations(tool.inputSchema);
6985
7886
  if (!scan.valid) excluded.push({ tool, reason: scan.reason });
@@ -6995,6 +7896,90 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
6995
7896
  this.dispatchTypedEvent("excludedToolsChange", excluded);
6996
7897
  return excluded;
6997
7898
  }
7899
+ /**
7900
+ * One page of the RAW `tools/list` for the SEP-2243 scan, tolerating a
7901
+ * malformed sibling entry (#1632 + #1909).
7902
+ *
7903
+ * The scan needs the unfiltered list, so it uses the strict single-page call
7904
+ * — but that call rejects the whole page when any entry is non-conforming,
7905
+ * which would blank the excluded set and leave a tool that vanished for an
7906
+ * invalid header with no explanation at all. That is the same failure #1909
7907
+ * is about, one level down. So on a decode rejection the page is re-fetched
7908
+ * leniently and scanned entry by entry.
7909
+ *
7910
+ * The malformed ones are not "excluded tools" and are not returned: they
7911
+ * describe THIS exchange, not the aggregate the Tools panel renders, so they
7912
+ * surface as the rejection reason on this response's own Protocol entry and
7913
+ * go no further (see `refreshExcludedTools`).
7914
+ */
7915
+ async listToolsForScan(cursor, metadata) {
7916
+ try {
7917
+ return await this.listTools(cursor, metadata);
7918
+ } catch (err) {
7919
+ if (!isSalvageableRejection(err)) throw err;
7920
+ const rejectedResponseId = this.lastAnsweredRequestByMethod.get("tools/list");
7921
+ try {
7922
+ const effectiveMeta = this.mergeMeta(metadata);
7923
+ const params = {
7924
+ ...effectiveMeta ? { _meta: effectiveMeta } : {},
7925
+ // See `salvageList`: "" is a valid cursor.
7926
+ ...cursor !== void 0 && { cursor }
7927
+ };
7928
+ const pageSchema = lenientListPageSchema(
7929
+ ListToolsResultSchema,
7930
+ "tools"
7931
+ );
7932
+ const page = await this.invokeMcpClient(
7933
+ () => this.isModernEra() ? this.rawWireRequest(
7934
+ "tools/list",
7935
+ this.withModernEnvelope(params),
7936
+ pageSchema
7937
+ ) : this.client.request(
7938
+ { method: "tools/list", params },
7939
+ pageSchema,
7940
+ this.getRequestOptions(metadata?.progressToken)
7941
+ ),
7942
+ { method: "tools/list" }
7943
+ );
7944
+ if (this.isModernEra() && !ModernResultEnvelopeSchema.safeParse(page).success) {
7945
+ throw err;
7946
+ }
7947
+ const items = rawItemsOf(page, "tools");
7948
+ if (items === void 0) throw err;
7949
+ const { valid, malformed } = salvageListItems({
7950
+ method: "tools/list",
7951
+ items,
7952
+ schema: toolItemSchemaForEra(this.isModernEra())
7953
+ });
7954
+ if (rejectedResponseId !== void 0) {
7955
+ this.dispatchTypedEvent("responseRejected", {
7956
+ id: rejectedResponseId,
7957
+ reason: malformed.length > 0 ? summarizeMalformed(malformed) : err instanceof Error ? err.message : String(err)
7958
+ });
7959
+ }
7960
+ const nextCursor = nextCursorOf(page);
7961
+ return {
7962
+ tools: valid,
7963
+ ...nextCursor !== void 0 && { nextCursor }
7964
+ };
7965
+ } catch (fallbackErr) {
7966
+ if (rejectedResponseId !== void 0) {
7967
+ this.dispatchTypedEvent("responseRejected", {
7968
+ id: rejectedResponseId,
7969
+ reason: err instanceof Error ? err.message : String(err)
7970
+ });
7971
+ }
7972
+ if (fallbackErr !== err) {
7973
+ this.logger.warn(
7974
+ { err: fallbackErr },
7975
+ "Excluded-tools salvage re-fetch failed; surfacing the original list error"
7976
+ );
7977
+ }
7978
+ this.restoreResponseCorrelation("tools/list", rejectedResponseId);
7979
+ throw err;
7980
+ }
7981
+ }
7982
+ }
6998
7983
  /**
6999
7984
  * Call a tool. Caller must provide the Tool (e.g. from a state manager).
7000
7985
  * @param tool The tool to call (use tool.name for the request)
@@ -7781,13 +8766,20 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7781
8766
  if (!this.client) {
7782
8767
  throw new Error("Client is not connected");
7783
8768
  }
7784
- const response = await this.invokeMcpClient(
7785
- () => this.client.listResources(
7786
- this.aggregateListParams(options?.metadata),
7787
- this.getCacheableRequestOptions(options?.cacheMode)
7788
- )
7789
- );
7790
- return { resources: [...response.resources] };
8769
+ const resources = await this.listAllSalvaging({
8770
+ method: "resources/list",
8771
+ itemsKey: "resources",
8772
+ resultSchema: ListResourcesResultSchema,
8773
+ itemSchema: ResourceSchema,
8774
+ metadata: options?.metadata,
8775
+ aggregate: async () => [
8776
+ ...(await this.client.listResources(
8777
+ this.aggregateListParams(options?.metadata),
8778
+ this.getCacheableRequestOptions(options?.cacheMode)
8779
+ )).resources
8780
+ ]
8781
+ });
8782
+ return { resources };
7791
8783
  }
7792
8784
  /**
7793
8785
  * Read a resource by URI
@@ -7843,8 +8835,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7843
8835
  const uriTemplateString = uriTemplate;
7844
8836
  let expandedUri;
7845
8837
  try {
7846
- const uriTemplate2 = new UriTemplate(uriTemplateString);
7847
- expandedUri = uriTemplate2.expand(params);
8838
+ expandedUri = expandUriTemplateStrict(uriTemplateString, params);
7848
8839
  } catch (error) {
7849
8840
  throw new Error(
7850
8841
  `Failed to expand URI template "${uriTemplate}": ${error instanceof Error ? error.message : String(error)}`,
@@ -7906,14 +8897,20 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7906
8897
  if (!this.client) {
7907
8898
  throw new Error("Client is not connected");
7908
8899
  }
7909
- const response = await this.invokeMcpClient(
7910
- () => this.client.listResourceTemplates(
7911
- this.aggregateListParams(options?.metadata),
7912
- this.getCacheableRequestOptions(options?.cacheMode)
7913
- ),
7914
- { method: "resources/templates/list" }
7915
- );
7916
- return { resourceTemplates: [...response.resourceTemplates] };
8900
+ const resourceTemplates = await this.listAllSalvaging({
8901
+ method: "resources/templates/list",
8902
+ itemsKey: "resourceTemplates",
8903
+ resultSchema: ListResourceTemplatesResultSchema,
8904
+ itemSchema: ResourceTemplateSchema,
8905
+ metadata: options?.metadata,
8906
+ aggregate: async () => [
8907
+ ...(await this.client.listResourceTemplates(
8908
+ this.aggregateListParams(options?.metadata),
8909
+ this.getCacheableRequestOptions(options?.cacheMode)
8910
+ )).resourceTemplates
8911
+ ]
8912
+ });
8913
+ return { resourceTemplates };
7917
8914
  }
7918
8915
  /**
7919
8916
  * List available prompts with pagination support
@@ -7952,13 +8949,20 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7952
8949
  if (!this.client) {
7953
8950
  throw new Error("Client is not connected");
7954
8951
  }
7955
- const response = await this.invokeMcpClient(
7956
- () => this.client.listPrompts(
7957
- this.aggregateListParams(options?.metadata),
7958
- this.getCacheableRequestOptions(options?.cacheMode)
7959
- )
7960
- );
7961
- return { prompts: [...response.prompts] };
8952
+ const prompts = await this.listAllSalvaging({
8953
+ method: "prompts/list",
8954
+ itemsKey: "prompts",
8955
+ resultSchema: ListPromptsResultSchema,
8956
+ itemSchema: PromptSchema,
8957
+ metadata: options?.metadata,
8958
+ aggregate: async () => [
8959
+ ...(await this.client.listPrompts(
8960
+ this.aggregateListParams(options?.metadata),
8961
+ this.getCacheableRequestOptions(options?.cacheMode)
8962
+ )).prompts
8963
+ ]
8964
+ });
8965
+ return { prompts };
7962
8966
  }
7963
8967
  /**
7964
8968
  * Get a prompt by name
@@ -8874,11 +9878,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8874
9878
  };
8875
9879
 
8876
9880
  // ../../core/mcp/state/managedListState.ts
8877
- import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
8878
9881
  var DEFAULT_LIST_CHANGED_DEBOUNCE_MS = 250;
8879
- function isClientDecodeRejection(err) {
8880
- return SdkError.isInstance(err) && (err.code === SdkErrorCode.InvalidResult || err.code === SdkErrorCode.UnsupportedResultType);
8881
- }
8882
9882
  var ManagedListState = class extends TypedEventTarget {
8883
9883
  items = [];
8884
9884
  client = null;
@@ -9509,6 +10509,7 @@ function useInspectorClient(inspectorClient) {
9509
10509
  const [excludedTools, setExcludedTools] = useState(
9510
10510
  inspectorClient?.getExcludedTools() ?? []
9511
10511
  );
10512
+ const [malformedListItems, setMalformedListItems] = useState(inspectorClient?.getMalformedListItems() ?? []);
9512
10513
  const [lastError, setLastError] = useState(void 0);
9513
10514
  useEffect(() => {
9514
10515
  if (!inspectorClient) {
@@ -9520,6 +10521,7 @@ function useInspectorClient(inspectorClient) {
9520
10521
  setProtocolEra(void 0);
9521
10522
  setDiscoverResult(void 0);
9522
10523
  setExcludedTools([]);
10524
+ setMalformedListItems([]);
9523
10525
  setLastError(void 0);
9524
10526
  return;
9525
10527
  }
@@ -9531,6 +10533,7 @@ function useInspectorClient(inspectorClient) {
9531
10533
  setProtocolEra(inspectorClient.getProtocolEra());
9532
10534
  setDiscoverResult(inspectorClient.getDiscoverResult());
9533
10535
  setExcludedTools(inspectorClient.getExcludedTools());
10536
+ setMalformedListItems(inspectorClient.getMalformedListItems());
9534
10537
  setLastError(void 0);
9535
10538
  const onStatusChange = (event) => {
9536
10539
  setStatus(event.detail);
@@ -9559,6 +10562,9 @@ function useInspectorClient(inspectorClient) {
9559
10562
  const onDiscoverResultChange = (event) => {
9560
10563
  setDiscoverResult(event.detail);
9561
10564
  };
10565
+ const onMalformedListItemsChange = (event) => {
10566
+ setMalformedListItems(event.detail);
10567
+ };
9562
10568
  const onExcludedToolsChange = (event) => {
9563
10569
  setExcludedTools(event.detail);
9564
10570
  };
@@ -9586,6 +10592,10 @@ function useInspectorClient(inspectorClient) {
9586
10592
  "excludedToolsChange",
9587
10593
  onExcludedToolsChange
9588
10594
  );
10595
+ inspectorClient.addEventListener(
10596
+ "malformedListItemsChange",
10597
+ onMalformedListItemsChange
10598
+ );
9589
10599
  return () => {
9590
10600
  inspectorClient.removeEventListener("statusChange", onStatusChange);
9591
10601
  inspectorClient.removeEventListener("error", onError);
@@ -9617,6 +10627,10 @@ function useInspectorClient(inspectorClient) {
9617
10627
  "excludedToolsChange",
9618
10628
  onExcludedToolsChange
9619
10629
  );
10630
+ inspectorClient.removeEventListener(
10631
+ "malformedListItemsChange",
10632
+ onMalformedListItemsChange
10633
+ );
9620
10634
  };
9621
10635
  }, [inspectorClient]);
9622
10636
  const connect = useCallback(async () => {
@@ -9642,6 +10656,7 @@ function useInspectorClient(inspectorClient) {
9642
10656
  protocolEra,
9643
10657
  discoverResult,
9644
10658
  excludedTools,
10659
+ malformedListItems,
9645
10660
  lastError,
9646
10661
  appRendererClient: inspectorClient?.getAppRendererClient() ?? null,
9647
10662
  connect,
@@ -9652,9 +10667,9 @@ function useInspectorClient(inspectorClient) {
9652
10667
  // ../../core/react/useManagedTools.ts
9653
10668
  import { useState as useState2, useEffect as useEffect2, useCallback as useCallback3 } from "react";
9654
10669
 
9655
- // ../../core/react/useManagedListError.ts
10670
+ // ../../core/react/useListError.ts
9656
10671
  import { useCallback as useCallback2, useSyncExternalStore } from "react";
9657
- function useManagedListError(state) {
10672
+ function useListError(state) {
9658
10673
  const subscribe = useCallback2(
9659
10674
  (onStoreChange) => {
9660
10675
  if (!state) return () => {
@@ -9706,7 +10721,7 @@ function useManagedTools(client, managedToolsState) {
9706
10721
  );
9707
10722
  };
9708
10723
  }, [managedToolsState]);
9709
- const error = useManagedListError(managedToolsState);
10724
+ const error = useListError(managedToolsState);
9710
10725
  const refresh = useCallback3(async () => {
9711
10726
  if (!managedToolsState || !client) return [];
9712
10727
  managedToolsState.clearListChanged();
@@ -9762,7 +10777,7 @@ function useManagedResources(client, managedResourcesState) {
9762
10777
  );
9763
10778
  };
9764
10779
  }, [managedResourcesState]);
9765
- const error = useManagedListError(managedResourcesState);
10780
+ const error = useListError(managedResourcesState);
9766
10781
  const refresh = useCallback4(async () => {
9767
10782
  if (!managedResourcesState || !client) return [];
9768
10783
  managedResourcesState.clearListChanged();
@@ -9800,7 +10815,7 @@ function useManagedResourceTemplates(client, managedResourceTemplatesState) {
9800
10815
  );
9801
10816
  };
9802
10817
  }, [managedResourceTemplatesState]);
9803
- const error = useManagedListError(managedResourceTemplatesState);
10818
+ const error = useListError(managedResourceTemplatesState);
9804
10819
  const refresh = useCallback5(async () => {
9805
10820
  if (!managedResourceTemplatesState || !client) return [];
9806
10821
  const next = await managedResourceTemplatesState.refresh(
@@ -9849,7 +10864,7 @@ function useManagedPrompts(client, managedPromptsState) {
9849
10864
  );
9850
10865
  };
9851
10866
  }, [managedPromptsState]);
9852
- const error = useManagedListError(managedPromptsState);
10867
+ const error = useListError(managedPromptsState);
9853
10868
  const refresh = useCallback6(async () => {
9854
10869
  if (!managedPromptsState || !client) return [];
9855
10870
  managedPromptsState.clearListChanged();
@@ -11796,7 +12811,7 @@ function ResourcesTab({
11796
12811
  setShouldFetchResource(null);
11797
12812
  }
11798
12813
  };
11799
- fetchContent();
12814
+ void fetchContent();
11800
12815
  }, [shouldFetchResource, inspectorClient, onAuthRecoveryRequired]);
11801
12816
  const listWidth = Math.floor(width * 0.4);
11802
12817
  const detailWidth = width - listWidth;
@@ -11979,7 +12994,7 @@ function PromptsTab({
11979
12994
  if (selectedPrompt.arguments && selectedPrompt.arguments.length > 0) {
11980
12995
  onFetchPrompt(selectedPrompt);
11981
12996
  } else {
11982
- (async () => {
12997
+ void (async () => {
11983
12998
  try {
11984
12999
  const invocation = await inspectorClient.getPrompt(
11985
13000
  selectedPrompt.name
@@ -14361,6 +15376,161 @@ var Form = (props) => {
14361
15376
  import { render } from "ink";
14362
15377
  import React24 from "react";
14363
15378
 
15379
+ // ../../core/json/nullableUnion.ts
15380
+ var RENDERABLE_TYPES = [
15381
+ "string",
15382
+ "number",
15383
+ "integer",
15384
+ "boolean",
15385
+ "array",
15386
+ "object"
15387
+ ];
15388
+ function isRenderableType(type) {
15389
+ return RENDERABLE_TYPES.includes(type);
15390
+ }
15391
+ function toBranch(value) {
15392
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
15393
+ return null;
15394
+ }
15395
+ return value;
15396
+ }
15397
+ function isStringEnum(value) {
15398
+ return Array.isArray(value) && value.length > 0 && value.every((member) => typeof member === "string");
15399
+ }
15400
+ function nullExcludedBySiblings(schema) {
15401
+ if (Array.isArray(schema.enum) && !schema.enum.includes(null)) {
15402
+ return true;
15403
+ }
15404
+ return schema.const !== void 0 && schema.const !== null;
15405
+ }
15406
+ var ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
15407
+ "title",
15408
+ "description",
15409
+ "default",
15410
+ "enumNames",
15411
+ "examples",
15412
+ "deprecated",
15413
+ "readOnly",
15414
+ "writeOnly",
15415
+ "$comment",
15416
+ "$schema",
15417
+ "$id"
15418
+ ]);
15419
+ function wrapperCarriesConstraints(schema) {
15420
+ return Object.keys(schema).some(
15421
+ (key) => key !== "anyOf" && !ANNOTATION_KEYWORDS.has(key)
15422
+ );
15423
+ }
15424
+ function typeNamesNull(type) {
15425
+ return Array.isArray(type) ? type.includes("null") : type === "null";
15426
+ }
15427
+ function hasOpaqueApplicator(schema) {
15428
+ return schema.not !== void 0 || schema.allOf !== void 0 || schema.oneOf !== void 0 || // `$ref` points at a schema this module never resolves, and it applies
15429
+ // *alongside* its siblings — `{ type: "string", $ref: "#/$defs/intOnly" }`
15430
+ // can be unsatisfiable. The wrapper path already declines on it (it is not
15431
+ // an annotation); this is the same rule for every other position.
15432
+ schema.$ref !== void 0;
15433
+ }
15434
+ function hasUnevaluatedComposition(schema) {
15435
+ return schema.anyOf !== void 0 || hasOpaqueApplicator(schema);
15436
+ }
15437
+ function stripNullEnumMembers(members, names) {
15438
+ if (!Array.isArray(members)) {
15439
+ return { kind: "unchanged" };
15440
+ }
15441
+ const kept = members.map((member, index) => ({ member, index })).filter((entry) => entry.member !== null);
15442
+ if (kept.length === 0) {
15443
+ return { kind: "unselectable" };
15444
+ }
15445
+ if (kept.length === members.length) {
15446
+ return { kind: "unchanged" };
15447
+ }
15448
+ const wasParallel = Array.isArray(names) && names.length === members.length;
15449
+ const aligned = wasParallel ? kept.map((entry) => names[entry.index]) : void 0;
15450
+ return {
15451
+ kind: "filtered",
15452
+ members: kept.map((entry) => entry.member),
15453
+ names: aligned
15454
+ };
15455
+ }
15456
+ function collapsed(schema, branch, type) {
15457
+ const merged = { ...schema, ...branch };
15458
+ const strip = stripNullEnumMembers(
15459
+ merged.enum,
15460
+ merged.enumNames
15461
+ );
15462
+ if (strip.kind === "unselectable") {
15463
+ return null;
15464
+ }
15465
+ return {
15466
+ ...merged,
15467
+ type,
15468
+ anyOf: void 0,
15469
+ // Read off the ORIGINAL schema, not the merge: a hoisted branch `enum` is
15470
+ // not a sibling constraint, and treating it as one would mark the very
15471
+ // shape #1928 is about as non-nullable. See `nullExcludedBySiblings`.
15472
+ nullable: admitsNull(schema),
15473
+ ...strip.kind === "filtered" ? { enum: strip.members, enumNames: strip.names } : {}
15474
+ };
15475
+ }
15476
+ function admitsNull(schema) {
15477
+ if (nullExcludedBySiblings(schema)) {
15478
+ return false;
15479
+ }
15480
+ if (hasOpaqueApplicator(schema)) {
15481
+ return false;
15482
+ }
15483
+ if (schema.nullable === true) {
15484
+ return true;
15485
+ }
15486
+ if (schema.type !== void 0) {
15487
+ if (schema.anyOf !== void 0) {
15488
+ return false;
15489
+ }
15490
+ return typeNamesNull(schema.type);
15491
+ }
15492
+ return schema.anyOf?.some((entry) => {
15493
+ const branch = toBranch(entry);
15494
+ if (branch === null || !typeNamesNull(branch.type)) {
15495
+ return false;
15496
+ }
15497
+ const branchSchema = branch;
15498
+ return !nullExcludedBySiblings(branchSchema) && !hasUnevaluatedComposition(branchSchema);
15499
+ }) ?? false;
15500
+ }
15501
+ function normalizeNullableUnion(schema) {
15502
+ if (schema.anyOf?.length === 2) {
15503
+ const branches = schema.anyOf.map(toBranch);
15504
+ const nullBranch = branches.find((entry) => entry?.type === "null");
15505
+ const branch = branches.find(
15506
+ (entry) => entry !== null && entry.type !== "null"
15507
+ );
15508
+ if (nullBranch && branch && !wrapperCarriesConstraints(schema) && !hasUnevaluatedComposition(branch)) {
15509
+ const type = branch.type ?? (isStringEnum(branch.enum) ? "string" : void 0);
15510
+ if (isRenderableType(type)) {
15511
+ const result = collapsed(schema, branch, type);
15512
+ if (result !== null) {
15513
+ return result;
15514
+ }
15515
+ }
15516
+ }
15517
+ }
15518
+ if (Array.isArray(schema.type) && schema.type.length === 2 && schema.type.includes("null") && // This path keeps the schema's own keywords, but `collapsed` clears `anyOf`
15519
+ // and the renderers ignore the other applicators — so a compound schema
15520
+ // like `{ type: ["string", "null"], anyOf: [{ const: "a" }] }` would be
15521
+ // widened into an unconstrained string field. Leave it for the JSON editor.
15522
+ !hasUnevaluatedComposition(schema)) {
15523
+ const type = schema.type.find((member) => member !== "null");
15524
+ if (isRenderableType(type)) {
15525
+ const result = collapsed(schema, void 0, type);
15526
+ if (result !== null) {
15527
+ return result;
15528
+ }
15529
+ }
15530
+ }
15531
+ return schema;
15532
+ }
15533
+
14364
15534
  // src/utils/schemaToForm.ts
14365
15535
  function toSelectOptions(values, names) {
14366
15536
  const useNames = names !== void 0 && names.length === values.length;
@@ -14380,7 +15550,9 @@ function schemaToForm(schema, toolName) {
14380
15550
  const properties = schema.properties || {};
14381
15551
  const required = schema.required || [];
14382
15552
  for (const [key, prop] of Object.entries(properties)) {
14383
- const property = typeof prop === "object" && prop !== null ? prop : {};
15553
+ const property = normalizeNullableUnion(
15554
+ typeof prop === "object" && prop !== null ? prop : {}
15555
+ );
14384
15556
  const baseField = {
14385
15557
  name: key,
14386
15558
  label: property.title || key,
@@ -14393,11 +15565,11 @@ function schemaToForm(schema, toolName) {
14393
15565
  ...baseField,
14394
15566
  options: toSelectOptions(property.items.enum, property.items.enumNames)
14395
15567
  };
14396
- } else if (property.enum) {
15568
+ } else if (isStringEnum(property.enum)) {
14397
15569
  field = {
14398
15570
  type: "select",
14399
15571
  ...baseField,
14400
- options: toSelectOptions(property.enum, property.enumNames)
15572
+ options: toSelectOptions(property.enum ?? [], property.enumNames)
14401
15573
  };
14402
15574
  } else {
14403
15575
  switch (property.type) {
@@ -14639,20 +15811,18 @@ import { Box as Box24, Text as Text21, useInput as useInput18 } from "ink";
14639
15811
  // src/utils/uriTemplateToForm.ts
14640
15812
  import { UriTemplate as UriTemplate2 } from "@modelcontextprotocol/client";
14641
15813
  function uriTemplateToForm(uriTemplate, templateName) {
14642
- const fields = [];
15814
+ let fields = [];
14643
15815
  try {
14644
- const template = new UriTemplate2(uriTemplate);
14645
- const variableNames = template.variableNames || [];
14646
- for (const variableName of variableNames) {
14647
- const field = {
14648
- name: variableName,
14649
- label: variableName,
14650
- type: "string",
14651
- required: false
14652
- // URI template variables are typically optional
14653
- };
14654
- fields.push(field);
14655
- }
15816
+ new UriTemplate2(uriTemplate);
15817
+ const mandatory = new Set(
15818
+ requiredGroups(uriTemplate).filter((names) => names.length === 1).map(([name]) => name)
15819
+ );
15820
+ fields = templateVariables(uriTemplate).map(({ name }) => ({
15821
+ name,
15822
+ label: name,
15823
+ type: "string",
15824
+ required: mandatory.has(name)
15825
+ }));
14656
15826
  } catch (error) {
14657
15827
  console.error("Failed to parse URI template:", error);
14658
15828
  }
@@ -14749,12 +15919,28 @@ function ResourceTestModal({
14749
15919
  );
14750
15920
  const handleFormSubmit = async (values) => {
14751
15921
  if (!inspectorClient || !template) return;
15922
+ const unmetGroups = unmetRequiredGroups(
15923
+ requiredGroups(template.uriTemplate),
15924
+ values
15925
+ );
15926
+ if (unmetGroups.length > 0) {
15927
+ const unmet = unmetGroups.map((names) => names.join(" or "));
15928
+ setResult({
15929
+ input: values,
15930
+ output: null,
15931
+ error: `Missing required template variable(s): ${unmet.join(", ")}`,
15932
+ duration: 0,
15933
+ uri: template.uriTemplate
15934
+ });
15935
+ setState("results");
15936
+ return;
15937
+ }
14752
15938
  setState("loading");
14753
15939
  const startTime = Date.now();
14754
15940
  try {
14755
15941
  const invocation = await inspectorClient.readResourceFromTemplate(
14756
15942
  template.uriTemplate,
14757
- values
15943
+ definedValues(values)
14758
15944
  );
14759
15945
  const duration = Date.now() - startTime;
14760
15946
  setResult({
@@ -15222,6 +16408,8 @@ function App({
15222
16408
  pendingStepUpRef.current = pendingStepUp;
15223
16409
  }, [pendingStepUp]);
15224
16410
  const [connectError, setConnectError] = useState23(null);
16411
+ const disconnectAttemptRef = useRef13(0);
16412
+ const [disconnectError, setDisconnectError] = useState23(null);
15225
16413
  const oauthInProgressRef = useRef13(false);
15226
16414
  const callbackServerRef = useRef13(null);
15227
16415
  const selectedServerRef = useRef13(null);
@@ -15409,6 +16597,8 @@ function App({
15409
16597
  useEffect20(() => {
15410
16598
  setOauthStatus("idle");
15411
16599
  setOauthMessage(null);
16600
+ disconnectAttemptRef.current++;
16601
+ setDisconnectError(null);
15412
16602
  const stepUp = pendingStepUpRef.current;
15413
16603
  if (stepUp && selectedServer && stepUp.serverName !== selectedServer) {
15414
16604
  setPendingStepUp(null);
@@ -15653,6 +16843,8 @@ function App({
15653
16843
  if (!selectedServer || !selectedInspectorClient || !selectedServerConfig) {
15654
16844
  return;
15655
16845
  }
16846
+ disconnectAttemptRef.current++;
16847
+ setDisconnectError(null);
15656
16848
  const finishConnect = async () => {
15657
16849
  await connectInspector();
15658
16850
  setConnectError(null);
@@ -15760,7 +16952,17 @@ function App({
15760
16952
  }, [inspectorClients, handleAuthRecoveryRequired]);
15761
16953
  const handleDisconnect = useCallback11(async () => {
15762
16954
  if (!selectedServer) return;
15763
- await disconnectInspector();
16955
+ const attempt = ++disconnectAttemptRef.current;
16956
+ const attemptServer = selectedServer;
16957
+ setDisconnectError(null);
16958
+ try {
16959
+ await disconnectInspector();
16960
+ } catch (err) {
16961
+ if (disconnectAttemptRef.current !== attempt || selectedServerRef.current !== attemptServer) {
16962
+ return;
16963
+ }
16964
+ setDisconnectError(err instanceof Error ? err.message : String(err));
16965
+ }
15764
16966
  }, [selectedServer, disconnectInspector]);
15765
16967
  const handleClearOAuth = useCallback11(async () => {
15766
16968
  if (!selectedInspectorClient) return;
@@ -16121,9 +17323,9 @@ function App({
16121
17323
  }
16122
17324
  if (selectedServer) {
16123
17325
  if (input.toLowerCase() === "c" && (inspectorStatus === "disconnected" || inspectorStatus === "error")) {
16124
- handleConnect();
17326
+ void handleConnect();
16125
17327
  } else if (input.toLowerCase() === "d" && (inspectorStatus === "connected" || inspectorStatus === "connecting")) {
16126
- handleDisconnect();
17328
+ void handleDisconnect();
16127
17329
  }
16128
17330
  }
16129
17331
  });
@@ -16294,6 +17496,10 @@ function App({
16294
17496
  oauthStatus === "error" && oauthMessage && /* @__PURE__ */ jsx16(Box27, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text24, { color: "red", children: [
16295
17497
  "OAuth: ",
16296
17498
  oauthMessage
17499
+ ] }) }),
17500
+ disconnectError && /* @__PURE__ */ jsx16(Box27, { marginTop: 1, children: /* @__PURE__ */ jsxs15(Text24, { color: "red", children: [
17501
+ "Disconnect failed: ",
17502
+ disconnectError
16297
17503
  ] }) })
16298
17504
  ] })
16299
17505
  }