1. MCP 服务端与客户端到底在解决什么问题
MCP(Model Context Protocol,模型上下文协议)说白了就是给大模型装了一根“标准数据线”。以前你想让模型查天气、读数据库、调内部接口,每个客户端都得单独写一套适配代码;现在只要服务端按 MCP 规范暴露工具(Tools)、资源(Resources)、提示(Prompts),任何支持 MCP 的客户端都能即插即用。它解决的问题不是“让模型更聪明”,而是“让模型能稳定地拿到外部上下文并执行动作”。
这套架构里有三个角色要分清。主机(Host)是跑大模型的应用程序,比如 Claude Desktop、Cline、Cursor;客户端(Client)是主机内部负责跟服务端建立 1:1 连接的模块;服务端(Server)则是你写的那个真正提供工具的程序。工作链路是:主机启动客户端 → 客户端连接服务端 → 服务端注册工具 → 模型决定调用哪个工具 → 客户端把调用结果回传给模型。理解这条链路,后面排错才不会抓瞎。
适合谁看?如果你已经会用 Python 写点脚本,想让本地模型或 IDE 里的 AI 助手调用你自己的函数,但又不想研究各家私有插件协议,那 MCP 就是最短路径。我试过从零起一个本地 MCP 服务再让客户端完成握手和工具调用,整个最小链路跑通大概二十分钟,难点不在代码,而在配置路径和 Key 的接入方式。
这篇会聚焦“最小可运行链路”:先起一个本地 MCP 服务端,再让客户端完成初始化握手、列出工具、成功调用一次。同时把 TaoToken 统一 Key 和 API 通道的接入写法嵌进去,这样你后面换模型、换客户端都不用到处改配置。三步验证动作会贯穿全文:服务端日志出现初始化、客户端列出工具、成功调用一次。
2. TaoToken 统一 Key 与 API 通道的前置准备
在动手写代码前,先把“钥匙”和“通道”准备好。MCP 客户端在调用模型时需要一个兼容 Anthropic 或 OpenAI 风格的 API 端点,TaoToken 在这里扮演的就是统一入口:你只维护一个 Key,就能在模型对话、Coding Plan、API Keys 之间切换,不用为每个客户端单独申请凭证。
第一步,打开 TaoToken 官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册并登录。登录后进入控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在左侧找到 API Keys 页面 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,点“创建新 Key”。创建时建议按用途命名,比如mcp-client-local,方便后面排查是哪个客户端在消耗额度。Key 只显示一次,复制后先存到密码管理器里。
第二步,确认 API 通道地址。TaoToken 的 API 基础地址是 https://taotoken.net/api ,注意这个地址不带任何查询参数,直接作为 Base URL 使用。如果你用的是 Anthropic SDK,它默认会往/v1/messages发请求;如果你用的是 OpenAI 兼容 SDK,则走/v1/chat/completions。TaoToken 的通道对这两种风格都做了适配,所以你在客户端里只需要把 Base URL 指向它,再把 Key 填进去即可。
第三步,想清楚你要跑哪种场景。如果只是验证 MCP 工具调用能不能通,用模型对话 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 先手动问一句“你能看到哪些工具”就够了;如果你打算长期在 IDE 里跑编码 Agent,那更适合开 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,额度模型和按量计费不一样。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到参数不确定时优先查这里。
这里有个容易踩的坑:很多人把 Key 直接写进代码里提交到 Git,结果 Key 泄露被刷额度。正确做法是写进.env文件,并且第一时间把.env加进.gitignore。后面客户端配置里我会给出完整的.env写法。另外,TaoToken 的 Key 是统一凭证,服务端本身不需要 Key,只有客户端调用模型时才需要,所以服务端代码里不要塞 Key,避免权限扩散。
3. 可复制的服务端与客户端配置
先起服务端。用uv管理环境最省事,Windows 下打开 PowerShell:
uv init mcp-weather cd mcp-weather uv venv .venv\Scripts\activate uv add "mcp[cli]" httpx new-item weather.py然后把下面这段服务端代码完整复制进weather.py。它用FastMCP注册了两个工具:get_alerts查美国州级天气预警,get_forecast按经纬度查未来五天预报。注意mcp.run(transport='stdio')这行,stdio 是本地 MCP 最常用的传输方式,客户端通过标准输入输出跟它通信。
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_nws_request(url: str) -> dict[str, Any] | None: headers = {"User-Agent": USER_AGENT, "Accept": "application/geo+json"} async with httpx.AsyncClient() as client: try: response = await client.get(url, headers=headers, timeout=30.0) response.raise_for_status() return response.json() except Exception: return None def format_alert(feature: dict) -> str: props = feature["properties"] return f"Event: {props.get('event', 'Unknown')}\nArea: {props.get('areaDesc', 'Unknown')}\nSeverity: {props.get('severity', 'Unknown')}" @mcp.tool() async def get_alerts(state: str) -> str: """Get weather alerts for a US state. Args: state: Two-letter US state code.""" url = f"{NWS_API_BASE}/alerts/active/area/{state}" data = await make_nws_request(url) if not data or "features" not in data: return "Unable to fetch alerts." if not data["features"]: return "No active alerts." return "\n---\n".join(format_alert(f) for f in data["features"]) @mcp.tool() async def get_forecast(latitude: float, longitude: float) -> str: """Get weather forecast for a location.""" points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}" points_data = await make_nws_request(points_url) if not points_data: return "Unable to fetch forecast data." forecast_url = points_data["properties"]["forecast"] forecast_data = await make_nws_request(forecast_url) if not forecast_data: return "Unable to fetch detailed forecast." periods = forecast_data["properties"]["periods"] return "\n---\n".join( f"{p['name']}: {p['temperature']}°{p['temperatureUnit']} {p['detailedForecast']}" for p in periods[:5] ) if __name__ == "__main__": mcp.run(transport='stdio')服务端起好后,客户端这边要解决“连哪个服务端”和“用哪个模型”两件事。先建项目:
uv init mcp-client cd mcp-client uv venv .venv\Scripts\activate uv add mcp anthropic python-dotenv new-item client.py new-item .env.env文件里写 TaoToken 的 Key 和 Base URL。注意这里用的是 Anthropic SDK 风格,所以变量名保持ANTHROPIC_API_KEY,但值换成 TaoToken 的 Key,同时把ANTHROPIC_BASE_URL指向 TaoToken 的 API 通道:
ANTHROPIC_API_KEY=sk-你的TaoTokenKey ANTHROPIC_BASE_URL=https://taotoken.net/api紧接着把.env排除出版本控制:
echo ".env" >> .gitignore客户端代码的核心是connect_to_server和process_query两个方法。前者用StdioServerParameters拉起服务端进程并完成session.initialize()握手,后者把服务端返回的工具列表转成 Anthropic 的tools参数,再处理tool_use类型的响应。下面这段可以直接用:
import asyncio import sys from typing import Optional from contextlib import AsyncExitStack from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client from anthropic import Anthropic from dotenv import load_dotenv load_dotenv() class MCPClient: def __init__(self): self.session: Optional[ClientSession] = None self.exit_stack = AsyncExitStack() self.anthropic = Anthropic() async def connect_to_server(self, server_script_path: str): is_python = server_script_path.endswith('.py') command = "python" if is_python else "node" server_params = StdioServerParameters( command=command, args=[server_script_path], env=None ) stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) self.stdio, self.write = stdio_transport self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write)) await self.session.initialize() response = await self.session.list_tools() print("\nConnected with tools:", [t.name for t in response.tools]) async def process_query(self, query: str) -> str: messages = [{"role": "user", "content": query}] response = await self.session.list_tools() available_tools = [{ "name": t.name, "description": t.description, "input_schema": t.inputSchema } for t in response.tools] response = self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=1000, messages=messages, tools=available_tools ) final_text = [] assistant_content = [] for content in response.content: if content.type == 'text': final_text.append(content.text) assistant_content.append(content) elif content.type == 'tool_use': result = await self.session.call_tool(content.name, content.input) final_text.append(f"[Calling {content.name} with {content.input}]") assistant_content.append(content) messages.append({"role": "assistant", "content": assistant_content}) messages.append({"role": "user", "content": [{ "type": "tool_result", "tool_use_id": content.id, "content": result.content }]}) response = self.anthropic.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=1000, messages=messages, tools=available_tools ) final_text.append(response.content[0].text) return "\n".join(final_text) async def chat_loop(self): print("\nMCP Client Started! Type 'quit' to exit.") while True: query = input("\nQuery: ").strip() if query.lower() == 'quit': break print("\n" + await self.process_query(query)) async def cleanup(self): await self.exit_stack.aclose() async def main(): if len(sys.argv) < 2: print("Usage: python client.py <path_to_server_script>") sys.exit(1) client = MCPClient() try: await client.connect_to_server(sys.argv[1]) await client.chat_loop() finally: await client.cleanup() if __name__ == "__main__": asyncio.run(main())如果你用的是 Claude Code 或 Cline 这类客户端,配置方式不是写 Python,而是改 JSON。以 Claude Code 的settings.json为例,MCP 服务端配置长这样:
{ "mcpServers": { "weather": { "command": "uv", "args": ["--directory", "T:/PythonProject/mcp-weather", "run", "weather.py"] } } }注意路径必须用绝对路径,Windows 下反斜杠要转义或直接写正斜杠。Cline 的 MCP 配置在cline_mcp_settings.json里,结构类似,但多一个disabled和autoApprove字段。Codex 的auth.json则是另一套,它管的是模型凭证而不是 MCP 服务端,别混在一起。无论哪种客户端,只要涉及模型调用,三件套必须齐全:Base URL 指向https://taotoken.net/api、Key 填 TaoToken 的 Key、Model ID 填你实际要用的模型名。
4. 三步验证:日志、工具列表、成功调用
配置写完不代表通了,必须按顺序验证三步,任何一步失败都能快速定位问题层。
第一步,验证服务端能独立启动。在服务端目录下直接跑:
uv run weather.py如果 stdio 模式下没有报错、进程挂起等待输入,说明服务端本身没问题。更直观的方式是看日志:FastMCP 在收到客户端初始化请求时会打印类似Initialized server session或工具注册信息。如果你在客户端里连不上,先回到这一步确认服务端单独能跑。这一步对应“服务端日志出现初始化”。
第二步,验证客户端能列出工具。运行客户端并传入服务端脚本路径:
uv run client.py T:/PythonProject/mcp-weather/weather.py如果握手成功,终端会打印Connected with tools: ['get_alerts', 'get_forecast']。这行输出就是“客户端列出工具”的验证点。如果这里报Connection closed或卡住不动,八成是路径写错、Python 环境不对,或者服务端脚本里有语法错误导致进程直接退出。可以先把服务端脚本单独跑一遍,确认没有 import 错误。
第三步,验证工具调用能真正执行。在Query:提示符下输入:
what's the weather in NY正常流程是:客户端把工具列表发给模型 → 模型返回tool_use调用get_alerts,参数state=NY→ 客户端执行session.call_tool→ 服务端请求 NWS API → 结果回传模型 → 模型生成自然语言回答。终端会先打印[Calling get_alerts with {'state': 'NY'}],然后输出预警内容。看到这个,说明整条链路通了。
如果你用的是 Claude Desktop 而不是自己写的 Python 客户端,验证方式略有不同:改完claude_desktop_config.json后,必须彻底退出 Claude(任务管理器里结束进程),再重新打开。对话框下方出现锤子图标就代表 MCP 服务加载成功,点开能看到工具列表。然后在对话里问天气,观察是否触发工具调用。这里有个细节:Claude Desktop 的配置文件路径在 Windows 下是C:\Users\你的用户名\AppData\Roaming\Claude\claude_desktop_config.json,路径写错它不会报错,只是静默不加载。
三步验证的核心逻辑是分层排查:服务端独立能跑 → 客户端能握手并列出工具 → 工具能实际执行并返回结果。任何一步失败,问题一定在那一层,不用瞎猜。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
跑 MCP 最容易遇到的报错就那么几个,逐个拆。
401 Unauthorized。这个几乎都是 Key 的问题。检查.env里ANTHROPIC_API_KEY是不是 TaoToken 的 Key,有没有多余空格或换行;检查ANTHROPIC_BASE_URL是不是https://taotoken.net/api,末尾不要多加/v1,SDK 会自己拼。如果你用的是 OpenAI 兼容客户端,变量名可能是OPENAI_API_KEY和OPENAI_BASE_URL,别填错。还有一种情况是 Key 被禁用或额度耗尽,去控制台 API Keys 页面确认状态。
local proxy failed。这个报错通常出现在客户端尝试连接本地服务端时。原因一般是command或args里的路径不对,比如uv不在 PATH 里,或者--directory指向的目录不存在。Windows 下特别注意路径分隔符,JSON 里反斜杠要写成\\或直接用/。另外,如果你在虚拟环境里装了mcp但客户端用的是全局 Python,也会因为找不到模块而失败。解决办法是统一用uv run,让 uv 自己管理环境。
Error reading choices。这个报错多见于模型返回格式不符合预期,或者客户端解析响应时字段缺失。常见诱因是模型 ID 写错,比如把claude-3-5-sonnet-20241022写成了别的版本,导致 API 返回错误结构。另一个原因是max_tokens设得太小,模型还没输出完tool_use就被截断。把max_tokens调到 1000 以上,并确认 Model ID 跟 TaoToken 文档里列的一致。
OAuth 相关报错。如果你用的是 Claude Code 或某些需要 OAuth 的客户端,可能会遇到OAuth token expired或invalid_grant。这类问题跟 MCP 服务端无关,是客户端自身的登录态失效。重新走一遍客户端的登录流程即可。注意不要把 OAuth token 和 TaoToken 的 API Key 搞混,前者是客户端登录凭证,后者是模型调用凭证,两者作用域不同。
排查时有个通用技巧:把客户端和服务端的日志都打开。Python 客户端可以在connect_to_server前后加print,服务端可以在mcp.run之前加日志配置。看到底是哪一步断了,比盲目改配置快得多。另外,如果你同时装了多个 MCP 服务端,某个服务端启动失败可能导致整个客户端初始化卡住,建议先只配一个,跑通再加。
6. 把统一 Key 接入你的日常编码流
最小链路跑通后,下一步是把它变成日常工具。如果你主要在 IDE 里写代码,可以把 MCP 服务端配置进 Cline 或 Claude Code,让 AI 助手直接调用你的本地工具。这时候 TaoToken 的统一 Key 优势就体现出来了:你不需要为每个客户端单独申请凭证,改一处 Base URL 和 Key,所有走 API 通道的客户端都生效。
长期跑编码 Agent 的话,建议开 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,它的额度模型更适合高频调用场景。如果只是偶尔验证模型能力,用模型对话 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 手动问几句就够了。接入过程中遇到参数问题,优先查接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面按客户端分类列了配置示例。
最后提醒一个实操细节:MCP 服务端的工具描述(docstring)会直接影响模型是否愿意调用。get_alerts的 docstring 里写清楚参数格式和用途,模型命中率会高很多。如果你发现模型老是“忘记”调用工具,先检查工具描述是不是太模糊。这个坑我踩过,把描述补全后,同样的问法就能稳定触发工具调用了。