Skip to content

Support the Skills extension (SEP-2640): skills/list, skills/get, and digest verification #2234

Description

@cliffhall

Add Inspector support for the Skills extension, SEP-2640 (PR #2640) — servers serving Agent Skills (a SKILL.md plus its supporting files) over MCP. Design history and experimental findings remain in the Skills Over MCP WG incubation repo.

Status: Accepted (Extensions Track; extension identifier io.modelcontextprotocol/skills). Filed against the draft, now unblocked — the normative text is settled and the wire shapes below are the accepted ones. The Inspector is where server authors validate an implementation, and there is currently no way to exercise a skills server other than hand-rolled JSON-RPC; the same argument that carried MCP Apps (#1633) and Tasks (#1631).

What the extension is

Server-declared in the initialize response:

"capabilities": {
  "extensions": {
    "io.modelcontextprotocol/skills": { "directoryRead": true }
  }
}
Method Required? Purpose
skills/list Enumerate skills; cursor / nextCursor pagination
skills/get One skill entry by uri
resources/directory/read Gated on directoryRead: true Direct (non-recursive) children of a directory resource; mimeType: "inode/directory" marks a directory

A skill entry:

{
  "uri": "skill://<skill-path>/SKILL.md",
  "frontmatter": { "name": "skill-name", "description": "" },
  "resources": [{ "uri": "skill://<skill-path>/ref.md", "digest": "sha256:<64 hex>", "size": 12345 }]
}

frontmatter is the verbatim YAML frontmatter as JSON. resources is either the full file manifest or the literal string "dynamic" (generated content, no integrity verification possible). Skill files are fetched on demand through ordinary resources/read, not pre-fetched, and relative references resolve against the skill root.

Constraints worth checking against: the final <skill-path> segment — the segment before /SKILL.md, not the filename — must equal frontmatter.name, so the name is recoverable from the URI alone; the entry's frontmatter must match the fetched SKILL.md's frontmatter field-by-field; digests are sha256: + 64 lowercase hex; per-skill limits are 512 resource entries and 16 MiB total.

No SDK change is needed — confirmed against @modelcontextprotocol/client 2.0.0

Extensions are meant to be usable without SDK work, and SDK v2 encodes that explicitly. Verified by reading the installed client bundle:

  • The era gate skips non-spec methods. Protocol._assertOutboundRequestInEra (client/dist/src-D_zzAWoS.mjs:6021-6033) fires only when isSpecRequestMethod(method) is true, and that predicate (:4056) is "does either era codec define it". skills/list, skills/get and resources/directory/read are in neither codec, so they are era-blind — the SDK's own comment says so: "Methods outside the spec universe are consumer-owned extension methods and stay era-blind." They go out unchanged on both 2025- and 2026-era connections.
  • No capability assertion blocks them. Client.assertCapabilityForMethod (client/dist/index.mjs:3440) is a switch over spec methods that falls through to a no-op for anything else, so the SDK will not refuse to send a method it has never heard of.
  • The only requirement is an explicit result schema. Protocol.request (:5978) throws TypeError: '<method>' is not a spec method; pass a result schema as the second argument to request() when one is omitted. So the entire client-side mechanism is client.request({ method: "skills/list", params: {} }, SkillsListResultSchema, this.getRequestOptions()).

Two consequences for the plan below:

  • ⚠️ Do not reach for rawWireRequest. That escape hatch (core/mcp/inspectorClient.ts:2786) exists for the opposite problem — modern tasks/*, which are spec method names the 2026 codec deleted, so the era gate kills them before they reach the transport. Skills has no such collision, so ordinary client.request is correct and this is materially simpler than what Tasks needed. Using the raw path anyway would bypass the SDK's response correlation for no reason.
  • The test-server side is unblocked too. Protocol.setRequestHandler (:6207) rejects a non-spec method only when schemas are omitted; passing explicit request/result schemas lets @modelcontextprotocol/server serve skills/list and skills/get directly, so the fixtures below need nothing from upstream either.

So this issue depends on no upstream release, and an eventual @modelcontextprotocol/ext-skills (or SDK convenience wrappers — the SEP's Implementation Guidelines recommend list_skills / get_skill / read_skill_uri / read_directory, but they are non-normative) would be a later simplification, not a prerequisite.

Why this is more than a viewer

The SEP puts real obligations on the host, and every one of them is a conformance check the Inspector is well placed to run and report — this is the same angle as --strict tool-schema linting (clients/cli/src/handlers/schema-lint-report.ts) rather than a passive list view:

  • Verify each fetched file against its digest and treat a mismatch as a failure. An inspector that shows a digest mismatch loudly is the whole value proposition.
  • Flag resources: "dynamic" — it means integrity cannot be verified at all. That belongs on screen as a warning, not buried.
  • Check basename(uri) === frontmatter.name, the one structural invariant the spec states outright.
  • Report a skill that exceeds 512 entries or 16 MiB, since hosts must support up to those limits and servers must not exceed them.
  • Content-bound approval — approval is bound to the resources manifest observed at approval time and revoked when that set changes. Worth surfacing as a visible diff, since it is the part server authors are most likely to get wrong.

Integration points

Grounded in what is already here; nothing below needs a new dependency.

No SDK package exists. @modelcontextprotocol/ext-skills is a 404 on npm and the incubation repo carries no reference implementation, so the schemas are hand-rolled with Zod (already a root dependency) — the same shape as core/mcp/modernTaskSchemas.ts. Per Dependency placement this adds nothing to any manifest.

⚠️ This is a server-declared extension, so it does not belong in ADVERTISABLE_EXTENSIONS. That registry in core/mcp/extensions.ts is the catalog of extensions the Inspector advertises and the user toggles in Server Settings. Skills is read off the server's capabilities instead — the existing precedent is core/mcp/appElicitation.ts, which reads capabilities?.extensions?.[UI_EXTENSION_KEY] from the server side. Getting this backwards would put a meaningless toggle in Server Settings.

Area Work
core/mcp/ skills.ts (detection off server capabilities, digest verification, the conformance checks above) + skillsSchemas.ts (Zod). SKILLS_EXTENSION_KEY = "io.modelcontextprotocol/skills" alongside the existing key constants in extensions.ts.
core/mcp/state/ managedSkillsState.ts following managedPromptsState.ts / managedListState.ts — cursor pagination is in the SEP, so it fits the existing paged-list machinery.
core/react/ useManagedSkills.ts + usePagedSkills.ts, built on useStoreSnapshot per the store-subscription rules in AGENTS.md (never useState + a subscribing effect).
clients/web A Skills tab: add to INSPECTOR_TAB_IDS in src/utils/inspectorTabs.ts, SkillsScreen under src/components/screens/, modeled on AppsScreen. Shows frontmatter, the resource manifest with digest + size, verification status per file, and renders a fetched SKILL.md.
clients/cli skills/list / skills/get through handlers/method-types.ts + handlers/run-method.ts; a --verify mode reporting digest and conformance results is the natural CLI equivalent of --app-info.
clients/tui A Skills pane, once the web shape settles.
test-servers/ A composable fixture + configs/skills-http.json serving several skills — deliberately including one with resources: "dynamic", one with a deliberate digest mismatch, and one violating the name/path-segment rule, so every check above is exercisable. Without the bad cases the verification code is untestable.
Connection Info Show skills support and the directoryRead sub-flag, same as the existing extension rows.

Suggested phasing

Splittable into sub-issues when picked up; #1633 is the precedent for phasing extension work.

  1. Detect + list — capability detection, Zod schemas, skills/list with pagination, a read-only web tab. Cheap, and enough for a server author to see their skills at all.
  2. Fetch + verifyskills/get, resources/read of skill files, digest verification, the conformance checks, and the test-server fixtures that exercise the failure paths.
  3. Reach — CLI methods, TUI pane, resources/directory/read when the server declares directoryRead.

Risk

The spec-churn risk that motivated this section is largely retired: SEP-2640 is Accepted, so method names and the entry shape are settled. Keep the wire types isolated in core/mcp/skillsSchemas.ts anyway — the SEP leaves one point open (whether a skills/get result should also carry the SEP-2549 caching attributes ttlMs / cacheScope, as resources/read results do), and the skill format itself is delegated wholesale to the Agent Skills specification, which versions independently and whose future revisions apply automatically.

The remaining risk is scope, not churn: the SEP places heavy obligations on a host — activation, per-skill consent, content-bound approval, the window during which a host is "acting on" a skill. The Inspector is an inspector, not a host, and the SEP is explicit that a resources/read of a SKILL.md is not a load and confers no standing. Surface and verify; do not implement the activation and approval machinery.

Acceptance

  • The Inspector detects io.modelcontextprotocol/skills on a connecting server and surfaces it in Connection Info, including directoryRead.
  • skills/list is paginated through and rendered in a web Skills tab; skills/get retrieves a single entry.
  • Skill files are fetched via resources/read on demand and verified against their digests, with a mismatch reported as a visible failure rather than swallowed.
  • resources: "dynamic", a name/path-segment mismatch, and a limits violation each produce a clear, distinguishable warning.
  • Test-server fixtures cover the clean case and the dynamic, digest-mismatch and bad-name cases.
  • Wire types live in one module so a spec change is a single-file edit.
  • Per-file coverage clears the ≥90 gate on all four dimensions.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature requestv2Issues and PRs for v2

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions