Skills Extension - ChristianPresley/zig-sdk GitHub Wiki
The Skills extension lets a server publish skills together with its tools, resources and prompts1. A skill is a directory of files with a SKILL.md at its root2. The server serves each file as a resource, and the client reads the files with resources/read. The extension adds the methods skills/list, skills/get and resources/directory/read. The extension is optional. The server enables it with the skills option.
Applies to: MCP 2026-07-28, extension io.modelcontextprotocol/skills (SEP-26403), zig-sdk 0.2.0.
var server = try mcp.Server.init(gpa, io, .{
.info = .{ .name = "docs", .version = "0.1.0" },
.skills = .{},
});
try server.addSkill(.{
.name = "pdf-processing",
.skill_md = @embedFile("skills/pdf-processing/SKILL.md"),
.files = &.{
.{ .path = "templates/invoice.md", .content = @embedFile("skills/pdf-processing/templates/invoice.md") },
},
});The server then advertises the extension under capabilities.extensions in the server/discover result4. The option directory_read is true by default, and the settings object is then {"directoryRead": true}. With .skills = .{ .directory_read = false }, the settings object is empty and the server does not answer resources/directory/read. The server also declares the resources capability, because the extension needs it.
The extension defines no client declaration. Thus the server does not gate the methods on the client capabilities. A client sends the methods only after it saw the declaration of the server. Without the skills option, each method of the extension gives error -32601, and addSkill gives error.ExtensionNotEnabled.
addSkill takes the files of one skill in memory. The server copies all bytes.
| Field | Meaning |
|---|---|
scheme |
The URI scheme. The default is skill. |
prefix |
The organizational prefix of the skill path, for example acme/billing. The default is empty. |
name |
The name of the skill directory. |
skill_md |
The bytes of SKILL.md. |
files |
The other files. Each file has a relative path, the content and an optional mime_type. Without a MIME type, the server selects one from the file extension. |
The skill URI is scheme://prefix/name/SKILL.md, or scheme://name/SKILL.md with an empty prefix. A file has the URI scheme://prefix/name/path. The first segment of the URI is the authority, so it cannot contain : or @.
-
addSkillchecksnameagainst the name rules of the Agent Skills specification (error.InvalidSkillName). - It checks the scheme and the prefix (
error.InvalidSkillPath). - It refuses a second skill with the same URI (
error.DuplicateSkill). - It parses the frontmatter of
SKILL.mdinto a JSON object. - It checks that
nameanddescriptionare strings, and thatdescriptionhas 1 to 1024 bytes. - It checks that the frontmatter
nameis equal toname(error.NameMismatch). - It checks the number of files (
error.TooManySkillFiles). - It checks each file path (
error.InvalidFilePath). A file cannot have the pathSKILL.md. - It checks the sum of the file sizes (
error.SkillTooLarge). - It computes the SHA-256 digest and the size of each file.
- It checks the nested skills (
error.IncompleteSkill). - It registers each new file as a resource.
The frontmatter errors are error.MissingFrontmatter, error.UnsupportedFrontmatter and error.InvalidFrontmatter. When a check fails, the server registers no file of the skill. The last step can fail with error.DuplicateName when the application registered a resource at the URI of a skill file. The files that the server registered before that error stay as resources.
A skill can contain other skills in subdirectories. The file list of the outer skill must also list the files of the nested skill. Register the nested skill with its own addSkill call and a prefix that is the full path of the outer skill, for example .prefix = "tool", .name = "inner". The two skills share the files, and the server registers each shared file one time.
The check works in both orders of registration. A file of the nested skill that the outer skill does not list, or lists with a different digest, gives error.IncompleteSkill. The same URI with different bytes gives error.ConflictingSkillFile.
addDynamicSkill registers a skill whose content the application generates. It takes scheme, prefix, name and skill_md, and no files. Its entry has "resources": "dynamic", and the server takes the frontmatter of the entry from skill_md.
The server registers no resource for a dynamic skill, also not SKILL.md. The application serves SKILL.md and the other files with its own resources or resource templates. The server applies no limits and no nesting checks to a dynamic skill. resources/directory/read does not show the files of a dynamic skill.
The frontmatter is YAML. The SDK has its own parser for a safe subset of YAML. The server and the client use the same parser. Thus both sides get the same JSON object from the same text.
| Accepted | Rejected |
|---|---|
| Block maps and block sequences | Anchors, aliases, tags, directives and the merge key <<
|
| Plain scalars, also on more lines with a deeper indent | Flow collections with content |
| Single-quoted and double-quoted scalars on one line | Quoted keys, complex keys and duplicate keys |
Literal (|) and folded (>) block scalars, with - or +
|
Indentation indicators on block scalars, such as |2
|
Comments, [] and {}
|
Tabs in the indentation, and maps in sequence items |
Integers with 0x or 0o
|
Plain scalars that YAML 1.1 reads differently from YAML 1.2 |
Plain scalars resolve with the core schema of YAML 1.2. The parser rejects the words y, n, yes, no, on and off in each case, and =. It also rejects numbers with a leading zero, base-60 numbers, .inf, .nan, dates and times. YAML 1.1 parsers read them as other types. Quote such values in the frontmatter. The parser also rejects a nesting deeper than 16 levels.
| Method | Result |
|---|---|
skills/list |
The entries of all skills, with pagination. Each entry is complete and never splits across pages. The result has ttlMs and cacheScope from the server option cache.lists. |
skills/get |
The entry of the skill with the SKILL.md URI params.uri. The result has ttlMs and cacheScope from the server option cache.reads. An unknown URI gives error -32602. |
resources/directory/read |
The direct children of a directory, sorted by URI, with pagination. |
With the default cache options, the results have "ttlMs": 0 and "cacheScope": "private".
An entry has three fields:
-
uri: the URI ofSKILL.md. -
frontmatter: the frontmatter as a JSON object. The object keeps every field of the author. -
resources: the complete file list withuri,digest(sha256:and 64 hexadecimal digits) andsize, or the string"dynamic".
In a directory result, a file has uri, name, mimeType and size. A subdirectory has uri, name and the MIME type inode/directory. The result has no ttlMs and no cacheScope.
A directory URI has no slash at the end. The skill root, each subdirectory and each level of the prefix are directories. A URI that is not a directory gives error -32602. Without directory_read, the method gives error -32601.
Files that are valid UTF-8 go out as text. Other files go out as a base64 blob. The digest covers the raw bytes in both cases.
The client has these methods:
| Method | Effect |
|---|---|
listSkills(arena, cursor, options) |
Sends skills/list. |
getSkill(arena, uri, options) |
Sends skills/get. |
readDirectory(arena, uri, cursor, options) |
Sends resources/directory/read. Send it only when mcp.protocol.skills.serverSupportsDirectoryRead is true for the capabilities of the server. The client does not check this. |
readSkillFile(arena, entry, uri, options) |
Reads one file under a held entry, verifies it and returns the raw bytes. |
The extension gives the host a set of rules for the content of a skill1. readSkillFile applies the rules of the extension that concern one read:
- It refuses an entry that
mcp.protocol.skills.validateEntryrejects. - It refuses a URI that the file list of the entry does not list (
error.UnlistedFile). It does this before it sends a request. - It compares the size of the content with the entry (
error.SizeMismatch). - It compares the SHA-256 digest of the content with the entry (
error.DigestMismatch). - For
SKILL.md, it compares the frontmatter field by field with the entry (error.FrontmatterMismatch).
After a failure, do not use the content. Get the entry again with getSkill and ask the user again for approval. For a dynamic skill, the client can only check that the URI is inside the skill (error.OutsideSkill) and that the frontmatter agrees.
A directory read is a live observation. It does not extend the file list of a held entry.
The extension gives the host more rules. The application must apply them:
- Show the origin server of each skill to the model and to the user.
- Record skills, approvals and cache entries under the server identity and the skill URI together.
- Bind an approval to the file list of the entry. A changed file list cancels the approval.
- Do not read files before the skill needs them.
- Do not obey
allowed-toolswithout the explicit approval of the user. - Treat skill content as data from a source that you do not trust.
| Limit | Default | Effect |
|---|---|---|
limits.skills.max_files |
512 | The maximum files in one skill, SKILL.md included. Above it, addSkill gives error.TooManySkillFiles. |
limits.skills.max_bytes |
16 MiB | The maximum sum of the file sizes of one skill. Above it, addSkill gives error.SkillTooLarge. |
The defaults are the limits of the extension. A host must accept skills up to these limits. On the client, readSkillFile uses the larger of the client limits and the limits of the extension, so a smaller client value has no effect. The client reports both overflows as error.SkillTooLarge. The server applies the limits as the application sets them.
The official conformance suite has no scenarios for the extension. The unit tests of the SDK use the example of the specification and check its sizes and digests.
- MCP-EXT-SKILLS: Skills extension specification.
- AGENT-SKILLS-SPEC: Agent Skills specification.
- MCP-SEP-2640: SEP-2640, the Skills extension.
- MCP-EXT-OVERVIEW: Extensions of the protocol.