news 2026/10/2 9:14:06

用一个DEMO拆解MCP生命周期:TaoToken统一Key下的模型上下文协议实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用一个DEMO拆解MCP生命周期:TaoToken统一Key下的模型上下文协议实战

1. 从一次“查天气”说起:MCP 生命周期到底在解决什么问题

MCP(Model Context Protocol,模型上下文协议)是什么?一句话:它是一套让大模型用统一方式“伸手”去拿外部数据和调用外部工具的协议。能做什么?把“模型不知道的实时信息”和“模型不该自己瞎编的操作”交给外部服务处理。适合谁?正在做 AI Agent、智能客服、IDE 插件、企业内部助手的开发者,尤其是被 Function Calling 各家格式折磨过的人。

我拿一个最小场景切入:用户在聊天框里问“上海今天天气怎么样,适合出门吗”。模型训练数据里没有今天的天气,它必须调用一个get_weather工具,拿到“上海:多云,27°C”,再根据结果推荐活动。整个过程里,MCP 要经历四个阶段:初始化握手、能力协商(列出有哪些工具和资源)、工具调用、会话关闭。这四个阶段合起来就是 MCP 生命周期。

很多人第一次接触 MCP 会把它和 Function Calling 混为一谈。区别在于:Function Calling 是“应用层预先决定给模型哪些函数”,而 MCP 是“模型基于上下文自主推理该调哪个工具”,工具的实现细节被封装在独立的 MCP Server 里,对模型透明。这意味着你新增一个工具,只要符合 MCP 协议标准,模型侧代码一行都不用改。

这篇 DEMO 我会用 TaoToken 统一 Key 作为模型通道,把 MCP Server、MCP Client、MCP Host 三段代码串起来,让你能亲手跑通一次完整的工具调用,并在日志里看到生命周期每个阶段的真实输出。TaoToken 在这里的作用是:一个 Key 就能切换不同模型,省去为每个模型单独配 Key 的麻烦,特别适合做多模型接入验证。

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

在写 MCP 代码之前,先把模型通道打通。TaoToken 的定位是统一 API 通道,你注册后拿到一个 Key,就能通过兼容 OpenAI 格式的接口调用多种模型。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。

第一步,去控制台创建 API Key。打开 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,在 API Keys 页面点新建,复制生成的 Key。这个 Key 后面会同时用在 MCP Host 的 LLM 调用里。

第二步,确认你要用的模型 ID。在模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 可以看到当前支持的模型列表,选一个你熟悉的,比如gpt-4o-mini或claude-3-5-sonnet。记下这个 Model ID,配置里要用。

第三步,把 Key 和 Base URL 写进环境变量,避免硬编码进代码。在项目根目录建一个.env文件:

TAOTOKEN_API_KEY=sk-你的Key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL=gpt-4o-mini

然后在 Python 里用python-dotenv读取。如果你不想装额外依赖,也可以直接在 shell 里 export:

export TAOTOKEN_API_KEY="sk-你的Key" export TAOTOKEN_BASE_URL="https://taotoken.net/api" export TAOTOKEN_MODEL="gpt-4o-mini"

这里有个容易踩的坑:Base URL 末尾不要带/v1,TaoToken 的兼容层会自动补全路径。如果你手动拼成https://taotoken.net/api/v1/chat/completions,反而可能 404。正确的请求地址是https://taotoken.net/api/chat/completions。

另外,MCP Server 本身不需要 TaoToken Key,它只负责提供工具。Key 只用在 MCP Host 调用 LLM 的那一步。这个分工要理清楚,否则你会以为 Server 也要配 Key。

如果你打算长期跑编码类 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 ,遇到参数问题可以先查这里。

3. 可复制配置:MCP Server、Client、Host 三段代码

这一节是全文核心,我把三段代码完整贴出来,你复制就能跑。先装依赖:

pip install mcp openai python-dotenv

注意这里用openai库而不是requests,因为 TaoToken 兼容 OpenAI 格式,用官方 SDK 更省事,也方便你以后换模型。

3.1 MCP Server:注册两个工具

新建mcp_server_demo.py:

from mcp.server.fastmcp import FastMCP import asyncio mcp = FastMCP(name="weather-demo", host="0.0.0.0", port=1234) @mcp.tool(name="get_weather", description="获取指定城市的天气信息") async def get_weather(city: str) -> str: weather_data = { "北京": "北京:晴,25°C", "上海": "上海:多云,27°C", "广州": "广州:小雨,30°C" } return weather_data.get(city, f"{city}:天气信息未知") @mcp.tool(name="suggest_activity", description="根据天气描述推荐适合的活动") async def suggest_activity(condition: str) -> str: if "晴" in condition: return "天气晴朗,推荐你去户外散步或运动。" elif "多云" in condition: return "多云天气适合逛公园或咖啡馆。" elif "雨" in condition: return "下雨了,建议你在家阅读或看电影。" else: return "建议进行室内活动。" async def main(): print("启动 MCP Server: http://127.0.0.1:1234") await mcp.run_sse_async() if __name__ == "__main__": asyncio.run(main())

这段代码用FastMCP装饰器注册了两个工具。run_sse_async()会启动一个 SSE 服务,监听 1234 端口。启动后你会看到启动 MCP Server: http://127.0.0.1:1234。

3.2 MCP Client:连接 Server 并列出能力

新建mcp_client_demo.py:

import asyncio from mcp.client.session import ClientSession from mcp.client.sse import sse_client class WeatherMCPClient: def __init__(self, server_url="http://127.0.0.1:1234/sse"): self.server_url = server_url self._sse_context = None self._session = None async def __aenter__(self): self._sse_context = sse_client(self.server_url) self.read, self.write = await self._sse_context.__aenter__() self._session = ClientSession(self.read, self.write) await self._session.__aenter__() await self._session.initialize() return self async def __aexit__(self, exc_type, exc_val, exc_tb): if self._session: await self._session.__aexit__(exc_type, exc_val, exc_tb) if self._sse_context: await self._sse_context.__aexit__(exc_type, exc_val, exc_tb) async def list_tools(self): return await self._session.list_tools() async def list_resources(self): return await self._session.list_resources() async def call_tool(self, name, arguments): return await self._session.call_tool(name, arguments) async def main(): async with WeatherMCPClient() as client: print("成功连接 MCP Server") tools = await client.list_tools() print("\n可用工具:") print(tools) resources = await client.list_resources() print("\n可用资源:") print(resources) print("\n调用 get_weather 工具(city=上海)...") result = await client.call_tool("get_weather", {"city": "上海"}) print("\n工具返回:") for item in result.content: print(" -", item.text) if __name__ == "__main__": asyncio.run(main())

__aenter__里做了三件事:建立 SSE 通道、创建 ClientSession、调用initialize()完成握手。这就是生命周期的初始化阶段。list_tools()是能力协商阶段,call_tool()是工具调用阶段,__aexit__是会话关闭阶段。

3.3 MCP Host:串起 LLM 和工具

新建mcp_host_demo.py:

import asyncio import json import re import os from openai import OpenAI from dotenv import load_dotenv from mcp_client_demo import WeatherMCPClient load_dotenv() client_llm = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL") ) MODEL_ID = os.getenv("TAOTOKEN_MODEL", "gpt-4o-mini") def extract_json_from_reply(reply: str): if isinstance(reply, dict): return reply if isinstance(reply, str): reply = re.sub(r"^```(?:json)?|```$", "", reply.strip(), flags=re.IGNORECASE).strip() for _ in range(3): try: parsed = json.loads(reply) if isinstance(parsed, dict): return parsed else: reply = parsed except Exception: break return reply async def main(): client = WeatherMCPClient() await client.__aenter__() tools = await client.list_tools() resources = await client.list_resources() tool_names = [t.name for t in tools.tools] tool_descriptions = "\n".join(f"- {t.name}: {t.description}" for t in tools.tools) resource_descriptions = "\n".join(f"- {r.uri}" for r in resources.resources) while True: user_input = input("\n请输入你的问题(输入 exit 退出):\n> ") if user_input.lower() in ("exit", "退出"): break system_prompt = ( "你是一个智能助手,拥有以下工具和资源可以调用:\n\n" f"工具列表:\n{tool_descriptions or '(无)'}\n\n" f"资源列表:\n{resource_descriptions or '(无)'}\n\n" "请优先调用可用的 Tool 或 Resource,而不是 llm 内部生成。" "仅根据上下文调用工具,不传入不需要的参数进行调用\n" "如果需要,请以 JSON 返回 tool_calls,格式如下:\n" '{"tool_calls": [{"name": "get_weather", "arguments": {"city": "北京"}}]}\n' "如无需调用工具,返回:{\"tool_calls\": null}" ) messages = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input} ] final_reply = "" while True: response = client_llm.chat.completions.create( model=MODEL_ID, messages=messages ) reply = response.choices[0].message.content print(f"\nLLM 回复:\n{reply}") parsed = extract_json_from_reply(reply) if isinstance(parsed, str): final_reply = parsed break tool_calls = parsed.get("tool_calls") if not tool_calls: final_reply = parsed.get("content", "") break for tool_call in tool_calls: tool_name = tool_call["name"] arguments = tool_call["arguments"] if tool_name not in tool_names: raise ValueError(f"工具 {tool_name} 未注册") print(f"调用工具 {tool_name} 参数: {arguments}") result = await client.call_tool(tool_name, arguments) tool_output = result.content[0].text print(f"工具 {tool_name} 返回:{tool_output}") messages.append({ "role": "tool", "name": tool_name, "content": tool_output }) print(f"\n最终回复:{final_reply}") await client.__aexit__(None, None, None) if __name__ == "__main__": asyncio.run(main())

这里的关键点:client_llm用的是 TaoToken 的 Base URL 和 Key,Model ID 从环境变量读。messages里追加role: tool的消息,就是把工具结果回传给模型。整个循环直到模型返回纯文本才结束。

如果你用 Claude Code 或 Cline 这类工具,配置方式类似,需要填三件套:Base URL 填https://taotoken.net/api,Key 填你的 TaoToken Key,Model ID 填你选的模型。Claude Code 的接入文档在 https://taotoken.net/claude-code?utm_source=taotoken_aicg_blog_end&utm_content=claude-code&utm_campaign=rewrite 。

4. 验证请求:生命周期各阶段日志与成功结果

先启动 Server:

python mcp_server_demo.py

看到启动 MCP Server: http://127.0.0.1:1234就说明初始化成功。这个阶段对应生命周期的“初始化握手”,Server 在 1234 端口等待 SSE 连接。

另开一个终端,先单独测 Client:

python mcp_client_demo.py

你应该看到:

成功连接 MCP Server 可用工具: meta=None nextCursor=None tools=[Tool(name='get_weather', description='获取指定城市的天气信息', inputSchema={...}), Tool(name='suggest_activity', ...)] 可用资源: meta=None nextCursor=None resources=[] 调用 get_weather 工具(city=上海)... 工具返回: - 上海:多云,27°C

这段日志覆盖了三个生命周期阶段:成功连接是初始化,可用工具是能力协商,工具返回是工具调用。可用资源为空是因为我们没注册 resource,不影响 DEMO。

现在跑完整的 Host:

python mcp_host_demo.py

输入“上海今天天气怎么样,适合出门吗”,你会看到类似输出:

LLM 回复: {"tool_calls": [{"name": "get_weather", "arguments": {"city": "上海"}}]} 调用工具 get_weather 参数: {'city': '上海'} 工具 get_weather 返回:上海:多云,27°C LLM 回复: {"tool_calls": [{"name": "suggest_activity", "arguments": {"condition": "多云"}}]} 调用工具 suggest_activity 参数: {'condition': '多云'} 工具 suggest_activity 返回:多云天气适合逛公园或咖啡馆。 LLM 回复: 上海今天多云,27°C,适合逛公园或咖啡馆。 最终回复:上海今天多云,27°C,适合逛公园或咖啡馆。

注意这里发生了两次工具调用:第一次查天气,第二次根据天气推荐活动。这说明模型在拿到第一次结果后,自主决定再调一次工具。这就是 MCP 和传统 Function Calling 的区别——调用链是模型驱动的,不是应用层写死的。

输入exit退出,Client 的__aexit__会关闭 SSE 连接和 Session,生命周期进入会话关闭阶段。你可以在 Server 终端看到连接断开的日志。

如果你想验证多模型切换,只需改.env里的TAOTOKEN_MODEL,比如换成claude-3-5-sonnet,重启 Host 即可。Key 和 Base URL 都不用动,这就是统一 Key 的价值。想快速对比不同模型的工具调用表现,可以去模型对话页面 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 直接试。

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

跑 DEMO 时最容易撞上四类报错,我逐个拆。

401 Unauthorized。日志里出现Error code: 401,基本是 Key 问题。检查三点:.env里的TAOTOKEN_API_KEY有没有多余空格;Key 是不是复制时漏了前缀;Base URL 是不是写成了https://taotoken.net/api/v1。正确写法是https://taotoken.net/api。如果还报 401,去 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 重新生成一个 Key 试试。

local proxy failed。这个报错通常出现在你本地网络环境有额外代理设置时。MCP Client 连的是http://127.0.0.1:1234/sse,这是本地回环地址,不应该走任何外部通道。检查你的 shell 里有没有HTTP_PROXY或HTTPS_PROXY环境变量,有的话临时 unset 掉:

unset HTTP_PROXY unset HTTPS_PROXY

然后重启 Server 和 Client。另外确认 Server 确实在 1234 端口监听,用curl http://127.0.0.1:1234/sse能看到事件流就说明正常。

reading choices 报错。日志里出现KeyError: 'choices'或reading 'choices',说明 LLM 返回的 JSON 结构和你预期的不一样。常见原因是 Model ID 写错了,TaoToken 返回了一个错误对象而不是正常的 completion。打印完整响应看看:

print(response.model_dump_json(indent=2))

确认choices字段存在。如果返回的是{"error": {...}},那就是 Model ID 或 Key 的问题。去接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 核对当前支持的模型列表。

OAuth 相关报错。如果你在 Claude Code 或 Cline 里配置 MCP,可能会遇到OAuth token expired或authentication failed。这类工具通常有自己的认证流程,和 MCP Server 本身的 SSE 连接是两回事。排查顺序:先确认 TaoToken 的 Key 在工具设置里填对了,Base URL 是https://taotoken.net/api,Model ID 是有效值。三件套缺一不可。如果工具提示 OAuth,检查是不是把 TaoToken Key 填到了 OAuth 字段而不是 API Key 字段。

还有一个隐蔽的坑:MCP Server 启动后,如果你改了工具代码但没重启 Server,Client 列出的还是旧工具列表。能力协商阶段拿到的工具清单是 Server 启动时注册的,改代码必须重启。

6. 继续深入:把 DEMO 扩展成你自己的 Agent

跑通这个 DEMO 后,你可以做几件事让它更接近生产。

第一,把硬编码的天气数据换成真实 API 调用。在get_weather里发 HTTP 请求到天气服务,返回真实数据。MCP Server 的价值就在这里——工具实现怎么变,模型侧都不用改。

第二,增加 Resource。MCP 除了 Tool 还有 Resource 概念,适合暴露只读数据,比如“当前用户信息”“项目配置文件”。在 Server 里用@mcp.resource()注册,Client 用list_resources()和read_resource()访问。

第三,做多 Server 聚合。一个 Host 可以同时连多个 MCP Server,比如天气 Server、数据库 Server、文件系统 Server。Client 侧维护多个 session,Host 把所有工具汇总后传给模型。这样模型就能在一个对话里跨服务调用。

第四,加错误处理。现在工具调用失败会直接抛异常,生产环境应该捕获后把错误信息作为 tool 结果回传给模型,让模型决定是重试还是告知用户。

如果你要长期跑这类 Agent,Coding Plan 的额度模型更适合高频调用场景:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。接入过程中遇到协议细节问题,文档里对 SSE 和 JSON-RPC 的说明比较全:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。

最后提醒一个实操细节:MCP 的 SSE 连接是长连接,Server 和 Client 要同时运行。如果你在 Docker 里跑 Server,记得把 1234 端口映射出来,并且 Client 里的server_url要改成宿主机的地址,不能写127.0.0.1。这个坑我在本地和容器混合部署时踩过,日志里表现为连接超时,但 Server 明明在跑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 9:13:44

Flutter状态复杂度失控?架构层如何提前刹车与治理

接手Flutter项目最痛苦的事情,不是踩内存泄漏,也不是UI还原度,而是状态复杂度在一次一次迭代里悄悄失控。早期用 setState 写得很爽,到了项目中期,一个页面七八个 GlobalKey 、三四个 Provider ,改一…

作者头像 李华
网站建设 2026/10/2 9:13:30

MySQL数据类型选型指南:从底层存储到慢查询优化

做 MySQL 开发这几年,MySQL 数据类型是我见过引发线上事故最多的"基础问题"。很多慢查询、数据错乱、磁盘膨胀,追到根上往往就是建表时某个字段类型拍脑袋选的。这篇文章把 MySQL 数据类型从底层存储、选型逻辑到实操落地完整梳理一遍&#xf…

作者头像 李华
网站建设 2026/10/2 9:13:11

YOLOv8模型MATLAB部署实战:ONNX桥接与dlnetwork端到端推理

简介:本资源是一套可在MATLAB环境中直接部署YOLOv8目标检测模型的完整实践方案,面向计算机、人工智能及相关专业的本科生与研究生,特别适合作为毕业设计、课程设计或深度学习项目实战练习。资源包含训练、推理、模型导入、Simulink仿真支持及…

作者头像 李华
网站建设 2026/10/2 9:12:57

从RL规模化到自我改进:MiMo-V2.6技术报告深度解析

最近大模型圈子里最值得逐字读完的技术报告,我琢磨着应该是这篇:一个开源大模型站出来的姿态,不是继续喊参数规模、预训练数据量,而是把全部重心压在“强化学习规模化”和“自我改进”上。你见过很多模型说自己“能推理”&#xf…

作者头像 李华
网站建设 2026/10/2 9:12:48

基于YOLOv8的航拍图像分析系统:从部署训练到演示避坑全攻略

简介:一套基于YOLOv8的航拍图像分析系统源码包,面向计算机相关专业学生及毕业设计、课程设计场景,解决目标检测项目从数据到部署的全流程需求。压缩包共九十七个文件,包含七十个脚本文件、十二个编译文件、五个配置文件、四个权重…

作者头像 李华
网站建设 2026/10/2 9:12:22

Docker容器化部署实战指南:从Windows安装到MySQL与Redis主从

1. 为什么我最终选择了Docker来搞定所有部署先说说我的真实经历。上个月帮朋友把一套已经跑了三年的Python项目部署到新服务器,第一件事就是装MySQL 8.0,然后发现系统自带的MySQL是5.7,数据迁移、配置文件、字符集……每一项都在制造麻烦。我…

作者头像 李华