Skip to content

Support the Skills extension (SEP-2640) #1391

Open

Description

@DaleSeo

Summary

Add support for the Skills extension (SEP-2640, io.modelcontextprotocol/skills) to rmcp. The extension adds three methods: skills/list, skills/get, and the optional resources/directory/read. This issue covers both the server side and the client side.

Requirements

A server built with rmcp can:

  • Declare the extension in capabilities.extensions, with directoryRead only when it serves resources/directory/read.
  • Answer skills/list and skills/get with typed results, and resources/directory/read when declared.
  • Serve the files of a skill through resources/read, so that the bytes match the size and digest in the manifest.
  • Return the error codes that the spec defines: -32601 for a method it does not serve, -32602 for an unknown skill or a non-directory URI.
  • Send ttlMs and cacheScope to peers on protocol version 2026-07-28.

A client built with rmcp can:

  • Detect the extension and its directoryRead setting from the server capabilities.
  • Call the three methods through typed Peer methods, and receive server errors unchanged.
  • Verify a fetched file against the size and digest in the manifest.

Both sides pass the sep-2640-* conformance scenarios.

The spec targets protocol revision 2026-07-28 or later. rmcp also serves the methods to peers on an older revision and removes the cache hints from those responses, the same as the TypeScript, Python, and Go SDKs do. This needs the legacy strip helper to clear ttlMs and cacheScope as well as resultType, which also closes the same gap for the existing list results.

Proposal

This is how I plan to design and implement it. The choices below are open for discussion.

PR 1: protocol surface

Everything the spec defines, wired the same way as the tasks extension and without a feature flag.

  • Model types for the skill manifest and the three methods in model/skill.rs, with the matching ClientRequest and ServerResult variants. ReadResourceDirectoryResult is a type alias for ListResourcesResult, and ServerResult reuses the existing ListResourcesResult variant for it. No new directory variant: the two schemas are identical and the untagged ServerResult cannot tell them apart (see fix: prevent CallToolResult from shadowing CustomResult in untagged enum deserialization #771).
  • Three ServerHandler methods with capability-gated dispatch. Dispatch checks what the result object alone can show: the skill URI matches the request, the name matches the last URI segment, the manifest contains SKILL.md and no duplicate URIs, and every digest has the sha256: form. A result that fails these checks returns -32603. Whether the bytes behind each URI match the manifest is checked by the catalog (PR 2) and by the host that reads them, not by dispatch.
  • enable_skills() and enable_skills_with_directory_read() on the capability builder, with the matching supports_* getters. Whether the builder rejects skills without resources or only documents it is open.
  • The matching Peer methods for the client. They accept a 2026-07-28 response that omits the cache hints, the same as list_tools does today; the TypeScript and Go clients reject such a response, rmcp does not. SkillResource::verify(&[u8]) checks the size and the sha256: digest of one file. Frontmatter verification against skills/get is left to the host. sha2 becomes a regular dependency of rmcp: it is pure Rust, already in the tree for request-state, and a client must not have to enable a filesystem feature to verify a download.
  • Integration tests through Peer over a real transport: pagination across skills/list, a -32602 from skills/get arriving unchanged, read_resource_directory not sent when directoryRead is absent, the directory result arriving as ListResourcesResult, and verify failing on a wrong size and on a wrong digest.
  • Conformance fixtures with a nested skill, a subdirectory, and a "dynamic" skill, and the scenarios in conformance.yml.

PR 2: filesystem catalog

Behind a skills-fs feature. SkillCatalog::from_dir walks a directory tree and builds the manifests.

  • The catalog is a snapshot. It holds the file bytes and their digests, so resources/read serves the same bytes that the manifest describes, even if the files change on disk later.
  • reload() builds a new snapshot and swaps it in. If the rebuild fails, the old snapshot stays.
  • The walk rejects paths that leave the root, including through symlinks, and validates that each skill name matches its directory name. It warns at 512 files or 16 MiB.
  • A nested skill appears in the listing on its own, and its files also appear in the manifest of the parent skill.

Follow-ups, not blocking

  • PR 1 adds the two new results to the hand-written lists in strip_result_type_for_legacy_peer and fill_missing_cache_hints. A non-breaking refactor right after PR 1 replaces both lists with a trait on each cacheable result and a dispatch that ts_union! generates, since the macro already enumerates every ServerResult variant.
  • A ResultEnvelope { result_type, meta, ttl_ms, cache_scope } flattened into each cacheable result, with the result structs marked #[non_exhaustive], so that the next mixin field from the spec is not a major release. Breaking, so it waits for the next major batch.
  • An Extension trait for declaring and reading extension capabilities, so that each extension adds one impl instead of four functions. Skills and tasks move onto it. Non-breaking.

Acceptance

  • The sep-2640-* conformance checks pass on the fixture server. The PR body lists the numbers.
  • Goose can list, fetch, and verify remote skills through Peer without custom requests.
  • For PR 2: a file served through resources/read always matches the manifest, a path outside the root is never served, and the files of a nested skill are listed in both manifests.

Activity

  1. added
    T-enhancementNew features and enhancements
    T-handlerHandler implementation changes
    T-modelModel/data structure changes
    T-serviceService layer changes
    enhancementNew feature or request
    on Oct 23, 2026
  2. added
    P2Medium: important but non-blocking improvement
    ready for workIssue is well-defined and ready to be picked up
    on Oct 23, 2026
  3. github-actions commented on Oct 23, 2026

    @github-actions
    Contributor

    @DaleSeo (assignment failed) is on triage rotation this week and is the reviewer for this one.

    Codex labeled this enhancement / P2 (T-model, T-handler). Please confirm or correct those labels.

    Automated triage. Labels are a first pass and the assigned maintainer has the final say.

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

Metadata

Metadata

Assignees

Labels

P2Medium: important but non-blocking improvementT-enhancementNew features and enhancementsT-handlerHandler implementation changesT-modelModel/data structure changesT-serviceService layer changesenhancementNew feature or requestready for workIssue is well-defined and ready to be picked up

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions