高级 📋 6 个步骤 第 565 / 566 篇

用 MCP TypeScript SDK 2.4 让服务器直接分发 Skill:loadSkills 发布、SkillsExtension 挂载与逐文件摘要校验实操

MCP 服务器能给 Agent 工具,却一直没标准方式把「怎么用这些工具」一起交出去。Skills 扩展补齐了这段传输:服务器发布 SKILL.md 目录,客户端列出、按 size 与 digest 逐文件校验后按需加载。TypeScript SDK 2.4.0 已把它做成 ext/skills 子路径。本教程六步接入。

2026.10.25· 11 分钟上手· 约 3495 字· 🧩 MCP Server / 📦 MCP TypeScript SDK

你的 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 项目。

🧩 本教程适合:维护 MCP 服务器或宿主、想把「怎么用这套工具」跟着工具一起分发出去的 TypeScript 开发者。你需要 Node.js 与 v2 版的 @modelcontextprotocol/server 或 @modelcontextprotocol/client。文中 API 以 2.4.0 的发布说明与仓库内的 skill 说明为准;attach 方法标注为实验性,挂载方式可能在小版本内变动。

Step 1:先确认你装的是 v2,而且带 ext/skills 子路径

1 一步检查,省掉后面所有困惑

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:准备一个标准技能目录

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 读到,也可能被执行,凡是需要凭据的操作都不要在技能里内联密钥。

💡 description 是 Agent 决定是否加载这个技能的依据,写得像「触发条件」而不是「功能简介」。把「什么时候该用、什么时候不该用」写进去,比堆砌形容词更能提高命中率。

Step 3:服务端发布:加载目录并挂到服务器上

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:客户端消费:列出、校验、按需读

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」,不是名字——两个服务器提供同名技能是完全合法的,缓存与去重都要按这个键来做。

💡 把 list() 的结果缓存下来,键用服务器地址加技能 URI,值为清单里的摘要。下次连上先比对摘要,变了才重新读文件——这一步能省掉大量重复传输,也让内容漂移立刻可见。

Step 5:逐文件摘要给了什么,又没给什么

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:上线前要对齐的那些细节

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 行为的东西。

💡 采纳还早,但这恰恰是机会。公开可匿名访问的 MCP 端点里,按规范声明这个扩展的仍是极少数,而大量同时提供 MCP 服务与自己编写技能的团队还没把两半接起来。你先把服务器侧做好,客户端一旦普及就能直接用上。

技能内容会直接改变 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 判断是否相关,再按需读文件
← 返回教程中心