MultiServerMCPClient 工具列表为空?从 async with 生命周期到 TaoToken 通道排查

发布时间:2026/10/15 14:41:48

MultiServerMCPClient 工具列表为空?从 async with 生命周期到 TaoToken 通道排查 1. 为什么 get_tools 总是返回空列表从 MultiServerMCPClient 生命周期说起如果你正在用 LangChain 的 MCP 适配器接多个工具服务大概率踩过这个坑配置写得好好的MultiServerMCPClient也实例化了结果get_tools()打印出来永远是已加载 0 个MCP工具。没有报错没有异常就是空。这个现象在 MultiServerMCPClient 工具列表为空 的搜索里出现频率极高而根因往往不在配置本身而在客户端的生命周期管理。先把问题场景还原清楚。你手上有一份mcp_config里面登记了一个或多个 MCP Server比如用uvx启动的本地工具服务。你按直觉写了这样的代码mcp_config { mcp-akshare: { command: uvx, args: [src/tool/mcp-akshare], } } client MultiServerMCPClient(mcp_config) all_tools client.get_tools() print(f已加载 {len(all_tools)} 个MCP工具)运行结果稳定输出 0。这里的关键认知是MultiServerMCPClient并不是在__init__里就去连接服务器的。它把「连接」这件事推迟到了异步上下文管理器的进入阶段。也就是说你只创建了对象但从未触发连接动作get_tools()自然拿不到任何东西——它读的是一个还没被填充的工具注册表。这个设计不是 bug而是有意为之。MCP Server 的连接是异步的、有状态的、需要清理的资源。如果构造函数里直接发起连接你就失去了对连接时机的控制也没法保证异常时能正确断开。所以官方把它做成了async with协议进入时连接退出时断开。我试过把这个逻辑类比成「开一家店」MultiServerMCPClient(mcp_config)只是租了个店面、挂了招牌但门还没开、货还没上。async with才是真正开门营业、把货架摆满的动作。你在没开门的时候问「店里有几件商品」答案当然是零。所以排查路径的第一步非常明确确认你的get_tools()调用是否发生在async with块内部。如果不在先别怀疑配置、别怀疑 Key、别怀疑网络先把生命周期改对。这一步能解决绝大多数「工具列表为空」的问题。但现实里还有第二层坑即使你加了async with工具列表仍可能是空的。这时候问题就从「生命周期」转移到了「连接是否真的成功」以及「通道配置是否正确」。MCP Server 的连接依赖底层进程能正常启动、依赖模型侧或网关侧的鉴权通道能通。如果 Server 进程启动失败或者统一 Key/API 通道没配对__aenter__里的连接逻辑会静默失败或部分失败工具注册表依然是空的。这就是为什么排查要分两段走先确认时序async with再核对通道Base URL / Key / Model ID。下面我会把这两段都拆成可复制的步骤包括客户端初始化片段、get_tools的正确调用顺序、日志验证动作以及统一通道的配置写法。适合谁看正在用 LangChain MCP adapters 接多 Server 的开发者、被空列表卡住但没报错的人、以及想把 MCP 工具接进自己 Agent 流程但不确定通道怎么配的人。接下来从环境准备开始一步步把空列表定位到根因。2. 前置准备TaoToken 统一通道与 MCP 依赖环境在动async with之前先把运行环境和一个稳定的模型/API 通道准备好。MCP 工具本身是本地进程但很多工具在调用时会回连模型侧做推理或鉴权如果通道不稳连接阶段就可能超时或静默失败最终表现为工具列表为空。所以这一步不是可选项。先说依赖。LangChain 的 MCP 适配器包名是langchain-mcp-adapters它依赖mcp这个官方 Python SDK。安装命令pip install langchain-mcp-adapters mcp如果你用uv管理环境等价写法是uv add langchain-mcp-adapters mcp装完之后验证一下版本避免旧版本 API 不一致导致get_tools行为差异python -c import langchain_mcp_adapters, mcp; print(langchain_mcp_adapters.__version__ if hasattr(langchain_mcp_adapters,__version__) else ok)接着是通道。MCP 工具在运行时会需要一个可用的模型/API 入口尤其是涉及工具调用决策的场景。这里我用 TaoToken 作为统一通道它的好处是一个 Key 覆盖多种模型Base URL 固定配置一次就能被多个 MCP Server 复用减少「这个 Server 配了、那个 Server 没配」导致的空列表。你需要拿到三件套Base URL、API Key、Model ID。Base URL 是https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面创建地址是https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_multiserverutm_campaignrewrite创建后复制那串sk-开头的 Key只显示一次记得存好。Model ID 根据你要用的模型填比如做工具调用决策常用的模型 ID可以在模型对话页确认可用性https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_multiserverutm_campaignrewrite把这三件套写进环境变量后面 MCP 配置和客户端初始化都会读它export TAOTOKEN_BASE_URLhttps://taotoken.net/api export TAOTOKEN_API_KEYsk-你的Key export TAOTOKEN_MODEL_ID你的模型ID注意Base URL 不要带 UTM 参数API 调用路径只认https://taotoken.net/api。UTM 只用于网页跳转归因写进代码里会导致请求路径异常。环境变量设好后建议先做一次最小连通性验证确认通道本身是通的再去接 MCP。这一步能把「通道问题」和「MCP 生命周期问题」提前分离curl -s https://taotoken.net/api/v1/models \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ | head -c 300如果返回里有模型列表 JSON说明 Key 和 Base URL 没问题。如果返回 401先解决鉴权别急着调 MCP否则你会把通道错误误判成工具注册失败。到这里依赖装好、通道验证通过就可以进入客户端初始化环节了。下一节给出可复制的async with片段和get_tools的正确调用顺序。3. 可复制配置async with 初始化与 get_tools 调用顺序这一节是核心。先给结论get_tools()必须、且只能在async with MultiServerMCPClient(...) as client:块内部调用。块外调用拿到的是空列表这是设计行为不是异常。先看正确的完整片段。注意mcp_config的结构、async with的位置、以及get_tools的调用时机import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient mcp_config { mcp-akshare: { command: uvx, args: [src/tool/mcp-akshare], transport: stdio, } } async def main(): async with MultiServerMCPClient(mcp_config) as client: all_tools await client.get_tools() print(f已加载 {len(all_tools)} 个MCP工具: {, .join(t.name for t in all_tools)}) for t in all_tools: print(f- {t.name}: {t.description[:60]}) if __name__ __main__: asyncio.run(main())几个容易写错的点逐个说。第一get_tools()在新版适配器里是异步方法要await。如果你写client.get_tools()不加 await拿到的是一个 coroutine 对象len()会直接报错或者你打印出来是协程不是列表。旧版本可能是同步的但当前版本以await为准。如果你不确定先await报错再降级。第二mcp_config里建议显式写transport: stdio。虽然部分版本能推断但显式声明能避免 Server 类型判断错误导致连接被跳过。stdio 表示通过标准输入输出和本地进程通信适合uvx启动的本地工具。第三async with的进入阶段会依次连接mcp_config里所有 Server。如果某个 Server 启动失败默认行为可能是跳过它继续连其他的最终工具列表里就少了那部分工具甚至全空。所以连接阶段的日志非常关键下一节会讲怎么打开日志验证。如果你需要把 MCP 工具接进一个带模型的 Agent通道配置要一起写进去。下面是一个把 TaoToken 三件套和 MCP 工具组合的片段Base URL、Key、Model ID 都从环境变量读import os from langchain_openai import ChatOpenAI from langchain_mcp_adapters.client import MultiServerMCPClient llm ChatOpenAI( base_urlos.environ[TAOTOKEN_BASE_URL], api_keyos.environ[TAOTOKEN_API_KEY], modelos.environ[TAOTOKEN_MODEL_ID], temperature0, ) async def build_agent(): async with MultiServerMCPClient(mcp_config) as client: tools await client.get_tools() print(f工具数量: {len(tools)}) # 这里把 tools 绑定到 llm 或 Agent return tools如果你用配置文件方式管理比如settings.json或mcp.json结构要保证 Server 名、command、args、transport 四个字段齐全。一个可复制的 JSON 片段{ mcpServers: { mcp-akshare: { command: uvx, args: [src/tool/mcp-akshare], transport: stdio, env: { TAOTOKEN_BASE_URL: https://taotoken.net/api, TAOTOKEN_API_KEY: sk-你的Key, TAOTOKEN_MODEL_ID: 你的模型ID } } } }注意env里的 Key 不要提交到公开仓库。生产环境用密钥管理或环境变量注入配置文件里只留占位符。调用顺序总结成一句话先async with进入触发连接再await get_tools()读取注册表最后在块内使用工具。块外使用工具对象通常没问题但获取工具这个动作必须在块内完成。配置写对之后下一步是验证连接是否真的成功。很多人加了async with还是空列表就是因为连接阶段静默失败了。下一节用日志和请求验证把这一步坐实。4. 验证请求与成功结果日志、工具数量与调用回显配置改完不要直接跑业务先做三步验证看连接日志、看工具数量、看一次真实工具调用回显。这三步能把「连接成功但工具没注册」和「连接根本没成功」区分开。第一步打开 MCP 适配器和底层 SDK 的日志。在脚本最前面加import logging logging.basicConfig(levellogging.DEBUG) logging.getLogger(mcp).setLevel(logging.DEBUG) logging.getLogger(langchain_mcp_adapters).setLevel(logging.DEBUG)重新运行后你会看到类似这样的输出关键行DEBUG:mcp.client.stdio:Starting server with command: uvx src/tool/mcp-akshare DEBUG:mcp.client.stdio:Server started, pid12345 DEBUG:langchain_mcp_adapters.client:Connecting to server mcp-akshare DEBUG:langchain_mcp_adapters.client:Server mcp-akshare connected, tools3如果看到Server started但没有connected说明进程起来了但握手失败常见原因是 Server 启动参数不对或依赖缺失。如果连Starting server都没有说明mcp_config没被读到检查 Server 名和字段拼写。第二步打印工具数量并断言。不要只 print加一个显式检查让空列表直接暴露async with MultiServerMCPClient(mcp_config) as client: tools await client.get_tools() assert len(tools) 0, 工具列表为空检查连接日志 print(f已加载 {len(tools)} 个MCP工具) for t in tools: print(f- {t.name})成功时输出类似已加载 3 个MCP工具 - get_stock_data - get_financial_report - search_symbol第三步做一次真实工具调用回显确认工具不只是「注册了」而且「能用」。挑一个无副作用的查询类工具async with MultiServerMCPClient(mcp_config) as client: tools await client.get_tools() tool_map {t.name: t for t in tools} result await tool_map[get_stock_data].ainvoke({symbol: 000001}) print(result)如果这一步返回了数据说明从连接、注册到调用整条链路是通的。如果工具数量正常但调用报错问题就在工具参数或 Server 内部逻辑不在生命周期。第四步验证模型通道是否参与。如果你的工具调用需要模型决策用 TaoToken 的模型对话页做一次对照测试确认同一个 Key 在模型侧可用https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_multiserverutm_campaignrewrite在页面里发一条简单消息能正常返回就说明通道没问题。这样当 MCP 侧再出空列表时你可以排除通道因素专注查 Server 连接。实测下来按这四步走90% 的空列表问题会在第一步或第二步就暴露根因要么是async with没加要么是 Server 进程没起来。剩下的 10% 多半是通道配置或版本不匹配下一节集中列。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节按真实报错对照排查。每个报错给出触发场景、根因和修复动作。报错一401 Unauthorizedopenai.AuthenticationError: Error code: 401 - {error: {message: Invalid API key}}触发场景MCP 工具在调用模型侧时用了错误的 Key或者环境变量没被读到。根因通常是TAOTOKEN_API_KEY为空、拼写错误、或者配置文件里的 Key 是占位符没替换。修复先确认环境变量在当前 shell 生效echo $TAOTOKEN_API_KEY | head -c 8应该输出sk-开头的前几位。如果为空重新 export。如果代码里读的是配置文件确认env字段里的 Key 已替换。注意 Base URL 必须是https://taotoken.net/api写成带 UTM 的网页地址会 404 或 401。报错二local proxy failed / connection refusedmcp.client.stdio:Failed to start server: [Errno 111] Connection refused触发场景uvx启动的本地 Server 没起来或者端口被占用。根因可能是args路径不对、uvx不在 PATH、或者 Server 依赖没装。修复先在终端手动跑一遍 Server 命令确认能启动uvx src/tool/mcp-akshare如果手动跑也失败先解决 Server 本身。如果手动能跑但代码里失败检查command是否写成了绝对路径以及cwd是否需要指定。stdio 模式下不需要端口出现 connection refused 通常是 Server 内部又去连了别的服务。报错三reading choices / list index out of rangeIndexError: list index out of range或KeyError: choices触发场景模型返回结构不符合预期通常是通道返回了错误页或空响应代码却按标准 OpenAI 格式解析choices[0]。根因是 Base URL 或 Model ID 不对请求打到了错误端点。修复确认三件套齐全且一致。Base URL 用https://taotoken.net/apiModel ID 用模型对话页确认过的可用 ID。做一次最小请求验证curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:$TAOTOKEN_MODEL_ID,messages:[{role:user,content:hi}]} \ | head -c 300返回里有choices字段就说明通道正常。报错四OAuth / token expiredmcp.client.auth:OAuth token expired, re-authentication required触发场景某些远程 MCP Server 用 OAuth 鉴权token 过期后连接阶段失败工具列表变空。根因是 token 没刷新或凭据文件过期。修复找到该 Server 的凭据存储位置删除旧 token 重新授权。如果是本地 stdio Server一般不走 OAuth出现这个报错说明transport配错了把远程 Server 当本地配了。检查transport字段远程用sse或http本地用stdio。报错五工具数量对但名字缺失如果len(tools)大于 0 但少了某个 Server 的工具说明那个 Server 连接失败被跳过了。打开 DEBUG 日志找Connecting to server xxx后面有没有connected。没有就是那个 Server 的问题单独把它拎出来手动启动排查。排查顺序建议固定成先看async with有没有加再看 DEBUG 日志里 Server 有没有 connected再看通道三件套最后看单个 Server 的启动命令。按这个顺序基本不会绕弯路。6. 把 MCP 工具接进长期编码流通道复用与下一步空列表解决之后真正的工作才刚开始把 MCP 工具稳定地接进日常编码或 Agent 流程。这里有两个实践点值得展开。第一通道复用。多个 MCP Server 如果各自配一套 Key维护成本高且容易漏配。用 TaoToken 的统一通道一个 Base URL 加一个 Key 就能覆盖所有 Server 的模型侧调用。把三件套放在环境变量或统一的env块里新增 Server 时只加 command 和 args不用重复配鉴权。这样也能避免「某个 Server 忘了配 Key 导致连接静默失败、工具列表少几个」的问题。第二生命周期管理。async with适合脚本级的一次性任务但如果你在做长期运行的 Agent 服务每次请求都进出一个async with会反复建连断连开销大。更稳的做法是在服务启动时进入上下文把 client 和 tools 缓存在应用状态里服务关闭时再退出。伪代码结构class MCPService: async def start(self): self._ctx MultiServerMCPClient(mcp_config) self.client await self._ctx.__aenter__() self.tools await self.client.get_tools() print(f服务启动加载 {len(self.tools)} 个工具) async def stop(self): await self._ctx.__aexit__(None, None, None)这样工具只注册一次后续请求直接复用。注意异常时也要保证__aexit__被调用否则连接泄漏。如果你要把这套流程固化下来长期跑编码任务或 Agent可以了解下 Coding Plan它把通道和额度管理打包好省去自己维护 Key 轮换的麻烦https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_multiserverutm_campaignrewrite需要自己管理 Key 和查看用量的话控制台在这里https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_multiserverutm_campaignrewrite接入细节和参数说明看文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentmcp_multiserverutm_campaignrewrite最后回到最初那个空列表。它的本质不是配置错误而是对MultiServerMCPClient异步上下文协议的理解偏差。记住一句话创建对象不等于连接async with才是连接。把get_tools()放进块内打开 DEBUG 日志确认 connected再核对通道三件套空列表就没有藏身之处了。
延伸阅读

更多相关文章

2026/10/15 14:41:48

从一节剪纸课到一篇学术报告:艺术教育专业的 AI 搭子怎么选?

如果你是艺术教育专业的学生,大概很熟悉这种“分裂感”:刚带着小学生做完八周非遗剪纸社团课,转头就要把课堂观察、学生作品、问卷、访谈整理成一篇规范的学术报告。 比如这个很典型的毕业任务:设计并实施一门小学高段“非遗剪纸”…

2026/10/15 14:41:48

高清摄像机测试全攻略:从MTF50到ΔE的量化方法

简介:《高清摄像机测试方法》是一份面向高清摄像机研发、测试与质量控制人员的行业标准资料。它由全国安全防范报警系统标准化技术委员会(SAC/TC100)归口,围绕通用型应用高清电视摄像机,系统规定了分辨力、最低可用照度…

2026/10/15 14:41:48

Python高考志愿填报系统:数据聚合与智能推荐实战

1. 项目背景与核心价值高考志愿填报是每个考生家庭面临的重要决策节点。根据多年观察,我发现许多考生和家长在填报时存在三个典型痛点:信息过载导致选择困难、分数线预测不准确造成滑档风险、专业认知偏差引发入学后不适应。这个Python开发的辅助系统正是…

2026/10/15 17:33:41

Qt TCP服务端多客户端并发、断线重连与字节解析实战

简介:本资源面向Qt网络编程初学者与需要搭建多客户端通信框架的开发者,提供一套可直接参考的TCP服务器与客户端完整示例,重点解决多客户端并发连接、断线自动重连、按字节解析自定义协议以及定时循环发送等常见需求。压缩包共16个文件&#x…

2026/10/15 17:33:41

Python中文词云实战:jieba分词与wordcloud蒙版渲染全解析

1. 项目缘起与整体设计思路1.1 为什么选择 wordcloud 库来做词云做文本可视化的朋友大概率都绕不开一个需求:把一堆文本里出现频率最高的词,用一张图直观地呈现出来。不管是分析用户评论、整理会议纪要,还是做舆情监控的快速预览,…

2026/10/15 17:33:41

C++手写线性回归神经网络:从原理到嵌入式部署实战

1. 为什么用C手写线性回归神经网络1.1 从一次实际需求说起去年帮一个做工业设备监测的朋友处理振动传感器数据,需要在嵌入式采集板上直接跑回归模型做实时趋势预测。板子资源很紧张,Python解释器根本塞不进去,PyTorch那套更是想都别想。当时第…

2026/10/15 17:33:41

STM32F103 TIM1四通道单脉冲输出:ETR触发配置与避坑指南

简介:这份资源面向STM32F103嵌入式开发者与定时器学习者,聚焦高级定时器TIM1的ETR外部触发输入与四通道单脉冲输出(One Pulse Mode)实现,解决外部事件触发下多路精确时序脉冲生成的问题,适合具备一定寄存器…

2026/10/14 11:31:03

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/14 17:41:07

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/15 0:02:22

防关联浏览器哪个好?三款产品实测差异解析

新手做多店铺,最纠结的就是防关联浏览器选哪个。飞跨、网易、站斧这三款被问得最多,先给结论:要一体化省心、长期便宜,选站斧;要网易招牌和稳,选网易;预算紧做东南亚 Lazada,选飞跨。…

2026/10/15 0:02:22

影刀RPA深度实战:大模型Fine-tuning定制RPA专用Prompt与指令生成

影刀RPA深度实战:大模型Fine-tuning定制RPA专用Prompt与指令生成 一、技术背景 大模型(LLM)在通用场景下表现强大,但当我们将 GPT-4 或 Qwen 直接用于生成影刀RPA操作指令和 XPath 定位时,准确率通常只有 40%-60%。原因…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑