1. 为什么我要在本地把 MCP 链路跑通Anthropic 在 2024 年底开源的 MCPModel Context Protocol模型上下文协议解决的是一个很具体的问题大模型想调用外部工具时过去每个厂商、每个框架都有一套自己的 Function Calling 格式工具描述、参数 Schema、调用结果回填全靠开发者手写胶水代码。MCP 把这件事抽象成一套基于 JSON-RPC 2.0 的协议Server 端负责暴露工具、资源和提示词模板Client 端负责握手、发现工具、发起调用、把结果塞回上下文。它本质上是一个「工具调用的 USB-C 接口」谁都能插。这篇面向的是想自建本地工具调用链路的 Python 开发者。我会从零搭一个最小 MCP Server用 Python 客户端完成一次端到端调用把握手、工具注册、请求响应三个环节都跑通。你不需要先理解全部协议细节跟着代码走一遍再回头看协议文档会顺很多。适合已经会写 Python、用过 FastAPI 或 asyncio、想搞清楚 MCP 到底怎么落地的人。整条链路我实测下来从装包到看到工具返回结果大概二十分钟。核心检索词先明确Anthropic MCP 协议是一套让大模型与外部工具通信的开放标准Python 实现 MCP Server 和 Client 是当前最主流的落地方式。下面所有代码都可以直接复制运行。2. 前置准备TaoToken 接入与 Python 环境搭建在写 MCP Server 之前得先有一个能调用 Claude 模型的通道否则 Client 端没法验证「模型决定调用哪个工具」这一步。我用的是 TaoToken 的 Anthropic 兼容接口它提供标准的 Base URL 和 API Key直接对接 Anthropic SDK 即可不需要改任何请求逻辑。先装依赖。MCP 官方 Python SDK 叫mcpAnthropic SDK 叫anthropic再加httpx做工具内部的 HTTP 调用pip install mcp anthropic httpx uvicorn fastapi环境变量建议单独放一个.env避免 Key 写死在代码里export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你用 Claude Code 或 Cline 这类工具配置方式略有不同。以 Claude Code 的settings.json为例需要写全三件套 Base URL、Key、Model ID{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的key, ANTHROPIC_MODEL: claude-sonnet-4-20250514 } }Cline 的 MCP 配置则是在cline_mcp_settings.json里加 server 条目Codex 的auth.json结构类似核心都是把 Base URL 指向兼容端点、填 Key、指定 Model ID。这三件套缺一个都会在握手阶段报 401 或 model not found。拿到 Key 的入口在控制台的 API Keys 页面接入文档里有各语言的最小示例。我建议先把 Key 存好后面 Client 代码直接读环境变量。这里有个容易踩的坑TaoToken 的 Base URL 是https://taotoken.net/apiAnthropic SDK 会自动在末尾拼/v1/messages所以你不要手动加/v1否则会变成/api/v1/v1/messages直接 404。我第一次配的时候就多写了一段排查了十分钟。3. 可复制配置最小 MCP Server 与 Client 代码MCP 的架构是 Client-Server 分离。Server 端用mcpSDK 的Server类注册工具通过 stdio 或 SSE 传输Client 端用ClientSession连接 Server拿到工具列表后交给模型决策。下面是最小可运行版本。先写 Server文件叫weather_server.py。它注册两个工具查天气和查人口。注意工具的参数 Schema 用 JSON Schema 描述模型靠这个决定传什么参数import asyncio from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent app Server(weather-server) app.list_tools() async def list_tools(): return [ Tool( nameget_weather, description获取指定城市的实时天气, inputSchema{ type: object, properties: { city: {type: string, description: 城市名如北京} }, required: [city] } ), Tool( nameget_population, description获取指定城市的常住人口, inputSchema{ type: object, properties: { city: {type: string, description: 城市名} }, required: [city] } ) ] app.call_tool() async def call_tool(name: str, arguments: dict): if name get_weather: city arguments[city] return [TextContent(typetext, textf{city}今日气温25摄氏度晴)] elif name get_population: city arguments[city] return [TextContent(typetext, textf{city}常住人口约2189万)] raise ValueError(f未知工具: {name}) 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())再写 Client文件叫mcp_client.py。它做三件事启动 Server 子进程、列出工具、把工具定义转成 Anthropic 的 tools 格式发给模型import asyncio import os from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client import anthropic client anthropic.Anthropic( api_keyos.environ[TAOTOKEN_API_KEY], base_urlos.environ[TAOTOKEN_BASE_URL] ) async def main(): server_params StdioServerParameters( commandpython, args[weather_server.py] ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools_resp await session.list_tools() anthropic_tools [ { name: t.name, description: t.description, input_schema: t.inputSchema } for t in tools_resp.tools ] print(已发现工具:, [t[name] for t in anthropic_tools]) msg client.messages.create( modelclaude-sonnet-4-20250514, max_tokens1024, toolsanthropic_tools, messages[{role: user, content: 北京现在天气怎么样}] ) for block in msg.content: if block.type tool_use: print(f模型请求调用: {block.name} 参数: {block.input}) result await session.call_tool(block.name, block.input) print(工具返回:, result.content[0].text) if __name__ __main__: asyncio.run(main())这段代码里session.initialize()就是协议握手list_tools()是工具注册发现call_tool()是请求响应。三个环节全在这一段里。运行前确保weather_server.py和mcp_client.py在同一目录。4. 验证请求一次端到端调用与成功结果先单独测 Server 能不能起来。直接跑python weather_server.py如果没报错、进程挂起等待输入说明 stdio 传输正常。按 CtrlC 退出。然后跑 Clientpython mcp_client.py正常输出应该是这样已发现工具: [get_weather, get_population] 模型请求调用: get_weather 参数: {city: 北京} 工具返回: 北京今日气温25摄氏度晴看到这三行说明整条链路通了Client 通过 stdio 启动 Server握手成功列出两个工具把工具定义发给 Claude模型判断需要调get_weatherClient 执行call_toolServer 返回文本最后打印出来。如果你想验证多工具协同把用户问题改成「北京天气怎么样人口多少」模型会连续发起两次tool_use你会在输出里看到两次调用记录。这就是 MCP 相比手写 Function Calling 省事的地方——工具发现和参数构造全自动。再验证一下模型对话入口确认 Key 和 Base URL 没问题。如果上面 Client 跑通了说明通道是好的。想单独测模型响应可以直接用模型对话页面发一条消息看是否正常返回。实测下来从session.initialize()到拿到工具结果本地 stdio 传输的延迟基本在毫秒级瓶颈只在模型推理那一步。如果你把 Server 换成 SSE 传输mcp.server.sse就能跨机器部署Client 端改一下连接地址即可工具代码不用动。5. 本篇常见错误排查401、local proxy failed 与 reading choices跑不通的时候报错基本集中在这几类我按实际遇到的频率排一下。401 Unauthorized最常见。要么 Key 没读到环境变量要么 Base URL 写错。检查echo $TAOTOKEN_API_KEY有没有值再确认base_url是https://taotoken.net/api而不是带/v1的版本。Anthropic SDK 报 401 时不会告诉你具体是 Key 还是 URL 的问题两个都要查。local proxy failed / connection refused这个通常出现在 Client 启动 Server 子进程时。StdioServerParameters里的command必须是当前环境能直接执行的比如python在某些系统上要写成python3或绝对路径。如果 Server 脚本路径不对子进程起不来Client 就会报连接失败。用which python确认解释器路径。reading choices / KeyError choices这个报错说明你请求打到了 OpenAI 格式的端点但用的是 Anthropic SDK。Anthropic 的响应结构是content数组OpenAI 是choices。检查 Base URL 是不是被别的环境变量覆盖了或者 SDK 版本混用。TaoToken 的 Anthropic 兼容端点是/apiOpenAI 兼容是另一个路径别搞混。OAuth / authentication_errorClaude Code 或 Cline 里出现这个多半是settings.json里三件套没写全。Base URL、Key、Model ID 缺一个都会触发。Model ID 要写完整版本号比如claude-sonnet-4-20250514不能只写claude-sonnet。工具调用死循环模型反复请求同一个工具。检查call_tool的返回值是不是TextContent列表如果返回了空或格式不对模型拿不到结果会重试。另外inputSchema的required字段要和模型传的参数对得上否则参数校验失败也会循环。排障时建议先把 Server 单独跑起来用mcpSDK 自带的 inspector 工具测一下工具列表确认 Server 端没问题再查 Client。这样能把问题范围缩小一半。6. 继续深入从本地链路到长期编码工作流本地链路跑通之后下一步通常是把它接到真实的编码或 Agent 工作流里。MCP 的价值在长期使用中才明显——你写一次工具 ServerClaude Code、Cline、Codex 都能复用不用为每个客户端重写一遍工具描述。如果你打算把 MCP 用在日常编码里比如让模型直接查数据库、调内部 API、读本地文件建议走 Coding Plan 这类长期方案比按次调用省心。接入文档里有 SSE 传输和远程 Server 的部署示例工具代码和本地版完全一致只改传输层。我自己的做法是把常用工具拆成几个独立 Server一个管文件系统一个管 HTTP 请求一个管数据库查询。每个 Server 单独测试Client 端按需挂载。这样某个工具出问题不会拖垮整条链路排查也快。MCP 的协议设计本来就支持多 Server 并行Client 可以同时连多个工具列表合并后一起发给模型。最后留一个实用技巧工具描述description写得越具体模型选错工具的概率越低。别写「查询数据」写「根据城市名查询该城市过去24小时的逐小时气温返回摄氏度」。模型靠这段文字做决策它比参数 Schema 更重要。