@modelcontextprotocol/inspector 2.4.0 → 2.5.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.
@@ -390,6 +390,491 @@ async function deleteStoreFile(filePath) {
390
390
  }
391
391
  }
392
392
 
393
+ // ../../core/json/nullableUnion.ts
394
+ var RENDERABLE_TYPES = [
395
+ "string",
396
+ "number",
397
+ "integer",
398
+ "boolean",
399
+ "array",
400
+ "object"
401
+ ];
402
+ function isRenderableType(type) {
403
+ return RENDERABLE_TYPES.includes(type);
404
+ }
405
+ function toBranch(value) {
406
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
407
+ return null;
408
+ }
409
+ return value;
410
+ }
411
+ function isStringEnum(value) {
412
+ return Array.isArray(value) && value.length > 0 && value.every((member) => typeof member === "string");
413
+ }
414
+ function nullExcludedBySiblings(schema) {
415
+ if (Array.isArray(schema.enum) && !schema.enum.includes(null)) {
416
+ return true;
417
+ }
418
+ return schema.const !== void 0 && schema.const !== null;
419
+ }
420
+ var ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
421
+ "title",
422
+ "description",
423
+ "default",
424
+ "enumNames",
425
+ "examples",
426
+ "deprecated",
427
+ "readOnly",
428
+ "writeOnly",
429
+ "$comment",
430
+ "$schema",
431
+ "$id"
432
+ ]);
433
+ function wrapperCarriesConstraints(schema) {
434
+ return Object.keys(schema).some(
435
+ (key) => key !== "anyOf" && !ANNOTATION_KEYWORDS.has(key)
436
+ );
437
+ }
438
+ function typeNamesNull(type) {
439
+ return Array.isArray(type) ? type.includes("null") : type === "null";
440
+ }
441
+ function hasOpaqueApplicator(schema) {
442
+ 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
443
+ // *alongside* its siblings — `{ type: "string", $ref: "#/$defs/intOnly" }`
444
+ // can be unsatisfiable. The wrapper path already declines on it (it is not
445
+ // an annotation); this is the same rule for every other position.
446
+ schema.$ref !== void 0;
447
+ }
448
+ function hasUnevaluatedComposition(schema) {
449
+ return schema.anyOf !== void 0 || hasOpaqueApplicator(schema);
450
+ }
451
+ function stripNullEnumMembers(members, names) {
452
+ if (!Array.isArray(members)) {
453
+ return { kind: "unchanged" };
454
+ }
455
+ const kept = members.map((member, index) => ({ member, index })).filter((entry) => entry.member !== null);
456
+ if (kept.length === 0) {
457
+ return { kind: "unselectable" };
458
+ }
459
+ if (kept.length === members.length) {
460
+ return { kind: "unchanged" };
461
+ }
462
+ const wasParallel = Array.isArray(names) && names.length === members.length;
463
+ const aligned = wasParallel ? kept.map((entry) => names[entry.index]) : void 0;
464
+ return {
465
+ kind: "filtered",
466
+ members: kept.map((entry) => entry.member),
467
+ names: aligned
468
+ };
469
+ }
470
+ function collapsed(schema, branch, type) {
471
+ const merged = { ...schema, ...branch };
472
+ const strip = stripNullEnumMembers(
473
+ merged.enum,
474
+ merged.enumNames
475
+ );
476
+ if (strip.kind === "unselectable") {
477
+ return null;
478
+ }
479
+ return {
480
+ ...merged,
481
+ type,
482
+ anyOf: void 0,
483
+ // Read off the ORIGINAL schema, not the merge: a hoisted branch `enum` is
484
+ // not a sibling constraint, and treating it as one would mark the very
485
+ // shape #1928 is about as non-nullable. See `nullExcludedBySiblings`.
486
+ nullable: admitsNull(schema),
487
+ ...strip.kind === "filtered" ? { enum: strip.members, enumNames: strip.names } : {}
488
+ };
489
+ }
490
+ function typeAdmitsNull(type) {
491
+ return type === void 0 || typeNamesNull(type);
492
+ }
493
+ function admitsNull(schema) {
494
+ if (nullExcludedBySiblings(schema)) {
495
+ return false;
496
+ }
497
+ if (hasOpaqueApplicator(schema)) {
498
+ return false;
499
+ }
500
+ if (schema.const === null) {
501
+ if (schema.nullable !== true && !typeAdmitsNull(schema.type)) return false;
502
+ return schema.anyOf === void 0 || anyOfAdmitsNull(schema);
503
+ }
504
+ if (Array.isArray(schema.enum) && schema.enum.includes(null)) {
505
+ return (schema.nullable === true || typeAdmitsNull(schema.type)) && (schema.anyOf === void 0 || anyOfAdmitsNull(schema));
506
+ }
507
+ if (schema.nullable === true) {
508
+ return true;
509
+ }
510
+ if (schema.type !== void 0) {
511
+ if (schema.anyOf !== void 0) {
512
+ return false;
513
+ }
514
+ return typeNamesNull(schema.type);
515
+ }
516
+ return anyOfAdmitsNull(schema);
517
+ }
518
+ var ANNOTATION_ONLY_KEYWORDS = /* @__PURE__ */ new Set([
519
+ "title",
520
+ "description",
521
+ "examples",
522
+ "default",
523
+ "deprecated",
524
+ "readOnly",
525
+ "writeOnly",
526
+ "$comment",
527
+ // Core keywords that identify or declare rather than assert. `$ref` is
528
+ // deliberately absent: it applies whatever it points at, which is exactly an
529
+ // assertion this module cannot see.
530
+ "$defs",
531
+ "$id",
532
+ "$schema",
533
+ "$anchor",
534
+ "$dynamicAnchor",
535
+ "$vocabulary"
536
+ ]);
537
+ function constrainsNothing(schema) {
538
+ return Object.keys(schema).every(
539
+ (keyword) => ANNOTATION_ONLY_KEYWORDS.has(keyword)
540
+ );
541
+ }
542
+ function anyOfAdmitsNull(schema) {
543
+ const branches = schema.anyOf;
544
+ if (!Array.isArray(branches)) return false;
545
+ return branches.some((entry) => {
546
+ if (typeof entry === "boolean") return entry;
547
+ const branch = toBranch(entry);
548
+ if (branch === null) return false;
549
+ const branchSchema = branch;
550
+ if (hasUnevaluatedComposition(branchSchema)) return false;
551
+ if (constrainsNothing(branchSchema)) return true;
552
+ return admitsNull(branchSchema);
553
+ });
554
+ }
555
+ function normalizeNullableUnion(schema) {
556
+ if (schema.anyOf?.length === 2) {
557
+ const branches = schema.anyOf.map(toBranch);
558
+ const nullBranch = branches.find((entry) => entry?.type === "null");
559
+ const branch = branches.find(
560
+ (entry) => entry !== null && entry.type !== "null"
561
+ );
562
+ if (nullBranch && branch && !wrapperCarriesConstraints(schema) && !hasUnevaluatedComposition(branch)) {
563
+ const type = branch.type ?? (isStringEnum(branch.enum) ? "string" : void 0);
564
+ if (isRenderableType(type)) {
565
+ const result = collapsed(schema, branch, type);
566
+ if (result !== null) {
567
+ return result;
568
+ }
569
+ }
570
+ }
571
+ }
572
+ 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`
573
+ // and the renderers ignore the other applicators — so a compound schema
574
+ // like `{ type: ["string", "null"], anyOf: [{ const: "a" }] }` would be
575
+ // widened into an unconstrained string field. Leave it for the JSON editor.
576
+ !hasUnevaluatedComposition(schema)) {
577
+ const type = schema.type.find((member) => member !== "null");
578
+ if (isRenderableType(type)) {
579
+ const result = collapsed(schema, void 0, type);
580
+ if (result !== null) {
581
+ return result;
582
+ }
583
+ }
584
+ }
585
+ return schema;
586
+ }
587
+
588
+ // ../../core/json/rootUnion.ts
589
+ function membersOf(value) {
590
+ return Array.isArray(value) ? value : [];
591
+ }
592
+ function requiredOf(schema) {
593
+ const { required } = schema;
594
+ return Array.isArray(required) ? required.filter((name) => typeof name === "string") : [];
595
+ }
596
+ function toBranch2(value) {
597
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
598
+ return null;
599
+ }
600
+ return value;
601
+ }
602
+ function admitsObject(schema) {
603
+ const { type } = schema;
604
+ if (type === void 0) return true;
605
+ return Array.isArray(type) ? type.includes("object") : type === "object";
606
+ }
607
+ function hasReadableProperties(schema) {
608
+ const { properties } = schema;
609
+ return typeof properties === "object" && properties !== null && !Array.isArray(properties);
610
+ }
611
+ function propertiesOf(schema) {
612
+ return hasReadableProperties(schema) ? schema.properties : {};
613
+ }
614
+ function isOfferable(branch, merged) {
615
+ if (branch.properties !== void 0 && !hasReadableProperties(branch)) {
616
+ return false;
617
+ }
618
+ if (!admitsObject(branch)) return false;
619
+ const properties = Object.values(propertiesOf(merged));
620
+ return properties.length > 0 && // Every value has to be something a renderer can read AND something a
621
+ // caller can satisfy. A `null` or an array is not a schema at all, and the
622
+ // web form dereferences one on the way to choosing a widget. JSON Schema's
623
+ // boolean form is legal, but only `true` is harmless — it constrains
624
+ // nothing and answers every keyword lookup with `undefined`, while `false`
625
+ // admits no value whatsoever, so a field declared with it can never be
626
+ // filled and a required one makes the whole branch unsatisfiable. Either
627
+ // way the branch is declined rather than offered as a callable shape.
628
+ properties.every(
629
+ (property) => property === true || typeof property === "object" && property !== null && !Array.isArray(property)
630
+ );
631
+ }
632
+ var ANNOTATION_KEYWORDS2 = /* @__PURE__ */ new Set([
633
+ "title",
634
+ "description",
635
+ "examples",
636
+ "deprecated",
637
+ "readOnly",
638
+ "writeOnly",
639
+ "$comment",
640
+ // An annotation too: a suggested initial value constrains nothing, so two
641
+ // declarations suggesting different ones still accept the same values.
642
+ "default"
643
+ ]);
644
+ function canonicalJson(value) {
645
+ if (Array.isArray(value)) {
646
+ return `[${value.map(canonicalJson).join(",")}]`;
647
+ }
648
+ if (typeof value === "object" && value !== null) {
649
+ const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(
650
+ ([key, member]) => `${JSON.stringify(key)}:${canonicalJson(member)}`
651
+ );
652
+ return `{${entries.join(",")}}`;
653
+ }
654
+ return JSON.stringify(value) ?? "undefined";
655
+ }
656
+ function sameJsonValue(a, b) {
657
+ return sameValue(a, b);
658
+ }
659
+ function sameValue(a, b) {
660
+ return a === b || canonicalJson(a) === canonicalJson(b);
661
+ }
662
+ function conflicts(baseProperty, branchProperty) {
663
+ const a = toBranch2(baseProperty);
664
+ const b = toBranch2(branchProperty);
665
+ if (a === null || b === null) {
666
+ return !sameValue(baseProperty, branchProperty);
667
+ }
668
+ const left = a;
669
+ const right = b;
670
+ if (Object.keys(right).some(
671
+ (keyword) => !ANNOTATION_KEYWORDS2.has(keyword) && keyword in left && !sameValue(left[keyword], right[keyword])
672
+ )) {
673
+ return true;
674
+ }
675
+ return !constSatisfiesSiblings({ ...left, ...right });
676
+ }
677
+ function jsonTypeOf(value) {
678
+ if (value === null) return "null";
679
+ if (Array.isArray(value)) return "array";
680
+ if (typeof value === "number") {
681
+ return Number.isInteger(value) ? "integer" : "number";
682
+ }
683
+ return typeof value;
684
+ }
685
+ var CONST_CHECKABLE = /* @__PURE__ */ new Set(["const", "type", "enum"]);
686
+ function constSatisfiesSiblings(schema) {
687
+ if (!("const" in schema)) return true;
688
+ const value = schema.const;
689
+ const { type, enum: allowed } = schema;
690
+ if (Object.keys(schema).some(
691
+ (keyword) => !CONST_CHECKABLE.has(keyword) && !ANNOTATION_KEYWORDS2.has(keyword)
692
+ )) {
693
+ return false;
694
+ }
695
+ const actual = jsonTypeOf(value);
696
+ const admits = (name) => name === actual || name === "number" && actual === "integer";
697
+ if (typeof type === "string" && !admits(type)) return false;
698
+ if (Array.isArray(type) && !type.some(admits)) return false;
699
+ if (Array.isArray(allowed) && !allowed.some((member) => sameValue(member, value))) {
700
+ return false;
701
+ }
702
+ return true;
703
+ }
704
+ function mergeProperty(baseProperty, branchProperty) {
705
+ const a = toBranch2(baseProperty);
706
+ const b = toBranch2(branchProperty);
707
+ if (a === null || b === null) {
708
+ return branchProperty;
709
+ }
710
+ return { ...a, ...b };
711
+ }
712
+ var MERGEABLE_KEYWORDS = /* @__PURE__ */ new Set(["type", "properties", "required"]);
713
+ function isFlattenable(member) {
714
+ const branch = toBranch2(member);
715
+ if (branch === null || !admitsObject(branch)) return false;
716
+ return Object.keys(branch).every(
717
+ (keyword) => MERGEABLE_KEYWORDS.has(keyword) || ANNOTATION_KEYWORDS2.has(keyword)
718
+ );
719
+ }
720
+ function conflictsWithBase(base, branch) {
721
+ const baseProperties = propertiesOf(base);
722
+ const branchProperties = propertiesOf(branch);
723
+ return Object.entries(branchProperties).some(
724
+ ([name, branchProperty]) => (
725
+ // `hasOwn`, not `in`: `properties` is a JSON record, so `constructor` and
726
+ // `toString` are legal argument names that `in` would find on
727
+ // `Object.prototype` and report as collisions the root never declared.
728
+ Object.hasOwn(baseProperties, name) && conflicts(baseProperties[name], branchProperty)
729
+ )
730
+ );
731
+ }
732
+ function hasDiscriminator(members, named, rootRequired) {
733
+ const first = propertiesOf(members[0] ?? {});
734
+ const candidates = named !== void 0 ? [named] : Object.keys(first);
735
+ return candidates.some((name) => {
736
+ const constants = members.map((member) => {
737
+ if (!rootRequired.includes(name) && !requiredOf(member).includes(name)) {
738
+ return void 0;
739
+ }
740
+ const property = toBranch2(propertiesOf(member)[name]);
741
+ return property?.const;
742
+ });
743
+ if (constants.some((value) => value === void 0)) return false;
744
+ const seen = new Set(constants.map(canonicalJson));
745
+ return seen.size === constants.length;
746
+ });
747
+ }
748
+ function restrictsAdditional(schema) {
749
+ const additional = schema.additionalProperties;
750
+ return additional === false || typeof additional === "object" && additional !== null && Object.keys(additional).some(
751
+ (keyword) => !ANNOTATION_KEYWORDS2.has(keyword)
752
+ );
753
+ }
754
+ function addsForbiddenNames(base, member) {
755
+ if (!restrictsAdditional(base)) return false;
756
+ const baseNames = propertiesOf(base);
757
+ return Object.keys(propertiesOf(member)).some(
758
+ (name) => !Object.hasOwn(baseNames, name)
759
+ );
760
+ }
761
+ function mergeBranch(base, branch) {
762
+ const baseProperties = propertiesOf(base);
763
+ const branchProperties = propertiesOf(branch);
764
+ const properties = Object.fromEntries([
765
+ ...Object.entries(baseProperties),
766
+ ...Object.entries(branchProperties).map(([name, branchProperty]) => [
767
+ name,
768
+ Object.hasOwn(baseProperties, name) ? mergeProperty(baseProperties[name], branchProperty) : branchProperty
769
+ ])
770
+ ]);
771
+ const baseRequired = requiredOf(base);
772
+ const required = [
773
+ ...baseRequired,
774
+ ...requiredOf(branch).filter((name) => !baseRequired.includes(name))
775
+ ];
776
+ return {
777
+ ...base,
778
+ ...Object.keys(properties).length > 0 ? { properties } : {},
779
+ ...required.length > 0 ? { required } : {}
780
+ };
781
+ }
782
+ function withoutComposition(schema) {
783
+ const { allOf: _allOf, anyOf: _anyOf, oneOf: _oneOf, ...rest } = schema;
784
+ return rest;
785
+ }
786
+ function branchLabel(branch, index, discriminatorProperty) {
787
+ if (typeof branch.title === "string" && branch.title.trim() !== "") {
788
+ return branch.title;
789
+ }
790
+ const properties = propertiesOf(branch);
791
+ const constOf = (name) => {
792
+ const property = toBranch2(properties[name]);
793
+ if (property === null || !("const" in property)) return void 0;
794
+ const value = property.const;
795
+ return value === null || typeof value !== "object" ? String(value) : void 0;
796
+ };
797
+ if (discriminatorProperty !== void 0) {
798
+ const value = constOf(discriminatorProperty);
799
+ if (value !== void 0) return value;
800
+ }
801
+ const constants = Object.keys(properties).map((name) => constOf(name)).filter((value) => value !== void 0);
802
+ if (constants.length === 1) return constants[0];
803
+ return `Option ${index + 1}`;
804
+ }
805
+ function resolveRootUnion(schema) {
806
+ const allOfMembers = membersOf(schema.allOf);
807
+ let merged = schema;
808
+ for (const member of allOfMembers) {
809
+ const branch = toBranch2(member);
810
+ if (branch === null || !isFlattenable(member) || conflictsWithBase(merged, branch) || addsForbiddenNames(merged, branch)) {
811
+ return { base: schema, branches: [] };
812
+ }
813
+ merged = mergeBranch(merged, branch);
814
+ }
815
+ const base = withoutComposition(merged);
816
+ if (schema.oneOf !== void 0 && schema.anyOf !== void 0) {
817
+ return { base, branches: [] };
818
+ }
819
+ const isExclusiveUnion = schema.oneOf !== void 0;
820
+ const members = membersOf(schema.oneOf ?? schema.anyOf);
821
+ const branches = members.map(toBranch2);
822
+ if (branches.length === 0 || branches.some(
823
+ (branch) => branch === null || !isOfferable(branch, mergeBranch(base, branch)) || // The same faithfulness test the `allOf` fold applies: a member
824
+ // carrying a constraint the merge does not copy — a nested `allOf`, a
825
+ // `not`, a `$ref` — would have it erased along with the union keyword,
826
+ // which can present an unsatisfiable branch as a callable one.
827
+ !isFlattenable(branch) || conflictsWithBase(base, branch)
828
+ )) {
829
+ return { base, branches: [] };
830
+ }
831
+ if (branches.some(
832
+ (branch) => addsForbiddenNames(base, branch)
833
+ )) {
834
+ return { base, branches: [] };
835
+ }
836
+ if (isExclusiveUnion && !hasDiscriminator(
837
+ branches,
838
+ schema.discriminator?.propertyName,
839
+ requiredOf(base)
840
+ )) {
841
+ return { base, branches: [] };
842
+ }
843
+ const discriminatorProperty = schema.discriminator?.propertyName;
844
+ return {
845
+ base,
846
+ branches: branches.filter((branch) => branch !== null).map((branch, index) => ({
847
+ // `base` is already composition-free and `mergeBranch` copies only
848
+ // `properties`/`required` off the branch, so the merge stays that way.
849
+ schema: mergeBranch(base, branch),
850
+ label: branchLabel(branch, index, discriminatorProperty),
851
+ declaredFields: Object.keys(propertiesOf(branch))
852
+ }))
853
+ };
854
+ }
855
+ function narrowBySuppliedNames(branches, candidates, suppliedNames) {
856
+ if (candidates.length === 1) return candidates[0];
857
+ if (candidates.length === 0 || suppliedNames.length === 0) return null;
858
+ const supplied = new Set(suppliedNames);
859
+ const satisfied = candidates.filter((index) => {
860
+ const branch = branches[index];
861
+ const required = requiredOf(branch.schema);
862
+ const own = required.filter((name) => branch.declaredFields.includes(name));
863
+ return own.length > 0 && own.every((name) => supplied.has(name));
864
+ });
865
+ if (satisfied.length === 1) return satisfied[0];
866
+ const exclusiveTo = /* @__PURE__ */ new Map();
867
+ for (const index of candidates) {
868
+ for (const name of branches[index].declaredFields) {
869
+ exclusiveTo.set(name, exclusiveTo.has(name) ? -1 : index);
870
+ }
871
+ }
872
+ const named = new Set(
873
+ suppliedNames.map((name) => exclusiveTo.get(name)).filter((index) => index !== void 0 && index >= 0)
874
+ );
875
+ return named.size === 1 ? [...named][0] : null;
876
+ }
877
+
393
878
  // ../../core/json/jsonUtils.ts
394
879
  function isSerializableJson(value) {
395
880
  if (value === null) return true;
@@ -429,16 +914,90 @@ function convertParameterValue(value, schema) {
429
914
  }
430
915
  return value;
431
916
  }
917
+ function coercionProperties(base, branches, params) {
918
+ if (branches.length === 0) {
919
+ return { ...base.properties };
920
+ }
921
+ const candidates = branches.map((branch, index) => ({ branch, index })).filter(
922
+ ({ branch }) => matchesConstants(branch.schema.properties ?? {}, params)
923
+ ).map(({ index }) => index);
924
+ const selected = narrowBySuppliedNames(
925
+ branches,
926
+ candidates,
927
+ Object.keys(params)
928
+ );
929
+ if (selected !== null) {
930
+ return { ...branches[selected].schema.properties };
931
+ }
932
+ const properties = Object.fromEntries(
933
+ Object.entries(base.properties ?? {})
934
+ );
935
+ const pool = candidates.length > 0 ? candidates.map((i) => branches[i]) : branches;
936
+ for (const name of new Set(pool.flatMap((b) => b.declaredFields))) {
937
+ const declarations = pool.filter((branch) => branch.declaredFields.includes(name)).map((branch) => branch.schema.properties?.[name]).filter((schema) => typeof schema === "object" && schema !== null).map((schema) => normalizeNullableUnion(schema));
938
+ const types = new Set(declarations.map((schema) => typeNameOf(schema)));
939
+ const pinnedOf = (schema) => typeof schema === "object" && schema !== null && "const" in schema ? schema.const : void 0;
940
+ const constsAgree = declarations.every(
941
+ (schema) => sameJsonValue(pinnedOf(schema), pinnedOf(declarations[0]))
942
+ );
943
+ if (types.size === 1 && constsAgree && declarations.length > 0) {
944
+ Object.defineProperty(properties, name, {
945
+ value: declarations[0],
946
+ writable: true,
947
+ enumerable: true,
948
+ configurable: true
949
+ });
950
+ } else {
951
+ delete properties[name];
952
+ }
953
+ }
954
+ return properties;
955
+ }
956
+ function typeNameOf(schema) {
957
+ if (typeof schema !== "object" || schema === null) return "";
958
+ const { type } = schema;
959
+ return Array.isArray(type) ? [...type].map(String).sort().join(",") : typeof type === "string" ? type : "";
960
+ }
961
+ function matchesConstants(properties, params) {
962
+ return Object.entries(properties).every(([name, schema]) => {
963
+ if (typeof schema !== "object" || schema === null) return true;
964
+ const constValue = schema.const;
965
+ if (constValue === void 0) return true;
966
+ if (!Object.hasOwn(params, name)) return true;
967
+ const supplied = params[name];
968
+ return supplied === void 0 || suppliedMatchesConst(supplied, constValue);
969
+ });
970
+ }
971
+ function suppliedMatchesConst(value, constValue) {
972
+ if (constValue === null || typeof constValue !== "object") {
973
+ return value === String(constValue);
974
+ }
975
+ try {
976
+ return sameJsonValue(JSON.parse(value), constValue);
977
+ } catch {
978
+ return false;
979
+ }
980
+ }
432
981
  function convertToolParameters(tool, params) {
982
+ return convertParametersForSchema(tool.inputSchema, params);
983
+ }
984
+ function convertParametersForSchema(inputSchema, params) {
433
985
  const result = {};
434
- const properties = tool.inputSchema?.properties || {};
986
+ const { base, branches } = resolveRootUnion(
987
+ inputSchema ?? {}
988
+ );
989
+ const properties = coercionProperties(base, branches, params);
435
990
  for (const [key, value] of Object.entries(params)) {
436
- const paramSchema = properties[key];
437
- if (paramSchema) {
438
- result[key] = convertParameterValue(value, paramSchema);
439
- } else {
440
- result[key] = value;
441
- }
991
+ const declared = properties[key];
992
+ const paramSchema = typeof declared === "object" && declared !== null ? normalizeNullableUnion(declared) : declared;
993
+ const pinned = paramSchema?.const;
994
+ const converted = pinned !== void 0 && suppliedMatchesConst(value, pinned) ? pinned : paramSchema ? convertParameterValue(value, paramSchema) : value;
995
+ Object.defineProperty(result, key, {
996
+ value: converted,
997
+ writable: true,
998
+ enumerable: true,
999
+ configurable: true
1000
+ });
442
1001
  }
443
1002
  return result;
444
1003
  }
@@ -675,6 +1234,9 @@ function storedFieldsToInspectorSettings(stored) {
675
1234
  if (stored.oauth?.requestRefreshToken === false) {
676
1235
  settings.oauthRequestRefreshToken = false;
677
1236
  }
1237
+ if (stored.oauth?.revokeOnClear === false) {
1238
+ settings.oauthRevokeOnClear = false;
1239
+ }
678
1240
  if (stored.cwd) settings.cwd = stored.cwd;
679
1241
  return settings;
680
1242
  }
@@ -3460,6 +4022,25 @@ var OAuthStorageBase = class {
3460
4022
  this.memory.getState().setServerState(serverUrl, { discoveryState: void 0 });
3461
4023
  await this.persist();
3462
4024
  }
4025
+ async takeRevocationSnapshot(serverUrl) {
4026
+ await this.ensureLoaded();
4027
+ const state = this.memory.getState().getServerState(serverUrl);
4028
+ const snapshot = {
4029
+ byIssuer: Object.fromEntries(
4030
+ Object.entries(state.byIssuer ?? {}).map(([issuer, slot]) => [
4031
+ issuer,
4032
+ { tokens: slot?.tokens, clientInformation: slot?.clientInformation }
4033
+ ])
4034
+ ),
4035
+ legacyTokens: state.tokens,
4036
+ legacyClientInformation: state.clientInformation,
4037
+ preregisteredClientInformation: state.preregisteredClientInformation,
4038
+ serverMetadata: state.serverMetadata
4039
+ };
4040
+ this.memory.getState().clearServerState(serverUrl);
4041
+ await this.persist();
4042
+ return snapshot;
4043
+ }
3463
4044
  async clear(serverUrl) {
3464
4045
  await this.ensureLoaded();
3465
4046
  this.memory.getState().clearServerState(serverUrl);
@@ -3650,6 +4231,288 @@ var NodeOAuthStorage = class extends OAuthStorageBase {
3650
4231
  }
3651
4232
  };
3652
4233
 
4234
+ // ../../core/auth/revocation.ts
4235
+ import { selectClientAuthMethod } from "@modelcontextprotocol/client";
4236
+ import {
4237
+ OAuthClientInformationSchema as OAuthClientInformationSchema2,
4238
+ OAuthTokensSchema as OAuthTokensSchema2
4239
+ } from "@modelcontextprotocol/core";
4240
+ var DEFAULT_REVOCATION_TIMEOUT_MS = 5e3;
4241
+ function selectRevocableToken(tokens) {
4242
+ if (!tokens) return null;
4243
+ if (tokens.refresh_token) {
4244
+ return { token: tokens.refresh_token, tokenTypeHint: "refresh_token" };
4245
+ }
4246
+ if (tokens.access_token) {
4247
+ return { token: tokens.access_token, tokenTypeHint: "access_token" };
4248
+ }
4249
+ return null;
4250
+ }
4251
+ function revocationAuthMethods(metadata) {
4252
+ return metadata.revocation_endpoint_auth_methods_supported ?? [
4253
+ "client_secret_basic"
4254
+ ];
4255
+ }
4256
+ function buildRevocationRequest(params) {
4257
+ const body = new URLSearchParams({
4258
+ token: params.token,
4259
+ token_type_hint: params.tokenTypeHint
4260
+ });
4261
+ const headers = {
4262
+ "Content-Type": "application/x-www-form-urlencoded",
4263
+ Accept: "application/json"
4264
+ };
4265
+ const client = params.clientInformation;
4266
+ if (client) {
4267
+ const method = selectClientAuthMethod(client, params.supportedAuthMethods);
4268
+ if (method === "client_secret_basic" && client.client_secret) {
4269
+ const credentials = `${encodeURIComponent(client.client_id)}:${encodeURIComponent(client.client_secret)}`;
4270
+ headers.Authorization = `Basic ${base64Encode(credentials)}`;
4271
+ } else if (method === "client_secret_post" && client.client_secret) {
4272
+ body.set("client_id", client.client_id);
4273
+ body.set("client_secret", client.client_secret);
4274
+ } else {
4275
+ body.set("client_id", client.client_id);
4276
+ }
4277
+ }
4278
+ return {
4279
+ url: params.endpoint,
4280
+ init: { method: "POST", headers, body: body.toString() }
4281
+ };
4282
+ }
4283
+ function base64Encode(value) {
4284
+ if (typeof btoa === "function") return btoa(value);
4285
+ return Buffer.from(value, "utf8").toString("base64");
4286
+ }
4287
+ async function revokeToken(params) {
4288
+ const timeoutMs = params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS;
4289
+ try {
4290
+ const { url, init } = buildRevocationRequest(params);
4291
+ const response = await withDeadline(
4292
+ params.fetchFn(url, { ...init, signal: AbortSignal.timeout(timeoutMs) }),
4293
+ timeoutMs
4294
+ );
4295
+ if (!response.ok) {
4296
+ return {
4297
+ status: "failed",
4298
+ endpoint: url,
4299
+ detail: `revocation endpoint responded ${response.status} ${response.statusText}`.trim()
4300
+ };
4301
+ }
4302
+ return {
4303
+ status: "revoked",
4304
+ tokenTypeHint: params.tokenTypeHint,
4305
+ endpoint: url
4306
+ };
4307
+ } catch (err) {
4308
+ return {
4309
+ // `params.endpoint`, not the built `url`: construction is inside the try
4310
+ // now, so `url` may not exist on this path.
4311
+ status: "failed",
4312
+ endpoint: params.endpoint,
4313
+ detail: err instanceof Error ? err.message : String(err)
4314
+ };
4315
+ }
4316
+ }
4317
+ async function withDeadline(promise, timeoutMs) {
4318
+ let timer;
4319
+ const deadline = new Promise((_resolve, reject) => {
4320
+ timer = setTimeout(
4321
+ () => reject(new Error(`revocation request timed out after ${timeoutMs}ms`)),
4322
+ timeoutMs
4323
+ );
4324
+ });
4325
+ try {
4326
+ return await Promise.race([promise, deadline]);
4327
+ } finally {
4328
+ if (timer !== void 0) clearTimeout(timer);
4329
+ }
4330
+ }
4331
+ function emptyPlan(serverUrl, outcome, failures = []) {
4332
+ return {
4333
+ serverUrl,
4334
+ grants: [],
4335
+ failures,
4336
+ supportedAuthMethods: [],
4337
+ outcome: failures.length > 0 ? aggregateOutcomes([...failures, outcome]) : outcome
4338
+ };
4339
+ }
4340
+ async function clearAndPlanRevocation(params) {
4341
+ const { serverUrl, storage } = params;
4342
+ let snapshot;
4343
+ try {
4344
+ snapshot = await storage.takeRevocationSnapshot(serverUrl);
4345
+ } catch (err) {
4346
+ throw err instanceof Error ? err : new Error(String(err));
4347
+ }
4348
+ if (params.enabled === false) {
4349
+ return emptyPlan(serverUrl, { status: "skipped", reason: "disabled" });
4350
+ }
4351
+ try {
4352
+ const { grants, failures } = await collectGrants(snapshot);
4353
+ if (grants.length === 0) {
4354
+ return emptyPlan(
4355
+ serverUrl,
4356
+ { status: "skipped", reason: "no_tokens" },
4357
+ failures
4358
+ );
4359
+ }
4360
+ const metadata = parseServerMetadata(snapshot.serverMetadata);
4361
+ if (!metadata) {
4362
+ return emptyPlan(
4363
+ serverUrl,
4364
+ { status: "skipped", reason: "no_metadata" },
4365
+ failures
4366
+ );
4367
+ }
4368
+ if (!metadata.revocation_endpoint) {
4369
+ return emptyPlan(
4370
+ serverUrl,
4371
+ { status: "skipped", reason: "no_endpoint" },
4372
+ failures
4373
+ );
4374
+ }
4375
+ return {
4376
+ serverUrl,
4377
+ grants,
4378
+ failures,
4379
+ endpoint: metadata.revocation_endpoint,
4380
+ supportedAuthMethods: revocationAuthMethods(metadata),
4381
+ metadataIssuer: metadata.issuer
4382
+ };
4383
+ } catch (err) {
4384
+ return emptyPlan(serverUrl, {
4385
+ status: "failed",
4386
+ detail: err instanceof Error ? err.message : String(err)
4387
+ });
4388
+ }
4389
+ }
4390
+ function parseServerMetadata(raw) {
4391
+ if (raw === null || typeof raw !== "object") return void 0;
4392
+ return raw;
4393
+ }
4394
+ async function executeOAuthRevocation(plan, params) {
4395
+ const outcome = await runPlan(plan, params);
4396
+ const { logger } = params;
4397
+ if (outcome.status === "failed") {
4398
+ logger?.warn(
4399
+ {
4400
+ serverUrl: plan.serverUrl,
4401
+ endpoint: outcome.endpoint,
4402
+ detail: outcome.detail
4403
+ },
4404
+ "Token revocation failed; local OAuth state was cleared anyway"
4405
+ );
4406
+ } else if (outcome.status === "skipped" && outcome.reason === "no_endpoint") {
4407
+ logger?.debug(
4408
+ { serverUrl: plan.serverUrl },
4409
+ "Skipping token revocation: authorization server metadata has no revocation_endpoint"
4410
+ );
4411
+ }
4412
+ return outcome;
4413
+ }
4414
+ async function collectGrants(snapshot) {
4415
+ const grants = [];
4416
+ const failures = [];
4417
+ const seenKeys = /* @__PURE__ */ new Set();
4418
+ let preregistered;
4419
+ try {
4420
+ preregistered = await parseClient(snapshot.preregisteredClientInformation);
4421
+ } catch (err) {
4422
+ failures.push({
4423
+ status: "failed",
4424
+ detail: `could not read the preconfigured client registration: ${err instanceof Error ? err.message : String(err)}`
4425
+ });
4426
+ }
4427
+ const add2 = async (issuer, rawTokens, rawClient) => {
4428
+ const revocable = selectRevocableToken(await parseTokens(rawTokens));
4429
+ if (!revocable) return;
4430
+ const key = `${issuer ?? "\0legacy"}\0${revocable.token}`;
4431
+ if (seenKeys.has(key)) return;
4432
+ seenKeys.add(key);
4433
+ const bound = await parseClient(rawClient);
4434
+ grants.push({
4435
+ issuer,
4436
+ ...revocable,
4437
+ clientInformation: bound ?? preregistered
4438
+ });
4439
+ };
4440
+ const addSafely = async (issuer, rawTokens, rawClient) => {
4441
+ try {
4442
+ await add2(issuer, rawTokens, rawClient);
4443
+ } catch (err) {
4444
+ const where = issuer === void 0 ? "the unkeyed slot" : `issuer ${issuer}`;
4445
+ failures.push({
4446
+ status: "failed",
4447
+ detail: `could not read the stored grant for ${where}: ${err instanceof Error ? err.message : String(err)}`
4448
+ });
4449
+ }
4450
+ };
4451
+ for (const [issuer, slot] of Object.entries(snapshot.byIssuer)) {
4452
+ await addSafely(issuer, slot?.tokens, slot?.clientInformation);
4453
+ }
4454
+ await addSafely(
4455
+ void 0,
4456
+ snapshot.legacyTokens,
4457
+ snapshot.legacyClientInformation
4458
+ );
4459
+ return { grants, failures };
4460
+ }
4461
+ async function parseTokens(raw) {
4462
+ return raw === void 0 || raw === null ? void 0 : await OAuthTokensSchema2.parseAsync(raw);
4463
+ }
4464
+ async function parseClient(raw) {
4465
+ return raw === void 0 || raw === null ? void 0 : await OAuthClientInformationSchema2.parseAsync(raw);
4466
+ }
4467
+ function aggregateOutcomes(outcomes) {
4468
+ return outcomes.find((o) => o.status === "failed") ?? outcomes.find((o) => o.status === "revoked") ?? outcomes[0] ?? { status: "skipped", reason: "no_tokens" };
4469
+ }
4470
+ async function runPlan(plan, params) {
4471
+ if (plan.outcome) return plan.outcome;
4472
+ const endpoint = plan.endpoint;
4473
+ if (!endpoint) return { status: "skipped", reason: "no_endpoint" };
4474
+ const outcomes = [...plan.failures];
4475
+ const timeoutMs = params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS;
4476
+ const deadlineAt = Date.now() + timeoutMs;
4477
+ for (const grant of plan.grants) {
4478
+ if (grant.issuer === void 0) {
4479
+ outcomes.push({
4480
+ status: "failed",
4481
+ detail: "the stored grant is not bound to an authorization server (it predates issuer binding), so it could not be matched to this revocation endpoint and was cleared without revocation \u2014 re-authorize to make future clears revocable"
4482
+ });
4483
+ continue;
4484
+ }
4485
+ if (plan.metadataIssuer !== grant.issuer) {
4486
+ outcomes.push({
4487
+ status: "failed",
4488
+ detail: plan.metadataIssuer === void 0 ? `the cached authorization-server metadata names no issuer, so the grant bound to ${grant.issuer} could not be matched to this revocation endpoint and was cleared without revocation` : `the cached authorization-server metadata is for ${plan.metadataIssuer}, so the grant bound to ${grant.issuer} was cleared without revocation`
4489
+ });
4490
+ continue;
4491
+ }
4492
+ const remainingMs = deadlineAt - Date.now();
4493
+ if (remainingMs <= 0) {
4494
+ outcomes.push({
4495
+ status: "failed",
4496
+ endpoint,
4497
+ detail: `the ${timeoutMs}ms revocation budget was exhausted before this grant was attempted${grant.issuer === void 0 ? "" : ` (issuer ${grant.issuer})`}`
4498
+ });
4499
+ continue;
4500
+ }
4501
+ outcomes.push(
4502
+ await revokeToken({
4503
+ endpoint,
4504
+ token: grant.token,
4505
+ tokenTypeHint: grant.tokenTypeHint,
4506
+ clientInformation: grant.clientInformation,
4507
+ supportedAuthMethods: plan.supportedAuthMethods,
4508
+ fetchFn: params.fetchFn,
4509
+ timeoutMs: remainingMs
4510
+ })
4511
+ );
4512
+ }
4513
+ return aggregateOutcomes(outcomes);
4514
+ }
4515
+
3653
4516
  // src/clear-stored-auth-for-relogin.ts
3654
4517
  function normalizeServerUrl(serverUrl) {
3655
4518
  try {
@@ -3658,16 +4521,56 @@ function normalizeServerUrl(serverUrl) {
3658
4521
  return serverUrl;
3659
4522
  }
3660
4523
  }
3661
- async function clearStoredAuthForRelogin(serverUrl) {
3662
- if (!serverUrl?.trim()) return;
4524
+ async function clearStoredAuthForRelogin(serverUrl, options) {
4525
+ if (!serverUrl?.trim()) return void 0;
3663
4526
  const raw = serverUrl.trim();
3664
4527
  const normalized = normalizeServerUrl(raw);
3665
4528
  const storage = new NodeOAuthStorage();
3666
- await storage.clear(raw);
3667
- if (normalized !== raw) {
3668
- await storage.clear(normalized);
4529
+ const keys = normalized === raw ? [raw] : [normalized, raw];
4530
+ const plans = [];
4531
+ for (const key of keys) {
4532
+ plans.push(
4533
+ await clearAndPlanRevocation({
4534
+ serverUrl: key,
4535
+ storage,
4536
+ enabled: options?.revoke !== false
4537
+ })
4538
+ );
3669
4539
  }
3670
4540
  resetNodeOAuthStorageCache();
4541
+ return options?.revoke === false ? void 0 : sendPlans(plans, options?.budgetMs ?? DEFAULT_REVOCATION_TIMEOUT_MS);
4542
+ }
4543
+ async function sendPlans(plans, budgetMs) {
4544
+ const fetchFn = createProxyFetch() ?? fetch;
4545
+ const deadlineAt = Date.now() + budgetMs;
4546
+ let reported;
4547
+ let lastSkip;
4548
+ for (const plan of plans) {
4549
+ const remainingMs = deadlineAt - Date.now();
4550
+ const needsNetwork = plan.outcome === void 0;
4551
+ if (needsNetwork && remainingMs <= 0) {
4552
+ const exhausted = {
4553
+ status: "failed",
4554
+ detail: `the ${budgetMs}ms revocation budget was exhausted before "${plan.serverUrl}" was attempted`
4555
+ };
4556
+ if (reported?.status !== "failed") reported = exhausted;
4557
+ continue;
4558
+ }
4559
+ const outcome = await executeOAuthRevocation(plan, {
4560
+ fetchFn,
4561
+ timeoutMs: needsNetwork ? remainingMs : void 0
4562
+ });
4563
+ if (outcome.status === "skipped" && outcome.reason === "no_tokens") {
4564
+ lastSkip = outcome;
4565
+ continue;
4566
+ }
4567
+ if (outcome.status === "failed") {
4568
+ if (reported?.status !== "failed") reported = outcome;
4569
+ } else {
4570
+ reported ??= outcome;
4571
+ }
4572
+ }
4573
+ return reported ?? lastSkip;
3671
4574
  }
3672
4575
 
3673
4576
  // ../../core/mcp/inspectorClient.ts
@@ -3830,6 +4733,20 @@ function mcpParamHeadersForTool(tool, args) {
3830
4733
  return buildMcpParamHeaders(scan.declarations, args);
3831
4734
  }
3832
4735
 
4736
+ // ../../core/mcp/subscriptionAck.ts
4737
+ import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
4738
+ var NEVER_ACKNOWLEDGED_SDK_MESSAGE = "closed the subscription gracefully before acknowledging";
4739
+ var NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE = "The server closed the subscription without acknowledging it. A subscriptions/listen request must first be answered with a notifications/subscriptions/acknowledged notification; a JSON-RPC result for the listen id means graceful closure. Not retrying \u2014 the server would answer the same way again.";
4740
+ function isNeverAcknowledgedSubscriptionClose(error) {
4741
+ return SdkError.isInstance(error) && error.code === SdkErrorCode.ConnectionClosed && error.message.includes(NEVER_ACKNOWLEDGED_SDK_MESSAGE);
4742
+ }
4743
+ function subscriptionFailureMessage(error) {
4744
+ if (isNeverAcknowledgedSubscriptionClose(error)) {
4745
+ return NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE;
4746
+ }
4747
+ return error instanceof Error ? error.message : String(error);
4748
+ }
4749
+
3833
4750
  // ../../core/mcp/messageTrackingTransport.ts
3834
4751
  var MessageTrackingTransport = class {
3835
4752
  baseTransport;
@@ -3921,6 +4838,29 @@ var MessageTrackingTransport = class {
3921
4838
  get sessionId() {
3922
4839
  return this.baseTransport.sessionId;
3923
4840
  }
4841
+ /**
4842
+ * Forward the base transport's per-request-stream capability (#2140).
4843
+ *
4844
+ * The SDK's `Protocol.request` reads this off the transport it was handed —
4845
+ * which is always this wrapper — to decide how a cancelled request is
4846
+ * signalled: abort that request's own stream (the 2026-07-28 signal for
4847
+ * Streamable HTTP) or POST a `notifications/cancelled` (the stdio one).
4848
+ *
4849
+ * Not forwarding it made the wrapper *answer* the question, `undefined`, for
4850
+ * every client. So the Cancel button POSTed a notification even on a modern
4851
+ * Streamable HTTP connection, where a spec-compliant server acknowledges it
4852
+ * `202` and drops it — the tool kept running and only a disconnect really
4853
+ * cancelled anything. That hit the CLI and TUI as much as the web client:
4854
+ * their real `StreamableHTTPClientTransport` advertises the flag correctly
4855
+ * and this wrapper hid it.
4856
+ *
4857
+ * A base transport that says nothing stays `undefined` rather than becoming
4858
+ * `false`, since the SDK's check is `=== true` either way and inventing a
4859
+ * value here would misreport what the transport claimed.
4860
+ */
4861
+ get hasPerRequestStream() {
4862
+ return this.baseTransport.hasPerRequestStream;
4863
+ }
3924
4864
  // Implemented as a concrete method (rather than delegating the base
3925
4865
  // transport's optional `setProtocolVersion`) so the SDK Client always
3926
4866
  // invokes it after the initialize handshake — including for stdio, whose
@@ -4149,7 +5089,7 @@ function validateToolOutput(provider, tool, result) {
4149
5089
 
4150
5090
  // ../../core/mcp/listSalvage.ts
4151
5091
  import { z as z2 } from "zod/v4";
4152
- import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
5092
+ import { SdkError as SdkError2, SdkErrorCode as SdkErrorCode2 } from "@modelcontextprotocol/client";
4153
5093
  import { ToolSchema } from "@modelcontextprotocol/core";
4154
5094
  var LIST_MAX_PAGES = 64;
4155
5095
  function listPaginationExceeded(method) {
@@ -4158,10 +5098,10 @@ function listPaginationExceeded(method) {
4158
5098
  );
4159
5099
  }
4160
5100
  function isClientDecodeRejection(err) {
4161
- return SdkError.isInstance(err) && (err.code === SdkErrorCode.InvalidResult || err.code === SdkErrorCode.UnsupportedResultType);
5101
+ return SdkError2.isInstance(err) && (err.code === SdkErrorCode2.InvalidResult || err.code === SdkErrorCode2.UnsupportedResultType);
4162
5102
  }
4163
5103
  function isSalvageableRejection(err) {
4164
- return SdkError.isInstance(err) && err.code === SdkErrorCode.InvalidResult;
5104
+ return SdkError2.isInstance(err) && err.code === SdkErrorCode2.InvalidResult;
4165
5105
  }
4166
5106
  function lenientListPageSchema(resultSchema, itemsKey) {
4167
5107
  return resultSchema.extend({ [itemsKey]: z2.array(z2.unknown()) });
@@ -4471,6 +5411,52 @@ var TypedEventGeneric = class extends CustomEvent {
4471
5411
  }
4472
5412
  };
4473
5413
  var TypedEventTarget = class extends EventTarget {
5414
+ /**
5415
+ * Per-event-type dispatch counter. Read through `getEventRevision` by
5416
+ * `useStoreSnapshot` (core/react) to tell "nothing has happened on this
5417
+ * event since I last looked" from "something has", without comparing the
5418
+ * snapshot's contents (#1955).
5419
+ */
5420
+ eventRevisions = /* @__PURE__ */ new Map();
5421
+ /**
5422
+ * How many times an event of this type has been dispatched from this
5423
+ * target. Monotonic per type, and per type only: an `errorChange` does not
5424
+ * advance `toolsChange`, so a subscriber to one is not re-read because of
5425
+ * the other.
5426
+ *
5427
+ * The counter — not the list's contents — is what a `useSyncExternalStore`
5428
+ * snapshot is cached against. That distinction is load-bearing rather than
5429
+ * an optimization: these stores hand out a defensive copy, so comparing
5430
+ * contents is the only alternative, and a contents comparison cannot see an
5431
+ * in-place mutation of an entry the list already holds. `MessageLogState`
5432
+ * does exactly that when a response is folded into its request entry
5433
+ * (filling in `response`/`duration`), and it dispatches — so the counter
5434
+ * moves and the fold reaches the UI, where a shallow compare would report
5435
+ * "unchanged" and never re-render it.
5436
+ */
5437
+ getEventRevision(type) {
5438
+ return this.eventRevisions.get(type) ?? 0;
5439
+ }
5440
+ /**
5441
+ * Overridden solely to advance the counter. It sits on `dispatchEvent`
5442
+ * rather than on `dispatchTypedEvent` because that is the one funnel every
5443
+ * dispatch passes through — the typed helper below delegates to it, and so
5444
+ * does a hand-rolled `dispatchEvent(new CustomEvent(...))`, which the tests
5445
+ * use extensively. A counter bumped only in the typed helper would be
5446
+ * silently stale for those, i.e. exactly where nobody would look for it.
5447
+ *
5448
+ * Bumped BEFORE dispatching: the state mutation always precedes the
5449
+ * dispatch, so by the time a listener runs the counter must already reflect
5450
+ * it, or a snapshot read from inside that listener would return the cached
5451
+ * pre-change value.
5452
+ */
5453
+ dispatchEvent(event) {
5454
+ this.eventRevisions.set(
5455
+ event.type,
5456
+ (this.eventRevisions.get(event.type) ?? 0) + 1
5457
+ );
5458
+ return super.dispatchEvent(event);
5459
+ }
4474
5460
  dispatchTypedEvent(type, ...args) {
4475
5461
  const detail = args[0] ?? void 0;
4476
5462
  this.dispatchEvent(new TypedEventGeneric(type, detail));
@@ -4859,6 +5845,115 @@ function withOAuthEndpointOverrides(fetchFn, getOverrides) {
4859
5845
  };
4860
5846
  }
4861
5847
 
5848
+ // ../../core/auth/oidcDiscoveryCompat.ts
5849
+ import {
5850
+ OAuthMetadataSchema,
5851
+ OpenIdProviderDiscoveryMetadataSchema
5852
+ } from "@modelcontextprotocol/core";
5853
+ var RFC8414_WELL_KNOWN = "/.well-known/oauth-authorization-server";
5854
+ var OIDC_WELL_KNOWN = "/.well-known/openid-configuration";
5855
+ var COMPAT_SOURCE_HEADER = "x-inspector-oauth-metadata-source";
5856
+ function oidcDiscoveryCandidates(rfc8414Url) {
5857
+ let url;
5858
+ try {
5859
+ url = new URL(rfc8414Url);
5860
+ } catch {
5861
+ return [];
5862
+ }
5863
+ if (!url.pathname.startsWith(RFC8414_WELL_KNOWN)) return [];
5864
+ const path5 = url.pathname.slice(RFC8414_WELL_KNOWN.length);
5865
+ if (path5 !== "" && !path5.startsWith("/")) return [];
5866
+ if (path5 === "") return [new URL(OIDC_WELL_KNOWN, url.origin).href];
5867
+ return [
5868
+ new URL(`${OIDC_WELL_KNOWN}${path5}`, url.origin).href,
5869
+ new URL(`${path5}${OIDC_WELL_KNOWN}`, url.origin).href
5870
+ ];
5871
+ }
5872
+ function continuesDiscovery(status) {
5873
+ return status >= 400 && status < 500 || status === 502;
5874
+ }
5875
+ async function releaseBody(response) {
5876
+ await response.body?.cancel().catch(() => {
5877
+ });
5878
+ }
5879
+ function isRfc8414OnlyMetadata(value) {
5880
+ if (!OAuthMetadataSchema.safeParse(value).success) return false;
5881
+ return !OpenIdProviderDiscoveryMetadataSchema.safeParse(value).success;
5882
+ }
5883
+ function requestUrlOf2(input) {
5884
+ if (typeof input === "string") return input;
5885
+ if (input instanceof URL) return input.href;
5886
+ return input.url;
5887
+ }
5888
+ function discoveryHeaders(input, init) {
5889
+ if (init?.headers) return new Headers(init.headers);
5890
+ if (typeof input !== "string" && !(input instanceof URL)) {
5891
+ return new Headers(input.headers);
5892
+ }
5893
+ return new Headers();
5894
+ }
5895
+ function requestMethodOf(input, init) {
5896
+ if (init?.method) return init.method.toUpperCase();
5897
+ if (typeof input !== "string" && !(input instanceof URL)) {
5898
+ return input.method.toUpperCase();
5899
+ }
5900
+ return "GET";
5901
+ }
5902
+ function withRfc8414OidcCompat(fetchFn) {
5903
+ return async (input, init) => {
5904
+ const response = await fetchFn(input, init);
5905
+ if (response.ok) return response;
5906
+ if (!continuesDiscovery(response.status)) return response;
5907
+ if (requestMethodOf(input, init) !== "GET") return response;
5908
+ const candidates = oidcDiscoveryCandidates(requestUrlOf2(input));
5909
+ if (candidates.length === 0) return response;
5910
+ const headers = discoveryHeaders(input, init);
5911
+ for (const candidate of candidates) {
5912
+ let probe;
5913
+ try {
5914
+ probe = await fetchFn(candidate, { headers });
5915
+ } catch {
5916
+ return response;
5917
+ }
5918
+ if (!probe.ok) {
5919
+ await releaseBody(probe);
5920
+ if (continuesDiscovery(probe.status)) continue;
5921
+ return response;
5922
+ }
5923
+ let parsed;
5924
+ let body;
5925
+ try {
5926
+ body = await probe.text();
5927
+ parsed = JSON.parse(body);
5928
+ } catch {
5929
+ return response;
5930
+ }
5931
+ if (!isRfc8414OnlyMetadata(parsed)) {
5932
+ return response;
5933
+ }
5934
+ const source = probe.url || candidate;
5935
+ await releaseBody(response);
5936
+ console.warn(
5937
+ `[oauth] ${source} returned RFC 8414 OAuth 2.0 authorization server metadata, not an OpenID provider document. The MCP TypeScript SDK validates that path as OpenID Connect Discovery and would reject it (modelcontextprotocol/typescript-sdk#2733), so the Inspector is handing it to discovery as the RFC 8414 document it is.`
5938
+ );
5939
+ return new Response(body, {
5940
+ status: 200,
5941
+ statusText: "OK",
5942
+ headers: {
5943
+ "content-type": "application/json",
5944
+ // The substituted response is what the fetch trackers above this
5945
+ // wrapper record, so it says where its body actually came from
5946
+ // rather than letting the Network tab imply the RFC 8414 path
5947
+ // answered. Named on the response, not logged only, so it survives
5948
+ // into the captured entry.
5949
+ [COMPAT_SOURCE_HEADER]: source
5950
+ }
5951
+ });
5952
+ }
5953
+ return response;
5954
+ };
5955
+ }
5956
+
4862
5957
  // ../../core/logging/logger.ts
4863
5958
  var noop = () => {
4864
5959
  };
@@ -5277,7 +6372,7 @@ import {
5277
6372
  exchangeAuthorization,
5278
6373
  startAuthorization
5279
6374
  } from "@modelcontextprotocol/client";
5280
- import { OAuthMetadataSchema } from "@modelcontextprotocol/core";
6375
+ import { OAuthMetadataSchema as OAuthMetadataSchema2 } from "@modelcontextprotocol/core";
5281
6376
 
5282
6377
  // ../../core/auth/ema/constants.ts
5283
6378
  var GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
@@ -5326,7 +6421,7 @@ function idpOAuthStorageKey(issuer) {
5326
6421
  }
5327
6422
 
5328
6423
  // ../../core/auth/ema/tokenEndpoint.ts
5329
- import { selectClientAuthMethod } from "@modelcontextprotocol/client";
6424
+ import { selectClientAuthMethod as selectClientAuthMethod2 } from "@modelcontextprotocol/client";
5330
6425
  async function parseOAuthTokenErrorResponse(response, step) {
5331
6426
  let body;
5332
6427
  try {
@@ -5347,7 +6442,7 @@ async function parseOAuthTokenErrorResponse(response, step) {
5347
6442
  }
5348
6443
  function applyClientAuth(metadata, clientInformation, headers, body) {
5349
6444
  const supported = metadata?.token_endpoint_auth_methods_supported ?? [];
5350
- const method = selectClientAuthMethod(clientInformation, supported);
6445
+ const method = selectClientAuthMethod2(clientInformation, supported);
5351
6446
  if (method === "client_secret_basic" && clientInformation.client_secret) {
5352
6447
  const credentials = btoa(
5353
6448
  `${clientInformation.client_id}:${clientInformation.client_secret}`
@@ -5401,7 +6496,7 @@ async function discoverIdpMetadata(issuer, fetchFn) {
5401
6496
  `Failed to discover OIDC metadata for IdP issuer ${issuer}`
5402
6497
  );
5403
6498
  }
5404
- return OAuthMetadataSchema.parse(metadata);
6499
+ return OAuthMetadataSchema2.parse(metadata);
5405
6500
  }
5406
6501
  async function startIdpOidcAuthorization(params) {
5407
6502
  const issuer = normalizeIdpIssuer(params.idp.issuer);
@@ -5657,7 +6752,7 @@ async function discoverResourceAsMetadata(resourceAsUrl, fetchFn) {
5657
6752
 
5658
6753
  // ../../core/auth/ema/wire.ts
5659
6754
  import { discoverAndRequestJwtAuthGrant } from "@modelcontextprotocol/client";
5660
- import { OAuthTokensSchema as OAuthTokensSchema2 } from "@modelcontextprotocol/core";
6755
+ import { OAuthTokensSchema as OAuthTokensSchema3 } from "@modelcontextprotocol/core";
5661
6756
  async function exchangeIdJag(params) {
5662
6757
  const issuer = normalizeIdpIssuer(params.idp.issuer);
5663
6758
  const resource = params.resource.trim();
@@ -5736,7 +6831,7 @@ async function redeemIdJagForAccessToken(params) {
5736
6831
  "EMA leg 3 (resource AS JWT bearer grant)"
5737
6832
  );
5738
6833
  }
5739
- return OAuthTokensSchema2.parse(await response.json());
6834
+ return OAuthTokensSchema3.parse(await response.json());
5740
6835
  }
5741
6836
 
5742
6837
  // ../../core/auth/ema/emaFlow.ts
@@ -6465,14 +7560,41 @@ var OAuthManager = class {
6465
7560
  return void 0;
6466
7561
  }
6467
7562
  }
6468
- async clearOAuthTokens() {
7563
+ /**
7564
+ * Drop this server's local OAuth state, and revoke the grant at the
7565
+ * authorization server (RFC 7009).
7566
+ *
7567
+ * The state is taken and deleted in one atomic step, and the requests are
7568
+ * sent afterwards from what was taken. Both halves matter: the requests need
7569
+ * state the clear destroys, and the clear must not wait on the network, or a
7570
+ * fresh authorization completing during a five-second revocation would be
7571
+ * deleted by a clear reasoning about the grant it replaced.
7572
+ *
7573
+ * Best-effort by construction: every failure is reported through the returned
7574
+ * outcome and the clear has already happened regardless, because forgetting
7575
+ * the tokens is what the caller actually asked for (#2144).
7576
+ *
7577
+ * `options.revoke === false` skips the request. That is not only an escape
7578
+ * hatch for an authorization server that mishandles it — disconnecting while
7579
+ * still holding live tokens is a case a user may want to reproduce
7580
+ * deliberately, to watch how a server under test copes with it.
7581
+ */
7582
+ async clearOAuthTokens(options) {
6469
7583
  if (!this.oauthConfig?.storage) {
6470
- return;
7584
+ return { status: "skipped", reason: "no_tokens" };
6471
7585
  }
6472
7586
  const serverUrl = this.getServerUrl();
6473
- await this.oauthConfig.storage.clear(serverUrl);
7587
+ const plan = await clearAndPlanRevocation({
7588
+ serverUrl,
7589
+ storage: this.oauthConfig.storage,
7590
+ enabled: options?.revoke
7591
+ });
6474
7592
  this.oauthFlowState = null;
6475
7593
  this.pendingAuthorizationScope = void 0;
7594
+ return executeOAuthRevocation(plan, {
7595
+ fetchFn: this.params.effectiveAuthFetch,
7596
+ logger: this.params.logger
7597
+ });
6476
7598
  }
6477
7599
  async isOAuthAuthorized() {
6478
7600
  const tokens = await this.getOAuthTokens();
@@ -6799,10 +7921,11 @@ var OAuthManager = class {
6799
7921
  await this.params.onBeforeOAuthRedirect(parsedState.authId);
6800
7922
  }
6801
7923
  }
6802
- this.requireNavigation().navigateToAuthorization(authorizationUrl);
7924
+ const navigation = this.requireNavigation();
6803
7925
  const provider = await this.createOAuthProvider();
6804
7926
  const clientInfo = await provider.clientInformation();
6805
7927
  await this.recordAuthorizationCodeFlowState(authorizationUrl, clientInfo);
7928
+ navigation.navigateToAuthorization(authorizationUrl);
6806
7929
  this.params.dispatchOAuthAuthorizationRequired({ url: authorizationUrl });
6807
7930
  }
6808
7931
  requireNavigation() {
@@ -6959,6 +8082,24 @@ var RemoteClientTransport = class {
6959
8082
  sseResponseWaits = /* @__PURE__ */ new Map();
6960
8083
  options;
6961
8084
  config;
8085
+ /**
8086
+ * Whether the upstream connection gives each request its own response stream
8087
+ * (#2140). The SDK's `Protocol.request` reads this: on a 2026-era connection
8088
+ * a transport advertising it has its per-request stream **aborted** as the
8089
+ * spec's cancellation signal, and no `notifications/cancelled` is sent; a
8090
+ * transport that does not gets the stdio mechanism instead.
8091
+ *
8092
+ * The real upstream transport lives on the backend, so this transport has to
8093
+ * answer for it. Streamable HTTP opens one POST — and one SSE response
8094
+ * stream — per request, so it qualifies; stdio and SSE multiplex every
8095
+ * request over one shared channel and must keep sending the notification.
8096
+ *
8097
+ * Advertising it is only half the fix: `requestSend` applies the SDK's
8098
+ * `requestSignal` to the browser-to-backend fetch, and `/api/mcp/send`
8099
+ * forwards that disconnect to the upstream `transport.send`, which is what
8100
+ * actually closes the stream the server is watching.
8101
+ */
8102
+ hasPerRequestStream;
6962
8103
  /**
6963
8104
  * Intentionally returns undefined. The MCP Client checks transport.sessionId to detect
6964
8105
  * reconnects and skip initialize. Our _sessionId is the remote server's session ID, not
@@ -7003,6 +8144,7 @@ var RemoteClientTransport = class {
7003
8144
  constructor(options, config) {
7004
8145
  this.options = options;
7005
8146
  this.config = config;
8147
+ this.hasPerRequestStream = config.type === "streamable-http";
7006
8148
  }
7007
8149
  setAuthRecovery(handlers) {
7008
8150
  this.options.authRecovery = handlers;
@@ -7367,7 +8509,8 @@ var RemoteClientTransport = class {
7367
8509
  const res = await this.fetchFn(`${this.baseUrl}/api/mcp/send`, {
7368
8510
  method: "POST",
7369
8511
  headers: this.headers,
7370
- body: JSON.stringify(body)
8512
+ body: JSON.stringify(body),
8513
+ ...options?.requestSignal && { signal: options.requestSignal }
7371
8514
  });
7372
8515
  if (!res.ok) {
7373
8516
  const text = await res.text();
@@ -7604,6 +8747,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7604
8747
  // pending re-listen timer.
7605
8748
  modernReconnectAttempts = 0;
7606
8749
  modernReconnectTimer;
8750
+ // Set when the last `listen()` was rejected because the server answered it
8751
+ // with a bare graceful-closure result instead of acknowledging (#2097). That
8752
+ // condition is deterministic, so the reconnect machinery must not treat it as
8753
+ // a drop to retry; the flag is what carries the distinction from the rejection
8754
+ // site to the two failure handlers. Cleared by any user-initiated refresh (a
8755
+ // subscribe/unsubscribe is a fresh attempt, and the server may have changed).
8756
+ modernNeverAcknowledged = false;
7607
8757
  // Task ids the user explicitly cancelled. A cancel makes the in-flight
7608
8758
  // `callToolStream` reject with a generic -32603 error, which the stream's
7609
8759
  // error path would otherwise report as a *failed* task — flashing "failed"
@@ -7625,9 +8775,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7625
8775
  pendingRawWireRequests = /* @__PURE__ */ new Map();
7626
8776
  rawWireRequestCounter = 0;
7627
8777
  // Abort controller for the in-flight ordinary (non-task) tool call. Aborting
7628
- // it makes the SDK send a `notifications/cancelled` for that request (the MCP
7629
- // cancellation flow) and reject the pending call, which `callTool` surfaces as
7630
- // a `ToolCallCancelledError`. Undefined when no ordinary call is in flight.
8778
+ // it hands the SDK the MCP cancellation flow for that request and rejects the
8779
+ // pending call, which `callTool` surfaces as a `ToolCallCancelledError`. Which
8780
+ // signal reaches the server is the transport's business, not ours: a
8781
+ // per-request-stream transport on a 2026-era connection has that request's
8782
+ // stream aborted, and everything else gets `notifications/cancelled` (#2140).
8783
+ // Undefined when no ordinary call is in flight.
7631
8784
  // Task-augmented calls have a server-side task and are cancelled via
7632
8785
  // `cancelRequestorTask` instead, so they don't use this (#1458).
7633
8786
  activeToolCallAbortController;
@@ -7734,6 +8887,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7734
8887
  this.fetchFn ?? fetch,
7735
8888
  () => this.oauthManager?.getEndpointOverrides()
7736
8889
  );
8890
+ this.fetchFn = withRfc8414OidcCompat(this.fetchFn);
7737
8891
  this.effectiveAuthFetch = this.buildEffectiveAuthFetch();
7738
8892
  this.sessionId = options.sessionId;
7739
8893
  if (options.oauth || options.environment.oauth) {
@@ -7753,6 +8907,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
7753
8907
  return Promise.resolve();
7754
8908
  },
7755
8909
  initialConfig: oauthConfig,
8910
+ logger: this.logger,
7756
8911
  enterpriseManagedAuth: options.enterpriseManagedAuth,
7757
8912
  installEnterpriseManagedAuth: options.installEnterpriseManagedAuth,
7758
8913
  dispatchOAuthComplete: (detail) => this.dispatchTypedEvent("oauthComplete", detail),
@@ -8443,6 +9598,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8443
9598
  this.modernListenGeneration++;
8444
9599
  this.clearModernReconnectTimer();
8445
9600
  this.modernReconnectAttempts = 0;
9601
+ this.modernNeverAcknowledged = false;
8446
9602
  this.modernSubscription = null;
8447
9603
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
8448
9604
  this.dispatchSubscriptionsChange();
@@ -9229,10 +10385,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9229
10385
  }
9230
10386
  /**
9231
10387
  * Cancel the in-flight ordinary (non-task) tool call started by
9232
- * {@link callTool}. Aborting its request makes the SDK send a
9233
- * `notifications/cancelled` to the server (the MCP cancellation flow) and
9234
- * reject the pending call, which `callTool` surfaces as a
9235
- * {@link ToolCallCancelledError}.
10388
+ * {@link callTool}. Aborting its request runs the MCP cancellation flow and
10389
+ * rejects the pending call, which `callTool` surfaces as a
10390
+ * {@link ToolCallCancelledError}. The SDK chooses the wire signal from the
10391
+ * transport: on a 2026-era Streamable HTTP connection it aborts that
10392
+ * request's own SSE response stream — the spec's cancellation signal —
10393
+ * rather than sending `notifications/cancelled`, which is the stdio
10394
+ * mechanism and remains in use there (#2140).
9236
10395
  *
9237
10396
  * Task-augmented calls have a server-side task and are cancelled via
9238
10397
  * {@link cancelRequestorTask} instead — this is a no-op for them (and whenever
@@ -11448,6 +12607,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11448
12607
  if (!fromReconnect) {
11449
12608
  this.clearModernReconnectTimer();
11450
12609
  this.modernReconnectAttempts = 0;
12610
+ this.modernNeverAcknowledged = false;
11451
12611
  }
11452
12612
  const generation = ++this.modernListenGeneration;
11453
12613
  const previous = this.modernSubscription;
@@ -11459,10 +12619,18 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11459
12619
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
11460
12620
  return;
11461
12621
  }
11462
- const subscription = await this.client.listen(
11463
- this.buildSubscriptionFilter(),
11464
- this.getRequestOptions()
11465
- );
12622
+ let subscription;
12623
+ try {
12624
+ subscription = await this.client.listen(
12625
+ this.buildSubscriptionFilter(),
12626
+ this.getRequestOptions()
12627
+ );
12628
+ } catch (error) {
12629
+ if (generation === this.modernListenGeneration && isNeverAcknowledgedSubscriptionClose(error)) {
12630
+ this.modernNeverAcknowledged = true;
12631
+ }
12632
+ throw error;
12633
+ }
11466
12634
  if (generation !== this.modernListenGeneration) {
11467
12635
  await closeSubscriptionBestEffort(subscription);
11468
12636
  return;
@@ -11544,8 +12712,34 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11544
12712
  this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
11545
12713
  return;
11546
12714
  }
12715
+ if (this.modernNeverAcknowledged) {
12716
+ this.endModernStreamNeverAcknowledged();
12717
+ return;
12718
+ }
11547
12719
  this.scheduleModernReconnect();
11548
12720
  }
12721
+ /**
12722
+ * Settle the stream on the never-acknowledged close (#2097): a status of its
12723
+ * own so the UI can say what happened, no reconnect, and a log line carrying
12724
+ * the same sentence the UI shows.
12725
+ *
12726
+ * The state is *not* `"ended"` — that badge covers the two expected closes (a
12727
+ * server shutting an established stream down, and reconnection abandoned after
12728
+ * repeated failures), and reading this case as either of them is exactly the
12729
+ * silence #2063 reported.
12730
+ */
12731
+ endModernStreamNeverAcknowledged() {
12732
+ this.clearModernReconnectTimer();
12733
+ this.logger.warn(
12734
+ { message: NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE },
12735
+ "subscriptions/listen closed without an acknowledgement"
12736
+ );
12737
+ this.setModernStreamState({
12738
+ active: this.modernStreamActive(),
12739
+ status: "never-acknowledged",
12740
+ honoredUris: []
12741
+ });
12742
+ }
11549
12743
  /**
11550
12744
  * Schedule a reconnect re-listen after the current backoff delay (#1630).
11551
12745
  * `modernReconnectAttempts` reflects the number of *consecutive failed*
@@ -11579,6 +12773,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11579
12773
  * stream ended (re-subscribing resets the run and tries again).
11580
12774
  */
11581
12775
  onModernReconnectFailed() {
12776
+ if (this.modernNeverAcknowledged && !isTerminalStatus(this.status) && this.wantsModernStream()) {
12777
+ this.endModernStreamNeverAcknowledged();
12778
+ return;
12779
+ }
11582
12780
  this.modernReconnectAttempts += 1;
11583
12781
  if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || !this.wantsModernStream()) {
11584
12782
  this.setModernStreamState({
@@ -11635,7 +12833,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11635
12833
  this.dispatchSubscriptionsChange();
11636
12834
  } catch (error) {
11637
12835
  throw new Error(
11638
- `Failed to subscribe to resource: ${error instanceof Error ? error.message : String(error)}`,
12836
+ `Failed to subscribe to resource: ${subscriptionFailureMessage(error)}`,
11639
12837
  { cause: error }
11640
12838
  );
11641
12839
  }
@@ -11680,7 +12878,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11680
12878
  }
11681
12879
  } catch (error) {
11682
12880
  throw new Error(
11683
- `Failed to unsubscribe from resource: ${error instanceof Error ? error.message : String(error)}`,
12881
+ `Failed to unsubscribe from resource: ${subscriptionFailureMessage(error)}`,
11684
12882
  { cause: error }
11685
12883
  );
11686
12884
  }
@@ -11951,10 +13149,23 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
11951
13149
  return this.oauthManager.getOAuthTokens();
11952
13150
  }
11953
13151
  /**
11954
- * Clears OAuth tokens and client information
13152
+ * Drop this server's stored OAuth state and revoke the grant at the
13153
+ * authorization server (RFC 7009, #2144).
13154
+ *
13155
+ * The request is planned from the stored state, the state is cleared, and
13156
+ * only then is the request sent — so the clear never waits on the network.
13157
+ *
13158
+ * The revocation is best-effort — an authorization server that advertises no
13159
+ * `revocation_endpoint` is left behaving exactly as before, and a network
13160
+ * error, a non-2xx or a timeout is reported in the returned outcome rather
13161
+ * than thrown — so the local clear always completes. Pass
13162
+ * `{ revoke: false }` to skip it.
11955
13163
  */
11956
- async clearOAuthTokens() {
11957
- await this.oauthManager?.clearOAuthTokens();
13164
+ async clearOAuthTokens(options) {
13165
+ return await this.oauthManager?.clearOAuthTokens(options) ?? {
13166
+ status: "skipped",
13167
+ reason: "no_tokens"
13168
+ };
11958
13169
  }
11959
13170
  /**
11960
13171
  * Checks if client is currently OAuth authorized
@@ -14518,14 +15729,22 @@ var validLogLevels = Object.values(
14518
15729
  LoggingLevelSchema.enum
14519
15730
  );
14520
15731
  var CLI_CLIENT_NAME = "inspector-cli";
14521
- async function callMethod(serverConfig, serverSettings, args, clientConfig, cliAuthOverrides, callbackUrlConfig, storedAuthOnly, relogin) {
15732
+ async function callMethod(serverConfig, serverSettings, args, clientConfig, cliAuthOverrides, callbackUrlConfig, storedAuthOnly, relogin, revoke) {
14522
15733
  if (relogin) {
14523
15734
  if (!("url" in serverConfig && serverConfig.url)) {
14524
15735
  throw new Error(
14525
15736
  "--relogin requires an HTTP/SSE server URL (no OAuth store entry for stdio)"
14526
15737
  );
14527
15738
  }
14528
- await clearStoredAuthForRelogin(serverConfig.url);
15739
+ const revocation = await clearStoredAuthForRelogin(serverConfig.url, {
15740
+ revoke: revoke && serverSettings?.oauthRevokeOnClear !== false
15741
+ });
15742
+ if (revocation?.status === "failed") {
15743
+ process.stderr.write(
15744
+ `Warning: could not revoke the OAuth grant at the authorization server (${revocation.detail}); it may still be valid there.
15745
+ `
15746
+ );
15747
+ }
14529
15748
  }
14530
15749
  const clientIdentity = {
14531
15750
  name: CLI_CLIENT_NAME,
@@ -14646,7 +15865,13 @@ function findStoredToken(servers, serverUrl) {
14646
15865
  }
14647
15866
  async function refreshStoredAuthToken(serverUrl, statePath, deps = {}) {
14648
15867
  const refresh = deps.refresh ?? refreshAuthorization;
14649
- const discover = deps.discover ?? discoverAuthorizationServerMetadata4;
15868
+ const storedAuthFetch = withRfc8414OidcCompat(createProxyFetch() ?? fetch);
15869
+ const discover = deps.discover ?? ((authorizationServerUrl, options) => discoverAuthorizationServerMetadata4(authorizationServerUrl, {
15870
+ ...options,
15871
+ // A caller-supplied fetch is left alone — it is theirs to compose. The
15872
+ // walker below passes no options, so in practice this is ours.
15873
+ fetchFn: options?.fetchFn ?? storedAuthFetch
15874
+ }));
14650
15875
  const snapshot = await readOAuthSnapshot(statePath);
14651
15876
  const servers = snapshot.servers;
14652
15877
  const found = findStoredServerState(servers, serverUrl);
@@ -14684,7 +15909,8 @@ async function refreshStoredAuthToken(serverUrl, statePath, deps = {}) {
14684
15909
  metadata,
14685
15910
  clientInformation,
14686
15911
  refreshToken,
14687
- resource: new URL(serverUrl)
15912
+ resource: new URL(serverUrl),
15913
+ fetchFn: storedAuthFetch
14688
15914
  });
14689
15915
  } catch (err) {
14690
15916
  const message = err instanceof Error ? err.message : String(err);
@@ -14912,6 +16138,9 @@ async function parseArgs(argv) {
14912
16138
  ).option(
14913
16139
  "--relogin",
14914
16140
  "Delete stored OAuth for this server URL from the shared store before connect (HTTP/SSE URL keys only); interactive login runs only if the server requires auth. Rejected for stdio (no URL-keyed store entry)"
16141
+ ).option(
16142
+ "--no-revoke",
16143
+ "Requires --relogin. Skips the RFC 7009 revocation request that would otherwise end the grant at the authorization server when the local state is deleted. Also skipped when the server entry sets oauth.revokeOnClear to false."
14915
16144
  ).option(
14916
16145
  "--wait-for-auth <sec>",
14917
16146
  "Poll the OAuth state file until a token for --server-url appears (or the timeout elapses), then proceed as if --use-stored-auth were set. Use after handing off to a human to complete OAuth in a browser.",
@@ -14953,6 +16182,9 @@ async function parseArgs(argv) {
14953
16182
  );
14954
16183
  }
14955
16184
  }
16185
+ if (options.revoke === false && !options.relogin) {
16186
+ throw new Error("--no-revoke requires --relogin (it has no other effect).");
16187
+ }
14956
16188
  if (options.strict) {
14957
16189
  if (options.method !== "tools/list") {
14958
16190
  throw new Error("--strict requires --method tools/list.");
@@ -15138,7 +16370,10 @@ async function parseArgs(argv) {
15138
16370
  clientMetadataUrl: options.clientMetadataUrl,
15139
16371
  callbackUrl: options.callbackUrl,
15140
16372
  storedAuthOnly: options.storedAuthOnly === true,
15141
- relogin: options.relogin === true
16373
+ relogin: options.relogin === true,
16374
+ // Commander's `--no-revoke` defaults this to true; only an explicit
16375
+ // `--no-revoke` makes it false.
16376
+ revoke: options.revoke !== false
15142
16377
  };
15143
16378
  }
15144
16379
  async function runCli(argv) {
@@ -15154,7 +16389,8 @@ async function runCli(argv) {
15154
16389
  clientMetadataUrl,
15155
16390
  callbackUrl,
15156
16391
  storedAuthOnly,
15157
- relogin
16392
+ relogin,
16393
+ revoke
15158
16394
  } = parsed;
15159
16395
  const clientConfig = await loadRunnerClientConfig({ clientConfigPath });
15160
16396
  let callbackUrlConfig;
@@ -15178,7 +16414,8 @@ async function runCli(argv) {
15178
16414
  },
15179
16415
  callbackUrlConfig,
15180
16416
  storedAuthOnly === true,
15181
- relogin === true
16417
+ relogin === true,
16418
+ revoke !== false
15182
16419
  );
15183
16420
  }
15184
16421