1. 从 echo_upper 到文件级 Skill:为什么最小 MCP 工具不够用
如果你已经跟着上一篇把 C++ 版 MCP 服务跑起来,手里大概率有一个能用的echo_upper:客户端发一段文本,服务端把 ASCII 小写字母转成大写再返回。这个工具很适合理解 JSON-RPC 消息流,但一旦把它放进真实开发流程,问题立刻暴露出来。
用户不会说“把 hello 变成大写”,用户会说“把这个 txt 文件里的英文字母都转成大写,生成一个新文件,非字母不要动”。这两句话的差距,就是 MCP 工具和 Skill 之间的差距。MCP 工具解决的是“服务端能做什么、参数怎么传、结果怎么返回”,Skill 解决的是“什么场景触发、按什么步骤做、边界条件是什么、成功后回报什么”。前者是能力接口,后者是流程编排。
我这次要做的,是在原有 C++ MCP 服务上新增一个真正适合文件处理的工具uppercase_txt_file,再配一个最小 Skilltxt-uppercase-file,最后通过 TaoToken 的统一 Key/API 通道把整条链路跑通。整篇文章会给出可复制的 CMake 工程结构、skill 注册配置、端到端调用验证动作,以及我实际踩过的几个报错。
适合谁看:已经写过或跑通过一个最小 MCP 服务、想进一步理解 Skill 与 MCP 分工的 C++ 开发者;正在用 Codex CLI 或类似客户端做 AI 开发流程、想把自研能力接进统一通道的人;以及被std::filesystem链接错误、local proxy failed、reading choices这类报错卡住的同学。
核心检索词先明确:C++ 实现简易 skill 并接入 TaoToken,本质是把一个文件级 MCP 工具包装成 SKILL.md 描述的可复用流程,再通过统一 API 通道调用。下面从工程结构开始,一步步落地。
2. TaoToken 前置准备:统一 Key 与 API 通道配置
在写 Skill 之前,先把调用通道准备好。TaoToken 在这里扮演的角色是统一入口:你不需要为每个模型或每个工具单独维护一套鉴权和地址,而是用同一个 Key 走同一个 API 前缀。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api (这个地址不加 UTM 参数,直接用于配置)。
你需要准备三样东西,我把它称为“三件套”,后面无论配 Codex、Cline 还是 Claude Code,都绕不开:
| 配置项 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | 所有请求的统一前缀 |
| API Key | 在控制台创建 | 形如sk-...,只显示一次 |
| Model ID | 按需选择 | 例如claude-sonnet-4-5等 |
获取 Key 的路径是控制台里的 API Keys 页面,创建后立刻复制保存,页面刷新后不再完整显示。如果你还没决定用哪个模型,可以先去模型对话页面做一次最小验证,确认 Key 和通道都正常,再回来配本地工程。
这里要强调一个容易踩的坑:Base URL 和具体路径的拼接方式。很多客户端要求你填的是“到/api为止”的基址,然后由客户端自己拼/v1/messages或/v1/chat/completions;也有客户端要求你填完整路径。TaoToken 的 API 基址是https://taotoken.net/api,在 Codex 的config.toml里通常写成base_url = "https://taotoken.net/api",由客户端补全后续路径。如果你填成https://taotoken.net/api/v1,很可能出现 404 或路径重复。
另外,Key 不要硬编码进源码或提交到 Git。本地测试用环境变量,工程里用配置文件,这是基本纪律。我见过有人把 Key 写进mcp_cpp_demo.cpp里图省事,结果一提交就泄露,只能作废重建。
准备阶段做完,你应该能回答三个问题:Base URL 是什么、Key 存在哪、用哪个 Model ID。这三个答案会在第 3 节的配置片段里反复出现。如果你更偏向长期编码和 Agent 场景,可以顺带了解 Coding Plan,它适合把这类自研 Skill 纳入日常开发流;如果只是先验证模型通不通,模型对话页面就够了。
3. 可复制配置:CMake 工程结构、MCP 注册与 SKILL.md
这一节是全文的技术核心,给出可以直接抄的工程结构、配置片段和 SKILL.md。先看目录布局,我把它整理成 CMake 友好的形式,而不是依赖 VS 工程文件:
mcp_example/ ├── CMakeLists.txt ├── mcp_cpp_demo/ │ ├── CMakeLists.txt │ └── mcp_cpp_demo.cpp ├── skill_examples/ │ └── txt-uppercase-file/ │ └── SKILL.md └── sample.txt顶层CMakeLists.txt只做工程声明和子目录引入:
cmake_minimum_required(VERSION 3.16) project(mcp_example LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_subdirectory(mcp_cpp_demo)子目录mcp_cpp_demo/CMakeLists.txt负责可执行文件和依赖。这里用FetchContent拉取nlohmann/json,避免手动拷贝头文件:
include(FetchContent) FetchContent_Declare( nlohmann_json GIT_REPOSITORY https://github.com/nlohmann/json.git GIT_TAG v3.11.3 ) FetchContent_MakeAvailable(nlohmann_json) add_executable(mcp_cpp_demo mcp_cpp_demo.cpp) target_link_libraries(mcp_cpp_demo PRIVATE nlohmann_json::nlohmann_json)注意CMAKE_CXX_STANDARD 17这一行,std::filesystem需要 C++17。如果你沿用 VS 工程,对应的是在.vcxproj里加<LanguageStandard>stdcpp17</LanguageStandard>,否则会报std::filesystem未定义或链接错误。
源码部分,核心改动是在原有echo_upper之外新增uppercase_txt_file。工具声明放在tools/list的返回里,参数 schema 如下:
{ {"name", "uppercase_txt_file"}, {"description", "Read a UTF-8 txt file, convert ASCII lowercase letters to uppercase, keep non-letters unchanged, and write the result to a txt file"}, {"inputSchema", { {"type", "object"}, {"properties", { {"inputPath", {{"type", "string"}, {"description", "Path to the source .txt file"}}}, {"outputPath", {{"type", "string"}, {"description", "Optional output .txt path. Defaults to <input>_upper.txt"}}}, {"overwrite", {{"type", "boolean"}, {"description", "Whether to overwrite an existing output file"}, {"default", false}}} }}, {"required", json::array({"inputPath"})} }} }处理逻辑的关键点:只处理 ASCII 的a-z,不要用全局 locale 的大小写转换,否则会误伤 UTF-8 中文字节。转换函数保持逐字节判断:
static std::string to_upper_copy_ascii(std::string s) { for (char& ch : s) { if (ch >= 'a' && ch <= 'z') { ch = static_cast<char>(ch - 'a' + 'A'); } } return s; }handle_uppercase_txt_file负责校验参数、校验.txt扩展名、读取输入、转换、写入输出、返回摘要。默认输出路径用make_default_output_path生成<原文件名>_upper.txt,已存在且未允许覆盖时直接报错。
接下来是 Skill 侧。SKILL.md放在skill_examples/txt-uppercase-file/下,内容如下:
--- name: txt-uppercase-file description: Convert the contents of a UTF-8 .txt file so that ASCII lowercase letters become uppercase while non-letters stay unchanged. Use when the user wants to uppercase a txt file, batch-convert simple text content, or write the transformed result to a new txt file by calling the MCP tool `uppercase_txt_file`. --- # TXT 文件大写转换 接收用户传入的 .txt 文件路径。 优先调用 MCP 工具 `uppercase_txt_file`,参数如下: - `inputPath`:输入文件路径 - `outputPath`:用户指定输出路径时填写 - `overwrite`:仅当用户明确允许覆盖现有文件时填写 遵循规则: - 将文件按 UTF-8 文本处理 - 仅将 ASCII 小写字母 `a-z` 转为大写 `A-Z` - 汉字、数字、空格、标点符号保持不变 - 默认创建新文件,不覆盖源文件 如果输出路径已存在且未请求覆盖,停止执行并清晰提示文件冲突。 工具执行成功后,返回信息: - 源文件路径 - 输出文件路径 - 原文件是否被保留这份 SKILL.md 没有重新实现任何转换逻辑,它只做三件事:声明触发场景、指定优先调用的 MCP 工具、约束边界条件。这就是 Skill 与 MCP 的标准分工。
最后是 Codex CLI 的 MCP 注册配置,写在~/.codex/config.toml(Windows 下是C:\Users\<你>\.codex\config.toml):
[mcp_servers.cpp_demo] type = "stdio" command = "D:\\mcp_example\\build\\mcp_cpp_demo\\Debug\\mcp_cpp_demo.exe" cwd = "D:\\mcp_example" startup_timeout_sec = 120如果你同时要接 TaoToken 的模型通道,在同一个config.toml里补上模型配置,三件套齐全:
model = "claude-sonnet-4-5" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"env_key指向环境变量名,Key 本身放在系统环境变量里,不写进文件。Skill 文件放到 Codex 可发现的位置,例如C:\Users\<你>\.codex\skills\txt-uppercase-file\SKILL.md。配置完成后,输入/skills应该能看到txt-uppercase-file出现在列表里。
4. 验证请求与成功结果:从编译到端到端调用
配置写完必须验证,否则你永远不知道是工具没注册、Skill 没被发现,还是通道没通。我按“编译 → 手动 MCP 调用 → Skill 触发”三步走。
第一步,编译。用 CMake 的话:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug cmake --build build --config DebugWindows 上如果生成的是 VS 工程,也可以用 MSBuild:
& "D:\Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe" ` "D:\mcp_example\build\mcp_cpp_demo.sln" ` /t:Build /p:Configuration=Debug /p:Platform=x64编译成功后,可执行文件在build/mcp_cpp_demo/Debug/mcp_cpp_demo.exe(或你 VS 工程对应的x64\Debug\下)。
第二步,手动发 MCP 消息验证工具注册。启动程序后,先发初始化:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual-test","version":"0.1.0"}}}再查工具列表:
{"jsonrpc":"2.0","id":2,"method":"tools/list"}返回里应该同时出现echo_upper和uppercase_txt_file。如果只有前者,说明tools/list的数组没改对,回去检查 JSON 结构。
准备一个测试文件D:\mcp_example\sample.txt,内容写成:
hello, World! 123 今天abc然后调用文件工具:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"uppercase_txt_file","arguments":{"inputPath":"D:\\mcp_example\\sample.txt"}}}成功时返回类似:
{ "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "Converted file to uppercase.\nInput: D:\\mcp_example\\sample.txt\nOutput: D:\\mcp_example\\sample_upper.txt" } ], "isError": false } }同时sample_upper.txt生成,内容应为HELLO, WORLD! 123 今天ABC。注意中文“今天”没变,数字和标点也没变,只有 ASCII 小写字母被转换,这说明逐字节判断生效了。
第三步,Skill 触发。在 Codex CLI 里输入/skills确认txt-uppercase-file在列表,然后用自然语言触发:
用 txt-uppercase-file skill 处理 D:\mcp_example\sample.txt,把这个 txt 文件转成大写,不要覆盖原文件预期行为是模型优先调用uppercase_txt_file,传入inputPath,不传overwrite,返回源文件路径、输出文件路径和原文件保留状态。如果模型绕开工具自己读文件再写回,说明 SKILL.md 的 description 不够明确,或者 Skill 没被正确加载。
到这里,整条链路就通了:C++ MCP 工具提供能力,SKILL.md 提供流程约束,TaoToken 提供统一模型通道。三者各司其职,没有互相替代。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节对照真实报错,给出定位思路。这些错我在不同阶段都遇到过,按出现频率排序。
401 Unauthorized。最常见的原因是 Key 没生效或 Base URL 拼错。先确认环境变量TAOTOKEN_API_KEY在当前终端可见,Windows 下改完环境变量要重开终端。再确认base_url是https://taotoken.net/api,没有多写/v1。如果 Key 是从控制台复制的,检查有没有带多余空格或换行。401 基本是鉴权层问题,和 MCP 工具本身无关。
local proxy failed。这个报错通常出现在客户端尝试走本地代理但代理没启动或端口不对。检查你的客户端配置里有没有残留的代理设置,把代理相关字段清掉,直连https://taotoken.net/api。如果你在config.toml里同时配了model_provider和旧的代理项,两者会冲突,删掉不用的那个。
reading choices 相关报错。这类错误一般出现在响应解析阶段,客户端期望的返回结构和实际返回不匹配。常见诱因是 Base URL 指向了错误的路径,导致返回的是 HTML 错误页而不是 JSON。用curl直接打一次接口确认返回体是 JSON:
curl -s https://taotoken.net/api/v1/messages \ -H "x-api-key: $TAOTOKEN_API_KEY" \ -H "content-type: application/json" \ -d '{"model":"claude-sonnet-4-5","max_tokens":64,"messages":[{"role":"user","content":"ping"}]}'如果返回 HTML 或 404,说明路径不对;如果返回 JSON 但客户端仍报 reading choices,检查客户端版本是否支持当前响应格式。
OAuth 相关报错。有些客户端默认走 OAuth 流程,而 TaoToken 用的是 API Key 鉴权。如果你看到 OAuth token 获取失败,说明客户端在尝试它自己的登录流程,而不是用你配的 Key。检查配置里是否误开了 OAuth 模式,改成 API Key 模式,把env_key指向正确的环境变量。
MCP 工具调用返回 -32602。这是参数校验失败,常见于inputPath传了非字符串、overwrite传了非布尔值,或者输入文件不是.txt。对照handle_uppercase_txt_file里的校验顺序逐条排查:先看扩展名,再看文件是否存在,再看是否普通文件,最后看输出是否冲突。
Skill 不触发。先确认/skills列表里有它,再确认 SKILL.md 的 frontmatter 格式正确(---包裹,name和description都在)。description 里要包含触发关键词,比如“uppercase a txt file”,否则模型匹配不到。
排障时如果怀疑是通道问题,去 API Keys 页面重新确认 Key 状态,或对照接入文档检查路径拼接。如果只是想验证模型本身通不通,用模型对话页面发一句话最快。长期做编码和 Agent 的话,Coding Plan 能省掉不少重复配置。
6. 把自研 Skill 接进统一通道:下一步怎么走
走到这里,你已经有了一个可被 AI 开发流程调用的最小 Skill:C++ 写的 MCP 工具负责文件级大写转换,SKILL.md 负责触发条件和边界约束,TaoToken 负责统一模型通道。这套结构可以复制到任何“读文件 → 处理 → 写文件”的场景,比如批量重命名、格式转换、日志提取。
下一步我建议做两件事。第一,把uppercase_txt_file的参数再扩一层,比如支持encoding或dryRun,让工具边界更清晰,Skill 的 description 也能写得更具体。第二,把 Skill 目录纳入版本管理,和 MCP 源码放在同一个仓库,这样工具和流程说明不会脱节。
如果你还没拿到 Key,先去控制台创建;如果通道已经通了,直接去接入文档对照一遍路径拼接,确认没有多余后缀。需要长期跑编码和 Agent 任务的话,Coding Plan 比按次调用更省心。整条链路的关键不是某个单点配置,而是“能力用 MCP 暴露、流程用 Skill 约束、通道用统一 Key 收口”这个分工,记住这一点,后面接什么工具都不会乱。