Skip to content

Commit cde5cff

Browse files
author
vijay
committed
incorporated review comments and defined skills as extensions
1 parent e90e294 commit cde5cff

5 files changed

Lines changed: 269 additions & 195 deletions

File tree

‎docs/advanced/skills.md‎

Lines changed: 75 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,97 @@
11
# Skills
22

33
[SEP-2640](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640) defines a
4-
convention for serving [Agent Skills](https://agentskills.io/) over MCP: a skill is a directory
5-
of files — minimally a `SKILL.md` with YAML frontmatter — exposed as ordinary MCP resources,
6-
conventionally under a `skill://` URI. A server enumerates its skills with `skills/list`,
7-
answers for any one of them by URI with `skills/get`, and — optionally — lists a directory's
8-
direct children with `resources/directory/read`.
9-
10-
The SDK ships this as the built-in `Skills` extension (`io.modelcontextprotocol/skills`). If
11-
[Extensions](extensions.md) are new to you, skim that page first.
12-
13-
`Skills` provides the **protocol** primitives: request/response handling, capability
14-
advertisement, and SEP-2640 conformance validation. It does not discover, read, or hash skills
15-
from a filesystem — you supply handlers that answer from wherever your catalog actually lives
16-
(a database, a generated index, an in-memory list, or a directory you walk yourself), and serve
17-
each skill's files as ordinary resources through `MCPServer.add_resource` or an
18-
`@mcp.resource(...)` template handler.
4+
convention for serving [Agent Skills](https://agentskills.io/) over MCP. A skill is just a
5+
directory of files — at minimum a `SKILL.md` with YAML frontmatter — that you expose as ordinary
6+
MCP resources, conventionally under a `skill://` URI.
7+
8+
A server enumerates its skills with `skills/list`, answers for any single one by URI with
9+
`skills/get`, and — optionally — lists a directory's direct children with
10+
`resources/directory/read`.
11+
12+
The SDK ships this as the built-in `Skills` extension (`io.modelcontextprotocol/skills`). There's
13+
one on the server side and one on the client side. If [Extensions](extensions.md) are new to you,
14+
skim that page first.
15+
16+
!!! info
17+
`Skills` gives you the **protocol** primitives: request/response handling, capability
18+
advertisement, and SEP-2640 conformance validation.
19+
20+
It does **not** discover, read, or hash skills from a filesystem. You supply handlers that
21+
answer from wherever your catalog actually lives — a database, a generated index, an in-memory
22+
list, or a directory you walk yourself — and you serve each skill's files as ordinary resources
23+
through `MCPServer.add_resource` or an `@mcp.resource(...)` template handler.
1924

2025
## Serving a skill
2126

27+
Here's a server that serves one skill:
28+
2229
```python title="server.py" hl_lines="31-41 44-51 55"
2330
--8<-- "docs_src/skills/tutorial001.py"
2431
```
2532

26-
Three moves:
33+
There are three moves here:
2734

28-
* `Skill(uri=..., frontmatter=..., resources=[...])`: one entry, identical in shape whether it
35+
* `Skill(uri=..., frontmatter=..., resources=[...])` is one entry. It has the same shape whether it
2936
comes back from `skills/list` or `skills/get`. `resources` is the skill's complete file
3037
manifest — every file, `SKILL.md` included, each with a `sha256:...` digest and byte size — or
3138
the string `"dynamic"` for content generated on demand.
32-
* `list_skills`/`get_skill`: plain async callables, invoked per request. `get_skill` **must**
33-
answer for a skill even if a real `list_skills` implementation omitted it — SEP-2640 requires
34-
a server to answer by URI for every skill it serves, listed or not.
35-
* `mcp.add_resource(TextResource(uri=SKILL_URI, ...))`: the skill's actual file content, served
36-
through the SDK's ordinary resource machinery. `Skills` never reads or writes resource content
37-
itself.
39+
* `list_skills` and `get_skill` are plain async callables, invoked once per request. `get_skill`
40+
**must** answer for a skill even if your `list_skills` left it out — SEP-2640 requires a server
41+
to answer by URI for every skill it serves, listed or not.
42+
* `mcp.add_resource(TextResource(uri=SKILL_URI, ...))` registers the skill's actual file content,
43+
served through the SDK's ordinary resource machinery. `Skills` never reads or writes resource
44+
content itself.
3845

39-
`Skills(list_skills=..., get_skill=...)` is all a server needs; `resources/directory/read` is
40-
optional (below).
46+
And that's it. `Skills(list_skills=..., get_skill=...)` is all a server needs;
47+
`resources/directory/read` is optional (more on that below).
4148

4249
## Fetching a skill
4350

44-
```python title="client.py" hl_lines="5"
51+
On the client side, `Skills` is a [`ClientExtension`](extensions.md). You register it the same way
52+
you register any other one — by passing it to `Client(extensions=[...])` — and then call `bind` to
53+
get the verbs tied to that connection:
54+
55+
```python title="client.py" hl_lines="9-11"
4556
--8<-- "docs_src/skills/tutorial001_client.py"
4657
```
4758

48-
`list_skills` and `read_directory` follow `nextCursor` to completion, so you get every page's
49-
skills or resources in one call; `get_skill` costs exactly one request. These three validate the
50-
server's response against the SEP-2640 conformance rules before returning it — a name that doesn't
51-
match its URI, a digest in the wrong shape, or an incomplete manifest raises `ValueError` rather
52-
than reaching your code. `read_skill_uri` is the exception: a thin, discoverable alias for
53-
`resources/read` that returns a `ReadResourceResult` (text or blob contents) and validates nothing
54-
itself (see the next paragraph).
59+
`skills.bind(client)` hands you a `BoundSkills`, and its methods are the SEP-2640 verbs:
5560

56-
`verify_skill_resource(skill, uri, content)` checks a file's bytes — size, then SHA-256 digest —
57-
against the entry you hold for it. Call it after `read_skill_uri` and before treating the content
58-
as trustworthy: `resources/read` returns whatever bytes the server sends *right now*, verification
59-
is what ties those bytes back to the manifest you already validated. It applies to a static
60-
manifest only — a `"dynamic"` skill carries no digests, so calling it on one raises `ValueError`.
61+
* `list_skills` and `read_directory` follow `nextCursor` to completion, so a single call gives you
62+
every page's skills or resources.
63+
* `get_skill` costs exactly one request.
64+
65+
These three validate the server's response against the SEP-2640 conformance rules before returning
66+
it. A name that doesn't match its URI, a digest in the wrong shape, or an incomplete manifest raises
67+
`ValueError` rather than reaching your code.
68+
69+
`read_skill_uri` is the exception. It's a thin, discoverable alias for `resources/read` that returns
70+
a `ReadResourceResult` (text or blob contents) and validates nothing itself — that's the next step.
71+
72+
!!! tip
73+
`verify_skill_resource(skill, uri, content)` checks a file's bytes — size, then SHA-256
74+
digest — against the entry you hold for it. Call it after `read_skill_uri` and *before* you
75+
treat the content as trustworthy.
76+
77+
`resources/read` returns whatever bytes the server sends *right now*; verification is what ties
78+
those bytes back to the manifest you already validated. It applies to a static manifest only —
79+
a `"dynamic"` skill carries no digests, so calling it on one raises `ValueError`.
6180

6281
!!! warning
6382
Skill content is untrusted model input, exactly like any other server-provided text. SEP-2640
6483
requires a host to tag it with its originating server before it reaches the model, and to
6584
never grant the frontmatter's `allowed-tools` field (or any other permission-widening field)
66-
without explicit per-skill user approval. Both are host responsibilities the SDK cannot
67-
discharge for you — see the SEP's [Security Implications](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640)
85+
without explicit per-skill user approval.
86+
87+
Both are host responsibilities the SDK cannot discharge for you — read the SEP's
88+
[Security Implications](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2640)
6889
section before building a host on top of this extension.
6990

7091
## Directory reads
7192

7293
A skill's instructions often point at a directory rather than a file ("pick the matching
73-
template from `templates/`"). `resources/list` cannot answer that — it enumerates a server's
94+
template from `templates/`"). `resources/list` can't answer that — it enumerates a server's
7495
entire resource space, not one subtree — so SEP-2640 adds `resources/directory/read`, gated
7596
behind the `directoryRead` capability setting:
7697

@@ -88,22 +109,24 @@ mcp = MCPServer(
88109
```
89110

90111
Supplying `read_directory` advertises `{"directoryRead": true}` under the extension's
91-
capabilities; omitting it advertises neither the setting nor the method — a client calling
92-
`resources/directory/read` against such a server gets `METHOD_NOT_FOUND`.
93-
`mcp.client.skills.read_directory` raises before sending if the connected server hasn't
94-
advertised the setting.
112+
capabilities. Omitting it advertises neither the setting nor the method — a client calling
113+
`resources/directory/read` against such a server gets `METHOD_NOT_FOUND`. On the client side,
114+
`read_directory` raises before it sends anything if the connected server hasn't advertised the
115+
setting.
95116

96117
## Protocol version and caching
97118

98119
In protocol version `2026-07-28` and later, `skills/list` and `skills/get` results carry the base
99-
protocol's caching fields, [`ttlMs` and `cacheScope`](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549) — the
100-
same freshness hint `tools/list`, `resources/list`, and `resources/read` carry. `Skills` fills
120+
protocol's caching fields, [`ttlMs` and `cacheScope`](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549) —
121+
the same freshness hint `tools/list`, `resources/list`, and `resources/read` carry. `Skills` fills
101122
`cacheScope` with `"public"` when your handler leaves it unset, and omits both fields entirely on an
102-
older connection, so you don't have to branch on protocol version yourself.
123+
older connection. So you don't have to branch on protocol version yourself.
103124

104125
## What this SDK doesn't do
105126

106-
`Skills` is a protocol adapter, not a skills provider. It has no opinion on where a skill's
107-
bytes live, how they're indexed, or when a catalog is refreshed — that's for a higher-level
108-
library, or your own handler, to decide. If you're looking for "scan this directory and serve
109-
whatever's in it," you're looking for a provider built on top of `Skills`, not `Skills` itself.
127+
`Skills` is a protocol adapter, not a skills provider. It has no opinion on where a skill's bytes
128+
live, how they're indexed, or when a catalog is refreshed — that's for a higher-level library, or
129+
your own handler, to decide.
130+
131+
If you're looking for "scan this directory and serve whatever's in it," you're looking for a
132+
provider built on top of `Skills`, not `Skills` itself.

‎docs_src/skills/tutorial001_client.py‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import anyio
22

33
from mcp import Client
4-
from mcp.client.skills import get_skill, list_skills, read_skill_uri, verify_skill_resource
4+
from mcp.client.skills import Skills, verify_skill_resource
55
from mcp.types import TextResourceContents
66

77

88
async def main() -> None:
9-
async with Client("http://localhost:8000/mcp") as client:
10-
for skill in await list_skills(client.session):
9+
skills = Skills()
10+
async with Client("http://localhost:8000/mcp", extensions=[skills]) as client:
11+
catalog = skills.bind(client)
12+
for skill in await catalog.list_skills():
1113
print(skill.uri, skill.frontmatter["description"])
1214

13-
skill = await get_skill(client.session, "skill://git-workflow/SKILL.md")
14-
result = await read_skill_uri(client.session, skill.uri)
15+
skill = await catalog.get_skill("skill://git-workflow/SKILL.md")
16+
result = await catalog.read_skill_uri(skill.uri)
1517
content = result.contents[0]
1618
if isinstance(content, TextResourceContents):
1719
verify_skill_resource(skill, skill.uri, content.text.encode())

0 commit comments

Comments
 (0)