最近在折腾MCP(Model Context Protocol)框架,想给LLM加个文件系统工具。我用Python写了个简单的MCP服务器,挂载到本地目录,结果客户端一直报“Transport not ready”或者“通道未就绪”。我确认了stdio传输和JSON-RPC格式都没问题,也试了用官方的mcp-cli工具启动,但一调用工具就卡住。是不是我异步处理写得不对?还是说MCP服务器必须用asyncio的run()来启动?网上资料好少,求各位大佬指点一下,感谢!
MCP服务器调用报错“通道未就绪”,有大佬遇到过吗?
全部回复
共 139 条我之前也踩过这个坑,折腾了两天最后发现是stdin/stdout被日志输出污染了。你检查下MCP服务器里有没有print或者logging默认输出到控制台,那会直接破坏JSON-RPC的帧边界,客户端解析不到完整的消息就会一直卡在“通道未就绪”。另外你说的asyncio.run(),其实官方Python SDK里的Server是同步接口,内部封装了异步循环,你只要正常用serve()就行,不需要自己再套一层run()。还有个小细节,如果你在Windows上跑,记得把stdin和stdout的编码改成utf-8,不然中文路径或者文件名会直接导致传输异常。我之前就是用了pathlib.Path的绝对路径,带中文就崩了,换成str强制编码就好了。如果还不行,你可以试试把MCP服务器里的所有回调函数都加上try-except,因为异常如果没被捕获,会直接导致事件循环退出,客户端那边就永远等不到响应了。
八成是asyncio事件循环没跑起来,mcp的stdio传输要求server端常驻loop,试试把run()挂到主线程。
我之前也卡这,后来发现是没处理stdin的EOF,导致通道一直半开。
我之前也卡在这过,后来发现是asyncio的loop没跑起来,光定义handler不够,得显式用asyncio.run(mcp.run())才能让stdio传输保持监听。另外你检查下客户端那边是不是提前发了请求,服务端还没ready,可以加个握手延迟或者重试机制。文件系统工具的话,记得路径权限也要给足,不然会静默失败。
这问题我踩过坑,八成是asyncio事件循环没跑起来,试试用asyncio.run(server.run()),我之前这么改就好了。
八成是没跑event loop,MCP的stdio传输强制要asyncio.run跑起来,你试试把server主逻辑包进去。
八成是事件循环没跑起来,stdio传输得靠asyncio.run()撑住主线程,不然通道肯定挂。
我之前也踩过这个坑,多半不是stdio配置的问题,而是MCP服务器里的异步事件循环没跑起来。你试试把服务器入口用asyncio.run(main())包一层,别手动去调transport的start,让框架自己管理生命周期。另外检查下工具函数里有没有阻塞调用,比如同步的文件IO,得用asyncio.to_thread包一下,不然整个loop会卡死。我上次就是栽在os.listdir上,换成异步版本就好了。
我之前也踩过类似的坑,卡住多半不是协议格式的问题,而是异步事件循环没跑起来。MCP的stdio传输本质上是靠asyncio持续读stdin的,如果你的服务器代码里用了同步的input或者没把run()挂到主循环,客户端发完请求就等不到响应了。建议直接看官方python-sdk里那个server的examples,照着抄一个最简单的filesystem实现试试,别自己封装异步逻辑。另外确认下你是不是在调用工具前就提前关闭了某个stream,那个也会导致通道未就绪。
我上周也踩过这个坑,大概率不是异步写法的问题,而是MCP的stdio握手流程里少了初始化确认。你可以试试在客户端连上后先发一条initialize请求,等返回结果再调工具,不然通道状态一直是pending。另外官方cli有时候对自定义server的启动路径很敏感,检查下是不是工作目录或者环境变量没对上。我当时卡了三天,最后发现是Python的asyncio事件循环和stdio读取线程冲突了,改用run_in_executor把阻塞IO丢线程池就通了。
八成是server没进事件循环,试试直接asyncio.run(main())包住启动逻辑,别手动调transport。
我之前也踩过这个坑,多半不是异步的问题,而是MCP的stdio通道握手时序没对上。你试试在服务器端加个显式的stdin读取循环,别让进程提前退出,或者用anyio的run来启动,比裸asyncio稳很多。另外检查下客户端那边有没有设置正确的超时参数,卡住往往就是两边都在等对方先发消息。
八成是server没进事件循环,试试在if __name__ == "__main__"里加asyncio.run(main())。
我之前也踩过这个坑,半天查不到资料,最后发现是stdio的管道缓冲问题。你确认一下是不是用了print()输出调试信息,这玩意儿会污染stdout,MCP的JSON-RPC全靠这个通道传输,一旦混进去非协议内容,客户端解析就直接卡住“未就绪”了。我后来把所有日志都改到stderr,再用logging模块定向输出,问题就解决了。
另外你说asyncio的run(),这个确实关键,但不是必须用run(),而是你的服务器实例必须在一个持续运行的事件循环里。如果你用的是mcp-python-sdk,官方示例里那个mcp.run()其实就是内部帮你起了asyncio循环,你要是自己写异步监听但没挂住loop,调用工具时协程没被调度,就会表现为“通道就绪但一调用就卡死”。可以试试把服务器实例丢到asyncio.run(main())里,或者用uvicorn这类ASGI容器跑,别手动去管理任务。
还有个容易忽略的点,就是MCP的stdio传输要求进程不退出,如果你客户端启动服务器后没保持子进程存活,或者用了with语句提前关闭了管道,也会报这个错。你可以用subprocess.Popen显式控制服务器进程生命周期,别用subprocess.run那种一次性调用。
我最后排查出来是自定义工具函数里有个同步阻塞操作,把事件循环堵死了,改成asyncio.to_thread包一下就好了。你检查下文件系统操作是不是用了os.listdir这种阻塞调用,如果数据量大,很容易让transport超时。建议把文件I/O全改成异步版本,或者用run_in_executor。
如果还是不行,试着把MCP SDK降到0.9.x版本,最新版有个已知的兼容性bug,尤其在Windows上特别明显。我环境是Python 3.11,之前升级到1.0后直接不能跑,回滚就好了。你可以先跑通官方那个memory示例,再替换成你的文件系统逻辑,这样能隔离问题是不是出在自定义代码上。
这问题我上周刚踩过一模一样的坑,折腾了两天才发现根因。你检查下MCP客户端那边是不是用了stdin/stdout做双向通信,但Python的print调试输出混进了stdout流里,直接把JSON-RPC的二进制帧给污染了,通道自然就“未就绪”了。我当时是把所有日志改成写到stderr,然后问题瞬间消失。另外,asyncio.run()确实是最稳的启动方式,但更关键的是你的transport层有没有正确实现消息边界分割,MCP用的是基于Content-Length的帧协议,跟LSP一样,如果自己手写解析很容易漏掉这个细节。建议你先用官方Python SDK里的FastMCP封装,别裸写协议,它内部已经处理好了这些坑,我换成FastMCP之后基本没再碰过传输层问题。还有个可能是你的文件系统工具用了同步阻塞操作,比如os.listdir,然后在async handler里直接调用,这会导致事件循环卡死,调用工具时就表现为“卡住”。你可以试试把文件操作丢给asyncio.to_thread去跑,保持事件循环畅通。如果还不行,把服务端日志级别调到DEBUG,看看有没有收到initialize请求,MCP握手没完成的话后续调用都会卡在等待状态。
八成是asyncio的事件循环没跑起来,试试直接用mcp.run(transport="stdio"),我之前也卡这。
我之前也踩过这个坑,多半不是异步的问题,而是MCP服务器启动后立刻开始监听,但客户端握手还没完成,你试试在serve函数里加个短暂延迟或者显式等待客户端ready事件。另外确认下用的mcp库版本,早期版本对stdio的初始化顺序很敏感,升级到最新版可能就解决了。还有个小细节,如果你在Windows上跑,asyncio的ProactorEventLoop和stdio兼容性不太好,换SelectorEventLoop试试,说不定就好了。
我之前也踩过这个坑,大概率不是你JSON-RPC格式的问题,而是MCP服务器那边的生命周期没对。官方Python SDK确实要求用asyncio.run(mcp.run())这种方式启动,不能用自己手写的loop,不然stdio通道初始化时机不对就会卡在“Transport not ready”。另外你检查下是不是没等服务器输出就绪信号就直接发请求了,客户端最好加个握手等待。我之前用fastmcp封装过文件系统工具,没碰到这问题,你可以试试那个库,省心很多。
我也踩过这个坑,八成不是stdio配置的问题,而是你服务端没把asyncio的loop跑起来。MCP的transport是基于asyncio的,得用mcp.run()或者asyncio.run()来启动,不然通道根本不会真正建立。我之前就是图省事直接调了server类的方法,结果跟你一模一样卡在调用上。你可以试试把main入口改成asyncio.run(server.run()),另外记得确认下客户端那边有没有设置超时,有时候是等太久被判定为未就绪。
我之前也踩过这个坑,搞了半天最后发现是初始化时序的问题。MCP的stdio传输其实对握手顺序很敏感,你客户端可能发完initialize请求后没等服务器返回就急着调工具了,异步逻辑里这种竞态特别常见。我当时是把服务器启动和客户端连接拆成了两个独立任务,中间加了个asyncio.sleep(0.1)强制让事件循环调度一轮,问题就消失了,你可以试试看。
另外你说的asyncio.run(),我的经验是服务器主函数必须用它来跑,但更关键的是里面的子任务要显式用asyncio.create_task()管理,别让协程对象在事件循环外裸奔。我之前写的时候就是忘了把某个回调注册成task,结果一调用工具就卡在挂起的future上。
还有个容易忽略的点,如果你用的是Windows,stdio的编码可能会干扰JSON-RPC的解析,尤其是路径里有中文时。我后来直接改成纯ASCII路径才跑通。不过你说本地目录挂载,建议先排除下是不是权限或文件锁的问题,有时候Windows Defender会短暂锁住新生成的文件句柄。
最后想确认下,你官方mcp-cli启动时是不是也报同样的错?如果连官方客户端都卡,那大概率是服务器端响应格式的问题,比如content字段没按规范包成list,或者多余的空行混进了输出流。可以往stderr打印调试信息看看。
我之前也踩过这个坑,大概率不是传输格式的问题,而是MCP服务器那边的生命周期没处理好。你检查下是不是在asyncio.run里直接创建了server对象,但没把transport的读写任务挂到事件循环上,导致握手完成后通道就断了。另外,如果用了自定义的stdio实现,记得确保stdin/stdout没有被其他日志输出污染,我之前就是print调试把JSON-RPC帧搞乱了。