1. 从零理解 MCP:Python 手写一个可调试的 MCP Server 到底在做什么
MCP(Model Context Protocol)说白了就是一套“让模型调用外部工具”的约定。你可以把它想成模型和你的代码之间的一份菜单:模型看到菜单上有哪些工具、每个工具要什么参数,然后决定点哪道菜,你的 MCP Server 负责把这道菜做出来,再把结果端回去。Python 实现一个简单的 MCP,核心就三件事:注册工具、路由请求、把结果按协议格式返回。
很多人第一次接触 MCP 会以为它很玄,其实底层交互非常朴素。以常见的客户端为例:你在对话框里提问,客户端把你的问题连同“当前可用的 tools 列表”一起发给模型;模型分析后决定调用哪个 tool,并给出参数;客户端通过 MCP Server 执行这个 tool;执行结果回传给模型;模型再用自然语言把结果组织成回答展示给你。整条链路里,MCP Server 就是那个“真正干活”的角色。
这篇面向的是想自己动手写一个最小可用 MCP Server 的 Python 开发者。你不需要先精通协议细节,只要会写异步函数、懂一点类型注解,就能跟着把 server.py 骨架搭起来,注册两个工具,本地跑通,再把 endpoint 切到 TaoToken 完成一次真实的工具调用验证。适合谁?适合正在做 Agent、想让模型调用自己业务接口、又不想被复杂框架绑住的人。
我试过用官方 SDK 的 FastMCP 类来搭,体验是:它用 Python 的 type hints 和 docstring 自动生成 tool definition,省掉了手写 JSON Schema 的麻烦。你只要写一个带类型注解的 async 函数,加个装饰器,它就能被模型识别。下面从环境准备开始,一步步来。
2. TaoToken 前置准备:Python MCP Server 接入大模型 API 的 Base URL 与 Key 配置
在写 Server 之前,先把“模型侧”的接入准备好。MCP Server 本身只负责执行工具,真正决定调用哪个工具的是模型,所以你需要一个能访问模型 API 的入口。这里用 TaoToken 来做模型接入,它的接口兼容主流协议,Base URL 和 Key 配好之后,客户端或你的调试脚本就能直接调模型。
先拿到 API Key。打开 https://taotoken.net/api-keys ,登录后创建一个 Key,复制保存。注意 Key 只在创建时完整显示一次,丢了就重新建一个。这个 Key 后面会写进配置里,别硬编码进 Git 仓库。
Base URL 用 https://taotoken.net/api ,这是接口根地址,不要在后面乱加斜杠。模型 ID 按你实际要用的填,比如 claude 系列或 gpt 系列的模型标识,具体以控制台模型列表为准。你可以到 https://taotoken.net/console 查看可用模型和额度情况。
如果你用的是 Claude Code 这类工具,配置方式略有不同,可以参考接入文档 https://taotoken.net/doc 里的说明。文档里对 Base URL、Key、Model ID 三件套的填法讲得比较清楚。对于本篇的 MCP 调试,我们主要用 Python 脚本直接请求,所以只要保证 Base URL 和 Key 正确即可。
这里要强调一个容易踩的点:MCP Server 和模型 API 是两个独立的东西。Server 负责执行工具,模型 API 负责决策。很多人把两者混在一起,结果调试时不知道是工具没注册成功,还是模型没调通。分开验证,问题就好定位。先把模型 API 用 curl 或 Python 请求通,再回来调 MCP 工具链路。
3. 可复制配置:server.py 骨架、依赖清单与 MCP 工具注册
这一节是核心,直接给可复制的代码和配置。先建项目目录,用 uv 管理环境(用 conda 也行,命令自行替换)。
uv init mcp-demo cd mcp-demo uv venv # Windows 激活 .venv\Scripts\activate # macOS/Linux 激活 source .venv/bin/activate uv add "mcp[cli]" httpx依赖清单里,mcp[cli] 提供 FastMCP 和命令行调试能力,httpx 用来发异步 HTTP 请求。装完后新建 server.py,写入下面的骨架:
from typing import Any import httpx from mcp.server.fastmcp import FastMCP # 初始化 FastMCP server,名字会出现在客户端工具列表里 mcp = FastMCP("demo-server") # 模型 API 配置,实际使用时从环境变量读取更安全 TAOTOKEN_BASE_URL = "https://taotoken.net/api" TAOTOKEN_API_KEY = "sk-你的Key" async def call_model(prompt: str) -> str: """调用 TaoToken 模型接口,返回文本结果。""" headers = { "Authorization": f"Bearer {TAOTOKEN_API_KEY}", "Content-Type": "application/json", } payload = { "model": "你的模型ID", "messages": [{"role": "user", "content": prompt}], } async with httpx.AsyncClient() as client: resp = await client.post( f"{TAOTOKEN_BASE_URL}/v1/chat/completions", headers=headers, json=payload, timeout=60.0, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] @mcp.tool() async def echo(text: str) -> str: """原样返回输入文本,用于验证工具链路是否打通。 Args: text: 任意字符串 """ return f"echo: {text}" @mcp.tool() async def ask_model(question: str) -> str: """把问题转发给 TaoToken 模型并返回回答。 Args: question: 要问模型的问题 """ try: return await call_model(question) except Exception as e: return f"调用模型失败: {e}" if __name__ == "__main__": mcp.run(transport="stdio")这段代码里,@mcp.tool()装饰器把普通 async 函数注册成 MCP 工具,函数名就是工具名,docstring 会成为工具描述,参数类型注解会生成参数 schema。echo用来做链路自检,ask_model演示在工具内部再调模型 API,这是很多 Agent 场景的典型结构。
如果你用 Cline 或 Claude Code 这类客户端,配置通常是一个 JSON 片段,路径和字段名要对齐。以常见的 MCP 客户端配置为例:
{ "mcpServers": { "demo-server": { "command": "uv", "args": ["run", "server.py"], "env": { "TAOTOKEN_API_KEY": "sk-你的Key" } } } }注意 Base URL、Key、Model ID 三件套要齐全:Base URL 是 https://taotoken.net/api ,Key 从 API Keys 页面拿,Model ID 按控制台实际模型填。少任何一个,工具调用都会失败。
4. 验证请求:本地启动 MCP Server 并完成一次工具调用
代码写好后,先本地跑起来。用官方 CLI 调试最方便:
uv run mcp dev server.py这会启动一个调试界面,能看到已注册的工具列表。如果echo和ask_model都出现了,说明注册成功。接着在调试界面里调用echo,输入hello,应该返回echo: hello。这一步验证的是 MCP 协议链路本身,不涉及模型。
然后验证ask_model。调用它并传入用一句话解释什么是 MCP,如果返回一段模型生成的解释,说明工具内部调用 TaoToken 模型 API 也通了。这一步同时验证了两条链路:MCP 工具执行 + 模型 API 请求。
如果你想用脚本方式验证,可以写一个简单的 stdio 客户端,或者直接用 curl 先单独测模型 API:
curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-你的Key" \ -H "Content-Type: application/json" \ -d '{"model":"你的模型ID","messages":[{"role":"user","content":"ping"}]}'返回里有choices字段就说明模型侧没问题。再回到 MCP 调试界面调ask_model,成功结果应该是一段自然语言回答。实测下来,把两条链路分开验证,排错效率高很多。
成功结果长这样:调试界面显示工具调用状态为 completed,返回内容是你问的问题的模型回答。如果echo通了但ask_model不通,问题一定在模型 API 配置上,而不是 MCP 协议。
5. 本篇常见错排查:401、local proxy failed、reading choices 与 OAuth 报错
调试 MCP Server 时,报错基本集中在几类。下面按真实报错对照排查。
第一类:401 Unauthorized。这通常是 Key 不对或没带上。检查Authorization头是不是Bearer sk-xxx格式,Key 有没有多余空格,环境变量有没有真的传进去。如果你在客户端配置里写了env,确认字段名和代码里读取的变量名一致。
第二类:local proxy failed或连接被拒。这多半是 Base URL 写错,比如多加了/v1或结尾斜杠。Base URL 就用 https://taotoken.net/api ,路径拼接交给代码。另外确认本机网络能正常访问该地址,公司网络限制也可能导致连接失败。
第三类:reading choices相关报错,比如KeyError: 'choices'或list index out of range。这说明请求返回了,但结构不是预期的 chat completions 格式。常见原因是 Model ID 填错,或者请求体字段名不对。打印完整响应体再定位,别只看异常信息。
第四类:OAuth 相关报错。如果你用的是 Claude Code 这类带 OAuth 流程的工具,报错可能出现在授权环节。这时候检查接入文档里的配置步骤,确认 Base URL 和 Key 的填法符合要求。OAuth 和 API Key 是两种不同的鉴权方式,别混用。
还有一个隐蔽的坑:MCP Server 用 stdio 传输时,任何print输出都会污染协议流,导致客户端解析失败。调试信息一律用sys.stderr或日志文件,别用print。这个坑我踩过,排查了半天才发现是调试打印惹的祸。
6. 语义一致 CTA:把 MCP Server 接到真实模型与长期编码场景
工具链路跑通后,下一步就是把它接到真实使用场景。如果你主要做模型对话验证,可以到 https://taotoken.net/chat 直接体验模型对话,确认模型侧行为符合预期。如果你要把 MCP 用在长期编码或 Agent 任务里,建议看 Coding Plan https://taotoken.net/coding-plan ,它更适合持续性的开发场景。
接入文档在 https://taotoken.net/doc ,里面覆盖了 Base URL、Key、Model ID 的完整配置说明,遇到鉴权或路径问题先查这里。API Key 管理在 https://taotoken.net/api-keys ,Key 泄露或额度异常时及时轮换。控制台 https://taotoken.net/console 可以看用量和模型列表。
回到代码本身,一个实用的技巧是:把call_model里的模型 ID 和 Base URL 抽成环境变量,这样本地调试和线上部署用同一份代码,只换配置。另外给工具函数加超时和重试,模型 API 偶尔抖动时不会直接把整个 MCP 调用拖死。最后,工具描述(docstring)写清楚,模型选工具的准确率会明显提升,这比调参管用。