@modelcontextprotocol/inspector 2.5.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.
@@ -616,7 +616,11 @@ function isOfferable(branch, merged) {
616
616
  return false;
617
617
  }
618
618
  if (!admitsObject(branch)) return false;
619
- const properties = Object.values(propertiesOf(merged));
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);
620
624
  return properties.length > 0 && // Every value has to be something a renderer can read AND something a
621
625
  // caller can satisfy. A `null` or an array is not a schema at all, and the
622
626
  // web form dereferences one on the way to choosing a widget. JSON Schema's
@@ -4238,6 +4242,7 @@ import {
4238
4242
  OAuthTokensSchema as OAuthTokensSchema2
4239
4243
  } from "@modelcontextprotocol/core";
4240
4244
  var DEFAULT_REVOCATION_TIMEOUT_MS = 5e3;
4245
+ var MIN_REVOCATION_REQUEST_BUDGET_MS = 5;
4241
4246
  function selectRevocableToken(tokens) {
4242
4247
  if (!tokens) return null;
4243
4248
  if (tokens.refresh_token) {
@@ -4285,7 +4290,10 @@ function base64Encode(value) {
4285
4290
  return Buffer.from(value, "utf8").toString("base64");
4286
4291
  }
4287
4292
  async function revokeToken(params) {
4288
- const timeoutMs = params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS;
4293
+ const timeoutMs = Math.max(
4294
+ 0,
4295
+ Math.round(params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS)
4296
+ );
4289
4297
  try {
4290
4298
  const { url, init } = buildRevocationRequest(params);
4291
4299
  const response = await withDeadline(
@@ -4473,7 +4481,7 @@ async function runPlan(plan, params) {
4473
4481
  if (!endpoint) return { status: "skipped", reason: "no_endpoint" };
4474
4482
  const outcomes = [...plan.failures];
4475
4483
  const timeoutMs = params.timeoutMs ?? DEFAULT_REVOCATION_TIMEOUT_MS;
4476
- const deadlineAt = Date.now() + timeoutMs;
4484
+ const deadlineAt = performance.now() + timeoutMs;
4477
4485
  for (const grant of plan.grants) {
4478
4486
  if (grant.issuer === void 0) {
4479
4487
  outcomes.push({
@@ -4489,8 +4497,8 @@ async function runPlan(plan, params) {
4489
4497
  });
4490
4498
  continue;
4491
4499
  }
4492
- const remainingMs = deadlineAt - Date.now();
4493
- if (remainingMs <= 0) {
4500
+ const remainingMs = deadlineAt - performance.now();
4501
+ if (remainingMs <= MIN_REVOCATION_REQUEST_BUDGET_MS) {
4494
4502
  outcomes.push({
4495
4503
  status: "failed",
4496
4504
  endpoint,
@@ -4542,13 +4550,13 @@ async function clearStoredAuthForRelogin(serverUrl, options) {
4542
4550
  }
4543
4551
  async function sendPlans(plans, budgetMs) {
4544
4552
  const fetchFn = createProxyFetch() ?? fetch;
4545
- const deadlineAt = Date.now() + budgetMs;
4553
+ const deadlineAt = performance.now() + budgetMs;
4546
4554
  let reported;
4547
4555
  let lastSkip;
4548
4556
  for (const plan of plans) {
4549
- const remainingMs = deadlineAt - Date.now();
4557
+ const remainingMs = deadlineAt - performance.now();
4550
4558
  const needsNetwork = plan.outcome === void 0;
4551
- if (needsNetwork && remainingMs <= 0) {
4559
+ if (needsNetwork && remainingMs <= MIN_REVOCATION_REQUEST_BUDGET_MS) {
4552
4560
  const exhausted = {
4553
4561
  status: "failed",
4554
4562
  detail: `the ${budgetMs}ms revocation budget was exhausted before "${plan.serverUrl}" was attempted`
@@ -4978,6 +4986,653 @@ function buildClientExtensions(input) {
4978
4986
  return map;
4979
4987
  }
4980
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);
5521
+ }
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
+ ];
5538
+ }
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;
5634
+ }
5635
+
4981
5636
  // ../../core/mcp/appElicitation.ts
4982
5637
  import { ElicitResultSchema } from "@modelcontextprotocol/core";
4983
5638
  function uiExtension(capabilities) {
@@ -5065,7 +5720,7 @@ import {
5065
5720
  ListResourcesResultSchema,
5066
5721
  ListResourceTemplatesResultSchema,
5067
5722
  ListPromptsResultSchema,
5068
- ResourceSchema,
5723
+ ResourceSchema as ResourceSchema2,
5069
5724
  ResourceTemplateSchema,
5070
5725
  PromptSchema
5071
5726
  } from "@modelcontextprotocol/core";
@@ -5088,7 +5743,7 @@ function validateToolOutput(provider, tool, result) {
5088
5743
  }
5089
5744
 
5090
5745
  // ../../core/mcp/listSalvage.ts
5091
- import { z as z2 } from "zod/v4";
5746
+ import { z as z3 } from "zod/v4";
5092
5747
  import { SdkError as SdkError2, SdkErrorCode as SdkErrorCode2 } from "@modelcontextprotocol/client";
5093
5748
  import { ToolSchema } from "@modelcontextprotocol/core";
5094
5749
  var LIST_MAX_PAGES = 64;
@@ -5104,20 +5759,20 @@ function isSalvageableRejection(err) {
5104
5759
  return SdkError2.isInstance(err) && err.code === SdkErrorCode2.InvalidResult;
5105
5760
  }
5106
5761
  function lenientListPageSchema(resultSchema, itemsKey) {
5107
- return resultSchema.extend({ [itemsKey]: z2.array(z2.unknown()) });
5762
+ return resultSchema.extend({ [itemsKey]: z3.array(z3.unknown()) });
5108
5763
  }
5109
5764
  function nextCursorOf(page) {
5110
5765
  if (typeof page !== "object" || page === null) return void 0;
5111
5766
  const value = page.nextCursor;
5112
5767
  return typeof value === "string" ? value : void 0;
5113
5768
  }
5114
- var ModernResultEnvelopeSchema = z2.looseObject({
5115
- resultType: z2.literal("complete"),
5769
+ var ModernResultEnvelopeSchema = z3.looseObject({
5770
+ resultType: z3.literal("complete"),
5116
5771
  // A non-negative INTEGER, matching the codec: `ttlMs: -1` or `0.5` is an
5117
5772
  // envelope violation the strict path rejects, and accepting it here on the
5118
5773
  // strength of an unrelated bad entry would hide it.
5119
- ttlMs: z2.int().min(0),
5120
- cacheScope: z2.enum(["public", "private"])
5774
+ ttlMs: z3.int().min(0),
5775
+ cacheScope: z3.enum(["public", "private"])
5121
5776
  });
5122
5777
  function rawItemsOf(page, itemsKey) {
5123
5778
  if (typeof page !== "object" || page === null) return void 0;
@@ -5186,9 +5841,9 @@ var LegacyToolWireSchema = ToolSchema.extend({
5186
5841
  outputSchema: ToolSchema.shape.inputSchema.optional()
5187
5842
  });
5188
5843
  var ModernToolWireSchema = ToolSchema.extend({
5189
- inputSchema: z2.looseObject({
5190
- type: z2.literal("object"),
5191
- $schema: z2.string().optional()
5844
+ inputSchema: z3.looseObject({
5845
+ type: z3.literal("object"),
5846
+ $schema: z3.string().optional()
5192
5847
  })
5193
5848
  });
5194
5849
  function stripExecution(tool) {
@@ -5202,10 +5857,10 @@ function eraToolItemSchema(wire, normalize) {
5202
5857
 
5203
5858
  // ../../core/mcp/taskNotificationSchemas.ts
5204
5859
  import { NotificationSchema } from "@modelcontextprotocol/core";
5205
- import { z as z3 } from "zod/v4";
5860
+ import { z as z4 } from "zod/v4";
5206
5861
  var TasksListChangedNotificationSchema = NotificationSchema.extend({
5207
- method: z3.literal("notifications/tasks/list_changed"),
5208
- params: z3.record(z3.string(), z3.unknown()).optional()
5862
+ method: z4.literal("notifications/tasks/list_changed"),
5863
+ params: z4.record(z4.string(), z4.unknown()).optional()
5209
5864
  });
5210
5865
 
5211
5866
  // ../../core/mcp/uriTemplate.ts
@@ -6186,8 +6841,8 @@ var BaseOAuthClientProvider = class {
6186
6841
  );
6187
6842
  }
6188
6843
  async saveClientInformation(clientInformation, options) {
6189
- const registrationKind = options && "registrationKind" in options ? options.registrationKind : "dcr";
6190
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);
6191
6846
  await this.storage.saveClientInformation(
6192
6847
  this.serverUrl,
6193
6848
  clientInformation,
@@ -6197,6 +6852,70 @@ var BaseOAuthClientProvider = class {
6197
6852
  }
6198
6853
  );
6199
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
+ }
6200
6919
  async saveScope(scope) {
6201
6920
  await this.storage.saveScope(this.serverUrl, scope);
6202
6921
  this.cachedScope = scope;
@@ -7075,35 +7794,49 @@ import { discoverOAuthProtectedResourceMetadata as discoverOAuthProtectedResourc
7075
7794
  async function ensureCimdClientRegistration(params) {
7076
7795
  const clientMetadataUrl = params.provider.clientMetadataUrl?.trim();
7077
7796
  if (!clientMetadataUrl) return;
7078
- const existing = await params.provider.clientInformation();
7079
- if (existing?.client_id) return;
7080
- let resourceMetadata;
7081
- try {
7082
- resourceMetadata = await discoverOAuthProtectedResourceMetadata2(
7083
- params.serverUrl,
7084
- { resourceMetadataUrl: params.resourceMetadataUrl },
7085
- // The same fetch the AS-metadata leg below uses. On web that is
7086
- // `createRemoteFetch`, which proxies through the backend to sidestep
7087
- // CORS — on the global `fetch` this leg would fail in the browser, be
7088
- // swallowed by the catch, and leave CIMD probing the wrong
7089
- // authorization server (Copilot).
7090
- params.fetchFn
7091
- );
7092
- } catch {
7093
- resourceMetadata = void 0;
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
+ }
7094
7823
  }
7095
- const metadata = await discoverAuthorizationServerMetadataForServer(
7096
- params.serverUrl,
7097
- resourceMetadata,
7098
- params.fetchFn
7099
- );
7824
+ const issuer = metadata?.issuer;
7100
7825
  if (!metadata?.client_id_metadata_document_supported) return;
7101
- const clientInformation = {
7102
- client_id: clientMetadataUrl
7103
- };
7104
- await params.provider.saveClientInformation(clientInformation, {
7105
- registrationKind: "cimd"
7106
- });
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
+ );
7107
7840
  }
7108
7841
 
7109
7842
  // ../../core/auth/ema/transportProvider.ts
@@ -7130,11 +7863,26 @@ var EmaTransportOAuthProvider = class {
7130
7863
  state() {
7131
7864
  return this.inner.state();
7132
7865
  }
7133
- clientInformation() {
7134
- return this.inner.clientInformation();
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);
7135
7876
  }
7136
- saveClientInformation(clientInformation) {
7137
- return this.inner.saveClientInformation(clientInformation);
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);
7883
+ }
7884
+ discoveryState() {
7885
+ return this.inner.discoveryState();
7138
7886
  }
7139
7887
  async tokens() {
7140
7888
  const stored = await this.inner.tokens();
@@ -7203,8 +7951,11 @@ function issuerMismatchTitle() {
7203
7951
  return "Authorization server mismatch";
7204
7952
  }
7205
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
+ }
7206
7957
  function truncateIssuerForDisplay(issuer) {
7207
- return issuer.length > MAX_DISPLAYED_ISSUER_LENGTH ? `${issuer.slice(0, MAX_DISPLAYED_ISSUER_LENGTH)}\u2026` : issuer;
7958
+ return truncateUrlForDisplay(issuer);
7208
7959
  }
7209
7960
  function issuerMismatchMessage(options) {
7210
7961
  const target = options.serverName ? `"${options.serverName}"` : "this server";
@@ -10418,7 +11169,13 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
10418
11169
  throw new Error("Client is not connected");
10419
11170
  }
10420
11171
  const result = await this.client.request(
10421
- { method: "tasks/list", params: cursor ? { cursor } : {} },
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
+ },
10422
11179
  ListTasksResultSchema,
10423
11180
  this.getRequestOptions()
10424
11181
  );
@@ -12012,7 +12769,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12012
12769
  const effectiveMeta = this.mergeMeta(metadata);
12013
12770
  const params = {
12014
12771
  ...effectiveMeta ? { _meta: effectiveMeta } : {},
12015
- ...cursor ? { cursor } : {}
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 } : {}
12016
12776
  };
12017
12777
  const response = await this.invokeMcpClient(
12018
12778
  () => this.client.request(
@@ -12040,7 +12800,7 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12040
12800
  method: "resources/list",
12041
12801
  itemsKey: "resources",
12042
12802
  resultSchema: ListResourcesResultSchema,
12043
- itemSchema: ResourceSchema,
12803
+ itemSchema: ResourceSchema2,
12044
12804
  metadata: options?.metadata,
12045
12805
  aggregate: async () => [
12046
12806
  ...(await this.client.listResources(
@@ -12142,7 +12902,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12142
12902
  const effectiveMeta = this.mergeMeta(metadata);
12143
12903
  const params = {
12144
12904
  ...effectiveMeta ? { _meta: effectiveMeta } : {},
12145
- ...cursor ? { cursor } : {}
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 } : {}
12146
12909
  };
12147
12910
  const response = await this.invokeMcpClient(
12148
12911
  () => this.client.request(
@@ -12195,7 +12958,10 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12195
12958
  const effectiveMeta = this.mergeMeta(metadata);
12196
12959
  const params = {
12197
12960
  ...effectiveMeta ? { _meta: effectiveMeta } : {},
12198
- ...cursor ? { cursor } : {}
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 } : {}
12199
12965
  };
12200
12966
  const response = await this.invokeMcpClient(
12201
12967
  () => this.client.request(
@@ -12234,6 +13000,155 @@ var InspectorClient = class _InspectorClient extends InspectorClientEventTarget
12234
13000
  });
12235
13001
  return { prompts };
12236
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) {
13114
+ if (!this.client) {
13115
+ throw new Error("Client is not connected");
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
+ }
13123
+ const effectiveMeta = this.mergeMeta(metadata);
13124
+ const params = {
13125
+ uri,
13126
+ ...effectiveMeta ? { _meta: effectiveMeta } : {},
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 } : {}
13131
+ };
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;
13150
+ }
13151
+ }
12237
13152
  /**
12238
13153
  * Get a prompt by name
12239
13154
  * @param name Prompt name
@@ -13259,7 +14174,30 @@ var EXIT_CODES = {
13259
14174
  AUTH_REQUIRED: 3,
13260
14175
  UNREACHABLE: 4,
13261
14176
  TOOL_ERROR: 5,
13262
- 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
13263
14201
  };
13264
14202
  var CliExitCodeError = class extends Error {
13265
14203
  constructor(exitCode, message, envelope) {
@@ -13767,6 +14705,15 @@ async function consumeMethodOutcome(outcome, args) {
13767
14705
  for (const line of outcome.lines) {
13768
14706
  await awaitableLog(JSON.stringify(line) + "\n");
13769
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
+ }
13770
14717
  return;
13771
14718
  }
13772
14719
  await new Promise((resolve7) => {
@@ -14332,7 +15279,7 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
14332
15279
  const page = result.tasks.filter((t) => !this.dismissedTaskIds.has(t.taskId)).map(
14333
15280
  (t) => this.cancelledTaskIds.has(t.taskId) ? { ...t, status: "cancelled" } : t
14334
15281
  );
14335
- this.tasks = cursor ? [...this.tasks, ...page] : page;
15282
+ this.tasks = cursor === void 0 ? page : [...this.tasks, ...page];
14336
15283
  cursor = result.nextCursor;
14337
15284
  pageCount++;
14338
15285
  if (pageCount >= MAX_PAGES) {
@@ -14340,7 +15287,7 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
14340
15287
  `Maximum pagination limit (${MAX_PAGES} pages) reached while listing requestor tasks`
14341
15288
  );
14342
15289
  }
14343
- } while (cursor);
15290
+ } while (cursor !== void 0);
14344
15291
  this.dispatchTypedEvent("tasksChange", this.tasks);
14345
15292
  return this.getTasks();
14346
15293
  }
@@ -14393,6 +15340,146 @@ var ManagedRequestorTasksState = class extends TypedEventTarget {
14393
15340
  }
14394
15341
  };
14395
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
+
14396
15483
  // ../../core/mcp/apps.ts
14397
15484
  import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
14398
15485
  function getAppResourceUri(tool) {
@@ -14463,13 +15550,241 @@ async function collectAppInfo(client, tool, metadata) {
14463
15550
  }
14464
15551
  }
14465
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
+
14466
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
+ }
14467
15781
  async function runMethod(inspectorClient, args) {
14468
15782
  let managedToolsState = null;
14469
15783
  let managedResourcesState = null;
14470
15784
  let managedResourceTemplatesState = null;
14471
15785
  let managedPromptsState = null;
14472
15786
  let managedTasksState = null;
15787
+ let managedSkillsState = null;
14473
15788
  try {
14474
15789
  let result;
14475
15790
  let appInfo;
@@ -14688,6 +16003,73 @@ async function runMethod(inspectorClient, args) {
14688
16003
  result = await inspectorClient.getRequestorTaskResult(
14689
16004
  args.taskId
14690
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
+ );
14691
16073
  } else if (args.method === "roots/list") {
14692
16074
  result = { roots: inspectorClient.getRoots() };
14693
16075
  } else if (args.method === "roots/set") {
@@ -14722,6 +16104,7 @@ async function runMethod(inspectorClient, args) {
14722
16104
  managedResourcesState?.destroy();
14723
16105
  managedResourceTemplatesState?.destroy();
14724
16106
  managedPromptsState?.destroy();
16107
+ managedSkillsState?.destroy();
14725
16108
  managedTasksState?.destroy();
14726
16109
  }
14727
16110
  }
@@ -14736,7 +16119,13 @@ var ONE_SHOT_METHODS = [
14736
16119
  "resources/templates/list",
14737
16120
  "prompts/list",
14738
16121
  "prompts/get",
14739
- "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"
14740
16129
  ];
14741
16130
  function isOneShotMethod(method) {
14742
16131
  return ONE_SHOT_METHODS.includes(method);
@@ -15181,7 +16570,7 @@ function createCliOAuthNavigation(options = {}) {
15181
16570
  }
15182
16571
 
15183
16572
  // ../../core/client/config-parse.ts
15184
- import { z as z4 } from "zod";
16573
+ import { z as z5 } from "zod";
15185
16574
  function isAbsoluteHttpUrl(value) {
15186
16575
  let url;
15187
16576
  try {
@@ -15198,11 +16587,11 @@ function isRealHost(hostname) {
15198
16587
  const labels = hostname.split(".");
15199
16588
  return labels.length >= 2 && labels.every((label) => label !== "");
15200
16589
  }
15201
- var HttpUrlStringSchema = z4.string().min(1).superRefine((val, ctx) => {
16590
+ var HttpUrlStringSchema = z5.string().min(1).superRefine((val, ctx) => {
15202
16591
  const trimmed = val.trim();
15203
16592
  if (!isAbsoluteHttpUrl(trimmed)) {
15204
16593
  ctx.addIssue({
15205
- code: z4.ZodIssueCode.custom,
16594
+ code: z5.ZodIssueCode.custom,
15206
16595
  message: `Invalid URL: "${trimmed}" \u2014 must be an http(s) URL (e.g. https://idp.example.com)`
15207
16596
  });
15208
16597
  }
@@ -15234,7 +16623,7 @@ function refineCimdMetadataUrl(val, ctx, required) {
15234
16623
  if (trimmed === "") {
15235
16624
  if (required) {
15236
16625
  ctx.addIssue({
15237
- code: z4.ZodIssueCode.custom,
16626
+ code: z5.ZodIssueCode.custom,
15238
16627
  message: "CIMD client metadata URL is required when CIMD is enabled"
15239
16628
  });
15240
16629
  }
@@ -15243,25 +16632,25 @@ function refineCimdMetadataUrl(val, ctx, required) {
15243
16632
  const error = getCimdClientMetadataUrlError(trimmed);
15244
16633
  if (error) {
15245
16634
  ctx.addIssue({
15246
- code: z4.ZodIssueCode.custom,
16635
+ code: z5.ZodIssueCode.custom,
15247
16636
  message: error
15248
16637
  });
15249
16638
  }
15250
16639
  }
15251
- var CimdConfigSchema = z4.object({
15252
- enabled: z4.boolean(),
15253
- clientMetadataUrl: z4.string()
16640
+ var CimdConfigSchema = z5.object({
16641
+ enabled: z5.boolean(),
16642
+ clientMetadataUrl: z5.string()
15254
16643
  }).superRefine((data, ctx) => {
15255
16644
  refineCimdMetadataUrl(data.clientMetadataUrl, ctx, data.enabled === true);
15256
16645
  });
15257
- var EnterpriseManagedAuthIdpConfigSchema = z4.object({
16646
+ var EnterpriseManagedAuthIdpConfigSchema = z5.object({
15258
16647
  issuer: HttpUrlStringSchema,
15259
- clientId: z4.string().min(1),
15260
- clientSecret: z4.string().optional()
16648
+ clientId: z5.string().min(1),
16649
+ clientSecret: z5.string().optional()
15261
16650
  });
15262
- var ClientConfigSchema = z4.object({
15263
- enterpriseManagedAuth: z4.object({
15264
- enabled: z4.boolean().optional(),
16651
+ var ClientConfigSchema = z5.object({
16652
+ enterpriseManagedAuth: z5.object({
16653
+ enabled: z5.boolean().optional(),
15265
16654
  idp: EnterpriseManagedAuthIdpConfigSchema
15266
16655
  }).optional(),
15267
16656
  cimd: CimdConfigSchema.optional()
@@ -16040,7 +17429,13 @@ async function parseArgs(argv) {
16040
17429
  "Tool argument as key=value pair",
16041
17430
  parseKeyValuePair2,
16042
17431
  {}
16043
- ).option("--uri <uri>", "URI of the resource (for resources/read method)").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(
16044
17439
  "--prompt-name <promptName>",
16045
17440
  "Name of the prompt (for prompts/get method)"
16046
17441
  ).option(
@@ -16092,6 +17487,9 @@ async function parseArgs(argv) {
16092
17487
  ).option(
16093
17488
  "--strict",
16094
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."
16095
17493
  ).option(
16096
17494
  "--connect-timeout <ms>",
16097
17495
  `Connection timeout in ms (default ${DEFAULT_CONNECT_TIMEOUT_MS} for ad-hoc --server-url / target invocations; 0 = no timeout).`,
@@ -16195,6 +17593,13 @@ async function parseArgs(argv) {
16195
17593
  );
16196
17594
  }
16197
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
+ }
16198
17603
  const oauthStatePath = getStateFilePath();
16199
17604
  if (options.listStoredAuth) {
16200
17605
  const servers = await readOAuthServers(oauthStatePath);
@@ -16358,6 +17763,8 @@ async function parseArgs(argv) {
16358
17763
  toolMeta: options.toolMetadata,
16359
17764
  appInfo: options.appInfo === true,
16360
17765
  strict: options.strict === true,
17766
+ verify: options.verify === true,
17767
+ cursor: options.cursor,
16361
17768
  format: options.format
16362
17769
  };
16363
17770
  return {