
1. 从一次“查天气”说起MCP Tool 到底解决什么问题MCP Tool 是 Model Context Protocol 里最实用的一类能力它让大模型从“只会聊天”变成“能动手做事”。你可以把它理解成给模型装了一双手模型自己不会发 HTTP 请求、不会读数据库、不会调内部接口但它可以通过 Tool 声明“我需要调用某个函数”由客户端去执行再把结果喂回模型。天气信息查询就是最典型的入门场景——接口简单、返回结构清晰、结果一眼能验证对错。适合谁看这篇如果你已经跑通过 MCP 的 Hello World知道 Server 和 Client 是两个进程但一到“怎么定义参数 Schema”“模型为什么不调用我的工具”“返回的 JSON 怎么解析”就卡住那这篇就是给你写的。我会用天气查询把整条链路走完Tool 定义、参数 Schema、客户端调用、返回解析最后用 TaoToken 的统一 Key 和 API 通道把模型侧接上避免你在多个平台之间反复换 Key。很多人第一次写 MCP Tool 会踩一个坑以为写完app.tool()模型就会自动调用。实际上模型只负责“选择”真正执行的是客户端。模型返回的是tool_calls里面带着函数名和参数客户端拿到后去session.call_tool()把结果作为role: tool的消息再发回去模型才生成最终回答。这条“两次调用”的链路是理解 MCP Tool 的关键后面所有代码都围绕它展开。我试过把天气接口直接丢给模型让它自己编结果它一本正经地报了个不存在的温度。Tool 的价值就在于把“事实来源”交给真实接口模型只做自然语言到参数的翻译以及结果到人话的转述。下面从环境准备开始一步步把这条链路跑通。2. TaoToken 前置准备统一 Key 与 API 通道接入在写 Tool 之前先把模型侧的通道准备好。MCP 客户端需要调用一个大模型来做“工具选择”和“结果转述”这一步如果 Key 管理混乱后面排查错误会非常痛苦。TaoToken 的作用就是提供一个统一的 API 通道你只需要一个 Key、一个 Base URL就能在客户端里切换不同模型不用为每个模型单独维护一套配置。先到官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并进入控制台在 API Keys 页面创建一个 Key。创建后立刻复制保存页面刷新后就看不到完整 Key 了。这个 Key 就是后面客户端代码里api_key字段要填的值。Base URL 统一用 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 OpenAI 兼容客户端的base_url使用。TaoToken 的接口是 OpenAI 兼容格式所以openai这个 Python 包可以直接用不需要额外装 SDK。模型 ID 方面天气这种工具调用场景对推理要求不高选一个支持 function calling 的模型即可比如gpt-4o-mini这类具体可用模型以控制台模型列表为准。这里要强调一个容易忽略的点MCP 客户端里其实有两个“Key”概念。一个是模型 API 的 KeyTaoToken 的 Key用于OpenAI(api_key...)另一个是天气接口自己的 Keyweatherapi 的 key用于请求天气数据。这两个完全独立不要混。很多 401 报错就是因为把天气接口的 Key 填到了模型客户端里或者反过来。如果你打算长期做编码类 Agent可以顺手了解一下 Coding Plan它更适合高频调用场景只是验证模型连通性的话用模型对话页面手动发一条消息就能确认 Key 是否有效。接入文档在 https://taotoken.net/doc 有完整的参数说明遇到字段不确定时优先查文档而不是猜。3. 可复制配置Tool 定义、参数 Schema 与客户端 settings这一节给出可以直接复制的代码。先建一个文件夹Tool_mcp里面放两个文件weather_search_server.py和weather_search_client.py。服务端负责定义 Tool 并真正请求天气接口客户端负责连模型、拿工具列表、执行调用。先看服务端的 Tool 定义。app.tool()装饰器下面的函数docstring 非常关键模型就是靠这段描述判断“这个工具是干什么的、参数怎么填”。参数类型用 Python 类型注解声明FastMCP 会自动生成 JSON Schema。天气查询需要城市名用city: str并在 docstring 里说明要传拼音。from contextlib import AsyncExitStack import httpx from mcp.server.fastmcp import FastMCP weather_api_key 你的weatherapi_key weather_base_url http://api.weatherapi.com/v1/current.json app FastMCP() app.tool() async def get_weather(city: str) - dict: 获取城市当前的天气信息 :param city: 具体城市名称需要使用拼音例如 shenzhen :return: 包含当前温度、天气状况的 JSON 数据 params {key: weather_api_key, q: city} exit_stack AsyncExitStack() client await exit_stack.enter_async_context(httpx.AsyncClient()) try: response await client.get(weather_base_url, paramsparams) return response.json() except Exception as err: return {error: f查询接口异常:{err}} if __name__ __main__: app.run(transportstdio)注意返回类型写成了dict并且注释里说明返回 JSON。MCP 要求 Tool 返回的内容能被序列化成文本客户端拿到result.content[0].text后再解析。如果你返回的是 Python 对象而不是可序列化结构客户端解析时会报reading choices之类的错。再看客户端的模型配置片段。这里用 TaoToken 的统一通道把base_url指向 https://taotoken.net/api api_key填 TaoToken 的 Key。这段配置建议单独抽出来方便后面换模型from openai import OpenAI client OpenAI( api_key你的TaoToken_Key, base_urlhttps://taotoken.net/api ) response client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 帮我查深圳天气}], toolstools )如果你用配置文件管理可以写成 TOML放在项目根目录config.toml[llm] base_url https://taotoken.net/api api_key 你的TaoToken_Key model gpt-4o-mini [weather] base_url http://api.weatherapi.com/v1/current.json api_key 你的weatherapi_key这样服务端和客户端各读各的配置Key 不会写死在代码里。客户端完整逻辑的核心是把session.list_tools()返回的工具转成 OpenAI 的 function calling 格式字段名是input_schema注意不是parameters这是 MCP 和 OpenAI 格式的一个差异点写错模型就识别不到工具。tools [] for tool in response.tools: tools.append({ type: function, function: { name: tool.name, description: tool.description, input_schema: tool.inputSchema, } })拿到tool_calls后遍历执行session.call_tool(namefunction_name, argumentsfunction_arguments)把结果以role: tool追加进 messages再发第二次请求。这两次请求都走 TaoToken 通道Key 和 Base URL 保持一致即可。4. 验证请求一次真实调用与成功结果解析配置写完跑一次完整调用。先启动客户端脚本它会以 stdio 方式拉起服务端子进程。执行python weather_search_client.py客户端内部流程是初始化 session →list_tools→ 把工具塞给模型 → 模型返回tool_calls→ 执行天气接口 → 把结果回传模型 → 打印最终回答。第一次请求时模型返回的finish_reason应该是tool_callschoice.message.tool_calls里能看到函数名get_weather和参数{city: shenzhen}。这一步如果finish_reason是stop而不是tool_calls说明模型没选择工具通常是 docstring 描述太模糊或者工具没正确传进tools数组。执行call_tool后服务端会真实请求天气接口控制台会打印原始 JSON类似包含location、current字段的结构。客户端把这段 JSON 文本作为role: tool的消息追加然后发起第二次模型请求。第二次请求不带tools参数模型基于工具返回的数据生成自然语言回答比如“深圳当前气温 28 摄氏度多云”。成功的关键标志有三个一是控制台能看到原始天气 JSON 被打印二是第二次请求返回的finish_reason是stop三是最终输出是通顺的中文描述而不是原始 JSON。如果最终输出直接把 JSON 吐出来了说明第二次请求没生效或者 messages 里role: tool的消息没带上tool_call_id。这里有个细节值得注意tool_call_id必须和模型返回的tool_call.id完全一致否则模型无法把工具结果和它发起的调用对应起来会报参数校验错误。我见过有人手动写了个假 id结果模型一直说“我没有收到工具结果”。验证通过后你可以把city换成beijing再跑一次确认参数是从用户 query 里动态提取的而不是写死的。这一步能验证模型确实在做“自然语言到参数”的翻译而不是碰巧命中。5. 本篇常见错误排查401、local proxy failed 与 reading choices跑不通的时候报错信息往往指向几个固定位置。下面按真实遇到的错误逐条对照。401 Unauthorized出现在模型请求阶段说明 TaoToken 的 Key 无效或没带上。检查OpenAI(api_key...)里的值是不是从控制台复制的完整 Keybase_url是不是 https://taotoken.net/api 。如果 Key 正确还报 401确认请求头里Authorization: Bearer key格式没问题OpenAI SDK 会自动加一般不用手动处理。天气接口的 401 则是 weatherapi 的 key 问题两者要分清。local proxy failed / connection error这类错误通常出现在客户端启动服务端子进程时。检查StdioServerParameters里的command是不是当前环境可用的pythonargs里的服务端路径是不是相对路径写错。如果服务端脚本 import 了没装的包比如httpx、mcp子进程会直接退出客户端表现为连接失败。建议先在终端单独跑一次python weather_search_server.py确认服务端能正常启动再跑客户端。reading choices 报错这个错误一般发生在解析模型响应时。常见原因是tools数组里的字段名写成了parameters而不是input_schema导致模型返回结构异常或者第二次请求时 messages 里role: tool的消息缺少tool_call_id。还有一种情况是模型本身不支持 function calling换一个支持工具调用的模型 ID 即可。OAuth 相关报错如果你接的是需要 OAuth 的 MCP 服务端客户端初始化时会要求走授权流程。天气这个例子用的是 stdio 本地进程不涉及 OAuth。如果报 OAuth 错误说明你连的服务端配置了远程鉴权需要按服务端文档补上 token 或走授权回调不要硬套本地 stdio 的写法。工具没被调用finish_reason是stop模型直接回答了。优先检查 docstring 是否写清楚了功能和参数含义其次确认tools数组非空。可以在客户端打印response.tools的长度如果是 0说明list_tools没拿到工具服务端装饰器可能没生效。排查顺序建议从“服务端能否独立启动”开始再到“工具列表是否非空”最后看“模型是否返回 tool_calls”。按这个顺序走大部分问题能在三分钟内定位。6. 把天气 Tool 接进你的工作流下一步怎么走天气查询跑通后你已经掌握了 MCP Tool 的完整骨架定义、Schema、调用、解析、回传。接下来可以把这个模式复制到更实用的场景比如查数据库、调内部 API、读文件。核心不变变的只是app.tool()里那段业务逻辑和 docstring 的描述。如果你打算长期做编码类 Agent建议把模型通道固定成 TaoToken 的统一 Key这样换模型时只改一个model字段不用动客户端代码。API Keys 页面在 https://taotoken.net/api-keys 接入文档在 https://taotoken.net/doc 遇到字段问题先查文档。需要验证某个模型是否支持工具调用可以直接在模型对话里发一条带 tools 的请求试。最后留一个实用技巧天气接口返回的 JSON 字段很多直接丢给模型会浪费 Token。可以在服务端先提取current.temp_c、current.condition.text这几个关键字段再返回精简结构。这样第二次请求的输入更短回答也更聚焦。这个优化不影响链路正确性但能明显降低调用成本适合在跑通之后再做。