Skip to content

Serve Agent Skills over resources per SEP-2640 - #570

Closed
anirudhmungre wants to merge 3 commits into
modelcontextprotocol:mainfrom
anirudhmungre:feat/sep-2640-skills-extension
Closed

anirudhmungre wants to merge 3 commits into
modelcontextprotocol:mainfrom
anirudhmungre:feat/sep-2640-skills-extension

Conversation

@anirudhmungre

Copy link
Copy Markdown

Implements the server side of SEP-2640 (Skills Extension, Extensions Track, Final): Agent Skills served over the existing Resources primitive. Opened as a draft — I'd rather agree the shape before filling in the remaining pieces listed at the bottom.

Closes #568.

What a server gets

capabilities = MCP::Server::Capabilities.new
capabilities.support_resources
capabilities.support_extensions(MCP::Skills.capability(directory_read: true))

server = MCP::Server.new(
  name: "billing_server",
  capabilities: capabilities,
  skills: [
    MCP::Skill.new(
      uri: MCP::Skills.uri_for("acme/billing/refunds"),
      frontmatter: { "name" => "refunds", "description" => "Process customer refund requests" },
      resources: [{ uri: MCP::Skills.uri_for("acme/billing/refunds"), digest: MCP::Skills.digest(skill_md), size: skill_md.bytesize }],
    ),
  ],
)

That answers skills/list, skills/get and resources/directory/read with no further wiring; skill content is read through the ordinary resources_read_handler.

  • MCP::Skill is the entry shape both methods share, and enforces the extension's structural rules at construction: the URI addresses the skill's SKILL.md, the frontmatter carries name and description, that name equals the final skill-path segment, and the manifest is complete, duplicate-free and confined to the skill's root. resources: MCP::Skill::DYNAMIC covers generated skills. The per-skill limits (512 files, 16 MiB) are reported through #limit_violations and warned about at registration rather than enforced, since SEP-2640 leaves loading an oversized skill to the host.
  • MCP::Skills carries the wire vocabulary and negotiation helpers, mirroring MCP::Apps.
  • skills_list_handler / skills_get_handler / resources_directory_read_handler replace the defaults for catalogs a server cannot enumerate, and take server_context: on the same opt-in basis as resources_list_handler.

Decisions worth a look

  • skills/list joins CACHEABLE_RESULT_METHODS; skills/get does not. The SEP requires the SEP-2549 hints on the listing at 2026-07-28 and explicitly leaves them open for skills/get, so that result stays hint-free rather than guessing.
  • Declaring the extension without the resources capability raises at construction. The pairing is a MUST, and skill files are read through resources/read — the alternative is a server that lists skills nobody can read. It's the one place I've added a hard failure; happy to downgrade it to a warning.
  • require_extension! tolerates either key form. An extension identifier is a String on the wire and often a Symbol when written in Ruby, so dig alone isn't enough. Same tolerance MCP::Apps.client_supports? already applies.
  • Gating is consistent with every other capability-gated method, which means skills/list on a server that has not declared the extension now answers the capability error rather than -32601. This matches how prompts/list behaves without the prompts capability. A conforming client only calls these after seeing the declaration, so I don't think it's observable in practice — but it is a behavior change on an existing surface and worth flagging.
  • resources/directory/read derives children from the registered manifests, which is exact for non-dynamic skills and saves every user reimplementing it. mimeType is inferred from the file extension through a small table and omitted when unknown; the handler block is there for anything the manifests don't describe.

Deliberately not in this PR

Staged the way go-sdk and typescript-sdk split theirs, and happy to fold any of it in here instead:

  • Client wrappers (list_skills, get_skill, read_directory) — the SEP recommends them, and each wants the ListXResult struct and auto-paginating accessor the other client list methods have.
  • A filesystem provider that builds a MCP::Skill from a directory by reading frontmatter and computing digests — the ergonomic entry point the SEP sketches, and the thing that makes this usable without hand-assembling manifests (go-sdk split this out the same way).
  • ROADMAP.md, which names the unimplemented extensions — left alone rather than edited on your behalf.

Verification

  • rake test — 1927 runs, 5257 assertions, 0 failures. 40 of those assertions are new, across test/mcp/skill_test.rb, test/mcp/skills_test.rb and test/mcp/server_skills_test.rb.
  • rake rubocop — 168 files, no offenses.
  • rake conformance:test did not run here: the @modelcontextprotocol/conformance package needs Node >= 22 and this machine has 20 (fs.globSync is missing). Nothing in the diff is reachable without the extension declaration, which no conformance scenario makes, but I haven't confirmed that locally — worth a CI eye.

`MCP::Skill` is the entry shape `skills/list` and `skills/get` share: the verbatim
`SKILL.md` frontmatter plus the complete file manifest with per-file digests and sizes.
It enforces the extension's structural rules at construction — the URI addresses the
skill's `SKILL.md`, the frontmatter carries `name` and `description`, that name equals the
final skill-path segment, and the manifest is complete, duplicate-free and confined to the
skill's root — so a malformed entry fails where it is declared rather than on the wire.

The per-skill limits are reported rather than enforced: `#limit_violations` names what a
skill exceeds, because SEP-2640 leaves loading an oversized skill to the host.

`MCP::Skills` carries the wire vocabulary and the negotiation helpers, mirroring
`MCP::Apps`: the `capabilities.extensions` fragment, URI construction and relative-reference
resolution against a skill's root, and the `sha256:` digest form every manifest entry takes.
A server registers skills through the new `skills:` kwarg and answers all three methods with
no further wiring: `skills/list` paginates the registered entries and carries the SEP-2549
cache hints on the modern wire, `skills/get` resolves one entry by URI, and
`resources/directory/read` derives a directory's direct children from the registered
manifests, marking subdirectories `inode/directory`. `skills/get` stays out of
`CACHEABLE_RESULT_METHODS`: SEP-2640 leaves the hints on that result open rather than
requiring them.

Unknown URIs answer `-32602` with the URI in the error data, the code and shape
`resources/read` already uses for an unknown resource.

Negotiation runs through `capabilities.extensions`, so the methods are gated on the
declaration rather than a top-level capability, and `resources/directory/read` additionally
on its `directoryRead` setting. `require_extension!` tolerates either key form, since an
extension identifier is a String on the wire and often a Symbol in Ruby. Declaring the
extension without the `resources` capability is refused at construction — that pairing is a
MUST, and skill files are read through `resources/read`, so the alternative is a server that
lists skills nobody can read.

The three handler blocks replace the defaults for catalogs this server cannot enumerate,
dynamically generated skills among them, and take `server_context:` on the same opt-in basis
as `resources_list_handler`.
Covers the server-side declaration, why an entry is a complete manifest rather than a
summary, the dynamic marker, the per-skill limits, the handler blocks for unenumerable
catalogs, and the directory-read gate.
@anirudhmungre
anirudhmungre marked this pull request as ready for review September 22, 2026 18:06
@anirudhmungre
anirudhmungre marked this pull request as draft September 22, 2026 18:07
@anirudhmungre

Copy link
Copy Markdown
Author

@koic saw you picked my ticket up just recently - any thoughts on the shape here?

@anirudhmungre
anirudhmungre marked this pull request as ready for review September 22, 2026 18:09
@koic

koic commented Sep 22, 2026

Copy link
Copy Markdown
Member

Thank you for the detailed PR, and for writing up the decisions behind it. I read that section closely, and it is useful context.

I am going to close this PR, though, and keep #568 open. I would like to take the Skills extension forward myself and land it as a planned whole: the server side, the filesystem provider the SEP sketches for SDKs, and the client wrappers, together with the release planning around it. I also want to line up the shape with the MCP specification, the other SDKs' implementations (which are still open PRs at this point), and a few other considerations, so that the API does not have to move after it ships. I will do that in due course and track it on the issue.

None of this reflects on the quality of the work. The test suite, the linter, and the conformance suite pass locally on this branch, and the decisions you wrote down are exactly the kind of context that helps. I appreciate you taking the time.

@koic koic closed this Sep 22, 2026
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.

SEP-2640: Skills Extension

2 participants