Serve Agent Skills over resources per SEP-2640 - #570
anirudhmungre wants to merge 3 commits into
Conversation
`MCP::Skill` is the entry shape `skills/list` and `skills/get` share: the verbatim `SKILL.md` frontmatter plus the complete file manifest with per-file digests and sizes. It enforces the extension's structural rules at construction — the URI addresses the skill's `SKILL.md`, the frontmatter carries `name` and `description`, that name equals the final skill-path segment, and the manifest is complete, duplicate-free and confined to the skill's root — so a malformed entry fails where it is declared rather than on the wire. The per-skill limits are reported rather than enforced: `#limit_violations` names what a skill exceeds, because SEP-2640 leaves loading an oversized skill to the host. `MCP::Skills` carries the wire vocabulary and the negotiation helpers, mirroring `MCP::Apps`: the `capabilities.extensions` fragment, URI construction and relative-reference resolution against a skill's root, and the `sha256:` digest form every manifest entry takes.
A server registers skills through the new `skills:` kwarg and answers all three methods with no further wiring: `skills/list` paginates the registered entries and carries the SEP-2549 cache hints on the modern wire, `skills/get` resolves one entry by URI, and `resources/directory/read` derives a directory's direct children from the registered manifests, marking subdirectories `inode/directory`. `skills/get` stays out of `CACHEABLE_RESULT_METHODS`: SEP-2640 leaves the hints on that result open rather than requiring them. Unknown URIs answer `-32602` with the URI in the error data, the code and shape `resources/read` already uses for an unknown resource. Negotiation runs through `capabilities.extensions`, so the methods are gated on the declaration rather than a top-level capability, and `resources/directory/read` additionally on its `directoryRead` setting. `require_extension!` tolerates either key form, since an extension identifier is a String on the wire and often a Symbol in Ruby. Declaring the extension without the `resources` capability is refused at construction — that pairing is a MUST, and skill files are read through `resources/read`, so the alternative is a server that lists skills nobody can read. The three handler blocks replace the defaults for catalogs this server cannot enumerate, dynamically generated skills among them, and take `server_context:` on the same opt-in basis as `resources_list_handler`.
Covers the server-side declaration, why an entry is a complete manifest rather than a summary, the dynamic marker, the per-skill limits, the handler blocks for unenumerable catalogs, and the directory-read gate.
|
@koic saw you picked my ticket up just recently - any thoughts on the shape here? |
|
Thank you for the detailed PR, and for writing up the decisions behind it. I read that section closely, and it is useful context. I am going to close this PR, though, and keep #568 open. I would like to take the Skills extension forward myself and land it as a planned whole: the server side, the filesystem provider the SEP sketches for SDKs, and the client wrappers, together with the release planning around it. I also want to line up the shape with the MCP specification, the other SDKs' implementations (which are still open PRs at this point), and a few other considerations, so that the API does not have to move after it ships. I will do that in due course and track it on the issue. None of this reflects on the quality of the work. The test suite, the linter, and the conformance suite pass locally on this branch, and the decisions you wrote down are exactly the kind of context that helps. I appreciate you taking the time. |
Implements the server side of SEP-2640 (Skills Extension, Extensions Track, Final): Agent Skills served over the existing Resources primitive. Opened as a draft — I'd rather agree the shape before filling in the remaining pieces listed at the bottom.
Closes #568.
What a server gets
That answers
skills/list,skills/getandresources/directory/readwith no further wiring; skill content is read through the ordinaryresources_read_handler.MCP::Skillis the entry shape both methods share, and enforces the extension's structural rules at construction: the URI addresses the skill'sSKILL.md, the frontmatter carriesnameanddescription, that name equals the final skill-path segment, and the manifest is complete, duplicate-free and confined to the skill's root.resources: MCP::Skill::DYNAMICcovers generated skills. The per-skill limits (512 files, 16 MiB) are reported through#limit_violationsand warned about at registration rather than enforced, since SEP-2640 leaves loading an oversized skill to the host.MCP::Skillscarries the wire vocabulary and negotiation helpers, mirroringMCP::Apps.skills_list_handler/skills_get_handler/resources_directory_read_handlerreplace the defaults for catalogs a server cannot enumerate, and takeserver_context:on the same opt-in basis asresources_list_handler.Decisions worth a look
skills/listjoinsCACHEABLE_RESULT_METHODS;skills/getdoes not. The SEP requires the SEP-2549 hints on the listing at 2026-07-28 and explicitly leaves them open forskills/get, so that result stays hint-free rather than guessing.resourcescapability raises at construction. The pairing is a MUST, and skill files are read throughresources/read— the alternative is a server that lists skills nobody can read. It's the one place I've added a hard failure; happy to downgrade it to a warning.require_extension!tolerates either key form. An extension identifier is a String on the wire and often a Symbol when written in Ruby, sodigalone isn't enough. Same toleranceMCP::Apps.client_supports?already applies.skills/liston a server that has not declared the extension now answers the capability error rather than-32601. This matches howprompts/listbehaves without the prompts capability. A conforming client only calls these after seeing the declaration, so I don't think it's observable in practice — but it is a behavior change on an existing surface and worth flagging.resources/directory/readderives children from the registered manifests, which is exact for non-dynamic skills and saves every user reimplementing it.mimeTypeis inferred from the file extension through a small table and omitted when unknown; the handler block is there for anything the manifests don't describe.Deliberately not in this PR
Staged the way go-sdk and typescript-sdk split theirs, and happy to fold any of it in here instead:
list_skills,get_skill,read_directory) — the SEP recommends them, and each wants theListXResultstruct and auto-paginating accessor the other client list methods have.MCP::Skillfrom a directory by reading frontmatter and computing digests — the ergonomic entry point the SEP sketches, and the thing that makes this usable without hand-assembling manifests (go-sdk split this out the same way).ROADMAP.md, which names the unimplemented extensions — left alone rather than edited on your behalf.Verification
rake test— 1927 runs, 5257 assertions, 0 failures. 40 of those assertions are new, acrosstest/mcp/skill_test.rb,test/mcp/skills_test.rbandtest/mcp/server_skills_test.rb.rake rubocop— 168 files, no offenses.rake conformance:testdid not run here: the@modelcontextprotocol/conformancepackage needs Node >= 22 and this machine has 20 (fs.globSyncis missing). Nothing in the diff is reachable without the extension declaration, which no conformance scenario makes, but I haven't confirmed that locally — worth a CI eye.