Repository navigation
Conversation
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.
aba85c0 to
15ed73a
Compare
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.
15ed73a to
2585f28
Compare
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
left a comment
There was a problem hiding this comment.
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 theresourcescapability, so compliant clients won't call any skill methods. resourcestreated as optional: entries can omit it, and the demo leaves outSKILL.mdand subdirectory files. The spec requires a complete list.- Wrong directory-read wire format: it returns
childrenwithisDirectoryinstead ofresourceswithmimeType: "inode/directory". - Directory read misbehaves:
Routeralways returns an empty list, the demo lists the parent folder, and missing paths don't return-32602. skills/gettoo loose: it accepts any file URI inside a skill, returns-32601instead of-32602when nothing matches, and the macro version drops the frontmatter and resources.- URI parsing bug:
parse_skill_urigets 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.
| @@ -1,4 +1,5 @@ | |||
| #![doc = include_str!("../README.md")] | |||
| #![allow(dead_code)] | |||
There was a problem hiding this comment.
Why are we adding this?
There was a problem hiding this comment.
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
| ) | ||
| .await | ||
| .map(ClientResult::CustomResult), | ||
| ServerRequest::SkillsListRequest(request) => self |
There was a problem hiding this comment.
This is the wrong direction no? ServerRequest holds requests the server makes to the client.
There was a problem hiding this comment.
I misunderstood this and mentally had it the opposite! noted, and adjusted in my upcoming commit
client logic ended up in the wrong union
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
884623b to
301eac9
Compare
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.
745c669 to
73408a2
Compare
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. |
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, andresources/directory/readmethods, plus the
#[skill]macro trio that lets server authors declare skillendpoints the same way they declare tool endpoints.
How Has This Been Tested?
cargo check --all-featurespasses clean. 11 conformance tests pass inconformance/.FileSystemSkillServerdemo server runs end-to-end withfixtures 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 macrogenerates code at compile time, and the generated types satisfy the trait bounds
through transparent expansion, not a newtype wrapper.
Types of changes
Checklist