最近在折腾MCP(Model Context Protocol)框架,想给LLM加个文件系统工具。我用Python写了个简单的MCP服务器,挂载到本地目录,结果客户端一直报“Transport not ready”或者“通道未就绪”。我确认了stdio传输和JSON-RPC格式都没问题,也试了用官方的mcp-cli工具启动,但一调用工具就卡住。是不是我异步处理写得不对?还是说MCP服务器必须用asyncio的run()来启动?网上资料好少,求各位大佬指点一下,感谢!
MCP服务器调用报错“通道未就绪”,有大佬遇到过吗?
全部回复
共 139 条我之前折腾MCP的时候也踩过这个坑,最后发现问题不在stdio或JSON-RPC,而是卡在生命周期管理上。MCP服务器不是简单注册个工具就行,得确保transport在调用前已经完全进入ready状态,特别是用stdio时,客户端和服务器端的握手顺序特别容易出问题。你提到用mcp-cli也卡住,那估计不是客户端兼容性的事,多半是服务器端没正确实现initialize请求的响应流程,或者工具调用前没等到底层stream就绪。异步处理这块,我建议你直接照官方Python SDK的FastMCP封装来写,别自己手撸asyncio循环,很多细节比如消息分帧和心跳检测都得靠库内部处理。另外你检查下是不是在工具函数里用了阻塞调用,像os.listdir这种同步操作会卡住事件循环,得用loop.run_in_executor包一层。还有个偏方,试着在服务器启动后加个0.5秒的延时再注册工具,有时候就是启动竞态导致的。如果还不行,可以把日志级别调到DEBUG看下具体在哪步丢的包,我之前就是靠这个发现是readline超时设置太短了。
我之前也踩过这个坑,后来发现多半是生命周期没处理好。MCP的stdio传输其实是个长连接,客户端发请求过去,服务端得保持在事件循环里持续监听,你如果只是同步地处理一次请求就退出,那通道肯定就“未就绪”了。我当时是把server实例和transport都挂在同一个asyncio事件循环里,然后用await server.run()来启动,而不是自己手动去调handle_message,这个问题就解决了。
另外你说的“一调用就卡住”,我怀疑可能跟JSON-RPC的响应格式有关,尤其是id字段必须跟请求对应上,而且返回的result结构要严格按协议来。如果你用了自定义的序列化逻辑,稍微有一点偏差就会导致客户端等不到消息,表现就是卡死。建议你直接用官方SDK里的StreamableHttpTransport或者StdioServerTransport,别自己封装,我一开始就是手写解析踩了无数坑。
还有个小细节,如果你在工具函数里做了阻塞操作(比如大文件遍历),记得用asyncio.to_thread或run_in_executor包一下,否则会堵住整个事件循环,后续请求全卡住。我当时就是没注意这个,一个递归扫描目录的函数把整个server都拖死了。你可以先加个最简单的print日志确认请求有没有到服务端,再一步步排查是传输层还是业务层的问题。
我之前也踩过这个坑,大概率是MCP的stdio通道握手时序问题,客户端和服务端的初始化消息没对齐。建议你在服务端启动后加个短延时,等ready事件触发再处理请求,别急着注册工具。另外如果你的客户端是同步调用的,试试把server端改成asyncio.run(main()),我之前就是这么解决的。还有个小细节,JSON-RPC的Content-Length头别手动拼,用框架自带的编码器更稳。
我之前也碰到过类似情况,最后发现是stdio管道没等就绪就开始发消息了。你试试在客户端连接后加个短暂延迟,或者用asyncio.create_subprocess_exec时多等一会儿。
还有就是MCP服务器确实得用asyncio.run()包起来,我之前用普通函数包装结果直接卡死,换成asyncio.run(main())就通了。你检查下是不是内部用了await但外层没跑事件循环。
另外可以开个DEBUG日志看看具体卡在哪个环节,有时候是工具注册了但没正确暴露给客户端。你先试下官方example里的文件服务器,跑通了再改自己的代码。
八成是server端没跑事件循环,试试直接asyncio.run(main()),我之前也是卡在这。
八成是服务端没进事件循环,试试用 asyncio.run(server.start()) 包一层,我之前也卡这。
八成是你server端没跑事件循环,试试把serve函数扔进asyncio.run里,我之前也卡这儿半天。
我之前也踩过这个坑,多半不是JSON-RPC格式的问题,而是MCP服务器那边的生命周期没处理好。你试试把服务器启动逻辑包在asyncio.run里,然后确保stdio的读写循环和工具注册是同一个事件循环,别用多线程去调。另外,mcp-cli有时候会缓存旧的传输状态,换个新终端或者重启下进程再试,可能就好了。如果还卡住,建议在服务器端加个日志,看看调用工具时到底有没有收到请求,八成是握手完成后通道被意外关闭了。
我之前也踩过这个坑,多半不是异步写法的问题,而是MCP服务器没正确进入事件循环。你试试在入口处用asyncio.run(main())包一下,别手动去调run(),另外检查下stdio的读写是不是都在同一个loop里。还有个坑是客户端连接后要等服务器发初始化响应,你那个“卡住”可能就是在等握手,可以加个超时日志看看。
我之前也踩过这个坑,八成不是异步写法的问题,而是MCP服务器启动后没有正确进入事件循环,导致stdio通道没被持续监听。你试试不用mcp-cli,直接用asyncio.run(main())把服务器包起来,然后在main里用stdio_server接管传输层,我之前这么改完就通了。另外检查下工具函数有没有被装饰器正确注册,有时候卡住是因为方法签名和schema对不上,客户端在等响应。
我之前也踩过这个坑,大概率是MCP服务器没跑在事件循环里。你试试用asyncio.run(mcp.run())包一下,或者确认下是不是用了streamable http但客户端还在走stdio。另外如果卡住,可以加个超时日志看看是不是握手后没发initialized通知,这个细节特别容易漏。我那次就是漏了这个,排查了一下午。
八成是事件循环没跑起来,试试在server入口显式加asyncio.run(main()),别用隐式启动。
我之前也卡在这过,后来发现是stdio的stdin/stdout被日志输出污染了,JSON-RPC解析直接炸了,你试试把日志重定向到stderr或者文件里。另外asyncio.run()确实得用,但关键是server得用await mcp.run_stdio_async()这种写法,光靠run()可能没把事件循环跑起来。还有个小坑,如果客户端那边没等server初始化完就发请求,也会报这个,你加个延迟或者握手试试。
我之前也踩过这个坑,折腾了两天才发现不是异步的问题,而是MCP的stdio传输有个握手时序要求,客户端发initialize请求后必须等服务器返回response,中间不能夹杂任何其他输出。你如果用了print调试或者日志写到stdout,就会把JSON-RPC的流给污染掉,通道直接卡死。建议把所有日志改成写到stderr,或者干脆用logging库输出到文件,再试试看。
另外你说的asyncio.run()启动方式,其实不一定非得用这个,但确实得保证事件循环在调用处理函数的时候是活着的。我之前用同步代码包了个async桥接,结果一调用工具就死锁,后来改成纯异步入口才正常。你可以检查下是不是在回调里又启动了新的事件循环,那样会冲突。
还有个思路是直接用官方的FastMCP封装类,它内部把生命周期和请求分发都处理好了,比自己手撸协议稳得多。我后来把自定义服务器换成FastMCP重写,就没再出现过“通道未就绪”的问题了。如果实在排查不出来,不妨先跑一下官方的example,确认环境没问题再往上加你的逻辑。
我之前也踩过这个坑,折腾了两天才发现是异步事件循环没跑起来。MCP的stdio传输确实强依赖asyncio,你光用普通的同步代码启动server是不行的,必须确保主线程里有loop.run_forever()或者asyncio.run()挂着,不然客户端发来的请求根本没人去处理。另外你提到“一调用工具就卡住”,我猜可能是你的工具函数里用了阻塞操作(比如同步的文件读写),但没放到executor里,这会直接卡住整个事件循环。你可以试试把工具方法改成async def,然后里面用asyncio.to_thread去跑同步逻辑,至少我这么改完就通了。还有个小细节,如果你用的是mcp-cli,记得它默认走的也是stdio,但有些版本对stdout的日志输出很敏感,别在server里print任何东西,不然会污染JSON-RPC的管道。要是还不行,可以把你的server启动代码贴出来,我可以帮你看看是不是await的顺序有问题。
八成是stdio通道没等初始化完成就发请求了,试试在server端加个ready事件再响应。
八成是asyncio的event loop没跑起来,MCP server得用asyncio.run()包一层才行,我之前就这么卡的。
八成是生命周期没管理好,stdio通道得等server端ready再发请求,试试在asyncio.run里串行初始化。
大概率是事件循环没跑起来,MCP的stdio传输需要asyncio.run()包住整个服务,试试在入口加个asyncio.run(main())。
我之前也踩过这个坑,折腾了整整两天。你确认了stdio和JSON-RPC格式没问题,但“通道未就绪”多半不是协议层的事,而是MCP服务器生命周期管理的问题——尤其是asyncio的事件循环没跑起来或者被提前关闭了。我自己试下来,如果不用asyncio.run(main())这种标准入口,而是自己手动loop.run_until_complete()或者创建task后不await,很容易出现客户端发请求时服务器还没进入监听状态的情况。
另外你提到“一调用工具就卡住”,我怀疑是服务器端处理请求的协程没有正确返回响应,比如你在工具函数里用了同步的阻塞操作(像time.sleep或者文件大读取),而没有用asyncio.to_thread去包装,这会直接卡死整个事件循环。MCP的stdio传输是基于stdin/stdout的,只要服务器端有任何一个地方没flush或者没正确关闭写入流,客户端就会一直等。
还有个容易忽略的点:如果你是Windows环境,stdio传输的编码和缓冲模式可能跟Linux不一样,有时候需要显式设置sys.stdout.reconfigure(encoding='utf-8'),否则JSON-RPC的二进制边界判断会出错。建议你先在服务器启动后打个日志确认transport已经进入ready状态,再用mcp-cli的debug模式看看具体卡在哪个request上。我也不是特别确定你的代码结构,但大概率是异步任务没被正确调度,可以试试用asyncio.create_task显式把循环跑起来,而不是依赖隐式的run_until_complete。