☰
从零搭建一个 MCP Server:Python 完整示例 + AI 调用机制详解(TaoToken 统一 Key 接入版)
2026/10/7 19:28:03 网站建设 项目流程

1. 为什么我要自己写一个 MCP Server

MCP Server 是什么?一句话:它是一个用 JSON-RPC 2.0 协议把「工具、资源、提示词」暴露给 AI 客户端的本地进程。能做什么?让 Claude Desktop、Cursor、Cline 这类支持 MCP 的客户端,在对话里直接调用你写的 Python 函数,比如查数据库、读本地文件、调内部接口。适合谁?适合想把私有能力接进 AI 工作流、又不想改客户端源码的开发者。

我试过把公司内部的日志查询脚本包成 MCP Server,结果 AI 在对话里就能直接拉日志、做聚合,比每次手动跑脚本省事得多。但第一次写的时候踩了不少坑:工具注册了客户端看不到、参数类型对不上导致 AI 传字符串、Windows 下中文 docstring 乱码。这些问题后面会逐个拆。

这篇的目标很明确:给你一份能直接跑的 Python MCP Server 骨架,讲清 AI 到底是怎么「发现」并「调用」你的工具的,最后用 TaoToken 的统一 Key 通道做一次端到端验证。TaoToken 在这里的角色是提供兼容 Anthropic / OpenAI 的 API 入口,让你不用分别申请多家 Key 就能验证 MCP 工具被模型调用的完整链路。官网入口在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。

先理清一个常见误解:MCP Server 本身不「智能」,它只是被动响应请求。真正决定调不调、怎么调的是 LLM,而 LLM 看到的全部信息,来自你函数上的 docstring 和类型注解。所以写 MCP Server 的本质,是写一份给 AI 看的 API 文档。这个认知会贯穿全文。

环境上你只需要 Python 3.10+ 和 pip。官方 SDK 装一条命令就够:

pip install "mcp[cli]"

目录结构保持极简,一个文件起步:

mcp_demo/ ├── server.py # MCP Server 实现 └── README.md

下面进入正题,先写骨架,再拆机制,最后验证。

2. TaoToken 前置准备:统一 Key 与 API 通道

在写 Server 之前,先把「谁来调用」这件事定下来。MCP Server 只负责暴露能力,真正发起对话、决定调用工具的是 LLM 客户端。本地调试时,你可以用 TaoToken 的统一 Key 作为模型通道,这样验证阶段不用在多个平台之间切换。

TaoToken 提供的是兼容主流协议的统一 API 入口,Base URL 固定为https://taotoken.net/api。你需要先去控制台创建一个 API Key,然后把它写进环境变量,避免硬编码进代码。控制台地址带归因参数:

https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

创建 Key 的入口在 API Keys 页面:

https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

拿到 Key 之后,在终端里设置环境变量。Linux / macOS:

export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

Windows PowerShell:

$env:TAOTOKEN_API_KEY="sk-你的key" $env:TAOTOKEN_BASE_URL="https://taotoken.net/api"

这里有个关键点要讲清楚:MCP Server 和模型 API 是两条独立的链路。Server 通过 stdio 和客户端通信,客户端再通过 HTTP 把工具列表塞进模型上下文。TaoToken 负责的是后半段——模型调用。所以你在验证阶段需要同时准备两样东西:一个能跑 MCP 的客户端(比如 Cline、Claude Code),以及一个可用的模型通道(TaoToken 统一 Key)。

如果你用的是 Claude Code 这类工具,它的配置里需要同时填 Base URL、API Key 和 Model ID 三件套。缺任何一个都会在启动时报认证或模型不存在的错误。Model ID 按你实际要用的模型填,比如claude-sonnet-4-5或gpt-4o,具体以控制台可用列表为准。

对于长期做编码和 Agent 编排的场景,可以考虑 Coding Plan,它更适合高频调用:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

准备阶段做完,你应该手上有:一个 API Key、一个 Base URL、一个确定的 Model ID。这三样在后面的验证环节会直接用到。别急着往下写 Server,先把这三样确认能通,否则后面报错你分不清是 Server 的问题还是 Key 的问题。

3. 可复制配置:Python MCP Server 骨架与客户端接入

这一节给你两份可直接复制的配置:一份是 MCP Server 本体,一份是客户端接入配置。先写 Server。

# -*- coding: utf-8 -*- """ 最小可运行 MCP Server 示例 运行方式: python server.py 默认通过 stdio 与 Client 通信 """ import sys import json from mcp.server.fastmcp import FastMCP # Windows 下强制 UTF-8,避免中文 docstring 乱码 sys.stdout.reconfigure(encoding="utf-8") mcp = FastMCP("demo-server") @mcp.tool() def add(a: float, b: float) -> str: """两个数字相加,返回它们的和。 Args: a: 第一个数字 b: 第二个数字 """ return f"{a} + {b} = {a + b}" @mcp.tool() def get_weather(city: str) -> str: """查询某个城市的当前天气(模拟数据)。 Args: city: 城市名称,如 "北京" """ mock = { "北京": "晴,18℃", "上海": "多云,22℃", "深圳": "雷阵雨,28℃", } return mock.get(city, f"未收录 {city} 的天气,默认晴 20℃") @mcp.resource("config://app") def get_config() -> str: """暴露一份配置文件作为资源""" return json.dumps({"version": "1.0.0", "env": "prod"}, ensure_ascii=False) @mcp.prompt() def code_review(code: str) -> str: """代码评审提示词""" return f"请评审以下代码,关注安全性和可读性:\n\n```\n{code}\n```" if __name__ == "__main__": mcp.run(transport="stdio")

这份骨架暴露了两类能力:tools(可被 AI 主动调用的函数)和resources(只读数据源),外加一个prompts模板。装饰器@mcp.tool()是注册入口,函数名就是工具名,docstring 就是工具描述,类型注解会被转成 JSON Schema。

接下来是客户端接入配置。以 Claude Desktop 为例,编辑配置文件:

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json

写入以下 JSON:

{ "mcpServers": { "demo-server": { "command": "python", "args": ["D:/path/to/server.py"], "env": { "TAOTOKEN_API_KEY": "sk-你的key", "TAOTOKEN_BASE_URL": "https://taotoken.net/api" } } } }

注意args里的路径要换成你本机的绝对路径,Windows 用正斜杠或双反斜杠都行。env字段把 TaoToken 的 Key 和 Base URL 透传给 Server 进程,这样 Server 内部如果要调模型 API 就能直接读环境变量。

如果你用的是 Cline 或 Claude Code,配置思路一致,只是字段名不同。Claude Code 的配置里需要显式写全三件套:

{ "apiKey": "sk-你的key", "baseURL": "https://taotoken.net/api", "model": "claude-sonnet-4-5" }

Cline 的 MCP 配置则是在设置面板里填 Server 启动命令,模型通道单独在 API 配置区填 Base URL 和 Key。无论哪种客户端,核心都是两件事:告诉客户端怎么启动你的 Server,告诉客户端用哪个模型通道。

配置写完先别急着重启客户端,用官方 Inspector 做一次本地自检,能省掉大量「重启了但看不到工具」的排查时间:

npx @modelcontextprotocol/inspector python server.py

Inspector 会打开一个网页,你能在里面看到initialize返回的 capabilities、tools/list返回的工具清单,还能手动调一次get_weather看返回。这一步通了,再往客户端里接。

4. 验证请求:AI 调用机制与端到端成功结果

这一节是全文核心:AI 到底怎么知道你的 MCP 提供了哪些方法。拆成四个阶段看。

阶段一,协议握手。客户端启动 Server 子进程后,立刻发一条initialize请求:

{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2024-11-05", "capabilities": {"roots": {"listChanged": true}, "sampling": {}}, "clientInfo": {"name": "claude-desktop", "version": "1.0.0"} } }

Server 回:

{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": {"tools": {}, "resources": {}, "prompts": {}}, "serverInfo": {"name": "demo-server", "version": "1.0.0"} } }

关键在capabilities:Server 在这里告诉客户端「我有 tools、resources、prompts 这三类能力」,但还没列具体方法。就像饭店门口挂「本店有炒菜、面食、汤」的牌子,还没给你菜单。

阶段二,能力发现。握手完成后,客户端立刻请求tools/list:

{"jsonrpc": "2.0", "id": 2, "method": "tools/list"}

Server 返回:

{ "jsonrpc": "2.0", "id": 2, "result": { "tools": [ { "name": "add", "description": "两个数字相加,返回它们的和。", "inputSchema": { "type": "object", "properties": { "a": {"type": "number", "description": "第一个数字"}, "b": {"type": "number", "description": "第二个数字"} }, "required": ["a", "b"] } }, { "name": "get_weather", "description": "查询某个城市的当前天气(模拟数据)。", "inputSchema": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名称,如 \"北京\""} }, "required": ["city"] } } ] } }

三个字段的来源要记牢:name来自 Python 函数名,description来自 docstring,inputSchema来自类型注解加参数 docstring。也就是说,AI 看到的工具文档,完全由你写函数时的 docstring 和类型注解决定。docstring 不写,AI 就不知道这工具干嘛,很可能不调或乱调。

阶段三,注入上下文。客户端拿到tools/list结果后,把它转成 LLM 能理解的格式,塞进请求的tools字段。以 OpenAI 风格为例:

{ "model": "gpt-4o", "messages": [{"role": "user", "content": "北京天气怎么样?"}], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "查询某个城市的当前天气(模拟数据)。", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"] } } } ] }

字段映射关系是固定的:MCP 的name→ LLM 的name,description→description,inputSchema→parameters(Anthropic 风格里叫input_schema)。不同模型的 tools 字段格式略有差异,适配工作由客户端负责,Server 永远只输出 MCP 标准 schema。

阶段四,决策与回传。用户问「北京天气怎么样」,LLM 看到自己有get_weather工具,输出工具调用请求:

{ "role": "assistant", "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" } } ] }

客户端不直接执行函数,而是通过 MCP 协议把请求转给 Server:

{ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": {"name": "get_weather", "arguments": {"city": "北京"}} }

Server 按名字找到函数执行,返回:

{ "jsonrpc": "2.0", "id": 4, "result": { "content": [{"type": "text", "text": "晴,18℃"}] } }

客户端把结果塞回 LLM 上下文:

{"role": "tool", "tool_call_id": "call_abc123", "content": "晴,18℃"}

LLM 看到结果,生成最终回答:「北京现在是晴天,气温 18℃。」

整条链路走通后,你在客户端对话框里应该能看到工具调用图标亮起,点开能看到add和get_weather两个工具。如果用的是 TaoToken 通道,模型请求会走https://taotoken.net/api,你可以在控制台的调用日志里看到对应的请求记录,确认工具确实被模型调用了。

想单独验证模型通道是否正常,可以用模型对话页面发一条测试消息:

https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

5. 本篇常见错排查:401、local proxy failed 与 reading choices

这一节按真实报错来。你大概率会碰到下面几个。

报错一:401 Unauthorized。这个几乎都是 Key 的问题。检查三处:环境变量名是否拼错、Key 是否带了多余空格、Base URL 是否写成了https://taotoken.net/api/(末尾多斜杠有时会导致路径拼接异常)。如果你在客户端配置里同时填了 Base URL、Key、Model ID,三件套缺一不可,缺 Model ID 会报模型不存在,缺 Key 直接 401。

报错二:local proxy failed或连接被拒绝。这类错误通常出现在客户端启动 Server 子进程时。原因可能是command写的python不在 PATH 里,换成绝对路径试试;也可能是args里的脚本路径有中文或空格,用引号包起来。还有一种情况是 Server 启动后立刻退出,用 Inspector 单独跑一次就能看到真实堆栈。

报错三:reading choices或解析响应失败。这个多半是模型返回格式和客户端预期不一致。检查你填的 Model ID 是否和 Base URL 对应的服务匹配,比如把 OpenAI 格式的模型名填到了 Anthropic 通道。另外确认客户端版本支持你用的协议版本,老版本客户端可能不认2024-11-05。

报错四:工具没出现在客户端。先确认@mcp.tool()装饰器写对了,函数有return,docstring 不为空。然后用 Inspector 看tools/list返回,如果 Inspector 能看到而客户端看不到,问题在客户端配置;如果 Inspector 也看不到,问题在 Server 代码。

报错五:Windows 中文乱码。Server 进程默认编码可能是 GBK,导致 docstring 里的中文变乱码,AI 看不懂。解决方法是加sys.stdout.reconfigure(encoding="utf-8"),本文示例已经加了。

报错六:stdio 通信被污染。Server 里任何print()都会写到 stdout,被客户端当成 JSON-RPC 消息解析,直接报错。调试日志一律写sys.stderr或用logging模块。

报错七:异步函数阻塞。FastMCP 默认在事件循环里跑,长耗时同步函数会卡住整个 Server。解决方法是改成async def,或者把耗时任务丢线程池。

报错八:OAuth 相关报错。如果你接的是需要 OAuth 的远程 MCP Server,本地 stdio 模式不涉及;但如果你在客户端里配了远程 Server 又没配认证,会报 OAuth 失败。本地调试阶段建议先用 stdio,跑通再考虑远程。

排查顺序建议固定下来:先 Inspector 自检 Server,再检查客户端配置三件套,最后看模型通道日志。这样能把问题范围快速缩小到某一层。

6. 继续往下走:接入文档与 Coding Plan

Server 跑通只是起点。接下来你大概率会想做三件事:把工具粒度调细、加错误处理、接真实数据源。

工具粒度上,太粗的run_anythingAI 不会用,太细的add_one会让上下文爆炸。经验值是每个工具做一件明确的事,参数控制在 3 到 5 个以内。错误处理上,用 MCP 标准格式返回{"isError": true, "content": [...]}比直接raise异常更友好,AI 能读懂错误并决定是否重试。工具还要幂等,因为 AI 可能重复调用,多次调用结果要一致。

只读数据优先用resource,有副作用的才用tool。高频固定用法用prompt固化,比如代码评审模板,不必让用户每次手敲。

接入细节和协议规范可以查官方文档:

https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

如果你打算把 MCP 用在长期编码或 Agent 编排上,调用频率会明显上升,Coding Plan 比按次计费更划算:

https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

最后留一个实用技巧:写完 Server 先用 Inspector 手动调一遍每个工具,确认返回格式正确,再往客户端里接。这一步花五分钟,能省掉后面半小时的「为什么 AI 不调我的工具」的困惑。工具质量直接决定 AI 调用质量,把 docstring 当 API 文档写,把类型注解当契约写,剩下的交给协议。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询