Skip to content

skills: add SEP-2640 protocol support - #1238

Open
sambhav wants to merge 13 commits into
modelcontextprotocol:mainfrom
sambhav:skills-sep-2640
Open

skills: add SEP-2640 protocol support#1238
sambhav wants to merge 13 commits into
modelcontextprotocol:mainfrom
sambhav:skills-sep-2640

Conversation

@sambhav

@sambhav sambhav commented Sep 4, 2026

Copy link
Copy Markdown
Member

Summary

Add opt-in Go support for the Skills extension: skills/list, skills/get, optional resources/directory/read, manifests, pagination, and content verification.

This is PR 1 of 2, independently usable. Filesystem discovery and caching remain in draft #1240, which needs to adopt these interfaces when rebased.

Limits and validation

The spec's 512-file / 16-MiB values are an interoperability baseline: hosts must support at least that much and may support more; servers should stay within it for broad compatibility. They are not mandatory rejection thresholds.

Peter Alexander's clarification (Anthropic): In our “Size limits and dynamic content” thread in skills-over-mcp-wg, Peter said:

Because of this, I think SDK validation should be optional.

He explained that clients may support larger skills, servers should stay within the baseline for broad compatibility, and dynamic-content budgets should be enforced by applications according to their own disk, context, and network constraints.

  • Client.Limits and ServerOptions.Limits are plain skills.Limits values. The zero value imposes no count or size caps. Positive fields opt into exact caps; negative values are rejected before registration or client requests.
  • skills.BaselineLimits() returns a fresh value containing the spec's baseline. Applications can select it explicitly or supply numeric values to pin their own policy. This replaces the earlier implicit defaults, limit pointers, and exported default constants.
  • Client/server asymmetry is intentional. Both sides expose configurable limits, but these are independent local policies. The capability schema defines no advertised or negotiated limits. Raising server limits does not raise a client's chosen caps; a client can independently raise or omit its caps.
  • ValidateSkill performs structural validation only. ValidateSkillWithLimits additionally applies the supplied caps.
  • Dynamic skills have no countable manifest. Applications manage download, storage, and context budgets; the SDK does not fetch content or maintain cumulative read accounting.
  • Structural validation remains mandatory: URI/name agreement, complete static manifests including SKILL.md, required nonnegative sizes, digests, and frontmatter shape.

API and behavior

  • Register skills.AddMethods(client) before connecting, then use &skills.Client{Session: session}. List, Get, and All discover entries; ReadDirectory and DirectoryEntries provide optional browsing.
  • Register skills.AddHandlers(server, handlers, options) and serve bytes through ordinary resource or resource-template handlers. Registration advertises the extension; resource registration advertises the required base capability.
  • Handlers own pagination. Slice helpers sort by URI without modifying inputs. Iterators preserve parameters, capture configuration, and stop on errors, repeated cursors, or early termination.
  • Unknown skill or directory handlers return (nil, nil), producing -32602. Ordinary handler failures and invalid results produce -32603. Explicit JSON-RPC codes, messages, and data survive wrapping.
  • Resource decoding rejects omitted/null sizes and accepts escaped JSON representations of "dynamic". Name validation supports Unicode and counts characters.
  • VerifyResource checks membership, byte length, and digest. VerifySkillMD also compares every frontmatter field. JSON numbers use json.Number; equivalent decimal representations compare without rounding large integers. Dynamic frontmatter must still match before returning ErrDynamicResources.
  • On protocol 2026-07-28 and later, list/get include required cache hints and all three methods return resultType: "complete". Zero TTL and public scope match repository defaults. Earlier protocols are supported as a compatibility backport with those fields omitted.

Entries remain scoped to their originating server. Applications own approvals, caches, and execution policy; directory results do not expand a held manifest.

Go conventions and documentation

The package uses concrete types, context-first handlers, mcp.ParamsBase / mcp.ResultBase, and iter.Seq2. It builds on existing custom-method and resource APIs, following the applicable patterns in MCP Apps, the repository's custom-method example, and auth/extauth. The only added core API is Server.AddExtension; dependency types are not exposed.

The list-validation duplication is already consolidated: validateListResponse was removed, and client and server both use validateListResult in skills/validation.go. The server handler wraps it with output copying, nil-slice normalization, protocol envelope preparation, and error translation.

Client/server docs include the executable example. Sources and generated files are updated together.

Validation

Validated locally against conformance #330's current branch, chore/sep-2640-yaml, checked at 73ac2c4d0f40505fbd597c23399aebd7545900ed:

Protocol / transport Enumeration Manifest Directory Total
2025-11-25 / stateful 30 pass, 2 version skips 6 pass 7 pass 43 pass
2026-07-28 / stateless 32 pass 6 pass 7 pass 45 pass

Zero failures or warnings. Skills conformance runs locally through the existing scripts/server-conformance.sh; the custom Skills script has been removed. CI integration will follow after conformance #330 merges. No fork URL or revision is committed in the runner or CI configuration.

The runner accepts --conformance_repo <path|url> and --conformance_ref <branch|commit|tag>. An explicit revision uses a temporary clone, preserving the source checkout. The default remains the latest npm release and the existing conformance server. --server, --stateless, and arguments after -- select the fixture, transport, and conformance options.

To reproduce the modern enumeration run locally:

./scripts/server-conformance.sh \
  --conformance_repo https://github.com/panyam/mcpconformance.git \
  --conformance_ref chore/sep-2640-yaml \
  --server ./conformance/skills-server \
  --stateless \
  --result_dir /tmp/skills-conformance-results \
  -- --scenario sep-2640-skills-enumeration --spec-version 2026-07-28 --force

Repeat with sep-2640-skills-manifest and sep-2640-skills-directory, using separate result directories. For the legacy runs, omit --stateless and use --spec-version 2025-11-25.

Runner checks also pass for branch, commit, tag, repository URL, and local checkout inputs; paths and arguments containing spaces; cleanup; and failure exit-code propagation.

Local checks pass: go test ./..., go test -race ./skills, go vet ./..., go build ./..., and documentation generation. Regression tests cover opt-in caps, unlimited totals, dynamic manifests, configuration ownership, required sizes, Unicode names, escaped markers, numeric fidelity, and error codes/data on both protocol versions.

@sambhav
sambhav force-pushed the skills-sep-2640 branch 3 times, most recently from adc1ddf to ca89d93 Compare September 4, 2026 17:02
@panyam

panyam commented Sep 4, 2026

Copy link
Copy Markdown

Woooot great to see this @sambhav. I ran the SEP-2640 conformance scenarios against this branch (conformance PR 330, the traceability extraction and server scenarios Il share soon for the skills extension). 41 checks, 0 failures.

Scenario Result
sep-2640-skills-enumeration 30/30
sep-2640-skills-manifest 4/4, 2 untestable
sep-2640-skills-directory 7/7

Repro, pointing a minimal skills.AddDirectory server at any skills tree:

node dist/index.js server --url http://localhost:18299/ \
  --scenario sep-2640-skills-enumeration --spec-version 2025-11-25 --force

Got three tiny notes (none blocking):

1. ttlMs and cacheScope are emitted on protocol 2025-11-25. In the SEP - "In protocol versions 2026-07-28 and later, the result also carries the base protocol's list-caching attributes". ttlMs does not appear in the 2025-11-25 schema at all, so this is emitting a field that is not defined in the negotiated version. We might want a version guard if it was not deliberate? If it was, this is a nice data point. I pointed out in PR 138 that it dropped the condition and was wondering if it was intention so looks like two efforts came to this point independently. So the condition itself may need to be removed instead of changing impls.

2. Two untestable SHOULD rows instead of passing. sep-2640-skillmd-metadata-name and -description check that the SKILL.md resource carries name and description from frontmatter. Since the dir utility serves through resource-template dispatch, SKILL.md is not in resources/list, so the metadata is not observable. I dont think this is a bug and I also do not think it needs changing, but figured Id flag it. A server registering SKILL.md as a listed resource does/would exercise those two.

3. Possible doc gap dueto needing flags against this branch. The runner defaults to the draft stateless wire. here it asserts MCP-Protocol-Version: 2026-07-28 with no handshake, so the server correctly refuses with -32022. --spec-version 2025-11-25 --force and selects the stateful wire and overrides the extension-applicability skip. The scenarios themselves are version-portable, they just do not advertise that. Just wanted to call this out.

@sambhav
sambhav force-pushed the skills-sep-2640 branch 2 times, most recently from 60744e4 to 36000a2 Compare September 4, 2026 21:59
@sambhav sambhav changed the title skills: add SEP-2640 support skills: add SEP-2640 protocol support Sep 4, 2026
@sambhav

sambhav commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

Thanks for running the SEP scenarios. The PR is now split so this one contains only the generic protocol layer. I addressed note 1 in the latest revision: skills/list now omits ttlMs and cacheScope before protocol version 2026-07-28, with regression coverage. Notes 2 and 3 make sense and do not require changes in this generic layer; the filesystem metadata behavior and usage notes will remain explicit in the follow-up helper PR.

@sambhav

sambhav commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

I reran conformance PR 330 locally against the split filesystem helper. It caught a nested YAML mapping normalization bug (metadata decoded to a non-JSON concrete map shape), now fixed with regression coverage. Final 2025-11-25 results: enumeration 29/29, manifest 4/4 with the two expected untestable metadata SHOULDs, and directory 7/7; no failures. The runner still needs --spec-version 2025-11-25 --force for this stateful fixture, as documented in conformance PR 330.

@sambhav

sambhav commented Sep 4, 2026

Copy link
Copy Markdown
Member Author

The optional filesystem layer is now advertised as upstream draft #1240. While #1238 is pending, its GitHub Files changed view necessarily includes both stack commits; the clean helper-only diff is sambhav#1. I will rebase #1240 onto upstream main after this PR merges, then mark it ready.

@sambhav

sambhav commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

@guglielmo-san 🙏 this is ready. There is another PR stacked on top which provides a neater abstraction and utility for skills with filesystems (#1240). Would appreciate it if we can land this one first and the other one as a quick follow.

Comment thread skills/client.go Outdated
if !ok {
return fmt.Errorf("skills: server advertised invalid extension settings")
}
enabled, _ := m["directoryRead"].(bool)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would create a const for "directoryRead" as well

Comment thread skills/client.go Outdated
)

// AddClient registers the Skills extension methods that client may send.
func AddClient(client *mcp.Client) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rename it to AddMethods

Comment thread skills/server.go Outdated
return &cloned
}

func validateListResponse(ctx context.Context, result *ListSkillsResult) error {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this is defined on server?
also this is extremely similar to validateListResult, the two functions should be collapsed into one, using a wrapper for the additional operation needed on server side maybe

Comment thread skills/client.go Outdated
Comment on lines +42 to +43
if err := validateListResponse(ctx, result); err != nil {
return nil, fmt.Errorf("skills: server returned an invalid skills/list result: %w", err)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from server side it is possible to increase the limits, but not from client side as it is using the Defaults.. This means that client <-> server is possibly broken. If we want to allow the server to provide higher limits, then we should pass this option also from client

Comment thread skills/server.go Outdated

// UnsafeOptions permits behavior that may not interoperate with conforming hosts.
type UnsafeOptions struct {
DisableDefaultValidation bool

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand what is the advantage of disabling the defaultValidation. According to the spec is a must for both Client and Server

Comment thread skills/server.go Outdated
Comment on lines +90 to +91
if result == nil || result.Skill == nil {
return nil, fmt.Errorf("skills/get handler returned a nil skill")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the URI does not identify a skill the server serves, the server MUST return error -32602 (Invalid params

Comment thread skills/validation.go Outdated
Comment on lines +251 to +253
if parentPath == "" {
prefix = "/"
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is dead code?

sambhav and others added 9 commits September 11, 2026 04:00
Replace the repeated result-type and cache-hint assignments in AddHandlers
with a single stampEnvelope helper that also validates the hints it settles
on, and route the protocol version, result type, and cache scope through
named constants instead of repeated literals.

paginate now skips the clone-and-sort when the catalog is already ordered,
and resolves a cursor with a binary search rather than a linear scan.
allPages allocates its seen set only once a server hands out a second
cursor, and rejects a cursor equal to the initial one.

parseSkillURI returns the parsed URL so validateResourceURI can reuse it
instead of reparsing the skill root for every resource.

mcp: factor the SDK default capabilities into defaultCapabilities so that
AddExtension and capabilities cannot drift apart.

conformance/skills-server: index skills by URI instead of rescanning the
entry slice, and use path.Base for display names.

scripts: factor the repeated argument check into require_value and simplify
the work-directory setup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mcp/mrtr.go imports golang.org/x/sync/errgroup directly, so the "// indirect"
marking was stale and `go mod tidy -diff` reported a pending change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Group the package tests by what they exercise: validation_test.go holds the
pure unit tables, protocol_test.go the wire behavior, skills_test.go the
shared helpers and API contracts, and limits_test.go the manifest caps.

The limits matrix now calls ValidateSkillWithLimits directly instead of
standing up a streamable HTTP server per combination, taking that test from
24 connections to 4. TestLimitsArePlumbed keeps one end-to-end case per side
to prove Client.Limits and ServerOptions.Limits reach validation.

TestSpecErrorScenarios collects the cases that the sep-2640-skills-*
conformance scenarios also cover behind a single server, and carries a note
to delete it once those scenarios run in CI against ./conformance/skills-server.

Statement coverage rises from 82.2% to 90.8%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

3 participants