SEP-2640 Skills · rmcp Rust SDK

Implementation notes

A reviewer left 9 comments on PR #1286. This is what each one meant and what we changed.

Before the details

Context summary

The MCP protocol lets an AI assistant talk to a "server" that offers it abilities — tools it can call, files it can read, and now skills: reusable instruction bundles the assistant can look up and follow. This PR adds that skills feature to the Rust SDK people use to build those servers.

It has two halves: the runtime (the code that actually answers skill requests over the wire) and macros — Rust shorthand that writes the boring plumbing for you, so a server author types three lines instead of sixty. The reviewer found problems in both halves.

Server / client
The client is the assistant's app; the server is the thing offering tools and skills. Messages flow both ways, and which way a message goes matters — that's question #2 below.
Macro
A piece of Rust that writes Rust for you. The author writes #[skill]; the macro expands it into full working code at compile time. If a macro is never "exported", nobody can use it — question #1.
Wire format
The exact shape of the data sent over the network. If it doesn't match the spec, compliant clients won't understand the reply.
URI
An address, like skill://acme/billing/refunds/templates/x.md. The tricky part: you can't tell where the "skill name" ends and the "file path" begins just by looking.
The two inline comments, in detail
#1
Alex asked#1

Why is allow(dead_code) being added?

Alex Hancock · inline comment
crates/rmcp-macros/src/lib.rs, line 2
“Why are we adding this?”

Short answer: it was a band-aid that hid a real bug, and we removed it.

Rust normally warns you when code is never used ("dead code"). That warning is useful — it's how you notice something you wrote isn't wired up. allow(dead_code) switches that warning off for the whole file.

It got added back in September in a commit called "chore(rmcp): fix clippy warnings and formatting" — the goal was to make a lint check go green, not to solve a design problem. What it actually did was silence the exact warning that would have caught the reviewer's other finding: the three new skill macros were never exported, so nobody could call them. The warning was pointing at a real bug; the suppression made it invisible.

The fix (commits f51601e and fcaef81) removes the suppression and exports the three macros properly. Once they're exported and used, there's no dead code left to warn about, so the suppression isn't needed.

Added
8c7465c — "chore(rmcp): fix clippy warnings and formatting". One line, to quiet a lint.
What it hid
Zero pub fn skill* exports existed at review time. The three macros were unreachable, and their generated code wouldn't have compiled even if it had been reachable.
Removed
f51601e — suppression deleted; fcaef81 exports the macros, so the underlying warning has nothing left to flag.
#2
Alex asked#2

The skill requests were on the wrong side of the wire

Alex Hancock · inline comment
crates/rmcp/src/handler/client.rs, line 50
“This is the wrong direction no? ServerRequest holds requests the server makes to the client.”

Short answer: the reviewer is right, and we fixed it.

The SDK keeps two lists of message types. One list is "requests the server sends to the client" (for example: "please ask the human a question"). The other is "requests the client sends to the server" (for example: "list your skills").

The three new skill requests — skills/list, skills/get, resources/directory/read — are things the client asks the server. They were mistakenly added to both lists, including the server-to-client one where they don't belong.

That's not just tidiness: the server-to-client list is what a client's message router matches on. A request in the wrong list means the type system would let you send a "list your skills" message as the server, which the spec doesn't allow. The fix (commit 752882d) removes them from the wrong list, leaving them only where they belong.

Wrong list
ServerRequest — requests the server makes to the client.
Right list
ClientRequest — requests the client makes to the server.
Fixed
752882d — removed all three from ServerRequest across 4 files; they now live only in ClientRequest.
The other 7 review points, in order
IMPLEMENTATION_NOTES.md branch feat/sep-2640-skills
10
entries
3
deviations
#3
Fixed · 45f82bf#3

Servers never told clients they support skills

Plain: before a client asks for anything, the server announces what it can do — like a menu. The menu never listed "skills", so a well-behaved client would never ask. Fixed by declaring the io.modelcontextprotocol/skills extension in the capabilities map (not a top-level field — see deviation 3).

#4
Fixed · 07587b3#4

The file list for a skill was incomplete

Plain: when a client asks "what's in this skill?", the reply left out the skill's own SKILL.md (the main instruction file) and any subfolders. The spec says the list must be complete. Fixed so the whole set comes back.

#5
Fixed · 07587b3#5

Directory listings used the wrong shape

Plain: when listing a folder, the code returned a custom children structure. The spec says folders should come back as normal "resources" tagged with a folder marker (mimeType: "inode/directory"). Clients written to the spec wouldn't parse the old shape. Fixed to match the spec.

#6
Fixed · 07587b3#6

Two bugs in reading a directory

Plain: (a) one code path always returned an empty list; (b) asking for a folder that doesn't exist returned a success with nothing in it, instead of a proper "not found" error. Fixed: the empty path is wired up, and a missing folder now returns error code -32602.

#7
Fixed · 45f82bf + d225761#7

"Get this skill" was too permissive and lost data

Plain: three problems. It accepted any file address inside a skill rather than the skill itself. It returned the wrong error code ("method not found" instead of "invalid argument") when nothing matched. And the macro version threw away the skill's metadata after reading it. All three fixed.

#8
Fixed · 07587b3#8

Skill metadata got mangled when read

Plain: each skill starts with a small block of settings (its "frontmatter"). The code used a home-made reader that flattened nested settings into a mess. Replaced with a proper reader (serde_yaml) that preserves the block exactly as written.

#9
Discovery#9

The address bug can't be solved by reading the address at all

Plain: a skill address like skill://acme/billing/refunds/templates/x.md packs a skill name and a file path into one string, with no separator between them. Skill names can be several segments deep, so there's no way to split the address and know where the name ends. An initial attempt split at the first slash — wrong, it broke nested skills — so it was reverted.

dev 1
Deviation 1 of 2dev 1

Reverted the first address fix rather than patching it

What we tried
Split the address string at the first slash to separate skill name from file path.
Why it failed
Skill names can be several segments deep. Splitting at the first slash turned acme/billing/refunds into just acme — silently wrong.
What we did instead
Stop trying to read the address. Instead, match it against the list of skills the server actually knows about, and pick the longest matching one. Commit 394627d.
Note
The reviewer's own suggestion — "just match the registered roots" — was simpler than the first framing of it.
dev 2
Deviation 2 of 2dev 2

Dropped a test that couldn't actually run

What we tried
A test that launches the server and talks to it over a pipe, to check real responses.
Why it failed
The server answered the first handshake message and then went silent in the test harness. The pipe, not the server, was the problem — but a green-looking test that asserts nothing is worse than no test.
What we did instead
Replaced it with 5 focused tests on the address-matching logic directly. Deleted the flaky one rather than ship a no-op.
Closed 2026-10-11
A real end-to-end test was genuinely missing — and that gap had a cost. A real downstream consumer (wde-mcp-server, the WDE skill exposed over MCP) was built and driven with live requests. It immediately exposed 2 bugs in its own handlers, 1 compile-break from our DirectoryEntry→Resource rename, and 1 coverage lie in this very test suite. The suite went 10/10 → 12/12 with the two methods the header claimed but never called.
dev 3
Deviation 3 of 3dev 3

Dropped the top-level skills capability field

What we tried
Added ServerCapabilities.skills field + SkillsCapability struct + const SKILLS: bool in the builder macro.
Why it failed
Adding a const generic to ServerCapabilitiesBuilderState changed every existing generic instantiation — cargo-semver-checks flagged it as a major breaking change. CI is configured for minor.
What we did instead
Deleted the field, the struct, and the const generic. Used enable_skills_extension() (which writes to the extensions map) instead — matching how enable_tasks() works. The macro calls .enable_skills_extension() + .enable_resources(). No public API change. Serena-verified: SkillsCapability, enable_skills_with, and the skills field no longer exist.
Also fixed
Doc links in rmcp-macros: [SkillEntry] and [SkillRouter] were unresolved (types live in rmcp, not rmcp-macros). Changed to plain text. .enable_skills() corrected to .enable_skills_extension().
e2e
Verified 2026-10-11e2e

Driven by a real MCP consumer — and it found things the suite never could

Plain: the best test of "does this actually work" is a real program that uses it, not a test that pretends to. So the WDE skill was turned into a real MCP server and driven with live requests over HTTP.

Proved on the wire
initialize returned capabilities.extensions["io.modelcontextprotocol/skills"] — the Option D change confirmed against a real client, not a snapshot. skills/list, skills/get, and tools/list all returned real data.
Found — 1 compile-break
The DirectoryEntry → Resource rename (defect 6) broke the consumer at compile time. Every in-tree caller was updated; the one real external consumer was not, because no test watches for it.
Found — 2 consumer bugs
resources/directory/read returned empty (the file map was keyed by the SKILL.md address, but the request carries the skill root) and resources/read returned -32601 (never implemented). Both fixed in the consumer.
Discriminated: not the SDK
The same two requests were sent to the SDK's own example server — both passed. Same SDK, different server, opposite result. That is what proves the bugs were the consumer's, not the SDK's. The SDK only forwards the request to the handler; it does no address parsing.
Found — 1 coverage lie
The conformance suite's header listed resources/directory/read as covered. No test called it. Two real tests added; suite 10/10 → 12/12. Teeth proven by forcing the handler to return empty (RED: left: 0, right: 2), then restoring (GREEN).