@modelcontextprotocol/inspector 2.5.0 → 2.7.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.
@@ -33,6 +33,7 @@ var INACTIVE_SUBSCRIPTION_STREAM_STATE = {
33
33
  honoredUris: []
34
34
  };
35
35
  var DEFAULT_TASK_TTL_MS = 6e4;
36
+ var DEFAULT_CONNECTION_TIMEOUT_MS = 3e4;
36
37
  var DEFAULT_MAX_FETCH_REQUESTS = 1e3;
37
38
  var DEFAULT_MODERN_LOG_LEVEL = "debug";
38
39
  function resolveModernLogLevel(settings) {
@@ -390,6 +391,656 @@ async function deleteStoreFile(filePath) {
390
391
  }
391
392
  }
392
393
 
394
+ // ../../core/mcp/skillsSchemas.ts
395
+ import { z } from "zod/v4";
396
+ import { ResourceSchema } from "@modelcontextprotocol/core";
397
+ var SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
398
+ var SKILLS_LIST_METHOD = "skills/list";
399
+ var SKILLS_GET_METHOD = "skills/get";
400
+ var RESOURCES_DIRECTORY_READ_METHOD = "resources/directory/read";
401
+ var DYNAMIC_RESOURCES = "dynamic";
402
+ var SkillFrontmatterSchema = z.looseObject({
403
+ name: z.string().optional(),
404
+ description: z.string().optional()
405
+ });
406
+ var SkillResourceSchema = z.looseObject({
407
+ uri: z.string(),
408
+ digest: z.string().optional(),
409
+ size: z.number().optional()
410
+ });
411
+ var SkillEntrySchema = z.looseObject({
412
+ uri: z.string(),
413
+ frontmatter: SkillFrontmatterSchema,
414
+ resources: z.union([
415
+ z.literal(DYNAMIC_RESOURCES),
416
+ z.array(SkillResourceSchema)
417
+ ])
418
+ });
419
+ var ListSkillsResultSchema = z.looseObject({
420
+ skills: z.array(SkillEntrySchema),
421
+ nextCursor: z.string().optional()
422
+ });
423
+ var ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
424
+ // A non-negative INTEGER, matching the codec: `ttlMs: -1` or `0.5` is an
425
+ // envelope violation, and accepting it here would hide exactly the kind of
426
+ // defect this schema exists to surface.
427
+ ttlMs: z.int().min(0),
428
+ cacheScope: z.enum(["public", "private"])
429
+ });
430
+ var GetSkillEnvelopeSchema = z.looseObject({
431
+ skill: SkillEntrySchema
432
+ });
433
+ var GetSkillResultSchema = GetSkillEnvelopeSchema.transform(
434
+ (result) => result.skill
435
+ );
436
+ var DirectoryChildSchema = ResourceSchema;
437
+ var DirectoryReadResultSchema = z.looseObject({
438
+ resources: z.array(DirectoryChildSchema),
439
+ nextCursor: z.string().optional()
440
+ });
441
+
442
+ // ../../core/mcp/sha256.ts
443
+ var K = new Uint32Array([
444
+ 1116352408,
445
+ 1899447441,
446
+ 3049323471,
447
+ 3921009573,
448
+ 961987163,
449
+ 1508970993,
450
+ 2453635748,
451
+ 2870763221,
452
+ 3624381080,
453
+ 310598401,
454
+ 607225278,
455
+ 1426881987,
456
+ 1925078388,
457
+ 2162078206,
458
+ 2614888103,
459
+ 3248222580,
460
+ 3835390401,
461
+ 4022224774,
462
+ 264347078,
463
+ 604807628,
464
+ 770255983,
465
+ 1249150122,
466
+ 1555081692,
467
+ 1996064986,
468
+ 2554220882,
469
+ 2821834349,
470
+ 2952996808,
471
+ 3210313671,
472
+ 3336571891,
473
+ 3584528711,
474
+ 113926993,
475
+ 338241895,
476
+ 666307205,
477
+ 773529912,
478
+ 1294757372,
479
+ 1396182291,
480
+ 1695183700,
481
+ 1986661051,
482
+ 2177026350,
483
+ 2456956037,
484
+ 2730485921,
485
+ 2820302411,
486
+ 3259730800,
487
+ 3345764771,
488
+ 3516065817,
489
+ 3600352804,
490
+ 4094571909,
491
+ 275423344,
492
+ 430227734,
493
+ 506948616,
494
+ 659060556,
495
+ 883997877,
496
+ 958139571,
497
+ 1322822218,
498
+ 1537002063,
499
+ 1747873779,
500
+ 1955562222,
501
+ 2024104815,
502
+ 2227730452,
503
+ 2361852424,
504
+ 2428436474,
505
+ 2756734187,
506
+ 3204031479,
507
+ 3329325298
508
+ ]);
509
+ var H0 = new Uint32Array([
510
+ 1779033703,
511
+ 3144134277,
512
+ 1013904242,
513
+ 2773480762,
514
+ 1359893119,
515
+ 2600822924,
516
+ 528734635,
517
+ 1541459225
518
+ ]);
519
+ var rotr = (x, n) => x >>> n | x << 32 - n;
520
+ function sha256Bytes(bytes) {
521
+ const message = new Uint8Array(bytes);
522
+ const bitLength = message.length * 8;
523
+ const withLength = message.length + 9;
524
+ const padded = new Uint8Array(Math.ceil(withLength / 64) * 64);
525
+ padded.set(message);
526
+ padded[message.length] = 128;
527
+ const view = new DataView(padded.buffer);
528
+ view.setUint32(padded.length - 8, Math.floor(bitLength / 4294967296));
529
+ view.setUint32(padded.length - 4, bitLength >>> 0);
530
+ const h = new Uint32Array(H0);
531
+ const w = new Uint32Array(64);
532
+ for (let offset = 0; offset < padded.length; offset += 64) {
533
+ for (let i = 0; i < 16; i += 1) w[i] = view.getUint32(offset + i * 4);
534
+ for (let i = 16; i < 64; i += 1) {
535
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
536
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
537
+ w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
538
+ }
539
+ let [a, b, c, d, e, f, g, hh] = h;
540
+ for (let i = 0; i < 64; i += 1) {
541
+ const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
542
+ const ch = e & f ^ ~e & g;
543
+ const temp1 = hh + S1 + ch + K[i] + w[i] >>> 0;
544
+ const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
545
+ const maj = a & b ^ a & c ^ b & c;
546
+ const temp2 = S0 + maj >>> 0;
547
+ hh = g;
548
+ g = f;
549
+ f = e;
550
+ e = d + temp1 >>> 0;
551
+ d = c;
552
+ c = b;
553
+ b = a;
554
+ a = temp1 + temp2 >>> 0;
555
+ }
556
+ h[0] = h[0] + a >>> 0;
557
+ h[1] = h[1] + b >>> 0;
558
+ h[2] = h[2] + c >>> 0;
559
+ h[3] = h[3] + d >>> 0;
560
+ h[4] = h[4] + e >>> 0;
561
+ h[5] = h[5] + f >>> 0;
562
+ h[6] = h[6] + g >>> 0;
563
+ h[7] = h[7] + hh >>> 0;
564
+ }
565
+ const digest = new Uint8Array(32);
566
+ const out = new DataView(digest.buffer);
567
+ for (let i = 0; i < 8; i += 1) out.setUint32(i * 4, h[i]);
568
+ return digest;
569
+ }
570
+
571
+ // ../../core/mcp/skillFile.ts
572
+ import { parse as parseYaml } from "yaml";
573
+ function splitSkillFile(text) {
574
+ if (!/^---[ \t]*\r?\n/.test(text)) return { body: text };
575
+ const rest = text.slice(text.indexOf("\n") + 1);
576
+ const close = rest.search(/^---[ \t]*\r?$/m);
577
+ if (close === -1) return { body: text };
578
+ const frontmatter = rest.slice(0, close).replace(/\r?\n$/, "");
579
+ const after = rest.slice(close);
580
+ const newline = after.indexOf("\n");
581
+ if (newline === -1) return { frontmatter, body: "" };
582
+ return { frontmatter, body: after.slice(newline + 1).replace(/^\r?\n/, "") };
583
+ }
584
+ function hasContent(yamlText) {
585
+ return yamlText.split("\n").some((line) => line.trim() !== "" && !line.trimStart().startsWith("#"));
586
+ }
587
+ var MAX_FRONTMATTER_DEPTH = 64;
588
+ function jsonGraphError(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
589
+ if (typeof value !== "object" || value === null) return void 0;
590
+ if (depth > MAX_FRONTMATTER_DEPTH) {
591
+ return `Frontmatter nests deeper than ${MAX_FRONTMATTER_DEPTH} levels, which cannot be compared field by field.`;
592
+ }
593
+ const node = value;
594
+ if (seen.has(node)) {
595
+ return "Frontmatter contains a cyclic YAML alias, which has no JSON equivalent and cannot be compared against the listing.";
596
+ }
597
+ seen.add(node);
598
+ const members = Array.isArray(node) ? node : Object.values(node);
599
+ for (const member of members) {
600
+ const error = jsonGraphError(member, seen, depth + 1);
601
+ if (error) return error;
602
+ }
603
+ seen.delete(node);
604
+ return void 0;
605
+ }
606
+ function parseSkillFrontmatter(yamlText) {
607
+ let parsed;
608
+ try {
609
+ parsed = parseYaml(yamlText);
610
+ } catch (err) {
611
+ return { error: err instanceof Error ? err.message : String(err) };
612
+ }
613
+ if (parsed === null || parsed === void 0) {
614
+ return hasContent(yamlText) ? { error: "Frontmatter is not a YAML mapping of fields." } : { fields: {} };
615
+ }
616
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
617
+ return {
618
+ error: "Frontmatter is not a YAML mapping of fields."
619
+ };
620
+ }
621
+ const graphError = jsonGraphError(parsed, /* @__PURE__ */ new Set(), 0);
622
+ if (graphError) return { error: graphError };
623
+ return { fields: parsed };
624
+ }
625
+
626
+ // ../../core/mcp/skills.ts
627
+ var SKILL_MAX_RESOURCE_ENTRIES = 512;
628
+ var SKILL_MAX_TOTAL_BYTES = 16 * 1024 * 1024;
629
+ var SKILL_MAX_CATALOG_SKILLS = 256;
630
+ var SKILL_MAX_CATALOG_BYTES = 64 * 1024 * 1024;
631
+ function isSkillCatalogLimit(value) {
632
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
633
+ }
634
+ function resolveSkillCatalogBudget(settings) {
635
+ return {
636
+ maxSkills: isSkillCatalogLimit(settings?.skillCatalogMaxSkills) ? settings.skillCatalogMaxSkills : SKILL_MAX_CATALOG_SKILLS,
637
+ maxBytes: isSkillCatalogLimit(settings?.skillCatalogMaxBytes) ? settings.skillCatalogMaxBytes : SKILL_MAX_CATALOG_BYTES
638
+ };
639
+ }
640
+ var SKILL_FILE_SUFFIX = "/SKILL.md";
641
+ var DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
642
+ var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
643
+ var SKILL_NAME_MAX_LENGTH = 64;
644
+ var SKILL_DESCRIPTION_MAX_LENGTH = 1024;
645
+ function codePointLength(value) {
646
+ return [...value].length;
647
+ }
648
+ function getSkillsExtension(capabilities) {
649
+ const declared = capabilities?.extensions?.[SKILLS_EXTENSION_KEY];
650
+ if (typeof declared !== "object" || declared === null) return void 0;
651
+ const directoryRead = declared.directoryRead === true;
652
+ return { directoryRead };
653
+ }
654
+ function normalizeSkillUri(uri) {
655
+ let parsed;
656
+ try {
657
+ parsed = new URL(uri);
658
+ } catch {
659
+ return void 0;
660
+ }
661
+ const pathlessRoot = parsed.pathname === "" && parsed.host !== "";
662
+ if (!pathlessRoot && !parsed.pathname.startsWith("/")) return void 0;
663
+ return canonicalizePercentEncoding(parsed.href);
664
+ }
665
+ function canonicalizePercentEncoding(value) {
666
+ return value.replace(/%[0-9a-fA-F]{2}/g, (escape) => {
667
+ const char = String.fromCharCode(Number.parseInt(escape.slice(1), 16));
668
+ return /[A-Za-z0-9\-._~]/.test(char) ? char : escape.toUpperCase();
669
+ });
670
+ }
671
+ function skillNameFromUri(uri) {
672
+ const normalized = normalizeSkillUri(uri);
673
+ if (normalized === void 0 || !normalized.endsWith(SKILL_FILE_SUFFIX)) {
674
+ return void 0;
675
+ }
676
+ const path5 = normalized.slice(0, -SKILL_FILE_SUFFIX.length);
677
+ const segment = path5.slice(path5.lastIndexOf("/") + 1);
678
+ return segment.length > 0 ? segment : void 0;
679
+ }
680
+ function skillUriIdentity(uri) {
681
+ return normalizeSkillUri(uri) ?? uri;
682
+ }
683
+ function skillDisplayName(entry) {
684
+ const declared = entry.frontmatter.name?.trim();
685
+ if (declared) return declared;
686
+ return skillNameFromUri(entry.uri) ?? entry.uri;
687
+ }
688
+ function checkSkillConformance(entry) {
689
+ const issues = [];
690
+ const rawName = entry.frontmatter.name;
691
+ const declaredName = rawName?.trim() ? rawName : void 0;
692
+ const uriName = skillNameFromUri(entry.uri);
693
+ if (!declaredName) {
694
+ issues.push({
695
+ code: "missing-name",
696
+ severity: "error",
697
+ message: "frontmatter.name is required but missing or empty."
698
+ });
699
+ } else if (codePointLength(declaredName) > SKILL_NAME_MAX_LENGTH || !SKILL_NAME_PATTERN.test(declaredName)) {
700
+ issues.push({
701
+ code: "malformed-name",
702
+ severity: "error",
703
+ message: `frontmatter.name "${declaredName}" is not a valid Agent Skills name: 1\u2013${SKILL_NAME_MAX_LENGTH} lowercase alphanumerics and hyphens, with no leading, trailing or consecutive hyphen.`
704
+ });
705
+ }
706
+ const rawDescription = entry.frontmatter.description;
707
+ if (!rawDescription?.trim()) {
708
+ issues.push({
709
+ code: "missing-description",
710
+ severity: "error",
711
+ message: "frontmatter.description is required but missing or empty."
712
+ });
713
+ } else if (codePointLength(rawDescription) > SKILL_DESCRIPTION_MAX_LENGTH) {
714
+ issues.push({
715
+ code: "malformed-description",
716
+ severity: "error",
717
+ message: `frontmatter.description is ${codePointLength(rawDescription)} characters, above the ${SKILL_DESCRIPTION_MAX_LENGTH}-character limit.`
718
+ });
719
+ }
720
+ if (uriName === void 0) {
721
+ issues.push({
722
+ code: "malformed-uri",
723
+ severity: "error",
724
+ message: `Skill URI must be a hierarchical URI ending with "${SKILL_FILE_SUFFIX}" and carrying a non-empty path segment before it.`
725
+ });
726
+ } else if (declaredName && uriName !== declaredName) {
727
+ issues.push({
728
+ code: "name-path-mismatch",
729
+ severity: "error",
730
+ message: `URI path segment "${uriName}" does not match frontmatter.name "${declaredName}".`
731
+ });
732
+ }
733
+ if (entry.resources === DYNAMIC_RESOURCES) {
734
+ issues.push({
735
+ code: "dynamic-resources",
736
+ severity: "warning",
737
+ message: 'resources is "dynamic": the file set is generated, so no digest is advertised and integrity cannot be verified.'
738
+ });
739
+ return issues;
740
+ }
741
+ if (entry.resources.length > SKILL_MAX_RESOURCE_ENTRIES) {
742
+ issues.push({
743
+ code: "resource-limit-exceeded",
744
+ severity: "warning",
745
+ message: `Skill declares ${entry.resources.length} resource entries, above the ${SKILL_MAX_RESOURCE_ENTRIES}-entry interoperability limit; a host is only required to support up to it.`
746
+ });
747
+ }
748
+ const totalBytes = totalSkillBytes(entry.resources);
749
+ if (totalBytes > SKILL_MAX_TOTAL_BYTES) {
750
+ issues.push({
751
+ code: "size-limit-exceeded",
752
+ severity: "warning",
753
+ message: `Skill resources total ${totalBytes} bytes, above the ${SKILL_MAX_TOTAL_BYTES}-byte (16 MiB) interoperability limit; a host is only required to support up to it.`
754
+ });
755
+ }
756
+ const entryIdentity = skillUriIdentity(entry.uri);
757
+ if (!entry.resources.some(
758
+ (resource) => skillUriIdentity(resource.uri) === entryIdentity
759
+ )) {
760
+ issues.push({
761
+ code: "manifest-missing-self",
762
+ severity: "error",
763
+ message: `Manifest does not list the skill's own entry file (${entry.uri}); a manifest must be the complete file set.`
764
+ });
765
+ }
766
+ const seenUris = /* @__PURE__ */ new Set();
767
+ const normalizedEntryUri = normalizeSkillUri(entry.uri);
768
+ const root = normalizedEntryUri !== void 0 && normalizedEntryUri.endsWith(SKILL_FILE_SUFFIX) ? `${normalizedEntryUri.slice(0, -SKILL_FILE_SUFFIX.length)}/` : void 0;
769
+ for (const resource of entry.resources) {
770
+ const identity2 = skillUriIdentity(resource.uri);
771
+ if (seenUris.has(identity2)) {
772
+ issues.push({
773
+ code: "duplicate-resource",
774
+ severity: "error",
775
+ message: "Manifest lists this URI more than once; entries must be unique.",
776
+ resourceUri: resource.uri
777
+ });
778
+ }
779
+ seenUris.add(identity2);
780
+ if (root !== void 0) {
781
+ const normalized = normalizeSkillUri(resource.uri);
782
+ if (normalized === void 0 || !normalized.startsWith(root)) {
783
+ issues.push({
784
+ code: "resource-outside-skill-root",
785
+ severity: "error",
786
+ message: `Manifest entry does not resolve inside the skill root "${root}".`,
787
+ resourceUri: resource.uri
788
+ });
789
+ }
790
+ }
791
+ if (resource.digest === void 0) {
792
+ issues.push({
793
+ code: "missing-digest",
794
+ severity: "error",
795
+ message: "Manifest entry declares no digest, which is required \u2014 and without it the file cannot be verified.",
796
+ resourceUri: resource.uri
797
+ });
798
+ } else if (!DIGEST_PATTERN.test(resource.digest)) {
799
+ issues.push({
800
+ code: "malformed-digest",
801
+ severity: "error",
802
+ message: `Digest "${resource.digest}" is not "sha256:" followed by 64 lowercase hex characters.`,
803
+ resourceUri: resource.uri
804
+ });
805
+ }
806
+ if (resource.size === void 0) {
807
+ issues.push({
808
+ code: "missing-size",
809
+ severity: "error",
810
+ message: "Manifest entry declares no size, which is required \u2014 and without it the entry is excluded from the 16 MiB total and its length cannot be cross-checked.",
811
+ resourceUri: resource.uri
812
+ });
813
+ } else if (!isUsableSize(resource.size)) {
814
+ issues.push({
815
+ code: "malformed-size",
816
+ severity: "error",
817
+ message: `Size ${resource.size} is not a non-negative integer byte length.`,
818
+ resourceUri: resource.uri
819
+ });
820
+ }
821
+ }
822
+ return issues;
823
+ }
824
+ function isUsableSize(size) {
825
+ return size !== void 0 && Number.isSafeInteger(size) && size >= 0;
826
+ }
827
+ function totalSkillBytes(resources) {
828
+ return resources.reduce(
829
+ (sum, r) => sum + (isUsableSize(r.size) ? r.size : 0),
830
+ 0
831
+ );
832
+ }
833
+ function canonicalize(value) {
834
+ if (Array.isArray(value)) return value.map(canonicalize);
835
+ if (value === null || typeof value !== "object") return value;
836
+ return sortKeys(value);
837
+ }
838
+ function sortKeys(value) {
839
+ return Object.fromEntries(
840
+ Object.entries(value).map(([key, member]) => [key, canonicalize(member)]).sort(([x], [y]) => x.localeCompare(y))
841
+ );
842
+ }
843
+ function toHex(bytes) {
844
+ let out = "";
845
+ for (const byte of bytes) out += byte.toString(16).padStart(2, "0");
846
+ return out;
847
+ }
848
+ async function sha256Digest(bytes) {
849
+ const subtle = globalThis.crypto?.subtle;
850
+ if (!subtle) return `sha256:${toHex(sha256Bytes(bytes))}`;
851
+ const copy = new Uint8Array(bytes);
852
+ const hash = await subtle.digest("SHA-256", copy.buffer);
853
+ return `sha256:${toHex(new Uint8Array(hash))}`;
854
+ }
855
+ function textToBytes(text) {
856
+ return new TextEncoder().encode(text);
857
+ }
858
+ function bytesToText(bytes) {
859
+ return new TextDecoder().decode(bytes);
860
+ }
861
+ function base64ToBytes(blob) {
862
+ const binary = atob(blob);
863
+ const bytes = new Uint8Array(binary.length);
864
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
865
+ return bytes;
866
+ }
867
+ function skillFileBytes(contents) {
868
+ if (typeof contents.text === "string") return textToBytes(contents.text);
869
+ if (typeof contents.blob === "string") return base64ToBytes(contents.blob);
870
+ throw new Error("resources/read returned neither text nor blob content.");
871
+ }
872
+ async function verifySkillResource(resource, bytes) {
873
+ const expectedSize = resource.size;
874
+ if (expectedSize !== void 0 && expectedSize !== bytes.byteLength) {
875
+ return {
876
+ status: "mismatch",
877
+ expectedSize,
878
+ actualSize: bytes.byteLength,
879
+ ...resource.digest !== void 0 ? { expectedDigest: resource.digest } : {},
880
+ reason: `Manifest declares ${expectedSize} bytes but the fetched file is ${bytes.byteLength}.`
881
+ };
882
+ }
883
+ const expectedDigest = resource.digest;
884
+ if (expectedDigest === void 0) {
885
+ return {
886
+ status: "unverifiable",
887
+ reason: "The manifest entry advertises no digest."
888
+ };
889
+ }
890
+ if (!DIGEST_PATTERN.test(expectedDigest)) {
891
+ return {
892
+ status: "unverifiable",
893
+ expectedDigest,
894
+ reason: 'The advertised digest is not "sha256:" followed by 64 lowercase hex characters.'
895
+ };
896
+ }
897
+ const actualDigest = await sha256Digest(bytes);
898
+ return {
899
+ status: actualDigest === expectedDigest ? "verified" : "mismatch",
900
+ actualDigest,
901
+ expectedDigest,
902
+ ...expectedSize !== void 0 ? { expectedSize, actualSize: bytes.byteLength } : {}
903
+ };
904
+ }
905
+ function jsonLikeEqual(a, b) {
906
+ if (typeof a === "number" || typeof b === "number") {
907
+ if (typeof a === "number" && typeof b === "number") {
908
+ return Number.isFinite(a) && Number.isFinite(b) ? a === b : Object.is(a, b);
909
+ }
910
+ return false;
911
+ }
912
+ if (a === null || b === null) return a === b;
913
+ if (typeof a !== "object" || typeof b !== "object") return Object.is(a, b);
914
+ const aArray = Array.isArray(a);
915
+ if (aArray !== Array.isArray(b)) return false;
916
+ if (aArray) {
917
+ const x2 = a;
918
+ const y2 = b;
919
+ return x2.length === y2.length && x2.every((v, i) => jsonLikeEqual(v, y2[i]));
920
+ }
921
+ const x = a;
922
+ const y = b;
923
+ const keys = Object.keys(x);
924
+ return keys.length === Object.keys(y).length && keys.every((k) => Object.hasOwn(y, k) && jsonLikeEqual(x[k], y[k]));
925
+ }
926
+ function displayValue(value) {
927
+ if (typeof value === "number" && !Number.isFinite(value)) {
928
+ return String(value);
929
+ }
930
+ return JSON.stringify(
931
+ canonicalize(value),
932
+ (_key, member) => typeof member === "number" && !Number.isFinite(member) ? `<${String(member)}>` : member
933
+ );
934
+ }
935
+ function checkSkillFrontmatterMatch(entry, skillFileText) {
936
+ const { frontmatter } = splitSkillFile(skillFileText);
937
+ if (frontmatter === void 0) {
938
+ return [
939
+ {
940
+ code: "frontmatter-absent",
941
+ severity: "error",
942
+ message: "The served SKILL.md carries no YAML frontmatter block, so the listing's frontmatter cannot be the file's.",
943
+ resourceUri: entry.uri
944
+ }
945
+ ];
946
+ }
947
+ const listedError = jsonGraphError(entry.frontmatter);
948
+ if (listedError) {
949
+ return [
950
+ {
951
+ code: "frontmatter-unparsable",
952
+ severity: "error",
953
+ message: `The listing's own frontmatter cannot be compared: ${listedError}`,
954
+ resourceUri: entry.uri
955
+ }
956
+ ];
957
+ }
958
+ const parsed = parseSkillFrontmatter(frontmatter);
959
+ if ("error" in parsed) {
960
+ return [
961
+ {
962
+ code: "frontmatter-unparsable",
963
+ severity: "error",
964
+ message: `The served SKILL.md's frontmatter is not valid YAML: ${parsed.error}`,
965
+ resourceUri: entry.uri
966
+ }
967
+ ];
968
+ }
969
+ const issues = [];
970
+ const fields = [
971
+ .../* @__PURE__ */ new Set([
972
+ ...Object.keys(entry.frontmatter),
973
+ ...Object.keys(parsed.fields)
974
+ ])
975
+ ].sort();
976
+ for (const field of fields) {
977
+ const listed = entry.frontmatter[field];
978
+ const served = parsed.fields[field];
979
+ if (listed === void 0) {
980
+ issues.push({
981
+ code: "frontmatter-mismatch",
982
+ severity: "error",
983
+ message: `The served SKILL.md declares "${field}" but the listing's frontmatter omits it.`,
984
+ resourceUri: entry.uri
985
+ });
986
+ continue;
987
+ }
988
+ if (served === void 0) {
989
+ issues.push({
990
+ code: "frontmatter-mismatch",
991
+ severity: "error",
992
+ message: `The listing declares "${field}" but the served SKILL.md omits it.`,
993
+ resourceUri: entry.uri
994
+ });
995
+ continue;
996
+ }
997
+ if (!jsonLikeEqual(listed, served)) {
998
+ issues.push({
999
+ code: "frontmatter-mismatch",
1000
+ severity: "error",
1001
+ message: `Field "${field}" differs: the listing says ${displayValue(listed)} but the served SKILL.md says ${displayValue(served)}.`,
1002
+ resourceUri: entry.uri
1003
+ });
1004
+ }
1005
+ }
1006
+ return issues;
1007
+ }
1008
+ var COLLISION_SAMPLE = 3;
1009
+ function checkSkillNameCollisions(entries) {
1010
+ const byName = /* @__PURE__ */ new Map();
1011
+ for (const entry of entries) {
1012
+ const name = entry.frontmatter.name;
1013
+ if (typeof name !== "string" || name.trim() === "") continue;
1014
+ const group = byName.get(name);
1015
+ if (group) group.push(entry);
1016
+ else byName.set(name, [entry]);
1017
+ }
1018
+ const issues = /* @__PURE__ */ new Map();
1019
+ for (const [name, group] of byName) {
1020
+ const identities = new Set(group.map((e) => skillUriIdentity(e.uri)));
1021
+ if (identities.size < 2) continue;
1022
+ const uris = [...identities].sort();
1023
+ for (const entry of group) {
1024
+ const self = skillUriIdentity(entry.uri);
1025
+ const sample = [];
1026
+ for (const uri of uris) {
1027
+ if (uri === self) continue;
1028
+ sample.push(uri);
1029
+ if (sample.length === COLLISION_SAMPLE) break;
1030
+ }
1031
+ const unshown = identities.size - 1 - sample.length;
1032
+ const others = unshown > 0 ? `${sample.join(", ")}, and ${unshown} more` : sample.join(", ");
1033
+ const subject = identities.size === 2 ? "Another skill in this listing also declares" : `${identities.size - 1} other skills in this listing also declare`;
1034
+ issues.set(self, {
1035
+ code: "duplicate-name",
1036
+ severity: "warning",
1037
+ message: `${subject} the name "${name}" (${others}). This is legal \u2014 a consumer must tell them apart by their URIs rather than collapsing or preferring one.`
1038
+ });
1039
+ }
1040
+ }
1041
+ return issues;
1042
+ }
1043
+
393
1044
  // ../../core/json/nullableUnion.ts
394
1045
  var RENDERABLE_TYPES = [
395
1046
  "string",
@@ -616,7 +1267,11 @@ function isOfferable(branch, merged) {
616
1267
  return false;
617
1268
  }
618
1269
  if (!admitsObject(branch)) return false;
619
- const properties = Object.values(propertiesOf(merged));
1270
+ const mergedProperties = propertiesOf(merged);
1271
+ if (requiredOf(merged).some((name) => !Object.hasOwn(mergedProperties, name))) {
1272
+ return false;
1273
+ }
1274
+ const properties = Object.values(mergedProperties);
620
1275
  return properties.length > 0 && // Every value has to be something a renderer can read AND something a
621
1276
  // caller can satisfy. A `null` or an array is not a schema at all, and the
622
1277
  // web form dereferences one on the way to choosing a widget. JSON Schema's
@@ -1176,16 +1831,19 @@ function oauthEndpointOverridesFromSettings(settings) {
1176
1831
  };
1177
1832
  }
1178
1833
  function storedFieldsToInspectorSettings(stored) {
1179
- 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;
1834
+ 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.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== 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;
1180
1835
  if (!hasAny) return void 0;
1181
1836
  const headersPairs = stored.headers ? Object.entries(stored.headers).map(([key, value]) => ({ key, value })) : [];
1182
1837
  const settings = {
1183
1838
  headers: headersPairs,
1184
1839
  env: envRecordToPairs(stored.env),
1185
1840
  metadata: normalizeStoredMetadata(stored.metadata),
1186
- connectionTimeout: stored.connectionTimeout ?? 0,
1841
+ // Concrete product default, like taskTtl below: an absent field reads back
1842
+ // as 30 s so the form shows the bound that will actually apply. An explicit
1843
+ // 0 is preserved as-is — it is the user's opt-out, not a sentinel (#2320).
1844
+ connectionTimeout: stored.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT_MS,
1187
1845
  requestTimeout: stored.requestTimeout ?? 0,
1188
- // Unlike the timeouts (0 = "SDK default"), task TTL has a concrete product
1846
+ // Unlike requestTimeout (0 = "SDK default"), task TTL has a concrete product
1189
1847
  // default so the form shows it and "Run as task" has a value to send.
1190
1848
  taskTtl: stored.taskTtl ?? DEFAULT_TASK_TTL_MS,
1191
1849
  autoRefreshOnListChanged: stored.autoRefreshOnListChanged ?? false,
@@ -1199,6 +1857,12 @@ function storedFieldsToInspectorSettings(stored) {
1199
1857
  // `[]`, which `inspectorSettingsToStoredFields` then omits on write.
1200
1858
  roots: stored.roots ?? []
1201
1859
  };
1860
+ if (isSkillCatalogLimit(stored.skillCatalogMaxSkills)) {
1861
+ settings.skillCatalogMaxSkills = stored.skillCatalogMaxSkills;
1862
+ }
1863
+ if (isSkillCatalogLimit(stored.skillCatalogMaxBytes)) {
1864
+ settings.skillCatalogMaxBytes = stored.skillCatalogMaxBytes;
1865
+ }
1202
1866
  if (isProtocolEra(stored.protocolEra)) {
1203
1867
  settings.protocolEra = stored.protocolEra;
1204
1868
  }
@@ -1252,6 +1916,8 @@ var INSPECTOR_FIELD_KEY_MAP = {
1252
1916
  paginatedLists: true,
1253
1917
  advertisedExtensions: true,
1254
1918
  maxFetchRequests: true,
1919
+ skillCatalogMaxSkills: true,
1920
+ skillCatalogMaxBytes: true,
1255
1921
  oauth: true,
1256
1922
  roots: true
1257
1923
  };
@@ -1352,6 +2018,14 @@ function parseHeaderPair(value, previous = {}) {
1352
2018
  }
1353
2019
  return { ...previous, [key]: val };
1354
2020
  }
2021
+ function parseProtocolEra(value) {
2022
+ if (!isProtocolEra(value)) {
2023
+ throw new Error(
2024
+ `Invalid protocol era: ${value}. Valid eras are: legacy, auto, modern`
2025
+ );
2026
+ }
2027
+ return value;
2028
+ }
1355
2029
  var EMPTY_CATALOG_CONTENT = `${JSON.stringify({ mcpServers: {} }, null, 2)}
1356
2030
  `;
1357
2031
  function seedEmptyCatalog(resolvedPath) {
@@ -2753,10 +3427,16 @@ function headersToServerSettings(headers) {
2753
3427
  return void 0;
2754
3428
  }
2755
3429
  return {
2756
- headers: Object.entries(headers).map(([key, value]) => ({ key, value })),
3430
+ ...defaultServerSettings(),
3431
+ headers: Object.entries(headers).map(([key, value]) => ({ key, value }))
3432
+ };
3433
+ }
3434
+ function defaultServerSettings() {
3435
+ return {
3436
+ headers: [],
2757
3437
  env: [],
2758
3438
  metadata: {},
2759
- connectionTimeout: 0,
3439
+ connectionTimeout: DEFAULT_CONNECTION_TIMEOUT_MS,
2760
3440
  requestTimeout: 0,
2761
3441
  taskTtl: DEFAULT_TASK_TTL_MS,
2762
3442
  maxFetchRequests: DEFAULT_MAX_FETCH_REQUESTS,
@@ -2765,11 +3445,19 @@ function headersToServerSettings(headers) {
2765
3445
  roots: []
2766
3446
  };
2767
3447
  }
2768
- function mergeSettings(base, headers) {
2769
- const fromHeaders = headersToServerSettings(headers);
2770
- if (!fromHeaders) return base;
2771
- if (!base) return fromHeaders;
2772
- return { ...base, headers: fromHeaders.headers };
3448
+ function mergeSettings(base, overrides) {
3449
+ let settings = base;
3450
+ const fromHeaders = headersToServerSettings(overrides.headers);
3451
+ if (fromHeaders) {
3452
+ settings = settings ? { ...settings, headers: fromHeaders.headers } : fromHeaders;
3453
+ }
3454
+ if (overrides.protocolEra) {
3455
+ settings = {
3456
+ ...settings ?? defaultServerSettings(),
3457
+ protocolEra: overrides.protocolEra
3458
+ };
3459
+ }
3460
+ return settings;
2773
3461
  }
2774
3462
  async function loadServerEntries(serverOptions) {
2775
3463
  serverOptions = withDefaultCatalogPath(serverOptions);
@@ -2794,10 +3482,11 @@ async function loadServerEntries(serverOptions) {
2794
3482
  env: serverOptions.env,
2795
3483
  cwd: serverOptions.cwd
2796
3484
  }),
2797
- // Deliberate broadcast: a single `--header` set is merged into EVERY
2798
- // server in the catalog/config (fine for the common single-server case;
2799
- // for multi-server files, prefer per-server headers in the file itself).
2800
- settings: mergeSettings(entry.settings, serverOptions.headers)
3485
+ // Deliberate broadcast: a single `--header` set (and `--protocol-era`)
3486
+ // is merged into EVERY server in the catalog/config (fine for the
3487
+ // common single-server case; for multi-server files, prefer per-server
3488
+ // settings in the file itself).
3489
+ settings: mergeSettings(entry.settings, serverOptions)
2801
3490
  };
2802
3491
  }
2803
3492
  return result;
@@ -2811,7 +3500,7 @@ async function loadServerEntries(serverOptions) {
2811
3500
  return {
2812
3501
  default: {
2813
3502
  config: configs[0],
2814
- settings: headersToServerSettings(serverOptions.headers)
3503
+ settings: mergeSettings(void 0, serverOptions)
2815
3504
  }
2816
3505
  };
2817
3506
  }
@@ -2961,6 +3650,7 @@ function redactBody(body, contentType) {
2961
3650
  return body;
2962
3651
  }
2963
3652
  function findHeader(headers, name) {
3653
+ if (!headers) return void 0;
2964
3654
  const target = name.toLowerCase();
2965
3655
  for (const [key, value] of Object.entries(headers)) {
2966
3656
  if (key.toLowerCase() === target) return value;
@@ -2968,9 +3658,146 @@ function findHeader(headers, name) {
2968
3658
  return void 0;
2969
3659
  }
2970
3660
  function isLongLivedStreamResponse(method, contentType) {
2971
- if (method !== "GET") return false;
3661
+ if (method.toUpperCase() !== "GET") return false;
2972
3662
  if (!contentType) return false;
2973
- return contentType.includes("text/event-stream") || contentType.includes("application/x-ndjson");
3663
+ const type = contentType.toLowerCase();
3664
+ return type.includes("text/event-stream") || type.includes("application/x-ndjson");
3665
+ }
3666
+ var CHALLENGE_BODY_MAX_BYTES = 64 * 1024;
3667
+ var CHALLENGE_BODY_READ_MS = 2e3;
3668
+ var TRUNCATED_BODY_SUFFIX = "\n[truncated]";
3669
+ var WITHHELD_TRUNCATED_JSON_BODY = "[truncated JSON body withheld: an incomplete body cannot be redacted]";
3670
+ function recordableChallengeBody(read, contentType) {
3671
+ if (!read.truncated) return redactBody(read.text, contentType);
3672
+ const looksLikeJson = (contentType ?? "").toLowerCase().includes("json") || /^\s*[{[]/.test(read.text);
3673
+ if (looksLikeJson) return WITHHELD_TRUNCATED_JSON_BODY;
3674
+ return redactBody(read.text, contentType) + TRUNCATED_BODY_SUFFIX;
3675
+ }
3676
+ async function readChallengeBody(response) {
3677
+ let reader;
3678
+ try {
3679
+ const body = response.clone().body;
3680
+ if (!body) return void 0;
3681
+ reader = body.getReader();
3682
+ } catch {
3683
+ return void 0;
3684
+ }
3685
+ const decoder = new TextDecoder();
3686
+ let text = "";
3687
+ let bytes = 0;
3688
+ let timer;
3689
+ const deadline = new Promise((resolve7) => {
3690
+ timer = setTimeout(() => resolve7("timeout"), CHALLENGE_BODY_READ_MS);
3691
+ });
3692
+ try {
3693
+ while (bytes < CHALLENGE_BODY_MAX_BYTES) {
3694
+ const next = await Promise.race([reader.read(), deadline]);
3695
+ if (next === "timeout") break;
3696
+ if (next.done) return { text: text + decoder.decode(), truncated: false };
3697
+ const chunk = next.value.subarray(0, CHALLENGE_BODY_MAX_BYTES - bytes);
3698
+ bytes += chunk.byteLength;
3699
+ text += decoder.decode(chunk, { stream: true });
3700
+ }
3701
+ reader.cancel().catch(() => {
3702
+ });
3703
+ return { text: text + decoder.decode(), truncated: true };
3704
+ } catch {
3705
+ return void 0;
3706
+ } finally {
3707
+ clearTimeout(timer);
3708
+ }
3709
+ }
3710
+ var DEFAULT_STREAM_UPDATE_INTERVAL_MS = 250;
3711
+ function longLivedStreamFraming(contentType) {
3712
+ return contentType?.toLowerCase().includes("application/x-ndjson") ? "ndjson" : "sse";
3713
+ }
3714
+ function watchLongLivedStream(response, id, framing, callbacks) {
3715
+ const listener = callbacks.updateStream;
3716
+ if (!listener) return;
3717
+ const report = (state) => {
3718
+ try {
3719
+ listener(id, state);
3720
+ } catch {
3721
+ }
3722
+ };
3723
+ const intervalMs = callbacks.streamUpdateIntervalMs ?? DEFAULT_STREAM_UPDATE_INTERVAL_MS;
3724
+ let body;
3725
+ try {
3726
+ body = response.clone().body;
3727
+ } catch {
3728
+ return;
3729
+ }
3730
+ if (!body) return;
3731
+ const reader = body.getReader();
3732
+ const decoder = new TextDecoder();
3733
+ let eventCount = 0;
3734
+ let pending = "";
3735
+ let blockHasData = false;
3736
+ let reportTimer = null;
3737
+ const reportCount = () => {
3738
+ if (intervalMs <= 0) {
3739
+ report({ eventCount });
3740
+ return;
3741
+ }
3742
+ if (reportTimer !== null) return;
3743
+ reportTimer = setTimeout(() => {
3744
+ reportTimer = null;
3745
+ report({ eventCount });
3746
+ }, intervalMs);
3747
+ };
3748
+ report({ eventCount });
3749
+ const consumeLine = (line) => {
3750
+ if (framing === "ndjson") {
3751
+ if (line.trim() === "") return;
3752
+ eventCount += 1;
3753
+ reportCount();
3754
+ return;
3755
+ }
3756
+ if (line === "") {
3757
+ if (blockHasData) {
3758
+ eventCount += 1;
3759
+ blockHasData = false;
3760
+ reportCount();
3761
+ }
3762
+ return;
3763
+ }
3764
+ if (line === "data" || line.startsWith("data:")) blockHasData = true;
3765
+ };
3766
+ const consumeCompleteLines = () => {
3767
+ for (; ; ) {
3768
+ const cr = pending.indexOf("\r");
3769
+ const lf = pending.indexOf("\n");
3770
+ const end = cr === -1 ? lf : lf === -1 ? cr : Math.min(cr, lf);
3771
+ if (end === -1) return;
3772
+ if (pending[end] === "\r") {
3773
+ if (end === pending.length - 1) return;
3774
+ const width = pending[end + 1] === "\n" ? 2 : 1;
3775
+ consumeLine(pending.slice(0, end));
3776
+ pending = pending.slice(end + width);
3777
+ } else {
3778
+ consumeLine(pending.slice(0, end));
3779
+ pending = pending.slice(end + 1);
3780
+ }
3781
+ }
3782
+ };
3783
+ const pump = async () => {
3784
+ try {
3785
+ for (; ; ) {
3786
+ const { value, done } = await reader.read();
3787
+ if (done) break;
3788
+ pending += decoder.decode(value, { stream: true });
3789
+ consumeCompleteLines();
3790
+ }
3791
+ if (pending.endsWith("\r")) consumeLine(pending.slice(0, -1));
3792
+ } catch {
3793
+ }
3794
+ if (reportTimer !== null) {
3795
+ clearTimeout(reportTimer);
3796
+ reportTimer = null;
3797
+ }
3798
+ report({ eventCount, closedAt: /* @__PURE__ */ new Date() });
3799
+ };
3800
+ void pump();
2974
3801
  }
2975
3802
  function createFetchTracker(baseFetch, callbacks) {
2976
3803
  return async (input, init) => {
@@ -2978,7 +3805,7 @@ function createFetchTracker(baseFetch, callbacks) {
2978
3805
  const timestamp = /* @__PURE__ */ new Date();
2979
3806
  const id = `${timestamp.getTime()}-${Math.random().toString(36).slice(2, 11)}`;
2980
3807
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
2981
- const method = init?.method || "GET";
3808
+ const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
2982
3809
  const rawRequestHeaders = {};
2983
3810
  if (input instanceof Request) {
2984
3811
  input.headers.forEach((value, key) => {
@@ -3058,7 +3885,21 @@ function createFetchTracker(baseFetch, callbacks) {
3058
3885
  responseBody: void 0,
3059
3886
  duration
3060
3887
  };
3888
+ if ((responseStatus === 401 || responseStatus === 403) && response.body && !response.bodyUsed) {
3889
+ const read = await readChallengeBody(response);
3890
+ entry.responseBody = read === void 0 ? void 0 : recordableChallengeBody(read, response.headers.get("content-type"));
3891
+ callbacks.trackRequest?.(entry);
3892
+ return response;
3893
+ }
3061
3894
  callbacks.trackRequest?.(entry);
3895
+ if (isLongLivedStream && response.body && !response.bodyUsed) {
3896
+ watchLongLivedStream(
3897
+ response,
3898
+ id,
3899
+ longLivedStreamFraming(response.headers.get("content-type")),
3900
+ callbacks
3901
+ );
3902
+ }
3062
3903
  if (!isLongLivedStream && response.body && !response.bodyUsed) {
3063
3904
  const responseContentType = response.headers.get("content-type");
3064
3905
  try {
@@ -3380,7 +4221,7 @@ function createAuthChallengeInterceptFetch(baseFetch) {
3380
4221
  if (!challenge) {
3381
4222
  return response;
3382
4223
  }
3383
- await response.body?.cancel().catch(() => {
4224
+ void response.body?.cancel().catch(() => {
3384
4225
  });
3385
4226
  throw new AuthChallengeError(
3386
4227
  challenge,
@@ -3528,14 +4369,24 @@ function createTransportNode(config, options = {}) {
3528
4369
  pipeStderr = false,
3529
4370
  onFetchRequest,
3530
4371
  onFetchResponseBody,
4372
+ onFetchStreamUpdate,
3531
4373
  authProvider,
3532
4374
  settings,
3533
4375
  interceptAuthChallenges = false,
3534
4376
  onAuthChallengeObserved
3535
4377
  } = options;
4378
+ const wantsFetchTracking = Boolean(
4379
+ onFetchRequest || onFetchResponseBody || onFetchStreamUpdate
4380
+ );
3536
4381
  const baseFetch = optionsFetchFn ?? createProxyFetch() ?? globalThis.fetch;
3537
4382
  const withChallengeObserver = (inner) => onAuthChallengeObserved ? createAuthChallengeObserverFetch(inner, onAuthChallengeObserved) : inner;
3538
- const fetchWithOptionalAuthIntercept = interceptAuthChallenges ? createAuthChallengeInterceptFetch(withChallengeObserver(baseFetch)) : withChallengeObserver(baseFetch);
4383
+ const withTracking = (inner) => wantsFetchTracking ? createFetchTracker(inner, {
4384
+ trackRequest: onFetchRequest,
4385
+ updateResponseBody: onFetchResponseBody,
4386
+ updateStream: onFetchStreamUpdate
4387
+ }) : inner;
4388
+ const trackedBase = withTracking(withChallengeObserver(baseFetch));
4389
+ const fetchWithOptionalAuthIntercept = interceptAuthChallenges ? createAuthChallengeInterceptFetch(trackedBase) : trackedBase;
3539
4390
  if (serverType === "stdio") {
3540
4391
  const stdioConfig = config;
3541
4392
  const transport = new StdioClientTransport({
@@ -3561,11 +4412,7 @@ function createTransportNode(config, options = {}) {
3561
4412
  const sseConfig = config;
3562
4413
  const url = new URL(sseConfig.url);
3563
4414
  const configuredSseFetch = sseConfig.eventSourceInit?.fetch;
3564
- const sseFetch = configuredSseFetch ? withChallengeObserver(configuredSseFetch) : fetchWithOptionalAuthIntercept;
3565
- const trackedFetch = onFetchRequest ? createFetchTracker(sseFetch, {
3566
- trackRequest: onFetchRequest,
3567
- updateResponseBody: onFetchResponseBody
3568
- }) : sseFetch;
4415
+ const trackedFetch = configuredSseFetch ? withTracking(withChallengeObserver(configuredSseFetch)) : fetchWithOptionalAuthIntercept;
3569
4416
  const headers = headersFromSettings(settings);
3570
4417
  const eventSourceInit = {
3571
4418
  ...sseConfig.eventSourceInit,
@@ -3576,15 +4423,11 @@ function createTransportNode(config, options = {}) {
3576
4423
  ...sseConfig.requestInit,
3577
4424
  ...headers && { headers }
3578
4425
  };
3579
- const postFetch = onFetchRequest ? createFetchTracker(fetchWithOptionalAuthIntercept, {
3580
- trackRequest: onFetchRequest,
3581
- updateResponseBody: onFetchResponseBody
3582
- }) : fetchWithOptionalAuthIntercept;
3583
4426
  const transport = new SSEClientTransport(url, {
3584
4427
  authProvider,
3585
4428
  eventSourceInit,
3586
4429
  requestInit,
3587
- fetch: postFetch
4430
+ fetch: fetchWithOptionalAuthIntercept
3588
4431
  });
3589
4432
  return { transport };
3590
4433
  } else {
@@ -3595,14 +4438,10 @@ function createTransportNode(config, options = {}) {
3595
4438
  ...httpConfig.requestInit,
3596
4439
  ...headers && { headers }
3597
4440
  };
3598
- const transportFetch = onFetchRequest ? createFetchTracker(fetchWithOptionalAuthIntercept, {
3599
- trackRequest: onFetchRequest,
3600
- updateResponseBody: onFetchResponseBody
3601
- }) : fetchWithOptionalAuthIntercept;
3602
4441
  const transport = new StreamableHTTPClientTransport(url, {
3603
4442
  authProvider,
3604
4443
  requestInit,
3605
- fetch: transportFetch,
4444
+ fetch: fetchWithOptionalAuthIntercept,
3606
4445
  // SEP-2350: how the transport reacts to a `403 insufficient_scope`
3607
4446
  // challenge. Defaults to the SDK's `reauthorize` when unset.
3608
4447
  ...settings?.oauthOnInsufficientScope && {
@@ -4238,6 +5077,7 @@ import {
4238
5077
  OAuthTokensSchema as OAuthTokensSchema2
4239
5078
  } from "@modelcontextprotocol/core";
4240
5079
  var DEFAULT_REVOCATION_TIMEOUT_MS = 5e3;
5080
+ var MIN_REVOCATION_REQUEST_BUDGET_MS = 5;
4241
5081
  function selectRevocableToken(tokens) {
4242
5082
  if (!tokens) return null;
4243
5083
  if (tokens.refresh_token) {
@@ -4285,7 +5125,10 @@ function base64Encode(value) {
4285
5125
  return Buffer.from(value, "utf8").toString("base64");
4286
5126
  }
4287
5127
  async function revokeToken(params) {
4288
- const timeoutMs = params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS;
5128
+ const timeoutMs = Math.max(
5129
+ 0,
5130
+ Math.round(params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS)
5131
+ );
4289
5132
  try {
4290
5133
  const { url, init } = buildRevocationRequest(params);
4291
5134
  const response = await withDeadline(
@@ -4473,7 +5316,7 @@ async function runPlan(plan, params) {
4473
5316
  if (!endpoint) return { status: "skipped", reason: "no_endpoint" };
4474
5317
  const outcomes = [...plan.failures];
4475
5318
  const timeoutMs = params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS;
4476
- const deadlineAt = Date.now() + timeoutMs;
5319
+ const deadlineAt = performance.now() + timeoutMs;
4477
5320
  for (const grant of plan.grants) {
4478
5321
  if (grant.issuer === void 0) {
4479
5322
  outcomes.push({
@@ -4489,8 +5332,8 @@ async function runPlan(plan, params) {
4489
5332
  });
4490
5333
  continue;
4491
5334
  }
4492
- const remainingMs = deadlineAt - Date.now();
4493
- if (remainingMs <= 0) {
5335
+ const remainingMs = deadlineAt - performance.now();
5336
+ if (remainingMs <= MIN_REVOCATION_REQUEST_BUDGET_MS) {
4494
5337
  outcomes.push({
4495
5338
  status: "failed",
4496
5339
  endpoint,
@@ -4542,13 +5385,13 @@ async function clearStoredAuthForRelogin(serverUrl, options) {
4542
5385
  }
4543
5386
  async function sendPlans(plans, budgetMs) {
4544
5387
  const fetchFn = createProxyFetch() ?? fetch;
4545
- const deadlineAt = Date.now() + budgetMs;
5388
+ const deadlineAt = performance.now() + budgetMs;
4546
5389
  let reported;
4547
5390
  let lastSkip;
4548
5391
  for (const plan of plans) {
4549
- const remainingMs = deadlineAt - Date.now();
5392
+ const remainingMs = deadlineAt - performance.now();
4550
5393
  const needsNetwork = plan.outcome === void 0;
4551
- if (needsNetwork && remainingMs <= 0) {
5394
+ if (needsNetwork && remainingMs <= MIN_REVOCATION_REQUEST_BUDGET_MS) {
4552
5395
  const exhausted = {
4553
5396
  status: "failed",
4554
5397
  detail: `the ${budgetMs}ms revocation budget was exhausted before "${plan.serverUrl}" was attempted`
@@ -4576,7 +5419,7 @@ async function sendPlans(plans, budgetMs) {
4576
5419
  // ../../core/mcp/inspectorClient.ts
4577
5420
  import { Client } from "@modelcontextprotocol/client";
4578
5421
  import {
4579
- ProgressTokenSchema,
5422
+ ProgressTokenSchema as ProgressTokenSchema2,
4580
5423
  RequestMetaSchema
4581
5424
  } from "@modelcontextprotocol/core";
4582
5425
 
@@ -4769,7 +5612,15 @@ var MessageTrackingTransport = class {
4769
5612
  "client"
4770
5613
  );
4771
5614
  } else if ("method" in message) {
4772
- this.callbacks.trackRequest?.(message, "client");
5615
+ const request = message;
5616
+ this.callbacks.trackRequest?.(request, "client");
5617
+ const abortedBeforeSend = options?.requestSignal?.aborted === true;
5618
+ return this.baseTransport.send(message, options).catch((err) => {
5619
+ if (abortedBeforeSend || !options?.requestSignal?.aborted) {
5620
+ this.callbacks.trackSendFailure?.(request, err);
5621
+ }
5622
+ throw err;
5623
+ });
4773
5624
  }
4774
5625
  } else if ("method" in message) {
4775
5626
  this.callbacks.trackNotification?.(
@@ -4890,7 +5741,7 @@ import {
4890
5741
  } from "@modelcontextprotocol/client";
4891
5742
 
4892
5743
  // ../../core/mcp/modernTaskSchemas.ts
4893
- import { z } from "zod/v4";
5744
+ import { z as z2 } from "zod/v4";
4894
5745
  var TASKS_EXTENSION_KEY = "io.modelcontextprotocol/tasks";
4895
5746
  var MODERN_PROTOCOL_VERSION2 = "2026-07-28";
4896
5747
  var TASKS_EXTENSION_CLIENT_CAPABILITY = {
@@ -4900,31 +5751,31 @@ var MODERN_TASK_HANDLE_META = "io.modelcontextprotocol/inspector/modernTaskHandl
4900
5751
  function isModernCreateTaskResult(result) {
4901
5752
  return typeof result === "object" && result !== null && result.resultType === "task" && typeof result.taskId === "string";
4902
5753
  }
4903
- var ModernTaskStatusSchema = z.enum([
5754
+ var ModernTaskStatusSchema = z2.enum([
4904
5755
  "working",
4905
5756
  "input_required",
4906
5757
  "completed",
4907
5758
  "failed",
4908
5759
  "cancelled"
4909
5760
  ]);
4910
- var ModernDetailedTaskSchema = z.looseObject({
4911
- taskId: z.string(),
5761
+ var ModernDetailedTaskSchema = z2.looseObject({
5762
+ taskId: z2.string(),
4912
5763
  status: ModernTaskStatusSchema,
4913
- statusMessage: z.string().optional(),
4914
- createdAt: z.string(),
4915
- lastUpdatedAt: z.string(),
4916
- ttlMs: z.number().nullable().optional(),
4917
- pollIntervalMs: z.number().optional(),
5764
+ statusMessage: z2.string().optional(),
5765
+ createdAt: z2.string(),
5766
+ lastUpdatedAt: z2.string(),
5767
+ ttlMs: z2.number().nullable().optional(),
5768
+ pollIntervalMs: z2.number().optional(),
4918
5769
  /** Present on `completed`: the original request's result (e.g. CallToolResult). */
4919
- result: z.record(z.string(), z.unknown()).optional(),
5770
+ result: z2.record(z2.string(), z2.unknown()).optional(),
4920
5771
  /** Present on `failed`: the JSON-RPC error that ended the task. */
4921
- error: z.record(z.string(), z.unknown()).optional(),
5772
+ error: z2.record(z2.string(), z2.unknown()).optional(),
4922
5773
  /** Present on `input_required`: embedded server→client requests, keyed by id. */
4923
- inputRequests: z.record(z.string(), z.unknown()).optional()
5774
+ inputRequests: z2.record(z2.string(), z2.unknown()).optional()
4924
5775
  });
4925
5776
  var ModernGetTaskResultSchema = ModernDetailedTaskSchema;
4926
- var ModernUpdateTaskResultSchema = z.looseObject({});
4927
- var ModernCancelTaskResultSchema = z.looseObject({});
5777
+ var ModernUpdateTaskResultSchema = z2.looseObject({});
5778
+ var ModernCancelTaskResultSchema = z2.looseObject({});
4928
5779
  function normalizeModernTask(modern) {
4929
5780
  const { ttlMs, pollIntervalMs, ...rest } = modern;
4930
5781
  const normalized = { ...rest };
@@ -4937,9 +5788,10 @@ function readInputRequests(modern) {
4937
5788
  }
4938
5789
 
4939
5790
  // ../../core/mcp/extensions.ts
5791
+ import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
4940
5792
  var EMA_EXTENSION_KEY = "io.modelcontextprotocol/enterprise-managed-authorization";
4941
5793
  var UI_EXTENSION_KEY = "io.modelcontextprotocol/ui";
4942
- var MCP_APP_MIME_TYPE = "text/html;profile=mcp-app";
5794
+ var MCP_APP_MIME_TYPE = RESOURCE_MIME_TYPE;
4943
5795
  var ADVERTISABLE_EXTENSIONS = [
4944
5796
  {
4945
5797
  key: TASKS_EXTENSION_KEY,
@@ -4958,6 +5810,18 @@ var ADVERTISABLE_EXTENSIONS = [
4958
5810
  // a conforming server checks the `mimeTypes` before serving a UI resource.
4959
5811
  defaultAdvertised: true,
4960
5812
  advertisement: { mimeTypes: [MCP_APP_MIME_TYPE] }
5813
+ },
5814
+ {
5815
+ key: SKILLS_EXTENSION_KEY,
5816
+ label: "Skills (io.modelcontextprotocol/skills)",
5817
+ // The Skills extension (SEP-2640). SEP-2133 negotiates an extension from
5818
+ // both sides, so a server may refuse `skills/list`, `skills/get` and
5819
+ // `resources/directory/read` to a client that did not declare it — and the
5820
+ // Inspector calls all three once the server declares its half. Advertised
5821
+ // by default for that reason; turning it off is how to check a server's
5822
+ // refusal path. Declared with no settings: SEP-2640 defines none for the
5823
+ // client side. (#2373)
5824
+ defaultAdvertised: true
4961
5825
  }
4962
5826
  ];
4963
5827
  function buildClientExtensions(input) {
@@ -5065,7 +5929,7 @@ import {
5065
5929
  ListResourcesResultSchema,
5066
5930
  ListResourceTemplatesResultSchema,
5067
5931
  ListPromptsResultSchema,
5068
- ResourceSchema,
5932
+ ResourceSchema as ResourceSchema2,
5069
5933
  ResourceTemplateSchema,
5070
5934
  PromptSchema
5071
5935
  } from "@modelcontextprotocol/core";
@@ -5088,7 +5952,7 @@ function validateToolOutput(provider, tool, result) {
5088
5952
  }
5089
5953
 
5090
5954
  // ../../core/mcp/listSalvage.ts
5091
- import { z as z2 } from "zod/v4";
5955
+ import { z as z3 } from "zod/v4";
5092
5956
  import { SdkError as SdkError2, SdkErrorCode as SdkErrorCode2 } from "@modelcontextprotocol/client";
5093
5957
  import { ToolSchema } from "@modelcontextprotocol/core";
5094
5958
  var LIST_MAX_PAGES = 64;
@@ -5104,20 +5968,20 @@ function isSalvageableRejection(err) {
5104
5968
  return SdkError2.isInstance(err) && err.code === SdkErrorCode2.InvalidResult;
5105
5969
  }
5106
5970
  function lenientListPageSchema(resultSchema, itemsKey) {
5107
- return resultSchema.extend({ [itemsKey]: z2.array(z2.unknown()) });
5971
+ return resultSchema.extend({ [itemsKey]: z3.array(z3.unknown()) });
5108
5972
  }
5109
5973
  function nextCursorOf(page) {
5110
5974
  if (typeof page !== "object" || page === null) return void 0;
5111
5975
  const value = page.nextCursor;
5112
5976
  return typeof value === "string" ? value : void 0;
5113
5977
  }
5114
- var ModernResultEnvelopeSchema = z2.looseObject({
5115
- resultType: z2.literal("complete"),
5978
+ var ModernResultEnvelopeSchema = z3.looseObject({
5979
+ resultType: z3.literal("complete"),
5116
5980
  // A non-negative INTEGER, matching the codec: `ttlMs: -1` or `0.5` is an
5117
5981
  // envelope violation the strict path rejects, and accepting it here on the
5118
5982
  // strength of an unrelated bad entry would hide it.
5119
- ttlMs: z2.int().min(0),
5120
- cacheScope: z2.enum(["public", "private"])
5983
+ ttlMs: z3.int().min(0),
5984
+ cacheScope: z3.enum(["public", "private"])
5121
5985
  });
5122
5986
  function rawItemsOf(page, itemsKey) {
5123
5987
  if (typeof page !== "object" || page === null) return void 0;
@@ -5186,9 +6050,9 @@ var LegacyToolWireSchema = ToolSchema.extend({
5186
6050
  outputSchema: ToolSchema.shape.inputSchema.optional()
5187
6051
  });
5188
6052
  var ModernToolWireSchema = ToolSchema.extend({
5189
- inputSchema: z2.looseObject({
5190
- type: z2.literal("object"),
5191
- $schema: z2.string().optional()
6053
+ inputSchema: z3.looseObject({
6054
+ type: z3.literal("object"),
6055
+ $schema: z3.string().optional()
5192
6056
  })
5193
6057
  });
5194
6058
  function stripExecution(tool) {
@@ -5202,10 +6066,10 @@ function eraToolItemSchema(wire, normalize) {
5202
6066
 
5203
6067
  // ../../core/mcp/taskNotificationSchemas.ts
5204
6068
  import { NotificationSchema } from "@modelcontextprotocol/core";
5205
- import { z as z3 } from "zod/v4";
6069
+ import { z as z4 } from "zod/v4";
5206
6070
  var TasksListChangedNotificationSchema = NotificationSchema.extend({
5207
- method: z3.literal("notifications/tasks/list_changed"),
5208
- params: z3.record(z3.string(), z3.unknown()).optional()
6071
+ method: z4.literal("notifications/tasks/list_changed"),
6072
+ params: z4.record(z4.string(), z4.unknown()).optional()
5209
6073
  });
5210
6074
 
5211
6075
  // ../../core/mcp/uriTemplate.ts
@@ -5845,42 +6709,191 @@ function withOAuthEndpointOverrides(fetchFn, getOverrides) {
5845
6709
  };
5846
6710
  }
5847
6711
 
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;
6712
+ // ../../core/auth/requestTimeout.ts
6713
+ var DEFAULT_OAUTH_REQUEST_TIMEOUT_MS = 3e4;
6714
+ var OAuthRequestTimeoutError = class extends Error {
6715
+ /**
6716
+ * The endpoint that stalled, with sensitive query values redacted.
6717
+ *
6718
+ * Redacted in the constructor rather than at each display site, so every
6719
+ * consumer is covered by construction: this error's message is recorded
6720
+ * verbatim by `createFetchTracker` into the Network log and the persisted
6721
+ * session, and an OAuth endpoint can carry a `code`, an `access_token` or a
6722
+ * `client_secret` in its query string — which `fetchTracking` already
6723
+ * deliberately redacts everywhere else it records a URL (Copilot). The path
6724
+ * and every non-sensitive parameter survive, so the endpoint stays
6725
+ * identifiable, which was the point of naming it.
6726
+ */
6727
+ url;
6728
+ timeoutMs;
6729
+ constructor(url, timeoutMs) {
6730
+ const safeUrl = redactUrlQuery(url);
6731
+ super(`OAuth request to ${safeUrl} timed out after ${timeoutMs}ms`);
6732
+ this.name = "OAuthRequestTimeoutError";
6733
+ this.url = safeUrl;
6734
+ this.timeoutMs = timeoutMs;
6735
+ }
6736
+ };
6737
+ var MAX_TIMER_DELAY_MS = 2147483647;
6738
+ function normalizeBudget(timeoutMs) {
6739
+ if (!Number.isFinite(timeoutMs)) return DEFAULT_OAUTH_REQUEST_TIMEOUT_MS;
6740
+ return Math.min(MAX_TIMER_DELAY_MS, Math.max(0, Math.round(timeoutMs)));
6741
+ }
6742
+ function readNegotiatesEndpoint(hint) {
6743
+ if (!hint) return true;
5858
6744
  try {
5859
- url = new URL(rfc8414Url);
6745
+ return hint();
5860
6746
  } catch {
5861
- return [];
6747
+ return true;
5862
6748
  }
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
6749
  }
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) {
6750
+ var OAUTH_FORM_MEDIA_TYPE = "application/x-www-form-urlencoded";
6751
+ function exemptMcpEndpoint(getServerUrl, negotiatesEndpoint) {
6752
+ return (url, headers) => {
6753
+ if ((headers.get("content-type") ?? "").split(";")[0].trim().toLowerCase() === OAUTH_FORM_MEDIA_TYPE) {
6754
+ return false;
6755
+ }
6756
+ let serverUrl;
6757
+ try {
6758
+ serverUrl = getServerUrl();
6759
+ } catch {
6760
+ return true;
6761
+ }
6762
+ if (!serverUrl) return true;
6763
+ const negotiated = readNegotiatesEndpoint(negotiatesEndpoint);
6764
+ try {
6765
+ const a = new URL(url);
6766
+ const b = new URL(serverUrl);
6767
+ if (a.origin !== b.origin) return false;
6768
+ return negotiated || a.pathname === b.pathname;
6769
+ } catch {
6770
+ return true;
6771
+ }
6772
+ };
6773
+ }
6774
+ var REQUEST_DEADLINES = /* @__PURE__ */ new WeakMap();
6775
+ function requestHeadersOf(input, init) {
6776
+ if (init?.headers) return new Headers(init.headers);
6777
+ if (typeof input !== "string" && !(input instanceof URL)) {
6778
+ return new Headers(input.headers);
6779
+ }
6780
+ return new Headers();
6781
+ }
6782
+ async function drainToEnd(reader) {
6783
+ if (!reader) return;
6784
+ for (; ; ) {
6785
+ const { done } = await reader.read();
6786
+ if (done) return;
6787
+ }
6788
+ }
6789
+ function cancelBoth(reader, response) {
6790
+ void reader?.cancel().catch(() => {
6791
+ });
6792
+ void response?.body?.cancel().catch(() => {
6793
+ });
6794
+ }
6795
+ function requestUrlOf2(input) {
6796
+ if (typeof input === "string") return input;
6797
+ if (input instanceof URL) return input.href;
6798
+ return input.url;
6799
+ }
6800
+ function callerSignalOf(input, init) {
6801
+ const explicit = init?.signal;
6802
+ if (explicit !== void 0) return explicit ?? void 0;
6803
+ if (typeof input !== "string" && !(input instanceof URL)) return input.signal;
6804
+ return void 0;
6805
+ }
6806
+ function withOAuthRequestTimeout(fetchFn, timeoutMs = DEFAULT_OAUTH_REQUEST_TIMEOUT_MS, isExempt) {
6807
+ const budget = normalizeBudget(timeoutMs);
6808
+ return async (input, init) => {
6809
+ const url = requestUrlOf2(input);
6810
+ if (isExempt?.(url, requestHeadersOf(input, init))) {
6811
+ return fetchFn(input, init);
6812
+ }
6813
+ const controller = new AbortController();
6814
+ const callerSignal = callerSignalOf(input, init);
6815
+ if (callerSignal?.aborted) throw callerSignal.reason;
6816
+ const signal = callerSignal ? AbortSignal.any([controller.signal, callerSignal]) : controller.signal;
6817
+ let timer;
6818
+ let timedOut = false;
6819
+ let onCallerAbort;
6820
+ const abandoned = new Promise((_resolve, reject) => {
6821
+ timer = setTimeout(() => {
6822
+ timedOut = true;
6823
+ controller.abort(new OAuthRequestTimeoutError(url, budget));
6824
+ reject(new OAuthRequestTimeoutError(url, budget));
6825
+ }, budget);
6826
+ if (callerSignal) {
6827
+ onCallerAbort = () => reject(callerSignal.reason);
6828
+ callerSignal.addEventListener("abort", onCallerAbort, { once: true });
6829
+ }
6830
+ });
6831
+ const nextInit = { ...init, signal };
6832
+ REQUEST_DEADLINES.set(nextInit, budget);
6833
+ let settled;
6834
+ let drainReader;
6835
+ try {
6836
+ const response = await Promise.race([
6837
+ fetchFn(input, nextInit),
6838
+ abandoned
6839
+ ]);
6840
+ settled = response;
6841
+ const copy = response.clone();
6842
+ drainReader = copy.body?.getReader();
6843
+ await Promise.race([drainToEnd(drainReader), abandoned]);
6844
+ return response;
6845
+ } catch (err) {
6846
+ cancelBoth(drainReader, settled);
6847
+ if (timedOut) throw new OAuthRequestTimeoutError(url, budget);
6848
+ throw err;
6849
+ } finally {
6850
+ if (timer !== void 0) clearTimeout(timer);
6851
+ if (onCallerAbort && callerSignal) {
6852
+ callerSignal.removeEventListener("abort", onCallerAbort);
6853
+ }
6854
+ }
6855
+ };
6856
+ }
6857
+
6858
+ // ../../core/auth/oidcDiscoveryCompat.ts
6859
+ import {
6860
+ OAuthMetadataSchema,
6861
+ OpenIdProviderDiscoveryMetadataSchema
6862
+ } from "@modelcontextprotocol/core";
6863
+ var RFC8414_WELL_KNOWN = "/.well-known/oauth-authorization-server";
6864
+ var OIDC_WELL_KNOWN = "/.well-known/openid-configuration";
6865
+ var COMPAT_SOURCE_HEADER = "x-inspector-oauth-metadata-source";
6866
+ function oidcDiscoveryCandidates(rfc8414Url) {
6867
+ let url;
6868
+ try {
6869
+ url = new URL(rfc8414Url);
6870
+ } catch {
6871
+ return [];
6872
+ }
6873
+ if (!url.pathname.startsWith(RFC8414_WELL_KNOWN)) return [];
6874
+ const path5 = url.pathname.slice(RFC8414_WELL_KNOWN.length);
6875
+ if (path5 !== "" && !path5.startsWith("/")) return [];
6876
+ if (path5 === "") return [new URL(OIDC_WELL_KNOWN, url.origin).href];
6877
+ return [
6878
+ new URL(`${OIDC_WELL_KNOWN}${path5}`, url.origin).href,
6879
+ new URL(`${path5}${OIDC_WELL_KNOWN}`, url.origin).href
6880
+ ];
6881
+ }
6882
+ function continuesDiscovery(status) {
6883
+ return status >= 400 && status < 500 || status === 502;
6884
+ }
6885
+ async function releaseBody(response) {
6886
+ await response.body?.cancel().catch(() => {
6887
+ });
6888
+ }
6889
+ function releaseBodyDetached(response) {
6890
+ void releaseBody(response);
6891
+ }
6892
+ function isRfc8414OnlyMetadata(value) {
5880
6893
  if (!OAuthMetadataSchema.safeParse(value).success) return false;
5881
6894
  return !OpenIdProviderDiscoveryMetadataSchema.safeParse(value).success;
5882
6895
  }
5883
- function requestUrlOf2(input) {
6896
+ function requestUrlOf3(input) {
5884
6897
  if (typeof input === "string") return input;
5885
6898
  if (input instanceof URL) return input.href;
5886
6899
  return input.url;
@@ -5905,18 +6918,35 @@ function withRfc8414OidcCompat(fetchFn) {
5905
6918
  if (response.ok) return response;
5906
6919
  if (!continuesDiscovery(response.status)) return response;
5907
6920
  if (requestMethodOf(input, init) !== "GET") return response;
5908
- const candidates = oidcDiscoveryCandidates(requestUrlOf2(input));
6921
+ const candidates = oidcDiscoveryCandidates(requestUrlOf3(input));
5909
6922
  if (candidates.length === 0) return response;
5910
6923
  const headers = discoveryHeaders(input, init);
6924
+ const callerSignal = callerSignalOf(input, init);
5911
6925
  for (const candidate of candidates) {
6926
+ if (callerSignal?.aborted) {
6927
+ releaseBodyDetached(response);
6928
+ throw callerSignal.reason;
6929
+ }
5912
6930
  let probe;
5913
6931
  try {
5914
- probe = await fetchFn(candidate, { headers });
5915
- } catch {
6932
+ probe = await fetchFn(candidate, { headers, signal: callerSignal });
6933
+ } catch (err) {
6934
+ if (err instanceof OAuthRequestTimeoutError) {
6935
+ releaseBodyDetached(response);
6936
+ throw err;
6937
+ }
6938
+ if (callerSignal?.aborted) {
6939
+ releaseBodyDetached(response);
6940
+ throw callerSignal.reason;
6941
+ }
5916
6942
  return response;
5917
6943
  }
5918
6944
  if (!probe.ok) {
5919
6945
  await releaseBody(probe);
6946
+ if (callerSignal?.aborted) {
6947
+ releaseBodyDetached(response);
6948
+ throw callerSignal.reason;
6949
+ }
5920
6950
  if (continuesDiscovery(probe.status)) continue;
5921
6951
  return response;
5922
6952
  }
@@ -5926,6 +6956,10 @@ function withRfc8414OidcCompat(fetchFn) {
5926
6956
  body = await probe.text();
5927
6957
  parsed = JSON.parse(body);
5928
6958
  } catch {
6959
+ if (callerSignal?.aborted) {
6960
+ releaseBodyDetached(response);
6961
+ throw callerSignal.reason;
6962
+ }
5929
6963
  return response;
5930
6964
  }
5931
6965
  if (!isRfc8414OnlyMetadata(parsed)) {
@@ -5973,6 +7007,82 @@ function createSilentLogger() {
5973
7007
  }
5974
7008
  var silentLogger = createSilentLogger();
5975
7009
 
7010
+ // ../../core/mcp/inspectorClient.ts
7011
+ import { SdkError as SdkError5, SdkErrorCode as SdkErrorCode5 } from "@modelcontextprotocol/client";
7012
+
7013
+ // ../../core/mcp/connectionDiagnostics.ts
7014
+ import { SdkError as SdkError3, SdkErrorCode as SdkErrorCode3 } from "@modelcontextprotocol/client";
7015
+ function isRequestTimeoutData(value) {
7016
+ return typeof value === "object" && value !== null && typeof value.timeout === "number";
7017
+ }
7018
+ function isRequestTimeoutError(err) {
7019
+ return SdkError3.isInstance(err) && err.code === SdkErrorCode3.RequestTimeout && isRequestTimeoutData(err.data);
7020
+ }
7021
+ function formatDuration(ms) {
7022
+ const clamped = Math.max(0, ms);
7023
+ if (clamped < 1e3) return `${Math.round(clamped)}ms`;
7024
+ const totalSeconds = Math.round(clamped / 1e3);
7025
+ const hours = Math.floor(totalSeconds / 3600);
7026
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
7027
+ const seconds = totalSeconds % 60;
7028
+ if (hours > 0) return `${hours}h${String(minutes).padStart(2, "0")}m`;
7029
+ if (minutes > 0) return `${minutes}m${String(seconds).padStart(2, "0")}s`;
7030
+ return `${seconds}s`;
7031
+ }
7032
+ var MAX_LISTED_OUTSTANDING_REQUESTS = 5;
7033
+ function describeStreamEndpoint(url) {
7034
+ try {
7035
+ return `GET ${new URL(url).pathname}`;
7036
+ } catch {
7037
+ return `GET ${url}`;
7038
+ }
7039
+ }
7040
+ function plural(count, noun) {
7041
+ return `${count} ${noun}${count === 1 ? "" : "s"}`;
7042
+ }
7043
+ function describeOutstanding(requests, now) {
7044
+ if (requests.length === 0) return "No requests are unanswered.";
7045
+ const listed = requests.slice(0, MAX_LISTED_OUTSTANDING_REQUESTS).map((r) => `${r.method} (sent ${formatDuration(now - r.sentAt)} ago)`);
7046
+ const rest = requests.length - listed.length;
7047
+ const tail = rest > 0 ? `, and ${rest} more` : "";
7048
+ const verb = requests.length === 1 ? "is" : "are";
7049
+ return `${plural(requests.length, "request")} ${verb} unanswered: ${listed.join(", ")}${tail}.`;
7050
+ }
7051
+ function describeLastResponse(lastResponse, now) {
7052
+ if (!lastResponse) return "No response has been received this session.";
7053
+ return `Last response received ${formatDuration(now - lastResponse.receivedAt)} ago (${lastResponse.method}).`;
7054
+ }
7055
+ function describeStream(stream, now) {
7056
+ if (!stream) return void 0;
7057
+ const endpoint = describeStreamEndpoint(stream.url);
7058
+ const events = plural(stream.eventCount, "event");
7059
+ if (stream.closedAt !== void 0) {
7060
+ return `Notification stream (${endpoint}) closed ${formatDuration(now - stream.closedAt)} ago after ${formatDuration(stream.closedAt - stream.openedAt)}, ${events} delivered.`;
7061
+ }
7062
+ return `Notification stream (${endpoint}) open for ${formatDuration(now - stream.openedAt)}, ${events} delivered.`;
7063
+ }
7064
+ function describeConnectionDiagnostics(diagnostics) {
7065
+ const now = diagnostics.capturedAt;
7066
+ const parts = [
7067
+ describeOutstanding(diagnostics.outstandingRequests, now),
7068
+ describeLastResponse(diagnostics.lastResponse, now),
7069
+ describeStream(diagnostics.notificationStream, now)
7070
+ ];
7071
+ return parts.filter((p) => p !== void 0).join(" ");
7072
+ }
7073
+ function annotateRequestTimeout(err, method, diagnostics) {
7074
+ if (!isRequestTimeoutError(err)) return err;
7075
+ const data = {
7076
+ ...err.data,
7077
+ method,
7078
+ diagnostics
7079
+ };
7080
+ const message = `Request timed out after ${formatDuration(err.data.timeout)} (${method}). ${describeConnectionDiagnostics(diagnostics)}`;
7081
+ const annotated = new SdkError3(SdkErrorCode3.RequestTimeout, message, data);
7082
+ annotated.cause = err;
7083
+ return annotated;
7084
+ }
7085
+
5976
7086
  // ../../core/auth/authorizationParams.ts
5977
7087
  var RESERVED_AUTHORIZATION_PARAMS = [
5978
7088
  "client_id",
@@ -6186,8 +7296,8 @@ var BaseOAuthClientProvider = class {
6186
7296
  );
6187
7297
  }
6188
7298
  async saveClientInformation(clientInformation, options) {
6189
- const registrationKind = options && "registrationKind" in options ? options.registrationKind : "dcr";
6190
7299
  const issuer = options && "issuer" in options ? options.issuer : void 0;
7300
+ const registrationKind = options && "registrationKind" in options ? options.registrationKind : await this.resolveSdkRegistrationKind(clientInformation, issuer);
6191
7301
  await this.storage.saveClientInformation(
6192
7302
  this.serverUrl,
6193
7303
  clientInformation,
@@ -6197,6 +7307,70 @@ var BaseOAuthClientProvider = class {
6197
7307
  }
6198
7308
  );
6199
7309
  }
7310
+ /**
7311
+ * Resolve the registration kind for a save that carries no explicit one — that
7312
+ * is, one the SDK made. SDK v2's `saveClientInformation` contract passes only
7313
+ * `{ issuer }`, so the mechanism cannot be handed to us; treating every such
7314
+ * save as DCR is what relabeled a CIMD registration `Dynamic (DCR)` in
7315
+ * Connection Info the moment the SDK bound it to an issuer (#2242).
7316
+ *
7317
+ * Two cases reach here, and they are told apart by whether a registration
7318
+ * already exists for this issuer:
7319
+ *
7320
+ * - **A back-stamp.** A registration is already stored for this issuer under
7321
+ * this `client_id`, and the SDK is only adding the `issuer` to it. Its
7322
+ * recorded kind is the answer — kind and credential are written and cleared
7323
+ * together, so a stored registration always has one.
7324
+ * - **A new registration.** Nothing is stored for this issuer, so this save
7325
+ * creates it. SDK v2 `auth()` reaches its URL-based-client-ID branch — rather
7326
+ * than `registerClient` — exactly when the AS advertises
7327
+ * `client_id_metadata_document_supported` and a `clientMetadataUrl` is
7328
+ * configured, and it persists the AS metadata via `saveDiscoveryState`
7329
+ * *before* it reads or writes client information. So the branch it took is
7330
+ * not inferred here, it is read back from the state it just wrote.
7331
+ *
7332
+ * This is why the check is not "the `client_id` looks like our metadata URL".
7333
+ * RFC 7591 §3.2 makes a dynamically issued `client_id` opaque, so an AS may
7334
+ * mint that very URL from `POST /register`; the URL comparison only decides
7335
+ * whether CIMD is *in play* for this connection, and the two cases above decide
7336
+ * what actually happened (#2242, Copilot).
7337
+ *
7338
+ * Consequences worth stating, since each was a defect on the way here:
7339
+ *
7340
+ * - An existing DCR whose `client_id` happens to be the metadata URL stays
7341
+ * `dcr` — it takes the back-stamp path and its recorded kind says so.
7342
+ * - `invalidateCredentials("client")`, which SDK `auth()` calls on
7343
+ * `invalid_client` before retrying, clears the registration and its kind. The
7344
+ * retry therefore takes the new-registration path and is answered from
7345
+ * discovery state, which that clear does not touch.
7346
+ * - A second AS behind one resource gets its own answer, since discovery state
7347
+ * describes the issuer the SDK actually resolved. One that does not advertise
7348
+ * CIMD is `dcr` even when it mints the metadata URL as its `client_id`.
7349
+ * - A transient failure in our own CIMD preflight costs nothing: the SDK's own
7350
+ * discovery is what this reads.
7351
+ */
7352
+ async resolveSdkRegistrationKind(clientInformation, issuer) {
7353
+ const clientMetadataUrl = this.clientMetadataUrl?.trim();
7354
+ if (!clientMetadataUrl || clientInformation.client_id !== clientMetadataUrl) {
7355
+ return "dcr";
7356
+ }
7357
+ const stored = await this.storage.getClientInformation(
7358
+ this.serverUrl,
7359
+ false,
7360
+ issuer
7361
+ );
7362
+ if (stored?.client_id === clientMetadataUrl) {
7363
+ const storedKind = await this.storage.getClientRegistrationKind(
7364
+ this.serverUrl,
7365
+ issuer
7366
+ );
7367
+ return storedKind === "cimd" ? "cimd" : "dcr";
7368
+ }
7369
+ const discovery = await this.storage.getDiscoveryState(this.serverUrl);
7370
+ const metadata = discovery?.authorizationServerMetadata;
7371
+ if (issuer !== void 0 && metadata?.issuer !== issuer) return "dcr";
7372
+ return metadata?.client_id_metadata_document_supported === true ? "cimd" : "dcr";
7373
+ }
6200
7374
  async saveScope(scope) {
6201
7375
  await this.storage.saveScope(this.serverUrl, scope);
6202
7376
  this.cachedScope = scope;
@@ -7075,35 +8249,49 @@ import { discoverOAuthProtectedResourceMetadata as discoverOAuthProtectedResourc
7075
8249
  async function ensureCimdClientRegistration(params) {
7076
8250
  const clientMetadataUrl = params.provider.clientMetadataUrl?.trim();
7077
8251
  if (!clientMetadataUrl) return;
7078
- const existing = await params.provider.clientInformation();
7079
- if (existing?.client_id) return;
7080
- let resourceMetadata;
7081
- try {
7082
- resourceMetadata = await discoverOAuthProtectedResourceMetadata2(
7083
- params.serverUrl,
7084
- { resourceMetadataUrl: params.resourceMetadataUrl },
7085
- // The same fetch the AS-metadata leg below uses. On web that is
7086
- // `createRemoteFetch`, which proxies through the backend to sidestep
7087
- // CORS — on the global `fetch` this leg would fail in the browser, be
7088
- // swallowed by the catch, and leave CIMD probing the wrong
7089
- // authorization server (Copilot).
7090
- params.fetchFn
7091
- );
7092
- } catch {
7093
- resourceMetadata = void 0;
8252
+ let metadata = (await params.provider.discoveryState())?.authorizationServerMetadata;
8253
+ if (!metadata) {
8254
+ let resourceMetadata;
8255
+ try {
8256
+ resourceMetadata = await discoverOAuthProtectedResourceMetadata2(
8257
+ params.serverUrl,
8258
+ { resourceMetadataUrl: params.resourceMetadataUrl },
8259
+ // The same fetch the AS-metadata leg below uses. On web that is
8260
+ // `createRemoteFetch`, which proxies through the backend to sidestep
8261
+ // CORS — on the global `fetch` this leg would fail in the browser, be
8262
+ // swallowed by the catch, and leave CIMD probing the wrong
8263
+ // authorization server (Copilot).
8264
+ params.fetchFn
8265
+ );
8266
+ } catch {
8267
+ resourceMetadata = void 0;
8268
+ }
8269
+ try {
8270
+ metadata = await discoverAuthorizationServerMetadataForServer(
8271
+ params.serverUrl,
8272
+ resourceMetadata,
8273
+ params.fetchFn
8274
+ );
8275
+ } catch {
8276
+ return;
8277
+ }
7094
8278
  }
7095
- const metadata = await discoverAuthorizationServerMetadataForServer(
7096
- params.serverUrl,
7097
- resourceMetadata,
7098
- params.fetchFn
7099
- );
8279
+ const issuer = metadata?.issuer;
7100
8280
  if (!metadata?.client_id_metadata_document_supported) return;
7101
- const clientInformation = {
7102
- client_id: clientMetadataUrl
7103
- };
7104
- await params.provider.saveClientInformation(clientInformation, {
7105
- registrationKind: "cimd"
7106
- });
8281
+ const existing = await params.provider.clientInformation(
8282
+ issuer ? { issuer } : void 0
8283
+ );
8284
+ if (existing?.client_id) return;
8285
+ await params.provider.saveClientInformation(
8286
+ { client_id: clientMetadataUrl },
8287
+ {
8288
+ registrationKind: "cimd",
8289
+ // An AS metadata document without an `issuer` is malformed (RFC 8414 §2),
8290
+ // but the type allows it; fall back to the unkeyed slot rather than
8291
+ // inventing a key.
8292
+ ...issuer && { issuer }
8293
+ }
8294
+ );
7107
8295
  }
7108
8296
 
7109
8297
  // ../../core/auth/ema/transportProvider.ts
@@ -7130,11 +8318,26 @@ var EmaTransportOAuthProvider = class {
7130
8318
  state() {
7131
8319
  return this.inner.state();
7132
8320
  }
7133
- clientInformation() {
7134
- return this.inner.clientInformation();
8321
+ // SEP-2352: `ctx` carries the authorization-server `issuer` the SDK resolved,
8322
+ // and the inner provider keys registrations by it. Dropping it here made every
8323
+ // EMA read and write land on the unkeyed slot — and, since #2242, left
8324
+ // `resolveSdkRegistrationKind` with no issuer to check, so a CIMD registration
8325
+ // made over an EMA connection was recorded as DCR (Copilot).
8326
+ clientInformation(ctx) {
8327
+ return this.inner.clientInformation(ctx);
8328
+ }
8329
+ saveClientInformation(clientInformation, ctx) {
8330
+ return this.inner.saveClientInformation(clientInformation, ctx);
7135
8331
  }
7136
- saveClientInformation(clientInformation) {
7137
- return this.inner.saveClientInformation(clientInformation);
8332
+ // Without these the SDK persists no discovery state for an EMA connection, so
8333
+ // it re-discovers on every call, cannot perform its SEP-2352 callback-leg
8334
+ // authorization-server binding check (it warns as much), and — since #2242 —
8335
+ // leaves the registration-kind resolver nothing to read back.
8336
+ saveDiscoveryState(state) {
8337
+ return this.inner.saveDiscoveryState(state);
8338
+ }
8339
+ discoveryState() {
8340
+ return this.inner.discoveryState();
7138
8341
  }
7139
8342
  async tokens() {
7140
8343
  const stored = await this.inner.tokens();
@@ -7203,8 +8406,11 @@ function issuerMismatchTitle() {
7203
8406
  return "Authorization server mismatch";
7204
8407
  }
7205
8408
  var MAX_DISPLAYED_ISSUER_LENGTH = 120;
8409
+ function truncateUrlForDisplay(url) {
8410
+ return url.length > MAX_DISPLAYED_ISSUER_LENGTH ? `${url.slice(0, MAX_DISPLAYED_ISSUER_LENGTH)}\u2026` : url;
8411
+ }
7206
8412
  function truncateIssuerForDisplay(issuer) {
7207
- return issuer.length > MAX_DISPLAYED_ISSUER_LENGTH ? `${issuer.slice(0, MAX_DISPLAYED_ISSUER_LENGTH)}\u2026` : issuer;
8413
+ return truncateUrlForDisplay(issuer);
7208
8414
  }
7209
8415
  function issuerMismatchMessage(options) {
7210
8416
  const target = options.serverName ? `"${options.serverName}"` : "this server";
@@ -7994,6 +9200,9 @@ var OAuthManager = class {
7994
9200
  }
7995
9201
  };
7996
9202
 
9203
+ // ../../core/mcp/remote/remoteClientTransport.ts
9204
+ import { SdkError as SdkError4, SdkErrorCode as SdkErrorCode4 } from "@modelcontextprotocol/client";
9205
+
7997
9206
  // ../../core/mcp/remote/types.ts
7998
9207
  function oauthTokensToRemoteAuthState(tokens) {
7999
9208
  return {
@@ -8008,6 +9217,20 @@ function oauthTokensToRemoteAuthState(tokens) {
8008
9217
  };
8009
9218
  }
8010
9219
 
9220
+ // ../../core/mcp/remote/progressToken.ts
9221
+ import { ProgressTokenSchema } from "@modelcontextprotocol/core";
9222
+ function progressTokenOf(message) {
9223
+ if ("method" in message && message.method === "notifications/progress" && "params" in message && typeof message.params === "object" && message.params !== null) {
9224
+ const parsed = ProgressTokenSchema.safeParse(
9225
+ message.params.progressToken
9226
+ );
9227
+ if (parsed.success) {
9228
+ return parsed.data;
9229
+ }
9230
+ }
9231
+ return void 0;
9232
+ }
9233
+
8011
9234
  // ../../core/mcp/remote/remoteClientTransport.ts
8012
9235
  var DEFAULT_SSE_RESPONSE_TIMEOUT_MS = 6e4;
8013
9236
  function requestIdForMessage(message) {
@@ -8079,6 +9302,14 @@ var RemoteClientTransport = class {
8079
9302
  eventStreamConsumeTask = null;
8080
9303
  restartingEventStream = false;
8081
9304
  closed = false;
9305
+ /**
9306
+ * Long-lived streams the backend has reported open, by fetch entry id, with
9307
+ * the last event count seen for each. `close()` reports them closed
9308
+ * itself: it aborts the event channel before asking the backend to
9309
+ * disconnect, so the backend watcher's own close report lands on an
9310
+ * unconsumed queue and is lost with the session (#2318).
9311
+ */
9312
+ openStreams = /* @__PURE__ */ new Map();
8082
9313
  sseResponseWaits = /* @__PURE__ */ new Map();
8083
9314
  options;
8084
9315
  config;
@@ -8301,10 +9532,20 @@ var RemoteClientTransport = class {
8301
9532
  if (parsed.type === "message") {
8302
9533
  const msg = parsed.data;
8303
9534
  this.settleSseResponseWait(msg);
9535
+ const progressToken = progressTokenOf(msg);
9536
+ if (progressToken !== void 0) {
9537
+ this.resetSseResponseWait(progressToken);
9538
+ }
8304
9539
  this.onmessage?.(msg, void 0);
8305
- } else if (parsed.type === "fetch_request" && this.options.onFetchRequest) {
9540
+ } else if (parsed.type === "fetch_request") {
8306
9541
  const entry = parsed.data;
8307
- this.options.onFetchRequest({
9542
+ if (this.options.onFetchStreamUpdate && isLongLivedStreamResponse(
9543
+ entry.method,
9544
+ findHeader(entry.responseHeaders, "content-type")
9545
+ )) {
9546
+ this.openStreams.set(entry.id, 0);
9547
+ }
9548
+ this.options.onFetchRequest?.({
8308
9549
  ...entry,
8309
9550
  timestamp: typeof entry.timestamp === "string" ? new Date(entry.timestamp) : entry.timestamp
8310
9551
  });
@@ -8313,6 +9554,16 @@ var RemoteClientTransport = class {
8313
9554
  parsed.data.id,
8314
9555
  parsed.data.responseBody
8315
9556
  );
9557
+ } else if (parsed.type === "fetch_stream_update" && this.options.onFetchStreamUpdate) {
9558
+ const { id, eventCount, closedAt } = parsed.data;
9559
+ if (closedAt !== void 0) this.openStreams.delete(id);
9560
+ else if (this.openStreams.has(id)) {
9561
+ this.openStreams.set(id, eventCount);
9562
+ }
9563
+ this.options.onFetchStreamUpdate(id, {
9564
+ eventCount,
9565
+ ...closedAt !== void 0 && { closedAt: new Date(closedAt) }
9566
+ });
8316
9567
  } else if (parsed.type === "stdio_log" && this.options.onStderr) {
8317
9568
  this.options.onStderr({
8318
9569
  timestamp: new Date(parsed.data.timestamp),
@@ -8423,14 +9674,18 @@ var RemoteClientTransport = class {
8423
9674
  }
8424
9675
  waitForSseResponse(requestId) {
8425
9676
  return new Promise((resolve7, reject) => {
8426
- const timer = setTimeout(() => {
8427
- this.sseResponseWaits.delete(requestId);
8428
- reject(
8429
- new Error(
8430
- `Timed out waiting for MCP response on SSE (${this.sseResponseTimeoutMs}ms)`
8431
- )
8432
- );
8433
- }, this.sseResponseTimeoutMs);
9677
+ let timer;
9678
+ const arm = () => {
9679
+ timer = setTimeout(() => {
9680
+ this.sseResponseWaits.delete(requestId);
9681
+ reject(
9682
+ new SdkError4(SdkErrorCode4.RequestTimeout, "Request timed out", {
9683
+ timeout: this.sseResponseTimeoutMs
9684
+ })
9685
+ );
9686
+ }, this.sseResponseTimeoutMs);
9687
+ };
9688
+ arm();
8434
9689
  this.sseResponseWaits.set(requestId, {
8435
9690
  resolve: () => {
8436
9691
  clearTimeout(timer);
@@ -8439,10 +9694,21 @@ var RemoteClientTransport = class {
8439
9694
  reject: (error) => {
8440
9695
  clearTimeout(timer);
8441
9696
  reject(error);
9697
+ },
9698
+ // A progress notification for this request re-arms the full window so
9699
+ // the browser-side deadline tracks the SDK client's progress-aware one
9700
+ // rather than firing at a flat 60s (#2028).
9701
+ resetTimeout: () => {
9702
+ clearTimeout(timer);
9703
+ arm();
8442
9704
  }
8443
9705
  });
8444
9706
  });
8445
9707
  }
9708
+ /** Re-arm a pending request's SSE wait timeout on a matching progress note. */
9709
+ resetSseResponseWait(requestId) {
9710
+ this.sseResponseWaits.get(requestId)?.resetTimeout();
9711
+ }
8446
9712
  async postSend(message, options, retryCount) {
8447
9713
  const requestId = requestIdForMessage(message);
8448
9714
  let sseWait;
@@ -8525,6 +9791,11 @@ var RemoteClientTransport = class {
8525
9791
  this.closed = true;
8526
9792
  this.cancelAllSseWaits(new Error("Transport closed"));
8527
9793
  await this.stopEventStream();
9794
+ const closedAt = /* @__PURE__ */ new Date();
9795
+ for (const [id, eventCount] of this.openStreams) {
9796
+ this.options.onFetchStreamUpdate?.(id, { eventCount, closedAt });
9797
+ }
9798
+ this.openStreams.clear();
8528
9799
  if (this._sessionId) {
8529
9800
  try {
8530
9801
  await this.fetchFn(`${this.baseUrl}/api/mcp/disconnect`, {
@@ -8549,6 +9820,9 @@ var corePackageJson = {
8549
9820
  version: "0.0.0"
8550
9821
  };
8551
9822
  var MAX_URL_ELICITATION_RETRIES = 5;
9823
+ function connectionTimeoutMessage(timeoutMs) {
9824
+ return `Connection timed out after ${timeoutMs} ms. To accommodate a slower server, you may increase the timeout value in Server Settings.`;
9825
+ }
8552
9826
  function createPendingAbortError() {
8553
9827
  return new Error("Pending request aborted");
8554
9828
  }
@@ -8560,20 +9834,13 @@ async function closeSubscriptionBestEffort(subscription) {
8560
9834
  } catch {
8561
9835
  }
8562
9836
  }
8563
- function notificationMethodFromSchema(schema) {
8564
- if (schema !== null && typeof schema === "object") {
8565
- const literal = schema.shape?.method?.value;
8566
- if (typeof literal === "string") return literal;
8567
- }
8568
- return void 0;
8569
- }
8570
9837
  function describeJsonType2(value) {
8571
9838
  if (value === null) return "null";
8572
9839
  if (Array.isArray(value)) return "array";
8573
9840
  return typeof value;
8574
9841
  }
8575
9842
  function isProgressToken(value) {
8576
- return ProgressTokenSchema.safeParse(value).success;
9843
+ return ProgressTokenSchema2.safeParse(value).success;
8577
9844
  }
8578
9845
  function dropInvalidReservedMeta(meta, logger) {
8579
9846
  const result = RequestMetaSchema.safeParse(meta);
@@ -8603,19 +9870,28 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8603
9870
  */
8604
9871
  static MRTR_MAX_ROUNDS = 10;
8605
9872
  client = null;
8606
- appRendererClientProxy = null;
8607
9873
  // Lazily-built validator used only on the skipOutputValidation path to detect
8608
9874
  // (non-fatally) when a delivered result violates the tool's outputSchema.
8609
9875
  outputValidator = null;
8610
9876
  transport = null;
8611
9877
  baseTransport = null;
8612
- // Correlation for `markResponseRejected` (#1953): the method of each
8613
- // outbound request still awaiting a response, and — once one is answered —
8614
- // the id of the most recently answered request per method. Entries are
8615
- // dropped as responses arrive, so this holds at most one id per method
8616
- // rather than growing with the session.
8617
- outboundRequestMethods = /* @__PURE__ */ new Map();
9878
+ // Every outbound request still awaiting a response, keyed by JSON-RPC id.
9879
+ // Serves two readers: the `markResponseRejected` correlation (#1953), which
9880
+ // needs the method, and the connection diagnostics (#2318), which need the
9881
+ // method and when it went out. Entries are dropped as responses arrive; a
9882
+ // request the SDK gave up on (a timeout, a cancel) stays until the session
9883
+ // resets, because it is still unanswered and that is what the diagnostics
9884
+ // report.
9885
+ outstandingRequests = /* @__PURE__ */ new Map();
9886
+ // The id of the most recently answered request per method, for
9887
+ // `markResponseRejected` (#1953). At most one id per method, so it does not
9888
+ // grow with the session.
8618
9889
  lastAnsweredRequestByMethod = /* @__PURE__ */ new Map();
9890
+ // Connection diagnostics (#2318): the most recent response to one of our
9891
+ // requests, and the most recent long-lived transport stream (the standalone
9892
+ // `GET` on Streamable HTTP). Both per-session, reset with the maps above.
9893
+ lastResponse;
9894
+ notificationStream;
8619
9895
  /** True when the cached transport was built with an OAuth authProvider attached. */
8620
9896
  transportHasAuthProvider = false;
8621
9897
  /** Dedupes concurrent ambient auth challenges (reason + scopes). */
@@ -8883,12 +10159,27 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8883
10159
  resources: options.listChangedNotifications?.resources ?? true,
8884
10160
  prompts: options.listChangedNotifications?.prompts ?? true
8885
10161
  };
8886
- this.fetchFn = withOAuthEndpointOverrides(
10162
+ const withOverrides = withOAuthEndpointOverrides(
8887
10163
  this.fetchFn ?? fetch,
8888
10164
  () => this.oauthManager?.getEndpointOverrides()
8889
10165
  );
8890
- this.fetchFn = withRfc8414OidcCompat(this.fetchFn);
8891
- this.effectiveAuthFetch = this.buildEffectiveAuthFetch();
10166
+ this.fetchFn = withRfc8414OidcCompat(
10167
+ withOAuthRequestTimeout(
10168
+ withOverrides,
10169
+ DEFAULT_OAUTH_REQUEST_TIMEOUT_MS,
10170
+ exemptMcpEndpoint(
10171
+ () => this.getServerUrl(),
10172
+ // Legacy SSE negotiates its message endpoint inside the stream, so
10173
+ // its path is unknowable here and the exemption has to widen to the
10174
+ // origin. Streamable HTTP sends everything to the configured URL, so
10175
+ // the path rule is exact there and same-origin OAuth stays bounded.
10176
+ () => this.transportConfig?.type === "sse"
10177
+ )
10178
+ )
10179
+ );
10180
+ this.effectiveAuthFetch = this.buildEffectiveAuthFetch(
10181
+ withRfc8414OidcCompat(withOAuthRequestTimeout(withOverrides))
10182
+ );
8892
10183
  this.sessionId = options.sessionId;
8893
10184
  if (options.oauth || options.environment.oauth) {
8894
10185
  const oauthConfig = {
@@ -8996,7 +10287,6 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
8996
10287
  this.tasksCapabilityAdvertised = capabilities.tasks !== void 0;
8997
10288
  this.urlElicitationCapabilityAdvertised = capabilities.elicitation?.url !== void 0;
8998
10289
  this.rootsListChangedCapabilityAdvertised = capabilities.roots?.listChanged === true;
8999
- this.appRendererClientProxy = null;
9000
10290
  this.clientInfo = options.clientIdentity ?? {
9001
10291
  name: corePackageJson.name.split("/")[1] ?? corePackageJson.name,
9002
10292
  version: corePackageJson.version
@@ -9005,9 +10295,60 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9005
10295
  this.clientInfo,
9006
10296
  Object.keys(clientOptions).length > 0 ? clientOptions : void 0
9007
10297
  );
10298
+ this.annotateSdkRequestTimeouts(this.client);
9008
10299
  }
9009
- buildEffectiveAuthFetch() {
9010
- const base = this.fetchFn ?? fetch;
10300
+ /**
10301
+ * Decorate the SDK Client's `request` so a per-request timeout rejects with
10302
+ * the connection's state at that moment instead of a bare `Request timed
10303
+ * out` (#2318).
10304
+ *
10305
+ * `Protocol.request` is the one funnel every SDK verb goes through —
10306
+ * `listTools`, `callTool`, `readResource`, the handshake's `initialize`,
10307
+ * and this class's own raw `client.request` calls all reach the wire
10308
+ * through it — so decorating it here covers the whole surface without a
10309
+ * `catch` at each of the several dozen call sites, and covers the next
10310
+ * verb added without anyone remembering to. The SDK offers no hook on the
10311
+ * rejection path: the timeout error is constructed and thrown inside
10312
+ * `request()` itself.
10313
+ *
10314
+ * Replaced on the instance rather than through a subclass because
10315
+ * `request` is declared with two overloads (spec method with a keyed
10316
+ * result, custom method with an explicit schema), and an `override` would
10317
+ * have to restate both against types that move with every SDK release. One
10318
+ * implementation signature serves both at runtime; the single cast on the
10319
+ * way back is what the overloads cost.
10320
+ */
10321
+ annotateSdkRequestTimeouts(client) {
10322
+ const sdkRequest = client.request.bind(client);
10323
+ const decorated = async (...args) => {
10324
+ try {
10325
+ return await sdkRequest(...args);
10326
+ } catch (err) {
10327
+ throw annotateRequestTimeout(
10328
+ err,
10329
+ args[0].method,
10330
+ this.getConnectionDiagnostics()
10331
+ );
10332
+ }
10333
+ };
10334
+ client.request = decorated;
10335
+ }
10336
+ /**
10337
+ * @param base - the composed OAuth-path fetch, deadline innermost (#2319).
10338
+ *
10339
+ * Passed in rather than read off `this.fetchFn` because the two chains are
10340
+ * bounded on different terms, not because one of them is unbounded. Both
10341
+ * carry a deadline; the transport chain additionally exempts the MCP endpoint
10342
+ * (`exemptMcpEndpoint`), since its bodies are streams meant to stay open and
10343
+ * a long-running tool call may withhold its headers for minutes. Reusing it
10344
+ * here would extend that exemption to the OAuth manager's own requests — and
10345
+ * an authorization server published at the MCP endpoint's path would then go
10346
+ * unbounded on the one chain that exists to bound it.
10347
+ *
10348
+ * The SDK's transport-internal discovery (the 401/refresh path) is covered by
10349
+ * the transport chain's own deadline, not by this one.
10350
+ */
10351
+ buildEffectiveAuthFetch(base) {
9011
10352
  return createFetchTracker(base, {
9012
10353
  trackRequest: (entry) => this.dispatchFetchRequest({ ...entry, category: "auth" }),
9013
10354
  updateResponseBody: (id, body) => this.dispatchFetchRequestBodyUpdate(id, body)
@@ -9017,7 +10358,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9017
10358
  return {
9018
10359
  trackRequest: (message, origin) => {
9019
10360
  if (origin === "client") {
9020
- this.outboundRequestMethods.set(message.id, message.method);
10361
+ this.outstandingRequests.set(message.id, {
10362
+ id: message.id,
10363
+ method: message.method,
10364
+ sentAt: Date.now()
10365
+ });
10366
+ this.dispatchConnectionDiagnosticsChange();
9021
10367
  }
9022
10368
  const entry = {
9023
10369
  id: crypto.randomUUID(),
@@ -9028,14 +10374,22 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9028
10374
  };
9029
10375
  this.dispatchTypedEvent("message", entry);
9030
10376
  },
10377
+ trackSendFailure: (message, error) => {
10378
+ if (isRequestTimeoutError(error)) return;
10379
+ if (this.outstandingRequests.delete(message.id)) {
10380
+ this.dispatchConnectionDiagnosticsChange();
10381
+ }
10382
+ },
9031
10383
  trackResponse: (message, origin) => {
9032
10384
  const responseId = message.id;
9033
10385
  if (origin === "server" && responseId !== void 0) {
9034
- const method = this.outboundRequestMethods.get(responseId);
9035
- this.outboundRequestMethods.delete(responseId);
10386
+ const method = this.outstandingRequests.get(responseId)?.method;
10387
+ this.outstandingRequests.delete(responseId);
9036
10388
  if (method !== void 0) {
9037
10389
  this.lastAnsweredRequestByMethod.set(method, responseId);
10390
+ this.lastResponse = { method, receivedAt: Date.now() };
9038
10391
  }
10392
+ this.dispatchConnectionDiagnosticsChange();
9039
10393
  }
9040
10394
  const entry = {
9041
10395
  id: crypto.randomUUID(),
@@ -9642,8 +10996,11 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9642
10996
  this.clearReceiverTasks();
9643
10997
  this.resetSubscriptionStream();
9644
10998
  this.cancelledTaskIds.clear();
9645
- this.outboundRequestMethods.clear();
10999
+ this.outstandingRequests.clear();
9646
11000
  this.lastAnsweredRequestByMethod.clear();
11001
+ this.lastResponse = void 0;
11002
+ this.notificationStream = void 0;
11003
+ this.dispatchConnectionDiagnosticsChange();
9647
11004
  if (this.malformedListItems.size > 0) {
9648
11005
  this.malformedListItems.clear();
9649
11006
  this.dispatchTypedEvent("malformedListItemsChange", []);
@@ -9744,10 +11101,14 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9744
11101
  },
9745
11102
  onFetchRequest: (entry) => {
9746
11103
  this.dispatchFetchRequest({ ...entry, category: "transport" });
11104
+ this.noteTransportStream(entry);
9747
11105
  },
9748
11106
  onFetchResponseBody: (id, body) => {
9749
11107
  this.dispatchFetchRequestBodyUpdate(id, body);
9750
11108
  },
11109
+ onFetchStreamUpdate: (id, stream) => {
11110
+ this.dispatchFetchRequestStreamUpdate(id, stream);
11111
+ },
9751
11112
  ...this.serverSettings && { settings: this.serverSettings }
9752
11113
  };
9753
11114
  if (this.isHttpOAuthConfig() && oauthManager) {
@@ -9834,7 +11195,8 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9834
11195
  this.dispatchTypedEvent("statusChange", this.status);
9835
11196
  this.registerPeerRequestHandlers();
9836
11197
  this.registerPeerNotificationHandlers();
9837
- const connectTimeoutMs = this.serverSettings?.connectionTimeout ?? 0;
11198
+ const connectTimeoutMs = this.serverSettings?.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT_MS;
11199
+ let connectTimedOut = false;
9838
11200
  const connectPromise = this.client.connect(this.transport).catch((err) => {
9839
11201
  throw findNestedAuthError(err) ?? err;
9840
11202
  });
@@ -9854,14 +11216,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9854
11216
  });
9855
11217
  let timer;
9856
11218
  const timeoutPromise = new Promise((_, reject) => {
9857
- timer = setTimeout(
9858
- () => reject(
9859
- new Error(
9860
- `Connection timed out after ${connectTimeoutMs} ms`
9861
- )
9862
- ),
9863
- connectTimeoutMs
9864
- );
11219
+ timer = setTimeout(() => {
11220
+ connectTimedOut = true;
11221
+ reject(new Error(connectionTimeoutMessage(connectTimeoutMs)));
11222
+ }, connectTimeoutMs);
9865
11223
  });
9866
11224
  try {
9867
11225
  await Promise.race([connectPromise, timeoutPromise]);
@@ -9875,7 +11233,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
9875
11233
  try {
9876
11234
  await this.invokeMcpClient(runConnect);
9877
11235
  } catch (err) {
9878
- if (connectTimeoutMs > 0) {
11236
+ if (connectTimedOut) {
9879
11237
  await this.disconnect().catch(() => {
9880
11238
  });
9881
11239
  }
@@ -10024,7 +11382,6 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10024
11382
  this.activeToolCallAbortController?.abort("Disconnected");
10025
11383
  this.activeToolCallAbortController = void 0;
10026
11384
  this.clearReceiverTasks();
10027
- this.appRendererClientProxy = null;
10028
11385
  this.capabilities = void 0;
10029
11386
  this.serverInfo = void 0;
10030
11387
  this.instructions = void 0;
@@ -10049,31 +11406,19 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10049
11406
  this.dispatchTypedEvent("malformedListItemsChange", []);
10050
11407
  }
10051
11408
  /**
10052
- * Returns a client proxy for use by AppRenderer / @mcp-ui. Delegates to the
10053
- * internal MCP Client. Returns null when not connected. Use this instead of
10054
- * accessing the raw client so behavior can be adapted here later if needed.
11409
+ * The SDK client for the MCP Apps host bridge, or null when not connected.
11410
+ *
11411
+ * ext-apps' `AppBridge` (2.0.0+, an SDK v2 peer) takes the v2 `Client`
11412
+ * directly and registers its list-changed forwarding on it in the
11413
+ * method-string form, so this hands out the real client. The Proxy that
11414
+ * translated the 1.x peer's schema-first `setNotificationHandler(Schema,
11415
+ * handler)` into that form is gone with it (#1745). Kept as the seam rather
11416
+ * than exposing the field so the connected-status gate lives in one place
11417
+ * and hooks, fakes and tests depend on the protocol, not the class.
10055
11418
  */
10056
11419
  getAppRendererClient() {
10057
11420
  if (!this.client || this.status !== "connected") return null;
10058
- if (this.appRendererClientProxy !== null)
10059
- return this.appRendererClientProxy;
10060
- const target = this.client;
10061
- this.appRendererClientProxy = new Proxy(this.client, {
10062
- get(proxyTarget, prop, receiver) {
10063
- const value = Reflect.get(proxyTarget, prop, receiver);
10064
- if (prop === "setNotificationHandler" && typeof value === "function") {
10065
- return (schemaOrMethod, ...rest) => {
10066
- const method = typeof schemaOrMethod === "string" ? schemaOrMethod : notificationMethodFromSchema(schemaOrMethod) ?? schemaOrMethod;
10067
- return value.apply(target, [
10068
- method,
10069
- ...rest
10070
- ]);
10071
- };
10072
- }
10073
- return value;
10074
- }
10075
- });
10076
- return this.appRendererClientProxy;
11421
+ return this.client;
10077
11422
  }
10078
11423
  /**
10079
11424
  * Send a ping request to the server. Resolves when the server responds.
@@ -10235,7 +11580,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10235
11580
  const timer = setTimeout(() => {
10236
11581
  this.pendingRawWireRequests.delete(id);
10237
11582
  reject(
10238
- new Error(`Raw request "${method}" timed out after ${timeoutMs} ms`)
11583
+ annotateRequestTimeout(
11584
+ new SdkError5(SdkErrorCode5.RequestTimeout, "Request timed out", {
11585
+ timeout: timeoutMs
11586
+ }),
11587
+ method,
11588
+ this.getConnectionDiagnostics()
11589
+ )
10239
11590
  );
10240
11591
  }, timeoutMs);
10241
11592
  this.pendingRawWireRequests.set(id, { resolve: resolve7, reject, timer });
@@ -10245,7 +11596,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10245
11596
  clearTimeout(pending.timer);
10246
11597
  this.pendingRawWireRequests.delete(id);
10247
11598
  }
10248
- reject(err instanceof Error ? err : new Error(String(err)));
11599
+ reject(
11600
+ annotateRequestTimeout(
11601
+ err instanceof Error ? err : new Error(String(err)),
11602
+ method,
11603
+ this.getConnectionDiagnostics()
11604
+ )
11605
+ );
10249
11606
  });
10250
11607
  });
10251
11608
  return resultSchema.parse(raw);
@@ -10418,7 +11775,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10418
11775
  throw new Error("Client is not connected");
10419
11776
  }
10420
11777
  const result = await this.client.request(
10421
- { method: "tasks/list", params: cursor ? { cursor } : {} },
11778
+ {
11779
+ method: "tasks/list",
11780
+ // `!== undefined`, not truthiness: a cursor is opaque and `""` is a
11781
+ // legal value a server may hand back. Dropping it asks for page one
11782
+ // again, so a caller walking pages would loop on the first page.
11783
+ params: cursor !== void 0 ? { cursor } : {}
11784
+ },
10422
11785
  ListTasksResultSchema,
10423
11786
  this.getRequestOptions()
10424
11787
  );
@@ -12012,7 +13375,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12012
13375
  const effectiveMeta = this.mergeMeta(metadata);
12013
13376
  const params = {
12014
13377
  ...effectiveMeta ? { _meta: effectiveMeta } : {},
12015
- ...cursor ? { cursor } : {}
13378
+ // `!== undefined`, not truthiness: a cursor is opaque and `""` is a
13379
+ // legal value a server may hand back. Dropping it asks for page one
13380
+ // again, so a caller walking pages would loop on the first page.
13381
+ ...cursor !== void 0 ? { cursor } : {}
12016
13382
  };
12017
13383
  const response = await this.invokeMcpClient(
12018
13384
  () => this.client.request(
@@ -12040,7 +13406,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12040
13406
  method: "resources/list",
12041
13407
  itemsKey: "resources",
12042
13408
  resultSchema: ListResourcesResultSchema,
12043
- itemSchema: ResourceSchema,
13409
+ itemSchema: ResourceSchema2,
12044
13410
  metadata: options?.metadata,
12045
13411
  aggregate: async () => [
12046
13412
  ...(await this.client.listResources(
@@ -12142,7 +13508,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12142
13508
  const effectiveMeta = this.mergeMeta(metadata);
12143
13509
  const params = {
12144
13510
  ...effectiveMeta ? { _meta: effectiveMeta } : {},
12145
- ...cursor ? { cursor } : {}
13511
+ // `!== undefined`, not truthiness: a cursor is opaque and `""` is a
13512
+ // legal value a server may hand back. Dropping it asks for page one
13513
+ // again, so a caller walking pages would loop on the first page.
13514
+ ...cursor !== void 0 ? { cursor } : {}
12146
13515
  };
12147
13516
  const response = await this.invokeMcpClient(
12148
13517
  () => this.client.request(
@@ -12195,7 +13564,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12195
13564
  const effectiveMeta = this.mergeMeta(metadata);
12196
13565
  const params = {
12197
13566
  ...effectiveMeta ? { _meta: effectiveMeta } : {},
12198
- ...cursor ? { cursor } : {}
13567
+ // `!== undefined`, not truthiness: a cursor is opaque and `""` is a
13568
+ // legal value a server may hand back. Dropping it asks for page one
13569
+ // again, so a caller walking pages would loop on the first page.
13570
+ ...cursor !== void 0 ? { cursor } : {}
12199
13571
  };
12200
13572
  const response = await this.invokeMcpClient(
12201
13573
  () => this.client.request(
@@ -12234,6 +13606,155 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12234
13606
  });
12235
13607
  return { prompts };
12236
13608
  }
13609
+ /**
13610
+ * The Skills extension (SEP-2640) the server declared, or `undefined` when it
13611
+ * declared none. Gates the Skills tab and the skills store the same way
13612
+ * {@link isTasksExtensionNegotiated} gates Tasks — but deliberately without an
13613
+ * era check: `skills/*` are not spec method names in either codec, so a
13614
+ * legacy-era server that declares the extension is serving it (#2234).
13615
+ */
13616
+ getSkillsExtension() {
13617
+ return getSkillsExtension(this.capabilities);
13618
+ }
13619
+ /**
13620
+ * One page of `skills/list` (SEP-2640).
13621
+ *
13622
+ * An ordinary `client.request` with an explicit result schema — the SDK's era
13623
+ * gate skips methods neither codec defines, and `assertCapabilityForMethod`
13624
+ * falls through to a no-op for them, so this is all the extension needs. The
13625
+ * raw-wire path modern `tasks/*` uses would be wrong here: it exists for spec
13626
+ * names the 2026 codec deleted, and taking it would bypass the SDK's response
13627
+ * correlation for nothing (#2234).
13628
+ */
13629
+ async listSkills(cursor, metadata) {
13630
+ if (!this.client) {
13631
+ throw new Error("Client is not connected");
13632
+ }
13633
+ const effectiveMeta = this.mergeMeta(metadata);
13634
+ const params = {
13635
+ ...effectiveMeta ? { _meta: effectiveMeta } : {},
13636
+ // `!== undefined`, not truthiness: a cursor is opaque and the empty
13637
+ // string is a legal value. Dropping `""` would silently re-request page
13638
+ // one, which the store then reports as a repeated-cursor failure — a
13639
+ // conforming server made to look broken.
13640
+ ...cursor !== void 0 ? { cursor } : {}
13641
+ };
13642
+ const resultSchema = this.isModernEra() ? ModernListSkillsResultSchema : ListSkillsResultSchema;
13643
+ const response = await this.invokeMcpClient(
13644
+ () => this.client.request(
13645
+ { method: SKILLS_LIST_METHOD, params },
13646
+ resultSchema,
13647
+ this.getRequestOptions(this.progressTokenOf(metadata))
13648
+ ),
13649
+ { method: SKILLS_LIST_METHOD }
13650
+ );
13651
+ return {
13652
+ skills: response.skills,
13653
+ nextCursor: response.nextCursor
13654
+ };
13655
+ }
13656
+ /**
13657
+ * One skill entry by URI (`skills/get`, SEP-2640). The result envelope is
13658
+ * required — `GetSkillEnvelopeSchema` requires `{ skill }` and rejects an entry
13659
+ * returned inline, so a non-conforming shape fails here rather than being
13660
+ * silently normalized past the conformance checks.
13661
+ */
13662
+ async getSkill(uri, metadata) {
13663
+ return (await this.getSkillResult(uri, metadata)).skill;
13664
+ }
13665
+ /**
13666
+ * `skills/get` as the SDK decoded it — the `{ skill }` envelope **and any
13667
+ * other members the server sent**, such as the caching attributes. On a
13668
+ * modern connection `resultType` is not among them: the codec checks it and
13669
+ * removes it before the result gets here (#2373).
13670
+ *
13671
+ * Separate from {@link getSkill} because the callers differ: the UIs want the
13672
+ * entry, while the CLI prints the result and must not reshape it. SEP-2640
13673
+ * explicitly leaves open whether this result carries `ttlMs` / `cacheScope`,
13674
+ * so a server may send them — and unwrapping to the entry discards exactly
13675
+ * those (Copilot).
13676
+ */
13677
+ async getSkillResult(uri, metadata) {
13678
+ if (!this.client) {
13679
+ throw new Error("Client is not connected");
13680
+ }
13681
+ const effectiveMeta = this.mergeMeta(metadata);
13682
+ const params = {
13683
+ uri,
13684
+ ...effectiveMeta ? { _meta: effectiveMeta } : {}
13685
+ };
13686
+ try {
13687
+ return await this.invokeMcpClient(
13688
+ () => this.client.request(
13689
+ { method: SKILLS_GET_METHOD, params },
13690
+ GetSkillEnvelopeSchema,
13691
+ this.getRequestOptions(this.progressTokenOf(metadata))
13692
+ ),
13693
+ { method: SKILLS_GET_METHOD }
13694
+ );
13695
+ } catch (err) {
13696
+ if (isClientDecodeRejection(err)) {
13697
+ this.markResponseRejected(
13698
+ SKILLS_GET_METHOD,
13699
+ err instanceof Error ? err.message : String(err)
13700
+ );
13701
+ }
13702
+ throw err;
13703
+ }
13704
+ }
13705
+ /**
13706
+ * One page of `resources/directory/read` (SEP-2640): the direct children of
13707
+ * a directory resource.
13708
+ *
13709
+ * **Gated on the server's own declaration, and the gate throws rather than
13710
+ * asks.** SEP-2640 is explicit that *"clients MUST NOT call
13711
+ * `resources/directory/read` against a server that has not declared
13712
+ * `directoryRead: true`"*, so this refuses locally instead of sending a call
13713
+ * the spec forbids and letting the server answer `-32601`. Refusing here also
13714
+ * keeps the Protocol tab honest: a request we were never allowed to make
13715
+ * should not appear in the exchange log as a server-side failure.
13716
+ *
13717
+ * Not recursive — the SEP says clients descend by calling the method again on
13718
+ * a child directory, so the walking is the caller's, not this method's.
13719
+ */
13720
+ async readResourceDirectory(uri, cursor, metadata) {
13721
+ if (!this.client) {
13722
+ throw new Error("Client is not connected");
13723
+ }
13724
+ const extension = this.getSkillsExtension();
13725
+ if (!extension?.directoryRead) {
13726
+ throw new Error(
13727
+ `Server did not declare directoryRead in ${SKILLS_EXTENSION_KEY}; ${RESOURCES_DIRECTORY_READ_METHOD} must not be called.`
13728
+ );
13729
+ }
13730
+ const effectiveMeta = this.mergeMeta(metadata);
13731
+ const params = {
13732
+ uri,
13733
+ ...effectiveMeta ? { _meta: effectiveMeta } : {},
13734
+ // `!== undefined` for the same reason `listSkills` uses it: a cursor is
13735
+ // opaque and `""` is a legal value, so truthiness would silently re-ask
13736
+ // for page one.
13737
+ ...cursor !== void 0 ? { cursor } : {}
13738
+ };
13739
+ try {
13740
+ return await this.invokeMcpClient(
13741
+ () => this.client.request(
13742
+ { method: RESOURCES_DIRECTORY_READ_METHOD, params },
13743
+ DirectoryReadResultSchema,
13744
+ this.getRequestOptions(this.progressTokenOf(metadata))
13745
+ ),
13746
+ { method: RESOURCES_DIRECTORY_READ_METHOD }
13747
+ );
13748
+ } catch (err) {
13749
+ if (isClientDecodeRejection(err)) {
13750
+ this.markResponseRejected(
13751
+ RESOURCES_DIRECTORY_READ_METHOD,
13752
+ err instanceof Error ? err.message : String(err)
13753
+ );
13754
+ }
13755
+ throw err;
13756
+ }
13757
+ }
12237
13758
  /**
12238
13759
  * Get a prompt by name
12239
13760
  * @param name Prompt name
@@ -12380,6 +13901,72 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12380
13901
  dispatchFetchRequestBodyUpdate(id, responseBody) {
12381
13902
  this.dispatchTypedEvent("fetchRequestBodyUpdate", { id, responseBody });
12382
13903
  }
13904
+ dispatchFetchRequestStreamUpdate(id, stream) {
13905
+ this.dispatchTypedEvent("fetchRequestStreamUpdate", { id, stream });
13906
+ if (this.notificationStream?.id !== id) return;
13907
+ this.notificationStream = {
13908
+ ...this.notificationStream,
13909
+ eventCount: stream.eventCount,
13910
+ ...stream.closedAt && { closedAt: stream.closedAt.getTime() }
13911
+ };
13912
+ this.dispatchConnectionDiagnosticsChange();
13913
+ }
13914
+ /**
13915
+ * Adopt a transport fetch as the notification stream when it is one — a
13916
+ * `GET` answered with an unbounded event stream — so the diagnostics can
13917
+ * say how long it has been open and what it has delivered (#2318). The
13918
+ * newest such stream wins: the SDK reopens the standalone `GET` after a
13919
+ * drop, and the current one is the one a stalled request is queued behind.
13920
+ */
13921
+ noteTransportStream(entry) {
13922
+ if (!isLongLivedStreamResponse(
13923
+ entry.method,
13924
+ findHeader(entry.responseHeaders, "content-type")
13925
+ )) {
13926
+ return;
13927
+ }
13928
+ this.notificationStream = {
13929
+ id: entry.id,
13930
+ url: entry.url,
13931
+ // The entry's `timestamp` is when the request went out; the stream is
13932
+ // open from when the response headers arrived, `duration` later.
13933
+ openedAt: entry.timestamp.getTime() + (entry.duration ?? 0),
13934
+ eventCount: 0
13935
+ };
13936
+ this.dispatchConnectionDiagnosticsChange();
13937
+ }
13938
+ /**
13939
+ * What this client is still waiting on, when it last heard back, and the
13940
+ * state of the notification stream (#2318). A fresh snapshot per call —
13941
+ * the same one `annotateRequestTimeout` writes a timeout message from, and
13942
+ * the one `connectionDiagnosticsChange` carries.
13943
+ */
13944
+ getConnectionDiagnostics() {
13945
+ return {
13946
+ capturedAt: Date.now(),
13947
+ // Copies, not the tracked records: the snapshot is handed to callers
13948
+ // and carried on errors, and a mutation there must not reach the
13949
+ // correlation state.
13950
+ outstandingRequests: [...this.outstandingRequests.values()].map(
13951
+ (request) => ({ ...request })
13952
+ ),
13953
+ ...this.lastResponse && { lastResponse: { ...this.lastResponse } },
13954
+ ...this.notificationStream && {
13955
+ notificationStream: this.publicStreamState(this.notificationStream)
13956
+ }
13957
+ };
13958
+ }
13959
+ /** The stream state minus the entry id, which is bookkeeping, not a fact. */
13960
+ publicStreamState(stream) {
13961
+ const { id: _id, ...state } = stream;
13962
+ return state;
13963
+ }
13964
+ dispatchConnectionDiagnosticsChange() {
13965
+ this.dispatchTypedEvent(
13966
+ "connectionDiagnosticsChange",
13967
+ this.getConnectionDiagnostics()
13968
+ );
13969
+ }
12383
13970
  /**
12384
13971
  * Get current session ID (from OAuth state authId)
12385
13972
  */
@@ -13259,7 +14846,30 @@ var EXIT_CODES = {
13259
14846
  AUTH_REQUIRED: 3,
13260
14847
  UNREACHABLE: 4,
13261
14848
  TOOL_ERROR: 5,
13262
- SCHEMA_UNPORTABLE: 6
14849
+ SCHEMA_UNPORTABLE: 6,
14850
+ /**
14851
+ * `--verify` found a SEP-2640 violation: a conformance error, a digest or
14852
+ * size mismatch, or a manifest file that could not be read (#2248).
14853
+ *
14854
+ * Its own code rather than reusing `SCHEMA_UNPORTABLE`, for the reason that
14855
+ * one exists at all: a CI job that fails on an unportable tool schema and a
14856
+ * CI job that fails on a tampered skill digest are different jobs, and
14857
+ * collapsing them would make `if [ $? -eq 6 ]` ambiguous.
14858
+ */
14859
+ SKILL_NONCONFORMANT: 7,
14860
+ /**
14861
+ * `--verify` could not check the whole catalog: the read bounds stopped the
14862
+ * walk before it finished (#2248).
14863
+ *
14864
+ * Distinct from `SKILL_NONCONFORMANT` because the server has broken no
14865
+ * **MUST** — SEP-2640 states the 512-entry and 16 MiB limits as SHOULD NOT,
14866
+ * with hosts free to support more — so exiting 7 would call a conforming
14867
+ * server nonconformant. It is still non-zero, because reporting success for
14868
+ * a manifest whose unread entries were never fetched is a false pass. A CI
14869
+ * job that wants to tolerate oversized catalogs can allow 8 and still fail
14870
+ * on 7.
14871
+ */
14872
+ SKILL_INCOMPLETE: 8
13263
14873
  };
13264
14874
  var CliExitCodeError = class extends Error {
13265
14875
  constructor(exitCode, message, envelope) {
@@ -13293,7 +14903,7 @@ function statusOf(error) {
13293
14903
  }
13294
14904
  return void 0;
13295
14905
  }
13296
- var UNREACHABLE_PATTERN = /ENOTFOUND|ECONNREFUSED|ECONNRESET|EAI_AGAIN|ETIMEDOUT|fetch failed|getaddrinfo|connect timed out|aborted/i;
14906
+ var UNREACHABLE_PATTERN = /ENOTFOUND|ECONNREFUSED|ECONNRESET|EAI_AGAIN|ETIMEDOUT|fetch failed|getaddrinfo|connect(?:ion)? timed out|aborted/i;
13297
14907
  function classifyError(error, context) {
13298
14908
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Unknown error";
13299
14909
  const cause = causeOf(error);
@@ -13663,12 +15273,12 @@ function countFindings(results) {
13663
15273
  }
13664
15274
  return { errors, warnings };
13665
15275
  }
13666
- function plural(count, noun) {
15276
+ function plural2(count, noun) {
13667
15277
  return `${count} ${noun}${count === 1 ? "" : "s"}`;
13668
15278
  }
13669
15279
  function summarizeFindings(results) {
13670
15280
  const { errors, warnings } = countFindings(results);
13671
- return `${plural(errors, "error")}, ${plural(warnings, "warning")} across ${plural(results.length, "tool")}`;
15281
+ return `${plural2(errors, "error")}, ${plural2(warnings, "warning")} across ${plural2(results.length, "tool")}`;
13672
15282
  }
13673
15283
  function formatSchemaLintReport(results) {
13674
15284
  const lines = [];
@@ -13767,6 +15377,15 @@ async function consumeMethodOutcome(outcome, args) {
13767
15377
  for (const line of outcome.lines) {
13768
15378
  await awaitableLog(JSON.stringify(line) + "\n");
13769
15379
  }
15380
+ if (outcome.summary) await awaitableError(`${outcome.summary}
15381
+ `);
15382
+ if (outcome.exitCode) {
15383
+ throw new CliExitCodeError(outcome.exitCode, outcome.summary ?? "", {
15384
+ // The envelope's `code` follows the exit code, so a caller reading one
15385
+ // never has to reconcile it against the other.
15386
+ code: outcome.exitCode === EXIT_CODES.SKILL_INCOMPLETE ? "skills_incomplete" : "skills_nonconformant"
15387
+ });
15388
+ }
13770
15389
  return;
13771
15390
  }
13772
15391
  await new Promise((resolve7) => {
@@ -14332,7 +15951,7 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
14332
15951
  const page = result.tasks.filter((t) => !this.dismissedTaskIds.has(t.taskId)).map(
14333
15952
  (t) => this.cancelledTaskIds.has(t.taskId) ? { ...t, status: "cancelled" } : t
14334
15953
  );
14335
- this.tasks = cursor ? [...this.tasks, ...page] : page;
15954
+ this.tasks = cursor === void 0 ? page : [...this.tasks, ...page];
14336
15955
  cursor = result.nextCursor;
14337
15956
  pageCount++;
14338
15957
  if (pageCount >= MAX_PAGES) {
@@ -14340,7 +15959,7 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
14340
15959
  `Maximum pagination limit (${MAX_PAGES} pages) reached while listing requestor tasks`
14341
15960
  );
14342
15961
  }
14343
- } while (cursor);
15962
+ } while (cursor !== void 0);
14344
15963
  this.dispatchTypedEvent("tasksChange", this.tasks);
14345
15964
  return this.getTasks();
14346
15965
  }
@@ -14393,12 +16012,150 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
14393
16012
  }
14394
16013
  };
14395
16014
 
16015
+ // ../../core/mcp/state/managedSkillsState.ts
16016
+ var REPEATED_CURSOR_MESSAGE = "Server repeated a pagination cursor in skills/list; stopped to avoid an infinite walk.";
16017
+ var SKILLS_MAX_PAGES = LIST_MAX_PAGES;
16018
+ var SKILLS_PAGE_LIMIT_MESSAGE = `skills/list exceeded ${SKILLS_MAX_PAGES} pages without the server's pagination converging`;
16019
+ var ManagedSkillsState = class extends TypedEventTarget {
16020
+ skills = [];
16021
+ pageCount = 0;
16022
+ error = null;
16023
+ client = null;
16024
+ unsubscribe = null;
16025
+ // Overlap guard, held as the generation whose walk is in flight (or `null`).
16026
+ // See `refresh` for why this is not a boolean.
16027
+ runningGeneration = null;
16028
+ // Session counter, advanced by `reset` (disconnect) and by `destroy`. A walk
16029
+ // captures it and abandons its writes when it no longer matches.
16030
+ generation = 0;
16031
+ constructor(client) {
16032
+ super();
16033
+ this.client = client;
16034
+ const onConnect = () => {
16035
+ void this.refresh().catch(() => {
16036
+ });
16037
+ };
16038
+ const onStatusChange = () => {
16039
+ if (isTerminalStatus(this.client?.getStatus())) {
16040
+ this.reset();
16041
+ }
16042
+ };
16043
+ this.client.addEventListener("connect", onConnect);
16044
+ this.client.addEventListener("statusChange", onStatusChange);
16045
+ this.unsubscribe = () => {
16046
+ if (this.client) {
16047
+ this.client.removeEventListener("connect", onConnect);
16048
+ this.client.removeEventListener("statusChange", onStatusChange);
16049
+ }
16050
+ this.client = null;
16051
+ };
16052
+ }
16053
+ /** Defensive copy of the current list. */
16054
+ getSkills() {
16055
+ return [...this.skills];
16056
+ }
16057
+ getPagination() {
16058
+ return { pageCount: this.pageCount };
16059
+ }
16060
+ /** The last walk's failure, or `null` when it succeeded. */
16061
+ getError() {
16062
+ return this.error;
16063
+ }
16064
+ // Compared by identity rather than message: two distinct failures with the
16065
+ // same text are still two events, and a re-render on a repeat failure is
16066
+ // cheap next to silently coalescing them.
16067
+ setError(value) {
16068
+ if (this.error === value) return;
16069
+ this.error = value;
16070
+ this.dispatchTypedEvent("errorChange", value);
16071
+ }
16072
+ reset() {
16073
+ this.generation += 1;
16074
+ this.skills = [];
16075
+ this.pageCount = 0;
16076
+ this.dispatchTypedEvent("skillsChange", this.getSkills());
16077
+ this.dispatchTypedEvent("paginationChange", this.getPagination());
16078
+ this.setError(null);
16079
+ }
16080
+ /**
16081
+ * Walk every page of `skills/list` and commit the result.
16082
+ *
16083
+ * A failure is recorded as observable state (`getError`) AND re-thrown: the
16084
+ * state drives the panel's error rendering, while the rejection is what a
16085
+ * caller's auth-recovery wrapper keys off to detect a 401 and start a
16086
+ * re-authorization. The connect-time load, which has no such caller, catches
16087
+ * it above.
16088
+ */
16089
+ async refresh(metadata) {
16090
+ const client = this.client;
16091
+ if (!client || client.getStatus() !== "connected") return this.getSkills();
16092
+ if (!client.getSkillsExtension()) {
16093
+ this.applyPages([], 0);
16094
+ return this.getSkills();
16095
+ }
16096
+ const generation = this.generation;
16097
+ const isCurrent = () => this.generation === generation;
16098
+ if (this.runningGeneration === generation) return this.getSkills();
16099
+ this.runningGeneration = generation;
16100
+ try {
16101
+ const collected = [];
16102
+ const seen = /* @__PURE__ */ new Set();
16103
+ let cursor;
16104
+ let pages = 0;
16105
+ for (; ; ) {
16106
+ const page = await client.listSkills(cursor, metadata);
16107
+ if (!isCurrent()) return this.getSkills();
16108
+ collected.push(...page.skills);
16109
+ pages += 1;
16110
+ if (page.nextCursor === void 0) break;
16111
+ if (pages >= SKILLS_MAX_PAGES) {
16112
+ throw new Error(SKILLS_PAGE_LIMIT_MESSAGE);
16113
+ }
16114
+ if (seen.has(page.nextCursor)) {
16115
+ throw new Error(REPEATED_CURSOR_MESSAGE);
16116
+ }
16117
+ seen.add(page.nextCursor);
16118
+ cursor = page.nextCursor;
16119
+ }
16120
+ this.setError(null);
16121
+ this.applyPages(collected, pages);
16122
+ return this.getSkills();
16123
+ } catch (err) {
16124
+ if (isCurrent()) {
16125
+ const error = err instanceof Error ? err : new Error(String(err));
16126
+ this.setError(error);
16127
+ if (isClientDecodeRejection(err)) {
16128
+ client.markResponseRejected?.(SKILLS_LIST_METHOD, error.message);
16129
+ }
16130
+ }
16131
+ throw err;
16132
+ } finally {
16133
+ if (this.runningGeneration === generation) {
16134
+ this.runningGeneration = null;
16135
+ }
16136
+ }
16137
+ }
16138
+ applyPages(skills, pageCount) {
16139
+ this.skills = skills;
16140
+ this.pageCount = pageCount;
16141
+ this.dispatchTypedEvent("skillsChange", this.getSkills());
16142
+ this.dispatchTypedEvent("paginationChange", this.getPagination());
16143
+ }
16144
+ /** Unsubscribe from the client and drop the list; idempotent. */
16145
+ destroy() {
16146
+ this.generation += 1;
16147
+ this.unsubscribe?.();
16148
+ this.unsubscribe = null;
16149
+ this.skills = [];
16150
+ this.pageCount = 0;
16151
+ this.error = null;
16152
+ }
16153
+ };
16154
+
14396
16155
  // ../../core/mcp/apps.ts
14397
16156
  import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
14398
16157
  function getAppResourceUri(tool) {
14399
- return getToolUiResourceUri(
14400
- tool
14401
- );
16158
+ return getToolUiResourceUri(tool);
14402
16159
  }
14403
16160
  function readUiMeta(carrier) {
14404
16161
  const ui = carrier !== null && typeof carrier === "object" ? carrier._meta?.ui : void 0;
@@ -14463,13 +16220,245 @@ async function collectAppInfo(client, tool, metadata) {
14463
16220
  }
14464
16221
  }
14465
16222
 
16223
+ // src/handlers/skills-verify.ts
16224
+ function summarizeSkillVerification(reports) {
16225
+ const failed = reports.filter((report) => report.outcome === "failed").length;
16226
+ const incomplete = reports.filter(
16227
+ (report) => report.outcome === "incomplete"
16228
+ ).length;
16229
+ const files = reports.reduce((sum, report) => sum + report.files.length, 0);
16230
+ const mismatched = reports.reduce(
16231
+ (sum, report) => sum + report.files.filter((file) => file.status === "mismatch").length,
16232
+ 0
16233
+ );
16234
+ const skillWord = reports.length === 1 ? "skill" : "skills";
16235
+ const fileWord = files === 1 ? "file" : "files";
16236
+ const incompleteClause = incomplete === 0 ? "" : ` ${incomplete} of ${reports.length} ${skillWord} could not be fully checked: the read bounds stopped the walk.`;
16237
+ const headline = failed === 0 ? incomplete === 0 ? `Verified ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors.` : `Checked ${reports.length} ${skillWord} and ${files} ${fileWord}: no conformance errors in what was read.` : `${failed} of ${reports.length} ${skillWord} failed verification (${mismatched} digest/size mismatch across ${files} ${fileWord}).`;
16238
+ return `${headline}${incompleteClause}`;
16239
+ }
16240
+
16241
+ // ../../core/mcp/skillsVerification.ts
16242
+ function reasonOf(err) {
16243
+ return err instanceof Error ? err.message : String(err);
16244
+ }
16245
+ function boundedManifest(declared) {
16246
+ const kept = [];
16247
+ let bytes = 0;
16248
+ for (const resource of declared) {
16249
+ if (kept.length >= SKILL_MAX_RESOURCE_ENTRIES) break;
16250
+ const size = typeof resource.size === "number" && Number.isSafeInteger(resource.size) && resource.size >= 0 ? resource.size : 0;
16251
+ if (bytes + size > SKILL_MAX_TOTAL_BYTES) break;
16252
+ bytes += size;
16253
+ kept.push(resource);
16254
+ }
16255
+ return kept;
16256
+ }
16257
+ function contentsFor(result, uri) {
16258
+ const contents = result?.contents;
16259
+ if (!Array.isArray(contents)) return void 0;
16260
+ const wanted = skillUriIdentity(uri);
16261
+ for (const block of contents) {
16262
+ if (typeof block !== "object" || block === null) continue;
16263
+ const got = block.uri;
16264
+ if (typeof got !== "string") continue;
16265
+ if (skillUriIdentity(got) === wanted) return block;
16266
+ }
16267
+ return void 0;
16268
+ }
16269
+ function utf8Length(value) {
16270
+ let total = 0;
16271
+ for (let i = 0; i < value.length; i += 1) {
16272
+ const code = value.charCodeAt(i);
16273
+ if (code < 128) {
16274
+ total += 1;
16275
+ } else if (code < 2048) {
16276
+ total += 2;
16277
+ } else if (code >= 55296 && code <= 56319) {
16278
+ const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
16279
+ if (next >= 56320 && next <= 57343) {
16280
+ total += 4;
16281
+ i += 1;
16282
+ } else {
16283
+ total += 3;
16284
+ }
16285
+ } else {
16286
+ total += 3;
16287
+ }
16288
+ }
16289
+ return total;
16290
+ }
16291
+ function responseBytes(result) {
16292
+ const contents = result?.contents;
16293
+ if (!Array.isArray(contents)) return 0;
16294
+ let total = 0;
16295
+ for (const block of contents) {
16296
+ if (typeof block !== "object" || block === null) continue;
16297
+ const { text, blob } = block;
16298
+ if (typeof text === "string") total += utf8Length(text);
16299
+ if (typeof blob === "string") total += blob.length;
16300
+ }
16301
+ return total;
16302
+ }
16303
+ async function verifySkills(client, entries, metadata) {
16304
+ const collisions = checkSkillNameCollisions(entries);
16305
+ const reports = [];
16306
+ const budget = resolveSkillCatalogBudget(client.getServerSettings?.());
16307
+ let walkedSkills = 0;
16308
+ let catalogBytes = 0;
16309
+ for (const entry of entries) {
16310
+ const withinBudget = walkedSkills < budget.maxSkills && catalogBytes < budget.maxBytes;
16311
+ let entryBytes;
16312
+ const files = [];
16313
+ const declared = entry.resources === DYNAMIC_RESOURCES ? [] : entry.resources;
16314
+ const manifest = withinBudget ? boundedManifest(declared) : [];
16315
+ let incomplete = !withinBudget ? `Not read: this run already reached its catalog budget of ${budget.maxSkills} skills / ${budget.maxBytes} bytes (raise it in the server's Skills settings). Nothing about this skill's files has been checked \u2014 verify it on its own with \`--method skills/get --uri\` to get a verdict.` : manifest.length < declared.length ? `Only ${manifest.length} of ${declared.length} manifest entries were read: the skill exceeds the ${SKILL_MAX_RESOURCE_ENTRIES}-entry / ${SKILL_MAX_TOTAL_BYTES}-byte interoperability limits, so the rest were not fetched and cannot be reported on.` : void 0;
16316
+ let receivedBytes = 0;
16317
+ const entryIdentity = skillUriIdentity(entry.uri);
16318
+ let selfAttempted = false;
16319
+ for (const [index, resource] of manifest.entries()) {
16320
+ if (skillUriIdentity(resource.uri) === entryIdentity) {
16321
+ selfAttempted = true;
16322
+ }
16323
+ let charged = 0;
16324
+ try {
16325
+ const invocation = await client.readResource(resource.uri, metadata);
16326
+ charged = responseBytes(invocation.result);
16327
+ const contents = contentsFor(invocation.result, resource.uri);
16328
+ if (!contents) {
16329
+ files.push({
16330
+ uri: resource.uri,
16331
+ status: "read-error",
16332
+ reason: "resources/read returned no content block for this URI, so there are no bytes that can be checked against its digest."
16333
+ });
16334
+ } else {
16335
+ let bytes;
16336
+ try {
16337
+ bytes = skillFileBytes(contents);
16338
+ } catch (err) {
16339
+ files.push({
16340
+ uri: resource.uri,
16341
+ status: "read-error",
16342
+ reason: reasonOf(err)
16343
+ });
16344
+ }
16345
+ if (bytes) {
16346
+ charged = Math.max(charged, bytes.byteLength);
16347
+ if (skillUriIdentity(resource.uri) === entryIdentity)
16348
+ entryBytes = bytes;
16349
+ const verification = await verifySkillResource(resource, bytes);
16350
+ files.push({ uri: resource.uri, ...verification });
16351
+ }
16352
+ }
16353
+ } catch (err) {
16354
+ if (err instanceof AuthRecoveryRequiredError) throw err;
16355
+ files.push({
16356
+ uri: resource.uri,
16357
+ status: "read-error",
16358
+ reason: reasonOf(err)
16359
+ });
16360
+ }
16361
+ receivedBytes += charged;
16362
+ if (receivedBytes > SKILL_MAX_TOTAL_BYTES) {
16363
+ if (index < manifest.length - 1) {
16364
+ incomplete = `Stopped after ${index + 1} of ${declared.length} manifest entries: the files actually served exceed the ${SKILL_MAX_TOTAL_BYTES}-byte interoperability limit, whatever sizes the manifest declared.`;
16365
+ }
16366
+ break;
16367
+ }
16368
+ }
16369
+ if (withinBudget && !selfAttempted) {
16370
+ const fail = (reason) => files.push({ uri: entry.uri, status: "read-error", reason });
16371
+ try {
16372
+ const invocation = await client.readResource(entry.uri, metadata);
16373
+ let charged = responseBytes(invocation.result);
16374
+ const contents = contentsFor(invocation.result, entry.uri);
16375
+ if (!contents) {
16376
+ fail(
16377
+ "resources/read returned no content block for this skill's own SKILL.md, so its frontmatter cannot be checked against the listing."
16378
+ );
16379
+ } else {
16380
+ try {
16381
+ entryBytes = skillFileBytes(contents);
16382
+ charged = Math.max(charged, entryBytes.byteLength);
16383
+ } catch (err) {
16384
+ fail(reasonOf(err));
16385
+ }
16386
+ }
16387
+ receivedBytes += charged;
16388
+ const declaredSelf = declared.find(
16389
+ (resource) => skillUriIdentity(resource.uri) === entryIdentity
16390
+ );
16391
+ if (declaredSelf && entryBytes !== void 0) {
16392
+ files.push({
16393
+ // The DECLARED spelling, not the entry's. They can differ — a
16394
+ // manifest may write its self-entry in a normalized-equivalent
16395
+ // form — and a consumer matching rows against the manifest then
16396
+ // finds nothing, while a normalized "extra files" filter suppresses
16397
+ // it as already covered. The result was a verdict that existed in
16398
+ // the report and appeared nowhere on screen (Copilot).
16399
+ uri: declaredSelf.uri,
16400
+ ...await verifySkillResource(declaredSelf, entryBytes)
16401
+ });
16402
+ }
16403
+ } catch (err) {
16404
+ if (err instanceof AuthRecoveryRequiredError) throw err;
16405
+ fail(reasonOf(err));
16406
+ }
16407
+ }
16408
+ if (withinBudget) {
16409
+ walkedSkills += 1;
16410
+ catalogBytes += receivedBytes;
16411
+ }
16412
+ const entryText = entryBytes === void 0 ? void 0 : bytesToText(entryBytes);
16413
+ const collision = collisions.get(skillUriIdentity(entry.uri));
16414
+ const conformance = [
16415
+ ...checkSkillConformance(entry),
16416
+ ...collision ? [collision] : []
16417
+ ];
16418
+ const frontmatter = entryText === void 0 ? [] : checkSkillFrontmatterMatch(entry, entryText);
16419
+ const hasError = [...conformance, ...frontmatter].some(
16420
+ (issue) => issue.severity === "error"
16421
+ );
16422
+ const fileFailed = files.some(
16423
+ (file) => file.status === "mismatch" || file.status === "read-error"
16424
+ );
16425
+ reports.push({
16426
+ uri: entry.uri,
16427
+ name: skillDisplayName(entry),
16428
+ conformance,
16429
+ frontmatter,
16430
+ files,
16431
+ ...incomplete ? { incomplete } : {},
16432
+ ok: !hasError && !fileFailed,
16433
+ outcome: hasError || fileFailed ? "failed" : incomplete !== void 0 ? "incomplete" : "verified"
16434
+ });
16435
+ }
16436
+ return reports;
16437
+ }
16438
+ function allSkillsVerified(reports) {
16439
+ return reports.every((report) => report.outcome === "verified");
16440
+ }
16441
+ function anySkillFailed(reports) {
16442
+ return reports.some((report) => report.outcome === "failed");
16443
+ }
16444
+
14466
16445
  // src/handlers/run-method.ts
16446
+ function assertSkillsSupported(inspectorClient, method) {
16447
+ if (!inspectorClient.getSkillsExtension()) {
16448
+ throw new CliExitCodeError(
16449
+ EXIT_CODES.USAGE,
16450
+ `Server does not declare the ${SKILLS_EXTENSION_KEY} extension, so ${method} is not available.`,
16451
+ { code: "skills_unsupported" }
16452
+ );
16453
+ }
16454
+ }
14467
16455
  async function runMethod(inspectorClient, args) {
14468
16456
  let managedToolsState = null;
14469
16457
  let managedResourcesState = null;
14470
16458
  let managedResourceTemplatesState = null;
14471
16459
  let managedPromptsState = null;
14472
16460
  let managedTasksState = null;
16461
+ let managedSkillsState = null;
14473
16462
  try {
14474
16463
  let result;
14475
16464
  let appInfo;
@@ -14688,6 +16677,73 @@ async function runMethod(inspectorClient, args) {
14688
16677
  result = await inspectorClient.getRequestorTaskResult(
14689
16678
  args.taskId
14690
16679
  );
16680
+ } else if (args.method === "skills/list") {
16681
+ assertSkillsSupported(inspectorClient, args.method);
16682
+ managedSkillsState = new ManagedSkillsState(inspectorClient);
16683
+ const skills = await managedSkillsState.refresh(args.metadata);
16684
+ if (args.verify) {
16685
+ const reports = await verifySkills(
16686
+ inspectorClient,
16687
+ skills,
16688
+ args.metadata
16689
+ );
16690
+ return {
16691
+ kind: "ndjson",
16692
+ lines: reports,
16693
+ summary: summarizeSkillVerification(reports),
16694
+ // Three outcomes, three exit codes: a broken MUST is 7, a walk the
16695
+ // read bounds cut short is 8, and everything checked and passing is
16696
+ // 0. Collapsing the middle case into either of the others reports
16697
+ // something untrue about the server (Copilot).
16698
+ ...allSkillsVerified(reports) ? {} : {
16699
+ exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
16700
+ }
16701
+ };
16702
+ }
16703
+ result = { skills };
16704
+ } else if (args.method === "skills/get") {
16705
+ if (!args.uri) {
16706
+ throw new Error(
16707
+ "URI is required for skills/get method. Use --uri to specify the skill URI."
16708
+ );
16709
+ }
16710
+ assertSkillsSupported(inspectorClient, args.method);
16711
+ const envelope = await inspectorClient.getSkillResult(
16712
+ args.uri,
16713
+ args.metadata
16714
+ );
16715
+ const skill = envelope.skill;
16716
+ if (args.verify) {
16717
+ const reports = await verifySkills(
16718
+ inspectorClient,
16719
+ [skill],
16720
+ args.metadata
16721
+ );
16722
+ return {
16723
+ kind: "ndjson",
16724
+ lines: reports,
16725
+ summary: summarizeSkillVerification(reports),
16726
+ // Three outcomes, three exit codes: a broken MUST is 7, a walk the
16727
+ // read bounds cut short is 8, and everything checked and passing is
16728
+ // 0. Collapsing the middle case into either of the others reports
16729
+ // something untrue about the server (Copilot).
16730
+ ...allSkillsVerified(reports) ? {} : {
16731
+ exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
16732
+ }
16733
+ };
16734
+ }
16735
+ result = envelope;
16736
+ } else if (args.method === "resources/directory/read") {
16737
+ if (!args.uri) {
16738
+ throw new Error(
16739
+ "URI is required for resources/directory/read. Use --uri to specify the directory URI."
16740
+ );
16741
+ }
16742
+ result = await inspectorClient.readResourceDirectory(
16743
+ args.uri,
16744
+ args.cursor,
16745
+ args.metadata
16746
+ );
14691
16747
  } else if (args.method === "roots/list") {
14692
16748
  result = { roots: inspectorClient.getRoots() };
14693
16749
  } else if (args.method === "roots/set") {
@@ -14722,6 +16778,7 @@ async function runMethod(inspectorClient, args) {
14722
16778
  managedResourcesState?.destroy();
14723
16779
  managedResourceTemplatesState?.destroy();
14724
16780
  managedPromptsState?.destroy();
16781
+ managedSkillsState?.destroy();
14725
16782
  managedTasksState?.destroy();
14726
16783
  }
14727
16784
  }
@@ -14736,7 +16793,13 @@ var ONE_SHOT_METHODS = [
14736
16793
  "resources/templates/list",
14737
16794
  "prompts/list",
14738
16795
  "prompts/get",
14739
- "logging/setLevel"
16796
+ "logging/setLevel",
16797
+ // SEP-2640. All three are ordinary one-shot request/response calls — no
16798
+ // stream, no long-lived subscription — so they belong here alongside the
16799
+ // other list verbs rather than being reachable only from the session CLI.
16800
+ "skills/list",
16801
+ "skills/get",
16802
+ "resources/directory/read"
14740
16803
  ];
14741
16804
  function isOneShotMethod(method) {
14742
16805
  return ONE_SHOT_METHODS.includes(method);
@@ -15181,7 +17244,7 @@ function createCliOAuthNavigation(options = {}) {
15181
17244
  }
15182
17245
 
15183
17246
  // ../../core/client/config-parse.ts
15184
- import { z as z4 } from "zod";
17247
+ import { z as z5 } from "zod";
15185
17248
  function isAbsoluteHttpUrl(value) {
15186
17249
  let url;
15187
17250
  try {
@@ -15198,17 +17261,21 @@ function isRealHost(hostname) {
15198
17261
  const labels = hostname.split(".");
15199
17262
  return labels.length >= 2 && labels.every((label) => label !== "");
15200
17263
  }
15201
- var HttpUrlStringSchema = z4.string().min(1).superRefine((val, ctx) => {
17264
+ var HttpUrlStringSchema = z5.string().min(1).superRefine((val, ctx) => {
15202
17265
  const trimmed = val.trim();
15203
17266
  if (!isAbsoluteHttpUrl(trimmed)) {
15204
17267
  ctx.addIssue({
15205
- code: z4.ZodIssueCode.custom,
17268
+ code: z5.ZodIssueCode.custom,
15206
17269
  message: `Invalid URL: "${trimmed}" \u2014 must be an http(s) URL (e.g. https://idp.example.com)`
15207
17270
  });
15208
17271
  }
15209
17272
  });
15210
17273
  var CIMD_METADATA_URL_INVALID_ERROR = "Must be a valid URL, like https://example.com/oauth/client.json";
15211
- var CIMD_METADATA_URL_HTTPS_ERROR = "CIMD client metadata URL must use HTTPS";
17274
+ var CIMD_METADATA_URL_HTTPS_ERROR = "CIMD client metadata URL must use HTTPS, except on localhost, 127.0.0.1 or [::1]";
17275
+ var CIMD_HTTP_EXEMPT_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
17276
+ function isCimdHttpExemptHost(hostname) {
17277
+ return CIMD_HTTP_EXEMPT_HOSTS.has(hostname);
17278
+ }
15212
17279
  var CIMD_METADATA_URL_PATH_ERROR = "Must include a path (not the site root), like https://example.com/oauth/client.json";
15213
17280
  function getCimdClientMetadataUrlError(value) {
15214
17281
  const trimmed = value.trim();
@@ -15218,7 +17285,7 @@ function getCimdClientMetadataUrlError(value) {
15218
17285
  }
15219
17286
  try {
15220
17287
  const url = new URL(trimmed);
15221
- if (url.protocol !== "https:") {
17288
+ if (url.protocol !== "https:" && !isCimdHttpExemptHost(url.hostname)) {
15222
17289
  return CIMD_METADATA_URL_HTTPS_ERROR;
15223
17290
  }
15224
17291
  if (url.pathname === "/" || url.pathname === "") {
@@ -15234,7 +17301,7 @@ function refineCimdMetadataUrl(val, ctx, required) {
15234
17301
  if (trimmed === "") {
15235
17302
  if (required) {
15236
17303
  ctx.addIssue({
15237
- code: z4.ZodIssueCode.custom,
17304
+ code: z5.ZodIssueCode.custom,
15238
17305
  message: "CIMD client metadata URL is required when CIMD is enabled"
15239
17306
  });
15240
17307
  }
@@ -15243,25 +17310,25 @@ function refineCimdMetadataUrl(val, ctx, required) {
15243
17310
  const error = getCimdClientMetadataUrlError(trimmed);
15244
17311
  if (error) {
15245
17312
  ctx.addIssue({
15246
- code: z4.ZodIssueCode.custom,
17313
+ code: z5.ZodIssueCode.custom,
15247
17314
  message: error
15248
17315
  });
15249
17316
  }
15250
17317
  }
15251
- var CimdConfigSchema = z4.object({
15252
- enabled: z4.boolean(),
15253
- clientMetadataUrl: z4.string()
17318
+ var CimdConfigSchema = z5.object({
17319
+ enabled: z5.boolean(),
17320
+ clientMetadataUrl: z5.string()
15254
17321
  }).superRefine((data, ctx) => {
15255
17322
  refineCimdMetadataUrl(data.clientMetadataUrl, ctx, data.enabled === true);
15256
17323
  });
15257
- var EnterpriseManagedAuthIdpConfigSchema = z4.object({
17324
+ var EnterpriseManagedAuthIdpConfigSchema = z5.object({
15258
17325
  issuer: HttpUrlStringSchema,
15259
- clientId: z4.string().min(1),
15260
- clientSecret: z4.string().optional()
17326
+ clientId: z5.string().min(1),
17327
+ clientSecret: z5.string().optional()
15261
17328
  });
15262
- var ClientConfigSchema = z4.object({
15263
- enterpriseManagedAuth: z4.object({
15264
- enabled: z4.boolean().optional(),
17329
+ var ClientConfigSchema = z5.object({
17330
+ enterpriseManagedAuth: z5.object({
17331
+ enabled: z5.boolean().optional(),
15265
17332
  idp: EnterpriseManagedAuthIdpConfigSchema
15266
17333
  }).optional(),
15267
17334
  cimd: CimdConfigSchema.optional()
@@ -15865,7 +17932,9 @@ function findStoredToken(servers, serverUrl) {
15865
17932
  }
15866
17933
  async function refreshStoredAuthToken(serverUrl, statePath, deps = {}) {
15867
17934
  const refresh = deps.refresh ?? refreshAuthorization;
15868
- const storedAuthFetch = withRfc8414OidcCompat(createProxyFetch() ?? fetch);
17935
+ const storedAuthFetch = withRfc8414OidcCompat(
17936
+ withOAuthRequestTimeout(createProxyFetch() ?? fetch)
17937
+ );
15869
17938
  const discover = deps.discover ?? ((authorizationServerUrl, options) => discoverAuthorizationServerMetadata4(authorizationServerUrl, {
15870
17939
  ...options,
15871
17940
  // A caller-supplied fetch is left alone — it is theirs to compose. The
@@ -16040,7 +18109,13 @@ async function parseArgs(argv) {
16040
18109
  "Tool argument as key=value pair",
16041
18110
  parseKeyValuePair2,
16042
18111
  {}
16043
- ).option("--uri <uri>", "URI of the resource (for resources/read method)").option(
18112
+ ).option(
18113
+ "--uri <uri>",
18114
+ "URI of the resource (resources/read, resources/directory/read) or of the skill (skills/get)"
18115
+ ).option(
18116
+ "--cursor <cursor>",
18117
+ "Opaque pagination cursor (for resources/directory/read; pass back the nextCursor from the previous page)."
18118
+ ).option(
16044
18119
  "--prompt-name <promptName>",
16045
18120
  "Name of the prompt (for prompts/get method)"
16046
18121
  ).option(
@@ -16092,6 +18167,9 @@ async function parseArgs(argv) {
16092
18167
  ).option(
16093
18168
  "--strict",
16094
18169
  "Report tool-schema portability problems in full (path, issue, suggested fix) on stderr, and exit 6 if any is error-severity. Use with --method tools/list. Without it, a one-line count is printed instead."
18170
+ ).option(
18171
+ "--verify",
18172
+ "Run the SEP-2640 conformance and digest checks over the skills returned, emit one JSON report per skill on stdout, and exit 7 if any fails or 8 if any could not be fully checked within the read bounds. Use with --method skills/list or --method skills/get."
16095
18173
  ).option(
16096
18174
  "--connect-timeout <ms>",
16097
18175
  `Connection timeout in ms (default ${DEFAULT_CONNECT_TIMEOUT_MS} for ad-hoc --server-url / target invocations; 0 = no timeout).`,
@@ -16102,6 +18180,10 @@ async function parseArgs(argv) {
16102
18180
  }
16103
18181
  return n;
16104
18182
  }
18183
+ ).option(
18184
+ "--protocol-era <era>",
18185
+ "Protocol era to negotiate: legacy, auto, or modern. Overrides the file-level protocolEra for --catalog/--config runs; ad-hoc --server-url / target runs otherwise default to legacy.",
18186
+ parseProtocolEra
16105
18187
  ).option(
16106
18188
  "--format <format>",
16107
18189
  "Output format: text (default; pretty-printed) or json (one JSON object on stdout, no banners).",
@@ -16195,6 +18277,13 @@ async function parseArgs(argv) {
16195
18277
  );
16196
18278
  }
16197
18279
  }
18280
+ if (options.verify) {
18281
+ if (options.method !== "skills/list" && options.method !== "skills/get") {
18282
+ throw new Error(
18283
+ "--verify requires --method skills/list or --method skills/get."
18284
+ );
18285
+ }
18286
+ }
16198
18287
  const oauthStatePath = getStateFilePath();
16199
18288
  if (options.listStoredAuth) {
16200
18289
  const servers = await readOAuthServers(oauthStatePath);
@@ -16240,7 +18329,10 @@ async function parseArgs(argv) {
16240
18329
  env: options.e,
16241
18330
  // `--header` is merged into the resolved server's settings (overriding any
16242
18331
  // file-level headers); file timeouts/OAuth are preserved. See #1482.
16243
- headers: options.header
18332
+ headers: options.header,
18333
+ // `--protocol-era` feeds `settings.protocolEra` the same way, so an ad-hoc
18334
+ // launch can pick a non-legacy era without an mcp.json entry (#2208).
18335
+ protocolEra: options.protocolEra
16244
18336
  };
16245
18337
  if (options.method === "servers/list") {
16246
18338
  const servers = await listServerEntries(serverOptions);
@@ -16358,6 +18450,8 @@ async function parseArgs(argv) {
16358
18450
  toolMeta: options.toolMetadata,
16359
18451
  appInfo: options.appInfo === true,
16360
18452
  strict: options.strict === true,
18453
+ verify: options.verify === true,
18454
+ cursor: options.cursor,
16361
18455
  format: options.format
16362
18456
  };
16363
18457
  return {