Skip to content

Commit a32f4e6

Browse files
committed
docs(skills): document the SEP-2640 Skills extension
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.
1 parent 2999456 commit a32f4e6

2 files changed

Lines changed: 108 additions & 0 deletions

File tree

‎docs/_extensions/index.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ as described on [Capability Extensions](/extensions/capability-extensions/).
1515
The extensions this SDK ships support for:
1616

1717
- [MCP Apps](/extensions/mcp-apps/) (SEP-1865) - interactive HTML user interfaces rendered by the host for tool results
18+
- [Skills](/extensions/skills/) (SEP-2640) - Agent Skills served as resources, with `skills/list`, `skills/get` and scoped directory reads

‎docs/_extensions/skills.md‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
---
2+
layout: default
3+
title: Skills
4+
nav_order: 4
5+
---
6+
7+
# Skills
8+
9+
Skills (SEP-2640) is a Final extension (negotiated via [Capability Extensions](/extensions/capability-extensions/))
10+
that serves [Agent Skills](https://agentskills.io/specification) over the existing Resources primitive.
11+
A skill is a directory of files, minimally a `SKILL.md`, and each of those files is an ordinary MCP resource
12+
under the `skill://` scheme. A host that already treats resources as a virtual filesystem consumes an
13+
MCP-served skill exactly as it consumes one from disk.
14+
15+
The extension adds three methods on top of that: `skills/list` enumerates the skills a server serves,
16+
`skills/get` returns one entry by URI, and the optional `resources/directory/read` lists a directory's
17+
direct children. Skill *content* is always read through ordinary `resources/read`.
18+
19+
```ruby
20+
skill_md = File.read("skills/refunds/SKILL.md")
21+
email_md = File.read("skills/refunds/examples/email.md")
22+
uri = MCP::Skills.uri_for("acme/billing/refunds") # => "skill://acme/billing/refunds/SKILL.md"
23+
24+
capabilities = MCP::Server::Capabilities.new
25+
capabilities.support_resources # required: skill files are read through `resources/read`
26+
capabilities.support_extensions(MCP::Skills.capability(directory_read: true))
27+
28+
server = MCP::Server.new(
29+
name: "billing_server",
30+
capabilities: capabilities,
31+
skills: [
32+
MCP::Skill.new(
33+
uri: uri,
34+
# The verbatim SKILL.md frontmatter: every field the author wrote, not a curated subset.
35+
frontmatter: { "name" => "refunds", "description" => "Process customer refund requests per company policy" },
36+
# The complete manifest: every file of the skill, SKILL.md included, with digests and sizes.
37+
resources: [
38+
{ uri: uri, digest: MCP::Skills.digest(skill_md), size: skill_md.bytesize },
39+
{ uri: MCP::Skills.resolve(uri, "examples/email.md"), digest: MCP::Skills.digest(email_md), size: email_md.bytesize },
40+
],
41+
),
42+
],
43+
)
44+
45+
# Skill files are served as ordinary resources.
46+
server.resources_read_handler do |params|
47+
[{ uri: params[:uri], mimeType: "text/markdown", text: load_skill_file(params[:uri]) }]
48+
end
49+
```
50+
51+
That server answers `skills/list`, `skills/get` and `resources/directory/read` with no further wiring:
52+
directory children are derived from the registered skills' manifests.
53+
54+
## Entries
55+
56+
A `skills/list` entry is a complete manifest rather than a summary, so a host that pages the listing has,
57+
in that one pass, everything it needs to build its registry, present the skill for approval, bind that
58+
approval to content, and verify every file it later reads. `skills/get` returns the identical shape for a
59+
single skill and is never a step a host must take to complete a listed entry; it exists to refresh one
60+
entry's digests, and to answer for a skill the listing omitted.
61+
62+
`MCP::Skill` enforces the extension's structural rules at construction: the URI addresses the skill's
63+
`SKILL.md`, the frontmatter carries `name` and `description`, the frontmatter `name` equals the final
64+
segment of the skill path, and the manifest is complete, duplicate-free and confined to the skill's root.
65+
66+
{: .important }
67+
A skill whose content is generated per request cannot publish stable digests. Pass
68+
`resources: MCP::Skill::DYNAMIC` instead of a manifest. Such a skill offers no content integrity and
69+
cannot be content-bound, and hosts MAY decline to load it.
70+
71+
## Limits
72+
73+
SEP-2640 fixes two per-skill limits every conforming host accepts: 512 resources and 16 MiB in total.
74+
A registered skill that exceeds either is kept and warned about rather than refused — servers SHOULD stay
75+
within them, but only the host decides whether to load an oversized skill. `MCP::Skill#limit_violations`
76+
reports what a given skill exceeds.
77+
78+
## Unenumerable catalogs
79+
80+
A server whose skill catalog is large, generated, or otherwise unenumerable MAY return an empty or partial
81+
`skills/list`; hosts MUST NOT read that as proof the server has no skills. Such a server replaces the
82+
default lookups, and MUST still answer `skills/get` for every skill it serves:
83+
84+
```ruby
85+
server.skills_list_handler { |_params| [] }
86+
server.skills_get_handler { |params| SkillCatalog.find(params[:uri]) }
87+
server.resources_directory_read_handler { |params| SkillCatalog.children_of(params[:uri]) }
88+
```
89+
90+
Each block may declare `server_context:` to receive the request context, the same opt-in
91+
`resources_list_handler` uses.
92+
93+
## Directory reads
94+
95+
`resources/directory/read` is gated behind the `directoryRead` setting, which
96+
`MCP::Skills.capability(directory_read: true)` declares; a client MUST NOT call it otherwise. It returns the
97+
direct children of a directory resource as the same `Resource` objects `resources/list` returns,
98+
subdirectories carrying `mimeType: "inode/directory"`. The listing is never recursive: a client descends by
99+
calling the method again on a child directory. A URI that does not exist, or that is not a directory
100+
resource, answers `-32602`, as `skills/get` does for an unknown skill.
101+
102+
{: .note }
103+
For a skill whose entry carries a manifest, a directory read tells a host nothing the manifest did not.
104+
It earns its place for dynamically generated skills, for resource trees that are not skills at all, and for
105+
observing a directory without refreshing the entry. A host MUST NOT treat the result as extending a manifest.
106+
107+
See the [Skills extension specification](https://modelcontextprotocol.io/seps/2640-skills-extension).

0 commit comments

Comments
 (0)