番外03:MCP 与工具转换体系——从协议到 Skill
📌 本文是《从零吃透企业级 AI 平台:元景万悟源码学习手记》第一季·番外篇的第 3 篇
🔗 原理对照:回链第一季 06「MCP 协议实战」。06 讲了 MCP 协议标准(ListTools/CallTool/JSON-RPC)和万悟的 mcp-service。本篇深挖万悟的真正特色——MCP→Skill 和 OpenAPI→Skill 的自动转换引擎。
🎯 读完本文:① 理解 mcp2skill 的 6 种使用模式 ② 看懂 OpenAPI→Skill 的分组/过滤/Schema 策略 ③ 掌握渐进式披露设计 ④ 理解万悟如何把外部工具"本地化"为 Skill
⏱️ 预计阅读时间:30 分钟 | 动手实践:30 分钟
⚠️ 诚实说明:本文基于 pkg/mcp2skill/ 和 pkg/openapi2skill/ 的完整 README(GitHub 已验证)。这两个包的文档极为详尽——含完整 API、数据结构、使用示例、对比表——可以写到代码级。
一、这篇文章要解决什么问题?
第一季 06 教了你 MCP 协议——"一个标准化的工具调用协议,让 AI 能用 GitHub、Slack、数据库等外部工具"。
但你有没有想过一个更深的问题:
接通了工具 ≠ Agent 会用工具。
MCP Server 可能暴露几十个、上百个工具。Agent 的 prompt 塞得下这么多工具描述吗?就算塞得下了,LLM 能从中准确选择吗?选错了怎么办?
万悟的做法不是"把所有工具平铺给 Agent",而是通过 mcp2skill 和 openapi2skill 两个转换引擎,把外部工具本地化为结构化 Skill——附带分类、渐进式加载、按需检索。
三个核心问题:
-
mcp2skill 怎么把 MCP 工具转成 Skill? 生成的目录结构长什么样?6 种使用模式分别解决什么问题?
-
openapi2skill 怎么处理庞大的 API 规范? 一个 OpenAPI spec 可能有几百个端点——怎么分组?怎么过滤?怎么生成 Skill 文档?
-
为什么这套转换比"直接把工具扔给 Agent"更好? 渐进式披露、按需加载、工具搜索——这些设计在代码中怎么体现?
二、核心概念

2.1 为什么需要转换?

直接让 Agent 调用 MCP 工具的痛点:
❌ 直接把 50 个 MCP 工具塞给 Agent→ System Prompt 暴涨→ LLM 混淆相似工具("get_user" vs "list_users" vs "search_users")→ Token 成本翻倍(每次对话都要传全套工具描述)→ 新工具上线要改 Agent prompt
转换后的 Skill 方案:
✅ mcp2skill/openapi2skill 把工具转为结构化 Skill→ SKILL.md 只写技能概览(~200 字)→ 每个工具一个独立 .md 文件(按需加载)→ Agent 先读概览 → 按关键词搜索具体工具 → 只加载需要的工具文件→ 新工具上线 = 重建 Skill,Agent prompt 不变
2.2 渐进式披露(Progressive Disclosure)
这是万悟 Skill 体系的核心设计理念。
Layer 1: SKILL.md ← Agent 最先看到(概览,极小)↓ "我需要发个 Slack 消息" ← Agent 按需求向下探索
Layer 2: references/operations/ ← 按工具名搜索(按需加载)↓ "send_message 的参数是什么" ← Agent 找到具体工具文档
Layer 3: references/schemas/ ← 复杂类型需要看 Schema 定义
每一层只加载需要的信息,而不是把整个 Skill 一次性塞给 LLM。对比传统方式节省 70-90% 的 token。
2.3 mcp2skill vs openapi2skill
| 维度 | mcp2skill | openapi2skill |
|---|---|---|
| 输入 | MCP Server 的 SSE URL | OpenAPI 3.x spec(JSON/YAML) |
| 输出来源 | 运行时调用 list_tools() |
静态解析 spec 文件 |
| 适用 | 已有 MCP Server(GitHub/Slack/数据库) | 已有 OpenAPI 文档的 REST API |
| 工具数量 | 通常 5-30 个 | 可能 50-500+ 个 |
| 分组策略 | 按 tool annotations 分组 | 按 resource + operation 分组 |
三、源码拆解
3.1 mcp2skill:MCP → Skill 自动转换
目录结构
pkg/mcp2skill/
├── cmd/ # CLI 入口:mcp2skill convert
├── auth.go # URL Key 脱敏(安全)
├── converter.go # 核心转换逻辑
└── README.md # 详细的文档# 生成的 Skill 目录:
{outputDir}/{skillName}/
├── SKILL.md # 技能入口概览
├── scripts/
│ └── mcp_client.py # 自动生成的 Python MCP 客户端
└── references/operations/├── {tool-name-1}.md # 工具 1 详情├── {tool-name-2}.md # 工具 2 详情└── ...
6 种使用模式
| # | 模式 | 命令/参数 | 适用场景 |
|---|---|---|---|
| 1 | 基础模式 | mcp2skill --url <SSE_URL> |
最简单,自动发现所有工具 |
| 2 | 过滤模式 | mcp2skill --url <URL> --include "send,create" --exclude "delete" |
只想暴露部分工具给 Agent |
| 3 | 认证模式 | mcp2skill --url <URL> --auth-header "Bearer xxx" |
MCP Server 需要认证 |
| 4 | 自定义输出 | mcp2skill --url <URL> --output ./skills/github |
指定输出目录 |
| 5 | 重命名 | mcp2skill --url <URL> --skill-name "github-tools" |
自定义 Skill 名称 |
| 6 | 批量转换 | mcp2skill --config config.yaml |
批量处理多个 MCP Server |
💡 过滤模式是最常用的:你不需要把所有 GitHub MCP 工具都给 Agent——"delete_repo" 这种危险工具应该在 Skill 创建时就排除,而不是等 Agent 调用了再拦截。
converter.go 核心逻辑(教学重建版)
// 教学重建版(真实实现:pkg/mcp2skill/converter.go)
type Converter struct {input string // MCP Server SSE URLoutputDir string // 输出目录skillName string // Skill 名称includes []string // 白名单excludes []string // 黑名单authHeader string // 认证头
}func (c *Converter) Convert(ctx context.Context) error {// Step 1: 连接 MCP Server,调用 list_tools()client := mcp.NewClient(c.input)tools, err := client.ListTools(ctx)if err != nil {return fmt.Errorf("list tools: %w", err)}// Step 2: 过滤工具tools = c.filterTools(tools)// 按 annotations 分组:readOnly / destructive / idempotenttools = c.groupByAnnotation(tools)// Step 3: 生成 SKILL.md(概览文件)c.generateSkillMD(tools)// Step 4: 生成 mcp_client.py(Python MCP 客户端)c.generatePythonClient(tools)// Step 5: 为每个工具生成独立 .md 文件for _, tool := range tools {c.generateToolMD(tool)}return nil
}
生成的 SKILL.md 示例
# GitHub Tools一个通过 Model Context Protocol (MCP) 连接 GitHub API 的技能。## 可用工具| 工具名 | 说明 | 类型 |
|--------|------|------|
| search_issues | 搜索 Issues | readOnly |
| create_issue | 创建新 Issue | destructive |
| list_repos | 列出仓库列表 | readOnly |
| get_pull_request | 获取 PR 详情 | readOnly |
| create_pr_comment | 在 PR 上添加评论 | destructive |## 使用说明本技能连接 GitHub API v3。所有操作基于当前认证账号的权限范围。详细工具文档见 `references/operations/`。
生成的工具详情文件示例(references/operations/search_issues.md)
# search_issues在 GitHub 仓库中搜索 Issues。## 参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `query` | string | 是 | 搜索关键词,支持 GitHub 搜索语法 |
| `repo` | string | 是 | 仓库名(格式:owner/name) |
| `state` | string | 否 | 状态:open/closed/all,默认 open |
| `labels` | string[] | 否 | 按标签过滤 |## 返回
- 匹配的 Issue 列表(标题、编号、状态、创建者、URL)## 示例
```json
{"tool": "search_issues", "params": {"query": "bug", "repo": "myorg/myrepo", "state": "open"}}
#### auth.go:URL Key 脱敏```go
// 教学重建版(真实实现:pkg/mcp2skill/auth.go)
// 如果 MCP Server URL 含 API Key,自动脱敏
// 例如: https://api.github.com?key=ghp_xxxx → https://api.github.com?key=***
func sanitizeURL(rawURL string) string {u, _ := url.Parse(rawURL)q := u.Query()for k := range q {if strings.Contains(strings.ToLower(k), "key") ||strings.Contains(strings.ToLower(k), "token") ||strings.Contains(strings.ToLower(k), "secret") {q.Set(k, "***")}}u.RawQuery = q.Encode()return u.String()
}
3.2 openapi2skill:OpenAPI → Skill 转换
处理的是 REST API(有 OpenAPI 文档),输出结构更丰富。
目录结构
pkg/openapi2skill/
├── cmd/ # CLI 入口
├── README.md # 详细文档
└── (转换逻辑)# 生成的 Skill 目录:
{outputDir}/{skillName}/
├── SKILL.md # API 入口概览
└── references/├── resources/ # 按资源分组的操作索引│ ├── users.md # 用户资源的所有操作│ ├── orders.md # 订单资源的所有操作│ └── ...├── operations/ # 每个操作一个详情文件│ ├── users.list.md│ ├── users.create.md│ └── ...├── schemas/ # 按命名前缀分组的 Schema│ ├── user.md # User 类型的 Schema 展开│ ├── order.md # Order 类型的 Schema 展开│ └── ...└── authentication.md # 认证方案文档
分组策略
openapi2skill 的分组比 mcp2skill 更精细——因为 API 的端点数量可能很大:
OpenAPI Spec│├─ 按 resource 分(/users → user resource)│ ├─ 按 operation 分(list/create/get/update/delete)│ └─ 同 resource 的相关 schema 放一起│├─ 按 schema 分(命名前缀相同的放一起)│ └─ User / UserCreate / UserUpdate → user schema 组│└─ 认证信息单独提取
Schema 策略
OpenAPI spec 中的嵌套 schema 对 LLM 来说是灾难——User { address: { street, city, zip }, orders: [{ id, items: [{ sku, qty }] }] } 展开后极长。openapi2skill 的做法:
- 按命名前缀分组:
User/UserCreate/UserUpdate→user.md - 展开第一层引用:展开直接引用(
$ref: '#/components/schemas/User') - 深度引用留链接:第三层以上的嵌套不展开,留链接"详见
orders.md" - 去重:同一 schema 被多个端点引用时只在 schemas/ 出现一次
3.3 工具注解类型(DeepWiki 揭示)
MCP 协议定义了四种工具注解,mcp2skill 在生成 Skill 时利用它们做分组:
| 注解 | 含义 | 分组行为 |
|---|---|---|
| readOnlyHint | 只读操作 | 安全工具,默认全部包含 |
| destructiveHint | 破坏性操作(删除/修改) | 高危工具,生成警告标注 |
| idempotentHint | 幂等操作(可安全重试) | 标注"失败可重试" |
| openWorldHint | 开放世界操作 | 标注"结果可能不完整" |
💡 这四种注解是 MCP 协议的一部分。但不是所有 MCP Server 都正确设置了。mcp2skill 在生成 Skill 时会检查——如果 tools 缺少注解,会在 SKILL.md 标注"⚠️ 工具未声明安全级别,请手动审查"。
3.4 MCP Square:前端集成
转换只是第一步。万悟的 MCP 能力还通过前端市场(MCP Square)展示:
web/src/views/mcpManagementPublic/
├── square.vue # MCP 工具市场——浏览、搜索、安装
├── detail.vue # MCP Server 详情页(工具列表 + 参数 + 描述)
└── sendDialog.vue # 测试工具调用
MCP Square 让非开发者也能发现和安装 MCP 工具——搜索一个工具(如"发邮件"),点"安装",mcp2skill 在后台自动生成 Skill,Agent 立即可用。
四、动手实操
1. 用 mcp2skill 转换一个 GitHub MCP 工具
# 假设你有一个 GitHub MCP Server 跑在本地
mcp2skill convert \--url http://localhost:8080/sse \--skill-name "github-tools" \--include "search_issues,create_issue,list_repos,get_pr" \--exclude "delete_repo,delete_branch" \--output ./skills/
观察生成的 Skill 结构:
ls -R ./skills/github-tools/
# SKILL.md ← 概览
# scripts/mcp_client.py ← Python 客户端
# references/operations/ ← 每个工具一个文件
2. 用 openapi2skill 转换一个 REST API
openapi2skill convert \--spec https://api.example.com/openapi.json \--skill-name "example-api" \--resource-filter "users,orders" \--auth-header "Bearer xxx" \--output ./skills/
3. 验证渐进式披露的效果
对比两种方式的 token 消耗:
- 方式 A:把原始 OpenAPI spec 的全部端点描述塞入 System Prompt
- 方式 B:用 Skill(先加载 SKILL.md,按需加载 operations/)
方式 B 在工具数 > 10 时通常节省 70%+ token。
五、Mini 版 / 踩坑录
Mini 版:简易 MCP 客户端(~30 行 Python)
# 教学重建版——连接 MCP Server 并列出工具
import json, requestsclass MCPClient:def __init__(self, url: str):self.url = urlself.req_id = 0def _call(self, method: str, params: dict = None) -> dict:self.req_id += 1payload = {"jsonrpc": "2.0","id": self.req_id,"method": method,"params": params or {}}resp = requests.post(self.url, json=payload)return resp.json()def list_tools(self) -> list:result = self._call("tools/list")return result.get("result", {}).get("tools", [])def call_tool(self, name: str, arguments: dict) -> dict:return self._call("tools/call", {"name": name,"arguments": arguments})# 使用
client = MCPClient("http://localhost:8080/sse")
tools = client.list_tools()
for t in tools:print(f"{t['name']}: {t['description']}")
踩坑录
| 踩坑 | 现象 | 原因 | 解法 |
|---|---|---|---|
| 工具名冲突 | 两个 MCP Server 的 search 重名 |
mcp2skill 默认用工具原名 | --prefix 参数加前缀 |
| Schema 展开过长 | openapi2skill 生成的文件几百 KB | 嵌套 schema 全展开了 | 限制展开深度=2 |
| SSE 连接超时 | mcp2skill 调用 list_tools 超时 | MCP Server 响应慢 | 加 --timeout 参数 |
| 认证信息泄露 | 生成的 SKILL.md 里含 API Key | auth.go 没拦截自定义 header | 加 --sanitize 强制脱敏 |
六、总结 & 延伸阅读
⏱️ 30 秒速览
这篇你只需要记住 3 件事:
- 万悟特色是 MCP→Skill 和 OpenAPI→Skill 自动转换(pkg/mcp2skill、pkg/openapi2skill)
- 转换 = 协议适配 + 工具 Schema 生成 + 运行时注入,第三方 API 秒变 Agent 工具
- 两个 MCP 库:ThinkInAIXYZ/go-mcp + mark3labs/mcp-go
想深挖?
- 转换引擎实现:§2 核心概念;完整转换器见 GitHub
pkg/mcp2skill与pkg/openapi2skill
本文要点回顾
- ✅ mcp2skill 把 MCP 工具转为结构化 Skill——SKILL.md 概览 + 每个工具一个独立 .md 文件
- ✅ 6 种使用模式覆盖:基础/过滤/认证/自定义输出/重命名/批量——过滤模式最常用
- ✅ openapi2skill 处理 REST API,按 resource/operation/schema 三级分组
- ✅ 渐进式披露:概览 → 按需搜索 → 具体工具文档,节省 70-90% token
- ✅ MCP 工具的 4 种注解(readOnly/destructive/idempotent/openWorld)用于安全分组
- ✅ MCP Square 前端市场让非开发者也能发现和安装工具——搜索→安装→Agent 立即可用
架构决策回顾
| 决策 | 选了 | 理由 |
|---|---|---|
| 输出格式 | Markdown Skill | LLM 最擅长的格式,人类也可读 |
| 存储策略 | 独立文件 /references/operations/ | 支持按需加载,不一次性塞给 LLM |
| Schema 深度 | 展开最多 2 层 | 平衡完整性和 token 消耗 |
| 工具过滤 | include/exclude 白名单+黑名单 | 危险工具在 Skill 层就排除,不等 Agent 调用 |
| URL 脱敏 | 自动检测 + 替换 *** | 安全第一,默认行为 |
延伸阅读
- 第一季 06「MCP 协议实战」—— MCP 协议基础(ListTools/CallTool/JSON-RPC)
- 第一季 05「Agent 推理引擎」—— Agent 如何使用 Skill 中的工具
pkg/mcp2skill/README.md—— 万悟仓库中 mcp2skill 的完整文档(非常详尽)pkg/openapi2skill/README.md—— openapi2skill 的完整文档- Model Context Protocol —— MCP 官方规范
📱 关注公众号,追更不迷路
本系列文章首发于微信公众号「农夫三拳有点癫」,每周更新源码拆解与架构实战。
在微信扫描下方二维码即可关注:
