Commit graph inspector/core/mcp/skillsSchemas.ts
Author SHA1 Message Date
cliffhall
f3e0edd257 docs: describe DirectoryReadResultSchema as the shape both eras share (#2374 review)
Its doc comment still called it the legacy result, though both eras now
parse with it once the codec has removed resultType.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3RsrgCxVoFPokLB4vAkxi
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-14 09:25:16 -04:00
cliffhall
2243849711 docs: bring comments and docs in line with -32021 and the Skills registry entry (#2374 review)
Round 2 found text the PR's own changes made stale: the fixture's
option doc, handler summary and repro step still promised -32601 on
modern; the core skills.ts header still said Skills must never be in
ADVERTISABLE_EXTENSIONS; the listSkills and getSkillResult comments
still said the codec validates none of a skills result, or that the
result arrives as the server sent it; and two tests and a docs step
still described Tasks as the only registry extension. The backfill
test also now asserts the Skills declaration is on by default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3RsrgCxVoFPokLB4vAkxi
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-14 09:07:58 -04:00
cliffhall
1d765b8af6 fix: declare the skills extension in the Inspector's client capabilities (#2373)
SEP-2133 negotiates an extension from both sides, and a strict server
refuses skills/* to a client that did not declare
io.modelcontextprotocol/skills. The Inspector called those methods
without ever declaring it. Add it to ADVERTISABLE_EXTENSIONS, on by
default, which also gives it a Server Settings toggle.

Give the composable test server skillsRequireClientExtension and two
per-era strict showcase configs, so a strict server is reproducible here.

Writing the modern-era test exposed two defects the existing suite hid:
its "modern" cases set protocolEra on the transport config, which
InspectorClient does not read, so they connected on legacy; and once
they really ran modern, every skills result failed, because the modern
schemas required resultType, which the SDK codec checks and then lifts
off before a caller schema runs. Connect with versionNegotiation and
assert the era, and drop the resultType requirement (and the two modern
schema variants it was the only difference in).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q3RsrgCxVoFPokLB4vAkxi
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-14 08:36:37 -04:00
cliffhall
4f12d07d2b fix: address Copilot review round 13 on #2248
**The byte budget trusted server-declared sizes.** A manifest advertising
`size: 1` — or, since the wire schema deliberately accepts it for reporting,
no size at all — sailed through the declared budget and then served
arbitrarily large bodies. The 16 MiB safeguard protected against an honest
server only, which is no safeguard.

The walk now tracks bytes ACTUALLY received and stops issuing reads once the
real total crosses the cap, marking the report `incomplete` and so `ok:
false`. The declared prefilter is kept rather than replaced: it refuses to
schedule an obviously oversized set, and the received-byte counter stops one
that lied. The count happens after verifying the crossing file, so that file
is still reported rather than fetched and discarded.

⚠️ This bounds the total across responses, not any single one: a first
response larger than the cap is already in memory before it can be measured,
which would need a streaming read the client API does not expose. Stated in
the code rather than left to look closed.

**A modern `skills/get` did not require `resultType`.** The "left open"
question SEP-2640 states covers `ttlMs` / `cacheScope` and only those;
`resultType` is base-protocol per SEP-2322 and appears in the SEP's own
`skills/get` example. Requiring it of `resources/directory/read` but not here
was an inconsistency in the module rather than a distinction the spec draws.
Era-selected now, with the caching attributes still optional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND46Ci64DC3tzBy9aG81Nc
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-07 22:09:19 -04:00
cliffhall
d6b1e36e7e fix: address Copilot review round 7 on #2248
Four findings.

**The CLI dropped what it promised to print.** `getSkill` transforms the
`skills/get` envelope to a `SkillEntry`, so every other member the loose
schema accepted was gone before the CLI saw it — including the `ttlMs` /
`cacheScope` SEP-2640 explicitly leaves open. The code contradicted a comment
two lines above it saying a CLI whose contract is "print the result" must not
reshape one. `getSkillResult` returns the envelope whole; `getSkill` is a
one-line unwrap over it, which matches the callers: the UIs want the entry,
the CLI wants the result.

**The frontmatter verdict and the digest came from different reads.** The
preview fetch and the Verify fetch are separate `resources/read` calls, so a
resource changing between them could pair a verified digest with a
frontmatter verdict for other bytes. `VerificationState.entryText` now holds
the SKILL.md as the verification read it, written in the SAME state update as
the verdict — a separate write was clobbered, because `write` returns a fresh
object and drops anything not named in it. The preview stays the fallback so
a reader who has not clicked Verify still gets the check.

**The Conformance badge excluded the frontmatter findings** it renders, so it
said `0 error(s)` above a red mismatch. Digest and size mismatches stay in
their own badge: "the listing is wrong" and "the bytes are wrong" are
different answers.

**A repeated skill URI collided two React keys** in the TUI list, in a pane
whose job is to show both entries. Index-keyed like the manifest rows.

The web fixture also gains its collision pair in the served-file lookup —
without it they were handed another skill's SKILL.md, which the badge fix
correctly began reporting as a frontmatter error.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND46Ci64DC3tzBy9aG81Nc
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-07 20:44:51 -04:00
cliffhall
9a8488681e feat: Skills extension phase 3 — CLI, TUI, directory reads, frontmatter check
Closes #2248.

Completes SEP-2640 support across all three clients, and closes the one
obligation #2234 deliberately left open.

- `checkSkillFrontmatterMatch` compares a served SKILL.md's own YAML
  frontmatter against the entry the listing advertised, field by field. No
  digest can cover this: a digest is taken over the bytes the server served,
  so it proves the file was not altered in transit and says nothing about
  whether the listing described it honestly. Needs a real YAML parser; `yaml`
  was already a root dependency, so this adds no package — but it does newly
  put it on core/'s import graph, hence the three bundler `external` lists.

- `resources/directory/read`: result schemas defined against the normative
  text, `InspectorClient.readResourceDirectory`, a Directory section on the
  Skills screen, and `--method resources/directory/read` in the CLI. The
  client refuses the call locally when the server did not declare
  `directoryRead`, which is the SEP's MUST NOT. A child the directory lists
  but the entry does not is marked `not listed` rather than merged into the
  manifest — the SEP calls a directory result a live observation and forbids
  treating it as extending the manifest.

- CLI: `skills/list`, `skills/get` and `resources/directory/read`, plus
  `--verify` — one NDJSON report per skill, a summary on stderr, exit 7 on a
  violation. Reads are sequential; a conforming manifest may declare 512
  entries.

- TUI: a Skills pane, shown only when the server declares the extension. Each
  row carries its conformance verdict as a glyph as well as a colour, since
  the pane is read over ssh and through script(1). Enter verifies.

- `verifySkills` re-throws `AuthRecoveryRequiredError` rather than recording
  it per file: it says the session's authorization expired, so absorbing it
  would report N identical read failures and swallow the error the TUI and
  the web commands key off to reauthorize.

Two open questions settled, both in code comments where they will be found:
`skills/get` carries no caching attributes because SEP-2640 leaves the
question open in as many words, so requiring them would fail a conforming
server; and there is no `PagedSkillsState` because every consumer of this
list is a whole-catalog verdict — computed over page one of three, "this
server's skills conform" is not merely partial but wrong.

The fixture grows two skills, both for checks that were otherwise
undemonstrable: `lying-listing` (listing and file disagree, digest still
verifies) and `stale-manifest` (serves a file its manifest does not declare).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND46Ci64DC3tzBy9aG81Nc
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-07 18:10:37 -04:00
cliffhall
11ac134975 feat: require the modern list envelope on skills/list (review round 14)
The review finally quoted the normative text, which is what I asked for
across five rounds before declining: SEP-2640's `skills/list` section says
"In protocol versions 2026-07-28 and later, the result also carries …
`ttlMs` and `cacheScope`." So the era-aware validation is in.

- skillsSchemas: `ModernListSkillsResultSchema` = the page plus the base
  list envelope, with field shapes mirroring `ModernResultEnvelopeSchema`
  in `listSalvage.ts` so this repo's two statements of a modern envelope
  cannot drift. `InspectorClient.listSkills` picks it from the negotiated
  era; the legacy shape stays permissive, because those are 2026-era
  attributes a legacy server has no business sending.
  Nothing else catches this: `skills/*` is consumer-owned, so it is absent
  from the SDK's cacheable-method registry and a modern server answering
  `{ skills: [] }` was reaching the conformance UI as a clean list.
- skills.ts: count `frontmatter.description` (and `name`) length in Unicode
  CODE POINTS. `String.length` counts UTF-16 code units, so 600 non-BMP
  characters measured as 1200 and a perfectly valid description was
  reported `malformed-description` — a conforming server failed by an
  off-by-encoding, which is the direction this module works hardest to
  avoid. The Agent Skills reference validator uses Python `len()`.
- #2248 narrowed: the `skills/list` half is settled here; what remains is
  whether `skills/get` carries the same attributes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-05 02:01:18 -04:00
cliffhall
1cb05ca573 fix: address Copilot review round 4 on #2251
- SkillsScreen: actually call `skills/get`. The client method and its
  tests existed but no production caller invoked it, so #2234's acceptance
  criterion ("skills/get retrieves a single entry") was unmet and a server
  author's required handler could not be exercised. The detail pane now
  fetches the selected URI on demand and reports whether the fetched entry
  AGREES with the one skills/list advertised — both describe the same
  skill, so a disagreement is a server bug only a side-by-side fetch shows.
- core/mcp/sha256.ts: a dependency-free SHA-256, used when `crypto.subtle`
  is absent. `SubtleCrypto` needs a secure context, and this app is
  documented as servable over plain HTTP on a LAN IP — where every skill
  verification threw and the UI reported a read failure for files it had
  fetched fine. Checked against the FIPS 180-4 vectors and differentially
  against WebCrypto.
- skills.ts: require the `skill:` scheme in `normalizeSkillUri`. Checking
  only that a URI was hierarchical let `https://demo/SKILL.md` pass the
  name and root checks — a manifest pointing anywhere on the web, reported
  as conforming.
- skills.ts: `missing-digest` and `missing-size` are errors. Both are
  required fields, and an omitted `size` is what lets a server slip past
  the 16 MiB pre-fetch limit while the UI reports zero errors. `warning`
  is now reserved for what is legal yet unverifiable — `"dynamic"`.
- SkillsScreen: key the "Verify all" batch guard to the manifest. A global
  flag left a newly selected skill's button disabled until the previous
  skill's reads settled — forever, if one hung.

Not changed, deliberately: whether a modern-era `skills/list` result MUST
carry the SEP-2549 caching attributes. #2234's analysis records it as open
and the review asserts the opposite; neither reading was checked against
the normative text. Leaving the schema permissive accepts a server that
omits them, while tightening on a wrong reading would reject conforming
responses — the more expensive direction. `skillsSchemas.ts` states this
and #2248 settles it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-04 22:38:03 -04:00
cliffhall
bfeebd440d fix: address Copilot review round 2 on #2251
- SkillsScreen: key verdicts and React elements by manifest ROW INDEX,
  not URI. The checker deliberately tolerates a duplicated URI so it can
  report `duplicate-resource`; a URI key collided those rows into one
  verdict, so verifying either updated both and "Verify all" raced two
  different digest/size declarations into the same slot.
- SkillsScreen: hold the invalidation generation in React state keyed by
  the manifest, not a ref bumped during render. `useValueChange` runs its
  callback during render and requires setState-only purity — an abandoned
  render left the ref incremented and silently discarded a live
  verification.
- SkillsScreen: title a size disagreement "Size mismatch" and render its
  reason. `verifySkillResource` catches it before hashing, so the alert
  was showing "actual undefined" under "Digest mismatch".
- SkillsScreen: pass `contents` to ContentViewer so a base64 SKILL.md
  renders. The text-block form substituted "" and painted a blank preview
  for a file verification had just read correctly.
- skills.ts: add `malformed-size` for a size that is not a non-negative
  safe integer, and exclude such values from the 16 MiB total. A negative
  one could pull the sum back under the limit and hide a violation.
- managedSkillsState: make the overlap guard per-session instead of a
  boolean. A disconnect during an in-flight walk left it set, so the
  reconnect's own load no-oped and was never retried — permanently, if
  the stale request never settled. A stale `finally` can no longer clear
  the live session's guard either.
- skillsSchemas: require the `{ skill }` envelope for `skills/get`. The
  accepted SEP settles it, and normalizing an inline entry would let a
  non-conforming response past the one place that could report it.
- #2248 and the PR description: corrected — they said
  `ReadResourceDirectoryResultSchema` was already declared, which round 1
  removed. #2248 now owns defining it, and records the frontmatter
  cross-check gap too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-04 21:45:49 -04:00
cliffhall
da8cc166ce fix: address Copilot review round 1 on #2251
- skills.ts: report a manifest that omits the skill's own SKILL.md
  (an empty list included), duplicate URIs, entries outside the skill
  root, and a missing size. `Conforms` was reachable for manifests that
  break invariants SEP-2640 states.
- skills.ts: cross-check the declared byte length before hashing. A size
  that disagrees fails verification on its own — the digest is taken over
  the bytes the server served, so agreeing with it says nothing about
  whether the manifest describes them.
- skills.ts: copy the view instead of slicing its backing store in
  `sha256Digest`. `SharedArrayBuffer.prototype.slice()` returns another
  SharedArrayBuffer, which `crypto.subtle.digest` rejects — the cast
  claimed to handle the exact input that would have thrown. No cast now.
- skills.ts: state the one obligation NOT checked here — that an entry's
  frontmatter matches the fetched SKILL.md's. The digest cannot cover it,
  and closing it needs a YAML parser, so it is tracked on #2248.
- managedSkillsState: cap the walk at LIST_MAX_PAGES. The repeated-cursor
  guard only catches a server stuck on one cursor; endlessly unique ones
  walked forever. Raises rather than truncating, like the salvage walk.
- managedSkillsState: gate every write on a session generation, so a walk
  that resolves after a disconnect or destroy cannot repopulate a cleared
  store or deliver the previous session's skills into the next.
- inspectorClient: send a cursor when it is `!== undefined`, not when it
  is truthy. An opaque cursor may be `""`, and dropping it re-requested
  page one — which the store then reported as a repeated-cursor failure.
- SkillsScreen: key verdict invalidation on the manifest (URI + digests +
  sizes), not the URI alone. A Refresh that changed the manifest left a
  green badge attached to a digest nothing had checked.
- SkillsScreen: epoch-guard every read continuation, so a fetch that
  resolves after the selection moved on cannot write into the new one.
- SkillsScreen: bound "Verify all" to 4 concurrent reads. A conforming
  manifest may hold 512 files.
- skillsSchemas: drop the guessed `resources/directory/read` result
  schema. Nothing calls it, so an unverified shape could sit wrong
  indefinitely; phase 3 adds it against the normative text.
- skills-http.json: declare the extension bare. It advertised
  `directoryRead: true` with no handler, so Connection Info reported
  "Supported" for a method that answers -32601.
- types.ts: restore the Tasks doc comment the Skills interface displaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-04 21:23:58 -04:00
cliffhall
63623b754b feat: support the Skills extension (SEP-2640) with digest verification
Adds detection, enumeration, and conformance checking for the Skills
extension (`io.modelcontextprotocol/skills`) — phases 1 and 2 of #2234,
which is everything its Acceptance list names.

Skills is a *server*-declared extension, read off the connecting server's
`capabilities.extensions`, so it is deliberately absent from
`ADVERTISABLE_EXTENSIONS` — that registry is what the Inspector advertises
and the user toggles, and an entry there would be a meaningless toggle.

`skills/list` and `skills/get` are consumer-owned extension methods that
neither era codec defines, so the SDK's era gate skips them and they go out
as ordinary `client.request` calls with explicit result schemas. The
raw-wire channel modern `tasks/*` needs is not used, and the Skills tab is
not era-gated: a legacy-era server that declares the extension is serving it.

The checks are the point, not the list view. `core/mcp/skills.ts` reports
each obligation SEP-2640 states — the name/path invariant, the digest
format, the 512-entry and 16 MiB limits, and `resources: "dynamic"`, which
means integrity cannot be verified at all. Digest verification hashes the
fetched bytes with WebCrypto and returns a mismatch with both digests
attached rather than throwing, because showing a mismatch loudly is the
whole value proposition.

Files are fetched on demand: SEP-2640 is explicit that a `resources/read`
of a `SKILL.md` is not a load and confers no standing, so none of the SEP's
host machinery is implemented.

The `skills-http` fixture serves four skills over two pages, three of them
deliberately non-conforming — without those the verification code is
untestable.

Phase 3 (CLI, TUI, `resources/directory/read`, paged mode) is #2248.

Closes #2234

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY
Signed-off-by: cliffhall <cliff@futurescale.com>
2026-09-04 20:45:15 -04:00