You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 036abf7
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: docs/advanced/skills.md
+131Lines changed: 131 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,6 +22,10 @@ skim that page first.
22
22
list, or a directory you walk yourself — and you serve each skill's files as ordinary resources
23
23
through `MCPServer.add_resource` or an `@mcp.resource(...)` template handler.
24
24
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
27
+
[Serving skills from a directory](#serving-skills-from-a-directory).
28
+
25
29
## Serving a skill
26
30
27
31
Here's a server that serves one skill:
@@ -116,6 +120,124 @@ capabilities. Omitting it advertises neither the setting nor the method — a cl
116
120
`read_directory` raises before it sends anything if the connected server hasn't advertised the
117
121
setting.
118
122
123
+
## Serving skills from a directory
124
+
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.
128
+
129
+
```python title="server.py" hl_lines="11-15"
130
+
--8<--"docs_src/skills/tutorial002.py"
131
+
```
132
+
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 depend on one.
136
+
137
+
### How skills are discovered
138
+
139
+
A skill is a directory that contains a `SKILL.md` file. The skill's files are all files beneath
140
+
that directory, including the files of any skill nested inside it. A nested skill is also listed
141
+
as a skill in its own right.
142
+
143
+
Each file is served as a `skill://` resource. Its URI is `skill://` followed by its path, with
144
+
each path segment percent-encoded. A file has the same URI regardless of which skill lists it.
145
+
146
+
Files and directories whose names begin with `.` are excluded. They are never read, listed, or
147
+
served. Files outside any skill directory are not served.
148
+
149
+
The catalog reads and hashes every file once, when `load()` runs, and serves that snapshot. The
150
+
`resources` manifest of each skill therefore describes the files exactly as they were read.
151
+
The catalog is fixed for the lifetime of the server; to publish changes, create a new catalog.
152
+
153
+
### Options
154
+
155
+
`SkillCatalog.load(provider, *, parse_frontmatter, ...)` accepts the following arguments.
156
+
157
+
| Argument | Default | Description |
158
+
|---|---|---|
159
+
|`provider`| required | The `SkillProvider` that lists and reads the files. |
160
+
|`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`. |
161
+
|`strict`|`False`| Controls the handling of invalid skills. See [Invalid skills](#invalid-skills). |
162
+
|`directory_read`|`False`| Serves `resources/directory/read` and advertises the `directoryRead` setting. |
163
+
|`ttl_ms`|`0`| The `ttlMs` value set on `skills/list`, `skills/get` and directory read results. |
164
+
|`cache_scope`|`"private"`| The `cacheScope` value set on the same results. |
165
+
|`page_size`|`None`| The maximum number of entries in one `skills/list` or directory read response. See [Pagination](#pagination). |
166
+
167
+
`ttl_ms` and `cache_scope` follow the rules described in
168
+
[Protocol version and caching](#protocol-version-and-caching). A negative `ttl_ms` raises
169
+
`ValueError`.
170
+
171
+
The frontmatter mapping is returned in `skills/list` and `skills/get` without modification,
172
+
including keys the SDK does not interpret. Values are converted to JSON; for example, YAML dates
173
+
are returned as ISO 8601 strings. A value with no JSON representation, such as NaN, makes the
174
+
skill invalid.
175
+
176
+
With `directory_read=True`, every directory that contains a served file can be read, including
177
+
directories above the skill root such as `skill://acme`. A directory lists its direct children;
178
+
subdirectories are returned with the MIME type `inode/directory`. Empty directories are not served.
179
+
180
+
### Pagination
181
+
182
+
By default, `skills/list` and `resources/directory/read` return every entry in a single response.
183
+
When `page_size` is set, each response contains at most that many entries, and a response that is
184
+
followed by another page carries a `nextCursor`. The client passes it back as `cursor`, as for any
185
+
other paginated list in the protocol. `SkillsClient.list_skills()` and `read_directory()` do this
186
+
automatically.
187
+
188
+
A skill entry is never split across pages. The cursor is opaque to clients. A cursor that the
189
+
catalog did not issue is rejected with Invalid params (`-32602`), and so is any cursor when
190
+
`page_size` is not set. A `page_size` below 1 raises `ValueError`.
191
+
192
+
### Invalid skills
193
+
194
+
A skill is invalid when its `SKILL.md` has no frontmatter block, has an unterminated block, cannot
195
+
be parsed, or fails the SEP-2640 requirements for `name` and `description`. A `SKILL.md` placed
196
+
directly in the provider root is also invalid, because the skill has no directory name.
197
+
198
+
* With `strict=False`, an invalid skill is omitted and a warning is logged.
199
+
* With `strict=True`, `load()` raises a single `ValueError` that lists every invalid skill.
200
+
201
+
A skill whose manifest exceeds the 512 files or 16 MiB recommended by SEP-2640 is still served,
202
+
and a warning is logged.
203
+
204
+
### File content
205
+
206
+
`SKILL.md` and other Markdown files are served as `text/markdown`, and YAML files as
207
+
`application/yaml`. The MIME type of any other file is determined with `mimetypes`, with
208
+
`application/octet-stream` as the fallback.
209
+
210
+
File content is returned unchanged. A file whose bytes are valid UTF-8 is returned as text,
211
+
including any byte order mark and line endings; any other file is returned as a base64 blob. In
212
+
both cases the content matches the `digest` and `size` in the skill's manifest. Each read is
213
+
verified against the values recorded at load time, and a file that has changed is reported as an
214
+
internal error (`-32603`).
215
+
216
+
### Providers
217
+
218
+
A `SkillProvider` supplies the files to the catalog. It is a `typing.Protocol`, so an
219
+
implementation does not need to subclass it.
220
+
221
+
```python
222
+
classSkillProvider(Protocol):
223
+
asyncdeflist_files(self) -> Sequence[str]: ...
224
+
asyncdefread_file(self, path: str) -> bytes: ...
225
+
```
226
+
227
+
`list_files()` returns the path of every file, recursively and without directories. `read_file()`
228
+
returns the raw bytes of one file. Paths use `/` as the separator and are relative to the root of
229
+
the provider. A provider can therefore be backed by any storage, such as an archive, a database,
230
+
or object storage.
231
+
232
+
`load()` raises `ValueError` if the provider lists a path that is empty, absolute, contains a `.`
233
+
or `..` segment, is listed more than once, or lies beneath another listed file. This applies
234
+
regardless of `strict`, because it indicates an error in the provider rather than in a skill.
235
+
236
+
`FilesystemSkillProvider(root)` is the provider for a local directory. It serves regular files
237
+
only. Symbolic links and Windows junctions are never followed: they are omitted, with a warning,
238
+
when listing, and refused when read. A skill directory therefore cannot expose files outside
239
+
itself. Blocking file system calls run in a worker thread.
240
+
119
241
## Protocol version and caching
120
242
121
243
In protocol version `2026-07-28` and later, `skills/list`, `skills/get`, and
@@ -134,3 +256,12 @@ your own handler, to decide.
134
256
135
257
If you're looking for "scan this directory and serve whatever's in it," you're looking for a
136
258
provider built on top of `Skills`, not `Skills` itself.
259
+
260
+
[`SkillCatalog`](#serving-skills-from-a-directory) is such a provider. It has the following
261
+
limitations:
262
+
263
+
* It does not include a YAML parser. The `parse_frontmatter` argument supplies one.
264
+
* The catalog is not reloaded. To publish changes, create a new catalog.
265
+
* Empty directories are not served by directory reads.
266
+
* Pagination is opt-in through `page_size`. Without it, `skills/list` and directory reads return
0 commit comments