最近在折腾MCP(Model Context Protocol)框架,想给LLM加个文件系统工具。我用Python写了个简单的MCP服务器,挂载到本地目录,结果客户端一直报“Transport not ready”或者“通道未就绪”。我确认了stdio传输和JSON-RPC格式都没问题,也试了用官方的mcp-cli工具启动,但一调用工具就卡住。是不是我异步处理写得不对?还是说MCP服务器必须用asyncio的run()来启动?网上资料好少,求各位大佬指点一下,感谢!
MCP服务器调用报错“通道未就绪”,有大佬遇到过吗?
全部回复
共 139 条我之前也卡在这个“Transport not ready”上,后来发现是MCP server的stdio输入输出被Python的print日志污染了,JSON-RPC解析直接崩掉。建议把日志全部写到stderr或者文件里,stdout只留协议数据。另外你用的asyncio.run()启动方式本身没问题,但记得要给消息循环加个超时或者手动跑一下read操作,不然很容易卡死。
八成是asyncio事件循环没跑起来,stdio通道得一直挂在那儿等消息。试试把serve函数丢进asyncio.run里,别用普通函数启动。
我之前也被这个坑过,八成不是你协议写错,而是MCP的stdio握手时机问题。试试把服务器端启动逻辑改成asyncio.run(main()),然后确保在打印“ready”之前先别急着处理请求,不然客户端以为通道没就绪就容易卡住。另外,如果你用了自定义的transport,检查下是不是没把stderr重定向,有时候日志混进stdout会把JSON-RPC搞乱。
我之前也卡在这过,后来发现是asyncio事件循环没跑起来,MCP的stdio传输确实得靠run()常驻。你试着把server挂到asyncio.run(main())里,然后确保handlers都是async函数,别用同步阻塞调用。另外,如果用的是自定义transport,检查下read和write是不是在同一个loop里,分开很容易报通道未就绪。
我之前也踩过这个坑,多半不是JSON-RPC格式的问题,而是MCP服务器启动后没有正确进入事件循环。你试试把入口函数用asyncio.run(main())包起来,然后确保server在run()之前已经注册好所有工具,别在回调里再动态加。另外检查一下stdio是不是被缓冲了,有时候print调试信息会污染管道导致卡住,要么全走logging,要么flush一下。
我之前折腾MCP的时候也卡在这一步过,后来发现多半不是JSON-RPC格式的问题,而是stdio的stdin/stdout被缓冲或者被别的日志输出污染了。你试着在服务端代码里把所有print都换成logging到文件,因为print默认走stdout,会和MCP的协议数据混在一起,导致客户端解析不到完整的消息帧,自然就一直“Transport not ready”。另外,你说的异步处理确实是个大坑,MCP的stdio transport在Python里基本必须用asyncio.run()或者asyncio.run_coroutine_threadsafe()来启动,不能直接同步调用,因为它的内部握手是异步完成的,你同步跑的话,事件循环没跑起来,客户端发来的initialize请求就永远得不到响应。我之前还踩过一个更隐蔽的坑——Windows下stdin的文本模式会把\r\n转换掉,破坏JSON-RPC的字节长度,必须用sys.stdin.buffer和sys.stdout.buffer来做原始IO。你可以先用mcp-cli加上--debug参数看看握手阶段到底卡在哪个方法上,如果调用工具时卡住,大概率是工具函数里用了同步阻塞操作,比如os.listdir在大目录下也会卡,得用asyncio.to_thread包一层。还有个思路是直接看官方Python SDK里的FastMCP封装,它把底层细节都处理好了,比自己手写transport稳很多,实在不行就换FastMCP重写一遍,省时省力。
大概率是server端没起事件循环,试试在main里加asyncio.run()包住启动逻辑,我之前也栽这儿了。
我之前也踩过这个坑,当时差点把电脑砸了。你确认stdio和JSON-RPC格式没问题,但一调用就卡住,很可能问题不在协议层,而是服务器端的事件循环没跑起来。MCP的stdio传输是半双工的,如果服务器没有正确进入asyncio的run()或类似的主循环,客户端发过来的initialize请求根本没人处理,自然就一直“未就绪”了。你可以试试在服务器代码里显式加一个await asyncio.sleep(0)或者在初始化完成后打印一条日志,看看请求到底有没有被收到。另外,检查一下你的工具函数是不是用了同步的open()或者input()这类阻塞调用,在异步环境里会直接卡死整个事件循环,我之前就是栽在文件读取上。还有一个容易忽略的点:stdio通道的缓冲区,记得要flush,有时候数据积压没发出去也会造成假死。如果方便的话,把服务器端异常输出重定向到文件里,跑一遍看traceback,比猜要快得多。
八成是stdio管道没等就绪就发消息了,试试在server端加个启动完成的握手信号。
我之前也卡这儿,后来发现得用asyncio.run(main())包一层,直接裸跑event loop会漏初始化。
八成是stdio握手后没保持事件循环,试试在server里显式加asyncio.run(main())再启动。
我之前也卡在这玩意儿上,最后发现是stdio的stdout被日志输出污染了,JSON-RPC握手直接乱掉,你试试把日志全重定向到stderr。另外MCP的异步循环确实得用asyncio.run()包起来,但更关键的是确保server在接收请求前已经完成初始化握手,不然通道就绪事件根本不会触发。你要是用官方cli都卡,大概率是server端某个await卡死了,建议在工具调用入口加个超时打印看看。
八成是asyncio事件循环没跑起来,试试把serve函数直接包在asyncio.run里,我之前就这么解决的。
我之前折腾MCP的时候也踩过这个坑,后来发现问题往往不在stdio或者JSON-RPC本身,而是MCP的传输层握手机制。官方文档里其实写得很隐晦,客户端和服务器之间需要先完成initialize请求,然后等服务器返回协议版本和能力列表,如果这个阶段没处理好,后面的工具调用就会一直卡在“Transport not ready”上。你可以试试在服务器端手动打印一下收到的原始消息,看看是不是客户端发了initialize之后,你的服务端没正确回包。另外,关于asyncio.run()的问题,我觉得倒不一定是必须的,但MCP的Python SDK内部用的是异步流式处理,如果你自己写的循环是同步阻塞的,消息就永远无法被读取,建议直接用SDK自带的mcp.run(transport="stdio")入口,别自己手动起事件循环。还有个容易忽略的点,就是如果你在Windows上测试,stdio的编码和缓冲模式可能会搞鬼,加个-u参数或者设置PYTHONUNBUFFERED=1试试。我之前就是被这个坑了一整天,最后换成官方示例里的写法才跑通。
八成是server没正常进入事件循环,试试把serve函数直接扔给asyncio.run跑,别自己套loop。
我之前折腾MCP的时候也撞到过这个“Transport not ready”,当时排查了半天,最后发现是stdio的读写循环没处理好,客户端发请求过来后,服务端在异步任务里没及时把响应写回stdout,导致握手卡住了。你提到用mcp-cli启动也卡,那大概率不是启动方式的问题,而是你的server实现里,对JSON-RPC的响应没有显式flush,或者用了print调试导致输出流被污染了。建议你检查一下是不是在异步回调里直接调用了同步的打印,或者把日志写到了stdout,这会让MCP的stdio通道收到非协议数据,客户端就会一直等。另外,asyncio.run()不是必须的,但如果你用了loop.create_task或者run_in_executor,要确保事件循环一直在跑,别在请求处理中途退出循环。我之前是把serve函数改成async,然后内部用asyncio.StreamReader/Writer去手动处理stdin/stdout,这样更可控。你可以先写个最小复现,只处理initialize和tools/call,看能不能通,逐步加功能。
八成是server端没进事件循环,试试把serve函数包进asyncio.run里,我之前也卡这儿半天。
八成是server端没在stdio上保持长连接,试试把asyncio的run换成run_forever,或者检查下子进程有没有提前退出。
我前几天也踩过这个坑,最后发现是stdio的stdin/stdout被日志输出污染了。你检查下服务端有没有print或者logging默认输出到控制台,JSON-RPC解析到非协议内容就会直接卡住,报错信息还不明显。另外你说的asyncio.run()启动,我这边实测是必须的,但光有run还不够,得确保transport的读写循环在同一个事件循环里跑,不然子协程调工具时通道还没就绪。可以试试把日志切到stderr,或者用mcp的LoggingConfiguration单独配置,这招对我管用。还有个思路是看下客户端是不是用了多线程,MCP的stdio传输对线程模型很敏感,如果客户端在子线程里发请求,服务端主循环响应了但回调线程没绑定事件循环,也会出现“通道未就绪”。我后来干脆把客户端和服务端都改成纯asyncio,用asyncio.to_thread包一下阻塞操作才稳定。你那个挂载目录的工具如果是同步文件操作,记得用loop.run_in_executor,别直接阻塞事件循环。最后建议把mcp库升到最新版,之前有个版本对stdio的握手时序有bug,升级后就没复现了。
我之前也踩过这个坑,折腾了两天才发现是事件循环的问题。你用的stdio传输,如果客户端和服务器不在同一个asyncio loop里,确实会报“Transport not ready”。我当时是把MCP server塞进FastAPI的线程池里跑,结果stdio的reader/writer绑定的loop和调用方的loop不一致,直接卡死。后来我干脆把server的启动逻辑单独拎出来,用asyncio.run()包一层,再在子进程里跑,就好了。另外你提到mcp-cli也卡住,那大概率不是调用方式的问题,而是你的server端在初始化工具时没正确注册回调。你可以试试在server里显式打印一下每个工具的schema,看看是不是返回了非JSON-RPC格式的response。还有个小细节,MCP的stdio传输要求所有日志输出都走stderr,千万别print到stdout,不然客户端解析会直接懵。异步处理的话,建议检查一下你是不是在handle_call里用了同步的open()或read(),这些阻塞操作会卡住事件循环,导致后续消息无法处理。如果还不行,可以加个超时机制,把每次调用的响应时间打印出来,定位是卡在握手还是卡在工具执行。
八成是server端没等client握手就退出循环了,试试把serve函数里加个await asyncio.Event().wait()挂住。