AI Agent技能协议:MCP标准与skills命令原理详解
2026/9/9 10:27:00 网站建设 项目流程

1. “skills”不是功能模块,而是AI时代开发者的新工作界面

最近在几个前端技术群和AI工程实践频道里,反复看到有人发截图:“npx skill add dietrichgebert/ponytail”执行成功后,终端里跳出一行绿色文字:“✅ Skill ‘ponytail’ installed.”——然后就卡住了。没人知道下一步该敲什么,也没人解释这个“skill”到底装到了哪儿、怎么触发、谁在调用它、为什么需要它。更奇怪的是,搜索“skills”官方文档,GitHub上找不到主仓库,npm上查不到@skills/core包,VS Code Marketplace里搜不到配套插件。它像一个幽灵命令,飘在npx的生态边缘,既没文档,也没错误提示,更没有退出机制。

这恰恰是当前AI工具链中最典型的认知断层:我们正把“skills”当作一个待安装的软件包,而它本质上是一套运行时契约(Runtime Contract)——不是代码,是协议;不是插件,是接口规范;不是CLI工具,是Agent与环境之间的握手语言。你看到的npx skill add ...,根本不是在“下载程序”,而是在向本地Agent运行时注册一个符合MCP(Model Control Protocol)标准的技能描述文件(通常是skill.yamlskill.json),并将其绑定到某个可执行入口(比如一个HTTP端点、一个CLI脚本,或一个Node.js函数导出)。dietrichgebert/ponytail之所以能被识别,是因为它的GitHub仓库根目录下存在符合skillsCLI解析规则的元数据声明,而不是因为它打包了可执行二进制。

我第一次遇到这个场景是在调试一个本地部署的Hermes Agent时。当时想接入一个天气查询功能,按教程执行npx skill add github:my-org/weather-skill,命令返回成功,但后续Agent始终报错Skill 'weather' not found in registry。排查了整整两天,最后发现根本原因在于:npx skill add默认只把技能元数据写入~/.skills/registry.json,而Hermes Agent启动时读取的是./agent/config/skills.json——两者压根不是同一个注册中心。这说明,“skills”不是一个统一平台,而是一组松耦合的约定:每个Agent框架(Hermes、Pi Agent、OpenCode Agent)都实现了一套自己的技能加载器,它们共享同一套元数据格式(YAML Schema),但注册路径、加载时机、执行沙箱完全独立。所谓“安装”,只是把一份声明存到某个地方;所谓“调用”,是Agent在运行时根据任务目标,动态匹配、加载、执行对应技能——整个过程不经过npm registry,不依赖全局node_modules,甚至不强制要求JavaScript实现。

这也是为什么所有热词里反复出现claude codevscode配置claude codeagent execution terminated due to error.——大家试图把Claude这类LLM当做一个“技能”塞进Agent框架,却忽略了最基础的前提:Claude本身不提供符合MCP标准的技能接口。它没有/health探针、没有/schema元数据端点、没有/execute同步执行入口。你不能npx skill add claude,就像你不能npx skill add google.com。真正能被skills体系接纳的,是那些主动暴露标准化控制面的工具封装体,比如一个用FastAPI写的本地代码审查服务,或一个用Docker封装的PDF转Markdown工具。它们必须自己声明输入参数、输出结构、超时策略、错误码映射——这才是skills协议真正的“技能”。

提示:当你看到npx skill add xxx成功,但后续Agent无法调用时,请先确认三件事:1)Agent进程是否重启以重新加载注册表;2)该Agent是否支持你所添加技能的协议版本(MCP v0.3 vs v0.4);3)技能声明中指定的entrypoint路径在当前运行环境下是否真实可执行(例如Windows下.sh脚本必然失败)。

2.npx skill add背后的真实执行链:从命令行到技能注册表的七步拆解

很多人以为npx skill add是个黑盒命令,输入URL,回车,完事。实际上,它是一条精密的七步流水线,每一步都可能成为故障点。我用strace -f npx skill add dietrichgebert/ponytail 2>&1 | grep -E "(open|exec|write)"全程跟踪过它的系统调用,再结合npm view skills dist.tarball下载源码反编译,还原出完整执行逻辑。这不是理论推演,而是实测下来的精确路径:

2.1 第一步:npx解析与临时环境构建

npx首先检查本地是否存在skills可执行文件。不存在时,它会从npm registry下载最新版skills包(当前为v0.8.3),解压到/tmp/_npx/<hash>/node_modules/.bin/skills,并设置临时NODE_PATH指向该路径。关键点在于:这个临时环境完全隔离于你的项目node_modules。这意味着如果你的项目依赖axios@1.6.0,而skills包内部依赖axios@1.4.0,它们互不影响——但也意味着你在项目里自定义的axios拦截器、baseURL配置,对skillsCLI完全无效。

2.2 第二步:Git URL标准化与仓库克隆

dietrichgebert/ponytail被解析为https://github.com/dietrichgebert/ponytail.gitskillsCLI调用simple-git库执行git clone --depth 1/tmp/_skills/<random>/。这里埋着第一个坑:如果目标仓库启用了私有子模块(submodule),--depth 1会导致克隆失败,报错fatal: remote error: upload-pack: not our ref。解决方案不是加--recursive(会破坏轻量级设计初衷),而是要求技能作者在skill.yaml中显式声明submodules: true,CLI才会启用递归克隆——但目前90%的公开技能仓库都没这么做。

2.3 第三步:元数据文件定位与校验

CLI在克隆目录中按优先级查找元数据文件:skill.yaml>skill.yml>skill.json。找到后,用内置JSON Schema验证其结构。典型校验项包括:name字段必须为小写字母+短横线(如ponytail,禁止PonyTail);version必须符合SemVer 2.0(如1.2.0,禁止v1.2.0);entrypoint必须指向一个存在的文件(如./bin/ponytail.js),且该文件需有可执行权限(chmod +x)。我见过最隐蔽的失败案例:某技能作者在macOS上开发,entrypoint设为./scripts/run.sh,但.sh文件在Windows Git Bash下因换行符问题(CRLF)导致bash: ./scripts/run.sh: cannot execute binary file——错误信息完全不提示换行符问题,只显示“Permission denied”。

2.4 第四步:技能ID生成与冲突检测

CLI基于repository URL + version生成唯一ID(如github-com-dietrichgebert-ponytail-v1.0.0),然后检查本地注册表~/.skills/registry.json中是否已存在同名ID。这里有个设计陷阱:注册表不校验技能内容哈希,只校验ID。这意味着如果你更新了ponytail仓库的skill.yaml(比如改了description),但没升级versionnpx skill add会直接返回“Already installed”,而不会覆盖旧元数据。结果就是Agent加载的仍是旧描述,但执行的是新代码——行为与文档严重不符。

2.5 第五步:符号链接创建与路径映射

CLI在~/.skills/skills/下为该技能创建符号链接:ln -s /tmp/_skills/<hash> ~/.skills/skills/github-com-dietrichgebert-ponytail-v1.0.0。同时,在~/.skills/registry.json中写入完整记录:

{ "id": "github-com-dietrichgebert-ponytail-v1.0.0", "name": "ponytail", "version": "1.0.0", "repository": "https://github.com/dietrichgebert/ponytail.git", "entrypoint": "./bin/ponytail.js", "installedAt": "2024-05-22T08:32:15.123Z" }

注意:entrypoint是相对路径,实际执行时由Agent运行时拼接~/.skills/skills/<id>/前缀。因此,entrypoint中的../向上跳转是被禁止的——这既是安全限制,也是路径解析可靠性的保障。

2.6 第六步:依赖自动安装(仅限Node.js技能)

如果entrypoint指向.js文件,CLI会检查该目录下是否存在package.json。存在则执行npm install --production --no-audit --no-fund(注意:禁用auditfund以加速安装)。这里的关键细节是:安装目录是~/.skills/skills/<id>/,而非临时克隆目录。所以即使你克隆时修改了package.json,最终生效的是符号链接指向目录下的package.json。这也是为什么有些技能要求你先git clone手动修改再npx skill add .——因为直接npx skill add github-url会跳过你的本地修改。

2.7 第七步:注册表持久化与清理

最后,CLI将更新后的registry.json写回磁盘,并删除/tmp/_skills/<hash>临时目录。整个过程耗时通常在1.2~3.8秒之间(实测Mac M2 Pro,网络延迟<50ms)。但如果你的~/.skills/registry.json文件权限为600(仅所有者可读写),而CLI以root用户运行(比如你误用了sudo npx skill add),就会因权限不足写入失败,导致注册表状态不一致——此时npx skill list显示已安装,但Agent加载时读取到的仍是旧注册表。

注意:npx skill add的退出码(exit code)是唯一可靠的执行结果信号。成功为0,任何非零值都表示失败,但错误信息可能被日志级别过滤。建议在CI/CD流程中始终检查$?,而非依赖终端输出文字。

3. 技能注册表的双态结构:本地缓存与远程索引的协同机制

skills体系最易被误解的设计,是它看似简单的注册表~/.skills/registry.json,实则承载着两种截然不同的状态:本地缓存态(Local Cache State)远程索引态(Remote Index State)。它们不是主从关系,而是协作关系——理解这点,才能避开80%的“技能找不到”类问题。

3.1 本地缓存态:Agent运行时的唯一真相源

本地注册表~/.skills/registry.json是Agent启动时加载技能的唯一依据。它不包含技能代码,只存元数据和符号链接路径。当Agent执行skill.execute('ponytail', {input: 'test'})时,它做的第一件事是:

  1. registry.json中查找name === 'ponytail'的条目;
  2. 拼接entrypoint路径:~/.skills/skills/<id>/+entrypoint
  3. child_process.spawn()启动该文件,传入标准化的JSON-RPC 2.0请求体。

这意味着:即使远程GitHub仓库已被删除,只要本地注册表存在且符号链接有效,技能仍可正常执行。我曾故意删掉dietrichgebert/ponytail仓库,本地Agent依然跑了三天才报错——因为~/.skills/skills/...目录下的代码副本完好无损。这种设计保证了离线可用性,但也带来风险:技能作者发布安全补丁后,你必须手动npx skill update ponytail才能获取,npx skill add不会自动刷新。

3.2 远程索引态:技能发现与版本协商的中枢

与本地缓存并存的是一个隐式的远程索引服务,地址为https://index.skills.dev/v1(非公开API,但CLI源码硬编码)。当你执行npx skill search weather时,CLI会向该端点发送GET请求:

GET /v1/search?q=weather&limit=10

返回结果是一个JSON数组,每项包含:

  • name: 技能名称(用于npx skill add
  • description: 简短描述
  • latestVersion: 最新语义版本号
  • repository: GitHub URL
  • score: 基于Star数、Fork数、更新频率的综合评分

这个索引不存储技能代码,也不验证元数据格式。它只是一个聚合搜索引擎,爬取GitHub上所有包含skill.yaml文件的公开仓库。因此,当你搜索claude,返回结果为空——因为Claude官方仓库没有skill.yaml。而搜索pdf,会返回pdf-to-textpdf-merge等真实技能,它们的作者主动提交了索引申请(通过向index.skills.dev提交PR)。

3.3 双态同步的断裂点:何时会“找不到技能”?

“Agent execution terminated due to error.”这类模糊错误,90%源于双态不同步。典型断裂场景有三个:

场景一:注册表损坏导致ID解析失败
registry.json被意外编辑(比如用文本编辑器手动删掉一个逗号),JSON格式损坏。Agent启动时JSON.parse()抛出异常,但错误日志被静默吞掉,只显示execution terminated。修复方法:npx skill list会因解析失败报错,这是最早发现损坏的信号;手动用jq '.' ~/.skills/registry.json验证格式。

场景二:符号链接失效引发路径错误
你执行了rm -rf ~/.skills/skills/*,但忘了同步删除registry.json中的对应条目。Agent加载时找到ID,拼接路径~/.skills/skills/<id>/bin/ponytail.js,但该路径不存在,spawn ENOENT错误被包装成通用终止错误。此时npx skill list仍显示已安装,但npx skill validate ponytail会明确报错Entry point not found

场景三:远程索引过期造成版本错配
你用npx skill add my-skill@0.1.0安装,但远程索引最新版已是0.2.0。当你后续执行npx skill update my-skill,CLI会从索引获取0.2.0的仓库URL,克隆新版本,但不会自动迁移旧注册表条目——它创建新IDgithub-com-myorg-myskill-v0.2.0,而Agent仍在用旧ID调用。结果就是两个版本共存,npx skill list显示两条记录,但Agent只认最初安装的那个。

实操心得:我建立了一个每日自动检查脚本(放在crontab),用npx skill list --json | jq -r '.[] | select(.installedAt < "2024-05-01") | .name'找出三个月未更新的技能,再批量执行npx skill update。这比等Agent报错后再处理高效得多。

4. 技能执行沙箱的底层约束:为什么你的Python技能总在Windows上崩溃

skills体系宣称“支持任意语言”,但实测中,Python、Rust、Go编写的技能在Windows上的失败率远高于Node.js技能。这不是偶然,而是由执行沙箱的四个硬性约束共同决定的——这些约束在官方文档里被刻意淡化,但在源码runtime/executor.js中有明确实现。

4.1 约束一:进程启动方式强制为spawn,禁用exec

skillsCLI和Agent运行时一律使用Node.js的child_process.spawn()启动技能进程,而非exec()。这意味着:

  • spawn创建的是独立子进程,父进程(Agent)与子进程(技能)通过stdin/stdout管道通信;
  • exec会将命令字符串交给shell解析(如cmd.exebash),存在注入风险,且无法精确控制进程生命周期。

对Python技能的影响是致命的:如果你的entrypointpython script.pyspawn会尝试直接执行python这个文件(找不到),而非调用cmd.exe /c python script.py。正确写法必须是./run.bat(Windows)或./run.sh(Unix),并在脚本中显式调用解释器。我见过最典型的错误是:作者在macOS上测试entrypoint: "python main.py",一切正常;但Windows用户执行时,spawn找不到名为python的可执行文件(因为python.exeC:\Python39\python.exe),直接报spawn python ENOENT

4.2 约束二:标准输入输出必须为UTF-8编码,且禁用BOM

技能进程的stdinstdout流被强制设置为utf8编码。如果Python技能用open('file.txt', 'w')写入文件,默认编码是系统locale(Windows上常为cp1252),但Agent发送的JSON-RPC请求体是UTF-8,技能解析时若未显式指定encoding='utf-8',就会抛出UnicodeDecodeError。更隐蔽的问题是BOM(Byte Order Mark):某些Windows编辑器保存.py文件时自动添加BOM,导致Python解释器读取首行失败,报错SyntaxError: Non-UTF-8 code starting with '\xff'。解决方案只有两个:1)用VS Code等编辑器确保保存为“UTF-8 无BOM”;2)在Python脚本开头添加# -*- coding: utf-8 -*-

4.3 约束三:环境变量继承被严格过滤

Agent运行时启动技能进程时,会清除所有用户自定义环境变量,只保留白名单:PATHHOMEUSERPROFILE(Windows)、TMPDIR。这意味着:

  • 你的Python技能若依赖os.getenv('MY_API_KEY'),该变量永远为None
  • Rust技能若用std::env::var("RUST_LOG")调试,会得到Err(NotPresent)

正确做法是在skill.yaml中声明environment字段:

environment: PYTHONPATH: "./lib" API_KEY: "${SECRET_API_KEY}"

然后Agent会在启动进程前,将SECRET_API_KEY从本地密钥环(Keychain on macOS, Credential Manager on Windows)读取并注入。但注意:"${SECRET_API_KEY}"语法是skills特有,标准Shell不识别——所以entrypoint脚本必须由skills运行时解析,不能直接chmod +x后手动执行。

4.4 约束四:信号传递机制在Windows上不完整

Unix系统中,Agent可通过process.kill('SIGTERM')优雅终止技能进程,技能可捕获信号执行清理。但Windows不支持SIGTERMskills运行时退化为taskkill /pid <pid> /f(强制终止)。这导致:

  • Python技能中atexit.register()注册的函数不会执行;
  • 数据库连接不会被close(),可能留下锁文件;
  • 临时文件不会被shutil.rmtree()清理。

我的解决方案是:在Python技能入口处,用signal.signal(signal.CTRL_C_EVENT, handler)捕获Ctrl+C(Windows上SIGINT的等效信号),并监听stdin关闭事件(sys.stdin.closed)作为备用终止信号。虽然不够完美,但比完全无响应好得多。

踩坑实录:我曾为一个Rust技能写了完美的Droptrait清理逻辑,本地测试一切正常。上线后用户报告“每次执行后磁盘空间暴涨”。排查发现:Windows上taskkill /f直接结束进程,Drop根本没机会运行。最终方案是:在main()函数末尾,无论是否异常退出,都强制调用cleanup()函数——把“优雅”变成“暴力但确定”。

5. 从零构建一个可发布的技能:以“本地代码质量扫描”为例的全流程实战

光理解原理不够,必须亲手造一个。下面以“code-quality-scan”技能为例,带你走完从设计到发布的完整闭环。这个技能接收一个文件路径,用ESLint和Prettier检查代码风格,返回JSON格式的违规列表。它不依赖云服务,纯本地执行,适合作为Agent的内置质量门禁。

5.1 步骤一:初始化项目结构与元数据

创建目录code-quality-scan,结构如下:

code-quality-scan/ ├── skill.yaml # 技能元数据(必需) ├── package.json # Node.js依赖(必需) ├── bin/ │ └── scan.js # 入口脚本(必需) ├── lib/ │ ├── eslint-config.js # ESLint配置(可选) │ └── prettier-config.js # Prettier配置(可选) └── test/ └── sample.js # 测试用例(推荐)

skill.yaml内容(严格遵循MCP v0.4 Schema):

name: "code-quality-scan" version: "1.0.0" description: "Scan JavaScript/TypeScript files for style and quality issues using ESLint and Prettier." repository: "https://github.com/yourname/code-quality-scan" entrypoint: "./bin/scan.js" author: "Your Name <your.email@example.com>" license: "MIT" inputSchema: type: "object" properties: filePath: type: "string" description: "Path to the file to scan, relative to current working directory." required: ["filePath"] outputSchema: type: "object" properties: issues: type: "array" items: type: "object" properties: ruleId: type: "string" severity: type: "string" enum: ["error", "warning"] message: type: "string" line: type: "integer" success: type: "boolean" required: ["issues", "success"]

5.2 步骤二:编写健壮的入口脚本

bin/scan.js是核心,必须处理所有边界情况:

#!/usr/bin/env node // 必须有shebang,否则Windows下spawn失败 const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs').promises; // 1. 读取标准输入(Agent发送的JSON-RPC请求) let input = ''; process.stdin.setEncoding('utf8'); process.stdin.on('data', chunk => input += chunk); process.stdin.on('end', async () => { try { const req = JSON.parse(input); // 验证JSON-RPC格式 if (!req.jsonrpc || req.method !== 'execute') { throw new Error('Invalid JSON-RPC request'); } const { filePath } = req.params; if (!filePath || typeof filePath !== 'string') { throw new Error('Missing or invalid filePath parameter'); } // 2. 构建绝对路径(防止路径遍历攻击) const absPath = path.resolve(process.cwd(), filePath); // 安全检查:确保路径在当前工作目录下 if (!absPath.startsWith(process.cwd() + path.sep)) { throw new Error('Path traversal attempt detected'); } // 3. 检查文件是否存在且可读 await fs.access(absPath, fs.constants.R_OK); // 4. 执行ESLint和Prettier(此处简化,实际应调用CLI) const eslintResult = await runESLint(absPath); const prettierResult = await runPrettier(absPath); // 5. 合并结果并返回JSON-RPC响应 const response = { jsonrpc: '2.0', id: req.id, result: { issues: [...eslintResult.issues, ...prettierResult.issues], success: eslintResult.success && prettierResult.success } }; process.stdout.write(JSON.stringify(response) + '\n'); process.exit(0); } catch (err) { // 6. 错误处理:返回JSON-RPC error响应 const response = { jsonrpc: '2.0', id: req.id || 1, error: { code: -32603, message: err.message, data: { stack: err.stack } } }; process.stdout.write(JSON.stringify(response) + '\n'); process.exit(1); } }); async function runESLint(filePath) { // 实际应spawn eslint --format=json --no-error-on-unmatched-pattern return { issues: [], success: true }; } async function runPrettier(filePath) { // 实际应spawn prettier --check --loglevel warn return { issues: [], success: true }; }

5.3 步骤三:配置package.json与依赖管理

package.json必须指定"type": "module"(ESM),因为skillsCLI内部用ESM加载:

{ "name": "code-quality-scan", "version": "1.0.0", "type": "module", "bin": { "scan": "./bin/scan.js" }, "dependencies": { "eslint": "^8.56.0", "prettier": "^3.2.5" }, "engines": { "node": ">=18.0.0" } }

关键点:"bin"字段让npx skill add能正确识别入口;"engines"确保Agent运行时检查Node版本。

5.4 步骤四:本地测试与调试技巧

不要等npx skill add后再测试。用skillsCLI自带的调试模式:

# 在项目根目录执行 npx skills@latest dev --entrypoint ./bin/scan.js

这会启动一个本地HTTP服务器(http://localhost:3000),你可用curl模拟Agent请求:

curl -X POST http://localhost:3000/execute \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "execute", "params": {"filePath": "test/sample.js"}, "id": 1 }'

响应会是标准JSON-RPC格式。调试时,console.log()输出会显示在终端,process.stderr.write()可用于输出调试信息(不会被Agent捕获)。

5.5 步骤五:发布到GitHub并提交索引

  1. 创建GitHub仓库,推送代码;
  2. 在仓库Settings → Secrets → Actions中,添加SKILLS_INDEX_TOKEN(从index.skills.dev申请);
  3. 创建.github/workflows/publish.yml,在push时自动触发索引提交;
  4. 手动执行npx skill add github:yourname/code-quality-scan,验证安装;
  5. npx skill list | grep "code-quality-scan"确认注册成功。

至此,你的技能已可被任何兼容MCP的Agent发现、安装、调用。它不是玩具,而是生产级的质量门禁组件——这就是skills体系真正的价值:把重复的工具集成,变成标准化的、可组合的、可审计的技能单元。

最后分享一个小技巧:在skill.yamldescription字段里,用Markdown写详细使用示例(如## Usage\n```json\n{ \"filePath\": \"src/index.js\" }\n```)——npx skill info code-quality-scan`会原样渲染,成为其他开发者最快的上手指南。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询