
跑 hello-agents 的 MCPTaoTokenhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content注册后建一把 Key这一步放在最后做也行但越早准备越省事。整条链路里最容易翻车的不是 agent 本身而是05_UseMCPToolInAgent里的LLM_BASE_URL——前面01_TestConnect、02_Connect2MCP、03_GitHubMCP全都顺顺当当一到 SimpleAgent 把 MCPTool 挂上去模型通道才真正开始跑Token 也是从这一刻开始出的。前面那些脚本连的是本地 stdio 子进程压根不经过模型所以你在那几步里怎么改.env都看不出区别真正要动的是最后这个文件的模型地址。我按原文的推进顺序把整条路重走了一遍包括中间那个看着吓人的RuntimeError: Event loop is closed。这篇不打算只给一份.env模板就收工想把每一步的因果讲清楚为什么npx会找不到、14 个文件系统工具是怎么冒出来的、agent.run(计算 123 456)到底把什么塞进了上下文、以及LLM_BASE_URL后面到底该写什么。1. pip install hello-agents[protocol]0.2.2 之后先撞上 npx: None1.1 conda 环境里只装了 PythonNode.js 是缺的起手式跟原文一样先建一个干净的 conda 环境再装库避免污染系统里的 Pythonconda create -n agent python3.11 -y conda activate agent pip install hello-agents[protocol]0.2.2装完不会有任何异常提示import也正常于是很容易误以为环境已经齐了。问题出在第一个真正去连 MCP 服务器的脚本上MCPClient默认走 stdio 传输也就是由它自己subprocess起一个子进程把npx当成可执行文件去拉modelcontextprotocol/server-filesystem。conda 环境里只有 Python 解释器没有 Node.js 运行时这个npx就不存在。报错信息通常长这样看着像是在说“npx 等于 None”其实核心是上一层的FileNotFoundErrorNo such file or directory: npx npx: None很多人第一反应是去改脚本里的command参数把npx换成绝对路径或者干脆写npx.cmd。方向错了——根因是这台机器没装 Node.js换成什么路径都没用。先确认一下which node which npx两条都返回空就说明确实缺运行时接着装就好。1.2 conda install -n agent -c conda-forge nodejs -y 把 npx 补上按原文的做法直接往当前 conda 环境里塞 Node.js这样node、npx会跟着环境一起激活不会跑到全局去conda install -n agent -c conda-forge nodejs -y node -v npx -v两条版本号都能打出来npx: None这一类报错就彻底消失了。这里有个细节值得留意装完 Node.js 之后要确认当前 shell 还在agent环境里如果你中途开过新终端记得conda activate agent再跑脚本否则依然会提示找不到npx。另外一个坑是npx首次拉包需要联网下载modelcontextprotocol/server-filesystem第一次启动会慢十几秒。如果脚本里有超时设置别把它调得太短否则你会看到一个莫名其妙的连接超时而不是缺 Node.js 的报错排查方向就被带偏了。2. 01_TestConnect 到 03_GitHubMCP14 个文件系统工具是怎么列出来的2.1 MCPClient 的 stdio 启动参数要写对Node.js 到位之后01_TestConnect基本就是一次连通性检查。传给MCPClient的关键信息无非三样启动命令、参数列表、以及允许访问的目录。命令行那部分概念上等价于npx -y modelcontextprotocol/server-filesystem ./sandbox-y的作用是跳过 npx 的交互式确认脚本环境下必须加否则子进程会停在“是否安装该包”的提示上表现就是脚本卡住不动。后面那个目录参数决定了文件系统服务器能碰哪些路径——它是一个白名单传./sandbox就只能读./sandbox里的东西传项目根目录就能读整个项目。这个白名单是 MCP 文件系统服务器的安全边界不是可选项。脚本里对应的地方大致是把命令和参数组成一个列表交给MCPClient如果你的版本里字段名不一样有的叫command/args有的包了一层server_params照着你本地那份源码里的签名填就行值本身不变。2.2 02_Connect2MCP 里那份 14 个工具的清单02_Connect2MCP做的事比第一个脚本多一步连上之后调一次列出工具的方法把服务器暴露的能力打出来。文件系统服务器通常会给出十来个工具不同版本在数量上会差一两个常见的有read_file读单个文本文件read_multiple_files一次读多个write_file、edit_file写和改create_directory、list_directory、list_directory_with_sizesdirectory_tree把目录结构整个吐出来move_file、search_filesget_file_info、list_allowed_directories原文那次跑出来是 14 个。这个数字本身不重要重要的是你能在输出里看到工具名和入参 schema——它意味着握手成功、tools/list拿到了返回。如果你的输出里工具列表是空的先别急着怀疑通道问题检查两件事一是sandbox目录是否真的存在二是白名单路径是不是写成了相对路径而脚本的工作目录又变了。2.3 03_GitHubMCP 顺带跑通说明协议层没问题03_GitHubMCP是另一个 MCP 服务器走的是 GitHub 那套工具。它能跑通等于给前面的结论又加了一层佐证MCPClient的连接逻辑、stdio 子进程管理、工具发现这一整套都是好的。这一步不需要你换任何模型通道——它调的依然是 MCP 服务器本身跟LLM_BASE_URL一点关系都没有。所以到03_GitHubMCP为止你手上其实已经有了一个判断依据报错如果出现在这三个脚本里八成是环境或服务器参数问题报错出现在后面的 Agent 脚本里才轮到模型通道背锅。这条分界线后面排障会反复用到。3. RuntimeError: Event loop is closed 与换通道无关3.1 这条噪音来自子进程管道的析构跑完01、02之后终端里经常会在正常输出下面多出一段RuntimeError: Event loop is closed Exception ignored in: function BaseSubprocessTransport.__del__它出现在脚本已经打印完结果之后退出阶段才冒出来。原因是 asyncio 的BaseSubprocessTransport在垃圾回收时试图去关闭子进程的管道而此时事件循环已经关掉了于是析构函数抛了个异常因为发生在__del__里Python 只能打印成 “Exception ignored”。它不影响任何业务结果工具列出来了文件读到了函数返回值也对了。原文的处理方式是加一层sys.unraisablehook把这类特定异常吞掉让终端干净一点。这一步纯粹是观感问题不做也不会让程序算错。别把这条报错跟换通道混在一起排查它在你改.env之前和之后都会出现属于典型的“看起来吓人、其实无害”。3.2 sys.unraisablehook 过滤的写法思路是自定义一个 hook判断异常是不是那种退出阶段才出现的 asyncio 噪音是就静默跳过不是就交回默认处理import sys import asyncio _default_hook sys.unraisablehook def _quiet_hook(unraisable): text repr(getattr(unraisable, exc_value, )) if isinstance(getattr(unraisable, exc_value, None), RuntimeError) and Event loop is closed in text: return _default_hook(unraisable) sys.unraisablehook _quiet_hook放在脚本入口最前面即可注意别把整个RuntimeError类别一刀切Event loop is closed这个字符串还是要匹配一下不然真出了别的运行时错误你也会看不见。4. 05_UseMCPToolInAgentSimpleAgent 一挂上 MCPTool 就开始花 Token4.1 agent.run(计算 123 456) 背后发生了什么前三个脚本都是“连服务器、列工具”05_UseMCPToolInAgent是第一次让模型参与进来把 MCPTool 交给 SimpleAgent然后跑一句agent.run(计算 123 456)。这一句看着像小学算术链路却是完整的这句用户输入被拼进对话上下文随请求发给模型模型在可用的工具列表里挑发现有个内置的add于是返回一个工具调用意图Agent 框架执行add(123, 456)拿到579这个结果会被塞回上下文再发一次请求给模型让模型组织出自然语言答复。第 2 步和第 4 步都是真实的模型调用也就是两次消耗。如果 MCPTool 里挂了文件系统服务器同样的流程会再多几轮模型决定调read_file框架执行把文件内容塞回上下文模型再回复。所以文件一大上下文就跟着涨。这就是为什么前面几步不花钱、这一步开始花钱——真正发请求的是HelloAgentsLLM不是 MCP 服务器。4.2 HelloAgentsLLM 只认 .env 里的 LLM_API_KEY 和 LLM_BASE_URLHelloAgentsLLM读配置的方式很朴素从环境变量里取LLM_API_KEY和LLM_BASE_URL项目里一般由.env加python-dotenv在启动时加载。也就是说你想让这条模型通道接哪儿改的就是这两行改脚本、改 MCPTool 的配置都是白费劲。这也是最容易被忽略的一点.env往往放在脚本目录下而你可能在别的目录里用 IDE 打开项目、从别的地方执行脚本结果.env根本没被加载程序用的是系统环境变量或者干脆回落到某个默认地址。表现就是“我明明改了怎么还报鉴权失败”。跑之前先确认工作目录在脚本目录下执行或者显式指定env文件路径。5. 把 LLM_BASE_URL 指到 TaoToken.env 的写法与两个坑5.1 先在 TaoToken 落地页建一把 Key打开 TaoToken 注册进控制台创建一把 API Key复制出来。这把 Key 就是.env里LLM_API_KEY的值。它跟 MCP 没关系MCP 服务器不需要任何 Key需要 Key 的只有模型通道这一层。建议按项目分钥匙一个项目一把 Key团队里几台机器各建各的别几个人共用一把。后面要是哪台机器配置写错了疯狂重试或者某个项目要停掉你在控制台能直接定位到是哪把 Key 在动。5.2 .env 完整示例LLM_BASE_URL 用 https://taotoken.net/api回到脚本目录打开或者新建.env把两行改掉LLM_API_KEYYOUR_API_KEY LLM_BASE_URLhttps://taotoken.net/api如果你的 hello-agents 版本还需要指定模型名再加一行值从模型广场拿LLM_MODEL_NAME以模型广场当时列表里的 ID 为准这里有两个高频错误都是顺手多写造成的。第一个是在 Base URL 后面加/v1https://taotoken.net/api/v1这种写法会让路径拼接后变成/api/v1/chat/completions之类的双段地址直接 404。填进去的就是https://taotoken.net/api末尾不要/v1也不要多余的斜杠。第二个是把官网首页当成接口地址https://taotoken.net/?utm_source...那一串是给人点的落地页浏览器能打开不代表接口能用往里面 POST 请求只会拿到一个 HTML 页面。人的页面和机器调用的地址是两回事.env里只写https://taotoken.net/api。改完.env记得别把它提交进 Git。这个文件里放的是明文 Key进仓库等于公开。5.3 模型 ID 一律以模型广场当时列表为准模型名这一项别凭记忆写。不同接入方对同一个模型的命名习惯不一样有的带前缀有的带日期后缀抄别人博客里的字符串大概率对不上。以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 上的模型广场当时列出来的 ID 为准复制粘贴不要手打。选哪个模型取决于你要干什么。这里的任务是把工具调用结果翻译成人话属于典型的轻量对话加结构化输出不需要挑最贵的那个。先拿一个便宜快速的试通链路确认add能被正确调用再按需要往上换。先用小模型验证通路再用大模型跑真实任务这个顺序能帮你省掉不少排查时间。6. 验证add 工具被自动调用、filesystem 工具能读文件、请求落在 TaoToken6.1 先确认 hello-agents 装全在改完.env之后、跑 Agent 之前先用一行命令确认库本身没问题python -c from hello_agents.tools import MCPTool, A2ATool, ANPTool; print(ok)打印出ok说明protocol那部分依赖装齐了。这一步很值因为它把“库没装全”和“配置写错”两种可能提前分开了如果这里就报ImportError那后面所有排查都没意义先回去补装。6.2 跑 05_UseMCPToolInAgent看两件事python 05_UseMCPToolInAgent.py输出里要盯两处。第一处是内置add工具被自动调用了模型返回的最终答复里应该出现579而且中间能看到工具调用的痕迹——这说明工具注册和自动调用这条链路是通的。第二处是文件系统那边的外部工具确实能读文件让 agent 去读sandbox里的某个文本文件输出里能看到文件内容说明 MCP 服务器挂载成功。第三件事得换个地方看回到 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_content 的控制台看这次调用的用量记录有没有新增。如果请求确实打到了这边用量会往上走如果记录纹丝不动而程序也没报错那多半是你的.env压根没被加载程序用的还是老配置。这一条比看日志直观得多。6.3 多台机器各建各的 KeyBase URL 保持同一个团队协作时的常见做法是每台机器、每个开发者各建一把 KeyLLM_API_KEY各不相同而LLM_BASE_URL全部写成同一行https://taotoken.net/api。这样出问题时能按 Key 定位到人改起配置来又不用记不同的地址。把这两行抽成模板发给同事比在群里贴截图靠谱。还有一点要注意LLM_BASE_URL是全局生效的同一个 shell 里如果有别的项目也在用LLM_*系列环境变量会互相串。跑之前echo $LLM_BASE_URL看一眼当前实际生效的值比对着.env干猜快得多。7. 本篇三个报错对照与下一步7.1 报错对照表现象出现的脚本根因处理No such file or directory: npx、npx: None01_TestConnect环境里没有 Node.js 运行时conda install -n agent -c conda-forge nodejs -y后npx -v验证Exception ignored ... Event loop is closed02_Connect2MCP退出阶段子进程管道在事件循环关闭后析构sys.unraisablehook过滤与通道无关404 或返回 HTML 页面05_UseMCPToolInAgentLLM_BASE_URL多了/v1或误填了官网首页改回https://taotoken.net/api这张表的价值在于定位顺序先看报错出现在哪个脚本再往上套。第一类永远先查 Node.js别去动.env第二类直接忽略只有第三类才需要回头检查 Key 和 Base URL。很多人在第一步就冲去改配置白白绕一大圈。7.2 下一步从模型对话到 Coding Plan链路跑通之后建议用一个更直接的场景再验一次配置那就是 模型对话用跟.env里同一把 Key 发条消息如果这边通、Agent 那边不通问题就锁定在文件加载或环境变量上而不是 Key 本身。如果你打算把 MCP 这条路继续往深里做日常调用量会上来可以顺手看看 Coding Plan 是否够用需要再建新 Key 或者给同事开一把去 控制台 API Keys 就行。要是你同时也在折腾 Claude Code 的接入环境变量对照可以看 接入文档那套变量名跟 hello-agents 的LLM_*不是一回事别混用。回头看这整条链路真正需要你手动配置的其实只有.env里那两行。Node.js 是环境问题Event loop is closed是观感问题14 个工具是协议层正常的输出只有HelloAgentsLLM读的那两行决定了请求发去哪。把这两行改对、把/v1那个尾巴忍住不加剩下的就是让 agent 自己去调工具了。