AI Agent技能实战:从 SKILL.md 到 MCP Server 的完整实现(TaoToken 统一 Key 接入篇)

发布时间:2026/10/12 15:03:25
AI Agent技能实战:从 SKILL.md 到 MCP Server 的完整实现(TaoToken 统一 Key 接入篇) 1. 为什么我要把 SKILL.md 和 MCP Server 绑在一起做AI Agent 的能力扩展这两年一直在换姿势。最早大家写死在代码里的 function calling后来变成 MCP 协议统一工具调用再到现在开始流行用一份 SKILL.md 把「能力是什么、怎么触发、参数长什么样」全部自描述出来。我最近在做一个 Rust 的小项目目标很明确让一个技能包既能被 Cursor、Claude Desktop 这类 IDE 通过 MCP 调用又能把工具鉴权和模型调用统一收敛到同一个 Key 通道上省得每接一个客户端就配一遍环境变量。SKILL.md 解决的是「能力定义」问题MCP Server 解决的是「能力暴露」问题而 TaoToken 解决的是「鉴权和模型通道」问题。这三件事拆开看都不复杂但拼在一起的时候最容易踩的坑就是工具调用要鉴权、模型调用也要鉴权两套 Key 两套 Base URL配置一多就乱。所以这篇我按「先定义 Schema再落地 MCP Server最后统一走一个 Key」的顺序写每一步都给可复制的代码和配置。适合谁看写过一点 Rust、想让自己的工具被 AI IDE 直接调用的同学或者已经在用 MCP但被多客户端配置搞烦、想把 Key 收敛到一处的同学。全文的检索核心词就是 AI Agent、SKILL.md、MCP Server、Rust、Schema我会围绕这几个词把链路走通。先说结论SKILL.md 用 YAML front matter 描述元信息正文写触发场景Rust 侧用 serde 解析成结构体再推断出 JSON SchemaMCP Server 用 stdio 跑 JSON-RPC 2.0把技能注册成 tool模型调用和工具鉴权都指向同一个 API 通道。下面从场景问题开始拆。2. 原问题与场景SKILL.md 解析和 MCP 注册为什么总对不上我一开始的做法很土SKILL.md 手写一份JSON Schema 再手写一份MCP Server 里 tool 定义再抄一份。三份东西只要有一处字段名不一致Agent 调用就会报参数校验失败而且报错信息往往只告诉你「invalid arguments」不告诉你是哪一层对不上。这个问题的根子在于能力定义SKILL.md、参数契约Schema、协议暴露MCP tool三者没有单一数据源。2.1 三个典型症状第一个症状是 description 写得太模糊。SKILL.md 里如果只写「处理浏览器相关任务」Agent 在工具列表里根本匹配不到该用哪个技能因为它靠 description 做语义检索。我试过把 description 写成「Browser automation CLI for AI agents. Use when the user needs to navigate pages, fill forms, click buttons, take screenshots, extract data」命中率立刻上来了。description 是唯一的触发机制这句话要刻在脑子里。第二个症状是参数类型对不上。Python 脚本里parser.add_argument(--detailed, actionstore_true)手写 Schema 时写成了type: stringAgent 传true进来直接类型错误。正确映射应该是type: boolean。这类错误在联调阶段特别费时间因为你要同时看脚本、Schema、MCP 日志三个地方。第三个症状是工具名带连字符。技能目录叫frontend-designMCP 注册时 tool name 如果直接用目录名某些客户端会对带连字符的名字做标准化调用时传frontend_design服务端找不到就返回 not found。这个坑我在多脚本技能上踩过后面会用模糊匹配兜住。2.2 我要的最终形态理想状态是我只维护一份 SKILL.md 和一份入口脚本Rust 侧自动解析出元信息和参数 SchemaMCP Server 启动后自动把技能注册成 toolAgent 调用时参数校验、鉴权、模型请求全部走同一条通道。这样新增一个技能只需要加一个目录不用改任何 Rust 代码。注意SKILL.md 的 front matter 必须是文件最开头---前后不能有空行或 BOM否则 YAML 解析会直接失败报错通常是did not find expected key。3. TaoToken 前置把工具鉴权和模型调用收敛到一个 Key在写 MCP Server 之前先把鉴权通道定下来。我的做法是MCP Server 本身不直接持有任何模型厂商的 Key所有需要模型能力的地方比如技能内部要调 LLM 做总结以及工具侧的身份校验都统一走 TaoToken 的 API 通道。这样客户端配置里只需要一个 Base URL 和一个 Key换模型、换客户端都不用动技能代码。3.1 拿 Key 和确认 Base URL先去控制台创建一个 API Key地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。创建完复制出来形如sk-开头的一串。Base URL 统一用 https://taotoken.net/api 注意这个地址后面不加任何路径后缀具体端点由 SDK 或请求体里的 model 字段决定。模型 ID 这块我常用的是claude-sonnet-4-5和gpt-4o这两个前者适合长上下文和代码后者适合通用工具调用。你可以在模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 先手动发一条消息确认 Key 可用再写进配置。3.2 三件套配置片段不管后面接的是 Cline、Claude Code 还是自己写的 MCP Server配置里永远只有这三样Base URL、API Key、Model ID。我把它写成一个settings.json片段路径按客户端不同放对位置即可{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: sk-你的Key, ANTHROPIC_MODEL: claude-sonnet-4-5 } }如果你用的是 OpenAI 兼容风格的客户端字段名换成OPENAI_BASE_URL、OPENAI_API_KEY、OPENAI_MODEL值不变。我实测下来同一份 Key 在两种风格下都能用省去了分别申请的成本。3.3 为什么不在技能里硬编码 Key有人图省事直接在 Python 技能脚本里os.environ[API_KEY]读一个写死的值。这样做的问题是技能包一旦分享出去Key 就泄露了而且每个技能都要重复读环境变量维护成本高。我的做法是 MCP Server 启动时从环境变量读一次技能执行时通过沙箱环境注入技能脚本只认TAOTOKEN_API_KEY这个变量名不关心值从哪来。提示环境变量名建议统一用TAOTOKEN_API_KEY和TAOTOKEN_BASE_URL避免和客户端自带的OPENAI_API_KEY冲突。如果客户端已经注入了后者可以在 MCP Server 启动脚本里做一次映射。4. 可复制配置SKILL.md 字段模板与 Rust Schema 推断这一节是全文的技术核心。我先把 SKILL.md 的字段模板给全再给 Rust 侧解析和 Schema 推断的骨架代码最后给 MCP Server 的 tool 注册逻辑。三块拼起来就是一条完整链路。4.1 SKILL.md 字段模板一个技能目录长这样weather/SKILL.md加weather/scripts/main.py。SKILL.md 的 front matter 我固定用这几个字段--- name: weather description: 查询指定城市的真实天气信息。当用户询问某个城市的天气、温度、湿度、风力时使用此技能。 license: MIT compatibility: Requires Python 3.x, network access metadata: author: yourname version: 1.0 ---字段含义我逐个说name是技能标识也是 MCP tool 名的来源description是唯一触发机制必须穷举触发场景compatibility里出现network或网络关键词时Rust 侧会自动把沙箱网络打开metadata是自定义扩展不影响解析。正文部分写使用示例和输入输出格式Agent 在需要时会按需读取。4.2 Rust 解析结构体用 serde 把 front matter 解析成结构体核心是SkillMetadatause serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SkillMetadata { pub name: String, pub description: String, #[serde(default)] pub license: OptionString, #[serde(default)] pub compatibility: OptionString, #[serde(default)] pub metadata: Optionserde_yaml::Value, } impl SkillMetadata { pub fn needs_network(self) - bool { let compat self.compatibility.as_deref().unwrap_or().to_lowercase(); compat.contains(network) || compat.contains(internet) || compat.contains(网络) || compat.contains(联网) } pub fn language(self) - OptionString { let compat self.compatibility.as_deref()?.to_lowercase(); if compat.contains(python) { Some(python.into()) } else if compat.contains(node) { Some(node.into()) } else { None } } }解析 front matter 的时候我习惯先按---切三段取中间那段喂给serde_yaml::from_str。这里有个细节如果文件开头有 UTF-8 BOM第一段会变成\u{feff}---判断会失败。稳妥做法是先trim_start_matches(\u{feff})再切。4.3 从 argparse 推断 JSON Schema技能入口是 Python 时我从add_argument调用里正则提取参数映射成 OpenAI function calling 风格的 Schema。核心映射规则如下表argparse 写法JSON Schema 字段位置参数required: true--flagrequired: falsetypeinttype: integertypefloattype: numberactionstore_truetype: booleanchoices[...]enum: [...]nargs*type: arrayhelp...description: ...default...default: ...Rust 侧推断函数骨架pub fn parse_argparse_schema(script: str) - serde_json::Value { let mut properties serde_json::Map::new(); let mut required Vec::new(); let re regex::Regex::new( r#add_argument\(\s*[]([^])[]([^)]*)\)# ).unwrap(); for cap in re.captures_iter(script) { let arg_name cap[1].trim_start_matches(-).to_string(); let rest cap[2]; let mut prop serde_json::Map::new(); if rest.contains(typeint) { prop.insert(type.into(), integer.into()); } else if rest.contains(typefloat) { prop.insert(type.into(), number.into()); } else if rest.contains(store_true) { prop.insert(type.into(), boolean.into()); } else { prop.insert(type.into(), string.into()); } if let Some(help) extract_quoted(rest, help) { prop.insert(description.into(), help.into()); } if let Some(choices) extract_choices(rest) { prop.insert(enum.into(), choices.into()); } if !cap[1].starts_with(-) { required.push(arg_name.clone()); } properties.insert(arg_name, serde_json::Value::Object(prop)); } serde_json::json!({ type: object, properties: properties, required: required }) }extract_quoted和extract_choices是两个小工具函数分别抓help...里的字符串和choices[...]里的枚举值实现不复杂按需补即可。这样一份 Python 脚本写完后Schema 自动就有了不用手写第二遍。4.4 MCP Server 的 tool 注册MCP Server 用 stdio 跑 JSON-RPC 2.0启动后先响应initialize再响应tools/list。我把每个技能注册成一个 tool名字用name字段参数用上一步推断出的 Schemafn build_tool_definition(meta: SkillMetadata, schema: serde_json::Value) - serde_json::Value { serde_json::json!({ name: meta.name, description: meta.description, inputSchema: schema }) }tools/call收到请求后按name找到技能目录把arguments序列化成 JSON 传给入口脚本的 stdin脚本输出 JSON 到 stdoutServer 再包一层content返回。整个过程不涉及任何模型厂商的直连需要模型能力时统一走 TaoToken 通道。注意MCP 的 stdio 模式下stdout 只能输出 JSON-RPC 消息任何调试日志必须走 stderr否则客户端解析会直接崩。我早期把println!当日志用排查了半天才发现是这个问题。5. 验证请求与成功结果一次端到端调用回执配置写完必须验证不然你不知道是 Schema 错了还是鉴权错了。我按「先本地直连、再 MCP 客户端」两步走。5.1 本地直连验证模型通道先用 curl 确认 Key 和 Base URL 可用curl https://taotoken.net/api/v1/messages \ -H x-api-key: sk-你的Key \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d { model: claude-sonnet-4-5, max_tokens: 64, messages: [{role: user, content: 只回复两个字通了}] }返回里能看到content数组和usage字段就说明通道没问题。如果返回 401先检查 Key 有没有复制全、有没有多余空格。5.2 本地启动 MCP Server编译后直接跑cargo build --release TAOTOKEN_API_KEYsk-你的Key \ TAOTOKEN_BASE_URLhttps://taotoken.net/api \ ./target/release/skilllite mcp-serve --skills-dir .skills启动后 Server 会在 stderr 打印已加载的技能列表形如loaded skill: weather (python, networktrue)。看到这行说明 SKILL.md 解析和 Schema 推断都成功了。5.3 客户端配置与调用回执在 Cursor 的 MCP 配置里加上{ mcpServers: { skilllite: { command: /abs/path/target/release/skilllite, args: [mcp-serve, --skills-dir, /abs/path/.skills], env: { TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_BASE_URL: https://taotoken.net/api } } } }重启客户端后在对话里输入「帮我查一下深圳天气」Agent 会先tools/list拿到 weather 的 Schema再tools/call传{city: 深圳}。我实测的回执长这样{ jsonrpc: 2.0, id: 3, result: { content: [ { type: text, text: {\city\:\深圳\,\temperature\:\18°C\,\weather\:\多云\,\humidity\:\72%\} } ], isError: false } }看到isError: false且 content 里有真实数据整条链路就通了。从用户输入到结果返回中间经过了 SKILL.md 解析、Schema 推断、MCP 注册、沙箱执行、结果封装五步每一步都有日志可查。6. 本篇常见错排查401、local proxy failed 与 reading choices联调阶段我遇到过的报错基本集中在四类逐个说清楚原因和改法。6.1 401 Unauthorized最常见。原因通常是 Key 没传进 MCP Server 进程。MCP 客户端启动子进程时env字段里的变量才会注入如果你只在 shell 里 export 了客户端不一定继承。改法是把 Key 写进客户端的env块或者用env命令显式传递。还有一种情况是 Key 前后带了换行复制时容易带上用echo -n检查一下长度。6.2 local proxy failed这个报错一般出现在客户端尝试连接本地代理端口时。原因是你配置的 Base URL 指向了一个本地端口但那个端口没有服务在监听。改法是确认 Base URL 直接写 https://taotoken.net/api 不要写http://localhost:xxxx之类的地址。如果你之前配过别的工具残留了本地代理设置把相关环境变量清掉再重启客户端。6.3 reading choices 报错这个报错说明客户端按 OpenAI 格式解析响应但服务端返回的是 Anthropic 格式content数组字段对不上。改法是统一协议风格要么客户端用 Anthropic 风格配置ANTHROPIC_BASE_URL要么在请求里明确走 OpenAI 兼容端点。我一般让客户端和模型 ID 匹配用claude-开头的模型就配 Anthropic 风格用gpt-开头的就配 OpenAI 风格。6.4 OAuth 相关报错有些客户端默认走 OAuth 流程会提示需要登录或 token 过期。MCP Server 场景下不需要 OAuth直接用 API Key 即可。改法是在客户端配置里关掉 OAuth 选项或者把认证方式显式设为 API Key。如果客户端强制走 OAuth检查一下是不是选错了接入模式。6.5 工具名 not foundAgent 调用时传了frontend_design但注册的是frontend-design。改法是在查找技能时做一次标准化匹配先把 tool name 里的连字符换成下划线再和注册名比对。Rust 侧加一个sanitize_tool_name函数即可逻辑就是name.replace(-, _)。提示排查顺序建议从外到内——先 curl 验证 Key再本地启动 Server 看日志最后接客户端。这样能快速定位是通道问题还是协议问题。7. 语义一致 CTA把这条链路用起来整条链路跑通后新增技能的成本就降到了「加一个目录、写一份 SKILL.md、放一个入口脚本」。Rust 侧不用改MCP 配置不用改Key 也不用重新申请。如果你想让模型调用和工具鉴权继续收敛在同一个通道上可以去控制台再建几个不同用途的 Key 做隔离地址还是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。需要长期跑编码类 Agent、或者技能里要频繁调模型的场景可以看下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 按用量走比单次调用更划算。接入细节和字段说明在文档里 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到协议层的问题可以先翻一遍。最后留一个我踩过的坑SKILL.md 的 description 一定要写全触发场景别嫌长。Agent 匹配工具全靠它写短了命中率低写全了反而省事。技能目录名尽量用下划线避免连字符带来的标准化问题。这两条改完你的 MCP Server 基本就能稳定被调用了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询