Skip to content

Commit 2fc1932

Browse files
Rewrite the SkillCatalog docs tutorial-first
1 parent c71635c commit 2fc1932

2 files changed

Lines changed: 100 additions & 71 deletions

File tree

‎docs/advanced/skills.md‎

Lines changed: 98 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ skim that page first.
1717
`Skills` gives you the **protocol** primitives: request/response handling, capability
1818
advertisement, and SEP-2640 conformance validation.
1919

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.
20+
On its own it doesn't discover, read, or hash anything. You supply handlers that answer from
21+
wherever your catalog lives — a database, a generated index, an in-memory list — and you serve
22+
each skill's files as ordinary resources through `MCPServer.add_resource` or an
23+
`@mcp.resource(...)` template handler.
2424

25-
`SkillCatalog` is a provider built on top of `Skills` that implements these handlers for a
26-
source of skill files, such as a local directory. See
25+
If your skills live in a directory, skip the handlers and use `SkillCatalog`. See
2726
[Serving skills from a directory](#serving-skills-from-a-directory).
2827

2928
## Serving a skill
@@ -122,82 +121,115 @@ setting.
122121

123122
## Serving skills from a directory
124123

125-
`SkillCatalog` is a `Skills` extension that discovers skills in a `SkillProvider` and answers
126-
`skills/list`, `skills/get`, and optionally `resources/directory/read` from what it finds. It
127-
removes the need to write handlers or to register each file as a resource.
124+
Most of the time your skills are just a folder on disk. For that, you don't need to write handlers
125+
or register each file: `SkillCatalog` reads the folder and answers everything for you.
126+
127+
Say you have this layout:
128+
129+
```text
130+
skills/
131+
└── git-workflow/
132+
├── SKILL.md
133+
└── references/
134+
└── conventions.md
135+
```
136+
137+
Load it and pass it to your server:
128138

129139
```python title="server.py" hl_lines="11-15"
130140
--8<-- "docs_src/skills/tutorial002.py"
131141
```
132142

133-
`SkillCatalog.load()` is a coroutine, so the server is constructed inside a running event loop.
134-
`FilesystemSkillProvider` reads skills from a local directory. `parse_frontmatter` supplies the
135-
YAML parser. The SDK does not install one, so this example uses [PyYAML](https://pypi.org/project/PyYAML/),
136-
which you add to your own project (`uv add pyyaml`).
143+
That's the whole server. `skills/list`, `skills/get`, and `resources/directory/read` are answered
144+
from the folder, and every file in it is served as a `skill://` resource.
137145

138-
### How skills are discovered
146+
* `FilesystemSkillProvider(root)` tells the catalog where the files are.
147+
* `parse_frontmatter` turns the YAML between the `---` lines of a `SKILL.md` into a dict. The SDK
148+
doesn't ship a YAML parser, so bring your own. This example uses
149+
[PyYAML](https://pypi.org/project/PyYAML/) (`uv add pyyaml`).
150+
* `directory_read=True` also serves `resources/directory/read` (more on that
151+
[below](#directory-reads-from-a-catalog)).
139152

140-
A skill is a directory that contains a `SKILL.md` file. The skill's files are all files beneath
141-
that directory, including the files of any skill nested inside it. A nested skill is also listed
142-
as a skill in its own right.
153+
`SkillCatalog.load()` is a coroutine, so build the server inside a running event loop, as
154+
`main()` does above.
143155

144-
Each file is served as a `skill://` resource. Its URI is `skill://` followed by its path, with
145-
each path segment percent-encoded. A file has the same URI regardless of which skill lists it.
156+
### How skills are found
146157

147-
Files and directories whose names begin with `.` are excluded. They are never read, listed, or
148-
served. Files outside any skill directory are not served.
158+
A skill is a directory that contains a `SKILL.md`. Its files are everything beneath that
159+
directory, including the files of any skill nested inside it, which is also listed as a skill in
160+
its own right.
149161

150-
The catalog reads and hashes every file once, when `load()` runs, and serves that snapshot. The
151-
`resources` manifest of each skill therefore describes the files exactly as they were read.
152-
The catalog is fixed for the lifetime of the server; to publish changes, create a new catalog.
162+
Each file's URI is `skill://` followed by its path, with every segment percent-encoded. Files and
163+
directories whose names start with `.` are ignored entirely, and files outside any skill directory
164+
are not served.
153165

154-
### Options
166+
!!! note
167+
The catalog reads and hashes every file **once**, when `load()` runs, and serves that snapshot.
168+
That's how the `digest` and `size` in each skill's manifest are guaranteed to match what
169+
`resources/read` returns.
155170

156-
`SkillCatalog.load(provider, *, parse_frontmatter, ...)` accepts the following arguments.
171+
It also means the catalog is fixed for the lifetime of the server. If a file changes
172+
afterwards, reading it fails with an internal error (`-32603`) until you restart the server or
173+
create a new catalog. And because `load()` reads every file, a large asset directory makes
174+
startup slower.
157175

158-
| Argument | Default | Description |
159-
|---|---|---|
160-
| `provider` | required | The `SkillProvider` that lists and reads the files. |
161-
| `parse_frontmatter` | required | Receives the text between the `---` lines of a `SKILL.md` and returns a mapping with `str` keys, for example `yaml.safe_load`. |
162-
| `strict` | `False` | Controls the handling of invalid skills. See [Invalid skills](#invalid-skills). |
163-
| `directory_read` | `False` | Serves `resources/directory/read` and advertises the `directoryRead` setting. |
164-
| `ttl_ms` | `0` | The `ttlMs` value set on `skills/list`, `skills/get` and directory read results. |
165-
| `cache_scope` | `"private"` | The `cacheScope` value set on the same results. |
166-
| `page_size` | `None` | The maximum number of entries in one `skills/list` or directory read response. See [Pagination](#pagination). |
176+
### Directory reads from a catalog
167177

168-
`ttl_ms` and `cache_scope` follow the rules described in
169-
[Protocol version and caching](#protocol-version-and-caching). A negative `ttl_ms` raises
170-
`ValueError`.
178+
Pass `directory_read=True` and every directory that contains a served file can be read, including
179+
directories above a skill such as `skill://acme`. A directory lists its direct children;
180+
subdirectories come back with the MIME type `inode/directory`. Empty directories are not served.
171181

172-
The frontmatter mapping is returned in `skills/list` and `skills/get` without modification,
173-
including keys the SDK does not interpret. Values are converted to JSON; for example, YAML dates
174-
are returned as ISO 8601 strings. A value with no JSON representation, such as NaN, makes the
175-
skill invalid.
182+
### Caching hints
176183

177-
With `directory_read=True`, every directory that contains a served file can be read, including
178-
directories above the skill root such as `skill://acme`. A directory lists its direct children;
179-
subdirectories are returned with the MIME type `inode/directory`. Empty directories are not served.
184+
`ttl_ms` and `cache_scope` set the `ttlMs` and `cacheScope` values on every result:
185+
186+
```python
187+
async def build_catalog(root: Path) -> SkillCatalog:
188+
return await SkillCatalog.load(
189+
FilesystemSkillProvider(root),
190+
parse_frontmatter=yaml.safe_load,
191+
ttl_ms=60_000,
192+
cache_scope="public",
193+
)
194+
```
195+
196+
They follow the rules in [Protocol version and caching](#protocol-version-and-caching). A negative
197+
`ttl_ms` raises `ValueError`.
180198

181199
### Pagination
182200

183-
By default, `skills/list` and `resources/directory/read` return every entry in a single response.
184-
When `page_size` is set, each response contains at most that many entries, and a response that is
185-
followed by another page carries a `nextCursor`. The client passes it back as `cursor`, as for any
186-
other paginated list in the protocol. `SkillsClient.list_skills()` and `read_directory()` do this
187-
automatically.
201+
By default every entry comes back in one response. If your catalog is big, set `page_size`:
202+
203+
```python
204+
async def build_catalog(root: Path) -> SkillCatalog:
205+
return await SkillCatalog.load(
206+
FilesystemSkillProvider(root),
207+
parse_frontmatter=yaml.safe_load,
208+
directory_read=True,
209+
page_size=50,
210+
)
211+
```
212+
213+
Each response then holds at most 50 entries and carries a `nextCursor` while more remain.
214+
`SkillsClient.list_skills()` and `read_directory()` follow it for you. A skill entry is never split
215+
across pages.
188216

189-
A skill entry is never split across pages. The cursor is opaque to clients. A cursor that the
190-
catalog did not issue is rejected with Invalid params (`-32602`), and so is any cursor when
191-
`page_size` is not set. A `page_size` below 1 raises `ValueError`.
217+
The cursor is opaque. One the catalog didn't issue is rejected with Invalid params (`-32602`), and
218+
so is any cursor when `page_size` isn't set. A `page_size` below 1 raises `ValueError`.
192219

193220
### Invalid skills
194221

195-
A skill is invalid when its `SKILL.md` has no frontmatter block, has an unterminated block, cannot
222+
A skill is invalid when its `SKILL.md` has no frontmatter block, has an unterminated block, can't
196223
be parsed, or fails the SEP-2640 requirements for `name` and `description`. A `SKILL.md` placed
197-
directly in the provider root is also invalid, because the skill has no directory name.
224+
directly in the root is also invalid, because the skill has no directory name.
225+
226+
By default (`strict=False`) an invalid skill is left out and a warning is logged, so one bad skill
227+
doesn't take the server down. Pass `strict=True` to make `load()` raise a single `ValueError` that
228+
lists every invalid skill, which is handy in CI.
198229

199-
* With `strict=False`, an invalid skill is omitted and a warning is logged.
200-
* With `strict=True`, `load()` raises a single `ValueError` that lists every invalid skill.
230+
The frontmatter is returned in `skills/list` and `skills/get` as written, including keys the SDK
231+
doesn't interpret. Values are converted to JSON, so YAML dates become ISO 8601 strings. A value with
232+
no JSON form, such as NaN, makes the skill invalid.
201233

202234
A skill whose manifest exceeds the 512 files or 16 MiB recommended by SEP-2640 is still served,
203235
and a warning is logged.
@@ -254,18 +286,15 @@ You don't have to branch on protocol version yourself.
254286

255287
## What this SDK doesn't do
256288

257-
`Skills` is a protocol adapter, not a skills provider. It has no opinion on where a skill's bytes
258-
live, how they're indexed, or when a catalog is refreshed — that's for a higher-level library, or
259-
your own handler, to decide.
289+
`Skills` is a protocol adapter, not a skills store. It has no opinion on where a skill's bytes
290+
live or when a catalog is refreshed.
260291

261-
If you're looking for "scan this directory and serve whatever's in it," you're looking for a
262-
provider built on top of `Skills`, not `Skills` itself.
292+
`SkillCatalog` covers the common case of a directory (or any other file source behind a
293+
`SkillProvider`), with a few limits:
263294

264-
[`SkillCatalog`](#serving-skills-from-a-directory) is such a provider. It has the following
265-
limitations:
295+
* It doesn't include a YAML parser. `parse_frontmatter` supplies one.
296+
* The catalog isn't reloaded. To publish changes, create a new catalog.
297+
* Empty directories aren't served by directory reads.
266298

267-
* It does not include a YAML parser. The `parse_frontmatter` argument supplies one.
268-
* The catalog is not reloaded. To publish changes, create a new catalog.
269-
* Empty directories are not served by directory reads.
270-
* Pagination is opt-in through `page_size`. Without it, `skills/list` and directory reads return
271-
all entries in a single response.
299+
For anything else, such as a database or a catalog that changes while the server runs, write your
300+
own handlers for `Skills`.

‎docs_src/skills/tutorial002.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@
88

99

1010
async def build_server(root: Path) -> MCPServer:
11-
ext_skills = await SkillCatalog.load(
11+
skills_catalog = await SkillCatalog.load(
1212
FilesystemSkillProvider(root),
1313
parse_frontmatter=yaml.safe_load,
1414
directory_read=True,
1515
)
16-
return MCPServer("MCP-over-skills", extensions=[ext_skills])
16+
return MCPServer("MCP-over-skills", extensions=[skills_catalog])
1717

1818

1919
async def main() -> None:

0 commit comments

Comments
 (0)