Skip to content

[Schema][Server] Add Skills extension (io.modelcontextprotocol/skills) support#372

Draft
wachterjohannes wants to merge 3 commits into
modelcontextprotocol:mainfrom
wachterjohannes:feature/skills-extension
Draft

[Schema][Server] Add Skills extension (io.modelcontextprotocol/skills) support#372
wachterjohannes wants to merge 3 commits into
modelcontextprotocol:mainfrom
wachterjohannes:feature/skills-extension

Conversation

@wachterjohannes

@wachterjohannes wachterjohannes commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the MCP Skills extension (SEP-2640): a way for servers to ship skills — multi-step workflow instructions that tell an agent how to orchestrate tools to reach a goal. Skills ride on the existing Resources primitive, mirroring the MCP Apps extension (io.modelcontextprotocol/ui). This PR adds no new protocol methods (the SEP's one optional method, resources/directory/read, is a planned follow-up — see below).

A server can expose a whole directory of skills in one line:

$server = Server::builder()
    ->setServerInfo('My Server', '1.0.0')
    ->addSkillsFromDirectory(__DIR__.'/skills')
    ->build();

This registers each SKILL.md (and its supporting files) as skill:// resources, derives the resource metadata from the YAML frontmatter, and serves a skill://index.json discovery index whose entries mirror each skill's frontmatter verbatim and carry a SHA-256 digest of the SKILL.md.

Optionally, skills can also be served as one-shot archives:

->addSkillsFromDirectory(__DIR__.'/skills', archiveFormats: ['application/gzip'])

What's included

  • Schema/Extension/Skills/McpSkills — extension marker (EXTENSION_ID, MIME_TYPE, URI_SCHEME, ENTRY_POINT, DISCOVERY_URI, META_PREFIX).
  • DTOsSkillMetadata, SkillDiscoveryEntry (frontmatter + optional url/digest + optional archives), SkillDiscoveryIndex, SkillArchive — readonly, fromArray, omit-null jsonSerialize.
  • Server/Skill/FrontmatterParser — splits SKILL.md into YAML frontmatter + body (handles BOM/CRLF; rejects non-mapping frontmatter).
  • Server/Skill/SkillProvider — walks a directory, registers each skill and its supporting files as skill:// resources, enforces the spec's name↔final-path-segment rule, sanitizes schema-valid resource names (URIs keep real paths), guesses MIME types, guards against path traversal, mirrors frontmatter + digest into the index, and (when requested) serves packed archives.
  • Server/Skill/SkillArchiver — deterministic ustar + gzip writer (no temp files, zeroed mtime/owner; SKILL.md at the archive root) for application/gzip.
  • Server/Builder::addSkillsFromDirectory() — convenience that auto-enables the extension; takes archiveFormats.

Notable fix

ServerCapabilities::jsonSerialize() only cast the outer extensions map, so an extension with an empty payload (like Skills) serialized to [] instead of {}. The Apps extension never hit this because its payload is non-empty. Empty inner payloads are now coerced to {}.

Dependency

Adds symfony/yaml (^5.4 || ^6.4 || ^7.3 || ^8.0) for frontmatter parsing — the feature is non-functional without it.

Tests & docs

  • Unit tests for the extension, DTOs, FrontmatterParser, SkillProvider, and SkillArchiver (PharData round-trip, determinism, the digest-matches-served-bytes invariant), plus a serialization regression test for the {} fix.
  • Inspector stdio snapshot test (resources/list, resources/read of a SKILL.md, a supporting file, and the discovery index).
  • Example server at examples/server/skills/; docs in docs/extensions.md.

Verified: phpstan (level 6) clean, php-cs-fixer clean, full unit suite green (812 tests), inspector skills test green.

Updates tracking the SEP

The SEP redesigned the discovery index after this PR opened; it has been brought in line:

  • Index format — entries now mirror SKILL.md frontmatter verbatim and carry a digest; dropped the $schema field and the type/SkillType enum (the mcp-resource-template type was removed from the index in the redesign).
  • Archives — added optional archive serving (application/gzip), listed under each entry's archives.

Deferred / follow-up

  • resources/directory/read — the SEP's one optional method (scoped directory listing) plus the directoryRead capability flag.
  • ZIP archives (application/zip) — the SEP requires at least one of gzip-tar or zip; gzip-tar is implemented.

…) support

Implement the MCP Skills extension (SEP-2640): serve skills — multi-step
workflow instructions — through the existing Resources primitive with zero
protocol changes, mirroring the MCP Apps extension pattern.

- Add McpSkills extension marker and discovery/metadata DTOs
  (SkillType, SkillDiscoveryEntry, SkillDiscoveryIndex, SkillMetadata).
- Add FrontmatterParser and SkillProvider to expose a directory of skills as
  skill:// resources, deriving name/description from SKILL.md frontmatter,
  enforcing the name<->final-path-segment rule, and serving skill://index.json.
- Add Builder::addSkillsFromDirectory() convenience that auto-enables the
  extension.
- Fix ServerCapabilities::jsonSerialize() so an empty extension payload
  serializes to {} instead of [].
- Add symfony/yaml dependency for frontmatter parsing.
- Add example server, unit tests, and inspector snapshot tests.
@wachterjohannes

Copy link
Copy Markdown
Contributor Author

For cross-reference, the downstream pieces are now open:

Together: this PR is the transport, symfony/ai#2132 is the content, modelcontextprotocol/experimental-ext-skills#95 is the write-up.

…2640 format

The index format was redesigned upstream (decoupled from the agentskills.io
.well-known schema): entries now mirror the SKILL.md frontmatter verbatim and
carry a content digest, archives become an optional additional serving form,
and the type/$schema fields are dropped.

 * SkillDiscoveryIndex: drop $schema/SCHEMA_URL; serialize { skills: [...] }
 * SkillDiscoveryEntry: { frontmatter, url?, digest?, archives? } with the
   invariant that digest accompanies url and an entry has url, archives, or both
 * Add SkillArchive DTO (url, mimeType, digest) for the archives form
 * Remove the now-unused SkillType enum
 * SkillProvider: emit verbatim frontmatter + sha256 digest of SKILL.md
 * Update unit tests, the discovery-index snapshot, and docs

Archive emission and resources/directory/read are left as follow-ups.
Add optional archive serving to the Skills extension: passing archiveFormats
to addSkillsFromDirectory() packs each skill into one resource per format
(e.g. skill://<skill-path>.tar.gz) and lists it under the index entry's
archives, so a host can fetch a whole multi-file skill in one resources/read.

 * SkillArchiver: deterministic ustar + gzip writer (no temp files, zeroed
   mtime/owner) with SKILL.md at the archive root per the SEP layout
 * SkillProvider: build each archive once so its digest matches the served
   blob; emit SkillArchive index entries; default stays archive-free
 * Builder::addSkillsFromDirectory(): archiveFormats parameter
 * Tests for the writer (PharData round-trip, determinism), archive
   registration, and the digest-matches-served-bytes invariant; docs

Currently emits application/gzip; zip is left as a follow-up.
@wachterjohannes

Copy link
Copy Markdown
Contributor Author

Updated to track the recent SEP-2640 redesign and to round out the index entries:

  • Index format realigned (2fb9722): entries now mirror the SKILL.md frontmatter verbatim and carry a SHA-256 digest; dropped the $schema field and the type/SkillType enum (the mcp-resource-template type was removed from the index in the redesign).
  • Archives (634f81e): skills can now also be served as one-shot packed resources via addSkillsFromDirectory(..., archiveFormats: ['application/gzip']) — a deterministic .tar.gz per skill, listed under the entry's archives, with the index digest matching the served bytes.

All green: phpstan (level 6), php-cs-fixer, 812 unit tests, inspector snapshot. Description updated accordingly.

Remaining SEP surface, as a follow-up: the optional resources/directory/read method (+ directoryRead capability), and ZIP archives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant