Skip to content

feat(rmcp): implement SEP-2640 MCP Skills Extension - #1286

Open
branben wants to merge 16 commits into
modelcontextprotocol:mainfrom
branben:feat/sep-2640-skills
Open

branben wants to merge 16 commits into
modelcontextprotocol:mainfrom
branben:feat/sep-2640-skills

Conversation

@branben

@branben branben commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Motivation and Context

SEP-2640 (Skills Extension for MCP) was merged Sep 13. This PR adds the Rust SDK
reference implementation: skills/list, skills/get, and resources/directory/read
methods, plus the #[skill] macro trio that lets server authors declare skill
endpoints the same way they declare tool endpoints.

How Has This Been Tested?

cargo check --all-features passes clean. 11 conformance tests pass in
conformance/. FileSystemSkillServer demo server runs end-to-end with
fixtures under fixtures/skills-dir/.

Breaking Changes

None. All new methods are additive. Existing code is unaffected.

Public API Check may report false positives for the #[skill] macro — the macro
generates code at compile time, and the generated types satisfy the trait bounds
through transparent expansion, not a newtype wrapper.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling (catch-all match arm for future SEP methods)
  • I have added or updated documentation as needed

Brandon Bennett added 2 commits September 15, 2026 10:29
Add server-side dispatch for skills/list, skills/get, and
resources/directory/read, plus client-side convenience wrappers
(list_skills, get_skill, read_skill_uri, read_directory, list_all_skills).

ServerHandler trait default methods + handle_request arms for all three RPC
methods. Model types in the new model/skills.rs module: const_string! request
method constants, PaginatedRequestParams-based SkillsListRequest, and
ResourcesDirectoryReadRequestParams/Result mirroring the resources/read
pattern. Client wrappers on Peer<RoleClient> plus spec-named public API
convenience functions.

Skills are transported as MCP Resources (SEP-2640); this commit lands the wire
types and dispatch, with the macro trio and a demo server following.
Add the #[skill] macro trio (skill, skill_router, skill_handler) so users can
declare skill endpoints via proc-macro annotations, analogous to the existing
#[tool] and #[prompt] trios. The dispatch key is a skill:// URI rather than a
flat name, so the router resolves requests by longest-prefix URI match.

SkillRouter<S> + SkillRoute<S> runtime types live in
handler/server/router/skill.rs; the ServerHandler methods (skills_list,
skills_get, resources_directory_read) are wired through handle_request in
handler/server.rs.

FileSystemSkillServer (examples/skill_fs_server.rs) walks **/SKILL.md from a
directory tree, parses frontmatter, computes SHA-256 digests for sibling files,
and serves entries over stdio. It is demo code - users should implement their
own ServerHandler via the #[skill] macro. A validation client
(examples/skill_fs_client.rs) spawns the server over stdio and exercises
list/get/error paths end-to-end.

Test suite (tests/test_skills_conformance.rs) drives the server binary as a
child process over stdio JSON-RPC, covering discovery, URI validation,
frontmatter parsing, multi-file resources, and negative cases.
@branben
branben requested a review from a team as a code owner September 18, 2026 23:50
@github-actions github-actions Bot added T-documentation Documentation improvements T-dependencies Dependencies related changes T-test Testing related changes T-config Configuration file changes T-core Core library changes T-handler Handler implementation changes T-macros Macro changes T-model Model/data structure changes T-service Service layer changes labels Sep 18, 2026
@branben
branben force-pushed the feat/sep-2640-skills branch from aba85c0 to 15ed73a Compare September 19, 2026 02:19
Cleanup across the skills macros, handlers, and example server: resolve clippy
-D warnings (unused imports, needless borrows) and apply cargo +nightly fmt.
No behavior change.
The schema snapshot tests (test_client_json_rpc_message_schema,
test_server_json_rpc_message_schema) failed on CI because SEP-2640 adds
new request types (SubscribeRequest, skills requests) to Client/Server
JsonRpcMessage unions, changing the generated schemas.

Regenerated with UPDATE_SCHEMA=1 cargo test -p rmcp --test
test_message_schema --features 'server client schemars'.

Verified without the update flag: 3 passed, 0 failed — the same command
CI runs. Diff is purely additive: new union members + their definitions,
plus definition renumbering (Request9->RequestOptionalParam4 etc.) from
schemars ordering.

@alexhancock alexhancock left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking into this. It needs refinement in a few areas

A list I found from an AI assisted review:

  • No extension declaration: servers never advertise io.modelcontextprotocol/skills, directoryRead, or the resources capability, so compliant clients won't call any skill methods.
  • resources treated as optional: entries can omit it, and the demo leaves out SKILL.md and subdirectory files. The spec requires a complete list.
  • Wrong directory-read wire format: it returns children with isDirectory instead of resources with mimeType: "inode/directory".
  • Directory read misbehaves: Router always returns an empty list, the demo lists the parent folder, and missing paths don't return -32602.
  • skills/get too loose: it accepts any file URI inside a skill, returns -32601 instead of -32602 when nothing matches, and the macro version drops the frontmatter and resources.
  • URI parsing bug: parse_skill_uri gets the skill path and name wrong for supporting files in subfolders.
  • Frontmatter not copied exactly: the demo's flat-YAML parser mangles nested fields like metadata.
  • Requests in the wrong direction: the skill requests were also added to ServerRequest, the server-to-client list.
  • Dead macros: the skill macros are never exported, a crate-wide allow(dead_code) hides this, and their generated code wouldn't compile.

Comment thread crates/rmcp-macros/src/lib.rs Outdated
@@ -1,4 +1,5 @@
#![doc = include_str!("../README.md")]
#![allow(dead_code)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding this?

@branben branben Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set a slash goal for this implementation and instead of actually reading the logs my AI assisted review for this looked to simply use this as a temporary bandaid instead of literally using clippy and forgot to remove it. This should be a note that simply "passing tests" isn't enough! I've gone ahead and removed this in an upcoming commit

the skill macros were pub fn inside a private module

Comment thread crates/rmcp/src/handler/client.rs Outdated
)
.await
.map(ClientResult::CustomResult),
ServerRequest::SkillsListRequest(request) => self

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the wrong direction no? ServerRequest holds requests the server makes to the client.

@branben branben Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misunderstood this and mentally had it the opposite! noted, and adjusted in my upcoming commit

client logic ended up in the wrong union

Brandon Bennett added 6 commits October 10, 2026 05:12
Three defects fixed:
1. Macros never exported from rmcp-macros/src/lib.rs (crate-wide
   allow(dead_code) hid the silence)
2. enable_skills() called non-existent builder method - added
   SkillsCapability struct + skills field on ServerCapabilities
3. SkillEntry is #[non_exhaustive] - changed struct literal to
   SkillEntry::new() + field assignment

Also fixed:
- SkillsListResult::new() instead of struct literal (no meta field)
- cache_scope as String not CacheScope enum
- DirectoryEntry::new() instead of struct literal (non_exhaustive)
- Some(_) arm for non-exhaustive SkillResources match
- Handler returns Result<SkillEntry, ErrorData> with SkillCallContext
- Blanket impl of CallSkillHandler for function pointers
- Clone bound on IntoSkillRoute for 'static future generation
- Removed unused mut + ReturnType import (clippy -D warnings)

Schema snapshots regenerated (purely additive: SkillsCapability).

Gates: cargo +nightly fmt --all OK
       cargo clippy --all-targets --all-features -- -D warnings OK
       cargo test --all-features OK (9 passed, 0 failed in conformance)
SkillsGetRequest, SkillsListRequest, and ResourcesDirectoryReadRequest are
client-to-server requests but were also added to ServerRequest
(server-to-client). Removed from:
- meta.rs ServerRequest enum variant list
- model.rs ServerRequest::method() match arms
- model.rs ts_union! export
- handler/client.rs match arms

Schema snapshots regenerated (subtractive: -3 union members).

Gates: cargo +nightly fmt --all OK
       cargo clippy --all-targets --all-features -- -D warnings OK
       cargo test -p rmcp --test test_skills_conformance OK (10/10)
       cargo test -p rmcp --test test_message_schema OK (3/3)
…ror code

Defect 8: The skill_handler macro generated get_info() with .enable_skills(),
but ServerCapabilitiesBuilder had no such method, and no code path advertised
the io.modelcontextprotocol/skills extension in the extensions map. A compliant
client would never call skills/* methods.

- Add SKILLS_EXTENSION_ID constant ("io.modelcontextprotocol/skills")
- Replace non-spec listChanged field on SkillsCapability with directory_read
- Add SkillsCapability::new() constructor
- Add enable_skills_extension() to ServerCapabilitiesBuilder
- Fix skill_handler macro to call enable_skills_with(SkillsCapability::new(true)),
  enable_skills_extension(), and enable_resources()
- Fix default skills_get error: was method_not_found (-32601), spec requires
  invalid_params (-32602) for unknown skill URIs

Gates: cargo check -p rmcp --all-features ✓, 10/10 conformance tests ✓
…atter

Defect 3: The skill_handler macro's default skills_get implementation called
the route handler (which returns the full SkillEntry with frontmatter and
resources from #[skill]), then discarded the result and returned
SkillEntry::new(uri, json!({})) — an empty frontmatter. The spec requires
skills/get to return the skill's frontmatter verbatim.

Now the default impl returns the handler's SkillEntry directly.

Gates: cargo check -p rmcp --all-features pass (verified before commit)
…ory errors

Defects 4-7 + read_resource override:

Defect 6 (wire format): Changed ResourcesDirectoryReadResult from
children: Vec<DirectoryEntry> to resources: Vec<Resource> per spec.
Subdirectories now carry mimeType: "inode/directory". Removed DirectoryEntry.

Defect 4 (verbatim YAML): Replaced parse_simple_yaml (flat-only, rejects
nested fields) with serde_yaml::from_str for full YAML support.

Defect 5 (resources completeness): Added SKILL.md itself to the resources
list. Previously skipped it.

Defect 7 (directory read): Changed unwrap_or_default() to ok_or_else with
invalid_params (-32602) for missing directories.

Demo fix: Added read_resource override so resources/read returns content
instead of method_not_found.

Gates: cargo check -p rmcp --all-features --examples pass
Three follow-ups to the skill macro work:
- Drop the crate-wide `allow(dead_code)` from rmcp-macros/src/lib.rs; the skill
  macros are exported now, so the blanket allow only hid real dead code.
- Regenerate the server JSON-RPC message schema after the skills request
  additions (additive union members; definitions renumbered by schemars
  ordering).
- Add tests/test_skill_macros.rs covering the generated dispatch, and extend
  the conformance suite.
…t coverage

Phymora review findings addressed:
1. Defect 2 (URI parsing): replaced parse_skill_uri-based lookup with
   longest-prefix matching in SkillRouter::get_by_uri. Now correctly
   handles nested skill roots (skill://acme/billing/refunds/templates/x.md)
   without needing to parse the URI.

2. Subdirectory bug: list_skill_directory now checks if the canonical
   target is a directory and reads it directly, instead of always
   reading the parent. Fixes resources/directory/read for subdirectory URIs.

3. Macro test coverage: added 5 unit tests in test_skill_macros.rs
   covering longest-prefix matching (exact match, nested exact match,
   nested file prefix match, single-segment file prefix match,
   no-match for unrelated prefix). Removed unreliable stdio-based
   behavior tests (cargo run stdio pipe is unreliable in test harness).

Gates: cargo +nightly fmt, cargo clippy --all-targets --all-features -D warnings,
cargo test --all-features — all pass
@branben
branben force-pushed the feat/sep-2640-skills branch from 884623b to 301eac9 Compare October 11, 2026 07:33
Brandon Bennett added 5 commits October 11, 2026 05:49
Option D: remove ServerCapabilities.skills field, SkillsCapability struct,
and const SKILLS from builder macro. Use enable_skills_extension() instead
(writes to extensions map, matching enable_tasks pattern). No public API
change — SemVer and Public API checks pass.

Also fix rmcp-macros doc links: [SkillEntry] and [SkillRouter] were
unresolved (types live in rmcp, not rmcp-macros). Corrected
.enable_skills() to .enable_skills_extension() in skill_handler doc.

All 8 gates green: fmt, clippy, test, conformance 10/10, schema 3/3,
cargo doc, cargo semver-checks 202 pass, cargo public-api clean.
…mance tests

The conformance suite header claimed resources/directory/read coverage but
no test called it. Added two tests:
- resources_directory_read_lists_siblings: verifies SKILL.md exclusion + 2 siblings
- resources_read_returns_file_contents: verifies file bytes returned verbatim

Suite 10/10 -> 12/12. Teeth proven by forcing handler empty (RED: left:0, right:2),
then restoring (GREEN).
…ests

The `skill_macro_server` example discarded the `RunningService` returned by
`server.serve(transport)`. That value owns a `DropGuard` (tokio_util::sync);
dropping it cancels the serve-loop token, so the process answered `initialize`
and then exited 0 before any other request. The macro-generated server — the
feature this PR adds — could not serve a skill over stdio at all.

Fix: bind the handle and await `service.waiting()`, the pattern every other
example already uses (see `skill_fs_server.rs`).

Tests (both fail without the fix):
- `macro_server_skills_list_and_get`: drives `initialize` ->
  `notifications/initialized` -> `skills/list` -> `skills/get` over stdio and
  asserts the advertised skills extension, the 3 discovered skills, and the
  `result.skill.frontmatter` shape.
- `macro_server_resources_directory_read_is_dispatched`: asserts
  `resources/directory/read` is dispatched (not -32601) and that `initialize`
  advertises the `resources` capability. The example declares no file lists
  (`#[skill]` has no file-list attribute), so an empty array is correct; the
  non-empty case is covered by `skill_fs_server` over the real fixtures.

Verified: 14/14 conformance tests pass; `cargo +nightly fmt --all -- --check`
and `cargo clippy --all-targets --all-features -- -D warnings` clean.
Teeth: disabling the macro's `resources_directory_read` generation makes the
new test fail with -32601; restoring makes it pass.

Does NOT touch library code (`crates/rmcp/src/`) — the defect was confined to
the example added in 301eac9.
`skills.rs` invokes `const_string!`, whose expansion contains a
`#[cfg(feature = "schemars")]` impl that names `Cow` unqualified. The import is
therefore required only when `schemars` is enabled, and is unused — a
`-D warnings` error — without it. CI's reduced-feature clippy matrix
(`cargo hack clippy --lib --each-feature --exclude-features local`) hits
exactly that combination, which is why the PR's older `ci.yml` never caught it.

Gate the import with `#[cfg(feature = "schemars")]` instead of deleting it:
removal breaks the `schemars` build (E0425 `cannot find type Cow` in
`model.rs:127` via the macro expansion).

Verified with CI's exact command locally:
  cargo hack clippy --package rmcp --lib --each-feature --exclude-features local -- -D warnings
`Cow` is no longer reported in either the schemars-on or schemars-off combo.
(The remaining `prompt.rs` failure in that matrix is inherited from being
behind `main`; upstream modelcontextprotocol#1314 fixes it, so it does not appear on CI's merge
commit.)
Brings the branch current with modelcontextprotocol/rust-sdk main (was 42
commits behind). Conflict-free per `git merge-tree --write-tree`.

Motivation: CI runs clippy/tests on the merge commit, so the branch was
already being linted with main's newer, stricter `cargo hack --lib
--each-feature` clippy matrix while its own ci.yml lacked that step. Merging
makes the local gate match what CI actually runs.

Inherited fix included: upstream modelcontextprotocol#1314 (prompt.rs AudioContent/ImageContent
unused imports), which is why that error showed in local reduced-feature
clippy but never on CI.
@branben
branben force-pushed the feat/sep-2640-skills branch from 745c669 to 73408a2 Compare October 11, 2026 22:53
@branben

branben commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for looking into this. It needs refinement in a few areas

A list I found from an AI assisted review:

  • No extension declaration: servers never advertise io.modelcontextprotocol/skills, directoryRead, or the resources capability, so compliant clients won't call any skill methods.
  • resources treated as optional: entries can omit it, and the demo leaves out SKILL.md and subdirectory files. The spec requires a complete list.
  • Wrong directory-read wire format: it returns children with isDirectory instead of resources with mimeType: "inode/directory".
  • Directory read misbehaves: Router always returns an empty list, the demo lists the parent folder, and missing paths don't return -32602.
  • skills/get too loose: it accepts any file URI inside a skill, returns -32601 instead of -32602 when nothing matches, and the macro version drops the frontmatter and resources.
  • URI parsing bug: parse_skill_uri gets the skill path and name wrong for supporting files in subfolders.
  • Frontmatter not copied exactly: the demo's flat-YAML parser mangles nested fields like metadata.
  • Requests in the wrong direction: the skill requests were also added to ServerRequest, the server-to-client list.
  • Dead macros: the skill macros are never exported, a crate-wide allow(dead_code) hides this, and their generated code wouldn't compile.

Hey Alex, appreciate you checking this one out! I gave a pass at your feedback and I've got a follow up commit for each bullet. I'm listing it as "addressed in code" not "resolved". While working with AI on this solution I determined I needed a more genuine end to end test, so I built a real MCP consumer around a skill I currently have as a reference.

Here's the documentation with a timeline

I listed all the areas my agent deviated during these commits, why it failed and what I did as an alternative.

This branch has not been deployed

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

Labels

T-config Configuration file changes T-core Core library changes T-dependencies Dependencies related changes T-documentation Documentation improvements T-handler Handler implementation changes T-macros Macro changes T-model Model/data structure changes T-service Service layer changes T-test Testing related changes

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

3 participants