Skip to content

feat(skills): add SEP-2640 Skills extension APIs (phase 1: schemas, client ops, server handlers) - #2818

Draft
tobi-oye wants to merge 2 commits into
modelcontextprotocol:mainfrom
tobi-oye:feat/ext-skills-phase-1
Draft

tobi-oye wants to merge 2 commits into
modelcontextprotocol:mainfrom
tobi-oye:feat/ext-skills-phase-1

Conversation

@tobi-oye

@tobi-oye tobi-oye commented Sep 14, 2026

Copy link
Copy Markdown
Member

This PR adds first-class support for the Skills extension (SEP-2640) to the SDK.

A "skill" is a folder of instructions that a server publishes to a client. It has a SKILL.md manifest with YAML frontmatter, plus any number of supporting files. SEP-2640 defines two methods for discovering them — skills/list and skills/get — and a capability, io.modelcontextprotocol/skills, that a server uses to advertise support.

Today the SDK has no typed API for any of this. Anyone serving skills has to hand-write the Zod schemas and register raw custom request handlers. This PR gives all three packages a proper typed surface for it.

The new APIs live behind /ext/skills subpath exports, following the layout the Tasks extension introduced in #2782. No new workspace package was added, and the root barrels of every package are untouched.

What this adds

Package New subpath What you get
core @modelcontextprotocol/core/ext/skills Zod schemas, the TypeScript types inferred from them, the wire constants, and skillsCapabilityOf()
client @modelcontextprotocol/client/ext/skills listSkills(), getSkill(), getSkillsCapability()
server @modelcontextprotocol/server/ext/skills installSkills()

On the server, one call declares the capability and registers both handlers:

const mcpServer = new McpServer({ name: 'skills-demo', version: '1.0.0' }, { capabilities: { resources: {} } });

installSkills(mcpServer.server, {
    skills: [
        {
            uri: 'skill://git-workflow/SKILL.md',
            frontmatter: { name: 'git-workflow', description: 'Conventions for branching and review.' },
            resources: [{ uri: 'skill://git-workflow/SKILL.md', digest: `sha256:${hex}`, size: bytes.byteLength }]
        }
    ]
});

On the client, the two operations are free functions that take the client as their first argument:

if (getSkillsCapability(client) !== undefined) {
    const { skills, nextCursor } = await listSkills(client);
    const { skill } = await getSkill(client, { uri: 'skill://git-workflow/SKILL.md' });
}

listSkills() fetches a single page. To walk all of them, pass the previous result's nextCursor back as params.cursor and stop when a result comes back without one.

Validation

The schemas enforce what the SEP requires, rather than accepting loosely shaped input:

  • A resource digest must be exactly sha256: followed by 64 lowercase hex characters.
  • A skill may carry at most 512 resource entries, the SEP's normative limit.
  • resources accepts either the array of entries or the literal string "dynamic", for servers that generate content per request and cannot publish digests ahead of time.
  • Frontmatter requires name and description, and preserves any further keys the author wrote verbatim.

Capability negotiation

installSkills() publishes io.modelcontextprotocol/skills into ServerCapabilities.extensions. That record already exists in the core schemas, so no core schema change was needed.

On the client side, listSkills() and getSkill() both refuse to send a request unless the server advertised both the skills extension and the resources capability. SEP-2640 requires servers to declare resources alongside the extension, because every skill entry points at resource URIs the host later reads through resources/read. A call against a server missing either one throws SdkError with CapabilityNotSupported, rather than failing later on the wire.

Runtime neutrality

core/ext/skills imports nothing beyond zod/v4 and the existing core schema modules. It pulls in no Node built-ins and no YAML dependency, so the subpath stays safe to bundle for browsers and Cloudflare Workers.

Motivation and Context

Requested in #2798, which lays out a four-phase plan for bringing Skills into the SDK. This PR is phase 1 of that plan.

It is deliberately scoped to the metadata surface only — the schemas, the two discovery methods, and capability negotiation. That is the smallest piece that is useful on its own, and it is small enough to review in one sitting.

The following are explicitly not in this PR, and will follow separately:

  • Filesystem discovery and YAML frontmatter parsing.
  • Verified resource reads, meaning the client checking fetched bytes against the digest, size, and frontmatter in a skill entry.
  • resources/directory/read and the directoryRead: true capability flag.
  • Archive support, which the SEP does not require.

The files a skill points at remain ordinary MCP resources in this phase. Servers register and serve them exactly as they do today.

A note on #2789, which reported that skills results always fail validation

#2789 reports that skills/list and skills/get fail client-side validation even when the server's response is fully spec-conforming, with an error like:

Invalid result for skills/list: resultType: Invalid input: expected "complete"

I traced this before writing any schemas, because the issue asks whether a fix is needed at the codec boundary.

It is not an SDK bug, and no codec change is needed. Here is what actually happens:

  1. On protocol revision 2026-07-28, every result carries a resultType discriminator on the wire.
  2. The era codec validates that field on decode, and then deletes it from the result object before anything else sees it (packages/core-internal/src/wire/rev2026-07-28/codec.ts).
  3. Only then does the caller-supplied result schema run, against an object that no longer has the field.

So a result schema that re-declares resultType can never match. The client removes the field that the schema is about to require. This is intentional and documented in packages/core/src/schemas.ts, where the neutral ResultSchema explains why it models no resultType member.

The schemas in the original prototype re-declared it, which is what produced the reported failure. The schemas in this PR extend ResultSchema and PaginatedResultSchema, so they inherit the correct behaviour and the problem does not arise.

No validation was weakened to achieve this. The codec still hard-requires resultType on the wire and still rejects a response that omits it. The change is only in where the field is modelled. A client test in this PR drives a real, spec-conforming resultType: "complete" response through the live decode path to pin the behaviour against regressions.

How Has This Been Tested?

38 new tests were added: 16 in core, 13 in server, 9 in client. They cover only the new public behaviour:

  • Capability negotiation, in both directions — the server advertising the extension, and the client refusing to call when it is absent or when resources is missing.
  • Pagination, including a cursor round trip and the guarantee that a skill entry is never split across pages.
  • Successful skills/list and skills/get calls.
  • An unknown skill URI, and a skill root URI that is not the SKILL.md URI. Both must return -32602, as the SEP requires.
  • An out-of-range or non-numeric pagination cursor.
  • A server result that violates the skill schema, which the client must reject.
  • The skills/list, skills/get, resources/directory/read always fail: codec strips resultType before re-validating it #2789 regression, driven end to end through the real decode path.

Everything below was run locally and passed:

Check Result
tsgo --noEmit across core, client, server clean
eslint and prettier --check across core, client, server clean
tsdown build for all three packages clean; @modelcontextprotocol/core/ext/skills correctly stays an external import rather than being inlined
core test suite 18 passed
server test suite 495 passed
client test suite 896 passed
node scripts/smoke-dist-types.mjs clean

The conformance suite in modelcontextprotocol/conformance#330 is the acceptance target named in #2798.

Breaking Changes

None.

Every new API is additive and reachable only through a new subpath export. No existing export, type, or runtime behaviour was modified. The root barrels of core, client, and server are byte-identical to main.

Changes to existing files are limited to 50 lines across 8 configuration files: package.json exports and typesVersions, tsdown.config.ts build entries, and tsconfig.json path mappings.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Attribution

This work adapts the Apache-2.0 @olaservo/ext-skills prototype (modelcontextprotocol/ext-skills#71), as #2798 suggested. Where the prototype and the final SEP disagree, this PR follows the SEP. Attribution notices are in the module headers of all three ext/skills entry points.

On documentation

This PR adds JSDoc on every exported symbol, usage examples on the three main entry points, and a changeset describing the full surface. It does not add a page under docs/. That felt premature while the extension surface is still landing in phases. Happy to add one in this PR if you would prefer it now.

Two open questions for maintainers

Both of these touch code owned outside this extension, so I left both alone rather than deciding unilaterally.

1. Where should ttlMs and cacheScope come from?

SEP-2640 makes skills results CacheableResult extenders on revision 2026-07-28, and the draft leg of the conformance suite expects those two fields on skills/list.

The SDK already has machinery for this, but CACHEABLE_RESULT_METHODS in packages/core-internal/src/shared/resultCacheHints.ts is a deliberately closed list of six core operations. Two existing tests assert it is "closed at exactly six operations".

Rather than change that invariant from an extension PR, installSkills() emits the fields itself through an optional cacheHint option. This is the same approach the conformance fixture in #2797 takes.

If you would rather fold skills into core's cacheable set, that is a two-line change plus updates to those two tests. Happy to do it either way — it seemed like a core-owned decision.

2. Where should the Node-specific skills code live?

#2798 names @modelcontextprotocol/node/ext/skills as the home for filesystem discovery and YAML parsing in a later phase.

That package currently lives under packages/middleware/, and CLAUDE.md states that middleware packages "should not add new MCP functionality". Filesystem skill discovery is new functionality, so it does not obviously belong there.

Guidance on the right home would be useful before I open that follow-up PR.

🤖 Generated with Claude Code

…, server handlers)

Adds the first phase of first-class Skills extension support behind new
`/ext/skills` subpath exports. Root barrels are unchanged.

- `@modelcontextprotocol/core/ext/skills` — shared, runtime-neutral schemas,
  inferred types, wire constants and `skillsCapabilityOf()`.
- `@modelcontextprotocol/client/ext/skills` — `listSkills()`, `getSkill()` and
  `getSkillsCapability()`, gated on the server advertising both
  `io.modelcontextprotocol/skills` and `resources`.
- `@modelcontextprotocol/server/ext/skills` — `installSkills()` declares the
  extension capability and serves `skills/list` / `skills/get` from
  caller-provided skill definitions.

The skills result schemas deliberately carry no `resultType` member: the
2026-07-28 era codec validates that discriminator on decode and consumes it
before any caller-supplied result schema runs, so a schema that re-declares it
can never match (modelcontextprotocol#2789). Regression tests pin this end to end.

Metadata surface only — filesystem discovery, digest-verified resource reads
and `resources/directory/read` follow separately.

Refs modelcontextprotocol#2798

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@pkg-pr-new

pkg-pr-new Bot commented Sep 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2818

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2818

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2818

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2818

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2818

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2818

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2818

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2818

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2818

commit: b009106

@tobi-oye tobi-oye changed the title feat(skills): add SEP-2640 Skills extension APIs (schemas, client ops… feat(skills): add SEP-2640 Skills extension APIs (phase 1: schemas, client ops, server handlers) Sep 14, 2026
`packageTopologyPins` asserts each published package's export-map keys
exactly, so the new `./ext/skills` subpath on core, client and server trips
it by design. Per docs/behavior-surface-pins.md the pin is updated in the
same PR rather than loosened, with a note on each entry recording why the
subpath is public and why it stays off the root barrel.

No behavior change: this updates the expectation only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b009106

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/client Minor
@modelcontextprotocol/server Minor
@modelcontextprotocol/core Minor
@modelcontextprotocol/core-internal Patch
@modelcontextprotocol/server-legacy Minor
@modelcontextprotocol/codemod Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

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