
1. 从一次“工具调用失败”说起MCP 协议到底解决什么问题如果你刚开始接触模型上下文协议MCP协议大概率会被两个词绕晕MCP服务器和Agent生态。我第一次写 MCP 服务器时客户端能列出工具但一调用就报reading choices相关的解析错误排查了半天才发现是返回结构里content字段类型写错了。这篇就把这套流程完整走一遍从零搭一个最小可运行的本地 MCP 服务器注册一个工具用客户端发起调用并核对返回。先说清楚它是什么。MCPModel Context Protocol是一套让大模型安全访问外部工具和数据源的开放协议。你可以把它理解成 AI 和外部世界之间的“标准插座”模型本身不会查数据库、读文件、发请求但通过 MCP 服务器暴露出来的工具Tools、资源Resources、提示Prompts模型就能在受控范围内完成这些动作。适合谁适合想给 Agent 生态补上“手脚”的后端开发者、想把自己内部系统接进 AI 工作流的工具作者以及刚学完 Function Calling 想进一步理解标准化协议的人。它和直接写 Function Calling 的区别在于MCP 把“工具怎么描述、怎么被发现、怎么被调用、结果怎么回传”抽象成了统一协议。客户端比如 Claude Desktop、Cline、各类 IDE 插件只要实现一次协议就能对接任意符合规范的服务器。你写的服务器不用关心对面是哪个模型模型也不用关心你底层是 SQLite 还是 HTTP API。这就是 Agent 生态能快速扩张的原因——大家遵守同一套通信约定。通信流程是典型的客户端-服务器模型。客户端负责和模型对话、把模型想调用的工具转成协议请求服务器负责接收请求、执行真实逻辑、把结果按协议格式返回。传输层最常用的是 stdio也就是标准输入输出客户端启动你的进程通过 stdin/stdout 交换 JSON-RPC 消息。整个链路对用户透明看起来就像模型自己会这些能力。下面这张表帮你快速区分三个核心组件后面写代码时会反复用到组件作用典型场景是否必须Tools 工具模型可调用的函数查库、发请求、算数据是最小骨架核心Resources 资源模型可读取的数据源文档、配置、表结构否按需Prompts 提示预定义对话模板引导结构化交互否按需理解了这层你就明白为什么标题里把 MCP协议 和 Agent生态 绑在一起Agent 要干活靠的就是一个个 MCP服务器 提供的工具。接下来进入实操先把接入侧的前置准备好。2. 前置准备用 TaoToken 打通模型侧调用链路写服务器之前得先有一个能发起工具调用的模型客户端。很多新手卡在这一步本地服务器写好了但没有可用的模型端点去驱动它调试无从谈起。我的做法是先用 TaoToken 把模型调用链路跑通再回头调服务器这样出问题时能快速判断是协议层还是模型层。TaoToken 是一个聚合式的模型调用平台提供兼容 OpenAI 风格的接口适合用来做 Agent 和 MCP 场景的联调。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置时直接填这个。你需要准备三样东西我把它叫做“三件套”后面无论接 Claude Code、Cline 还是 Codex 都绕不开Base URLhttps://taotoken.net/apiAPI Key在控制台创建形如sk-开头的一串字符Model ID按你实际要用的模型填比如对话类或编码类模型 ID创建 Key 的入口在控制台路径是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 管理页在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。如果你只是想先验证模型能不能正常对话可以直接用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一条消息试试确认 Key 有效再往下走。为什么强调先跑通模型侧因为 MCP 的调试是双向的服务器要能被客户端启动客户端要能把工具描述喂给模型模型要能正确生成工具调用参数。任何一环断了现象都可能是“工具调用失败”。先把模型侧用最简请求验证一遍能省掉大量猜测。验证模型侧最直接的方式是发一个 chat completions 请求。你可以用 curl也可以用任意 HTTP 客户端。请求体里带上 model 和 messagesHeader 里带 Authorization。如果返回正常的 choices 结构说明 Key 和 Base URL 没问题。这一步不需要 MCP纯粹确认模型通道可用。对于长期做编码和 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 遇到参数细节可以对照查。前置做完你手里应该有了可用的 Base URL、Key 和 Model ID。下面开始写服务器本体。3. 可复制配置最小 MCP 服务器骨架与客户端接入这一节给你一份能直接跑起来的最小骨架。语言用 Python因为 MCP 的 Python SDK 上手快依赖清晰。项目结构尽量扁平方便你复制后立刻运行。先建目录和虚拟环境mkdir my-mcp-server cd my-mcp-server python3 -m venv .venv source .venv/bin/activate pip install mcp1.0.0 pydantic2.0.0然后是核心文件server.py。这个服务器只做一件事注册一个get_greeting工具接收语言和名字返回问候语。麻雀虽小但工具注册、参数校验、返回结构三要素齐全。# server.py import asyncio from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(greeting-server) app.list_tools() async def list_tools() - list[Tool]: return [ Tool( nameget_greeting, description根据语言和名字生成问候语, inputSchema{ type: object, properties: { language: {type: string, description: 语言代码如 zh、en}, name: {type: string, description: 用户名字}, }, required: [language, name], }, ) ] app.call_tool() async def call_tool(name: str, arguments: dict) - list[TextContent]: if name ! get_greeting: raise ValueError(f未知工具: {name}) lang arguments.get(language, zh) user arguments.get(name, 朋友) greetings {zh: f你好{user}, en: fHello, {user}!} text greetings.get(lang, greetings[zh]) return [TextContent(typetext, texttext)] async def main(): async with stdio_server() as (read, write): await app.run(read, write, app.create_initialization_options()) if __name__ __main__: asyncio.run(main())注意call_tool的返回必须是list[TextContent]这是最容易写错的地方。很多人直接返回字符串或 dict客户端解析时就会报reading choices之类的错误。返回结构统一用TextContent类型标成text内容放text字段。接着配置客户端。以 Claude Desktop 为例配置文件路径按系统区分macOS 在~/Library/Application Support/Claude/claude_desktop_config.jsonWindows 在%APPDATA%\Claude\claude_desktop_config.json。在mcpServers下加入你的服务器{ mcpServers: { greeting: { command: /absolute/path/to/.venv/bin/python, args: [/absolute/path/to/server.py] } } }这里有个坑command一定要写虚拟环境里 python 的绝对路径不要只写python。客户端启动子进程时不会继承你终端的 PATH写相对命令大概率报spawn python ENOENT。同理args里的脚本路径也用绝对路径。如果你用的是 Cline 或支持 MCP 的 IDE 插件配置结构类似通常也是commandargs两个字段。若涉及 Codex 的auth.json需要把 Base URL、Key、Model ID 三件套都写全缺一个都会导致鉴权失败。CC Switch 这类工具切换配置时也记得三件套同步更新别只换 Key 忘了 Base URL。配置保存后重启客户端。重启动作很关键MCP 服务器是在客户端启动时拉起的改完配置不重启不会生效。重启后如果服务器进程正常客户端会完成初始化握手工具列表就能被模型看到。4. 验证请求启动服务、注册工具、发起一次调用并核对返回配置写完进入验证环节。这一步要确认三件事服务器能启动、工具被正确注册、调用返回符合预期。先脱离客户端单独验证服务器进程能不能跑。在终端里直接运行.venv/bin/python server.py如果没有任何报错、进程挂起等待输入说明服务器启动正常。stdio 模式下它不会打印欢迎语这是正常的它在等 JSON-RPC 消息。按 CtrlC 退出即可。接着用 MCP 官方的调试客户端做一次完整调用。SDK 自带一个简易客户端可以列出工具并调用# client_test.py import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def main(): params StdioServerParameters( command.venv/bin/python, args[server.py], ) async with stdio_client(params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools await session.list_tools() print(工具列表:, [t.name for t in tools.tools]) result await session.call_tool( get_greeting, {language: zh, name: 小明}, ) print(调用返回:, result.content[0].text) asyncio.run(main())运行后你应该看到类似输出工具列表: [get_greeting] 调用返回: 你好小明看到这两行说明协议链路完全通了客户端启动服务器、初始化握手、列出工具、发起调用、服务器执行并返回、客户端解析结果。整个过程没有经过模型是纯协议层验证。协议层通了再接到模型侧就只剩“模型会不会正确生成参数”这一个变量。最后在客户端里做端到端验证。重启 Claude Desktop 后在对话里说“用中文问候一下小明”。模型会识别到get_greeting工具生成{language: zh, name: 小明}参数客户端转发给服务器服务器返回问候语模型再把结果组织成自然语言回复你。如果这一步成功你的第一个 MCP服务器 就正式跑通了。核对返回时重点看两点一是工具是否出现在可用列表里二是返回文本是否和服务器逻辑一致。如果工具没出现多半是配置路径问题如果调用报错多半是返回结构问题。这两类现象在下一节展开。5. 常见报错排查401、local proxy failed、reading choices、OAuth调试 MCP 时遇到的报错其实就那么几类对照着查能省很多时间。下面按真实报错逐条拆。401 未授权。这个通常出现在模型侧调用不是 MCP 协议本身。原因一般是 API Key 写错、过期或者 Base URL 和 Key 不匹配。检查三件套Base URL 是否为https://taotoken.net/apiKey 是否以sk-开头且没有多余空格Model ID 是否是平台支持的。如果用的是 Codex 的auth.json确认字段名和层级没写错三件套缺一不可。local proxy failed。这个报错多见于客户端启动服务器子进程失败。核心原因是command路径不对。客户端不继承终端环境变量所以python、node这类命令必须写绝对路径。另外检查脚本文件是否有可执行权限虚拟环境是否装好了依赖。用绝对路径的 python 解释器 绝对路径的脚本基本能解决。reading choices 相关解析错误。这是 MCP 服务器返回结构不符合协议导致的。典型场景是call_tool返回了字符串、dict 或None而协议要求返回list[TextContent]。修正方式是统一用TextContent(typetext, text...)包装外层用列表。如果你返回的是多个内容块也要保证每个块类型正确。OAuth 鉴权失败。部分客户端或远程 MCP 服务器会走 OAuth 流程。本地 stdio 服务器一般不需要但如果你接的是远程服务需要确认 token 是否过期、回调地址是否配置正确。本地调试阶段建议先用 stdio避开 OAuth 复杂度。再补几个容易忽略的点。工具inputSchema里required字段如果和properties对不上模型可能生成缺参数的调用服务器端要做兜底默认值。服务器日志建议输出到 stderr不要输出到 stdout因为 stdout 是协议通道混入日志会破坏 JSON-RPC 消息解析。这个坑我踩过现象是客户端随机报解析错误排查很久才发现是 print 打到了 stdout。排查顺序建议固定下来先单独跑服务器进程确认能启动再用调试客户端确认协议层通最后接模型确认端到端。每一层单独验证出问题时定位范围就小很多。6. 继续深入把 MCP 服务器接进你的 Agent 工作流最小骨架跑通后你可以按需扩展。加 Resources 让模型读取你的文档或表结构加 Prompts 提供结构化对话模板把工具从“问候”换成真实的查库、调 API、读文件。协议层不变变的只是业务逻辑。如果你要长期做编码类 Agent把 MCP 服务器和 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 。想先验证模型对工具描述的理解能力可以用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 手动试几轮。一个实用技巧工具描述写得越具体模型生成参数越准。description里把参数含义、取值范围、默认值都写清楚比只写一句“生成问候语”效果好得多。这是我在多个 Agent 项目里反复验证过的经验工具描述的质量直接决定调用成功率。最后提醒一句服务器返回大结果时记得分页或截断别一次性把整张表塞回去否则模型上下文会被撑爆。这个边界处理往往比工具本身更能决定 Agent 稳不稳定。