@modelcontextprotocol/inspector 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -579
- package/clients/cli/build/index.js +2805 -161
- package/clients/tui/build/index.js +4282 -1453
- package/clients/web/build/index.js +137 -37
- package/clients/web/dist/assets/index-30pI79Cf.js +1595 -0
- package/clients/web/dist/assets/{index-DQVN73FG.css → index-dqp8Sd8W.css} +1 -1
- package/clients/web/dist/assets/{prism-light-HLcnHJHw.js → prism-light-9SaQe5zv.js} +1 -1
- package/clients/web/dist/index.html +2 -2
- package/package.json +25 -12
- package/clients/web/dist/assets/index-JCsLrWVa.js +0 -1484
- package/clients/web/dist/assets/json-BJ8dCynk.js +0 -1
|
@@ -390,6 +390,495 @@ async function deleteStoreFile(filePath) {
|
|
|
390
390
|
}
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
// ../../core/json/nullableUnion.ts
|
|
394
|
+
var RENDERABLE_TYPES = [
|
|
395
|
+
"string",
|
|
396
|
+
"number",
|
|
397
|
+
"integer",
|
|
398
|
+
"boolean",
|
|
399
|
+
"array",
|
|
400
|
+
"object"
|
|
401
|
+
];
|
|
402
|
+
function isRenderableType(type) {
|
|
403
|
+
return RENDERABLE_TYPES.includes(type);
|
|
404
|
+
}
|
|
405
|
+
function toBranch(value) {
|
|
406
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
function isStringEnum(value) {
|
|
412
|
+
return Array.isArray(value) && value.length > 0 && value.every((member) => typeof member === "string");
|
|
413
|
+
}
|
|
414
|
+
function nullExcludedBySiblings(schema) {
|
|
415
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(null)) {
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
return schema.const !== void 0 && schema.const !== null;
|
|
419
|
+
}
|
|
420
|
+
var ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
421
|
+
"title",
|
|
422
|
+
"description",
|
|
423
|
+
"default",
|
|
424
|
+
"enumNames",
|
|
425
|
+
"examples",
|
|
426
|
+
"deprecated",
|
|
427
|
+
"readOnly",
|
|
428
|
+
"writeOnly",
|
|
429
|
+
"$comment",
|
|
430
|
+
"$schema",
|
|
431
|
+
"$id"
|
|
432
|
+
]);
|
|
433
|
+
function wrapperCarriesConstraints(schema) {
|
|
434
|
+
return Object.keys(schema).some(
|
|
435
|
+
(key) => key !== "anyOf" && !ANNOTATION_KEYWORDS.has(key)
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
function typeNamesNull(type) {
|
|
439
|
+
return Array.isArray(type) ? type.includes("null") : type === "null";
|
|
440
|
+
}
|
|
441
|
+
function hasOpaqueApplicator(schema) {
|
|
442
|
+
return schema.not !== void 0 || schema.allOf !== void 0 || schema.oneOf !== void 0 || // `$ref` points at a schema this module never resolves, and it applies
|
|
443
|
+
// *alongside* its siblings — `{ type: "string", $ref: "#/$defs/intOnly" }`
|
|
444
|
+
// can be unsatisfiable. The wrapper path already declines on it (it is not
|
|
445
|
+
// an annotation); this is the same rule for every other position.
|
|
446
|
+
schema.$ref !== void 0;
|
|
447
|
+
}
|
|
448
|
+
function hasUnevaluatedComposition(schema) {
|
|
449
|
+
return schema.anyOf !== void 0 || hasOpaqueApplicator(schema);
|
|
450
|
+
}
|
|
451
|
+
function stripNullEnumMembers(members, names) {
|
|
452
|
+
if (!Array.isArray(members)) {
|
|
453
|
+
return { kind: "unchanged" };
|
|
454
|
+
}
|
|
455
|
+
const kept = members.map((member, index) => ({ member, index })).filter((entry) => entry.member !== null);
|
|
456
|
+
if (kept.length === 0) {
|
|
457
|
+
return { kind: "unselectable" };
|
|
458
|
+
}
|
|
459
|
+
if (kept.length === members.length) {
|
|
460
|
+
return { kind: "unchanged" };
|
|
461
|
+
}
|
|
462
|
+
const wasParallel = Array.isArray(names) && names.length === members.length;
|
|
463
|
+
const aligned = wasParallel ? kept.map((entry) => names[entry.index]) : void 0;
|
|
464
|
+
return {
|
|
465
|
+
kind: "filtered",
|
|
466
|
+
members: kept.map((entry) => entry.member),
|
|
467
|
+
names: aligned
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
function collapsed(schema, branch, type) {
|
|
471
|
+
const merged = { ...schema, ...branch };
|
|
472
|
+
const strip = stripNullEnumMembers(
|
|
473
|
+
merged.enum,
|
|
474
|
+
merged.enumNames
|
|
475
|
+
);
|
|
476
|
+
if (strip.kind === "unselectable") {
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
return {
|
|
480
|
+
...merged,
|
|
481
|
+
type,
|
|
482
|
+
anyOf: void 0,
|
|
483
|
+
// Read off the ORIGINAL schema, not the merge: a hoisted branch `enum` is
|
|
484
|
+
// not a sibling constraint, and treating it as one would mark the very
|
|
485
|
+
// shape #1928 is about as non-nullable. See `nullExcludedBySiblings`.
|
|
486
|
+
nullable: admitsNull(schema),
|
|
487
|
+
...strip.kind === "filtered" ? { enum: strip.members, enumNames: strip.names } : {}
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function typeAdmitsNull(type) {
|
|
491
|
+
return type === void 0 || typeNamesNull(type);
|
|
492
|
+
}
|
|
493
|
+
function admitsNull(schema) {
|
|
494
|
+
if (nullExcludedBySiblings(schema)) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
if (hasOpaqueApplicator(schema)) {
|
|
498
|
+
return false;
|
|
499
|
+
}
|
|
500
|
+
if (schema.const === null) {
|
|
501
|
+
if (schema.nullable !== true && !typeAdmitsNull(schema.type)) return false;
|
|
502
|
+
return schema.anyOf === void 0 || anyOfAdmitsNull(schema);
|
|
503
|
+
}
|
|
504
|
+
if (Array.isArray(schema.enum) && schema.enum.includes(null)) {
|
|
505
|
+
return (schema.nullable === true || typeAdmitsNull(schema.type)) && (schema.anyOf === void 0 || anyOfAdmitsNull(schema));
|
|
506
|
+
}
|
|
507
|
+
if (schema.nullable === true) {
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
if (schema.type !== void 0) {
|
|
511
|
+
if (schema.anyOf !== void 0) {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
return typeNamesNull(schema.type);
|
|
515
|
+
}
|
|
516
|
+
return anyOfAdmitsNull(schema);
|
|
517
|
+
}
|
|
518
|
+
var ANNOTATION_ONLY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
519
|
+
"title",
|
|
520
|
+
"description",
|
|
521
|
+
"examples",
|
|
522
|
+
"default",
|
|
523
|
+
"deprecated",
|
|
524
|
+
"readOnly",
|
|
525
|
+
"writeOnly",
|
|
526
|
+
"$comment",
|
|
527
|
+
// Core keywords that identify or declare rather than assert. `$ref` is
|
|
528
|
+
// deliberately absent: it applies whatever it points at, which is exactly an
|
|
529
|
+
// assertion this module cannot see.
|
|
530
|
+
"$defs",
|
|
531
|
+
"$id",
|
|
532
|
+
"$schema",
|
|
533
|
+
"$anchor",
|
|
534
|
+
"$dynamicAnchor",
|
|
535
|
+
"$vocabulary"
|
|
536
|
+
]);
|
|
537
|
+
function constrainsNothing(schema) {
|
|
538
|
+
return Object.keys(schema).every(
|
|
539
|
+
(keyword) => ANNOTATION_ONLY_KEYWORDS.has(keyword)
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
function anyOfAdmitsNull(schema) {
|
|
543
|
+
const branches = schema.anyOf;
|
|
544
|
+
if (!Array.isArray(branches)) return false;
|
|
545
|
+
return branches.some((entry) => {
|
|
546
|
+
if (typeof entry === "boolean") return entry;
|
|
547
|
+
const branch = toBranch(entry);
|
|
548
|
+
if (branch === null) return false;
|
|
549
|
+
const branchSchema = branch;
|
|
550
|
+
if (hasUnevaluatedComposition(branchSchema)) return false;
|
|
551
|
+
if (constrainsNothing(branchSchema)) return true;
|
|
552
|
+
return admitsNull(branchSchema);
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
function normalizeNullableUnion(schema) {
|
|
556
|
+
if (schema.anyOf?.length === 2) {
|
|
557
|
+
const branches = schema.anyOf.map(toBranch);
|
|
558
|
+
const nullBranch = branches.find((entry) => entry?.type === "null");
|
|
559
|
+
const branch = branches.find(
|
|
560
|
+
(entry) => entry !== null && entry.type !== "null"
|
|
561
|
+
);
|
|
562
|
+
if (nullBranch && branch && !wrapperCarriesConstraints(schema) && !hasUnevaluatedComposition(branch)) {
|
|
563
|
+
const type = branch.type ?? (isStringEnum(branch.enum) ? "string" : void 0);
|
|
564
|
+
if (isRenderableType(type)) {
|
|
565
|
+
const result = collapsed(schema, branch, type);
|
|
566
|
+
if (result !== null) {
|
|
567
|
+
return result;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (Array.isArray(schema.type) && schema.type.length === 2 && schema.type.includes("null") && // This path keeps the schema's own keywords, but `collapsed` clears `anyOf`
|
|
573
|
+
// and the renderers ignore the other applicators — so a compound schema
|
|
574
|
+
// like `{ type: ["string", "null"], anyOf: [{ const: "a" }] }` would be
|
|
575
|
+
// widened into an unconstrained string field. Leave it for the JSON editor.
|
|
576
|
+
!hasUnevaluatedComposition(schema)) {
|
|
577
|
+
const type = schema.type.find((member) => member !== "null");
|
|
578
|
+
if (isRenderableType(type)) {
|
|
579
|
+
const result = collapsed(schema, void 0, type);
|
|
580
|
+
if (result !== null) {
|
|
581
|
+
return result;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return schema;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ../../core/json/rootUnion.ts
|
|
589
|
+
function membersOf(value) {
|
|
590
|
+
return Array.isArray(value) ? value : [];
|
|
591
|
+
}
|
|
592
|
+
function requiredOf(schema) {
|
|
593
|
+
const { required } = schema;
|
|
594
|
+
return Array.isArray(required) ? required.filter((name) => typeof name === "string") : [];
|
|
595
|
+
}
|
|
596
|
+
function toBranch2(value) {
|
|
597
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
602
|
+
function admitsObject(schema) {
|
|
603
|
+
const { type } = schema;
|
|
604
|
+
if (type === void 0) return true;
|
|
605
|
+
return Array.isArray(type) ? type.includes("object") : type === "object";
|
|
606
|
+
}
|
|
607
|
+
function hasReadableProperties(schema) {
|
|
608
|
+
const { properties } = schema;
|
|
609
|
+
return typeof properties === "object" && properties !== null && !Array.isArray(properties);
|
|
610
|
+
}
|
|
611
|
+
function propertiesOf(schema) {
|
|
612
|
+
return hasReadableProperties(schema) ? schema.properties : {};
|
|
613
|
+
}
|
|
614
|
+
function isOfferable(branch, merged) {
|
|
615
|
+
if (branch.properties !== void 0 && !hasReadableProperties(branch)) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
if (!admitsObject(branch)) return false;
|
|
619
|
+
const mergedProperties = propertiesOf(merged);
|
|
620
|
+
if (requiredOf(merged).some((name) => !Object.hasOwn(mergedProperties, name))) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
const properties = Object.values(mergedProperties);
|
|
624
|
+
return properties.length > 0 && // Every value has to be something a renderer can read AND something a
|
|
625
|
+
// caller can satisfy. A `null` or an array is not a schema at all, and the
|
|
626
|
+
// web form dereferences one on the way to choosing a widget. JSON Schema's
|
|
627
|
+
// boolean form is legal, but only `true` is harmless — it constrains
|
|
628
|
+
// nothing and answers every keyword lookup with `undefined`, while `false`
|
|
629
|
+
// admits no value whatsoever, so a field declared with it can never be
|
|
630
|
+
// filled and a required one makes the whole branch unsatisfiable. Either
|
|
631
|
+
// way the branch is declined rather than offered as a callable shape.
|
|
632
|
+
properties.every(
|
|
633
|
+
(property) => property === true || typeof property === "object" && property !== null && !Array.isArray(property)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
var ANNOTATION_KEYWORDS2 = /* @__PURE__ */ new Set([
|
|
637
|
+
"title",
|
|
638
|
+
"description",
|
|
639
|
+
"examples",
|
|
640
|
+
"deprecated",
|
|
641
|
+
"readOnly",
|
|
642
|
+
"writeOnly",
|
|
643
|
+
"$comment",
|
|
644
|
+
// An annotation too: a suggested initial value constrains nothing, so two
|
|
645
|
+
// declarations suggesting different ones still accept the same values.
|
|
646
|
+
"default"
|
|
647
|
+
]);
|
|
648
|
+
function canonicalJson(value) {
|
|
649
|
+
if (Array.isArray(value)) {
|
|
650
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
651
|
+
}
|
|
652
|
+
if (typeof value === "object" && value !== null) {
|
|
653
|
+
const entries = Object.entries(value).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(
|
|
654
|
+
([key, member]) => `${JSON.stringify(key)}:${canonicalJson(member)}`
|
|
655
|
+
);
|
|
656
|
+
return `{${entries.join(",")}}`;
|
|
657
|
+
}
|
|
658
|
+
return JSON.stringify(value) ?? "undefined";
|
|
659
|
+
}
|
|
660
|
+
function sameJsonValue(a, b) {
|
|
661
|
+
return sameValue(a, b);
|
|
662
|
+
}
|
|
663
|
+
function sameValue(a, b) {
|
|
664
|
+
return a === b || canonicalJson(a) === canonicalJson(b);
|
|
665
|
+
}
|
|
666
|
+
function conflicts(baseProperty, branchProperty) {
|
|
667
|
+
const a = toBranch2(baseProperty);
|
|
668
|
+
const b = toBranch2(branchProperty);
|
|
669
|
+
if (a === null || b === null) {
|
|
670
|
+
return !sameValue(baseProperty, branchProperty);
|
|
671
|
+
}
|
|
672
|
+
const left = a;
|
|
673
|
+
const right = b;
|
|
674
|
+
if (Object.keys(right).some(
|
|
675
|
+
(keyword) => !ANNOTATION_KEYWORDS2.has(keyword) && keyword in left && !sameValue(left[keyword], right[keyword])
|
|
676
|
+
)) {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
return !constSatisfiesSiblings({ ...left, ...right });
|
|
680
|
+
}
|
|
681
|
+
function jsonTypeOf(value) {
|
|
682
|
+
if (value === null) return "null";
|
|
683
|
+
if (Array.isArray(value)) return "array";
|
|
684
|
+
if (typeof value === "number") {
|
|
685
|
+
return Number.isInteger(value) ? "integer" : "number";
|
|
686
|
+
}
|
|
687
|
+
return typeof value;
|
|
688
|
+
}
|
|
689
|
+
var CONST_CHECKABLE = /* @__PURE__ */ new Set(["const", "type", "enum"]);
|
|
690
|
+
function constSatisfiesSiblings(schema) {
|
|
691
|
+
if (!("const" in schema)) return true;
|
|
692
|
+
const value = schema.const;
|
|
693
|
+
const { type, enum: allowed } = schema;
|
|
694
|
+
if (Object.keys(schema).some(
|
|
695
|
+
(keyword) => !CONST_CHECKABLE.has(keyword) && !ANNOTATION_KEYWORDS2.has(keyword)
|
|
696
|
+
)) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
const actual = jsonTypeOf(value);
|
|
700
|
+
const admits = (name) => name === actual || name === "number" && actual === "integer";
|
|
701
|
+
if (typeof type === "string" && !admits(type)) return false;
|
|
702
|
+
if (Array.isArray(type) && !type.some(admits)) return false;
|
|
703
|
+
if (Array.isArray(allowed) && !allowed.some((member) => sameValue(member, value))) {
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
function mergeProperty(baseProperty, branchProperty) {
|
|
709
|
+
const a = toBranch2(baseProperty);
|
|
710
|
+
const b = toBranch2(branchProperty);
|
|
711
|
+
if (a === null || b === null) {
|
|
712
|
+
return branchProperty;
|
|
713
|
+
}
|
|
714
|
+
return { ...a, ...b };
|
|
715
|
+
}
|
|
716
|
+
var MERGEABLE_KEYWORDS = /* @__PURE__ */ new Set(["type", "properties", "required"]);
|
|
717
|
+
function isFlattenable(member) {
|
|
718
|
+
const branch = toBranch2(member);
|
|
719
|
+
if (branch === null || !admitsObject(branch)) return false;
|
|
720
|
+
return Object.keys(branch).every(
|
|
721
|
+
(keyword) => MERGEABLE_KEYWORDS.has(keyword) || ANNOTATION_KEYWORDS2.has(keyword)
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
function conflictsWithBase(base, branch) {
|
|
725
|
+
const baseProperties = propertiesOf(base);
|
|
726
|
+
const branchProperties = propertiesOf(branch);
|
|
727
|
+
return Object.entries(branchProperties).some(
|
|
728
|
+
([name, branchProperty]) => (
|
|
729
|
+
// `hasOwn`, not `in`: `properties` is a JSON record, so `constructor` and
|
|
730
|
+
// `toString` are legal argument names that `in` would find on
|
|
731
|
+
// `Object.prototype` and report as collisions the root never declared.
|
|
732
|
+
Object.hasOwn(baseProperties, name) && conflicts(baseProperties[name], branchProperty)
|
|
733
|
+
)
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
function hasDiscriminator(members, named, rootRequired) {
|
|
737
|
+
const first = propertiesOf(members[0] ?? {});
|
|
738
|
+
const candidates = named !== void 0 ? [named] : Object.keys(first);
|
|
739
|
+
return candidates.some((name) => {
|
|
740
|
+
const constants = members.map((member) => {
|
|
741
|
+
if (!rootRequired.includes(name) && !requiredOf(member).includes(name)) {
|
|
742
|
+
return void 0;
|
|
743
|
+
}
|
|
744
|
+
const property = toBranch2(propertiesOf(member)[name]);
|
|
745
|
+
return property?.const;
|
|
746
|
+
});
|
|
747
|
+
if (constants.some((value) => value === void 0)) return false;
|
|
748
|
+
const seen = new Set(constants.map(canonicalJson));
|
|
749
|
+
return seen.size === constants.length;
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
function restrictsAdditional(schema) {
|
|
753
|
+
const additional = schema.additionalProperties;
|
|
754
|
+
return additional === false || typeof additional === "object" && additional !== null && Object.keys(additional).some(
|
|
755
|
+
(keyword) => !ANNOTATION_KEYWORDS2.has(keyword)
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
function addsForbiddenNames(base, member) {
|
|
759
|
+
if (!restrictsAdditional(base)) return false;
|
|
760
|
+
const baseNames = propertiesOf(base);
|
|
761
|
+
return Object.keys(propertiesOf(member)).some(
|
|
762
|
+
(name) => !Object.hasOwn(baseNames, name)
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
function mergeBranch(base, branch) {
|
|
766
|
+
const baseProperties = propertiesOf(base);
|
|
767
|
+
const branchProperties = propertiesOf(branch);
|
|
768
|
+
const properties = Object.fromEntries([
|
|
769
|
+
...Object.entries(baseProperties),
|
|
770
|
+
...Object.entries(branchProperties).map(([name, branchProperty]) => [
|
|
771
|
+
name,
|
|
772
|
+
Object.hasOwn(baseProperties, name) ? mergeProperty(baseProperties[name], branchProperty) : branchProperty
|
|
773
|
+
])
|
|
774
|
+
]);
|
|
775
|
+
const baseRequired = requiredOf(base);
|
|
776
|
+
const required = [
|
|
777
|
+
...baseRequired,
|
|
778
|
+
...requiredOf(branch).filter((name) => !baseRequired.includes(name))
|
|
779
|
+
];
|
|
780
|
+
return {
|
|
781
|
+
...base,
|
|
782
|
+
...Object.keys(properties).length > 0 ? { properties } : {},
|
|
783
|
+
...required.length > 0 ? { required } : {}
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
function withoutComposition(schema) {
|
|
787
|
+
const { allOf: _allOf, anyOf: _anyOf, oneOf: _oneOf, ...rest } = schema;
|
|
788
|
+
return rest;
|
|
789
|
+
}
|
|
790
|
+
function branchLabel(branch, index, discriminatorProperty) {
|
|
791
|
+
if (typeof branch.title === "string" && branch.title.trim() !== "") {
|
|
792
|
+
return branch.title;
|
|
793
|
+
}
|
|
794
|
+
const properties = propertiesOf(branch);
|
|
795
|
+
const constOf = (name) => {
|
|
796
|
+
const property = toBranch2(properties[name]);
|
|
797
|
+
if (property === null || !("const" in property)) return void 0;
|
|
798
|
+
const value = property.const;
|
|
799
|
+
return value === null || typeof value !== "object" ? String(value) : void 0;
|
|
800
|
+
};
|
|
801
|
+
if (discriminatorProperty !== void 0) {
|
|
802
|
+
const value = constOf(discriminatorProperty);
|
|
803
|
+
if (value !== void 0) return value;
|
|
804
|
+
}
|
|
805
|
+
const constants = Object.keys(properties).map((name) => constOf(name)).filter((value) => value !== void 0);
|
|
806
|
+
if (constants.length === 1) return constants[0];
|
|
807
|
+
return `Option ${index + 1}`;
|
|
808
|
+
}
|
|
809
|
+
function resolveRootUnion(schema) {
|
|
810
|
+
const allOfMembers = membersOf(schema.allOf);
|
|
811
|
+
let merged = schema;
|
|
812
|
+
for (const member of allOfMembers) {
|
|
813
|
+
const branch = toBranch2(member);
|
|
814
|
+
if (branch === null || !isFlattenable(member) || conflictsWithBase(merged, branch) || addsForbiddenNames(merged, branch)) {
|
|
815
|
+
return { base: schema, branches: [] };
|
|
816
|
+
}
|
|
817
|
+
merged = mergeBranch(merged, branch);
|
|
818
|
+
}
|
|
819
|
+
const base = withoutComposition(merged);
|
|
820
|
+
if (schema.oneOf !== void 0 && schema.anyOf !== void 0) {
|
|
821
|
+
return { base, branches: [] };
|
|
822
|
+
}
|
|
823
|
+
const isExclusiveUnion = schema.oneOf !== void 0;
|
|
824
|
+
const members = membersOf(schema.oneOf ?? schema.anyOf);
|
|
825
|
+
const branches = members.map(toBranch2);
|
|
826
|
+
if (branches.length === 0 || branches.some(
|
|
827
|
+
(branch) => branch === null || !isOfferable(branch, mergeBranch(base, branch)) || // The same faithfulness test the `allOf` fold applies: a member
|
|
828
|
+
// carrying a constraint the merge does not copy — a nested `allOf`, a
|
|
829
|
+
// `not`, a `$ref` — would have it erased along with the union keyword,
|
|
830
|
+
// which can present an unsatisfiable branch as a callable one.
|
|
831
|
+
!isFlattenable(branch) || conflictsWithBase(base, branch)
|
|
832
|
+
)) {
|
|
833
|
+
return { base, branches: [] };
|
|
834
|
+
}
|
|
835
|
+
if (branches.some(
|
|
836
|
+
(branch) => addsForbiddenNames(base, branch)
|
|
837
|
+
)) {
|
|
838
|
+
return { base, branches: [] };
|
|
839
|
+
}
|
|
840
|
+
if (isExclusiveUnion && !hasDiscriminator(
|
|
841
|
+
branches,
|
|
842
|
+
schema.discriminator?.propertyName,
|
|
843
|
+
requiredOf(base)
|
|
844
|
+
)) {
|
|
845
|
+
return { base, branches: [] };
|
|
846
|
+
}
|
|
847
|
+
const discriminatorProperty = schema.discriminator?.propertyName;
|
|
848
|
+
return {
|
|
849
|
+
base,
|
|
850
|
+
branches: branches.filter((branch) => branch !== null).map((branch, index) => ({
|
|
851
|
+
// `base` is already composition-free and `mergeBranch` copies only
|
|
852
|
+
// `properties`/`required` off the branch, so the merge stays that way.
|
|
853
|
+
schema: mergeBranch(base, branch),
|
|
854
|
+
label: branchLabel(branch, index, discriminatorProperty),
|
|
855
|
+
declaredFields: Object.keys(propertiesOf(branch))
|
|
856
|
+
}))
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function narrowBySuppliedNames(branches, candidates, suppliedNames) {
|
|
860
|
+
if (candidates.length === 1) return candidates[0];
|
|
861
|
+
if (candidates.length === 0 || suppliedNames.length === 0) return null;
|
|
862
|
+
const supplied = new Set(suppliedNames);
|
|
863
|
+
const satisfied = candidates.filter((index) => {
|
|
864
|
+
const branch = branches[index];
|
|
865
|
+
const required = requiredOf(branch.schema);
|
|
866
|
+
const own = required.filter((name) => branch.declaredFields.includes(name));
|
|
867
|
+
return own.length > 0 && own.every((name) => supplied.has(name));
|
|
868
|
+
});
|
|
869
|
+
if (satisfied.length === 1) return satisfied[0];
|
|
870
|
+
const exclusiveTo = /* @__PURE__ */ new Map();
|
|
871
|
+
for (const index of candidates) {
|
|
872
|
+
for (const name of branches[index].declaredFields) {
|
|
873
|
+
exclusiveTo.set(name, exclusiveTo.has(name) ? -1 : index);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const named = new Set(
|
|
877
|
+
suppliedNames.map((name) => exclusiveTo.get(name)).filter((index) => index !== void 0 && index >= 0)
|
|
878
|
+
);
|
|
879
|
+
return named.size === 1 ? [...named][0] : null;
|
|
880
|
+
}
|
|
881
|
+
|
|
393
882
|
// ../../core/json/jsonUtils.ts
|
|
394
883
|
function isSerializableJson(value) {
|
|
395
884
|
if (value === null) return true;
|
|
@@ -429,16 +918,90 @@ function convertParameterValue(value, schema) {
|
|
|
429
918
|
}
|
|
430
919
|
return value;
|
|
431
920
|
}
|
|
921
|
+
function coercionProperties(base, branches, params) {
|
|
922
|
+
if (branches.length === 0) {
|
|
923
|
+
return { ...base.properties };
|
|
924
|
+
}
|
|
925
|
+
const candidates = branches.map((branch, index) => ({ branch, index })).filter(
|
|
926
|
+
({ branch }) => matchesConstants(branch.schema.properties ?? {}, params)
|
|
927
|
+
).map(({ index }) => index);
|
|
928
|
+
const selected = narrowBySuppliedNames(
|
|
929
|
+
branches,
|
|
930
|
+
candidates,
|
|
931
|
+
Object.keys(params)
|
|
932
|
+
);
|
|
933
|
+
if (selected !== null) {
|
|
934
|
+
return { ...branches[selected].schema.properties };
|
|
935
|
+
}
|
|
936
|
+
const properties = Object.fromEntries(
|
|
937
|
+
Object.entries(base.properties ?? {})
|
|
938
|
+
);
|
|
939
|
+
const pool = candidates.length > 0 ? candidates.map((i) => branches[i]) : branches;
|
|
940
|
+
for (const name of new Set(pool.flatMap((b) => b.declaredFields))) {
|
|
941
|
+
const declarations = pool.filter((branch) => branch.declaredFields.includes(name)).map((branch) => branch.schema.properties?.[name]).filter((schema) => typeof schema === "object" && schema !== null).map((schema) => normalizeNullableUnion(schema));
|
|
942
|
+
const types = new Set(declarations.map((schema) => typeNameOf(schema)));
|
|
943
|
+
const pinnedOf = (schema) => typeof schema === "object" && schema !== null && "const" in schema ? schema.const : void 0;
|
|
944
|
+
const constsAgree = declarations.every(
|
|
945
|
+
(schema) => sameJsonValue(pinnedOf(schema), pinnedOf(declarations[0]))
|
|
946
|
+
);
|
|
947
|
+
if (types.size === 1 && constsAgree && declarations.length > 0) {
|
|
948
|
+
Object.defineProperty(properties, name, {
|
|
949
|
+
value: declarations[0],
|
|
950
|
+
writable: true,
|
|
951
|
+
enumerable: true,
|
|
952
|
+
configurable: true
|
|
953
|
+
});
|
|
954
|
+
} else {
|
|
955
|
+
delete properties[name];
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
return properties;
|
|
959
|
+
}
|
|
960
|
+
function typeNameOf(schema) {
|
|
961
|
+
if (typeof schema !== "object" || schema === null) return "";
|
|
962
|
+
const { type } = schema;
|
|
963
|
+
return Array.isArray(type) ? [...type].map(String).sort().join(",") : typeof type === "string" ? type : "";
|
|
964
|
+
}
|
|
965
|
+
function matchesConstants(properties, params) {
|
|
966
|
+
return Object.entries(properties).every(([name, schema]) => {
|
|
967
|
+
if (typeof schema !== "object" || schema === null) return true;
|
|
968
|
+
const constValue = schema.const;
|
|
969
|
+
if (constValue === void 0) return true;
|
|
970
|
+
if (!Object.hasOwn(params, name)) return true;
|
|
971
|
+
const supplied = params[name];
|
|
972
|
+
return supplied === void 0 || suppliedMatchesConst(supplied, constValue);
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
function suppliedMatchesConst(value, constValue) {
|
|
976
|
+
if (constValue === null || typeof constValue !== "object") {
|
|
977
|
+
return value === String(constValue);
|
|
978
|
+
}
|
|
979
|
+
try {
|
|
980
|
+
return sameJsonValue(JSON.parse(value), constValue);
|
|
981
|
+
} catch {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
}
|
|
432
985
|
function convertToolParameters(tool, params) {
|
|
986
|
+
return convertParametersForSchema(tool.inputSchema, params);
|
|
987
|
+
}
|
|
988
|
+
function convertParametersForSchema(inputSchema, params) {
|
|
433
989
|
const result = {};
|
|
434
|
-
const
|
|
990
|
+
const { base, branches } = resolveRootUnion(
|
|
991
|
+
inputSchema ?? {}
|
|
992
|
+
);
|
|
993
|
+
const properties = coercionProperties(base, branches, params);
|
|
435
994
|
for (const [key, value] of Object.entries(params)) {
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
995
|
+
const declared = properties[key];
|
|
996
|
+
const paramSchema = typeof declared === "object" && declared !== null ? normalizeNullableUnion(declared) : declared;
|
|
997
|
+
const pinned = paramSchema?.const;
|
|
998
|
+
const converted = pinned !== void 0 && suppliedMatchesConst(value, pinned) ? pinned : paramSchema ? convertParameterValue(value, paramSchema) : value;
|
|
999
|
+
Object.defineProperty(result, key, {
|
|
1000
|
+
value: converted,
|
|
1001
|
+
writable: true,
|
|
1002
|
+
enumerable: true,
|
|
1003
|
+
configurable: true
|
|
1004
|
+
});
|
|
442
1005
|
}
|
|
443
1006
|
return result;
|
|
444
1007
|
}
|
|
@@ -675,6 +1238,9 @@ function storedFieldsToInspectorSettings(stored) {
|
|
|
675
1238
|
if (stored.oauth?.requestRefreshToken === false) {
|
|
676
1239
|
settings.oauthRequestRefreshToken = false;
|
|
677
1240
|
}
|
|
1241
|
+
if (stored.oauth?.revokeOnClear === false) {
|
|
1242
|
+
settings.oauthRevokeOnClear = false;
|
|
1243
|
+
}
|
|
678
1244
|
if (stored.cwd) settings.cwd = stored.cwd;
|
|
679
1245
|
return settings;
|
|
680
1246
|
}
|
|
@@ -3460,6 +4026,25 @@ var OAuthStorageBase = class {
|
|
|
3460
4026
|
this.memory.getState().setServerState(serverUrl, { discoveryState: void 0 });
|
|
3461
4027
|
await this.persist();
|
|
3462
4028
|
}
|
|
4029
|
+
async takeRevocationSnapshot(serverUrl) {
|
|
4030
|
+
await this.ensureLoaded();
|
|
4031
|
+
const state = this.memory.getState().getServerState(serverUrl);
|
|
4032
|
+
const snapshot = {
|
|
4033
|
+
byIssuer: Object.fromEntries(
|
|
4034
|
+
Object.entries(state.byIssuer ?? {}).map(([issuer, slot]) => [
|
|
4035
|
+
issuer,
|
|
4036
|
+
{ tokens: slot?.tokens, clientInformation: slot?.clientInformation }
|
|
4037
|
+
])
|
|
4038
|
+
),
|
|
4039
|
+
legacyTokens: state.tokens,
|
|
4040
|
+
legacyClientInformation: state.clientInformation,
|
|
4041
|
+
preregisteredClientInformation: state.preregisteredClientInformation,
|
|
4042
|
+
serverMetadata: state.serverMetadata
|
|
4043
|
+
};
|
|
4044
|
+
this.memory.getState().clearServerState(serverUrl);
|
|
4045
|
+
await this.persist();
|
|
4046
|
+
return snapshot;
|
|
4047
|
+
}
|
|
3463
4048
|
async clear(serverUrl) {
|
|
3464
4049
|
await this.ensureLoaded();
|
|
3465
4050
|
this.memory.getState().clearServerState(serverUrl);
|
|
@@ -3650,6 +4235,292 @@ var NodeOAuthStorage = class extends OAuthStorageBase {
|
|
|
3650
4235
|
}
|
|
3651
4236
|
};
|
|
3652
4237
|
|
|
4238
|
+
// ../../core/auth/revocation.ts
|
|
4239
|
+
import { selectClientAuthMethod } from "@modelcontextprotocol/client";
|
|
4240
|
+
import {
|
|
4241
|
+
OAuthClientInformationSchema as OAuthClientInformationSchema2,
|
|
4242
|
+
OAuthTokensSchema as OAuthTokensSchema2
|
|
4243
|
+
} from "@modelcontextprotocol/core";
|
|
4244
|
+
var DEFAULT_REVOCATION_TIMEOUT_MS = 5e3;
|
|
4245
|
+
var MIN_REVOCATION_REQUEST_BUDGET_MS = 5;
|
|
4246
|
+
function selectRevocableToken(tokens) {
|
|
4247
|
+
if (!tokens) return null;
|
|
4248
|
+
if (tokens.refresh_token) {
|
|
4249
|
+
return { token: tokens.refresh_token, tokenTypeHint: "refresh_token" };
|
|
4250
|
+
}
|
|
4251
|
+
if (tokens.access_token) {
|
|
4252
|
+
return { token: tokens.access_token, tokenTypeHint: "access_token" };
|
|
4253
|
+
}
|
|
4254
|
+
return null;
|
|
4255
|
+
}
|
|
4256
|
+
function revocationAuthMethods(metadata) {
|
|
4257
|
+
return metadata.revocation_endpoint_auth_methods_supported ?? [
|
|
4258
|
+
"client_secret_basic"
|
|
4259
|
+
];
|
|
4260
|
+
}
|
|
4261
|
+
function buildRevocationRequest(params) {
|
|
4262
|
+
const body = new URLSearchParams({
|
|
4263
|
+
token: params.token,
|
|
4264
|
+
token_type_hint: params.tokenTypeHint
|
|
4265
|
+
});
|
|
4266
|
+
const headers = {
|
|
4267
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
4268
|
+
Accept: "application/json"
|
|
4269
|
+
};
|
|
4270
|
+
const client = params.clientInformation;
|
|
4271
|
+
if (client) {
|
|
4272
|
+
const method = selectClientAuthMethod(client, params.supportedAuthMethods);
|
|
4273
|
+
if (method === "client_secret_basic" && client.client_secret) {
|
|
4274
|
+
const credentials = `${encodeURIComponent(client.client_id)}:${encodeURIComponent(client.client_secret)}`;
|
|
4275
|
+
headers.Authorization = `Basic ${base64Encode(credentials)}`;
|
|
4276
|
+
} else if (method === "client_secret_post" && client.client_secret) {
|
|
4277
|
+
body.set("client_id", client.client_id);
|
|
4278
|
+
body.set("client_secret", client.client_secret);
|
|
4279
|
+
} else {
|
|
4280
|
+
body.set("client_id", client.client_id);
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
return {
|
|
4284
|
+
url: params.endpoint,
|
|
4285
|
+
init: { method: "POST", headers, body: body.toString() }
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
function base64Encode(value) {
|
|
4289
|
+
if (typeof btoa === "function") return btoa(value);
|
|
4290
|
+
return Buffer.from(value, "utf8").toString("base64");
|
|
4291
|
+
}
|
|
4292
|
+
async function revokeToken(params) {
|
|
4293
|
+
const timeoutMs = Math.max(
|
|
4294
|
+
0,
|
|
4295
|
+
Math.round(params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS)
|
|
4296
|
+
);
|
|
4297
|
+
try {
|
|
4298
|
+
const { url, init } = buildRevocationRequest(params);
|
|
4299
|
+
const response = await withDeadline(
|
|
4300
|
+
params.fetchFn(url, { ...init, signal: AbortSignal.timeout(timeoutMs) }),
|
|
4301
|
+
timeoutMs
|
|
4302
|
+
);
|
|
4303
|
+
if (!response.ok) {
|
|
4304
|
+
return {
|
|
4305
|
+
status: "failed",
|
|
4306
|
+
endpoint: url,
|
|
4307
|
+
detail: `revocation endpoint responded ${response.status} ${response.statusText}`.trim()
|
|
4308
|
+
};
|
|
4309
|
+
}
|
|
4310
|
+
return {
|
|
4311
|
+
status: "revoked",
|
|
4312
|
+
tokenTypeHint: params.tokenTypeHint,
|
|
4313
|
+
endpoint: url
|
|
4314
|
+
};
|
|
4315
|
+
} catch (err) {
|
|
4316
|
+
return {
|
|
4317
|
+
// `params.endpoint`, not the built `url`: construction is inside the try
|
|
4318
|
+
// now, so `url` may not exist on this path.
|
|
4319
|
+
status: "failed",
|
|
4320
|
+
endpoint: params.endpoint,
|
|
4321
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
4322
|
+
};
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
async function withDeadline(promise, timeoutMs) {
|
|
4326
|
+
let timer;
|
|
4327
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
4328
|
+
timer = setTimeout(
|
|
4329
|
+
() => reject(new Error(`revocation request timed out after ${timeoutMs}ms`)),
|
|
4330
|
+
timeoutMs
|
|
4331
|
+
);
|
|
4332
|
+
});
|
|
4333
|
+
try {
|
|
4334
|
+
return await Promise.race([promise, deadline]);
|
|
4335
|
+
} finally {
|
|
4336
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
function emptyPlan(serverUrl, outcome, failures = []) {
|
|
4340
|
+
return {
|
|
4341
|
+
serverUrl,
|
|
4342
|
+
grants: [],
|
|
4343
|
+
failures,
|
|
4344
|
+
supportedAuthMethods: [],
|
|
4345
|
+
outcome: failures.length > 0 ? aggregateOutcomes([...failures, outcome]) : outcome
|
|
4346
|
+
};
|
|
4347
|
+
}
|
|
4348
|
+
async function clearAndPlanRevocation(params) {
|
|
4349
|
+
const { serverUrl, storage } = params;
|
|
4350
|
+
let snapshot;
|
|
4351
|
+
try {
|
|
4352
|
+
snapshot = await storage.takeRevocationSnapshot(serverUrl);
|
|
4353
|
+
} catch (err) {
|
|
4354
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
4355
|
+
}
|
|
4356
|
+
if (params.enabled === false) {
|
|
4357
|
+
return emptyPlan(serverUrl, { status: "skipped", reason: "disabled" });
|
|
4358
|
+
}
|
|
4359
|
+
try {
|
|
4360
|
+
const { grants, failures } = await collectGrants(snapshot);
|
|
4361
|
+
if (grants.length === 0) {
|
|
4362
|
+
return emptyPlan(
|
|
4363
|
+
serverUrl,
|
|
4364
|
+
{ status: "skipped", reason: "no_tokens" },
|
|
4365
|
+
failures
|
|
4366
|
+
);
|
|
4367
|
+
}
|
|
4368
|
+
const metadata = parseServerMetadata(snapshot.serverMetadata);
|
|
4369
|
+
if (!metadata) {
|
|
4370
|
+
return emptyPlan(
|
|
4371
|
+
serverUrl,
|
|
4372
|
+
{ status: "skipped", reason: "no_metadata" },
|
|
4373
|
+
failures
|
|
4374
|
+
);
|
|
4375
|
+
}
|
|
4376
|
+
if (!metadata.revocation_endpoint) {
|
|
4377
|
+
return emptyPlan(
|
|
4378
|
+
serverUrl,
|
|
4379
|
+
{ status: "skipped", reason: "no_endpoint" },
|
|
4380
|
+
failures
|
|
4381
|
+
);
|
|
4382
|
+
}
|
|
4383
|
+
return {
|
|
4384
|
+
serverUrl,
|
|
4385
|
+
grants,
|
|
4386
|
+
failures,
|
|
4387
|
+
endpoint: metadata.revocation_endpoint,
|
|
4388
|
+
supportedAuthMethods: revocationAuthMethods(metadata),
|
|
4389
|
+
metadataIssuer: metadata.issuer
|
|
4390
|
+
};
|
|
4391
|
+
} catch (err) {
|
|
4392
|
+
return emptyPlan(serverUrl, {
|
|
4393
|
+
status: "failed",
|
|
4394
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
4395
|
+
});
|
|
4396
|
+
}
|
|
4397
|
+
}
|
|
4398
|
+
function parseServerMetadata(raw) {
|
|
4399
|
+
if (raw === null || typeof raw !== "object") return void 0;
|
|
4400
|
+
return raw;
|
|
4401
|
+
}
|
|
4402
|
+
async function executeOAuthRevocation(plan, params) {
|
|
4403
|
+
const outcome = await runPlan(plan, params);
|
|
4404
|
+
const { logger } = params;
|
|
4405
|
+
if (outcome.status === "failed") {
|
|
4406
|
+
logger?.warn(
|
|
4407
|
+
{
|
|
4408
|
+
serverUrl: plan.serverUrl,
|
|
4409
|
+
endpoint: outcome.endpoint,
|
|
4410
|
+
detail: outcome.detail
|
|
4411
|
+
},
|
|
4412
|
+
"Token revocation failed; local OAuth state was cleared anyway"
|
|
4413
|
+
);
|
|
4414
|
+
} else if (outcome.status === "skipped" && outcome.reason === "no_endpoint") {
|
|
4415
|
+
logger?.debug(
|
|
4416
|
+
{ serverUrl: plan.serverUrl },
|
|
4417
|
+
"Skipping token revocation: authorization server metadata has no revocation_endpoint"
|
|
4418
|
+
);
|
|
4419
|
+
}
|
|
4420
|
+
return outcome;
|
|
4421
|
+
}
|
|
4422
|
+
async function collectGrants(snapshot) {
|
|
4423
|
+
const grants = [];
|
|
4424
|
+
const failures = [];
|
|
4425
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
4426
|
+
let preregistered;
|
|
4427
|
+
try {
|
|
4428
|
+
preregistered = await parseClient(snapshot.preregisteredClientInformation);
|
|
4429
|
+
} catch (err) {
|
|
4430
|
+
failures.push({
|
|
4431
|
+
status: "failed",
|
|
4432
|
+
detail: `could not read the preconfigured client registration: ${err instanceof Error ? err.message : String(err)}`
|
|
4433
|
+
});
|
|
4434
|
+
}
|
|
4435
|
+
const add2 = async (issuer, rawTokens, rawClient) => {
|
|
4436
|
+
const revocable = selectRevocableToken(await parseTokens(rawTokens));
|
|
4437
|
+
if (!revocable) return;
|
|
4438
|
+
const key = `${issuer ?? "\0legacy"}\0${revocable.token}`;
|
|
4439
|
+
if (seenKeys.has(key)) return;
|
|
4440
|
+
seenKeys.add(key);
|
|
4441
|
+
const bound = await parseClient(rawClient);
|
|
4442
|
+
grants.push({
|
|
4443
|
+
issuer,
|
|
4444
|
+
...revocable,
|
|
4445
|
+
clientInformation: bound ?? preregistered
|
|
4446
|
+
});
|
|
4447
|
+
};
|
|
4448
|
+
const addSafely = async (issuer, rawTokens, rawClient) => {
|
|
4449
|
+
try {
|
|
4450
|
+
await add2(issuer, rawTokens, rawClient);
|
|
4451
|
+
} catch (err) {
|
|
4452
|
+
const where = issuer === void 0 ? "the unkeyed slot" : `issuer ${issuer}`;
|
|
4453
|
+
failures.push({
|
|
4454
|
+
status: "failed",
|
|
4455
|
+
detail: `could not read the stored grant for ${where}: ${err instanceof Error ? err.message : String(err)}`
|
|
4456
|
+
});
|
|
4457
|
+
}
|
|
4458
|
+
};
|
|
4459
|
+
for (const [issuer, slot] of Object.entries(snapshot.byIssuer)) {
|
|
4460
|
+
await addSafely(issuer, slot?.tokens, slot?.clientInformation);
|
|
4461
|
+
}
|
|
4462
|
+
await addSafely(
|
|
4463
|
+
void 0,
|
|
4464
|
+
snapshot.legacyTokens,
|
|
4465
|
+
snapshot.legacyClientInformation
|
|
4466
|
+
);
|
|
4467
|
+
return { grants, failures };
|
|
4468
|
+
}
|
|
4469
|
+
async function parseTokens(raw) {
|
|
4470
|
+
return raw === void 0 || raw === null ? void 0 : await OAuthTokensSchema2.parseAsync(raw);
|
|
4471
|
+
}
|
|
4472
|
+
async function parseClient(raw) {
|
|
4473
|
+
return raw === void 0 || raw === null ? void 0 : await OAuthClientInformationSchema2.parseAsync(raw);
|
|
4474
|
+
}
|
|
4475
|
+
function aggregateOutcomes(outcomes) {
|
|
4476
|
+
return outcomes.find((o) => o.status === "failed") ?? outcomes.find((o) => o.status === "revoked") ?? outcomes[0] ?? { status: "skipped", reason: "no_tokens" };
|
|
4477
|
+
}
|
|
4478
|
+
async function runPlan(plan, params) {
|
|
4479
|
+
if (plan.outcome) return plan.outcome;
|
|
4480
|
+
const endpoint = plan.endpoint;
|
|
4481
|
+
if (!endpoint) return { status: "skipped", reason: "no_endpoint" };
|
|
4482
|
+
const outcomes = [...plan.failures];
|
|
4483
|
+
const timeoutMs = params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS;
|
|
4484
|
+
const deadlineAt = performance.now() + timeoutMs;
|
|
4485
|
+
for (const grant of plan.grants) {
|
|
4486
|
+
if (grant.issuer === void 0) {
|
|
4487
|
+
outcomes.push({
|
|
4488
|
+
status: "failed",
|
|
4489
|
+
detail: "the stored grant is not bound to an authorization server (it predates issuer binding), so it could not be matched to this revocation endpoint and was cleared without revocation \u2014 re-authorize to make future clears revocable"
|
|
4490
|
+
});
|
|
4491
|
+
continue;
|
|
4492
|
+
}
|
|
4493
|
+
if (plan.metadataIssuer !== grant.issuer) {
|
|
4494
|
+
outcomes.push({
|
|
4495
|
+
status: "failed",
|
|
4496
|
+
detail: plan.metadataIssuer === void 0 ? `the cached authorization-server metadata names no issuer, so the grant bound to ${grant.issuer} could not be matched to this revocation endpoint and was cleared without revocation` : `the cached authorization-server metadata is for ${plan.metadataIssuer}, so the grant bound to ${grant.issuer} was cleared without revocation`
|
|
4497
|
+
});
|
|
4498
|
+
continue;
|
|
4499
|
+
}
|
|
4500
|
+
const remainingMs = deadlineAt - performance.now();
|
|
4501
|
+
if (remainingMs <= MIN_REVOCATION_REQUEST_BUDGET_MS) {
|
|
4502
|
+
outcomes.push({
|
|
4503
|
+
status: "failed",
|
|
4504
|
+
endpoint,
|
|
4505
|
+
detail: `the ${timeoutMs}ms revocation budget was exhausted before this grant was attempted${grant.issuer === void 0 ? "" : ` (issuer ${grant.issuer})`}`
|
|
4506
|
+
});
|
|
4507
|
+
continue;
|
|
4508
|
+
}
|
|
4509
|
+
outcomes.push(
|
|
4510
|
+
await revokeToken({
|
|
4511
|
+
endpoint,
|
|
4512
|
+
token: grant.token,
|
|
4513
|
+
tokenTypeHint: grant.tokenTypeHint,
|
|
4514
|
+
clientInformation: grant.clientInformation,
|
|
4515
|
+
supportedAuthMethods: plan.supportedAuthMethods,
|
|
4516
|
+
fetchFn: params.fetchFn,
|
|
4517
|
+
timeoutMs: remainingMs
|
|
4518
|
+
})
|
|
4519
|
+
);
|
|
4520
|
+
}
|
|
4521
|
+
return aggregateOutcomes(outcomes);
|
|
4522
|
+
}
|
|
4523
|
+
|
|
3653
4524
|
// src/clear-stored-auth-for-relogin.ts
|
|
3654
4525
|
function normalizeServerUrl(serverUrl) {
|
|
3655
4526
|
try {
|
|
@@ -3658,16 +4529,56 @@ function normalizeServerUrl(serverUrl) {
|
|
|
3658
4529
|
return serverUrl;
|
|
3659
4530
|
}
|
|
3660
4531
|
}
|
|
3661
|
-
async function clearStoredAuthForRelogin(serverUrl) {
|
|
3662
|
-
if (!serverUrl?.trim()) return;
|
|
4532
|
+
async function clearStoredAuthForRelogin(serverUrl, options) {
|
|
4533
|
+
if (!serverUrl?.trim()) return void 0;
|
|
3663
4534
|
const raw = serverUrl.trim();
|
|
3664
4535
|
const normalized = normalizeServerUrl(raw);
|
|
3665
4536
|
const storage = new NodeOAuthStorage();
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
4537
|
+
const keys = normalized === raw ? [raw] : [normalized, raw];
|
|
4538
|
+
const plans = [];
|
|
4539
|
+
for (const key of keys) {
|
|
4540
|
+
plans.push(
|
|
4541
|
+
await clearAndPlanRevocation({
|
|
4542
|
+
serverUrl: key,
|
|
4543
|
+
storage,
|
|
4544
|
+
enabled: options?.revoke !== false
|
|
4545
|
+
})
|
|
4546
|
+
);
|
|
3669
4547
|
}
|
|
3670
4548
|
resetNodeOAuthStorageCache();
|
|
4549
|
+
return options?.revoke === false ? void 0 : sendPlans(plans, options?.budgetMs ?? DEFAULT_REVOCATION_TIMEOUT_MS);
|
|
4550
|
+
}
|
|
4551
|
+
async function sendPlans(plans, budgetMs) {
|
|
4552
|
+
const fetchFn = createProxyFetch() ?? fetch;
|
|
4553
|
+
const deadlineAt = performance.now() + budgetMs;
|
|
4554
|
+
let reported;
|
|
4555
|
+
let lastSkip;
|
|
4556
|
+
for (const plan of plans) {
|
|
4557
|
+
const remainingMs = deadlineAt - performance.now();
|
|
4558
|
+
const needsNetwork = plan.outcome === void 0;
|
|
4559
|
+
if (needsNetwork && remainingMs <= MIN_REVOCATION_REQUEST_BUDGET_MS) {
|
|
4560
|
+
const exhausted = {
|
|
4561
|
+
status: "failed",
|
|
4562
|
+
detail: `the ${budgetMs}ms revocation budget was exhausted before "${plan.serverUrl}" was attempted`
|
|
4563
|
+
};
|
|
4564
|
+
if (reported?.status !== "failed") reported = exhausted;
|
|
4565
|
+
continue;
|
|
4566
|
+
}
|
|
4567
|
+
const outcome = await executeOAuthRevocation(plan, {
|
|
4568
|
+
fetchFn,
|
|
4569
|
+
timeoutMs: needsNetwork ? remainingMs : void 0
|
|
4570
|
+
});
|
|
4571
|
+
if (outcome.status === "skipped" && outcome.reason === "no_tokens") {
|
|
4572
|
+
lastSkip = outcome;
|
|
4573
|
+
continue;
|
|
4574
|
+
}
|
|
4575
|
+
if (outcome.status === "failed") {
|
|
4576
|
+
if (reported?.status !== "failed") reported = outcome;
|
|
4577
|
+
} else {
|
|
4578
|
+
reported ??= outcome;
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
return reported ?? lastSkip;
|
|
3671
4582
|
}
|
|
3672
4583
|
|
|
3673
4584
|
// ../../core/mcp/inspectorClient.ts
|
|
@@ -3830,6 +4741,20 @@ function mcpParamHeadersForTool(tool, args) {
|
|
|
3830
4741
|
return buildMcpParamHeaders(scan.declarations, args);
|
|
3831
4742
|
}
|
|
3832
4743
|
|
|
4744
|
+
// ../../core/mcp/subscriptionAck.ts
|
|
4745
|
+
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
|
|
4746
|
+
var NEVER_ACKNOWLEDGED_SDK_MESSAGE = "closed the subscription gracefully before acknowledging";
|
|
4747
|
+
var NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE = "The server closed the subscription without acknowledging it. A subscriptions/listen request must first be answered with a notifications/subscriptions/acknowledged notification; a JSON-RPC result for the listen id means graceful closure. Not retrying \u2014 the server would answer the same way again.";
|
|
4748
|
+
function isNeverAcknowledgedSubscriptionClose(error) {
|
|
4749
|
+
return SdkError.isInstance(error) && error.code === SdkErrorCode.ConnectionClosed && error.message.includes(NEVER_ACKNOWLEDGED_SDK_MESSAGE);
|
|
4750
|
+
}
|
|
4751
|
+
function subscriptionFailureMessage(error) {
|
|
4752
|
+
if (isNeverAcknowledgedSubscriptionClose(error)) {
|
|
4753
|
+
return NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE;
|
|
4754
|
+
}
|
|
4755
|
+
return error instanceof Error ? error.message : String(error);
|
|
4756
|
+
}
|
|
4757
|
+
|
|
3833
4758
|
// ../../core/mcp/messageTrackingTransport.ts
|
|
3834
4759
|
var MessageTrackingTransport = class {
|
|
3835
4760
|
baseTransport;
|
|
@@ -3921,6 +4846,29 @@ var MessageTrackingTransport = class {
|
|
|
3921
4846
|
get sessionId() {
|
|
3922
4847
|
return this.baseTransport.sessionId;
|
|
3923
4848
|
}
|
|
4849
|
+
/**
|
|
4850
|
+
* Forward the base transport's per-request-stream capability (#2140).
|
|
4851
|
+
*
|
|
4852
|
+
* The SDK's `Protocol.request` reads this off the transport it was handed —
|
|
4853
|
+
* which is always this wrapper — to decide how a cancelled request is
|
|
4854
|
+
* signalled: abort that request's own stream (the 2026-07-28 signal for
|
|
4855
|
+
* Streamable HTTP) or POST a `notifications/cancelled` (the stdio one).
|
|
4856
|
+
*
|
|
4857
|
+
* Not forwarding it made the wrapper *answer* the question, `undefined`, for
|
|
4858
|
+
* every client. So the Cancel button POSTed a notification even on a modern
|
|
4859
|
+
* Streamable HTTP connection, where a spec-compliant server acknowledges it
|
|
4860
|
+
* `202` and drops it — the tool kept running and only a disconnect really
|
|
4861
|
+
* cancelled anything. That hit the CLI and TUI as much as the web client:
|
|
4862
|
+
* their real `StreamableHTTPClientTransport` advertises the flag correctly
|
|
4863
|
+
* and this wrapper hid it.
|
|
4864
|
+
*
|
|
4865
|
+
* A base transport that says nothing stays `undefined` rather than becoming
|
|
4866
|
+
* `false`, since the SDK's check is `=== true` either way and inventing a
|
|
4867
|
+
* value here would misreport what the transport claimed.
|
|
4868
|
+
*/
|
|
4869
|
+
get hasPerRequestStream() {
|
|
4870
|
+
return this.baseTransport.hasPerRequestStream;
|
|
4871
|
+
}
|
|
3924
4872
|
// Implemented as a concrete method (rather than delegating the base
|
|
3925
4873
|
// transport's optional `setProtocolVersion`) so the SDK Client always
|
|
3926
4874
|
// invokes it after the initialize handshake — including for stdio, whose
|
|
@@ -4028,14 +4976,661 @@ function buildClientExtensions(input) {
|
|
|
4028
4976
|
map[ext.key] = ext.advertisement ? structuredClone(ext.advertisement) : {};
|
|
4029
4977
|
}
|
|
4030
4978
|
}
|
|
4031
|
-
const uiAdvertisement = map[UI_EXTENSION_KEY];
|
|
4032
|
-
if (input.appElicitation && uiAdvertisement) {
|
|
4033
|
-
map[UI_EXTENSION_KEY] = { ...uiAdvertisement, elicitation: {} };
|
|
4979
|
+
const uiAdvertisement = map[UI_EXTENSION_KEY];
|
|
4980
|
+
if (input.appElicitation && uiAdvertisement) {
|
|
4981
|
+
map[UI_EXTENSION_KEY] = { ...uiAdvertisement, elicitation: {} };
|
|
4982
|
+
}
|
|
4983
|
+
if (input.enterpriseManaged) {
|
|
4984
|
+
map[EMA_EXTENSION_KEY] = {};
|
|
4985
|
+
}
|
|
4986
|
+
return map;
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
// ../../core/mcp/skillsSchemas.ts
|
|
4990
|
+
import { z as z2 } from "zod/v4";
|
|
4991
|
+
import { ResourceSchema } from "@modelcontextprotocol/core";
|
|
4992
|
+
var SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills";
|
|
4993
|
+
var SKILLS_LIST_METHOD = "skills/list";
|
|
4994
|
+
var SKILLS_GET_METHOD = "skills/get";
|
|
4995
|
+
var RESOURCES_DIRECTORY_READ_METHOD = "resources/directory/read";
|
|
4996
|
+
var DYNAMIC_RESOURCES = "dynamic";
|
|
4997
|
+
var SkillFrontmatterSchema = z2.looseObject({
|
|
4998
|
+
name: z2.string().optional(),
|
|
4999
|
+
description: z2.string().optional()
|
|
5000
|
+
});
|
|
5001
|
+
var SkillResourceSchema = z2.looseObject({
|
|
5002
|
+
uri: z2.string(),
|
|
5003
|
+
digest: z2.string().optional(),
|
|
5004
|
+
size: z2.number().optional()
|
|
5005
|
+
});
|
|
5006
|
+
var SkillEntrySchema = z2.looseObject({
|
|
5007
|
+
uri: z2.string(),
|
|
5008
|
+
frontmatter: SkillFrontmatterSchema,
|
|
5009
|
+
resources: z2.union([
|
|
5010
|
+
z2.literal(DYNAMIC_RESOURCES),
|
|
5011
|
+
z2.array(SkillResourceSchema)
|
|
5012
|
+
])
|
|
5013
|
+
});
|
|
5014
|
+
var ListSkillsResultSchema = z2.looseObject({
|
|
5015
|
+
skills: z2.array(SkillEntrySchema),
|
|
5016
|
+
nextCursor: z2.string().optional()
|
|
5017
|
+
});
|
|
5018
|
+
var ModernListSkillsResultSchema = ListSkillsResultSchema.extend({
|
|
5019
|
+
resultType: z2.literal("complete"),
|
|
5020
|
+
// A non-negative INTEGER, matching the codec: `ttlMs: -1` or `0.5` is an
|
|
5021
|
+
// envelope violation, and accepting it here would hide exactly the kind of
|
|
5022
|
+
// defect this schema exists to surface.
|
|
5023
|
+
ttlMs: z2.int().min(0),
|
|
5024
|
+
cacheScope: z2.enum(["public", "private"])
|
|
5025
|
+
});
|
|
5026
|
+
var GetSkillEnvelopeSchema = z2.looseObject({
|
|
5027
|
+
skill: SkillEntrySchema
|
|
5028
|
+
});
|
|
5029
|
+
var ModernGetSkillEnvelopeSchema = GetSkillEnvelopeSchema.extend({
|
|
5030
|
+
resultType: z2.literal("complete")
|
|
5031
|
+
});
|
|
5032
|
+
var GetSkillResultSchema = GetSkillEnvelopeSchema.transform(
|
|
5033
|
+
(result) => result.skill
|
|
5034
|
+
);
|
|
5035
|
+
var DirectoryChildSchema = ResourceSchema;
|
|
5036
|
+
var DirectoryReadResultSchema = z2.looseObject({
|
|
5037
|
+
resources: z2.array(DirectoryChildSchema),
|
|
5038
|
+
nextCursor: z2.string().optional()
|
|
5039
|
+
});
|
|
5040
|
+
var ModernDirectoryReadResultSchema = DirectoryReadResultSchema.extend(
|
|
5041
|
+
{ resultType: z2.literal("complete") }
|
|
5042
|
+
);
|
|
5043
|
+
|
|
5044
|
+
// ../../core/mcp/sha256.ts
|
|
5045
|
+
var K = new Uint32Array([
|
|
5046
|
+
1116352408,
|
|
5047
|
+
1899447441,
|
|
5048
|
+
3049323471,
|
|
5049
|
+
3921009573,
|
|
5050
|
+
961987163,
|
|
5051
|
+
1508970993,
|
|
5052
|
+
2453635748,
|
|
5053
|
+
2870763221,
|
|
5054
|
+
3624381080,
|
|
5055
|
+
310598401,
|
|
5056
|
+
607225278,
|
|
5057
|
+
1426881987,
|
|
5058
|
+
1925078388,
|
|
5059
|
+
2162078206,
|
|
5060
|
+
2614888103,
|
|
5061
|
+
3248222580,
|
|
5062
|
+
3835390401,
|
|
5063
|
+
4022224774,
|
|
5064
|
+
264347078,
|
|
5065
|
+
604807628,
|
|
5066
|
+
770255983,
|
|
5067
|
+
1249150122,
|
|
5068
|
+
1555081692,
|
|
5069
|
+
1996064986,
|
|
5070
|
+
2554220882,
|
|
5071
|
+
2821834349,
|
|
5072
|
+
2952996808,
|
|
5073
|
+
3210313671,
|
|
5074
|
+
3336571891,
|
|
5075
|
+
3584528711,
|
|
5076
|
+
113926993,
|
|
5077
|
+
338241895,
|
|
5078
|
+
666307205,
|
|
5079
|
+
773529912,
|
|
5080
|
+
1294757372,
|
|
5081
|
+
1396182291,
|
|
5082
|
+
1695183700,
|
|
5083
|
+
1986661051,
|
|
5084
|
+
2177026350,
|
|
5085
|
+
2456956037,
|
|
5086
|
+
2730485921,
|
|
5087
|
+
2820302411,
|
|
5088
|
+
3259730800,
|
|
5089
|
+
3345764771,
|
|
5090
|
+
3516065817,
|
|
5091
|
+
3600352804,
|
|
5092
|
+
4094571909,
|
|
5093
|
+
275423344,
|
|
5094
|
+
430227734,
|
|
5095
|
+
506948616,
|
|
5096
|
+
659060556,
|
|
5097
|
+
883997877,
|
|
5098
|
+
958139571,
|
|
5099
|
+
1322822218,
|
|
5100
|
+
1537002063,
|
|
5101
|
+
1747873779,
|
|
5102
|
+
1955562222,
|
|
5103
|
+
2024104815,
|
|
5104
|
+
2227730452,
|
|
5105
|
+
2361852424,
|
|
5106
|
+
2428436474,
|
|
5107
|
+
2756734187,
|
|
5108
|
+
3204031479,
|
|
5109
|
+
3329325298
|
|
5110
|
+
]);
|
|
5111
|
+
var H0 = new Uint32Array([
|
|
5112
|
+
1779033703,
|
|
5113
|
+
3144134277,
|
|
5114
|
+
1013904242,
|
|
5115
|
+
2773480762,
|
|
5116
|
+
1359893119,
|
|
5117
|
+
2600822924,
|
|
5118
|
+
528734635,
|
|
5119
|
+
1541459225
|
|
5120
|
+
]);
|
|
5121
|
+
var rotr = (x, n) => x >>> n | x << 32 - n;
|
|
5122
|
+
function sha256Bytes(bytes) {
|
|
5123
|
+
const message = new Uint8Array(bytes);
|
|
5124
|
+
const bitLength = message.length * 8;
|
|
5125
|
+
const withLength = message.length + 9;
|
|
5126
|
+
const padded = new Uint8Array(Math.ceil(withLength / 64) * 64);
|
|
5127
|
+
padded.set(message);
|
|
5128
|
+
padded[message.length] = 128;
|
|
5129
|
+
const view = new DataView(padded.buffer);
|
|
5130
|
+
view.setUint32(padded.length - 8, Math.floor(bitLength / 4294967296));
|
|
5131
|
+
view.setUint32(padded.length - 4, bitLength >>> 0);
|
|
5132
|
+
const h = new Uint32Array(H0);
|
|
5133
|
+
const w = new Uint32Array(64);
|
|
5134
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
5135
|
+
for (let i = 0; i < 16; i += 1) w[i] = view.getUint32(offset + i * 4);
|
|
5136
|
+
for (let i = 16; i < 64; i += 1) {
|
|
5137
|
+
const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ w[i - 15] >>> 3;
|
|
5138
|
+
const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ w[i - 2] >>> 10;
|
|
5139
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
5140
|
+
}
|
|
5141
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
5142
|
+
for (let i = 0; i < 64; i += 1) {
|
|
5143
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
5144
|
+
const ch = e & f ^ ~e & g;
|
|
5145
|
+
const temp1 = hh + S1 + ch + K[i] + w[i] >>> 0;
|
|
5146
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
5147
|
+
const maj = a & b ^ a & c ^ b & c;
|
|
5148
|
+
const temp2 = S0 + maj >>> 0;
|
|
5149
|
+
hh = g;
|
|
5150
|
+
g = f;
|
|
5151
|
+
f = e;
|
|
5152
|
+
e = d + temp1 >>> 0;
|
|
5153
|
+
d = c;
|
|
5154
|
+
c = b;
|
|
5155
|
+
b = a;
|
|
5156
|
+
a = temp1 + temp2 >>> 0;
|
|
5157
|
+
}
|
|
5158
|
+
h[0] = h[0] + a >>> 0;
|
|
5159
|
+
h[1] = h[1] + b >>> 0;
|
|
5160
|
+
h[2] = h[2] + c >>> 0;
|
|
5161
|
+
h[3] = h[3] + d >>> 0;
|
|
5162
|
+
h[4] = h[4] + e >>> 0;
|
|
5163
|
+
h[5] = h[5] + f >>> 0;
|
|
5164
|
+
h[6] = h[6] + g >>> 0;
|
|
5165
|
+
h[7] = h[7] + hh >>> 0;
|
|
5166
|
+
}
|
|
5167
|
+
const digest = new Uint8Array(32);
|
|
5168
|
+
const out = new DataView(digest.buffer);
|
|
5169
|
+
for (let i = 0; i < 8; i += 1) out.setUint32(i * 4, h[i]);
|
|
5170
|
+
return digest;
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
// ../../core/mcp/skillFile.ts
|
|
5174
|
+
import { parse as parseYaml } from "yaml";
|
|
5175
|
+
function splitSkillFile(text) {
|
|
5176
|
+
if (!/^---[ \t]*\r?\n/.test(text)) return { body: text };
|
|
5177
|
+
const rest = text.slice(text.indexOf("\n") + 1);
|
|
5178
|
+
const close = rest.search(/^---[ \t]*\r?$/m);
|
|
5179
|
+
if (close === -1) return { body: text };
|
|
5180
|
+
const frontmatter = rest.slice(0, close).replace(/\r?\n$/, "");
|
|
5181
|
+
const after = rest.slice(close);
|
|
5182
|
+
const newline = after.indexOf("\n");
|
|
5183
|
+
if (newline === -1) return { frontmatter, body: "" };
|
|
5184
|
+
return { frontmatter, body: after.slice(newline + 1).replace(/^\r?\n/, "") };
|
|
5185
|
+
}
|
|
5186
|
+
function hasContent(yamlText) {
|
|
5187
|
+
return yamlText.split("\n").some((line) => line.trim() !== "" && !line.trimStart().startsWith("#"));
|
|
5188
|
+
}
|
|
5189
|
+
var MAX_FRONTMATTER_DEPTH = 64;
|
|
5190
|
+
function jsonGraphError(value, seen = /* @__PURE__ */ new Set(), depth = 0) {
|
|
5191
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
5192
|
+
if (depth > MAX_FRONTMATTER_DEPTH) {
|
|
5193
|
+
return `Frontmatter nests deeper than ${MAX_FRONTMATTER_DEPTH} levels, which cannot be compared field by field.`;
|
|
5194
|
+
}
|
|
5195
|
+
const node = value;
|
|
5196
|
+
if (seen.has(node)) {
|
|
5197
|
+
return "Frontmatter contains a cyclic YAML alias, which has no JSON equivalent and cannot be compared against the listing.";
|
|
5198
|
+
}
|
|
5199
|
+
seen.add(node);
|
|
5200
|
+
const members = Array.isArray(node) ? node : Object.values(node);
|
|
5201
|
+
for (const member of members) {
|
|
5202
|
+
const error = jsonGraphError(member, seen, depth + 1);
|
|
5203
|
+
if (error) return error;
|
|
5204
|
+
}
|
|
5205
|
+
seen.delete(node);
|
|
5206
|
+
return void 0;
|
|
5207
|
+
}
|
|
5208
|
+
function parseSkillFrontmatter(yamlText) {
|
|
5209
|
+
let parsed;
|
|
5210
|
+
try {
|
|
5211
|
+
parsed = parseYaml(yamlText);
|
|
5212
|
+
} catch (err) {
|
|
5213
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
5214
|
+
}
|
|
5215
|
+
if (parsed === null || parsed === void 0) {
|
|
5216
|
+
return hasContent(yamlText) ? { error: "Frontmatter is not a YAML mapping of fields." } : { fields: {} };
|
|
5217
|
+
}
|
|
5218
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5219
|
+
return {
|
|
5220
|
+
error: "Frontmatter is not a YAML mapping of fields."
|
|
5221
|
+
};
|
|
5222
|
+
}
|
|
5223
|
+
const graphError = jsonGraphError(parsed, /* @__PURE__ */ new Set(), 0);
|
|
5224
|
+
if (graphError) return { error: graphError };
|
|
5225
|
+
return { fields: parsed };
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
// ../../core/mcp/skills.ts
|
|
5229
|
+
var SKILL_MAX_RESOURCE_ENTRIES = 512;
|
|
5230
|
+
var SKILL_MAX_TOTAL_BYTES = 16 * 1024 * 1024;
|
|
5231
|
+
var SKILL_MAX_CATALOG_SKILLS = 256;
|
|
5232
|
+
var SKILL_MAX_CATALOG_BYTES = 64 * 1024 * 1024;
|
|
5233
|
+
var SKILL_FILE_SUFFIX = "/SKILL.md";
|
|
5234
|
+
var DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
5235
|
+
var SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5236
|
+
var SKILL_NAME_MAX_LENGTH = 64;
|
|
5237
|
+
var SKILL_DESCRIPTION_MAX_LENGTH = 1024;
|
|
5238
|
+
function codePointLength(value) {
|
|
5239
|
+
return [...value].length;
|
|
5240
|
+
}
|
|
5241
|
+
function getSkillsExtension(capabilities) {
|
|
5242
|
+
const declared = capabilities?.extensions?.[SKILLS_EXTENSION_KEY];
|
|
5243
|
+
if (typeof declared !== "object" || declared === null) return void 0;
|
|
5244
|
+
const directoryRead = declared.directoryRead === true;
|
|
5245
|
+
return { directoryRead };
|
|
5246
|
+
}
|
|
5247
|
+
function normalizeSkillUri(uri) {
|
|
5248
|
+
let parsed;
|
|
5249
|
+
try {
|
|
5250
|
+
parsed = new URL(uri);
|
|
5251
|
+
} catch {
|
|
5252
|
+
return void 0;
|
|
5253
|
+
}
|
|
5254
|
+
if (!parsed.pathname.startsWith("/")) return void 0;
|
|
5255
|
+
return canonicalizePercentEncoding(parsed.href);
|
|
5256
|
+
}
|
|
5257
|
+
function canonicalizePercentEncoding(value) {
|
|
5258
|
+
return value.replace(/%[0-9a-fA-F]{2}/g, (escape) => {
|
|
5259
|
+
const char = String.fromCharCode(Number.parseInt(escape.slice(1), 16));
|
|
5260
|
+
return /[A-Za-z0-9\-._~]/.test(char) ? char : escape.toUpperCase();
|
|
5261
|
+
});
|
|
5262
|
+
}
|
|
5263
|
+
function skillNameFromUri(uri) {
|
|
5264
|
+
const normalized = normalizeSkillUri(uri);
|
|
5265
|
+
if (normalized === void 0 || !normalized.endsWith(SKILL_FILE_SUFFIX)) {
|
|
5266
|
+
return void 0;
|
|
5267
|
+
}
|
|
5268
|
+
const path5 = normalized.slice(0, -SKILL_FILE_SUFFIX.length);
|
|
5269
|
+
const segment = path5.slice(path5.lastIndexOf("/") + 1);
|
|
5270
|
+
return segment.length > 0 ? segment : void 0;
|
|
5271
|
+
}
|
|
5272
|
+
function skillUriIdentity(uri) {
|
|
5273
|
+
return normalizeSkillUri(uri) ?? uri;
|
|
5274
|
+
}
|
|
5275
|
+
function skillDisplayName(entry) {
|
|
5276
|
+
const declared = entry.frontmatter.name?.trim();
|
|
5277
|
+
if (declared) return declared;
|
|
5278
|
+
return skillNameFromUri(entry.uri) ?? entry.uri;
|
|
5279
|
+
}
|
|
5280
|
+
function checkSkillConformance(entry) {
|
|
5281
|
+
const issues = [];
|
|
5282
|
+
const rawName = entry.frontmatter.name;
|
|
5283
|
+
const declaredName = rawName?.trim() ? rawName : void 0;
|
|
5284
|
+
const uriName = skillNameFromUri(entry.uri);
|
|
5285
|
+
if (!declaredName) {
|
|
5286
|
+
issues.push({
|
|
5287
|
+
code: "missing-name",
|
|
5288
|
+
severity: "error",
|
|
5289
|
+
message: "frontmatter.name is required but missing or empty."
|
|
5290
|
+
});
|
|
5291
|
+
} else if (codePointLength(declaredName) > SKILL_NAME_MAX_LENGTH || !SKILL_NAME_PATTERN.test(declaredName)) {
|
|
5292
|
+
issues.push({
|
|
5293
|
+
code: "malformed-name",
|
|
5294
|
+
severity: "error",
|
|
5295
|
+
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.`
|
|
5296
|
+
});
|
|
5297
|
+
}
|
|
5298
|
+
const rawDescription = entry.frontmatter.description;
|
|
5299
|
+
if (!rawDescription?.trim()) {
|
|
5300
|
+
issues.push({
|
|
5301
|
+
code: "missing-description",
|
|
5302
|
+
severity: "error",
|
|
5303
|
+
message: "frontmatter.description is required but missing or empty."
|
|
5304
|
+
});
|
|
5305
|
+
} else if (codePointLength(rawDescription) > SKILL_DESCRIPTION_MAX_LENGTH) {
|
|
5306
|
+
issues.push({
|
|
5307
|
+
code: "malformed-description",
|
|
5308
|
+
severity: "error",
|
|
5309
|
+
message: `frontmatter.description is ${codePointLength(rawDescription)} characters, above the ${SKILL_DESCRIPTION_MAX_LENGTH}-character limit.`
|
|
5310
|
+
});
|
|
5311
|
+
}
|
|
5312
|
+
if (uriName === void 0) {
|
|
5313
|
+
issues.push({
|
|
5314
|
+
code: "malformed-uri",
|
|
5315
|
+
severity: "error",
|
|
5316
|
+
message: `Skill URI must be a hierarchical URI ending with "${SKILL_FILE_SUFFIX}" and carrying a non-empty path segment before it.`
|
|
5317
|
+
});
|
|
5318
|
+
} else if (declaredName && uriName !== declaredName) {
|
|
5319
|
+
issues.push({
|
|
5320
|
+
code: "name-path-mismatch",
|
|
5321
|
+
severity: "error",
|
|
5322
|
+
message: `URI path segment "${uriName}" does not match frontmatter.name "${declaredName}".`
|
|
5323
|
+
});
|
|
5324
|
+
}
|
|
5325
|
+
if (entry.resources === DYNAMIC_RESOURCES) {
|
|
5326
|
+
issues.push({
|
|
5327
|
+
code: "dynamic-resources",
|
|
5328
|
+
severity: "warning",
|
|
5329
|
+
message: 'resources is "dynamic": the file set is generated, so no digest is advertised and integrity cannot be verified.'
|
|
5330
|
+
});
|
|
5331
|
+
return issues;
|
|
5332
|
+
}
|
|
5333
|
+
if (entry.resources.length > SKILL_MAX_RESOURCE_ENTRIES) {
|
|
5334
|
+
issues.push({
|
|
5335
|
+
code: "resource-limit-exceeded",
|
|
5336
|
+
severity: "warning",
|
|
5337
|
+
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.`
|
|
5338
|
+
});
|
|
5339
|
+
}
|
|
5340
|
+
const totalBytes = totalSkillBytes(entry.resources);
|
|
5341
|
+
if (totalBytes > SKILL_MAX_TOTAL_BYTES) {
|
|
5342
|
+
issues.push({
|
|
5343
|
+
code: "size-limit-exceeded",
|
|
5344
|
+
severity: "warning",
|
|
5345
|
+
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.`
|
|
5346
|
+
});
|
|
5347
|
+
}
|
|
5348
|
+
const entryIdentity = skillUriIdentity(entry.uri);
|
|
5349
|
+
if (!entry.resources.some(
|
|
5350
|
+
(resource) => skillUriIdentity(resource.uri) === entryIdentity
|
|
5351
|
+
)) {
|
|
5352
|
+
issues.push({
|
|
5353
|
+
code: "manifest-missing-self",
|
|
5354
|
+
severity: "error",
|
|
5355
|
+
message: `Manifest does not list the skill's own entry file (${entry.uri}); a manifest must be the complete file set.`
|
|
5356
|
+
});
|
|
5357
|
+
}
|
|
5358
|
+
const seenUris = /* @__PURE__ */ new Set();
|
|
5359
|
+
const normalizedEntryUri = normalizeSkillUri(entry.uri);
|
|
5360
|
+
const root = normalizedEntryUri !== void 0 && normalizedEntryUri.endsWith(SKILL_FILE_SUFFIX) ? `${normalizedEntryUri.slice(0, -SKILL_FILE_SUFFIX.length)}/` : void 0;
|
|
5361
|
+
for (const resource of entry.resources) {
|
|
5362
|
+
const identity2 = skillUriIdentity(resource.uri);
|
|
5363
|
+
if (seenUris.has(identity2)) {
|
|
5364
|
+
issues.push({
|
|
5365
|
+
code: "duplicate-resource",
|
|
5366
|
+
severity: "error",
|
|
5367
|
+
message: "Manifest lists this URI more than once; entries must be unique.",
|
|
5368
|
+
resourceUri: resource.uri
|
|
5369
|
+
});
|
|
5370
|
+
}
|
|
5371
|
+
seenUris.add(identity2);
|
|
5372
|
+
if (root !== void 0) {
|
|
5373
|
+
const normalized = normalizeSkillUri(resource.uri);
|
|
5374
|
+
if (normalized === void 0 || !normalized.startsWith(root)) {
|
|
5375
|
+
issues.push({
|
|
5376
|
+
code: "resource-outside-skill-root",
|
|
5377
|
+
severity: "error",
|
|
5378
|
+
message: `Manifest entry does not resolve inside the skill root "${root}".`,
|
|
5379
|
+
resourceUri: resource.uri
|
|
5380
|
+
});
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
if (resource.digest === void 0) {
|
|
5384
|
+
issues.push({
|
|
5385
|
+
code: "missing-digest",
|
|
5386
|
+
severity: "error",
|
|
5387
|
+
message: "Manifest entry declares no digest, which is required \u2014 and without it the file cannot be verified.",
|
|
5388
|
+
resourceUri: resource.uri
|
|
5389
|
+
});
|
|
5390
|
+
} else if (!DIGEST_PATTERN.test(resource.digest)) {
|
|
5391
|
+
issues.push({
|
|
5392
|
+
code: "malformed-digest",
|
|
5393
|
+
severity: "error",
|
|
5394
|
+
message: `Digest "${resource.digest}" is not "sha256:" followed by 64 lowercase hex characters.`,
|
|
5395
|
+
resourceUri: resource.uri
|
|
5396
|
+
});
|
|
5397
|
+
}
|
|
5398
|
+
if (resource.size === void 0) {
|
|
5399
|
+
issues.push({
|
|
5400
|
+
code: "missing-size",
|
|
5401
|
+
severity: "error",
|
|
5402
|
+
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.",
|
|
5403
|
+
resourceUri: resource.uri
|
|
5404
|
+
});
|
|
5405
|
+
} else if (!isUsableSize(resource.size)) {
|
|
5406
|
+
issues.push({
|
|
5407
|
+
code: "malformed-size",
|
|
5408
|
+
severity: "error",
|
|
5409
|
+
message: `Size ${resource.size} is not a non-negative integer byte length.`,
|
|
5410
|
+
resourceUri: resource.uri
|
|
5411
|
+
});
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
return issues;
|
|
5415
|
+
}
|
|
5416
|
+
function isUsableSize(size) {
|
|
5417
|
+
return size !== void 0 && Number.isSafeInteger(size) && size >= 0;
|
|
5418
|
+
}
|
|
5419
|
+
function totalSkillBytes(resources) {
|
|
5420
|
+
return resources.reduce(
|
|
5421
|
+
(sum, r) => sum + (isUsableSize(r.size) ? r.size : 0),
|
|
5422
|
+
0
|
|
5423
|
+
);
|
|
5424
|
+
}
|
|
5425
|
+
function canonicalize(value) {
|
|
5426
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
5427
|
+
if (value === null || typeof value !== "object") return value;
|
|
5428
|
+
return sortKeys(value);
|
|
5429
|
+
}
|
|
5430
|
+
function sortKeys(value) {
|
|
5431
|
+
return Object.fromEntries(
|
|
5432
|
+
Object.entries(value).map(([key, member]) => [key, canonicalize(member)]).sort(([x], [y]) => x.localeCompare(y))
|
|
5433
|
+
);
|
|
5434
|
+
}
|
|
5435
|
+
function toHex(bytes) {
|
|
5436
|
+
let out = "";
|
|
5437
|
+
for (const byte of bytes) out += byte.toString(16).padStart(2, "0");
|
|
5438
|
+
return out;
|
|
5439
|
+
}
|
|
5440
|
+
async function sha256Digest(bytes) {
|
|
5441
|
+
const subtle = globalThis.crypto?.subtle;
|
|
5442
|
+
if (!subtle) return `sha256:${toHex(sha256Bytes(bytes))}`;
|
|
5443
|
+
const copy = new Uint8Array(bytes);
|
|
5444
|
+
const hash = await subtle.digest("SHA-256", copy.buffer);
|
|
5445
|
+
return `sha256:${toHex(new Uint8Array(hash))}`;
|
|
5446
|
+
}
|
|
5447
|
+
function textToBytes(text) {
|
|
5448
|
+
return new TextEncoder().encode(text);
|
|
5449
|
+
}
|
|
5450
|
+
function bytesToText(bytes) {
|
|
5451
|
+
return new TextDecoder().decode(bytes);
|
|
5452
|
+
}
|
|
5453
|
+
function base64ToBytes(blob) {
|
|
5454
|
+
const binary = atob(blob);
|
|
5455
|
+
const bytes = new Uint8Array(binary.length);
|
|
5456
|
+
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
|
|
5457
|
+
return bytes;
|
|
5458
|
+
}
|
|
5459
|
+
function skillFileBytes(contents) {
|
|
5460
|
+
if (typeof contents.text === "string") return textToBytes(contents.text);
|
|
5461
|
+
if (typeof contents.blob === "string") return base64ToBytes(contents.blob);
|
|
5462
|
+
throw new Error("resources/read returned neither text nor blob content.");
|
|
5463
|
+
}
|
|
5464
|
+
async function verifySkillResource(resource, bytes) {
|
|
5465
|
+
const expectedSize = resource.size;
|
|
5466
|
+
if (expectedSize !== void 0 && expectedSize !== bytes.byteLength) {
|
|
5467
|
+
return {
|
|
5468
|
+
status: "mismatch",
|
|
5469
|
+
expectedSize,
|
|
5470
|
+
actualSize: bytes.byteLength,
|
|
5471
|
+
...resource.digest !== void 0 ? { expectedDigest: resource.digest } : {},
|
|
5472
|
+
reason: `Manifest declares ${expectedSize} bytes but the fetched file is ${bytes.byteLength}.`
|
|
5473
|
+
};
|
|
5474
|
+
}
|
|
5475
|
+
const expectedDigest = resource.digest;
|
|
5476
|
+
if (expectedDigest === void 0) {
|
|
5477
|
+
return {
|
|
5478
|
+
status: "unverifiable",
|
|
5479
|
+
reason: "The manifest entry advertises no digest."
|
|
5480
|
+
};
|
|
5481
|
+
}
|
|
5482
|
+
if (!DIGEST_PATTERN.test(expectedDigest)) {
|
|
5483
|
+
return {
|
|
5484
|
+
status: "unverifiable",
|
|
5485
|
+
expectedDigest,
|
|
5486
|
+
reason: 'The advertised digest is not "sha256:" followed by 64 lowercase hex characters.'
|
|
5487
|
+
};
|
|
5488
|
+
}
|
|
5489
|
+
const actualDigest = await sha256Digest(bytes);
|
|
5490
|
+
return {
|
|
5491
|
+
status: actualDigest === expectedDigest ? "verified" : "mismatch",
|
|
5492
|
+
actualDigest,
|
|
5493
|
+
expectedDigest,
|
|
5494
|
+
...expectedSize !== void 0 ? { expectedSize, actualSize: bytes.byteLength } : {}
|
|
5495
|
+
};
|
|
5496
|
+
}
|
|
5497
|
+
function jsonLikeEqual(a, b) {
|
|
5498
|
+
if (typeof a === "number" || typeof b === "number") {
|
|
5499
|
+
if (typeof a === "number" && typeof b === "number") {
|
|
5500
|
+
return Number.isFinite(a) && Number.isFinite(b) ? a === b : Object.is(a, b);
|
|
5501
|
+
}
|
|
5502
|
+
return false;
|
|
5503
|
+
}
|
|
5504
|
+
if (a === null || b === null) return a === b;
|
|
5505
|
+
if (typeof a !== "object" || typeof b !== "object") return Object.is(a, b);
|
|
5506
|
+
const aArray = Array.isArray(a);
|
|
5507
|
+
if (aArray !== Array.isArray(b)) return false;
|
|
5508
|
+
if (aArray) {
|
|
5509
|
+
const x2 = a;
|
|
5510
|
+
const y2 = b;
|
|
5511
|
+
return x2.length === y2.length && x2.every((v, i) => jsonLikeEqual(v, y2[i]));
|
|
5512
|
+
}
|
|
5513
|
+
const x = a;
|
|
5514
|
+
const y = b;
|
|
5515
|
+
const keys = Object.keys(x);
|
|
5516
|
+
return keys.length === Object.keys(y).length && keys.every((k) => Object.hasOwn(y, k) && jsonLikeEqual(x[k], y[k]));
|
|
5517
|
+
}
|
|
5518
|
+
function displayValue(value) {
|
|
5519
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
5520
|
+
return String(value);
|
|
4034
5521
|
}
|
|
4035
|
-
|
|
4036
|
-
|
|
5522
|
+
return JSON.stringify(
|
|
5523
|
+
canonicalize(value),
|
|
5524
|
+
(_key, member) => typeof member === "number" && !Number.isFinite(member) ? `<${String(member)}>` : member
|
|
5525
|
+
);
|
|
5526
|
+
}
|
|
5527
|
+
function checkSkillFrontmatterMatch(entry, skillFileText) {
|
|
5528
|
+
const { frontmatter } = splitSkillFile(skillFileText);
|
|
5529
|
+
if (frontmatter === void 0) {
|
|
5530
|
+
return [
|
|
5531
|
+
{
|
|
5532
|
+
code: "frontmatter-absent",
|
|
5533
|
+
severity: "error",
|
|
5534
|
+
message: "The served SKILL.md carries no YAML frontmatter block, so the listing's frontmatter cannot be the file's.",
|
|
5535
|
+
resourceUri: entry.uri
|
|
5536
|
+
}
|
|
5537
|
+
];
|
|
4037
5538
|
}
|
|
4038
|
-
|
|
5539
|
+
const listedError = jsonGraphError(entry.frontmatter);
|
|
5540
|
+
if (listedError) {
|
|
5541
|
+
return [
|
|
5542
|
+
{
|
|
5543
|
+
code: "frontmatter-unparsable",
|
|
5544
|
+
severity: "error",
|
|
5545
|
+
message: `The listing's own frontmatter cannot be compared: ${listedError}`,
|
|
5546
|
+
resourceUri: entry.uri
|
|
5547
|
+
}
|
|
5548
|
+
];
|
|
5549
|
+
}
|
|
5550
|
+
const parsed = parseSkillFrontmatter(frontmatter);
|
|
5551
|
+
if ("error" in parsed) {
|
|
5552
|
+
return [
|
|
5553
|
+
{
|
|
5554
|
+
code: "frontmatter-unparsable",
|
|
5555
|
+
severity: "error",
|
|
5556
|
+
message: `The served SKILL.md's frontmatter is not valid YAML: ${parsed.error}`,
|
|
5557
|
+
resourceUri: entry.uri
|
|
5558
|
+
}
|
|
5559
|
+
];
|
|
5560
|
+
}
|
|
5561
|
+
const issues = [];
|
|
5562
|
+
const fields = [
|
|
5563
|
+
.../* @__PURE__ */ new Set([
|
|
5564
|
+
...Object.keys(entry.frontmatter),
|
|
5565
|
+
...Object.keys(parsed.fields)
|
|
5566
|
+
])
|
|
5567
|
+
].sort();
|
|
5568
|
+
for (const field of fields) {
|
|
5569
|
+
const listed = entry.frontmatter[field];
|
|
5570
|
+
const served = parsed.fields[field];
|
|
5571
|
+
if (listed === void 0) {
|
|
5572
|
+
issues.push({
|
|
5573
|
+
code: "frontmatter-mismatch",
|
|
5574
|
+
severity: "error",
|
|
5575
|
+
message: `The served SKILL.md declares "${field}" but the listing's frontmatter omits it.`,
|
|
5576
|
+
resourceUri: entry.uri
|
|
5577
|
+
});
|
|
5578
|
+
continue;
|
|
5579
|
+
}
|
|
5580
|
+
if (served === void 0) {
|
|
5581
|
+
issues.push({
|
|
5582
|
+
code: "frontmatter-mismatch",
|
|
5583
|
+
severity: "error",
|
|
5584
|
+
message: `The listing declares "${field}" but the served SKILL.md omits it.`,
|
|
5585
|
+
resourceUri: entry.uri
|
|
5586
|
+
});
|
|
5587
|
+
continue;
|
|
5588
|
+
}
|
|
5589
|
+
if (!jsonLikeEqual(listed, served)) {
|
|
5590
|
+
issues.push({
|
|
5591
|
+
code: "frontmatter-mismatch",
|
|
5592
|
+
severity: "error",
|
|
5593
|
+
message: `Field "${field}" differs: the listing says ${displayValue(listed)} but the served SKILL.md says ${displayValue(served)}.`,
|
|
5594
|
+
resourceUri: entry.uri
|
|
5595
|
+
});
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
return issues;
|
|
5599
|
+
}
|
|
5600
|
+
var COLLISION_SAMPLE = 3;
|
|
5601
|
+
function checkSkillNameCollisions(entries) {
|
|
5602
|
+
const byName = /* @__PURE__ */ new Map();
|
|
5603
|
+
for (const entry of entries) {
|
|
5604
|
+
const name = entry.frontmatter.name;
|
|
5605
|
+
if (typeof name !== "string" || name.trim() === "") continue;
|
|
5606
|
+
const group = byName.get(name);
|
|
5607
|
+
if (group) group.push(entry);
|
|
5608
|
+
else byName.set(name, [entry]);
|
|
5609
|
+
}
|
|
5610
|
+
const issues = /* @__PURE__ */ new Map();
|
|
5611
|
+
for (const [name, group] of byName) {
|
|
5612
|
+
const identities = new Set(group.map((e) => skillUriIdentity(e.uri)));
|
|
5613
|
+
if (identities.size < 2) continue;
|
|
5614
|
+
const uris = [...identities].sort();
|
|
5615
|
+
for (const entry of group) {
|
|
5616
|
+
const self = skillUriIdentity(entry.uri);
|
|
5617
|
+
const sample = [];
|
|
5618
|
+
for (const uri of uris) {
|
|
5619
|
+
if (uri === self) continue;
|
|
5620
|
+
sample.push(uri);
|
|
5621
|
+
if (sample.length === COLLISION_SAMPLE) break;
|
|
5622
|
+
}
|
|
5623
|
+
const unshown = identities.size - 1 - sample.length;
|
|
5624
|
+
const others = unshown > 0 ? `${sample.join(", ")}, and ${unshown} more` : sample.join(", ");
|
|
5625
|
+
const subject = identities.size === 2 ? "Another skill in this listing also declares" : `${identities.size - 1} other skills in this listing also declare`;
|
|
5626
|
+
issues.set(self, {
|
|
5627
|
+
code: "duplicate-name",
|
|
5628
|
+
severity: "warning",
|
|
5629
|
+
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.`
|
|
5630
|
+
});
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
return issues;
|
|
4039
5634
|
}
|
|
4040
5635
|
|
|
4041
5636
|
// ../../core/mcp/appElicitation.ts
|
|
@@ -4125,7 +5720,7 @@ import {
|
|
|
4125
5720
|
ListResourcesResultSchema,
|
|
4126
5721
|
ListResourceTemplatesResultSchema,
|
|
4127
5722
|
ListPromptsResultSchema,
|
|
4128
|
-
ResourceSchema,
|
|
5723
|
+
ResourceSchema as ResourceSchema2,
|
|
4129
5724
|
ResourceTemplateSchema,
|
|
4130
5725
|
PromptSchema
|
|
4131
5726
|
} from "@modelcontextprotocol/core";
|
|
@@ -4148,8 +5743,8 @@ function validateToolOutput(provider, tool, result) {
|
|
|
4148
5743
|
}
|
|
4149
5744
|
|
|
4150
5745
|
// ../../core/mcp/listSalvage.ts
|
|
4151
|
-
import { z as
|
|
4152
|
-
import { SdkError, SdkErrorCode } from "@modelcontextprotocol/client";
|
|
5746
|
+
import { z as z3 } from "zod/v4";
|
|
5747
|
+
import { SdkError as SdkError2, SdkErrorCode as SdkErrorCode2 } from "@modelcontextprotocol/client";
|
|
4153
5748
|
import { ToolSchema } from "@modelcontextprotocol/core";
|
|
4154
5749
|
var LIST_MAX_PAGES = 64;
|
|
4155
5750
|
function listPaginationExceeded(method) {
|
|
@@ -4158,26 +5753,26 @@ function listPaginationExceeded(method) {
|
|
|
4158
5753
|
);
|
|
4159
5754
|
}
|
|
4160
5755
|
function isClientDecodeRejection(err) {
|
|
4161
|
-
return
|
|
5756
|
+
return SdkError2.isInstance(err) && (err.code === SdkErrorCode2.InvalidResult || err.code === SdkErrorCode2.UnsupportedResultType);
|
|
4162
5757
|
}
|
|
4163
5758
|
function isSalvageableRejection(err) {
|
|
4164
|
-
return
|
|
5759
|
+
return SdkError2.isInstance(err) && err.code === SdkErrorCode2.InvalidResult;
|
|
4165
5760
|
}
|
|
4166
5761
|
function lenientListPageSchema(resultSchema, itemsKey) {
|
|
4167
|
-
return resultSchema.extend({ [itemsKey]:
|
|
5762
|
+
return resultSchema.extend({ [itemsKey]: z3.array(z3.unknown()) });
|
|
4168
5763
|
}
|
|
4169
5764
|
function nextCursorOf(page) {
|
|
4170
5765
|
if (typeof page !== "object" || page === null) return void 0;
|
|
4171
5766
|
const value = page.nextCursor;
|
|
4172
5767
|
return typeof value === "string" ? value : void 0;
|
|
4173
5768
|
}
|
|
4174
|
-
var ModernResultEnvelopeSchema =
|
|
4175
|
-
resultType:
|
|
5769
|
+
var ModernResultEnvelopeSchema = z3.looseObject({
|
|
5770
|
+
resultType: z3.literal("complete"),
|
|
4176
5771
|
// A non-negative INTEGER, matching the codec: `ttlMs: -1` or `0.5` is an
|
|
4177
5772
|
// envelope violation the strict path rejects, and accepting it here on the
|
|
4178
5773
|
// strength of an unrelated bad entry would hide it.
|
|
4179
|
-
ttlMs:
|
|
4180
|
-
cacheScope:
|
|
5774
|
+
ttlMs: z3.int().min(0),
|
|
5775
|
+
cacheScope: z3.enum(["public", "private"])
|
|
4181
5776
|
});
|
|
4182
5777
|
function rawItemsOf(page, itemsKey) {
|
|
4183
5778
|
if (typeof page !== "object" || page === null) return void 0;
|
|
@@ -4246,9 +5841,9 @@ var LegacyToolWireSchema = ToolSchema.extend({
|
|
|
4246
5841
|
outputSchema: ToolSchema.shape.inputSchema.optional()
|
|
4247
5842
|
});
|
|
4248
5843
|
var ModernToolWireSchema = ToolSchema.extend({
|
|
4249
|
-
inputSchema:
|
|
4250
|
-
type:
|
|
4251
|
-
$schema:
|
|
5844
|
+
inputSchema: z3.looseObject({
|
|
5845
|
+
type: z3.literal("object"),
|
|
5846
|
+
$schema: z3.string().optional()
|
|
4252
5847
|
})
|
|
4253
5848
|
});
|
|
4254
5849
|
function stripExecution(tool) {
|
|
@@ -4262,10 +5857,10 @@ function eraToolItemSchema(wire, normalize) {
|
|
|
4262
5857
|
|
|
4263
5858
|
// ../../core/mcp/taskNotificationSchemas.ts
|
|
4264
5859
|
import { NotificationSchema } from "@modelcontextprotocol/core";
|
|
4265
|
-
import { z as
|
|
5860
|
+
import { z as z4 } from "zod/v4";
|
|
4266
5861
|
var TasksListChangedNotificationSchema = NotificationSchema.extend({
|
|
4267
|
-
method:
|
|
4268
|
-
params:
|
|
5862
|
+
method: z4.literal("notifications/tasks/list_changed"),
|
|
5863
|
+
params: z4.record(z4.string(), z4.unknown()).optional()
|
|
4269
5864
|
});
|
|
4270
5865
|
|
|
4271
5866
|
// ../../core/mcp/uriTemplate.ts
|
|
@@ -4471,6 +6066,52 @@ var TypedEventGeneric = class extends CustomEvent {
|
|
|
4471
6066
|
}
|
|
4472
6067
|
};
|
|
4473
6068
|
var TypedEventTarget = class extends EventTarget {
|
|
6069
|
+
/**
|
|
6070
|
+
* Per-event-type dispatch counter. Read through `getEventRevision` by
|
|
6071
|
+
* `useStoreSnapshot` (core/react) to tell "nothing has happened on this
|
|
6072
|
+
* event since I last looked" from "something has", without comparing the
|
|
6073
|
+
* snapshot's contents (#1955).
|
|
6074
|
+
*/
|
|
6075
|
+
eventRevisions = /* @__PURE__ */ new Map();
|
|
6076
|
+
/**
|
|
6077
|
+
* How many times an event of this type has been dispatched from this
|
|
6078
|
+
* target. Monotonic per type, and per type only: an `errorChange` does not
|
|
6079
|
+
* advance `toolsChange`, so a subscriber to one is not re-read because of
|
|
6080
|
+
* the other.
|
|
6081
|
+
*
|
|
6082
|
+
* The counter — not the list's contents — is what a `useSyncExternalStore`
|
|
6083
|
+
* snapshot is cached against. That distinction is load-bearing rather than
|
|
6084
|
+
* an optimization: these stores hand out a defensive copy, so comparing
|
|
6085
|
+
* contents is the only alternative, and a contents comparison cannot see an
|
|
6086
|
+
* in-place mutation of an entry the list already holds. `MessageLogState`
|
|
6087
|
+
* does exactly that when a response is folded into its request entry
|
|
6088
|
+
* (filling in `response`/`duration`), and it dispatches — so the counter
|
|
6089
|
+
* moves and the fold reaches the UI, where a shallow compare would report
|
|
6090
|
+
* "unchanged" and never re-render it.
|
|
6091
|
+
*/
|
|
6092
|
+
getEventRevision(type) {
|
|
6093
|
+
return this.eventRevisions.get(type) ?? 0;
|
|
6094
|
+
}
|
|
6095
|
+
/**
|
|
6096
|
+
* Overridden solely to advance the counter. It sits on `dispatchEvent`
|
|
6097
|
+
* rather than on `dispatchTypedEvent` because that is the one funnel every
|
|
6098
|
+
* dispatch passes through — the typed helper below delegates to it, and so
|
|
6099
|
+
* does a hand-rolled `dispatchEvent(new CustomEvent(...))`, which the tests
|
|
6100
|
+
* use extensively. A counter bumped only in the typed helper would be
|
|
6101
|
+
* silently stale for those, i.e. exactly where nobody would look for it.
|
|
6102
|
+
*
|
|
6103
|
+
* Bumped BEFORE dispatching: the state mutation always precedes the
|
|
6104
|
+
* dispatch, so by the time a listener runs the counter must already reflect
|
|
6105
|
+
* it, or a snapshot read from inside that listener would return the cached
|
|
6106
|
+
* pre-change value.
|
|
6107
|
+
*/
|
|
6108
|
+
dispatchEvent(event) {
|
|
6109
|
+
this.eventRevisions.set(
|
|
6110
|
+
event.type,
|
|
6111
|
+
(this.eventRevisions.get(event.type) ?? 0) + 1
|
|
6112
|
+
);
|
|
6113
|
+
return super.dispatchEvent(event);
|
|
6114
|
+
}
|
|
4474
6115
|
dispatchTypedEvent(type, ...args) {
|
|
4475
6116
|
const detail = args[0] ?? void 0;
|
|
4476
6117
|
this.dispatchEvent(new TypedEventGeneric(type, detail));
|
|
@@ -4859,6 +6500,115 @@ function withOAuthEndpointOverrides(fetchFn, getOverrides) {
|
|
|
4859
6500
|
};
|
|
4860
6501
|
}
|
|
4861
6502
|
|
|
6503
|
+
// ../../core/auth/oidcDiscoveryCompat.ts
|
|
6504
|
+
import {
|
|
6505
|
+
OAuthMetadataSchema,
|
|
6506
|
+
OpenIdProviderDiscoveryMetadataSchema
|
|
6507
|
+
} from "@modelcontextprotocol/core";
|
|
6508
|
+
var RFC8414_WELL_KNOWN = "/.well-known/oauth-authorization-server";
|
|
6509
|
+
var OIDC_WELL_KNOWN = "/.well-known/openid-configuration";
|
|
6510
|
+
var COMPAT_SOURCE_HEADER = "x-inspector-oauth-metadata-source";
|
|
6511
|
+
function oidcDiscoveryCandidates(rfc8414Url) {
|
|
6512
|
+
let url;
|
|
6513
|
+
try {
|
|
6514
|
+
url = new URL(rfc8414Url);
|
|
6515
|
+
} catch {
|
|
6516
|
+
return [];
|
|
6517
|
+
}
|
|
6518
|
+
if (!url.pathname.startsWith(RFC8414_WELL_KNOWN)) return [];
|
|
6519
|
+
const path5 = url.pathname.slice(RFC8414_WELL_KNOWN.length);
|
|
6520
|
+
if (path5 !== "" && !path5.startsWith("/")) return [];
|
|
6521
|
+
if (path5 === "") return [new URL(OIDC_WELL_KNOWN, url.origin).href];
|
|
6522
|
+
return [
|
|
6523
|
+
new URL(`${OIDC_WELL_KNOWN}${path5}`, url.origin).href,
|
|
6524
|
+
new URL(`${path5}${OIDC_WELL_KNOWN}`, url.origin).href
|
|
6525
|
+
];
|
|
6526
|
+
}
|
|
6527
|
+
function continuesDiscovery(status) {
|
|
6528
|
+
return status >= 400 && status < 500 || status === 502;
|
|
6529
|
+
}
|
|
6530
|
+
async function releaseBody(response) {
|
|
6531
|
+
await response.body?.cancel().catch(() => {
|
|
6532
|
+
});
|
|
6533
|
+
}
|
|
6534
|
+
function isRfc8414OnlyMetadata(value) {
|
|
6535
|
+
if (!OAuthMetadataSchema.safeParse(value).success) return false;
|
|
6536
|
+
return !OpenIdProviderDiscoveryMetadataSchema.safeParse(value).success;
|
|
6537
|
+
}
|
|
6538
|
+
function requestUrlOf2(input) {
|
|
6539
|
+
if (typeof input === "string") return input;
|
|
6540
|
+
if (input instanceof URL) return input.href;
|
|
6541
|
+
return input.url;
|
|
6542
|
+
}
|
|
6543
|
+
function discoveryHeaders(input, init) {
|
|
6544
|
+
if (init?.headers) return new Headers(init.headers);
|
|
6545
|
+
if (typeof input !== "string" && !(input instanceof URL)) {
|
|
6546
|
+
return new Headers(input.headers);
|
|
6547
|
+
}
|
|
6548
|
+
return new Headers();
|
|
6549
|
+
}
|
|
6550
|
+
function requestMethodOf(input, init) {
|
|
6551
|
+
if (init?.method) return init.method.toUpperCase();
|
|
6552
|
+
if (typeof input !== "string" && !(input instanceof URL)) {
|
|
6553
|
+
return input.method.toUpperCase();
|
|
6554
|
+
}
|
|
6555
|
+
return "GET";
|
|
6556
|
+
}
|
|
6557
|
+
function withRfc8414OidcCompat(fetchFn) {
|
|
6558
|
+
return async (input, init) => {
|
|
6559
|
+
const response = await fetchFn(input, init);
|
|
6560
|
+
if (response.ok) return response;
|
|
6561
|
+
if (!continuesDiscovery(response.status)) return response;
|
|
6562
|
+
if (requestMethodOf(input, init) !== "GET") return response;
|
|
6563
|
+
const candidates = oidcDiscoveryCandidates(requestUrlOf2(input));
|
|
6564
|
+
if (candidates.length === 0) return response;
|
|
6565
|
+
const headers = discoveryHeaders(input, init);
|
|
6566
|
+
for (const candidate of candidates) {
|
|
6567
|
+
let probe;
|
|
6568
|
+
try {
|
|
6569
|
+
probe = await fetchFn(candidate, { headers });
|
|
6570
|
+
} catch {
|
|
6571
|
+
return response;
|
|
6572
|
+
}
|
|
6573
|
+
if (!probe.ok) {
|
|
6574
|
+
await releaseBody(probe);
|
|
6575
|
+
if (continuesDiscovery(probe.status)) continue;
|
|
6576
|
+
return response;
|
|
6577
|
+
}
|
|
6578
|
+
let parsed;
|
|
6579
|
+
let body;
|
|
6580
|
+
try {
|
|
6581
|
+
body = await probe.text();
|
|
6582
|
+
parsed = JSON.parse(body);
|
|
6583
|
+
} catch {
|
|
6584
|
+
return response;
|
|
6585
|
+
}
|
|
6586
|
+
if (!isRfc8414OnlyMetadata(parsed)) {
|
|
6587
|
+
return response;
|
|
6588
|
+
}
|
|
6589
|
+
const source = probe.url || candidate;
|
|
6590
|
+
await releaseBody(response);
|
|
6591
|
+
console.warn(
|
|
6592
|
+
`[oauth] ${source} returned RFC 8414 OAuth 2.0 authorization server metadata, not an OpenID provider document. The MCP TypeScript SDK validates that path as OpenID Connect Discovery and would reject it (modelcontextprotocol/typescript-sdk#2733), so the Inspector is handing it to discovery as the RFC 8414 document it is.`
|
|
6593
|
+
);
|
|
6594
|
+
return new Response(body, {
|
|
6595
|
+
status: 200,
|
|
6596
|
+
statusText: "OK",
|
|
6597
|
+
headers: {
|
|
6598
|
+
"content-type": "application/json",
|
|
6599
|
+
// The substituted response is what the fetch trackers above this
|
|
6600
|
+
// wrapper record, so it says where its body actually came from
|
|
6601
|
+
// rather than letting the Network tab imply the RFC 8414 path
|
|
6602
|
+
// answered. Named on the response, not logged only, so it survives
|
|
6603
|
+
// into the captured entry.
|
|
6604
|
+
[COMPAT_SOURCE_HEADER]: source
|
|
6605
|
+
}
|
|
6606
|
+
});
|
|
6607
|
+
}
|
|
6608
|
+
return response;
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
6611
|
+
|
|
4862
6612
|
// ../../core/logging/logger.ts
|
|
4863
6613
|
var noop = () => {
|
|
4864
6614
|
};
|
|
@@ -5091,8 +6841,8 @@ var BaseOAuthClientProvider = class {
|
|
|
5091
6841
|
);
|
|
5092
6842
|
}
|
|
5093
6843
|
async saveClientInformation(clientInformation, options) {
|
|
5094
|
-
const registrationKind = options && "registrationKind" in options ? options.registrationKind : "dcr";
|
|
5095
6844
|
const issuer = options && "issuer" in options ? options.issuer : void 0;
|
|
6845
|
+
const registrationKind = options && "registrationKind" in options ? options.registrationKind : await this.resolveSdkRegistrationKind(clientInformation, issuer);
|
|
5096
6846
|
await this.storage.saveClientInformation(
|
|
5097
6847
|
this.serverUrl,
|
|
5098
6848
|
clientInformation,
|
|
@@ -5102,6 +6852,70 @@ var BaseOAuthClientProvider = class {
|
|
|
5102
6852
|
}
|
|
5103
6853
|
);
|
|
5104
6854
|
}
|
|
6855
|
+
/**
|
|
6856
|
+
* Resolve the registration kind for a save that carries no explicit one — that
|
|
6857
|
+
* is, one the SDK made. SDK v2's `saveClientInformation` contract passes only
|
|
6858
|
+
* `{ issuer }`, so the mechanism cannot be handed to us; treating every such
|
|
6859
|
+
* save as DCR is what relabeled a CIMD registration `Dynamic (DCR)` in
|
|
6860
|
+
* Connection Info the moment the SDK bound it to an issuer (#2242).
|
|
6861
|
+
*
|
|
6862
|
+
* Two cases reach here, and they are told apart by whether a registration
|
|
6863
|
+
* already exists for this issuer:
|
|
6864
|
+
*
|
|
6865
|
+
* - **A back-stamp.** A registration is already stored for this issuer under
|
|
6866
|
+
* this `client_id`, and the SDK is only adding the `issuer` to it. Its
|
|
6867
|
+
* recorded kind is the answer — kind and credential are written and cleared
|
|
6868
|
+
* together, so a stored registration always has one.
|
|
6869
|
+
* - **A new registration.** Nothing is stored for this issuer, so this save
|
|
6870
|
+
* creates it. SDK v2 `auth()` reaches its URL-based-client-ID branch — rather
|
|
6871
|
+
* than `registerClient` — exactly when the AS advertises
|
|
6872
|
+
* `client_id_metadata_document_supported` and a `clientMetadataUrl` is
|
|
6873
|
+
* configured, and it persists the AS metadata via `saveDiscoveryState`
|
|
6874
|
+
* *before* it reads or writes client information. So the branch it took is
|
|
6875
|
+
* not inferred here, it is read back from the state it just wrote.
|
|
6876
|
+
*
|
|
6877
|
+
* This is why the check is not "the `client_id` looks like our metadata URL".
|
|
6878
|
+
* RFC 7591 §3.2 makes a dynamically issued `client_id` opaque, so an AS may
|
|
6879
|
+
* mint that very URL from `POST /register`; the URL comparison only decides
|
|
6880
|
+
* whether CIMD is *in play* for this connection, and the two cases above decide
|
|
6881
|
+
* what actually happened (#2242, Copilot).
|
|
6882
|
+
*
|
|
6883
|
+
* Consequences worth stating, since each was a defect on the way here:
|
|
6884
|
+
*
|
|
6885
|
+
* - An existing DCR whose `client_id` happens to be the metadata URL stays
|
|
6886
|
+
* `dcr` — it takes the back-stamp path and its recorded kind says so.
|
|
6887
|
+
* - `invalidateCredentials("client")`, which SDK `auth()` calls on
|
|
6888
|
+
* `invalid_client` before retrying, clears the registration and its kind. The
|
|
6889
|
+
* retry therefore takes the new-registration path and is answered from
|
|
6890
|
+
* discovery state, which that clear does not touch.
|
|
6891
|
+
* - A second AS behind one resource gets its own answer, since discovery state
|
|
6892
|
+
* describes the issuer the SDK actually resolved. One that does not advertise
|
|
6893
|
+
* CIMD is `dcr` even when it mints the metadata URL as its `client_id`.
|
|
6894
|
+
* - A transient failure in our own CIMD preflight costs nothing: the SDK's own
|
|
6895
|
+
* discovery is what this reads.
|
|
6896
|
+
*/
|
|
6897
|
+
async resolveSdkRegistrationKind(clientInformation, issuer) {
|
|
6898
|
+
const clientMetadataUrl = this.clientMetadataUrl?.trim();
|
|
6899
|
+
if (!clientMetadataUrl || clientInformation.client_id !== clientMetadataUrl) {
|
|
6900
|
+
return "dcr";
|
|
6901
|
+
}
|
|
6902
|
+
const stored = await this.storage.getClientInformation(
|
|
6903
|
+
this.serverUrl,
|
|
6904
|
+
false,
|
|
6905
|
+
issuer
|
|
6906
|
+
);
|
|
6907
|
+
if (stored?.client_id === clientMetadataUrl) {
|
|
6908
|
+
const storedKind = await this.storage.getClientRegistrationKind(
|
|
6909
|
+
this.serverUrl,
|
|
6910
|
+
issuer
|
|
6911
|
+
);
|
|
6912
|
+
return storedKind === "cimd" ? "cimd" : "dcr";
|
|
6913
|
+
}
|
|
6914
|
+
const discovery = await this.storage.getDiscoveryState(this.serverUrl);
|
|
6915
|
+
const metadata = discovery?.authorizationServerMetadata;
|
|
6916
|
+
if (issuer !== void 0 && metadata?.issuer !== issuer) return "dcr";
|
|
6917
|
+
return metadata?.client_id_metadata_document_supported === true ? "cimd" : "dcr";
|
|
6918
|
+
}
|
|
5105
6919
|
async saveScope(scope) {
|
|
5106
6920
|
await this.storage.saveScope(this.serverUrl, scope);
|
|
5107
6921
|
this.cachedScope = scope;
|
|
@@ -5277,7 +7091,7 @@ import {
|
|
|
5277
7091
|
exchangeAuthorization,
|
|
5278
7092
|
startAuthorization
|
|
5279
7093
|
} from "@modelcontextprotocol/client";
|
|
5280
|
-
import { OAuthMetadataSchema } from "@modelcontextprotocol/core";
|
|
7094
|
+
import { OAuthMetadataSchema as OAuthMetadataSchema2 } from "@modelcontextprotocol/core";
|
|
5281
7095
|
|
|
5282
7096
|
// ../../core/auth/ema/constants.ts
|
|
5283
7097
|
var GRANT_TYPE_JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
|
|
@@ -5326,7 +7140,7 @@ function idpOAuthStorageKey(issuer) {
|
|
|
5326
7140
|
}
|
|
5327
7141
|
|
|
5328
7142
|
// ../../core/auth/ema/tokenEndpoint.ts
|
|
5329
|
-
import { selectClientAuthMethod } from "@modelcontextprotocol/client";
|
|
7143
|
+
import { selectClientAuthMethod as selectClientAuthMethod2 } from "@modelcontextprotocol/client";
|
|
5330
7144
|
async function parseOAuthTokenErrorResponse(response, step) {
|
|
5331
7145
|
let body;
|
|
5332
7146
|
try {
|
|
@@ -5347,7 +7161,7 @@ async function parseOAuthTokenErrorResponse(response, step) {
|
|
|
5347
7161
|
}
|
|
5348
7162
|
function applyClientAuth(metadata, clientInformation, headers, body) {
|
|
5349
7163
|
const supported = metadata?.token_endpoint_auth_methods_supported ?? [];
|
|
5350
|
-
const method =
|
|
7164
|
+
const method = selectClientAuthMethod2(clientInformation, supported);
|
|
5351
7165
|
if (method === "client_secret_basic" && clientInformation.client_secret) {
|
|
5352
7166
|
const credentials = btoa(
|
|
5353
7167
|
`${clientInformation.client_id}:${clientInformation.client_secret}`
|
|
@@ -5401,7 +7215,7 @@ async function discoverIdpMetadata(issuer, fetchFn) {
|
|
|
5401
7215
|
`Failed to discover OIDC metadata for IdP issuer ${issuer}`
|
|
5402
7216
|
);
|
|
5403
7217
|
}
|
|
5404
|
-
return
|
|
7218
|
+
return OAuthMetadataSchema2.parse(metadata);
|
|
5405
7219
|
}
|
|
5406
7220
|
async function startIdpOidcAuthorization(params) {
|
|
5407
7221
|
const issuer = normalizeIdpIssuer(params.idp.issuer);
|
|
@@ -5657,7 +7471,7 @@ async function discoverResourceAsMetadata(resourceAsUrl, fetchFn) {
|
|
|
5657
7471
|
|
|
5658
7472
|
// ../../core/auth/ema/wire.ts
|
|
5659
7473
|
import { discoverAndRequestJwtAuthGrant } from "@modelcontextprotocol/client";
|
|
5660
|
-
import { OAuthTokensSchema as
|
|
7474
|
+
import { OAuthTokensSchema as OAuthTokensSchema3 } from "@modelcontextprotocol/core";
|
|
5661
7475
|
async function exchangeIdJag(params) {
|
|
5662
7476
|
const issuer = normalizeIdpIssuer(params.idp.issuer);
|
|
5663
7477
|
const resource = params.resource.trim();
|
|
@@ -5736,7 +7550,7 @@ async function redeemIdJagForAccessToken(params) {
|
|
|
5736
7550
|
"EMA leg 3 (resource AS JWT bearer grant)"
|
|
5737
7551
|
);
|
|
5738
7552
|
}
|
|
5739
|
-
return
|
|
7553
|
+
return OAuthTokensSchema3.parse(await response.json());
|
|
5740
7554
|
}
|
|
5741
7555
|
|
|
5742
7556
|
// ../../core/auth/ema/emaFlow.ts
|
|
@@ -5980,35 +7794,49 @@ import { discoverOAuthProtectedResourceMetadata as discoverOAuthProtectedResourc
|
|
|
5980
7794
|
async function ensureCimdClientRegistration(params) {
|
|
5981
7795
|
const clientMetadataUrl = params.provider.clientMetadataUrl?.trim();
|
|
5982
7796
|
if (!clientMetadataUrl) return;
|
|
5983
|
-
|
|
5984
|
-
if (
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
7797
|
+
let metadata = (await params.provider.discoveryState())?.authorizationServerMetadata;
|
|
7798
|
+
if (!metadata) {
|
|
7799
|
+
let resourceMetadata;
|
|
7800
|
+
try {
|
|
7801
|
+
resourceMetadata = await discoverOAuthProtectedResourceMetadata2(
|
|
7802
|
+
params.serverUrl,
|
|
7803
|
+
{ resourceMetadataUrl: params.resourceMetadataUrl },
|
|
7804
|
+
// The same fetch the AS-metadata leg below uses. On web that is
|
|
7805
|
+
// `createRemoteFetch`, which proxies through the backend to sidestep
|
|
7806
|
+
// CORS — on the global `fetch` this leg would fail in the browser, be
|
|
7807
|
+
// swallowed by the catch, and leave CIMD probing the wrong
|
|
7808
|
+
// authorization server (Copilot).
|
|
7809
|
+
params.fetchFn
|
|
7810
|
+
);
|
|
7811
|
+
} catch {
|
|
7812
|
+
resourceMetadata = void 0;
|
|
7813
|
+
}
|
|
7814
|
+
try {
|
|
7815
|
+
metadata = await discoverAuthorizationServerMetadataForServer(
|
|
7816
|
+
params.serverUrl,
|
|
7817
|
+
resourceMetadata,
|
|
7818
|
+
params.fetchFn
|
|
7819
|
+
);
|
|
7820
|
+
} catch {
|
|
7821
|
+
return;
|
|
7822
|
+
}
|
|
5999
7823
|
}
|
|
6000
|
-
const
|
|
6001
|
-
params.serverUrl,
|
|
6002
|
-
resourceMetadata,
|
|
6003
|
-
params.fetchFn
|
|
6004
|
-
);
|
|
7824
|
+
const issuer = metadata?.issuer;
|
|
6005
7825
|
if (!metadata?.client_id_metadata_document_supported) return;
|
|
6006
|
-
const
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
7826
|
+
const existing = await params.provider.clientInformation(
|
|
7827
|
+
issuer ? { issuer } : void 0
|
|
7828
|
+
);
|
|
7829
|
+
if (existing?.client_id) return;
|
|
7830
|
+
await params.provider.saveClientInformation(
|
|
7831
|
+
{ client_id: clientMetadataUrl },
|
|
7832
|
+
{
|
|
7833
|
+
registrationKind: "cimd",
|
|
7834
|
+
// An AS metadata document without an `issuer` is malformed (RFC 8414 §2),
|
|
7835
|
+
// but the type allows it; fall back to the unkeyed slot rather than
|
|
7836
|
+
// inventing a key.
|
|
7837
|
+
...issuer && { issuer }
|
|
7838
|
+
}
|
|
7839
|
+
);
|
|
6012
7840
|
}
|
|
6013
7841
|
|
|
6014
7842
|
// ../../core/auth/ema/transportProvider.ts
|
|
@@ -6035,11 +7863,26 @@ var EmaTransportOAuthProvider = class {
|
|
|
6035
7863
|
state() {
|
|
6036
7864
|
return this.inner.state();
|
|
6037
7865
|
}
|
|
6038
|
-
|
|
6039
|
-
|
|
7866
|
+
// SEP-2352: `ctx` carries the authorization-server `issuer` the SDK resolved,
|
|
7867
|
+
// and the inner provider keys registrations by it. Dropping it here made every
|
|
7868
|
+
// EMA read and write land on the unkeyed slot — and, since #2242, left
|
|
7869
|
+
// `resolveSdkRegistrationKind` with no issuer to check, so a CIMD registration
|
|
7870
|
+
// made over an EMA connection was recorded as DCR (Copilot).
|
|
7871
|
+
clientInformation(ctx) {
|
|
7872
|
+
return this.inner.clientInformation(ctx);
|
|
7873
|
+
}
|
|
7874
|
+
saveClientInformation(clientInformation, ctx) {
|
|
7875
|
+
return this.inner.saveClientInformation(clientInformation, ctx);
|
|
7876
|
+
}
|
|
7877
|
+
// Without these the SDK persists no discovery state for an EMA connection, so
|
|
7878
|
+
// it re-discovers on every call, cannot perform its SEP-2352 callback-leg
|
|
7879
|
+
// authorization-server binding check (it warns as much), and — since #2242 —
|
|
7880
|
+
// leaves the registration-kind resolver nothing to read back.
|
|
7881
|
+
saveDiscoveryState(state) {
|
|
7882
|
+
return this.inner.saveDiscoveryState(state);
|
|
6040
7883
|
}
|
|
6041
|
-
|
|
6042
|
-
return this.inner.
|
|
7884
|
+
discoveryState() {
|
|
7885
|
+
return this.inner.discoveryState();
|
|
6043
7886
|
}
|
|
6044
7887
|
async tokens() {
|
|
6045
7888
|
const stored = await this.inner.tokens();
|
|
@@ -6108,8 +7951,11 @@ function issuerMismatchTitle() {
|
|
|
6108
7951
|
return "Authorization server mismatch";
|
|
6109
7952
|
}
|
|
6110
7953
|
var MAX_DISPLAYED_ISSUER_LENGTH = 120;
|
|
7954
|
+
function truncateUrlForDisplay(url) {
|
|
7955
|
+
return url.length > MAX_DISPLAYED_ISSUER_LENGTH ? `${url.slice(0, MAX_DISPLAYED_ISSUER_LENGTH)}\u2026` : url;
|
|
7956
|
+
}
|
|
6111
7957
|
function truncateIssuerForDisplay(issuer) {
|
|
6112
|
-
return issuer
|
|
7958
|
+
return truncateUrlForDisplay(issuer);
|
|
6113
7959
|
}
|
|
6114
7960
|
function issuerMismatchMessage(options) {
|
|
6115
7961
|
const target = options.serverName ? `"${options.serverName}"` : "this server";
|
|
@@ -6465,14 +8311,41 @@ var OAuthManager = class {
|
|
|
6465
8311
|
return void 0;
|
|
6466
8312
|
}
|
|
6467
8313
|
}
|
|
6468
|
-
|
|
8314
|
+
/**
|
|
8315
|
+
* Drop this server's local OAuth state, and revoke the grant at the
|
|
8316
|
+
* authorization server (RFC 7009).
|
|
8317
|
+
*
|
|
8318
|
+
* The state is taken and deleted in one atomic step, and the requests are
|
|
8319
|
+
* sent afterwards from what was taken. Both halves matter: the requests need
|
|
8320
|
+
* state the clear destroys, and the clear must not wait on the network, or a
|
|
8321
|
+
* fresh authorization completing during a five-second revocation would be
|
|
8322
|
+
* deleted by a clear reasoning about the grant it replaced.
|
|
8323
|
+
*
|
|
8324
|
+
* Best-effort by construction: every failure is reported through the returned
|
|
8325
|
+
* outcome and the clear has already happened regardless, because forgetting
|
|
8326
|
+
* the tokens is what the caller actually asked for (#2144).
|
|
8327
|
+
*
|
|
8328
|
+
* `options.revoke === false` skips the request. That is not only an escape
|
|
8329
|
+
* hatch for an authorization server that mishandles it — disconnecting while
|
|
8330
|
+
* still holding live tokens is a case a user may want to reproduce
|
|
8331
|
+
* deliberately, to watch how a server under test copes with it.
|
|
8332
|
+
*/
|
|
8333
|
+
async clearOAuthTokens(options) {
|
|
6469
8334
|
if (!this.oauthConfig?.storage) {
|
|
6470
|
-
return;
|
|
8335
|
+
return { status: "skipped", reason: "no_tokens" };
|
|
6471
8336
|
}
|
|
6472
8337
|
const serverUrl = this.getServerUrl();
|
|
6473
|
-
await
|
|
8338
|
+
const plan = await clearAndPlanRevocation({
|
|
8339
|
+
serverUrl,
|
|
8340
|
+
storage: this.oauthConfig.storage,
|
|
8341
|
+
enabled: options?.revoke
|
|
8342
|
+
});
|
|
6474
8343
|
this.oauthFlowState = null;
|
|
6475
8344
|
this.pendingAuthorizationScope = void 0;
|
|
8345
|
+
return executeOAuthRevocation(plan, {
|
|
8346
|
+
fetchFn: this.params.effectiveAuthFetch,
|
|
8347
|
+
logger: this.params.logger
|
|
8348
|
+
});
|
|
6476
8349
|
}
|
|
6477
8350
|
async isOAuthAuthorized() {
|
|
6478
8351
|
const tokens = await this.getOAuthTokens();
|
|
@@ -6799,10 +8672,11 @@ var OAuthManager = class {
|
|
|
6799
8672
|
await this.params.onBeforeOAuthRedirect(parsedState.authId);
|
|
6800
8673
|
}
|
|
6801
8674
|
}
|
|
6802
|
-
this.requireNavigation()
|
|
8675
|
+
const navigation = this.requireNavigation();
|
|
6803
8676
|
const provider = await this.createOAuthProvider();
|
|
6804
8677
|
const clientInfo = await provider.clientInformation();
|
|
6805
8678
|
await this.recordAuthorizationCodeFlowState(authorizationUrl, clientInfo);
|
|
8679
|
+
navigation.navigateToAuthorization(authorizationUrl);
|
|
6806
8680
|
this.params.dispatchOAuthAuthorizationRequired({ url: authorizationUrl });
|
|
6807
8681
|
}
|
|
6808
8682
|
requireNavigation() {
|
|
@@ -6959,6 +8833,24 @@ var RemoteClientTransport = class {
|
|
|
6959
8833
|
sseResponseWaits = /* @__PURE__ */ new Map();
|
|
6960
8834
|
options;
|
|
6961
8835
|
config;
|
|
8836
|
+
/**
|
|
8837
|
+
* Whether the upstream connection gives each request its own response stream
|
|
8838
|
+
* (#2140). The SDK's `Protocol.request` reads this: on a 2026-era connection
|
|
8839
|
+
* a transport advertising it has its per-request stream **aborted** as the
|
|
8840
|
+
* spec's cancellation signal, and no `notifications/cancelled` is sent; a
|
|
8841
|
+
* transport that does not gets the stdio mechanism instead.
|
|
8842
|
+
*
|
|
8843
|
+
* The real upstream transport lives on the backend, so this transport has to
|
|
8844
|
+
* answer for it. Streamable HTTP opens one POST — and one SSE response
|
|
8845
|
+
* stream — per request, so it qualifies; stdio and SSE multiplex every
|
|
8846
|
+
* request over one shared channel and must keep sending the notification.
|
|
8847
|
+
*
|
|
8848
|
+
* Advertising it is only half the fix: `requestSend` applies the SDK's
|
|
8849
|
+
* `requestSignal` to the browser-to-backend fetch, and `/api/mcp/send`
|
|
8850
|
+
* forwards that disconnect to the upstream `transport.send`, which is what
|
|
8851
|
+
* actually closes the stream the server is watching.
|
|
8852
|
+
*/
|
|
8853
|
+
hasPerRequestStream;
|
|
6962
8854
|
/**
|
|
6963
8855
|
* Intentionally returns undefined. The MCP Client checks transport.sessionId to detect
|
|
6964
8856
|
* reconnects and skip initialize. Our _sessionId is the remote server's session ID, not
|
|
@@ -7003,6 +8895,7 @@ var RemoteClientTransport = class {
|
|
|
7003
8895
|
constructor(options, config) {
|
|
7004
8896
|
this.options = options;
|
|
7005
8897
|
this.config = config;
|
|
8898
|
+
this.hasPerRequestStream = config.type === "streamable-http";
|
|
7006
8899
|
}
|
|
7007
8900
|
setAuthRecovery(handlers) {
|
|
7008
8901
|
this.options.authRecovery = handlers;
|
|
@@ -7367,7 +9260,8 @@ var RemoteClientTransport = class {
|
|
|
7367
9260
|
const res = await this.fetchFn(`${this.baseUrl}/api/mcp/send`, {
|
|
7368
9261
|
method: "POST",
|
|
7369
9262
|
headers: this.headers,
|
|
7370
|
-
body: JSON.stringify(body)
|
|
9263
|
+
body: JSON.stringify(body),
|
|
9264
|
+
...options?.requestSignal && { signal: options.requestSignal }
|
|
7371
9265
|
});
|
|
7372
9266
|
if (!res.ok) {
|
|
7373
9267
|
const text = await res.text();
|
|
@@ -7604,6 +9498,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7604
9498
|
// pending re-listen timer.
|
|
7605
9499
|
modernReconnectAttempts = 0;
|
|
7606
9500
|
modernReconnectTimer;
|
|
9501
|
+
// Set when the last `listen()` was rejected because the server answered it
|
|
9502
|
+
// with a bare graceful-closure result instead of acknowledging (#2097). That
|
|
9503
|
+
// condition is deterministic, so the reconnect machinery must not treat it as
|
|
9504
|
+
// a drop to retry; the flag is what carries the distinction from the rejection
|
|
9505
|
+
// site to the two failure handlers. Cleared by any user-initiated refresh (a
|
|
9506
|
+
// subscribe/unsubscribe is a fresh attempt, and the server may have changed).
|
|
9507
|
+
modernNeverAcknowledged = false;
|
|
7607
9508
|
// Task ids the user explicitly cancelled. A cancel makes the in-flight
|
|
7608
9509
|
// `callToolStream` reject with a generic -32603 error, which the stream's
|
|
7609
9510
|
// error path would otherwise report as a *failed* task — flashing "failed"
|
|
@@ -7625,9 +9526,12 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7625
9526
|
pendingRawWireRequests = /* @__PURE__ */ new Map();
|
|
7626
9527
|
rawWireRequestCounter = 0;
|
|
7627
9528
|
// Abort controller for the in-flight ordinary (non-task) tool call. Aborting
|
|
7628
|
-
// it
|
|
7629
|
-
//
|
|
7630
|
-
//
|
|
9529
|
+
// it hands the SDK the MCP cancellation flow for that request and rejects the
|
|
9530
|
+
// pending call, which `callTool` surfaces as a `ToolCallCancelledError`. Which
|
|
9531
|
+
// signal reaches the server is the transport's business, not ours: a
|
|
9532
|
+
// per-request-stream transport on a 2026-era connection has that request's
|
|
9533
|
+
// stream aborted, and everything else gets `notifications/cancelled` (#2140).
|
|
9534
|
+
// Undefined when no ordinary call is in flight.
|
|
7631
9535
|
// Task-augmented calls have a server-side task and are cancelled via
|
|
7632
9536
|
// `cancelRequestorTask` instead, so they don't use this (#1458).
|
|
7633
9537
|
activeToolCallAbortController;
|
|
@@ -7734,6 +9638,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7734
9638
|
this.fetchFn ?? fetch,
|
|
7735
9639
|
() => this.oauthManager?.getEndpointOverrides()
|
|
7736
9640
|
);
|
|
9641
|
+
this.fetchFn = withRfc8414OidcCompat(this.fetchFn);
|
|
7737
9642
|
this.effectiveAuthFetch = this.buildEffectiveAuthFetch();
|
|
7738
9643
|
this.sessionId = options.sessionId;
|
|
7739
9644
|
if (options.oauth || options.environment.oauth) {
|
|
@@ -7753,6 +9658,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
7753
9658
|
return Promise.resolve();
|
|
7754
9659
|
},
|
|
7755
9660
|
initialConfig: oauthConfig,
|
|
9661
|
+
logger: this.logger,
|
|
7756
9662
|
enterpriseManagedAuth: options.enterpriseManagedAuth,
|
|
7757
9663
|
installEnterpriseManagedAuth: options.installEnterpriseManagedAuth,
|
|
7758
9664
|
dispatchOAuthComplete: (detail) => this.dispatchTypedEvent("oauthComplete", detail),
|
|
@@ -8443,6 +10349,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
8443
10349
|
this.modernListenGeneration++;
|
|
8444
10350
|
this.clearModernReconnectTimer();
|
|
8445
10351
|
this.modernReconnectAttempts = 0;
|
|
10352
|
+
this.modernNeverAcknowledged = false;
|
|
8446
10353
|
this.modernSubscription = null;
|
|
8447
10354
|
this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
|
|
8448
10355
|
this.dispatchSubscriptionsChange();
|
|
@@ -9229,10 +11136,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
9229
11136
|
}
|
|
9230
11137
|
/**
|
|
9231
11138
|
* Cancel the in-flight ordinary (non-task) tool call started by
|
|
9232
|
-
* {@link callTool}. Aborting its request
|
|
9233
|
-
*
|
|
9234
|
-
*
|
|
9235
|
-
*
|
|
11139
|
+
* {@link callTool}. Aborting its request runs the MCP cancellation flow and
|
|
11140
|
+
* rejects the pending call, which `callTool` surfaces as a
|
|
11141
|
+
* {@link ToolCallCancelledError}. The SDK chooses the wire signal from the
|
|
11142
|
+
* transport: on a 2026-era Streamable HTTP connection it aborts that
|
|
11143
|
+
* request's own SSE response stream — the spec's cancellation signal —
|
|
11144
|
+
* rather than sending `notifications/cancelled`, which is the stdio
|
|
11145
|
+
* mechanism and remains in use there (#2140).
|
|
9236
11146
|
*
|
|
9237
11147
|
* Task-augmented calls have a server-side task and are cancelled via
|
|
9238
11148
|
* {@link cancelRequestorTask} instead — this is a no-op for them (and whenever
|
|
@@ -9259,7 +11169,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
9259
11169
|
throw new Error("Client is not connected");
|
|
9260
11170
|
}
|
|
9261
11171
|
const result = await this.client.request(
|
|
9262
|
-
{
|
|
11172
|
+
{
|
|
11173
|
+
method: "tasks/list",
|
|
11174
|
+
// `!== undefined`, not truthiness: a cursor is opaque and `""` is a
|
|
11175
|
+
// legal value a server may hand back. Dropping it asks for page one
|
|
11176
|
+
// again, so a caller walking pages would loop on the first page.
|
|
11177
|
+
params: cursor !== void 0 ? { cursor } : {}
|
|
11178
|
+
},
|
|
9263
11179
|
ListTasksResultSchema,
|
|
9264
11180
|
this.getRequestOptions()
|
|
9265
11181
|
);
|
|
@@ -10853,7 +12769,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
10853
12769
|
const effectiveMeta = this.mergeMeta(metadata);
|
|
10854
12770
|
const params = {
|
|
10855
12771
|
...effectiveMeta ? { _meta: effectiveMeta } : {},
|
|
10856
|
-
|
|
12772
|
+
// `!== undefined`, not truthiness: a cursor is opaque and `""` is a
|
|
12773
|
+
// legal value a server may hand back. Dropping it asks for page one
|
|
12774
|
+
// again, so a caller walking pages would loop on the first page.
|
|
12775
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
10857
12776
|
};
|
|
10858
12777
|
const response = await this.invokeMcpClient(
|
|
10859
12778
|
() => this.client.request(
|
|
@@ -10881,7 +12800,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
10881
12800
|
method: "resources/list",
|
|
10882
12801
|
itemsKey: "resources",
|
|
10883
12802
|
resultSchema: ListResourcesResultSchema,
|
|
10884
|
-
itemSchema:
|
|
12803
|
+
itemSchema: ResourceSchema2,
|
|
10885
12804
|
metadata: options?.metadata,
|
|
10886
12805
|
aggregate: async () => [
|
|
10887
12806
|
...(await this.client.listResources(
|
|
@@ -10983,7 +12902,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
10983
12902
|
const effectiveMeta = this.mergeMeta(metadata);
|
|
10984
12903
|
const params = {
|
|
10985
12904
|
...effectiveMeta ? { _meta: effectiveMeta } : {},
|
|
10986
|
-
|
|
12905
|
+
// `!== undefined`, not truthiness: a cursor is opaque and `""` is a
|
|
12906
|
+
// legal value a server may hand back. Dropping it asks for page one
|
|
12907
|
+
// again, so a caller walking pages would loop on the first page.
|
|
12908
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
10987
12909
|
};
|
|
10988
12910
|
const response = await this.invokeMcpClient(
|
|
10989
12911
|
() => this.client.request(
|
|
@@ -11029,51 +12951,203 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11029
12951
|
* @param metadata Optional metadata to include in the request
|
|
11030
12952
|
* @returns Object containing prompts array and optional nextCursor
|
|
11031
12953
|
*/
|
|
11032
|
-
async listPrompts(cursor, metadata) {
|
|
12954
|
+
async listPrompts(cursor, metadata) {
|
|
12955
|
+
if (!this.client) {
|
|
12956
|
+
throw new Error("Client is not connected");
|
|
12957
|
+
}
|
|
12958
|
+
const effectiveMeta = this.mergeMeta(metadata);
|
|
12959
|
+
const params = {
|
|
12960
|
+
...effectiveMeta ? { _meta: effectiveMeta } : {},
|
|
12961
|
+
// `!== undefined`, not truthiness: a cursor is opaque and `""` is a
|
|
12962
|
+
// legal value a server may hand back. Dropping it asks for page one
|
|
12963
|
+
// again, so a caller walking pages would loop on the first page.
|
|
12964
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
12965
|
+
};
|
|
12966
|
+
const response = await this.invokeMcpClient(
|
|
12967
|
+
() => this.client.request(
|
|
12968
|
+
{ method: "prompts/list", params },
|
|
12969
|
+
ListPromptsResultSchema,
|
|
12970
|
+
this.getRequestOptions(this.progressTokenOf(metadata))
|
|
12971
|
+
)
|
|
12972
|
+
);
|
|
12973
|
+
return {
|
|
12974
|
+
prompts: response.prompts || [],
|
|
12975
|
+
nextCursor: response.nextCursor
|
|
12976
|
+
};
|
|
12977
|
+
}
|
|
12978
|
+
/**
|
|
12979
|
+
* Aggregate ALL pages of `prompts/list` via the SDK's high-level
|
|
12980
|
+
* cache-aware `client.listPrompts()`. See {@link listAllTools} for the
|
|
12981
|
+
* `cacheMode` semantics; this is the path the managed prompt list uses on
|
|
12982
|
+
* refresh.
|
|
12983
|
+
*/
|
|
12984
|
+
async listAllPrompts(options) {
|
|
12985
|
+
if (!this.client) {
|
|
12986
|
+
throw new Error("Client is not connected");
|
|
12987
|
+
}
|
|
12988
|
+
const prompts = await this.listAllSalvaging({
|
|
12989
|
+
method: "prompts/list",
|
|
12990
|
+
itemsKey: "prompts",
|
|
12991
|
+
resultSchema: ListPromptsResultSchema,
|
|
12992
|
+
itemSchema: PromptSchema,
|
|
12993
|
+
metadata: options?.metadata,
|
|
12994
|
+
aggregate: async () => [
|
|
12995
|
+
...(await this.client.listPrompts(
|
|
12996
|
+
this.aggregateListParams(options?.metadata),
|
|
12997
|
+
this.getCacheableRequestOptions(options?.cacheMode)
|
|
12998
|
+
)).prompts
|
|
12999
|
+
]
|
|
13000
|
+
});
|
|
13001
|
+
return { prompts };
|
|
13002
|
+
}
|
|
13003
|
+
/**
|
|
13004
|
+
* The Skills extension (SEP-2640) the server declared, or `undefined` when it
|
|
13005
|
+
* declared none. Gates the Skills tab and the skills store the same way
|
|
13006
|
+
* {@link isTasksExtensionNegotiated} gates Tasks — but deliberately without an
|
|
13007
|
+
* era check: `skills/*` are not spec method names in either codec, so a
|
|
13008
|
+
* legacy-era server that declares the extension is serving it (#2234).
|
|
13009
|
+
*/
|
|
13010
|
+
getSkillsExtension() {
|
|
13011
|
+
return getSkillsExtension(this.capabilities);
|
|
13012
|
+
}
|
|
13013
|
+
/**
|
|
13014
|
+
* One page of `skills/list` (SEP-2640).
|
|
13015
|
+
*
|
|
13016
|
+
* An ordinary `client.request` with an explicit result schema — the SDK's era
|
|
13017
|
+
* gate skips methods neither codec defines, and `assertCapabilityForMethod`
|
|
13018
|
+
* falls through to a no-op for them, so this is all the extension needs. The
|
|
13019
|
+
* raw-wire path modern `tasks/*` uses would be wrong here: it exists for spec
|
|
13020
|
+
* names the 2026 codec deleted, and taking it would bypass the SDK's response
|
|
13021
|
+
* correlation for nothing (#2234).
|
|
13022
|
+
*/
|
|
13023
|
+
async listSkills(cursor, metadata) {
|
|
13024
|
+
if (!this.client) {
|
|
13025
|
+
throw new Error("Client is not connected");
|
|
13026
|
+
}
|
|
13027
|
+
const effectiveMeta = this.mergeMeta(metadata);
|
|
13028
|
+
const params = {
|
|
13029
|
+
...effectiveMeta ? { _meta: effectiveMeta } : {},
|
|
13030
|
+
// `!== undefined`, not truthiness: a cursor is opaque and the empty
|
|
13031
|
+
// string is a legal value. Dropping `""` would silently re-request page
|
|
13032
|
+
// one, which the store then reports as a repeated-cursor failure — a
|
|
13033
|
+
// conforming server made to look broken.
|
|
13034
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
13035
|
+
};
|
|
13036
|
+
const resultSchema = this.isModernEra() ? ModernListSkillsResultSchema : ListSkillsResultSchema;
|
|
13037
|
+
const response = await this.invokeMcpClient(
|
|
13038
|
+
() => this.client.request(
|
|
13039
|
+
{ method: SKILLS_LIST_METHOD, params },
|
|
13040
|
+
resultSchema,
|
|
13041
|
+
this.getRequestOptions(this.progressTokenOf(metadata))
|
|
13042
|
+
),
|
|
13043
|
+
{ method: SKILLS_LIST_METHOD }
|
|
13044
|
+
);
|
|
13045
|
+
return {
|
|
13046
|
+
skills: response.skills,
|
|
13047
|
+
nextCursor: response.nextCursor
|
|
13048
|
+
};
|
|
13049
|
+
}
|
|
13050
|
+
/**
|
|
13051
|
+
* One skill entry by URI (`skills/get`, SEP-2640). The result envelope is
|
|
13052
|
+
* required — `GetSkillEnvelopeSchema` requires `{ skill }` and rejects an entry
|
|
13053
|
+
* returned inline, so a non-conforming shape fails here rather than being
|
|
13054
|
+
* silently normalized past the conformance checks.
|
|
13055
|
+
*/
|
|
13056
|
+
async getSkill(uri, metadata) {
|
|
13057
|
+
return (await this.getSkillResult(uri, metadata)).skill;
|
|
13058
|
+
}
|
|
13059
|
+
/**
|
|
13060
|
+
* `skills/get` as the server sent it — the `{ skill }` envelope **and any
|
|
13061
|
+
* other members it carried**.
|
|
13062
|
+
*
|
|
13063
|
+
* Separate from {@link getSkill} because the callers differ: the UIs want the
|
|
13064
|
+
* entry, while the CLI prints the result and must not reshape it. SEP-2640
|
|
13065
|
+
* explicitly leaves open whether this result carries `ttlMs` / `cacheScope`,
|
|
13066
|
+
* so a server may send them — and unwrapping to the entry discards exactly
|
|
13067
|
+
* those (Copilot).
|
|
13068
|
+
*/
|
|
13069
|
+
async getSkillResult(uri, metadata) {
|
|
13070
|
+
if (!this.client) {
|
|
13071
|
+
throw new Error("Client is not connected");
|
|
13072
|
+
}
|
|
13073
|
+
const effectiveMeta = this.mergeMeta(metadata);
|
|
13074
|
+
const params = {
|
|
13075
|
+
uri,
|
|
13076
|
+
...effectiveMeta ? { _meta: effectiveMeta } : {}
|
|
13077
|
+
};
|
|
13078
|
+
const resultSchema = this.isModernEra() ? ModernGetSkillEnvelopeSchema : GetSkillEnvelopeSchema;
|
|
13079
|
+
try {
|
|
13080
|
+
return await this.invokeMcpClient(
|
|
13081
|
+
() => this.client.request(
|
|
13082
|
+
{ method: SKILLS_GET_METHOD, params },
|
|
13083
|
+
resultSchema,
|
|
13084
|
+
this.getRequestOptions(this.progressTokenOf(metadata))
|
|
13085
|
+
),
|
|
13086
|
+
{ method: SKILLS_GET_METHOD }
|
|
13087
|
+
);
|
|
13088
|
+
} catch (err) {
|
|
13089
|
+
if (isClientDecodeRejection(err)) {
|
|
13090
|
+
this.markResponseRejected(
|
|
13091
|
+
SKILLS_GET_METHOD,
|
|
13092
|
+
err instanceof Error ? err.message : String(err)
|
|
13093
|
+
);
|
|
13094
|
+
}
|
|
13095
|
+
throw err;
|
|
13096
|
+
}
|
|
13097
|
+
}
|
|
13098
|
+
/**
|
|
13099
|
+
* One page of `resources/directory/read` (SEP-2640): the direct children of
|
|
13100
|
+
* a directory resource.
|
|
13101
|
+
*
|
|
13102
|
+
* **Gated on the server's own declaration, and the gate throws rather than
|
|
13103
|
+
* asks.** SEP-2640 is explicit that *"clients MUST NOT call
|
|
13104
|
+
* `resources/directory/read` against a server that has not declared
|
|
13105
|
+
* `directoryRead: true`"*, so this refuses locally instead of sending a call
|
|
13106
|
+
* the spec forbids and letting the server answer `-32601`. Refusing here also
|
|
13107
|
+
* keeps the Protocol tab honest: a request we were never allowed to make
|
|
13108
|
+
* should not appear in the exchange log as a server-side failure.
|
|
13109
|
+
*
|
|
13110
|
+
* Not recursive — the SEP says clients descend by calling the method again on
|
|
13111
|
+
* a child directory, so the walking is the caller's, not this method's.
|
|
13112
|
+
*/
|
|
13113
|
+
async readResourceDirectory(uri, cursor, metadata) {
|
|
11033
13114
|
if (!this.client) {
|
|
11034
13115
|
throw new Error("Client is not connected");
|
|
11035
13116
|
}
|
|
13117
|
+
const extension = this.getSkillsExtension();
|
|
13118
|
+
if (!extension?.directoryRead) {
|
|
13119
|
+
throw new Error(
|
|
13120
|
+
`Server did not declare directoryRead in ${SKILLS_EXTENSION_KEY}; ${RESOURCES_DIRECTORY_READ_METHOD} must not be called.`
|
|
13121
|
+
);
|
|
13122
|
+
}
|
|
11036
13123
|
const effectiveMeta = this.mergeMeta(metadata);
|
|
11037
13124
|
const params = {
|
|
13125
|
+
uri,
|
|
11038
13126
|
...effectiveMeta ? { _meta: effectiveMeta } : {},
|
|
11039
|
-
|
|
11040
|
-
|
|
11041
|
-
|
|
11042
|
-
|
|
11043
|
-
{ method: "prompts/list", params },
|
|
11044
|
-
ListPromptsResultSchema,
|
|
11045
|
-
this.getRequestOptions(this.progressTokenOf(metadata))
|
|
11046
|
-
)
|
|
11047
|
-
);
|
|
11048
|
-
return {
|
|
11049
|
-
prompts: response.prompts || [],
|
|
11050
|
-
nextCursor: response.nextCursor
|
|
13127
|
+
// `!== undefined` for the same reason `listSkills` uses it: a cursor is
|
|
13128
|
+
// opaque and `""` is a legal value, so truthiness would silently re-ask
|
|
13129
|
+
// for page one.
|
|
13130
|
+
...cursor !== void 0 ? { cursor } : {}
|
|
11051
13131
|
};
|
|
11052
|
-
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
|
|
11059
|
-
|
|
11060
|
-
|
|
11061
|
-
|
|
13132
|
+
const resultSchema = this.isModernEra() ? ModernDirectoryReadResultSchema : DirectoryReadResultSchema;
|
|
13133
|
+
try {
|
|
13134
|
+
return await this.invokeMcpClient(
|
|
13135
|
+
() => this.client.request(
|
|
13136
|
+
{ method: RESOURCES_DIRECTORY_READ_METHOD, params },
|
|
13137
|
+
resultSchema,
|
|
13138
|
+
this.getRequestOptions(this.progressTokenOf(metadata))
|
|
13139
|
+
),
|
|
13140
|
+
{ method: RESOURCES_DIRECTORY_READ_METHOD }
|
|
13141
|
+
);
|
|
13142
|
+
} catch (err) {
|
|
13143
|
+
if (isClientDecodeRejection(err)) {
|
|
13144
|
+
this.markResponseRejected(
|
|
13145
|
+
RESOURCES_DIRECTORY_READ_METHOD,
|
|
13146
|
+
err instanceof Error ? err.message : String(err)
|
|
13147
|
+
);
|
|
13148
|
+
}
|
|
13149
|
+
throw err;
|
|
11062
13150
|
}
|
|
11063
|
-
const prompts = await this.listAllSalvaging({
|
|
11064
|
-
method: "prompts/list",
|
|
11065
|
-
itemsKey: "prompts",
|
|
11066
|
-
resultSchema: ListPromptsResultSchema,
|
|
11067
|
-
itemSchema: PromptSchema,
|
|
11068
|
-
metadata: options?.metadata,
|
|
11069
|
-
aggregate: async () => [
|
|
11070
|
-
...(await this.client.listPrompts(
|
|
11071
|
-
this.aggregateListParams(options?.metadata),
|
|
11072
|
-
this.getCacheableRequestOptions(options?.cacheMode)
|
|
11073
|
-
)).prompts
|
|
11074
|
-
]
|
|
11075
|
-
});
|
|
11076
|
-
return { prompts };
|
|
11077
13151
|
}
|
|
11078
13152
|
/**
|
|
11079
13153
|
* Get a prompt by name
|
|
@@ -11448,6 +13522,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11448
13522
|
if (!fromReconnect) {
|
|
11449
13523
|
this.clearModernReconnectTimer();
|
|
11450
13524
|
this.modernReconnectAttempts = 0;
|
|
13525
|
+
this.modernNeverAcknowledged = false;
|
|
11451
13526
|
}
|
|
11452
13527
|
const generation = ++this.modernListenGeneration;
|
|
11453
13528
|
const previous = this.modernSubscription;
|
|
@@ -11459,10 +13534,18 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11459
13534
|
this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
|
|
11460
13535
|
return;
|
|
11461
13536
|
}
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
this.
|
|
11465
|
-
|
|
13537
|
+
let subscription;
|
|
13538
|
+
try {
|
|
13539
|
+
subscription = await this.client.listen(
|
|
13540
|
+
this.buildSubscriptionFilter(),
|
|
13541
|
+
this.getRequestOptions()
|
|
13542
|
+
);
|
|
13543
|
+
} catch (error) {
|
|
13544
|
+
if (generation === this.modernListenGeneration && isNeverAcknowledgedSubscriptionClose(error)) {
|
|
13545
|
+
this.modernNeverAcknowledged = true;
|
|
13546
|
+
}
|
|
13547
|
+
throw error;
|
|
13548
|
+
}
|
|
11466
13549
|
if (generation !== this.modernListenGeneration) {
|
|
11467
13550
|
await closeSubscriptionBestEffort(subscription);
|
|
11468
13551
|
return;
|
|
@@ -11544,8 +13627,34 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11544
13627
|
this.setModernStreamState(INACTIVE_SUBSCRIPTION_STREAM_STATE);
|
|
11545
13628
|
return;
|
|
11546
13629
|
}
|
|
13630
|
+
if (this.modernNeverAcknowledged) {
|
|
13631
|
+
this.endModernStreamNeverAcknowledged();
|
|
13632
|
+
return;
|
|
13633
|
+
}
|
|
11547
13634
|
this.scheduleModernReconnect();
|
|
11548
13635
|
}
|
|
13636
|
+
/**
|
|
13637
|
+
* Settle the stream on the never-acknowledged close (#2097): a status of its
|
|
13638
|
+
* own so the UI can say what happened, no reconnect, and a log line carrying
|
|
13639
|
+
* the same sentence the UI shows.
|
|
13640
|
+
*
|
|
13641
|
+
* The state is *not* `"ended"` — that badge covers the two expected closes (a
|
|
13642
|
+
* server shutting an established stream down, and reconnection abandoned after
|
|
13643
|
+
* repeated failures), and reading this case as either of them is exactly the
|
|
13644
|
+
* silence #2063 reported.
|
|
13645
|
+
*/
|
|
13646
|
+
endModernStreamNeverAcknowledged() {
|
|
13647
|
+
this.clearModernReconnectTimer();
|
|
13648
|
+
this.logger.warn(
|
|
13649
|
+
{ message: NEVER_ACKNOWLEDGED_SUBSCRIPTION_MESSAGE },
|
|
13650
|
+
"subscriptions/listen closed without an acknowledgement"
|
|
13651
|
+
);
|
|
13652
|
+
this.setModernStreamState({
|
|
13653
|
+
active: this.modernStreamActive(),
|
|
13654
|
+
status: "never-acknowledged",
|
|
13655
|
+
honoredUris: []
|
|
13656
|
+
});
|
|
13657
|
+
}
|
|
11549
13658
|
/**
|
|
11550
13659
|
* Schedule a reconnect re-listen after the current backoff delay (#1630).
|
|
11551
13660
|
* `modernReconnectAttempts` reflects the number of *consecutive failed*
|
|
@@ -11579,6 +13688,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11579
13688
|
* stream ended (re-subscribing resets the run and tries again).
|
|
11580
13689
|
*/
|
|
11581
13690
|
onModernReconnectFailed() {
|
|
13691
|
+
if (this.modernNeverAcknowledged && !isTerminalStatus(this.status) && this.wantsModernStream()) {
|
|
13692
|
+
this.endModernStreamNeverAcknowledged();
|
|
13693
|
+
return;
|
|
13694
|
+
}
|
|
11582
13695
|
this.modernReconnectAttempts += 1;
|
|
11583
13696
|
if (this.modernReconnectAttempts > MODERN_RECONNECT_MAX_ATTEMPTS || isTerminalStatus(this.status) || !this.wantsModernStream()) {
|
|
11584
13697
|
this.setModernStreamState({
|
|
@@ -11635,7 +13748,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11635
13748
|
this.dispatchSubscriptionsChange();
|
|
11636
13749
|
} catch (error) {
|
|
11637
13750
|
throw new Error(
|
|
11638
|
-
`Failed to subscribe to resource: ${
|
|
13751
|
+
`Failed to subscribe to resource: ${subscriptionFailureMessage(error)}`,
|
|
11639
13752
|
{ cause: error }
|
|
11640
13753
|
);
|
|
11641
13754
|
}
|
|
@@ -11680,7 +13793,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11680
13793
|
}
|
|
11681
13794
|
} catch (error) {
|
|
11682
13795
|
throw new Error(
|
|
11683
|
-
`Failed to unsubscribe from resource: ${
|
|
13796
|
+
`Failed to unsubscribe from resource: ${subscriptionFailureMessage(error)}`,
|
|
11684
13797
|
{ cause: error }
|
|
11685
13798
|
);
|
|
11686
13799
|
}
|
|
@@ -11951,10 +14064,23 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
|
|
|
11951
14064
|
return this.oauthManager.getOAuthTokens();
|
|
11952
14065
|
}
|
|
11953
14066
|
/**
|
|
11954
|
-
*
|
|
14067
|
+
* Drop this server's stored OAuth state and revoke the grant at the
|
|
14068
|
+
* authorization server (RFC 7009, #2144).
|
|
14069
|
+
*
|
|
14070
|
+
* The request is planned from the stored state, the state is cleared, and
|
|
14071
|
+
* only then is the request sent — so the clear never waits on the network.
|
|
14072
|
+
*
|
|
14073
|
+
* The revocation is best-effort — an authorization server that advertises no
|
|
14074
|
+
* `revocation_endpoint` is left behaving exactly as before, and a network
|
|
14075
|
+
* error, a non-2xx or a timeout is reported in the returned outcome rather
|
|
14076
|
+
* than thrown — so the local clear always completes. Pass
|
|
14077
|
+
* `{ revoke: false }` to skip it.
|
|
11955
14078
|
*/
|
|
11956
|
-
async clearOAuthTokens() {
|
|
11957
|
-
await this.oauthManager?.clearOAuthTokens()
|
|
14079
|
+
async clearOAuthTokens(options) {
|
|
14080
|
+
return await this.oauthManager?.clearOAuthTokens(options) ?? {
|
|
14081
|
+
status: "skipped",
|
|
14082
|
+
reason: "no_tokens"
|
|
14083
|
+
};
|
|
11958
14084
|
}
|
|
11959
14085
|
/**
|
|
11960
14086
|
* Checks if client is currently OAuth authorized
|
|
@@ -12048,7 +14174,30 @@ var EXIT_CODES = {
|
|
|
12048
14174
|
AUTH_REQUIRED: 3,
|
|
12049
14175
|
UNREACHABLE: 4,
|
|
12050
14176
|
TOOL_ERROR: 5,
|
|
12051
|
-
SCHEMA_UNPORTABLE: 6
|
|
14177
|
+
SCHEMA_UNPORTABLE: 6,
|
|
14178
|
+
/**
|
|
14179
|
+
* `--verify` found a SEP-2640 violation: a conformance error, a digest or
|
|
14180
|
+
* size mismatch, or a manifest file that could not be read (#2248).
|
|
14181
|
+
*
|
|
14182
|
+
* Its own code rather than reusing `SCHEMA_UNPORTABLE`, for the reason that
|
|
14183
|
+
* one exists at all: a CI job that fails on an unportable tool schema and a
|
|
14184
|
+
* CI job that fails on a tampered skill digest are different jobs, and
|
|
14185
|
+
* collapsing them would make `if [ $? -eq 6 ]` ambiguous.
|
|
14186
|
+
*/
|
|
14187
|
+
SKILL_NONCONFORMANT: 7,
|
|
14188
|
+
/**
|
|
14189
|
+
* `--verify` could not check the whole catalog: the read bounds stopped the
|
|
14190
|
+
* walk before it finished (#2248).
|
|
14191
|
+
*
|
|
14192
|
+
* Distinct from `SKILL_NONCONFORMANT` because the server has broken no
|
|
14193
|
+
* **MUST** — SEP-2640 states the 512-entry and 16 MiB limits as SHOULD NOT,
|
|
14194
|
+
* with hosts free to support more — so exiting 7 would call a conforming
|
|
14195
|
+
* server nonconformant. It is still non-zero, because reporting success for
|
|
14196
|
+
* a manifest whose unread entries were never fetched is a false pass. A CI
|
|
14197
|
+
* job that wants to tolerate oversized catalogs can allow 8 and still fail
|
|
14198
|
+
* on 7.
|
|
14199
|
+
*/
|
|
14200
|
+
SKILL_INCOMPLETE: 8
|
|
12052
14201
|
};
|
|
12053
14202
|
var CliExitCodeError = class extends Error {
|
|
12054
14203
|
constructor(exitCode, message, envelope) {
|
|
@@ -12556,6 +14705,15 @@ async function consumeMethodOutcome(outcome, args) {
|
|
|
12556
14705
|
for (const line of outcome.lines) {
|
|
12557
14706
|
await awaitableLog(JSON.stringify(line) + "\n");
|
|
12558
14707
|
}
|
|
14708
|
+
if (outcome.summary) await awaitableError(`${outcome.summary}
|
|
14709
|
+
`);
|
|
14710
|
+
if (outcome.exitCode) {
|
|
14711
|
+
throw new CliExitCodeError(outcome.exitCode, outcome.summary ?? "", {
|
|
14712
|
+
// The envelope's `code` follows the exit code, so a caller reading one
|
|
14713
|
+
// never has to reconcile it against the other.
|
|
14714
|
+
code: outcome.exitCode === EXIT_CODES.SKILL_INCOMPLETE ? "skills_incomplete" : "skills_nonconformant"
|
|
14715
|
+
});
|
|
14716
|
+
}
|
|
12559
14717
|
return;
|
|
12560
14718
|
}
|
|
12561
14719
|
await new Promise((resolve7) => {
|
|
@@ -13121,7 +15279,7 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
|
|
|
13121
15279
|
const page = result.tasks.filter((t) => !this.dismissedTaskIds.has(t.taskId)).map(
|
|
13122
15280
|
(t) => this.cancelledTaskIds.has(t.taskId) ? { ...t, status: "cancelled" } : t
|
|
13123
15281
|
);
|
|
13124
|
-
this.tasks = cursor ? [...this.tasks, ...page]
|
|
15282
|
+
this.tasks = cursor === void 0 ? page : [...this.tasks, ...page];
|
|
13125
15283
|
cursor = result.nextCursor;
|
|
13126
15284
|
pageCount++;
|
|
13127
15285
|
if (pageCount >= MAX_PAGES) {
|
|
@@ -13129,7 +15287,7 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
|
|
|
13129
15287
|
`Maximum pagination limit (${MAX_PAGES} pages) reached while listing requestor tasks`
|
|
13130
15288
|
);
|
|
13131
15289
|
}
|
|
13132
|
-
} while (cursor);
|
|
15290
|
+
} while (cursor !== void 0);
|
|
13133
15291
|
this.dispatchTypedEvent("tasksChange", this.tasks);
|
|
13134
15292
|
return this.getTasks();
|
|
13135
15293
|
}
|
|
@@ -13182,6 +15340,146 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
|
|
|
13182
15340
|
}
|
|
13183
15341
|
};
|
|
13184
15342
|
|
|
15343
|
+
// ../../core/mcp/state/managedSkillsState.ts
|
|
15344
|
+
var REPEATED_CURSOR_MESSAGE = "Server repeated a pagination cursor in skills/list; stopped to avoid an infinite walk.";
|
|
15345
|
+
var SKILLS_MAX_PAGES = LIST_MAX_PAGES;
|
|
15346
|
+
var SKILLS_PAGE_LIMIT_MESSAGE = `skills/list exceeded ${SKILLS_MAX_PAGES} pages without the server's pagination converging`;
|
|
15347
|
+
var ManagedSkillsState = class extends TypedEventTarget {
|
|
15348
|
+
skills = [];
|
|
15349
|
+
pageCount = 0;
|
|
15350
|
+
error = null;
|
|
15351
|
+
client = null;
|
|
15352
|
+
unsubscribe = null;
|
|
15353
|
+
// Overlap guard, held as the generation whose walk is in flight (or `null`).
|
|
15354
|
+
// See `refresh` for why this is not a boolean.
|
|
15355
|
+
runningGeneration = null;
|
|
15356
|
+
// Session counter, advanced by `reset` (disconnect) and by `destroy`. A walk
|
|
15357
|
+
// captures it and abandons its writes when it no longer matches.
|
|
15358
|
+
generation = 0;
|
|
15359
|
+
constructor(client) {
|
|
15360
|
+
super();
|
|
15361
|
+
this.client = client;
|
|
15362
|
+
const onConnect = () => {
|
|
15363
|
+
void this.refresh().catch(() => {
|
|
15364
|
+
});
|
|
15365
|
+
};
|
|
15366
|
+
const onStatusChange = () => {
|
|
15367
|
+
if (isTerminalStatus(this.client?.getStatus())) {
|
|
15368
|
+
this.reset();
|
|
15369
|
+
}
|
|
15370
|
+
};
|
|
15371
|
+
this.client.addEventListener("connect", onConnect);
|
|
15372
|
+
this.client.addEventListener("statusChange", onStatusChange);
|
|
15373
|
+
this.unsubscribe = () => {
|
|
15374
|
+
if (this.client) {
|
|
15375
|
+
this.client.removeEventListener("connect", onConnect);
|
|
15376
|
+
this.client.removeEventListener("statusChange", onStatusChange);
|
|
15377
|
+
}
|
|
15378
|
+
this.client = null;
|
|
15379
|
+
};
|
|
15380
|
+
}
|
|
15381
|
+
/** Defensive copy of the current list. */
|
|
15382
|
+
getSkills() {
|
|
15383
|
+
return [...this.skills];
|
|
15384
|
+
}
|
|
15385
|
+
getPagination() {
|
|
15386
|
+
return { pageCount: this.pageCount };
|
|
15387
|
+
}
|
|
15388
|
+
/** The last walk's failure, or `null` when it succeeded. */
|
|
15389
|
+
getError() {
|
|
15390
|
+
return this.error;
|
|
15391
|
+
}
|
|
15392
|
+
// Compared by identity rather than message: two distinct failures with the
|
|
15393
|
+
// same text are still two events, and a re-render on a repeat failure is
|
|
15394
|
+
// cheap next to silently coalescing them.
|
|
15395
|
+
setError(value) {
|
|
15396
|
+
if (this.error === value) return;
|
|
15397
|
+
this.error = value;
|
|
15398
|
+
this.dispatchTypedEvent("errorChange", value);
|
|
15399
|
+
}
|
|
15400
|
+
reset() {
|
|
15401
|
+
this.generation += 1;
|
|
15402
|
+
this.skills = [];
|
|
15403
|
+
this.pageCount = 0;
|
|
15404
|
+
this.dispatchTypedEvent("skillsChange", this.getSkills());
|
|
15405
|
+
this.dispatchTypedEvent("paginationChange", this.getPagination());
|
|
15406
|
+
this.setError(null);
|
|
15407
|
+
}
|
|
15408
|
+
/**
|
|
15409
|
+
* Walk every page of `skills/list` and commit the result.
|
|
15410
|
+
*
|
|
15411
|
+
* A failure is recorded as observable state (`getError`) AND re-thrown: the
|
|
15412
|
+
* state drives the panel's error rendering, while the rejection is what a
|
|
15413
|
+
* caller's auth-recovery wrapper keys off to detect a 401 and start a
|
|
15414
|
+
* re-authorization. The connect-time load, which has no such caller, catches
|
|
15415
|
+
* it above.
|
|
15416
|
+
*/
|
|
15417
|
+
async refresh(metadata) {
|
|
15418
|
+
const client = this.client;
|
|
15419
|
+
if (!client || client.getStatus() !== "connected") return this.getSkills();
|
|
15420
|
+
if (!client.getSkillsExtension()) {
|
|
15421
|
+
this.applyPages([], 0);
|
|
15422
|
+
return this.getSkills();
|
|
15423
|
+
}
|
|
15424
|
+
const generation = this.generation;
|
|
15425
|
+
const isCurrent = () => this.generation === generation;
|
|
15426
|
+
if (this.runningGeneration === generation) return this.getSkills();
|
|
15427
|
+
this.runningGeneration = generation;
|
|
15428
|
+
try {
|
|
15429
|
+
const collected = [];
|
|
15430
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15431
|
+
let cursor;
|
|
15432
|
+
let pages = 0;
|
|
15433
|
+
for (; ; ) {
|
|
15434
|
+
const page = await client.listSkills(cursor, metadata);
|
|
15435
|
+
if (!isCurrent()) return this.getSkills();
|
|
15436
|
+
collected.push(...page.skills);
|
|
15437
|
+
pages += 1;
|
|
15438
|
+
if (page.nextCursor === void 0) break;
|
|
15439
|
+
if (pages >= SKILLS_MAX_PAGES) {
|
|
15440
|
+
throw new Error(SKILLS_PAGE_LIMIT_MESSAGE);
|
|
15441
|
+
}
|
|
15442
|
+
if (seen.has(page.nextCursor)) {
|
|
15443
|
+
throw new Error(REPEATED_CURSOR_MESSAGE);
|
|
15444
|
+
}
|
|
15445
|
+
seen.add(page.nextCursor);
|
|
15446
|
+
cursor = page.nextCursor;
|
|
15447
|
+
}
|
|
15448
|
+
this.setError(null);
|
|
15449
|
+
this.applyPages(collected, pages);
|
|
15450
|
+
return this.getSkills();
|
|
15451
|
+
} catch (err) {
|
|
15452
|
+
if (isCurrent()) {
|
|
15453
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
15454
|
+
this.setError(error);
|
|
15455
|
+
if (isClientDecodeRejection(err)) {
|
|
15456
|
+
client.markResponseRejected?.(SKILLS_LIST_METHOD, error.message);
|
|
15457
|
+
}
|
|
15458
|
+
}
|
|
15459
|
+
throw err;
|
|
15460
|
+
} finally {
|
|
15461
|
+
if (this.runningGeneration === generation) {
|
|
15462
|
+
this.runningGeneration = null;
|
|
15463
|
+
}
|
|
15464
|
+
}
|
|
15465
|
+
}
|
|
15466
|
+
applyPages(skills, pageCount) {
|
|
15467
|
+
this.skills = skills;
|
|
15468
|
+
this.pageCount = pageCount;
|
|
15469
|
+
this.dispatchTypedEvent("skillsChange", this.getSkills());
|
|
15470
|
+
this.dispatchTypedEvent("paginationChange", this.getPagination());
|
|
15471
|
+
}
|
|
15472
|
+
/** Unsubscribe from the client and drop the list; idempotent. */
|
|
15473
|
+
destroy() {
|
|
15474
|
+
this.generation += 1;
|
|
15475
|
+
this.unsubscribe?.();
|
|
15476
|
+
this.unsubscribe = null;
|
|
15477
|
+
this.skills = [];
|
|
15478
|
+
this.pageCount = 0;
|
|
15479
|
+
this.error = null;
|
|
15480
|
+
}
|
|
15481
|
+
};
|
|
15482
|
+
|
|
13185
15483
|
// ../../core/mcp/apps.ts
|
|
13186
15484
|
import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
13187
15485
|
function getAppResourceUri(tool) {
|
|
@@ -13252,13 +15550,241 @@ async function collectAppInfo(client, tool, metadata) {
|
|
|
13252
15550
|
}
|
|
13253
15551
|
}
|
|
13254
15552
|
|
|
15553
|
+
// src/handlers/skills-verify.ts
|
|
15554
|
+
function summarizeSkillVerification(reports) {
|
|
15555
|
+
const failed = reports.filter((report) => report.outcome === "failed").length;
|
|
15556
|
+
const incomplete = reports.filter(
|
|
15557
|
+
(report) => report.outcome === "incomplete"
|
|
15558
|
+
).length;
|
|
15559
|
+
const files = reports.reduce((sum, report) => sum + report.files.length, 0);
|
|
15560
|
+
const mismatched = reports.reduce(
|
|
15561
|
+
(sum, report) => sum + report.files.filter((file) => file.status === "mismatch").length,
|
|
15562
|
+
0
|
|
15563
|
+
);
|
|
15564
|
+
const skillWord = reports.length === 1 ? "skill" : "skills";
|
|
15565
|
+
const fileWord = files === 1 ? "file" : "files";
|
|
15566
|
+
const incompleteClause = incomplete === 0 ? "" : ` ${incomplete} of ${reports.length} ${skillWord} could not be fully checked: the read bounds stopped the walk.`;
|
|
15567
|
+
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}).`;
|
|
15568
|
+
return `${headline}${incompleteClause}`;
|
|
15569
|
+
}
|
|
15570
|
+
|
|
15571
|
+
// ../../core/mcp/skillsVerification.ts
|
|
15572
|
+
function reasonOf(err) {
|
|
15573
|
+
return err instanceof Error ? err.message : String(err);
|
|
15574
|
+
}
|
|
15575
|
+
function boundedManifest(declared) {
|
|
15576
|
+
const kept = [];
|
|
15577
|
+
let bytes = 0;
|
|
15578
|
+
for (const resource of declared) {
|
|
15579
|
+
if (kept.length >= SKILL_MAX_RESOURCE_ENTRIES) break;
|
|
15580
|
+
const size = typeof resource.size === "number" && Number.isSafeInteger(resource.size) && resource.size >= 0 ? resource.size : 0;
|
|
15581
|
+
if (bytes + size > SKILL_MAX_TOTAL_BYTES) break;
|
|
15582
|
+
bytes += size;
|
|
15583
|
+
kept.push(resource);
|
|
15584
|
+
}
|
|
15585
|
+
return kept;
|
|
15586
|
+
}
|
|
15587
|
+
function contentsFor(result, uri) {
|
|
15588
|
+
const contents = result?.contents;
|
|
15589
|
+
if (!Array.isArray(contents)) return void 0;
|
|
15590
|
+
const wanted = skillUriIdentity(uri);
|
|
15591
|
+
for (const block of contents) {
|
|
15592
|
+
if (typeof block !== "object" || block === null) continue;
|
|
15593
|
+
const got = block.uri;
|
|
15594
|
+
if (typeof got !== "string") continue;
|
|
15595
|
+
if (skillUriIdentity(got) === wanted) return block;
|
|
15596
|
+
}
|
|
15597
|
+
return void 0;
|
|
15598
|
+
}
|
|
15599
|
+
function utf8Length(value) {
|
|
15600
|
+
let total = 0;
|
|
15601
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
15602
|
+
const code = value.charCodeAt(i);
|
|
15603
|
+
if (code < 128) {
|
|
15604
|
+
total += 1;
|
|
15605
|
+
} else if (code < 2048) {
|
|
15606
|
+
total += 2;
|
|
15607
|
+
} else if (code >= 55296 && code <= 56319) {
|
|
15608
|
+
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : 0;
|
|
15609
|
+
if (next >= 56320 && next <= 57343) {
|
|
15610
|
+
total += 4;
|
|
15611
|
+
i += 1;
|
|
15612
|
+
} else {
|
|
15613
|
+
total += 3;
|
|
15614
|
+
}
|
|
15615
|
+
} else {
|
|
15616
|
+
total += 3;
|
|
15617
|
+
}
|
|
15618
|
+
}
|
|
15619
|
+
return total;
|
|
15620
|
+
}
|
|
15621
|
+
function responseBytes(result) {
|
|
15622
|
+
const contents = result?.contents;
|
|
15623
|
+
if (!Array.isArray(contents)) return 0;
|
|
15624
|
+
let total = 0;
|
|
15625
|
+
for (const block of contents) {
|
|
15626
|
+
if (typeof block !== "object" || block === null) continue;
|
|
15627
|
+
const { text, blob } = block;
|
|
15628
|
+
if (typeof text === "string") total += utf8Length(text);
|
|
15629
|
+
if (typeof blob === "string") total += blob.length;
|
|
15630
|
+
}
|
|
15631
|
+
return total;
|
|
15632
|
+
}
|
|
15633
|
+
async function verifySkills(client, entries, metadata) {
|
|
15634
|
+
const collisions = checkSkillNameCollisions(entries);
|
|
15635
|
+
const reports = [];
|
|
15636
|
+
let walkedSkills = 0;
|
|
15637
|
+
let catalogBytes = 0;
|
|
15638
|
+
for (const entry of entries) {
|
|
15639
|
+
const withinBudget = walkedSkills < SKILL_MAX_CATALOG_SKILLS && catalogBytes <= SKILL_MAX_CATALOG_BYTES;
|
|
15640
|
+
let entryBytes;
|
|
15641
|
+
const files = [];
|
|
15642
|
+
const declared = entry.resources === DYNAMIC_RESOURCES ? [] : entry.resources;
|
|
15643
|
+
const manifest = withinBudget ? boundedManifest(declared) : [];
|
|
15644
|
+
let incomplete = !withinBudget ? `Not read: this run already reached its catalog budget of ${SKILL_MAX_CATALOG_SKILLS} skills / ${SKILL_MAX_CATALOG_BYTES} bytes. 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;
|
|
15645
|
+
let receivedBytes = 0;
|
|
15646
|
+
const entryIdentity = skillUriIdentity(entry.uri);
|
|
15647
|
+
let selfAttempted = false;
|
|
15648
|
+
for (const [index, resource] of manifest.entries()) {
|
|
15649
|
+
if (skillUriIdentity(resource.uri) === entryIdentity) {
|
|
15650
|
+
selfAttempted = true;
|
|
15651
|
+
}
|
|
15652
|
+
let charged = 0;
|
|
15653
|
+
try {
|
|
15654
|
+
const invocation = await client.readResource(resource.uri, metadata);
|
|
15655
|
+
charged = responseBytes(invocation.result);
|
|
15656
|
+
const contents = contentsFor(invocation.result, resource.uri);
|
|
15657
|
+
if (!contents) {
|
|
15658
|
+
files.push({
|
|
15659
|
+
uri: resource.uri,
|
|
15660
|
+
status: "read-error",
|
|
15661
|
+
reason: "resources/read returned no content block for this URI, so there are no bytes that can be checked against its digest."
|
|
15662
|
+
});
|
|
15663
|
+
} else {
|
|
15664
|
+
let bytes;
|
|
15665
|
+
try {
|
|
15666
|
+
bytes = skillFileBytes(contents);
|
|
15667
|
+
} catch (err) {
|
|
15668
|
+
files.push({
|
|
15669
|
+
uri: resource.uri,
|
|
15670
|
+
status: "read-error",
|
|
15671
|
+
reason: reasonOf(err)
|
|
15672
|
+
});
|
|
15673
|
+
}
|
|
15674
|
+
if (bytes) {
|
|
15675
|
+
charged = Math.max(charged, bytes.byteLength);
|
|
15676
|
+
if (skillUriIdentity(resource.uri) === entryIdentity)
|
|
15677
|
+
entryBytes = bytes;
|
|
15678
|
+
const verification = await verifySkillResource(resource, bytes);
|
|
15679
|
+
files.push({ uri: resource.uri, ...verification });
|
|
15680
|
+
}
|
|
15681
|
+
}
|
|
15682
|
+
} catch (err) {
|
|
15683
|
+
if (err instanceof AuthRecoveryRequiredError) throw err;
|
|
15684
|
+
files.push({
|
|
15685
|
+
uri: resource.uri,
|
|
15686
|
+
status: "read-error",
|
|
15687
|
+
reason: reasonOf(err)
|
|
15688
|
+
});
|
|
15689
|
+
}
|
|
15690
|
+
receivedBytes += charged;
|
|
15691
|
+
if (receivedBytes > SKILL_MAX_TOTAL_BYTES) {
|
|
15692
|
+
if (index < manifest.length - 1) {
|
|
15693
|
+
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.`;
|
|
15694
|
+
}
|
|
15695
|
+
break;
|
|
15696
|
+
}
|
|
15697
|
+
}
|
|
15698
|
+
if (withinBudget && !selfAttempted) {
|
|
15699
|
+
const fail = (reason) => files.push({ uri: entry.uri, status: "read-error", reason });
|
|
15700
|
+
try {
|
|
15701
|
+
const invocation = await client.readResource(entry.uri, metadata);
|
|
15702
|
+
const contents = contentsFor(invocation.result, entry.uri);
|
|
15703
|
+
if (!contents) {
|
|
15704
|
+
fail(
|
|
15705
|
+
"resources/read returned no content block for this skill's own SKILL.md, so its frontmatter cannot be checked against the listing."
|
|
15706
|
+
);
|
|
15707
|
+
} else {
|
|
15708
|
+
try {
|
|
15709
|
+
entryBytes = skillFileBytes(contents);
|
|
15710
|
+
} catch (err) {
|
|
15711
|
+
fail(reasonOf(err));
|
|
15712
|
+
}
|
|
15713
|
+
}
|
|
15714
|
+
const declaredSelf = declared.find(
|
|
15715
|
+
(resource) => skillUriIdentity(resource.uri) === entryIdentity
|
|
15716
|
+
);
|
|
15717
|
+
if (declaredSelf && entryBytes !== void 0) {
|
|
15718
|
+
files.push({
|
|
15719
|
+
// The DECLARED spelling, not the entry's. They can differ — a
|
|
15720
|
+
// manifest may write its self-entry in a normalized-equivalent
|
|
15721
|
+
// form — and a consumer matching rows against the manifest then
|
|
15722
|
+
// finds nothing, while a normalized "extra files" filter suppresses
|
|
15723
|
+
// it as already covered. The result was a verdict that existed in
|
|
15724
|
+
// the report and appeared nowhere on screen (Copilot).
|
|
15725
|
+
uri: declaredSelf.uri,
|
|
15726
|
+
...await verifySkillResource(declaredSelf, entryBytes)
|
|
15727
|
+
});
|
|
15728
|
+
}
|
|
15729
|
+
} catch (err) {
|
|
15730
|
+
if (err instanceof AuthRecoveryRequiredError) throw err;
|
|
15731
|
+
fail(reasonOf(err));
|
|
15732
|
+
}
|
|
15733
|
+
}
|
|
15734
|
+
if (withinBudget) {
|
|
15735
|
+
walkedSkills += 1;
|
|
15736
|
+
catalogBytes += receivedBytes;
|
|
15737
|
+
}
|
|
15738
|
+
const entryText = entryBytes === void 0 ? void 0 : bytesToText(entryBytes);
|
|
15739
|
+
const collision = collisions.get(skillUriIdentity(entry.uri));
|
|
15740
|
+
const conformance = [
|
|
15741
|
+
...checkSkillConformance(entry),
|
|
15742
|
+
...collision ? [collision] : []
|
|
15743
|
+
];
|
|
15744
|
+
const frontmatter = entryText === void 0 ? [] : checkSkillFrontmatterMatch(entry, entryText);
|
|
15745
|
+
const hasError = [...conformance, ...frontmatter].some(
|
|
15746
|
+
(issue) => issue.severity === "error"
|
|
15747
|
+
);
|
|
15748
|
+
const fileFailed = files.some(
|
|
15749
|
+
(file) => file.status === "mismatch" || file.status === "read-error"
|
|
15750
|
+
);
|
|
15751
|
+
reports.push({
|
|
15752
|
+
uri: entry.uri,
|
|
15753
|
+
name: skillDisplayName(entry),
|
|
15754
|
+
conformance,
|
|
15755
|
+
frontmatter,
|
|
15756
|
+
files,
|
|
15757
|
+
...incomplete ? { incomplete } : {},
|
|
15758
|
+
ok: !hasError && !fileFailed,
|
|
15759
|
+
outcome: hasError || fileFailed ? "failed" : incomplete !== void 0 ? "incomplete" : "verified"
|
|
15760
|
+
});
|
|
15761
|
+
}
|
|
15762
|
+
return reports;
|
|
15763
|
+
}
|
|
15764
|
+
function allSkillsVerified(reports) {
|
|
15765
|
+
return reports.every((report) => report.outcome === "verified");
|
|
15766
|
+
}
|
|
15767
|
+
function anySkillFailed(reports) {
|
|
15768
|
+
return reports.some((report) => report.outcome === "failed");
|
|
15769
|
+
}
|
|
15770
|
+
|
|
13255
15771
|
// src/handlers/run-method.ts
|
|
15772
|
+
function assertSkillsSupported(inspectorClient, method) {
|
|
15773
|
+
if (!inspectorClient.getSkillsExtension()) {
|
|
15774
|
+
throw new CliExitCodeError(
|
|
15775
|
+
EXIT_CODES.USAGE,
|
|
15776
|
+
`Server does not declare the ${SKILLS_EXTENSION_KEY} extension, so ${method} is not available.`,
|
|
15777
|
+
{ code: "skills_unsupported" }
|
|
15778
|
+
);
|
|
15779
|
+
}
|
|
15780
|
+
}
|
|
13256
15781
|
async function runMethod(inspectorClient, args) {
|
|
13257
15782
|
let managedToolsState = null;
|
|
13258
15783
|
let managedResourcesState = null;
|
|
13259
15784
|
let managedResourceTemplatesState = null;
|
|
13260
15785
|
let managedPromptsState = null;
|
|
13261
15786
|
let managedTasksState = null;
|
|
15787
|
+
let managedSkillsState = null;
|
|
13262
15788
|
try {
|
|
13263
15789
|
let result;
|
|
13264
15790
|
let appInfo;
|
|
@@ -13477,6 +16003,73 @@ async function runMethod(inspectorClient, args) {
|
|
|
13477
16003
|
result = await inspectorClient.getRequestorTaskResult(
|
|
13478
16004
|
args.taskId
|
|
13479
16005
|
);
|
|
16006
|
+
} else if (args.method === "skills/list") {
|
|
16007
|
+
assertSkillsSupported(inspectorClient, args.method);
|
|
16008
|
+
managedSkillsState = new ManagedSkillsState(inspectorClient);
|
|
16009
|
+
const skills = await managedSkillsState.refresh(args.metadata);
|
|
16010
|
+
if (args.verify) {
|
|
16011
|
+
const reports = await verifySkills(
|
|
16012
|
+
inspectorClient,
|
|
16013
|
+
skills,
|
|
16014
|
+
args.metadata
|
|
16015
|
+
);
|
|
16016
|
+
return {
|
|
16017
|
+
kind: "ndjson",
|
|
16018
|
+
lines: reports,
|
|
16019
|
+
summary: summarizeSkillVerification(reports),
|
|
16020
|
+
// Three outcomes, three exit codes: a broken MUST is 7, a walk the
|
|
16021
|
+
// read bounds cut short is 8, and everything checked and passing is
|
|
16022
|
+
// 0. Collapsing the middle case into either of the others reports
|
|
16023
|
+
// something untrue about the server (Copilot).
|
|
16024
|
+
...allSkillsVerified(reports) ? {} : {
|
|
16025
|
+
exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
|
|
16026
|
+
}
|
|
16027
|
+
};
|
|
16028
|
+
}
|
|
16029
|
+
result = { skills };
|
|
16030
|
+
} else if (args.method === "skills/get") {
|
|
16031
|
+
if (!args.uri) {
|
|
16032
|
+
throw new Error(
|
|
16033
|
+
"URI is required for skills/get method. Use --uri to specify the skill URI."
|
|
16034
|
+
);
|
|
16035
|
+
}
|
|
16036
|
+
assertSkillsSupported(inspectorClient, args.method);
|
|
16037
|
+
const envelope = await inspectorClient.getSkillResult(
|
|
16038
|
+
args.uri,
|
|
16039
|
+
args.metadata
|
|
16040
|
+
);
|
|
16041
|
+
const skill = envelope.skill;
|
|
16042
|
+
if (args.verify) {
|
|
16043
|
+
const reports = await verifySkills(
|
|
16044
|
+
inspectorClient,
|
|
16045
|
+
[skill],
|
|
16046
|
+
args.metadata
|
|
16047
|
+
);
|
|
16048
|
+
return {
|
|
16049
|
+
kind: "ndjson",
|
|
16050
|
+
lines: reports,
|
|
16051
|
+
summary: summarizeSkillVerification(reports),
|
|
16052
|
+
// Three outcomes, three exit codes: a broken MUST is 7, a walk the
|
|
16053
|
+
// read bounds cut short is 8, and everything checked and passing is
|
|
16054
|
+
// 0. Collapsing the middle case into either of the others reports
|
|
16055
|
+
// something untrue about the server (Copilot).
|
|
16056
|
+
...allSkillsVerified(reports) ? {} : {
|
|
16057
|
+
exitCode: anySkillFailed(reports) ? EXIT_CODES.SKILL_NONCONFORMANT : EXIT_CODES.SKILL_INCOMPLETE
|
|
16058
|
+
}
|
|
16059
|
+
};
|
|
16060
|
+
}
|
|
16061
|
+
result = envelope;
|
|
16062
|
+
} else if (args.method === "resources/directory/read") {
|
|
16063
|
+
if (!args.uri) {
|
|
16064
|
+
throw new Error(
|
|
16065
|
+
"URI is required for resources/directory/read. Use --uri to specify the directory URI."
|
|
16066
|
+
);
|
|
16067
|
+
}
|
|
16068
|
+
result = await inspectorClient.readResourceDirectory(
|
|
16069
|
+
args.uri,
|
|
16070
|
+
args.cursor,
|
|
16071
|
+
args.metadata
|
|
16072
|
+
);
|
|
13480
16073
|
} else if (args.method === "roots/list") {
|
|
13481
16074
|
result = { roots: inspectorClient.getRoots() };
|
|
13482
16075
|
} else if (args.method === "roots/set") {
|
|
@@ -13511,6 +16104,7 @@ async function runMethod(inspectorClient, args) {
|
|
|
13511
16104
|
managedResourcesState?.destroy();
|
|
13512
16105
|
managedResourceTemplatesState?.destroy();
|
|
13513
16106
|
managedPromptsState?.destroy();
|
|
16107
|
+
managedSkillsState?.destroy();
|
|
13514
16108
|
managedTasksState?.destroy();
|
|
13515
16109
|
}
|
|
13516
16110
|
}
|
|
@@ -13525,7 +16119,13 @@ var ONE_SHOT_METHODS = [
|
|
|
13525
16119
|
"resources/templates/list",
|
|
13526
16120
|
"prompts/list",
|
|
13527
16121
|
"prompts/get",
|
|
13528
|
-
"logging/setLevel"
|
|
16122
|
+
"logging/setLevel",
|
|
16123
|
+
// SEP-2640. All three are ordinary one-shot request/response calls — no
|
|
16124
|
+
// stream, no long-lived subscription — so they belong here alongside the
|
|
16125
|
+
// other list verbs rather than being reachable only from the session CLI.
|
|
16126
|
+
"skills/list",
|
|
16127
|
+
"skills/get",
|
|
16128
|
+
"resources/directory/read"
|
|
13529
16129
|
];
|
|
13530
16130
|
function isOneShotMethod(method) {
|
|
13531
16131
|
return ONE_SHOT_METHODS.includes(method);
|
|
@@ -13970,7 +16570,7 @@ function createCliOAuthNavigation(options = {}) {
|
|
|
13970
16570
|
}
|
|
13971
16571
|
|
|
13972
16572
|
// ../../core/client/config-parse.ts
|
|
13973
|
-
import { z as
|
|
16573
|
+
import { z as z5 } from "zod";
|
|
13974
16574
|
function isAbsoluteHttpUrl(value) {
|
|
13975
16575
|
let url;
|
|
13976
16576
|
try {
|
|
@@ -13987,11 +16587,11 @@ function isRealHost(hostname) {
|
|
|
13987
16587
|
const labels = hostname.split(".");
|
|
13988
16588
|
return labels.length >= 2 && labels.every((label) => label !== "");
|
|
13989
16589
|
}
|
|
13990
|
-
var HttpUrlStringSchema =
|
|
16590
|
+
var HttpUrlStringSchema = z5.string().min(1).superRefine((val, ctx) => {
|
|
13991
16591
|
const trimmed = val.trim();
|
|
13992
16592
|
if (!isAbsoluteHttpUrl(trimmed)) {
|
|
13993
16593
|
ctx.addIssue({
|
|
13994
|
-
code:
|
|
16594
|
+
code: z5.ZodIssueCode.custom,
|
|
13995
16595
|
message: `Invalid URL: "${trimmed}" \u2014 must be an http(s) URL (e.g. https://idp.example.com)`
|
|
13996
16596
|
});
|
|
13997
16597
|
}
|
|
@@ -14023,7 +16623,7 @@ function refineCimdMetadataUrl(val, ctx, required) {
|
|
|
14023
16623
|
if (trimmed === "") {
|
|
14024
16624
|
if (required) {
|
|
14025
16625
|
ctx.addIssue({
|
|
14026
|
-
code:
|
|
16626
|
+
code: z5.ZodIssueCode.custom,
|
|
14027
16627
|
message: "CIMD client metadata URL is required when CIMD is enabled"
|
|
14028
16628
|
});
|
|
14029
16629
|
}
|
|
@@ -14032,25 +16632,25 @@ function refineCimdMetadataUrl(val, ctx, required) {
|
|
|
14032
16632
|
const error = getCimdClientMetadataUrlError(trimmed);
|
|
14033
16633
|
if (error) {
|
|
14034
16634
|
ctx.addIssue({
|
|
14035
|
-
code:
|
|
16635
|
+
code: z5.ZodIssueCode.custom,
|
|
14036
16636
|
message: error
|
|
14037
16637
|
});
|
|
14038
16638
|
}
|
|
14039
16639
|
}
|
|
14040
|
-
var CimdConfigSchema =
|
|
14041
|
-
enabled:
|
|
14042
|
-
clientMetadataUrl:
|
|
16640
|
+
var CimdConfigSchema = z5.object({
|
|
16641
|
+
enabled: z5.boolean(),
|
|
16642
|
+
clientMetadataUrl: z5.string()
|
|
14043
16643
|
}).superRefine((data, ctx) => {
|
|
14044
16644
|
refineCimdMetadataUrl(data.clientMetadataUrl, ctx, data.enabled === true);
|
|
14045
16645
|
});
|
|
14046
|
-
var EnterpriseManagedAuthIdpConfigSchema =
|
|
16646
|
+
var EnterpriseManagedAuthIdpConfigSchema = z5.object({
|
|
14047
16647
|
issuer: HttpUrlStringSchema,
|
|
14048
|
-
clientId:
|
|
14049
|
-
clientSecret:
|
|
16648
|
+
clientId: z5.string().min(1),
|
|
16649
|
+
clientSecret: z5.string().optional()
|
|
14050
16650
|
});
|
|
14051
|
-
var ClientConfigSchema =
|
|
14052
|
-
enterpriseManagedAuth:
|
|
14053
|
-
enabled:
|
|
16651
|
+
var ClientConfigSchema = z5.object({
|
|
16652
|
+
enterpriseManagedAuth: z5.object({
|
|
16653
|
+
enabled: z5.boolean().optional(),
|
|
14054
16654
|
idp: EnterpriseManagedAuthIdpConfigSchema
|
|
14055
16655
|
}).optional(),
|
|
14056
16656
|
cimd: CimdConfigSchema.optional()
|
|
@@ -14518,14 +17118,22 @@ var validLogLevels = Object.values(
|
|
|
14518
17118
|
LoggingLevelSchema.enum
|
|
14519
17119
|
);
|
|
14520
17120
|
var CLI_CLIENT_NAME = "inspector-cli";
|
|
14521
|
-
async function callMethod(serverConfig, serverSettings, args, clientConfig, cliAuthOverrides, callbackUrlConfig, storedAuthOnly, relogin) {
|
|
17121
|
+
async function callMethod(serverConfig, serverSettings, args, clientConfig, cliAuthOverrides, callbackUrlConfig, storedAuthOnly, relogin, revoke) {
|
|
14522
17122
|
if (relogin) {
|
|
14523
17123
|
if (!("url" in serverConfig && serverConfig.url)) {
|
|
14524
17124
|
throw new Error(
|
|
14525
17125
|
"--relogin requires an HTTP/SSE server URL (no OAuth store entry for stdio)"
|
|
14526
17126
|
);
|
|
14527
17127
|
}
|
|
14528
|
-
await clearStoredAuthForRelogin(serverConfig.url
|
|
17128
|
+
const revocation = await clearStoredAuthForRelogin(serverConfig.url, {
|
|
17129
|
+
revoke: revoke && serverSettings?.oauthRevokeOnClear !== false
|
|
17130
|
+
});
|
|
17131
|
+
if (revocation?.status === "failed") {
|
|
17132
|
+
process.stderr.write(
|
|
17133
|
+
`Warning: could not revoke the OAuth grant at the authorization server (${revocation.detail}); it may still be valid there.
|
|
17134
|
+
`
|
|
17135
|
+
);
|
|
17136
|
+
}
|
|
14529
17137
|
}
|
|
14530
17138
|
const clientIdentity = {
|
|
14531
17139
|
name: CLI_CLIENT_NAME,
|
|
@@ -14646,7 +17254,13 @@ function findStoredToken(servers, serverUrl) {
|
|
|
14646
17254
|
}
|
|
14647
17255
|
async function refreshStoredAuthToken(serverUrl, statePath, deps = {}) {
|
|
14648
17256
|
const refresh = deps.refresh ?? refreshAuthorization;
|
|
14649
|
-
const
|
|
17257
|
+
const storedAuthFetch = withRfc8414OidcCompat(createProxyFetch() ?? fetch);
|
|
17258
|
+
const discover = deps.discover ?? ((authorizationServerUrl, options) => discoverAuthorizationServerMetadata4(authorizationServerUrl, {
|
|
17259
|
+
...options,
|
|
17260
|
+
// A caller-supplied fetch is left alone — it is theirs to compose. The
|
|
17261
|
+
// walker below passes no options, so in practice this is ours.
|
|
17262
|
+
fetchFn: options?.fetchFn ?? storedAuthFetch
|
|
17263
|
+
}));
|
|
14650
17264
|
const snapshot = await readOAuthSnapshot(statePath);
|
|
14651
17265
|
const servers = snapshot.servers;
|
|
14652
17266
|
const found = findStoredServerState(servers, serverUrl);
|
|
@@ -14684,7 +17298,8 @@ async function refreshStoredAuthToken(serverUrl, statePath, deps = {}) {
|
|
|
14684
17298
|
metadata,
|
|
14685
17299
|
clientInformation,
|
|
14686
17300
|
refreshToken,
|
|
14687
|
-
resource: new URL(serverUrl)
|
|
17301
|
+
resource: new URL(serverUrl),
|
|
17302
|
+
fetchFn: storedAuthFetch
|
|
14688
17303
|
});
|
|
14689
17304
|
} catch (err) {
|
|
14690
17305
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -14814,7 +17429,13 @@ async function parseArgs(argv) {
|
|
|
14814
17429
|
"Tool argument as key=value pair",
|
|
14815
17430
|
parseKeyValuePair2,
|
|
14816
17431
|
{}
|
|
14817
|
-
).option(
|
|
17432
|
+
).option(
|
|
17433
|
+
"--uri <uri>",
|
|
17434
|
+
"URI of the resource (resources/read, resources/directory/read) or of the skill (skills/get)"
|
|
17435
|
+
).option(
|
|
17436
|
+
"--cursor <cursor>",
|
|
17437
|
+
"Opaque pagination cursor (for resources/directory/read; pass back the nextCursor from the previous page)."
|
|
17438
|
+
).option(
|
|
14818
17439
|
"--prompt-name <promptName>",
|
|
14819
17440
|
"Name of the prompt (for prompts/get method)"
|
|
14820
17441
|
).option(
|
|
@@ -14866,6 +17487,9 @@ async function parseArgs(argv) {
|
|
|
14866
17487
|
).option(
|
|
14867
17488
|
"--strict",
|
|
14868
17489
|
"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."
|
|
17490
|
+
).option(
|
|
17491
|
+
"--verify",
|
|
17492
|
+
"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."
|
|
14869
17493
|
).option(
|
|
14870
17494
|
"--connect-timeout <ms>",
|
|
14871
17495
|
`Connection timeout in ms (default ${DEFAULT_CONNECT_TIMEOUT_MS} for ad-hoc --server-url / target invocations; 0 = no timeout).`,
|
|
@@ -14912,6 +17536,9 @@ async function parseArgs(argv) {
|
|
|
14912
17536
|
).option(
|
|
14913
17537
|
"--relogin",
|
|
14914
17538
|
"Delete stored OAuth for this server URL from the shared store before connect (HTTP/SSE URL keys only); interactive login runs only if the server requires auth. Rejected for stdio (no URL-keyed store entry)"
|
|
17539
|
+
).option(
|
|
17540
|
+
"--no-revoke",
|
|
17541
|
+
"Requires --relogin. Skips the RFC 7009 revocation request that would otherwise end the grant at the authorization server when the local state is deleted. Also skipped when the server entry sets oauth.revokeOnClear to false."
|
|
14915
17542
|
).option(
|
|
14916
17543
|
"--wait-for-auth <sec>",
|
|
14917
17544
|
"Poll the OAuth state file until a token for --server-url appears (or the timeout elapses), then proceed as if --use-stored-auth were set. Use after handing off to a human to complete OAuth in a browser.",
|
|
@@ -14953,6 +17580,9 @@ async function parseArgs(argv) {
|
|
|
14953
17580
|
);
|
|
14954
17581
|
}
|
|
14955
17582
|
}
|
|
17583
|
+
if (options.revoke === false && !options.relogin) {
|
|
17584
|
+
throw new Error("--no-revoke requires --relogin (it has no other effect).");
|
|
17585
|
+
}
|
|
14956
17586
|
if (options.strict) {
|
|
14957
17587
|
if (options.method !== "tools/list") {
|
|
14958
17588
|
throw new Error("--strict requires --method tools/list.");
|
|
@@ -14963,6 +17593,13 @@ async function parseArgs(argv) {
|
|
|
14963
17593
|
);
|
|
14964
17594
|
}
|
|
14965
17595
|
}
|
|
17596
|
+
if (options.verify) {
|
|
17597
|
+
if (options.method !== "skills/list" && options.method !== "skills/get") {
|
|
17598
|
+
throw new Error(
|
|
17599
|
+
"--verify requires --method skills/list or --method skills/get."
|
|
17600
|
+
);
|
|
17601
|
+
}
|
|
17602
|
+
}
|
|
14966
17603
|
const oauthStatePath = getStateFilePath();
|
|
14967
17604
|
if (options.listStoredAuth) {
|
|
14968
17605
|
const servers = await readOAuthServers(oauthStatePath);
|
|
@@ -15126,6 +17763,8 @@ async function parseArgs(argv) {
|
|
|
15126
17763
|
toolMeta: options.toolMetadata,
|
|
15127
17764
|
appInfo: options.appInfo === true,
|
|
15128
17765
|
strict: options.strict === true,
|
|
17766
|
+
verify: options.verify === true,
|
|
17767
|
+
cursor: options.cursor,
|
|
15129
17768
|
format: options.format
|
|
15130
17769
|
};
|
|
15131
17770
|
return {
|
|
@@ -15138,7 +17777,10 @@ async function parseArgs(argv) {
|
|
|
15138
17777
|
clientMetadataUrl: options.clientMetadataUrl,
|
|
15139
17778
|
callbackUrl: options.callbackUrl,
|
|
15140
17779
|
storedAuthOnly: options.storedAuthOnly === true,
|
|
15141
|
-
relogin: options.relogin === true
|
|
17780
|
+
relogin: options.relogin === true,
|
|
17781
|
+
// Commander's `--no-revoke` defaults this to true; only an explicit
|
|
17782
|
+
// `--no-revoke` makes it false.
|
|
17783
|
+
revoke: options.revoke !== false
|
|
15142
17784
|
};
|
|
15143
17785
|
}
|
|
15144
17786
|
async function runCli(argv) {
|
|
@@ -15154,7 +17796,8 @@ async function runCli(argv) {
|
|
|
15154
17796
|
clientMetadataUrl,
|
|
15155
17797
|
callbackUrl,
|
|
15156
17798
|
storedAuthOnly,
|
|
15157
|
-
relogin
|
|
17799
|
+
relogin,
|
|
17800
|
+
revoke
|
|
15158
17801
|
} = parsed;
|
|
15159
17802
|
const clientConfig = await loadRunnerClientConfig({ clientConfigPath });
|
|
15160
17803
|
let callbackUrlConfig;
|
|
@@ -15178,7 +17821,8 @@ async function runCli(argv) {
|
|
|
15178
17821
|
},
|
|
15179
17822
|
callbackUrlConfig,
|
|
15180
17823
|
storedAuthOnly === true,
|
|
15181
|
-
relogin === true
|
|
17824
|
+
relogin === true,
|
|
17825
|
+
revoke !== false
|
|
15182
17826
|
);
|
|
15183
17827
|
}
|
|
15184
17828
|
|