如何制作一个 skill
本文介绍 skill、MCP、CLI 三者的关系,并提供一个基于 CLI 实现 skill 的具体案例。
本文介绍 skill、MCP、CLI 三者的关系,并用一个基于 CLI 实现的 skill 作为例子,说明一个 skill 大概应该包含哪些内容。
先给出一个粗略结论:
- MCP 解决的是客户端和工具服务器之间“怎么通信”的问题。
- CLI 解决的是模型如何借助命令行工具完成具体任务的问题。
- skill 解决的是“什么时候用、怎么用、参考资料在哪里、辅助脚本怎么跑”的问题。
也就是说,MCP 更像协议层,CLI 更像执行层,skill 更像给模型看的操作手册和工具包。
先说 MCP
MCP 是一种协议,它规定的是客户端和 MCP 服务器之间的交互规则。
这里不需要关心底层具体通过什么方式通信。它可能是 HTTP,也可能是 stdio。真正重要的是:客户端和服务器之间传递的消息长什么样,以及工具能力如何被发现和调用。
MCP 协议本身基于 JSON-RPC 2.0。按照 MCP 的设计,服务器可以暴露三类能力:
tools/list
tools/call
resources/list
resources/read
prompts/list
prompts/get
其中:
tools系列用于暴露和调用工具。resources系列用于暴露一些资料或上下文。prompts系列用于暴露提示词模板。
后两者当然也有用途,但在日常做 agent 或工具集成时,最常用、也最关键的通常还是 tools。
一次工具调用的完整流程
下面看一次 MCP 工具调用的大致过程。
1. Client 初始化
Client 先告诉 Server:我是谁、我支持什么协议版本、我有什么能力。
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"clientInfo": {
"name": "codex",
"version": "1.0.0"
},
"capabilities": {
"roots": {},
"sampling": {}
}
}
}
2. Server 返回自身信息
Server 返回:我是谁、我支持什么协议版本、我暴露了哪些能力。
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"serverInfo": {
"name": "Playwright",
"version": "1.61.0"
},
"capabilities": {
"tools": {}
}
}
}
3. Client 通知初始化完成
这是一个 notification,没有 id,所以 Server 不需要回复。
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {}
}
4. Client 请求工具列表
初始化完成后,Client 可以请求 Server 暴露的工具列表。
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}
5. Server 返回工具定义
Server 返回自己暴露的工具。工具定义里最重要的是三部分:
name:工具名称,后续调用时使用。description:工具描述,给模型判断什么时候该用它。inputSchema:参数结构,告诉模型这个工具需要什么参数。
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "browser_navigate",
"description": "Navigate to a URL",
"inputSchema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to navigate to"
}
},
"required": ["url"]
},
"annotations": {
"title": "Navigate to a URL",
"readOnlyHint": false,
"destructiveHint": true,
"openWorldHint": true
}
},
{
"name": "browser_snapshot",
"description": "Capture accessibility snapshot of the current page",
"inputSchema": {
"type": "object",
"properties": {}
},
"annotations": {
"title": "Capture snapshot",
"readOnlyHint": true
}
}
]
}
}
6. Client 调用工具
当模型决定要打开网页时,Codex 运行时会向 MCP Server 发送 tools/call。
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "browser_navigate",
"arguments": {
"url": "https://example.com"
}
}
}
7. Server 执行并返回结果
Server 执行 Playwright 操作,然后把结果返回给 Client。
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "### Ran Playwright code\n```js\nawait page.goto('https://example.com');\n```\n\n### Page\n- Page URL: https://example.com\n- Page Title: Example Domain\n\n### Snapshot\n```yaml\n- document:\n - heading \"Example Domain\"\n - paragraph: This domain is for use in illustrative examples\n```"
}
],
"isError": false
}
}
到这里,客户端和 MCP 服务器之间的一次工具调用就完成了。
客户端、模型和 MCP Server 的关系
上面讲的是客户端和 MCP Server 之间的通信。接下来再看客户端如何把这些工具能力交给模型使用。
1. 客户端把工具注入模型请求
客户端拿到 MCP Server 的 tool 列表后,会解析这些工具定义,并把工具名、描述、参数结构放进发给模型的请求里。
以 Codex 为例,模型请求大概像这样:
{
"model": "gpt-5.4",
"instructions": "You are Codex, a coding agent. Use tools when needed and explain results clearly.",
"input": [
{
"role": "user",
"content": [
{
"type": "input_text",
"text": "打开 https://example.com 看一下页面"
}
]
}
],
"tools": [
{
"type": "function",
"name": "browser_navigate",
"description": "Navigate to a URL",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The URL to navigate to"
}
},
"required": ["url"],
"additionalProperties": false
}
},
{
"type": "function",
"name": "browser_snapshot",
"description": "Capture accessibility snapshot of the current page",
"parameters": {
"type": "object",
"properties": {},
"additionalProperties": false
}
}
],
"tool_choice": "auto"
}
2. 模型决定是否调用工具
模型根据用户问题和工具描述,决定是否要调用工具,以及调用哪个工具。
例如它可能返回一次 function_call:
{
"id": "resp_001",
"object": "response",
"status": "completed",
"model": "gpt-5.4",
"output": [
{
"id": "fc_001",
"type": "function_call",
"status": "completed",
"call_id": "call_001",
"name": "browser_navigate",
"arguments": "{\"url\":\"https://example.com\"}"
}
]
}
3. 客户端执行工具并把结果喂回模型
Codex 收到模型的 function_call 后,会真正调用 MCP Server。拿到工具执行结果后,再通过 function_call_output 把结果发回模型。
{
"model": "gpt-5.4",
"previous_response_id": "resp_001",
"input": [
{
"type": "function_call_output",
"call_id": "call_001",
"output": "### Ran Playwright code\n```js\nawait page.goto('https://example.com');\n```\n\n### Page\n- Page URL: https://example.com\n- Page Title: Example Domain\n\n### Snapshot\n```yaml\n- document:\n - heading \"Example Domain\"\n - paragraph: This domain is for use in illustrative examples in documents.\n - link \"More information...\"\n```"
}
],
"tools": [
{
"type": "function",
"name": "browser_navigate",
"description": "Navigate to a URL",
"parameters": {
"type": "object",
"properties": {
"url": {
"type": "string"
}
},
"required": ["url"],
"additionalProperties": false
}
},
{
"type": "function",
"name": "browser_snapshot",
"description": "Capture accessibility snapshot of the current page",
"parameters": {
"type": "object",
"properties": {},
"additionalProperties": false
}
}
]
}
最终,模型拿到工具结果后,生成面向用户的回答。
整个过程可以简化成下面这个时序:
用户 -> Codex
Codex -> OpenAI 模型:input + tools
OpenAI 模型 -> Codex:function_call
Codex -> MCP Server:tools/call
MCP Server -> Codex:tool result
Codex -> OpenAI 模型:function_call_output
OpenAI 模型 -> Codex:final message
Codex -> 用户
所以 MCP 本身并不直接“让模型变聪明”。它更像一个标准接口,让客户端能够稳定发现工具、调用工具、拿回工具结果。真正决定工具是否被使用的是模型,而负责把模型调用转成真实执行的是客户端运行时。
再说 CLI
CLI 相较于 MCP 更简单。
CLI 就是命令行工具。模型可以通过客户端提供的 shell tool 调用 Windows 命令行、PowerShell、Python 脚本或其他可执行程序,从而完成实际任务。
现在很多 skill 都是通过 CLI 来实现的。比如 Codex 的一些 skill,会在 scripts/ 目录里放 Python 脚本,然后在 SKILL.md 里告诉模型:
- 什么时候应该使用这个脚本。
- 脚本需要哪些参数。
- 输入文件和输出文件在哪里。
- 执行后应该如何校验结果。
CLI 和 MCP 的差异大概是:
| 维度 | MCP | CLI |
|---|---|---|
| 形式 | 协议和服务 | 命令行工具 |
| 工具发现 | 通过 tools/list 暴露 | 通过文档告诉模型 |
| 工具调用 | 通过 tools/call | 通过 shell 命令 |
| 参数约束 | 有结构化 schema | 依赖说明文档和脚本参数 |
| 适合场景 | 长期运行的工具服务、多客户端复用 | 本地脚本、文件处理、一次性任务 |
所以 CLI 少了一层协议,但也因此更轻。对于很多文件处理、生成、校验类任务来说,CLI 实现起来反而更直接。
再说 skill
以 Codex 为例,一个使用 CLI 的 skill 案例结构可以长这样。
这里拿 skill-creator 举例,也就是“用创建 skill 的 skill 来创建 skill”,有点套娃,但正好说明问题。
skill-creator/
├─ SKILL.md
├─ license.txt
├─ agents/
│ └─ openai.yaml
├─ assets/
│ ├─ skill-creator-small.svg
│ └─ skill-creator.png
├─ references/
│ └─ openai_yaml.md
└─ scripts/
├─ generate_openai_yaml.py
├─ init_skill.py
└─ quick_validate.py
各部分大概作用如下:
| 路径 | 作用 |
|---|---|
SKILL.md | skill 的核心说明,决定什么时候触发、触发后怎么工作。 |
agents/openai.yaml | Codex UI 展示元数据,比如名称、简介、图标、默认 prompt。 |
assets/ | 图标、图片等资源。 |
references/ | 额外参考文档,按需读取,避免 SKILL.md 太长。 |
scripts/ | 可执行辅助脚本,用来生成、初始化、校验。 |
license.txt | 许可证文本。 |
实际使用时,最重要的通常是这三个部分:
SKILL.mdreferences/scripts/
SKILL.md 是入口,它告诉模型这个 skill 是干什么的、什么时候该触发、触发后先读什么、要不要跑脚本、结果怎么检查。
references/ 适合放比较长、但不是每次都必须读的资料。例如 openai.yaml 的字段说明、某种文件格式的完整规范、复杂案例等。这样可以避免 SKILL.md 变得太长。
scripts/ 适合放确定性强的辅助逻辑。比如初始化目录、生成配置文件、校验结构、渲染预览、导出产物等。模型负责判断和编排,脚本负责稳定执行。
一个 skill 文档应该写什么
我觉得一个好用的 SKILL.md 至少应该说清楚这些事情:
- 这个 skill 什么时候应该被使用。
- 输入通常来自哪里,比如用户提供的文件、当前仓库、某个目录。
- 输出应该是什么,比如新建文件、修改文件、生成报告或完成校验。
- 需要优先读取哪些参考文档。
- 有哪些脚本可以用,每个脚本怎么调用。
- 完成后怎么验证,失败时怎么处理。
比如可以这样组织:
# Skill Name
## When to use
说明触发条件。
## Workflow
说明模型应该按什么顺序工作。
## References
说明需要时读取 references/ 下的哪些文件。
## Scripts
说明 scripts/ 下每个脚本的用途和调用方式。
## Verification
说明如何检查结果是否正确。
这样写的好处是,模型不用猜。它不需要从一堆零散文件里推断你的意图,而是可以按文档里的流程一步一步执行。
如何创建一个 skill
我的建议是:使用创建 skill 的 skill 来创建 skill。
原因很简单:skill 本身有一套目录结构和文档约定,如果手写,容易漏掉元数据、参考文档说明或校验脚本。用专门的创建工具初始化一遍,再在生成结果上补充业务说明,会更稳。
一个比较舒服的流程是:
- 先用
skill-creator初始化目录。 - 在
SKILL.md里写清楚触发条件和主流程。 - 把长规范、字段说明、案例放进
references/。 - 把确定性的生成、校验逻辑放进
scripts/。 - 用一个真实任务跑一遍,检查模型是否会按预期读取资料和调用脚本。
总结一下:MCP 负责标准化工具服务,CLI 负责执行具体命令,skill 负责把“何时使用、如何使用、如何验证”讲清楚。三者并不是互相替代的关系,而是处在不同层次上配合工作。