Add TypeScript SDK for the Skills Extension SEP#71
Closed
olaservo wants to merge 34 commits into
Closed
Conversation
This was referenced Apr 13, 2026
This was referenced Apr 13, 2026
Reference implementation demonstrating the Skills Extension SEP with: - skill:// resource convention with multi-segment paths - Extension declaration (io.modelcontextprotocol/skills) via SEP-2133 - SEP-2093 shims: resources/metadata, URI-scoped resources/list, per-resource capabilities via _meta - SDK with server-side discovery/registration and client-side wrappers (listSkills, readSkillUri, fetchSkillMetadata, READ_RESOURCE_TOOL) - 4 sample skills: 2 single-segment, 2 multi-segment under acme/ - End-to-end client demo exercising all 7 features Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Each shim function now has a TODO with the specific PR link for when/if it can be removed. Uses "if" not "when" since the upstream PRs are drafts not guaranteed to merge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ASCII diagrams showing: overall system structure, URI anatomy, discovery flow, SDK-to-protocol mapping, and what's permanent SEP design vs temporary SDK shims. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align the reference implementation with PR #69's SEP spec, which defines skill://index.json as the primary enumeration mechanism and requires zero protocol dependencies beyond resources/read. Added: - skill://index.json well-known discovery index (Agent Skills format) - generateSkillIndex() server-side, listSkillsFromIndex() client-side - SkillIndex/SkillIndexEntry types and INDEX_JSON_URI constant - 53 tests across URI utilities, index round-trip, and client parsing Removed (SEP-2093 features not in the SEP): - resources/metadata handler and fetchSkillMetadata() - Scoped resources/list(uri=...) and listSkillsScoped() - Per-resource _meta capabilities on all resource registrations - ResourceCapabilities/ResourceMetadataResult types - Zod schemas and RequestHandlerRegistrar interface Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bump @modelcontextprotocol/sdk from 1.27.1 to 1.29.0, which includes native extensions capability support (typescript-sdk#1811, backport of #1630) and the missing size field on ResourceSchema (#1575). - Replace private _capabilities hack in declareSkillsExtension with the SDK's native registerCapabilities API - Add size field to SKILL.md resource metadata in registerSkillResources - Keep ServerInternals as a deprecated alias for backward compatibility - Update README shims table to reflect resolved workarounds Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
975e8d1 to
7a6a182
Compare
- Gap 1: Add $schema validation in listSkillsFromIndex() with KNOWN_SKILL_INDEX_SCHEMAS set. Warns on unrecognized schema but continues processing (SHOULD, not MUST per the SEP). - Gap 2: Extend SkillIndexEntry to discriminated union supporting skill-md, mcp-resource-template, and archive entry types. Add listSkillTemplatesFromIndex() client function and templates parameter to generateSkillIndex(). - Gap 3: Add well-known HTTP bridge module (well-known.ts) that fetches skills from /.well-known/agent-skills/index.json, verifies SHA-256 digests, supports archive extraction, and caches to a local directory for use with discoverSkills() + registerSkillResources(). - Gap 4: Digest-based caching in the bridge module — refreshFromWellKnown() skips unchanged entries based on stored digests. 86 tests passing across 4 test files. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- McpResourceTemplateIndexEntry: use `url` instead of `uriTemplate` per the SEP field table, and make `name` optional (SEP says omitted for mcp-resource-template entries) - SkillIndexEntry: remove ArchiveIndexEntry from the union — the SEP restricts skill://index.json to skill-md and mcp-resource-template only. ArchiveIndexEntry remains as a standalone type for the well-known HTTP bridge. - SkillTemplateEntry.name: make optional to match Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y fixes - discoverSkills(): index-first discovery with resources/list fallback - discoverAndBuildCatalog(): one-call discover + catalog for agent integration - listSkillsFromIndex() now explicitly handles any URI scheme in index entries - readSkillUri() documented as scheme-agnostic primary read function - buildSkillsCatalog() restored with scheme-agnostic XML generation - Case-insensitive skill name matching in server discovery - Export SkillSummary, SkillsCatalogOptions, and new types from client subpath - Merge duplicate JSDoc on READ_RESOURCE_TOOL - README updated with quick start and step-by-step client usage - 107 tests across 4 test files Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Align SDK resource metadata with skill-meta-keys.md recommendations: - Default audience to ["assistant"], configurable per-skill via SkillMetadata.audience or globally via RegisterSkillResourcesOptions - Add lastModified to index.json, template, and prompt-xml resources (derived from most recent skill mtime) - Add size to manifest, index.json, and prompt-xml resources (pre-computed from serialized content) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Member
Author
|
Disclaimer: have been iterating through a few versions of this while clarifying the implementation guidance we'd want to add to the SEP or other docs. Mostly opened this for extra 👀 in case any of this was way off track. I'm not sure if we actually plan to publish a dedicated SDK around this extension, or just refer to code examples and link to implemenations. |
The server clause in the catalog prompt is only useful when the reader tool accepts a `server` parameter. For tools already scoped to a single server (taking only `uri`), the clause mentions an argument the tool doesn't accept. Make `serverName` optional on `SkillsCatalogOptions` and drop the clause when omitted. `DiscoverCatalogOptions.serverName` stays required since the default `READ_RESOURCE_TOOL` takes a server param. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rescopes the SDK under the MCP org and adds the `experimental-` prefix to signal the SEP is still in draft. Version resets to 0.1.0 for the first publish under the new name. Also removes the `examples/` tree, which lived outside the SDK and isn't part of the published package. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The well-known bridge fetches /.well-known/agent-skills/index.json,
verifies digests, and unpacks tar.gz archives to a local cache. That's
agent-skills-spec scope, not SEP-2640 scope, and bundling them produced
an awkward split where ArchiveIndexEntry was annotated "HTTP bridge
only, not valid in skill://index.json" because both type systems lived
in the same package.
Drops src/well-known.{ts,test.ts}, src/well-known/index.ts, the four
WellKnown* types and ArchiveIndexEntry from types.ts, the ./well-known
subpath export, the tar dependency, and the README HTTP-bridge section.
Build clean, 87 vitest cases still pass.
SEP-2640 defines `type: "archive"` as a normative entry type in `skill://index.json` and requires hosts to support both `.tar.gz` (application/gzip) and `.zip` (application/zip). The SDK previously filtered archives to skill-md only on the client side and had no way to declare them on the server side; both gaps are now closed. Server: `registerSkillResources()` accepts `archives: SkillArchiveDeclaration[]` and `templates: SkillTemplateDeclaration[]`. Each archive declaration is read from disk, served as a single MCP resource at `skill://<skillPath>.<format>` with the SEP-mandated mimeType, and emitted as a `type: "archive"` entry in the generated index. The SDK enforces the path-name rule (final segment of skillPath MUST equal frontmatter name) at declaration time. Client: `listSkillsFromIndex()` returns archive entries with `type: "archive"`, `uri` set to the archive resource URL, and `skillPath` derived by stripping the archive suffix. New `readSkillArchive()` fetches via `resources/read` and dispatches to in-memory extraction based on the resource's mimeType (with URL-suffix fallback per the SEP), returning `Map<string, Buffer>` keyed by the archive-relative path so post-unpack files match the `skill://<skillPath>/<file-path>` namespace exactly. Archive safety per Agent Skills spec: rejects path-traversal segments, absolute paths, drive-letter prefixes, and symlinks resolving outside the skill directory; bounds total uncompressed size, per-file size, and entry count to defend against decompression bombs; requires SKILL.md at archive root (no wrapper directory). Adds tar-stream + yauzl as runtime deps; yazl as devDep for tests. 112 → 122 vitest cases passing.
The previous example sources were removed in fab1009 ("examples lived outside the SDK and aren't part of the published package"). Bringing them back as the reference end-to-end demo, since the README still points at them and SEP-2640 calls out the TypeScript SDK + reference implementations as a review prerequisite. Layout: sample-skills/ — 4 skill-md skills (single + multi-segment) code-review/ — w/ supporting REFERENCE.md git-commit-review/ acme/onboarding/ acme/billing/refunds/ — w/ supporting refund-email-template.md sample-archive-source/ — packed at server startup → archive entry pdf-processing/ — w/ references/FORMS.md skills-server/typescript/ — discovers, packs the archive, registers all three index entry types skills-client/typescript/ — walks 7 client-side surfaces: READ_RESOURCE_TOOL, listSkillsFromIndex, listSkillTemplatesFromIndex, listSkills, readSkillContent, readSkillArchive, readSkillManifest + readSkillDocument The pdf-processing skill is the archive demo: server packs it to a temp .tar.gz at startup, registers it as `skill://pdf-processing.tar.gz` with mimeType application/gzip, and lists it in skill://index.json with type="archive". Client fetches it via resources/read and unpacks in-memory through readSkillArchive() with full archive-safety checks. Verified: client run prints all 7 sections successfully against the spawned server.
Server: validate skill-name against ^[a-z0-9-]+$, add `index: false` opt-out for unenumerable catalogs, throw when template `complete` is set without `read`. Client: switch frontmatter parser to the yaml package so block scalars work, and extract `<skill-path>` per SEP from any-scheme URIs ending in /SKILL.md (was falling back to entry name). Plus accumulated alignment work: instructions-mining discovery path, READ_SKILL_TOOL export, xml.ts cleanup, examples/READMEs/CLAUDE.md updates, and 15 new test cases covering the above.
…directory/read
Brings @olaservo/ext-skills into line with the latest SEP-2640 (ADRs of
2026-06-02/05/09). Breaking change; version bumped to 0.10.0.
Index schema (skill://index.json):
- Type-less entries: verbatim `frontmatter` + `url`/`digest` and/or per-skill
`archives[]` ({url, mimeType, digest}). Drops the skill-md/archive/
mcp-resource-template discriminator and the $schema/version marker.
- Server computes sha256:{hex} digests over SKILL.md raw bytes (and each
archive); captures the full frontmatter block in discoverSkills.
- Client reads name/description from frontmatter; carries digest/archives on
SkillSummary; skips entries with neither url nor archives.
resources/directory/read (new directory.ts):
- inode/directory model, buildDirectoryTree + makeDirectoryReadHandler
(paginated, metadata-only, non-recursive, -32602 on misuse), registered on
the low-level Server.
- directoryRead capability via declareSkillsExtension(server, {directoryRead})
- Client: serverSupportsDirectoryRead, readDirectory, walkDirectory.
Digest integrity/caching:
- verifyDigest / readSkillUriVerified (UTF-8 round-trip is byte-exact for
SKILL.md, so a faithful read always matches).
Removes the parameterized-template serving feature (SkillTemplateDeclaration,
listSkillTemplatesFromIndex, mcp-resource-template) — it only existed to back
the dropped index entry type; the catch-all supporting-files template stays.
Tests rewritten/added (163 passing); examples + README + CLAUDE.md updated;
verified end-to-end via the reference client/server.
🦉 Generated with Claude Code
- parseFrontmatter (server): use line-anchored regex instead of
content.split("---") so a `---` inside frontmatter (e.g. an indented
block scalar) no longer truncates the parse; server now matches the
client parser.
- archive (tar.gz): stream gunzip with a running decompressed-byte
counter that aborts past maxTotalSize, instead of gunzipSync fully
inflating a possibly-huge payload before the size check.
- verifyDigest: throw on a malformed expected digest rather than
silently returning false, so callers distinguish tampering from an
uninterpretable digest (SEP-2640 verification is a MUST).
- walkDirectory: fix doc comment (FIFO queue is breadth-first).
Adds regression tests: gzip bomb rejection, verifyDigest throw on
malformed digest, and discoverSkills retaining a description whose block
scalar contains a `---` line. 166 tests pass.
🦉 Generated with [Claude Code](https://claude.com/claude-code)
…directoryRead no-op - loadDocument: reject `..` as a path segment, not a substring, so valid filenames like `notes..final.md` are no longer wrongly rejected (real traversal still blocked by isPathWithinBase). - registerSkillResources: compute each archive digest once from the bytes it serves and reuse it for the index via GenerateSkillIndexOptions .archiveDigests, so the advertised digest can't drift from the served artifact and each archive is read once instead of twice. - registerSkillResources: warn when directoryRead handler is installed without the matching declareSkillsExtension capability, instead of silently no-opping for spec-conformant clients. 🦉 Generated with [Claude Code](https://claude.com/claude-code)
- readSkillUri: accept legitimately empty text content (typeof check instead of falsy `content.text`), matching the archive read path - walkDirectory: guard against cyclic listings (visited set) and non-advancing pagination cursors (throw instead of looping forever) - listSkillsFromInstructions: read named URIs concurrently via Promise.allSettled instead of serially - uri: extract isSkillContentFilename() and route the four inline SKILL.md checks through it Adds 6 regression tests; full suite passes (172). 🦉 Generated with [Claude Code](https://claude.com/claude-code)
Member
Author
|
Closing since we will add this to the typescript SDK instead. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A TypeScript SDK + reference client/server demonstrating SEP-2640 (Skills Extension), serving Agent Skills as
skill://resources over MCP. Tracks the current SEP text on thesep/skills-extensionbranch, including the latest decision-log ADRs (2026-06-02 / 06-05 / 06-09). No protocol dependency beyondresources/readand the one new extension method below.skill://resource conventionskill://acme/billing/refunds/SKILL.md)name, and the no-nesting constraint — both enforced at discovery timeskill://is SHOULD; index entries may use any scheme (repo://,github://, …)skill://index.json(current SEP-2640 schema)The index is the WG's own schema (decoupled from the agentskills.io
.well-knownformat) with type-less entries:frontmatter— the skill'sSKILL.mdfrontmatter copied verbatim as JSON (carriesname/descriptionand anything else authored)url+digest— when served as individual files;digestissha256:{hex}over the SKILL.md raw bytesarchives[]—{ url, mimeType, digest }per packed formurl, a non-emptyarchives, or both. No$schema/version marker.This replaces the earlier discriminated
type: "skill-md" | "archive" | "mcp-resource-template"schema.resources/directory/read(new extension method)ls-style, metadata-only, paginated, non-recursive listing. Directories are identified bymimeType: "inode/directory";-32602on a non-directory/unknown URI.buildDirectoryTree()+makeDirectoryReadHandler(), installed on the low-levelServer. Client:serverSupportsDirectoryRead(),readDirectory(),walkDirectory().directoryRead(default off).Digest: integrity + caching
verifyDigest()/readSkillUriVerified()— tamper/integrity check against the index digest (UTF-8 round-trip is byte-exact forSKILL.md, so a faithful read always matches).Extension declaration
io.modelcontextprotocol/skillsvia native SDKregisterCapabilities(v1.29+)declareSkillsExtension(server, { directoryRead: true })to advertise the directory-read capability (paired withregisterSkillResources(..., { directoryRead: true }))Subpath exports
/server/clientRemoved since the original PR
mcp-resource-templateindex entry type and the parameterized-template serving feature (SkillTemplateDeclaration,listSkillTemplatesFromIndex) — it only existed to back the dropped index type. The catch-all supporting-files template stays.$schemafield + validation and theSKILL_INDEX_SCHEMAconstants.Breaking changes
Yes — this is a clean rewrite of the index schema and removes the template surface. Package version bumped to
0.10.0(pre-1.0).Verification / test plan
cd typescript/sdk && npm run build && npm test— cleantsc, 163 tests pass (newdirectory.test.ts; rewrittenindex-json.test.ts/register.test.ts)resources/directory/readenumeration +walkDirectory, archive fetch+unpack, digest verification (matches ✓), supporting-file flow🦉 Generated with Claude Code