Repository navigation
Conversation
koic
force-pushed
the
add_the_skills_extension
branch
4 times, most recently
from
October 27, 2026 04:45
58b995f to
88f84b1
Compare
## Motivation and Context Issue modelcontextprotocol#568 asks for the Skills extension (SEP-2640, Final): a server publishes Agent Skills, folders holding a `SKILL.md` with YAML frontmatter plus supporting files, through two methods, `skills/list` and `skills/get`, whose entries repeat the frontmatter and list every file with its SHA-256 digest and size, and a host reads the files through `resources/read` and verifies each one against the entry before loading it. Server side, `MCP::Server.new(skills:)` takes the catalog and advertises `io.modelcontextprotocol/skills`, with `resources` required alongside it as the SEP demands. `MCP::Skill.from_directory` covers the common case: every directory under a root that holds a `SKILL.md` becomes a skill, the frontmatter is read, digests and sizes are computed, a nested skill is published on its own and listed by the enclosing skill as supporting files (its manifest must be complete), and the default `resources/read` serves the files, answering `-32602` for an unserved URI under a skill. `resources/list` carries each skill's `SKILL.md` with the frontmatter `name` and `description`, and `skills/list` and `skills/get` carry the cache hints, both as the specification asks. `MCP::Skill.new` builds a skill in code and checks at definition time what a host may reject (URI grammar, `name` equal to the last path segment, entries under the root with the skill's own `SKILL.md` among them, the SEP limits). A skill defined locally is also held to the Agent Skills rules the specification requires of a served skill and the Python SDK enforces: a `name` of 1 to 64 lowercase letters, digits, and single hyphens, and a `description` of at most 1024 characters. An entry a client receives is not, as Agent Skills advises clients. `Server#skills_handlers(list:, get:)` serves catalogs that depend on the caller, the shape the Python SDK's `Skills(list_skills=, get_skill=)` converged on and the one a Rails user asked for on modelcontextprotocol#568; both handlers are required because a declaring server must answer `skills/get` for every skill it serves. Handlers do not combine with a registry that holds skills: a registered skill, its `SKILL.md` entry in `resources/list`, and its files are served to every caller, so `skills_handlers` refuses such a server, and a catalog that hides a skill from some callers serves the files under the same rule, with `MCP::Skill.read_file`. The files of a nested skill belong to the enclosing skill as well, so such a catalog hides a nested skill only by withholding both, which the documentation spells out. What the handlers answer is never stamped `cacheScope: "public"`, but `"private"` where the server-wide `cache_scope` would make it so: a handler answers per caller and, returning skills, has no result of its own to carry a scope in. Client side, `MCP::Client#list_skills`, `#get_skill`, and `#read_skill_resource` are admitted only after a handshake of the current connection advertised the extension and `resources`, as the SEP asks of clients and as the Python and TypeScript clients do. `read_skill_resource` verifies size, digest, and (for a `SKILL.md`) the frontmatter field by field, withholds content that does not match, and latches the skill so no further read of it is sent on this connection; that is what keeps a host from loading part of a tampered skill and what the `sep-2640-client-verify-*` conformance scenarios check. The SEP and the Python SDK name their thin, unverified wrapper `read_skill_uri`; the verified read gets its own name so the two never mean different things, and `MCP::Skill.verify_resource` and `verify_frontmatter` give hosts that read raw the same checks. Frontmatter is read under rules stricter than YAML so every library reads it the same way: one document, no tags, aliases, or merge keys, String keys, JSON-representable values, and no plain scalar whose YAML 1.1 reading (Psych's) differs from its YAML 1.2 reading (the JavaScript and Go libraries'). Files of directory-backed skills are looked up by exact URI in a map built at construction, opened without following a symbolic link in the last path component (hard links are refused), and refused when their inode, size, link count, or digest changed since registration. The advertised capabilities and the dispatch gate are computed from the configured state, so a server never advertises what it does not serve. `resources/directory/read`, which the specification leaves optional, is not served in this release; declaring `directoryRead` is refused so it can be added later without a change in meaning. `skills/list` and `skills/get` join the protocol request methods that are not run without an id. The entry's `_meta`, which the stable text declares on `Skill` and the Python and TypeScript entries carry, is passed through on both sides: `MCP::Skill.new(meta:)` puts it on the `skills/list` and `skills/get` entries, and a client entry exposes what the server sent as `#meta`, under the JSON rules of the frontmatter and with no key acted on, so an annotation a catalog attaches reaches the host whichever SDK is on the other side. Strings JSON cannot encode (bytes invalid in their labeled encoding, or a binary String with a byte beyond ASCII, which `valid_encoding?` alone lets through) are refused in the frontmatter and `_meta` at definition time as well: json 3.1 cannot generate the response carrying them, while older json still generates a binary String holding valid UTF-8 bytes, recent versions with a deprecation warning announcing that change. Frontmatter read from a file was already held to valid UTF-8. Closes modelcontextprotocol#568. ## How Has This Been Tested? test/mcp/skill_test.rb, test/mcp/skill/resource_entry_test.rb, test/mcp/skill/frontmatter_test.rb, test/mcp/skill/directory_test.rb, test/mcp/server_skills_test.rb, test/mcp/client_skills_test.rb, test/mcp/methods_test.rb, and the `sep-2640-client-*` drivers in conformance/client.rb; the pending `sep-2640-skills-enumeration` and `-manifest` server scenarios pass when run by name against conformance/server.rb, and `-directory` skips because `directoryRead` is not declared. Against the library at cc88656, `skills/list` answers `-32601` and `MCP::Client#read_resource` hands back a tampered `SKILL.md` without complaint. The server test also checks that neither skills method runs without a request id, however the skills were configured. The `_meta` of an entry is checked in the skill, server, and client tests, on the wire and as `#meta`. skill_test.rb requires digest itself: alone, it computed digests before anything autoloaded `MCP::Skill` and failed under most seeds. Without the encoding check, `MCP::Skill.new` accepted a binary string with a byte beyond ASCII and `JSON.generate` then failed on `to_h`. ## Breaking Changes None for servers that neither configure skills nor declare the extension by hand: `Server#capabilities=` now also accepts an `MCP::Server::Capabilities` builder and raises `ArgumentError` when the Hash declares the Skills extension without skills being configured, and `Server#define_resource` and `#resources=` refuse a URI that a directory-backed skill serves. `MCP::Client#connect` keeps its signature and return value. The server test helper takes the request params positionally only, since Ruby 2.7 reads a trailing Hash as keywords when the method also takes one.
koic
force-pushed
the
add_the_skills_extension
branch
from
October 27, 2026 04:49
88f84b1 to
e3d8c63
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation and Context
Issue #568 asks for the Skills extension (SEP-2640, Final): a server publishes Agent Skills, folders holding a
SKILL.mdwith YAML frontmatter plus supporting files, through two methods,skills/listandskills/get, whose entries repeat the frontmatter and list every file with its SHA-256 digest and size, and a host reads the files throughresources/readand verifies each one against the entry before loading it.Server side,
MCP::Server.new(skills:)takes the catalog and advertisesio.modelcontextprotocol/skills, withresourcesrequired alongside it as the SEP demands.MCP::Skill.from_directorycovers the common case: every directory under a root that holds aSKILL.mdbecomes a skill, the frontmatter is read, digests and sizes are computed, a nested skill is published on its own and listed by the enclosing skill as supporting files (its manifest must be complete), and the defaultresources/readserves the files, answering-32602for an unserved URI under a skill.resources/listcarries each skill'sSKILL.mdwith the frontmatternameanddescription, andskills/listandskills/getcarry the cache hints, both as the specification asks.MCP::Skill.newbuilds a skill in code and checks at definition time what a host may reject (URI grammar,nameequal to the last path segment, entries under the root with the skill's ownSKILL.mdamong them, the SEP limits). A skill defined locally is also held to the Agent Skills rules the specification requires of a served skill and the Python SDK enforces: anameof 1 to 64 lowercase letters, digits, and single hyphens, and adescriptionof at most 1024 characters. An entry a client receives is not, as Agent Skills advises clients.Server#skills_handlers(list:, get:)serves catalogs that depend on the caller, the shape the Python SDK'sSkills(list_skills=, get_skill=)converged on and the one a Rails user asked for on #568; both handlers are required because a declaring server must answerskills/getfor every skill it serves. Handlers do not combine with a registry that holds skills: a registered skill, itsSKILL.mdentry inresources/list, and its files are served to every caller, soskills_handlersrefuses such a server, and a catalog that hides a skill from some callers serves the files under the same rule, withMCP::Skill.read_file. The files of a nested skill belong to the enclosing skill as well, so such a catalog hides a nested skill only by withholding both, which the documentation spells out. What the handlers answer is never stampedcacheScope: "public", but"private"where the server-widecache_scopewould make it so: a handler answers per caller and, returning skills, has no result of its own to carry a scope in.Client side,
MCP::Client#list_skills,#get_skill, and#read_skill_resourceare admitted only after a handshake of the current connection advertised the extension andresources, as the SEP asks of clients and as the Python and TypeScript clients do.read_skill_resourceverifies size, digest, and (for aSKILL.md) the frontmatter field by field, withholds content that does not match, and latches the skill so no further read of it is sent on this connection; that is what keeps a host from loading part of a tampered skill and what thesep-2640-client-verify-*conformance scenarios check. The SEP and the Python SDK name their thin, unverified wrapperread_skill_uri; the verified read gets its own name so the two never mean different things, andMCP::Skill.verify_resourceandverify_frontmattergive hosts that read raw the same checks.Frontmatter is read under rules stricter than YAML so every library reads it the same way: one document, no tags, aliases, or merge keys, String keys, JSON-representable values, and no plain scalar whose YAML 1.1 reading (Psych's) differs from its YAML 1.2 reading (the JavaScript and Go libraries'). Files of directory-backed skills are looked up by exact URI in a map built at construction, opened without following a symbolic link in the last path component (hard links are refused), and refused when their inode, size, link count, or digest changed since registration. The advertised capabilities and the dispatch gate are computed from the configured state, so a server never advertises what it does not serve.
resources/directory/read, which the specification leaves optional, is not served in this release; declaringdirectoryReadis refused so it can be added later without a change in meaning.skills/listandskills/getjoin the protocol request methods that are not run without an id.Closes #568.
How Has This Been Tested?
test/mcp/skill_test.rb, test/mcp/skill/resource_entry_test.rb, test/mcp/skill/frontmatter_test.rb, test/mcp/skill/directory_test.rb, test/mcp/server_skills_test.rb, test/mcp/client_skills_test.rb, test/mcp/methods_test.rb, and the
sep-2640-client-*drivers in conformance/client.rb; the pendingsep-2640-skills-enumerationand-manifestserver scenarios pass when run by name against conformance/server.rb, and-directoryskips becausedirectoryReadis not declared. Against the library at cc88656,skills/listanswers-32601andMCP::Client#read_resourcehands back a tamperedSKILL.mdwithout complaint. The server test also checks that neither skills method runs without a request id, however the skills were configured.Breaking Changes
None for servers that neither configure skills nor declare the extension by hand:
Server#capabilities=now also accepts anMCP::Server::Capabilitiesbuilder and raisesArgumentErrorwhen the Hash declares the Skills extension without skills being configured, andServer#define_resourceand#resources=refuse a URI that a directory-backed skill serves.MCP::Client#connectkeeps its signature and return value.Motivation and Context
How Has This Been Tested?
Breaking Changes
Types of changes
Checklist