Skills
Показано англійською
Актуального перекладу цієї сторінки немає, тому ви читаєте її англійською. На сторінці Переклади пояснено, як влаштована перекладена документація.
SEP-2640 defines a
convention for serving Agent Skills over MCP. A skill is just a
directory of files — at minimum a SKILL.md with YAML frontmatter — that you expose as ordinary
MCP resources, conventionally under a skill:// URI.
A server enumerates its skills with skills/list, answers for any single one by URI with
skills/get, and — optionally — lists a directory's direct children with
resources/directory/read.
The SDK ships this as the built-in Skills extension (io.modelcontextprotocol/skills). There's
one on the server side and one on the client side. If Extensions are new to you,
skim that page first.
Info
Skills gives you the protocol primitives: request/response handling, capability
advertisement, and SEP-2640 conformance validation.
It does not discover, read, or hash skills from a filesystem. You supply handlers that
answer from wherever your catalog actually lives — a database, a generated index, an in-memory
list, or a directory you walk yourself — and you serve each skill's files as ordinary resources
through MCPServer.add_resource or an @mcp.resource(...) template handler.
Serving a skill
Here's a server that serves one skill:
import hashlib
from typing import Any
from mcp.server.context import ServerRequestContext
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.resources import TextResource
from mcp.server.skills import Skills
from mcp.shared.exceptions import MCPError
from mcp.shared.skills import (
GetSkillParams,
GetSkillResult,
ListSkillsParams,
ListSkillsResult,
Skill,
SkillResource,
)
from mcp.types import INVALID_PARAMS
SKILL_URI = "skill://git-workflow/SKILL.md"
SKILL_MD = """\
---
name: git-workflow
description: Follow this team's Git conventions for branching and commits
---
Branch from `main` using `type/short-description`. Write commit subjects in the
imperative mood, under 72 characters.
"""
GIT_WORKFLOW = Skill(
uri=SKILL_URI,
frontmatter={"name": "git-workflow", "description": "Follow this team's Git conventions for branching and commits"},
resources=[
SkillResource(
uri=SKILL_URI,
digest=f"sha256:{hashlib.sha256(SKILL_MD.encode()).hexdigest()}",
size=len(SKILL_MD.encode()),
)
],
)
async def list_skills(ctx: ServerRequestContext[Any, Any], params: ListSkillsParams) -> ListSkillsResult:
return ListSkillsResult(skills=[GIT_WORKFLOW])
async def get_skill(ctx: ServerRequestContext[Any, Any], params: GetSkillParams) -> GetSkillResult:
if params.uri != SKILL_URI:
raise MCPError(code=INVALID_PARAMS, message=f"unknown skill: {params.uri}")
return GetSkillResult(skill=GIT_WORKFLOW)
mcp = MCPServer("catalog", extensions=[Skills(list_skills=list_skills, get_skill=get_skill)])
mcp.add_resource(TextResource(uri=SKILL_URI, name="SKILL.md", mime_type="text/markdown", text=SKILL_MD))
There are three moves here:
Skill(uri=..., frontmatter=..., resources=[...])is one entry. It has the same shape whether it comes back fromskills/listorskills/get.resourcesis the skill's complete file manifest — every file,SKILL.mdincluded, each with asha256:...digest and byte size — or the string"dynamic"for content generated on demand.list_skillsandget_skillare plain async callables, invoked once per request.get_skillmust answer for a skill even if yourlist_skillsleft it out — SEP-2640 requires a server to answer by URI for every skill it serves, listed or not.mcp.add_resource(TextResource(uri=SKILL_URI, ...))registers the skill's actual file content, served through the SDK's ordinary resource machinery.Skillsnever reads or writes resource content itself.
And that's it. Skills(list_skills=..., get_skill=...) is all a server needs;
resources/directory/read is optional (more on that below).
Inspecting a skill
On the client side, Skills is a ClientExtension. You register it the same way
you register any other one — by passing it to Client(extensions=[...]) — and then call extension to
get the verbs tied to that connection:
import anyio
from mcp import Client
from mcp.client.extensions.skills import Skills
async def main() -> None:
async with Client("http://localhost:8000/mcp", extensions=[Skills()]) as client:
catalog = client.extension(Skills)
for skill in await catalog.list_skills():
print(skill.uri, skill.frontmatter["description"])
skill = await catalog.get_skill("skill://git-workflow/SKILL.md")
print(skill.resources)
if __name__ == "__main__":
anyio.run(main)
client.extension(Skills) hands you a SkillsClient with the SEP-2640 methods:
list_skillsandread_directoryfollownextCursorto completion, so a single call gives you every page's skills or resources.get_skillcosts exactly one request.
These three validate the server's response against the SEP-2640 conformance rules before returning
it. A name that doesn't match its URI, a digest in the wrong shape, or an incomplete manifest raises
ValueError rather than reaching your code.
Read a skill file with Client.read_resource(uri), the same method you use for any MCP resource.
It returns raw content without checking it against the skill's manifest.
Verify files before loading
A host that loads a skill must hold its Skill entry. Before using a fetched file, check that
its URI appears in that entry's resources and that its bytes match the advertised size and
SHA-256 digest. A "dynamic" skill has no digest to check. Refreshing the entry to accept
changed bytes also changes the content to which any prior approval applied.
Skill content is untrusted model input, exactly like any other server-provided text. SEP-2640
requires a host to tag it with its originating server before it reaches the model, and to
never grant the frontmatter's allowed-tools field (or any other permission-widening field)
without explicit per-skill user approval.
A matching digest confirms the bytes, not the frontmatter: it doesn't prove the
frontmatter the server advertised in skills/get matches the frontmatter inside the fetched
SKILL.md. If you act on skill.frontmatter — especially allowed-tools — parse the fetched
file and compare its frontmatter yourself.
These are host responsibilities the SDK cannot discharge for you — read the SEP's Security Implications section before building a host on top of this extension.
Directory reads
A skill's instructions often point at a directory rather than a file ("pick the matching
template from templates/"). resources/list can't answer that — it enumerates a server's
entire resource space, not one subtree — so SEP-2640 adds resources/directory/read, gated
behind the directoryRead capability setting:
mcp = MCPServer(
"catalog",
extensions=[
Skills(
list_skills=list_skills,
get_skill=get_skill,
read_directory=read_directory, # lists uri's direct children
)
],
)
Supplying read_directory advertises {"directoryRead": true} under the extension's
capabilities. Omitting it advertises neither the setting nor the method — a client calling
resources/directory/read against such a server gets METHOD_NOT_FOUND. On the client side,
read_directory raises before it sends anything if the connected server hasn't advertised the
setting.
Protocol version and caching
In protocol version 2026-07-28 and later, skills/list and skills/get results carry the base
protocol's caching fields, ttlMs and cacheScope —
the same freshness hint tools/list, resources/list, and resources/read carry. Skills fills
cacheScope with "private" when your handler leaves it unset, and omits both fields entirely on an
older connection. Set cache_scope="public" only when the result is the same for every user.
You don't have to branch on protocol version yourself.
What this SDK doesn't do
Skills is a protocol adapter, not a skills provider. It has no opinion on where a skill's bytes
live, how they're indexed, or when a catalog is refreshed — that's for a higher-level library, or
your own handler, to decide.
If you're looking for "scan this directory and serve whatever's in it," you're looking for a
provider built on top of Skills, not Skills itself.