你的 MCP 服务器能给 Agent 一堆工具,却一直没有标准方式把「这些工具该怎么用」一起交出去:先做什么后做什么、哪些参数必须预检、哪些操作绝不能碰。Skill 解决了内容那一半——一个带 SKILL.md 的目录,YAML frontmatter 里写名字与描述,正文写指示,旁边放 references 与 scripts。缺的是传输那一半:Agent 跑在浏览器里、手机上,或者根本没有文件系统时,技能包就送不过去。Skills 扩展(SEP-2640)补的正是这段:服务器声明 io.modelcontextprotocol/skills 能力,把技能目录作为资源发布出来,客户端按需列出、逐文件校验、再加载。提案在 2026 年 9 月 13 日标记为 Final,规范写在 2026-07-28 基线上;TypeScript SDK 在 10 月 23 日发布的 2.4.0 里把它做成了 ext/skills 子路径。本教程按确认版本、准备技能目录、服务端发布、客户端消费、理解逐文件摘要的边界、上线检查清单六步把它接进你的 TypeScript 项目。
Step 1:先确认你装的是 v2,而且带 ext/skills 子路径
Skills over MCP 只存在于 v2 的包里,通过 ./ext/skills 这个子路径导出。导入一个已安装包不具备的子路径,要到构建或运行时才报错,所以先查一次。如果本地是 0 而最新发布版有,就把 server 与 client 升到同一个版本再继续——两个包版本不一致是这类扩展最容易踩的坑。如果你的项目还在用 @modelcontextprotocol/sdk 这个旧包名,那是 v1,需要先把整套升级到 v2 再来。
# 在依赖 SDK 的那个包里执行(客户端把 server 换成 client)
grep -c '"./ext/skills"' node_modules/@modelcontextprotocol/server/package.json
# 打印 0 就看最新发布版有没有
npm view @modelcontextprotocol/server@latest exports --json | grep -c '"./ext/skills"'
如果最新发布版也还没有,就到此为止——不要自己手写 skills/list 处理器或 skill:// 资源来凑。规范里有明确的约定:名称必须能从 URI 反推、清单要带逐文件摘要、缓存字段只在特定协议修订下出现,手写实现几乎必然与官方客户端不兼容,等发布版支持再接是最省事的选择。
v1 没有这个能力:Skills over MCP 只存在于 v2。项目还在 @modelcontextprotocol/sdk 的话,先走官方的 v1 到 v2 升级路径,把 server 与 client 一起升到同一个版本;混用两个大版本的包会出现类型与协议修订都不匹配的怪问题。
Step 2:准备一个标准技能目录
扩展不重新定义「技能是什么」——它只定义传输绑定,内容格式归 agentskills.io。也就是说,技能就是一个根目录带 SKILL.md 的文件夹:YAML frontmatter 里有 name 与 description,正文是指示,旁边可以有 references、scripts、examples 这些子目录。如果你已经在仓库里维护技能目录,它们就是合法载荷,你要做的不是迁移,而是把它们暴露出去。有一条硬规则要记住:frontmatter 里的 name 必须等于 URI 路径的最后一段,这样光看 URI 就能反推技能名,不必先把文件读一遍。
skills/
git-workflow/
SKILL.md # frontmatter:name / description
references/
conventions.md
scripts/
preflight.sh
---
name: git-workflow
description: 提交前跑一遍分支检查、变更集拆分与提交信息规范
---
# git-workflow
1. 先看 git status 与当前分支,确认不在受保护分支上
2. 把改动按逻辑拆成若干变更集,一次提交只放一类改动
3. 提交信息用约定式格式,正文说明理由与影响面
4. 涉及迁移或删除操作时先停下来,向人要明确确认
把目录放在服务器进程能读到的位置,路径写进配置而不是硬编码在代码里——后面你会频繁改动技能内容,而服务器不该为此重启。技能里的脚本要当作不可信代码对待:它们会被 Agent 读到,也可能被执行,凡是需要凭据的操作都不要在技能里内联密钥。
Step 3:服务端发布:加载目录并挂到服务器上
服务端要做的只有两件事:把目录加载成技能目录对象,再挂到服务器上。挂载之后,扩展会替你提供 skills/list、skills/get 与可选的 resources/directory/read,并把技能文件注册成 skill:// 开头的资源模板——客户端照常用标准的 resources/read 读取文件,没有新传输、没有新载荷格式。加载时遇到不合规的技能,默认跳过并给出警告,也可以配置成严格模式直接失败;文件以 UTF-8 解读成功就当文本提供,否则按 base64 二进制块提供。
import { McpServer } from '@modelcontextprotocol/server';
import { SkillsExtension } from '@modelcontextprotocol/server/ext/skills';
import { loadSkills } from '@modelcontextprotocol/server/ext/skills/node';
const server = new McpServer({ name: 'data-platform', version: '1.4.0' });
// 从磁盘加载一个目录,返回 SkillCatalog(entries + reload())
const catalog = await loadSkills('./skills');
// 挂到服务器上:提供 skills/list、skills/get、resources/directory/read
// 以及 skill:// 资源模板
const skills = new SkillsExtension(catalog);
skills.attach(server); // 实验性:挂载方式可能在小版本内变动
目录对象带 reload(),技能内容更新后不用重启服务器就能刷新——把它接到一个文件监听或管理接口上,运营同学改完 SKILL.md 立刻生效。还有一个容易忽略的点:技能文件会同时出现在普通的 resources/list 里,所以即便客户端不认这个扩展,也能当作资源读到,只是拿不到清单与摘要带来的校验能力。
attach 标为实验性:扩展挂到服务器上的方式可能在小版本内变动,升级 SDK 时要专门回归一次技能相关链路,别只看类型能不能编译过。除 attach 之外的 Skills API 是稳定的。
Step 4:客户端消费:列出、校验、按需读
客户端这一侧有三条主路径:list() 列出服务器提供的技能,get() 按 URI 取单个条目,read() 读技能里的某个文件。list() 遇到不合规的条目会直接抛错,想要容错就传 skipInvalid 保留可用部分、把有问题的单独返回。read() 会逐项校验:字节长度对不对、摘要对不对、SKILL.md 的 frontmatter 对不对,任一项不符就抛校验错误;如果内容来自响应缓存且校验失败,它会先带缓存刷新标记再取一次,避免缓存坏数据一直挡路。规范明确要求客户端不要一上来就把所有技能与文件全下载下来——既浪费带宽也保护服务器。
import { Client } from '@modelcontextprotocol/client';
import { SkillsClient } from '@modelcontextprotocol/client/ext/skills';
const skills = new SkillsClient(client);
// list() 遇到非法条目会抛错;skipInvalid 保留可用部分
const { skills: entries, invalid } = await skills.list({ skipInvalid: true });
// read() 校验 size、digest 与 SKILL.md 的 frontmatter
const entry = await skills.get(entries[0].uri);
const md = await skills.read(entry, 'SKILL.md');
// 想看目录里还有什么,用 readDirectory()
const all = await skills.readDirectory(entry);
用起来的节奏建议是:先 list 拿到 frontmatter 与文件清单,只读 SKILL.md 判断这个技能跟当前任务有没有关系,确有必要再按路径读 references 或 scripts。这样一次任务通常只下载两三个文件,而不是把整套技能目录灌进上下文。技能身份是「服务器 + URI」,不是名字——两个服务器提供同名技能是完全合法的,缓存与去重都要按这个键来做。
Step 5:逐文件摘要给了什么,又没给什么
清单里每个文件都带一个 sha256 摘要与字节大小,而不只是给 SKILL.md 算一个总摘要。这个设计有明确来由:只摘要入口文件的话,服务器可以拿一个无害的技能通过审核,再悄悄换掉模型同样会认真读的参考文件;现在每个文件都被绑定,任何改动或新增都会表现为校验失败。字节长度不符也直接判失败,不用等算摘要。但规范在安全考量里说得很直白:摘要没有签名,由提供内容的同一个服务器提供。
// skills/list 返回的条目带逐文件清单
{
"uri": "skill://git-workflow/SKILL.md",
"resources": [
{
"uri": "skill://git-workflow/SKILL.md",
"digest": "sha256:b95a384300adeea2d902f7d19cd7c04b378ef58e09759107b9c7db4dcacbaa25",
"size": 1428
}
]
}
把话说完整:摘要匹配只证明「清单与内容彼此一致」,不证明两者可信。路径上任何一个中间环节——网关、代理、自建中转——都可以把内容与摘要一起改写,校验照样通过。所以它是抗漂移的证据,不是抗作恶的证据。真正需要信任边界时,仍要靠传输层与服务器身份:私有网络、mTLS、或者只允许你自己运维的服务器列表。
摘要不是信任状:不要因为校验通过就把技能里的脚本当可信代码执行。技能内容由服务器提供,服务器若被接管或路径上有中间人,内容与摘要可以一起被替换。涉及执行脚本、发起写操作、读取凭据的技能,仍然要走你自己的审批与沙箱。
Step 6:上线前要对齐的那些细节
这个扩展在 2025-11-25 与 2026-07-28 两个协议修订上都能工作,但缓存字段只在后者出现:ttlMs 与 cacheScope 由服务器下发,客户端要求它们并从 list()、get()、readDirectory() 回传。也就是说,你的代码不能假设这两个字段一定存在,读之前要判空。常量与上限在 @modelcontextprotocol/core/ext/skills 里,别自己拍脑袋定技能文件大小上限。最后是升级到 2.4 的两处连带改动:number 类型参数上的 x-mcp-header 已被规范拒绝,改成整数类型或去掉注解;内置的校验器换了 URI 实现,非法格式的 $id 或 $ref 现在会直接编译失败。
# 两个协议修订都支持,但缓存字段只在 2026-07-28 上出现
# 2025-11-25:list() / get() / readDirectory() 不带 ttlMs 与 cacheScope
# 2026-07-28:服务器下发这两个字段,客户端要求并回传
# 升级到 2.4 之后要顺手改的两处,否则工具会报错
# 1) number 类型参数上的 x-mcp-header 已被规范拒绝:改用 z.int() 或去掉注解
# 2) 内置校验器换用 fast-uri 3.1.8,拒绝非法 $id / $ref,例如 my_tool:output
落地顺序建议是:先在测试环境用一个只有两三个技能的目录跑通端到端,用官方的一致性套件确认服务器声明与三个方法的返回都合规,再放到生产。声明能力这一步别漏——服务器要在 initialize 返回的 capabilities 里带上 io.modelcontextprotocol/skills,否则客户端不会来找你要技能。上线后把技能目录当代码管:进版本库、走评审、改完打标签,因为它是会直接改变 Agent 行为的东西。
技能内容会直接改变 Agent 行为:把它当代码而不是文档管。改 SKILL.md 就是改 Agent 的决策依据,要有评审、有版本、有回滚;生产环境的技能目录不要允许在线随意编辑,reload() 之前先过一遍校验。
常见问题速查
| 现象 | 原因与处理 |
|---|---|
| 导入 ext/skills 报找不到模块 | 装的是 v1 或版本太旧;grep 子路径确认,升到 v2 同版本 |
| 客户端连上了却列不出技能 | 服务器没在 initialize 的 capabilities 里声明该扩展 |
| list() 直接抛错 | 有条目不合规;用 skipInvalid 保留可用部分并单独处理 invalid |
| read() 报校验错误 | 大小、摘要或 frontmatter 对不上;清缓存重取,仍失败就是内容变了 |
| 技能名与 URI 不一致 | frontmatter 的 name 必须等于 URI 路径最后一段 |
| 改完 SKILL.md 客户端还是旧的 | 调目录对象的 reload(),或重启服务器 |
| ttlMs / cacheScope 读不到 | 连接协商的是 2025-11-25,这两个字段只在 2026-07-28 下发 |
| 工具突然报 schema 无效 | 升级后 URI 校验更严,检查 $id / $ref 的格式 |
| number 参数带 header 注解被拒 | 规范已拒绝,改成 z.int() 或去掉注解 |
| 想把技能一次全拉下来省事 | 规范不建议;先只读 SKILL.md 判断是否相关,再按需读文件 |