This is the documentation for v2 — looking for the v1 documentation?
Skip to content

Skills ​

SkillsClient lists a server's skills and reads their files, checking each file against what the server published.

ts
import { SkillsClient } from '@modelcontextprotocol/client/ext/skills';

const skills = new SkillsClient(client);
const { skills: entries } = await skills.list();
console.log(entries.map(entry => `${entry.frontmatter.name}: ${entry.frontmatter.description}`).join('\n'));

Against a server with two skills, it prints each skill's name and description:

text
refunds: Process customer refunds.
git-workflow: Branching and review conventions.

Listing reads no skill file: each entry carries the skill's frontmatter and its file list with digests, enough to decide which skill to load.

Handle refused entries ​

Create the client with skipInvalid: true to keep the valid skills and log the refused entries.

ts
const tolerant = new SkillsClient(client, { skipInvalid: true });
const { invalid } = await tolerant.list();
for (const refused of invalid) console.error(`skills: refused entry #${refused.index} ${refused.uri ?? ''}: ${refused.reason}`);

By default list() throws an SdkError naming every entry that fails validation, and every copy of a repeated URI. With skipInvalid: true it returns the valid skills and puts those entries in invalid, each with index, uri when it has one, and reason. Against this server it is empty, so the loop prints nothing.

Load a skill ​

Read the entry's SKILL.md.

ts
const git = entries.find(entry => entry.frontmatter.name === 'git-workflow')!;
const manifest = await skills.read(git);
console.log(manifest.verified, manifest.frontmatter);

The file passes every check, and its frontmatter matches the entry:

text
true {
  name: 'git-workflow',
  description: 'Branching and review conventions.'
}

read(entry) checks the size and SHA-256 digest against the entry, then compares the frontmatter field by field. manifest.body holds the instructions to give the model.

The comparison parses the frontmatter with YAML 1.2 core-schema typing, so yes is the string "yes", not true. If a server builds its entries with a parser that types values differently, pass a matching YAML parser as parseFrontmatter to new SkillsClient; it receives the YAML between the --- lines.

Read the files a skill references ​

Pass the path SKILL.md uses.

ts
const review = await skills.read(git, 'references/REVIEW.md');
console.log(review.text);

review.text holds the verified file:

text
# Review checklist

- Tests pass.

A relative path resolves against the skill's folder, as a link in SKILL.md does, and read ignores a fragment. It matches the path to the entry's file list however either side percent-encodes the name, so references/Q&A.md works. bytes always holds the file's bytes; text holds the file's text when the server sent it as text, and always for the skill's own SKILL.md.

Handle verification failures ​

Catch SkillVerificationError.

ts
import { SkillVerificationError } from '@modelcontextprotocol/client/ext/skills';

try {
    await skills.read(git, 'references/MISSING.md');
} catch (error) {
    if (!(error instanceof SkillVerificationError)) throw error;
    console.log(error.reason, error.uri);
}

The entry does not list references/MISSING.md, so read refuses it before sending any request:

text
unlisted skill://git-workflow/references/MISSING.md

reason is unlisted, size, digest or frontmatter; a path that does not parse is unlisted too. When a size, digest or frontmatter check fails on a copy the client served from its response cache, read fetches the file once more with cacheMode: 'refresh' and throws only if the fresh copy fails as well; a copy that came from the server fails at once. On any reason, do not use the file; for SKILL.md, do not load the skill.

Browse a skill folder ​

List a folder's direct children when the server declares directoryRead.

ts
if (skills.directoryRead) {
    const { resources: children } = await skills.readDirectory('skill://acme/billing/refunds');
    console.log(children.map(child => `${child.name} ${child.mimeType}`).join('\n'));
}

The refunds folder holds its SKILL.md and a templates folder:

text
SKILL.md text/markdown
templates inode/directory

Get a skill by URI ​

Fetch one entry, listed or not.

ts
const { skill: refunds } = await skills.get('skill://acme/billing/refunds/SKILL.md');
console.log(refunds.frontmatter.name);

It prints the skill's name from the entry's frontmatter:

text
refunds

On 2026-07-28, list(), get() and readDirectory() also return the server's caching hints, ttlMs and cacheScope, and throw an SdkError when a response leaves either out.

Dynamic skills

An entry whose resources is "dynamic" has no file list. read then skips the size and digest checks and returns verified: false, but still compares the SKILL.md frontmatter.

Host responsibilities

Accept skills up to SKILL_LIMITS (512 files, 16 MiB) and explain why you decline one, get approval before you load a skill from a server, and record which server each skill came from. Read a skill's files when you load it, never while listing.

Recap ​

  • new SkillsClient(client) wraps a connected client; list() returns entries without reading files.
  • list() throws on an invalid entry; skipInvalid: true keeps the valid skills and returns the refused ones in invalid.
  • read(entry) verifies SKILL.md (size, digest, frontmatter) and returns its frontmatter and body.
  • read(entry, path) resolves a path against the skill's folder and verifies that file.
  • A failed check throws SkillVerificationError with a reason.