1. 从零写一个 MCP Server 到底难在哪Python 开发者最容易卡住的三个环节MCP Server 说白了就是给大模型装的一双手模型本身只会输出文字但通过 MCP 协议它可以调用你写的 Python 函数去查天气、读数据库、发请求。你写一个mcp.tool()装饰的函数模型就能在对话里决定要不要调用它。听起来很美好但真正动手时Python 开发者通常会在三个地方卡住。第一个卡点是环境。MCP 的 Python SDK 迭代很快mcp[cli]这个包如果你用系统 Python 直接pip install大概率会遇到依赖冲突尤其是你机器上已经装了 LangChain、FastAPI 这类同样依赖 pydantic 的库。我试过在全局环境里硬装结果把原来的项目搞崩了后来老老实实用uv隔离虚拟环境才干净。第二个卡点是传输层。MCP 支持 stdio 和 SSE 两种通信机制新手最容易搞混stdio 是本地进程间通信客户端把你的 server 当子进程启动通过标准输入输出传 JSON-RPC 消息SSE 是走 HTTP 的远程通信适合部署到服务器上给多个客户端用。选错了传输方式客户端根本连不上报错还特别隐晦。第三个卡点也是最容易被忽略的是模型侧的统一接入。你辛辛苦苦把 server 写好了工具也注册了但客户端里配置的模型 endpoint 五花八门——今天用这个平台的 Key明天换那个平台的 Key每个平台的 Base URL、模型 ID、鉴权头都不一样。MCP Server 本身不解决这个问题它只负责工具这一层。真正让整条链路跑通你还需要一个统一的模型接入层把 endpoint 和 Key 收敛到一处。这也是后面我会重点讲的 TaoToken 接入部分。这篇文章的定位很明确给 Python 开发者一份能直接复制、能跑起来、能排错的 MCP Server 实战指南。从环境准备到工具函数注册从 stdio/SSE 传输层实现到本地调试最后把模型 endpoint 和鉴权配置改到 TaoToken用统一 Key 完成一次端到端调用验证。全程代码可复制命令可执行报错有对照。适合谁看如果你已经会写 Python 函数想让自己的工具被大模型调用或者你在做 AI Agent 相关开发需要给 Agent 挂载自定义能力再或者你只是想搞明白 MCP 到底是怎么回事亲手跑一个 demo——这篇都合适。不需要你提前懂 JSON-RPC也不需要你读过 MCP 规范原文跟着步骤走就行。2. 动手前的准备uv 环境、MCP Python SDK 与 TaoToken 统一 Key 配置2.1 为什么用 uv 而不是 pipMCP Python SDK 官方推荐用uv管理项目。uv是 Rust 写的 Python 包管理器速度比 pip 快一个数量级而且它把虚拟环境、依赖锁定、Python 版本管理都揉在一起了。对于 MCP 这种依赖更新频繁的场景uv能帮你避免昨天还能跑今天装了个新包就崩了的尴尬。安装 uv 的命令macOS 和 Linux 通用curl -LsSf https://astral.sh/uv/install.sh | shWindows 用户用 PowerShellpowershell -ExecutionPolicy ByPass -c irm https://astral.sh/uv/install.ps1 | iex装完之后uv --version能输出版本号就说明成功了。这里有个小坑如果你之前装过旧版 uv记得先uv self update升到最新否则uv init生成的pyproject.toml格式可能和 SDK 要求的不一致。2.2 初始化项目与依赖清单新建一个目录初始化项目创建虚拟环境装依赖uv init mcp-demo cd mcp-demo uv venv source .venv/bin/activate # Windows 用 .venv\Scripts\activate uv add mcp[cli] httpxmcp[cli]里的[cli]是可选依赖组它会额外装上mcp dev命令需要的调试工具包括 MCP Inspector。httpx是异步 HTTP 客户端后面写工具函数调外部 API 会用到。装完之后你的pyproject.toml里应该能看到类似这样的依赖声明[project] name mcp-demo version 0.1.0 requires-python 3.10 dependencies [ mcp[cli]1.2.0, httpx0.27.0, ]注意requires-python这一行MCP Python SDK 要求 Python 3.10 及以上。如果你机器上默认是 3.9uv venv的时候要显式指定版本比如uv venv --python 3.11。2.3 TaoToken 统一 Key 的获取与配置MCP Server 写完之后真正调用模型的那一端需要配置 endpoint 和 Key。这里我用 TaoToken 来做统一接入原因是它把多个模型的 endpoint 收敛成一个 Base URLKey 也统一管理省得你在客户端里为每个模型单独配一遍。先去官网注册并拿到 Keyhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content登录后在控制台的 API Keys 页面创建一个新 Key复制出来。这个 Key 后面会用在客户端的模型配置里。TaoToken 的 API Base URL 是https://taotoken.net/api注意这个地址不带任何查询参数是纯粹的 API 入口。你在客户端里配置的时候Base URL 填这个Key 填刚才复制的模型 ID 填你想用的模型名比如claude-sonnet-4-20250514或gpt-4o之类具体以控制台模型列表为准。如果你用的是 Claude Code 这类工具配置方式略有不同需要设置环境变量export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_API_KEY你的TaoToken Key这样 Claude Code 就会把请求发到 TaoToken由它统一转发到对应模型。MCP Server 本身不关心模型走哪个 endpoint它只负责暴露工具模型侧的接入是客户端的事。把这两层分清楚后面调试的时候就不会乱。3. 可复制的 MCP Server 骨架工具注册、stdio 与 SSE 传输层实现3.1 最小可运行骨架先写一个最简单的 server只注册一个工具用 stdio 传输。新建server.pyfrom mcp.server.fastmcp import FastMCP mcp FastMCP(demo-server) mcp.tool() def add(a: int, b: int) - int: 把两个整数相加。 Args: a: 第一个整数 b: 第二个整数 return a b if __name__ __main__: mcp.run(transportstdio)这段代码里FastMCP是 SDK 提供的高层封装你只要用mcp.tool()装饰函数它就会自动把函数签名转成 JSON Schema注册成一个 MCP 工具。函数名add就是工具名docstring 就是工具描述——模型靠这个描述来判断什么时候该调用它。所以 docstring 一定要写清楚别偷懒。mcp.run(transportstdio)表示用标准输入输出通信。这种模式下客户端会以子进程的方式启动你的 server双方通过 stdin/stdout 交换 JSON-RPC 消息。适合本地开发也是 Cursor、Cline、Claude Desktop 默认支持的方式。3.2 注册带外部请求的工具光会加法没意思写个真正调外部 API 的工具。下面这个例子查天气预警用httpx发异步请求from typing import Any import httpx from mcp.server.fastmcp import FastMCP mcp FastMCP(weather) NWS_API_BASE https://api.weather.gov USER_AGENT weather-app/1.0 async def make_request(url: str) - dict[str, Any] | None: headers {User-Agent: USER_AGENT, Accept: application/geojson} async with httpx.AsyncClient() as client: try: resp await client.get(url, headersheaders, timeout30.0) resp.raise_for_status() return resp.json() except Exception: return None mcp.tool() async def get_alerts(state: str) - str: 获取美国某个州的天气预警。 Args: state: 两个字母的州代码比如 CA、NY url f{NWS_API_BASE}/alerts/active/area/{state} data await make_request(url) if not data or features not in data: return 无法获取预警数据。 if not data[features]: return 该州当前没有活跃预警。 return \n---\n.join( f事件: {f[properties].get(event, 未知)}\n f区域: {f[properties].get(areaDesc, 未知)} for f in data[features] ) if __name__ __main__: mcp.run(transportstdio)这里有几个细节值得说。第一工具函数可以是async的SDK 会自动处理异步调用。第二参数类型标注state: str会被转成 JSON Schema 的type: string模型据此知道该传什么。第三返回值必须是可序列化的字符串最省事复杂结构建议先转成字符串再返回避免模型看不懂。3.3 切换到 SSE 传输如果你想把 server 部署到远程让多个客户端通过 HTTP 访问就要用 SSE 传输。改动很小把最后一行换掉if __name__ __main__: mcp.run(transportsse, host0.0.0.0, port8000)启动后server 会在http://0.0.0.0:8000/sse上监听。客户端配置的时候类型选sseURL 填http://你的服务器IP:8000/sse。注意 SSE 模式下客户端不再以子进程方式启动 server而是通过网络连接所以 server 要单独跑着。两种传输方式的对照维度stdioSSE通信方式标准输入输出HTTP Server-Sent Events适用场景本地开发、单机远程部署、多客户端客户端配置command argsurl进程管理客户端拉起子进程server 独立运行调试难度低中涉及网络选哪个本地自己用stdio 就够了配置简单不用管端口和防火墙。要给团队共享或者部署到服务器再上 SSE。3.4 客户端配置片段以 Cline 为例它的 MCP 配置是一个 JSON 文件路径通常在~/.cline/mcp_settings.json不同版本可能略有差异。stdio 模式的配置{ mcpServers: { weather: { command: uv, args: [ --directory, /ABSOLUTE/PATH/TO/mcp-demo, run, server.py ] } } }注意--directory后面必须是绝对路径相对路径在某些客户端里会解析失败。SSE 模式的配置{ mcpServers: { weather: { type: sse, url: http://127.0.0.1:8000/sse } } }Cursor 的配置在设置里的 MCP 面板点 Add new MCP server类型选 command命令填uv --directory /绝对路径/mcp-demo run server.py。Claude Desktop 的配置在claude_desktop_config.json格式和 Cline 类似。4. 本地调试与端到端验证MCP Inspector 加 TaoToken 统一 Key 跑通一次调用4.1 用 MCP Inspector 做本地调试SDK 自带的mcp dev命令会启动 MCP Inspector这是一个网页版的调试工具能列出你注册的所有工具手动填参数调用看返回结果。启动命令mcp dev server.py终端会输出一个本地地址通常是http://localhost:5173浏览器打开就能看到界面。左侧是工具列表点进去能看到每个工具的参数 schema填上参数点 Run右侧显示返回的 JSON。这一步能帮你确认工具注册是否成功、参数类型对不对、函数逻辑有没有 bug不用先接客户端。Inspector 还有个好处是能看到完整的 JSON-RPC 消息流。如果你好奇 MCP 底层到底传了什么点开消息日志能看到tools/list、tools/call这些请求和响应的原始内容。调试协议层问题的时候特别有用。4.2 在客户端里接入 TaoToken 统一 Key工具调通了接下来验证模型侧。以 Cline 为例打开设置找到 API Provider 配置。这里选 OpenAI Compatible 或者 Anthropic取决于你想用哪个模型。关键是把 Base URL 和 Key 指向 TaoToken{ apiProvider: openai, baseUrl: https://taotoken.net/api, apiKey: 你的TaoToken Key, modelId: gpt-4o }如果你用的是 Claude Code配置方式是通过环境变量前面 2.3 节已经写过。Codex 的话配置在~/.codex/auth.json{ OPENAI_API_KEY: 你的TaoToken Key, OPENAI_BASE_URL: https://taotoken.net/api }三件套记住Base URL 填https://taotoken.net/apiKey 填 TaoToken 控制台创建的 KeyModel ID 填你要用的模型名。这三个配对了模型请求就能正常走通。4.3 端到端验证让模型调用你的工具配置好之后在客户端对话框里输入一句会触发工具调用的话比如帮我查一下加州现在的天气预警。模型会先分析这句话判断需要调用get_alerts工具然后客户端通过 MCP 协议调用你的 server拿到结果再交回模型生成自然语言回复。整个链路是这样的你的输入 → 客户端 → TaoToken模型推理→ 模型决定调用工具 → 客户端 → MCP Server执行get_alerts→ 返回结果 → 客户端 → TaoToken模型生成回复→ 你看到答案。如果这一步成功了说明 MCP Server 和模型接入都通了。如果模型没有调用工具而是直接瞎编了一个答案通常是工具描述写得不够清楚模型没意识到该用工具。回去改 docstring把使用场景写明确。4.4 验证请求的原始返回想确认请求真的走了 TaoToken可以在客户端开 debug 日志或者用 curl 直接测一下 API 是否可达curl https://taotoken.net/api/v1/models \ -H Authorization: Bearer 你的TaoToken Key能返回模型列表就说明 Key 和 Base URL 没问题。注意这个地址是https://taotoken.net/api/v1/models/api是基础路径/v1/models是具体的接口。不同客户端对 Base URL 的拼接方式不一样有的会自动加/v1有的不会配置的时候留意一下。5. 常见报错对照排查401、local proxy failed、reading choices 与 OAuth 问题5.1 401 Unauthorized最常见的报错意思是鉴权失败。可能原因有三个Key 填错了、Key 过期了、Base URL 和 Key 不匹配。排查步骤先去 TaoToken 控制台确认 Key 还在有效期内然后检查客户端配置里的 Key 有没有多余空格复制粘贴时特别容易带上换行符。如果 Key 没问题检查 Base URL 是不是https://taotoken.net/api别写成带/v1的版本有些客户端会自己拼/v1你多写一层就变成/api/v1/v1/...了。5.2 local proxy failed这个报错通常出现在客户端启动 MCP Server 子进程的时候。意思是客户端尝试用你配置的 command 启动 server但启动失败了。排查先在终端里手动跑一遍uv --directory /绝对路径/mcp-demo run server.py看能不能正常启动。如果终端里能跑客户端里报错多半是路径问题——客户端的工作目录和终端不一样相对路径会失效必须用绝对路径。另外确认uv在客户端的 PATH 里有些 GUI 客户端不继承 shell 的环境变量你可能要写uv的完整路径比如/Users/你的用户名/.local/bin/uv。5.3 reading choices 相关报错这个报错一般出现在模型返回的响应格式不符合预期的时候。典型信息是error reading choices或者cannot read property choices of undefined。原因是客户端期望 OpenAI 格式的响应带choices数组但实际拿到的不是。可能情况Base URL 配错了请求打到了非 OpenAI 兼容的接口或者模型 ID 填错了服务端返回了错误信息而不是正常响应。排查用 4.4 节的 curl 命令确认接口可达然后检查 Model ID 是否在 TaoToken 支持的模型列表里。5.4 OAuth 与鉴权头问题有些客户端尤其是 Claude Code 这类默认走 OAuth 流程而不是简单的 Bearer Token。如果你配置了 TaoToken 的 Key 但还是报鉴权错误检查客户端是不是在尝试 OAuth。Claude Code 的话设置ANTHROPIC_API_KEY环境变量后它会优先用这个 Key 而不是走 OAuth。如果还是不行检查有没有残留的 OAuth 配置文件比如~/.claude/目录下的凭据文件清掉再试。5.5 工具调用无响应模型不调用工具或者调用了但没返回结果。前者是工具描述问题改 docstring。后者可能是 server 执行超时检查工具函数里有没有阻塞操作httpx请求记得设timeout。还有一种情况是返回值太大模型处理不了把返回结果截断或者精简一下。5.6 版本不兼容MCP SDK 更新快有时候新版本改了 API旧代码跑不起来。报错通常是ImportError或者AttributeError。解决办法看 SDK 的 changelog或者直接锁版本在pyproject.toml里把mcp[cli]固定到某个具体版本比如mcp[cli]1.2.0。Node.js 相关的版本问题如果你用 npx 启动某些工具也类似用 nvm 切到兼容版本。6. 把 MCP Server 用起来从调试到长期编码的接入路径工具调通之后接下来就是把它用起来。如果你只是偶尔验证一下模型能力直接在客户端里对话就行TaoToken 的模型对话入口可以快速试各种模型https://taotoken.net/api如果你要长期做编码或者 Agent 开发建议用 Coding Plan它针对代码场景做了优化MCP 工具调用的稳定性更好。控制台里可以管理你的 Key、查看用量https://taotoken.net/console创建和管理 API Key 的页面https://taotoken.net/api-keys接入文档在这里配置细节以文档为准https://taotoken.net/docClaude Code 的接入说明单独有一页https://taotoken.net/ClaudeCodeAnthropic最后说个实际经验MCP Server 开发最容易踩的坑不是代码本身而是环境隔离和路径配置。用 uv 管好虚拟环境客户端配置一律用绝对路径工具函数的 docstring 写清楚使用场景这三件事做到位能省掉八成的调试时间。模型接入层用 TaoToken 统一 Key 之后换模型不用改 MCP Server 的代码只改客户端配置就行维护成本低很多。