This post is for developers who write MCP servers or clients and want to know what the Skills extension adds. It explains what a skill served over MCP is, which mismatches the specification says must stop a client from using one, and how the MCP TypeScript SDK 2.4.0 checks them. Nothing was run: the specification, release notes and SDK source were read on 2026-10-30, and dates are UTC.
A skill is a folder of files, served as MCP resources
A skill, in the Agent Skills specification, is a directory containing at least a SKILL.md file. That file holds YAML frontmatter followed by Markdown content.
The Skills extension, SEP-2640, carries those directories over MCP’s existing Resources primitive. Each file in a skill is an MCP resource, conventionally under the skill:// URI scheme, and the SEP defines only the transport binding. It is an Extensions Track proposal with status Final, written on behalf of the Skills Over MCP Working Group. Its extension identifier is io.modelcontextprotocol/skills, and the pull request that added it was merged on 2026-09-13. A server that declares it implements skills/list, which enumerates its skills, and skills/get, which returns one skill’s entry by URI.
The listing gives every file a digest and a byte size
A client checks downloaded bytes against this listing. Each skills/list entry in SEP-2640 has the skill’s uri, a verbatim copy of its frontmatter, and a resources field. That field is either an array of {uri, digest, size} triples or the string "dynamic". An array must be complete: every file exactly once, including SKILL.md. A digest is a SHA-256 hash of the file’s raw bytes, written as sha256: plus 64 lowercase hexadecimal characters.
SEP-2640 sets per-skill limits of 512 resources and 16 MiB (16,777,216 bytes) in total, and hosts must support skills up to those limits. A skill whose resources is "dynamic" “offers no content integrity”, and hosts may decline to load it.
Four kinds of mismatch stop a skill from being used
These are the checks SEP-2640 makes mandatory for the host, the application that loads skills for a model. Hosts must not use content that fails any of them.
| Check | What fails it |
|---|---|
| Size | Byte length differs from the listed size |
| Digest | SHA-256 differs from the listed digest |
| Unlisted file | A read of a file not in the listing |
| Frontmatter | SKILL.md frontmatter differs, field by field, from the listing’s copy |
A size mismatch counts as a failure whether or not the host goes on to compute the digest, per SEP-2640. A frontmatter mismatch also means the skill must not be loaded. The stated reason is “so that what a user approves from the listing is what the model actually receives.” To recover from a stale listing, the host calls skills/get for that skill.
A matching digest does not prove the content is safe
The checks catch inconsistency between listing and content, not a hostile server. SEP-2640 says digests are unsigned and come from the same server as the content, so “a match proves the two are consistent, not that either is trustworthy.” Hosts must not treat a digest match as a security boundary. A gateway or other intermediary on the path can rewrite the listing and the content together.
The same spec sets rules for skills a host caches on disk. Either the cache sits where nothing but the host can write and a cached file is never modified in place, or the host recomputes SHA-256 from the cached bytes on every access. Comparing a stored digest label or a modification time is not verification. Cached bytes “do not graduate to filesystem-skill trust by residing locally.” A persisted approval must be bound to every uri and digest in the resources set seen at approval.
How the TypeScript SDK 2.4.0 runs these checks
The TypeScript SDK v2.4.0 release, published on 2026-10-23, adds Skills over MCP. A server publishes a folder of skills with loadSkills(folder) and skills.attach(server), and a client lists, reads and verifies them with SkillsClient.
SkillsClient.read(skill, path?) verifies size, digest and SKILL.md frontmatter, and throws SkillVerificationError on a mismatch. In the 2.4.0 source of skillsClient.ts, the file must be listed in the entry, checked before any request. Byte length is compared first, then the SHA-256 digest, then frontmatter. For hashing, text content is UTF-8 encoded and blob content is base64-decoded.
Retrying exactly once is the SDK’s own choice. When a copy from the client’s response cache fails a check, read fetches it once more with cacheMode: 'refresh' and verifies it again. A code comment cites SEP-2640 for this: a cached copy that fails “must be fetched again”, without a number of tries. A fresh read that fails throws at once, and so does a refetched copy that fails a second time.
SkillsClient.list() throws on any invalid entry by default, according to the server@2.4.0 notes. With skipInvalid: true it keeps the valid entries and returns the rest in invalid. On the server, invalid skills are skipped with a warning through onWarning, or fail under the strict loading option. The frontmatter comparison in verify.ts checks every key both ways, objects in any key order, arrays in order, and no type coercion.
For "dynamic" skills, read skips the listing, size and digest checks and returns verified: false, but still compares frontmatter. The SkillsClient class’s doc comment says approval, origin tagging and loading policy “stay with the host”. So approval is left to whoever builds the host.
The release notes mark one server call as experimental
Experimental parts can change in a minor release, so this decides what to pin. The server@2.4.0 notes mark SkillsExtension.attach as @experimental, because how an extension is wired to a server may change. The same notes call the rest of the Skills API stable.
Caching fields on the 2026-07-28 protocol revision are freshness hints
Two new fields tell a client when to refetch a listing, not whether to trust it. Per the server@2.4.0 notes, Skills works on protocol revisions 2025-11-25 and 2026-07-28. Only on 2026-07-28 does the server send ttlMs and cacheScope, and SkillsClient requires them and returns them from list(), get() and readDirectory().
Both fields come from SEP-2549. ttlMs says how long a response may be considered fresh, and cacheScope controls who may cache it. A TTL is an estimate, and a server may change the list before it expires.
SEP-2640 calls these fields on skills/list “a freshness hint for the listing and a cache-scope marker, not an integrity property.” Whether skills/get results should carry them too is left open in the spec.
Limits
Nothing was installed or run for this post. The constants.ts file exports the 512-file and 16 MiB limits as SKILL_LIMITS, but client-side enforcement is not documented in the release notes. The source read here does not say where the client’s response cache lives or whether it meets the spec’s disk-cache rules. No SDK option to refuse "dynamic" skills was seen. SEP-2640 lists Python, C# and Go pull requests as reference implementations, and other SDKs were not checked. SkillsExtension.attach may change in a minor release.