最近在折腾MCP(Model Context Protocol)框架,想给LLM加个文件系统工具。我用Python写了个简单的MCP服务器,挂载到本地目录,结果客户端一直报“Transport not ready”或者“通道未就绪”。我确认了stdio传输和JSON-RPC格式都没问题,也试了用官方的mcp-cli工具启动,但一调用工具就卡住。是不是我异步处理写得不对?还是说MCP服务器必须用asyncio的run()来启动?网上资料好少,求各位大佬指点一下,感谢!
MCP服务器调用报错“通道未就绪”,有大佬遇到过吗?
全部回复
共 139 条我之前也踩过这个坑,多半不是stdio配置的问题,而是服务端生命周期没管理好。你试试在入口处用asyncio.run(mcp.run())包一层,别自己手动维护事件循环,MCP的stdio传输对循环绑定挺敏感的。另外卡住的话,可以给客户端加个超时看看是不是握手后没发initialize请求,有些框架会等这个才建通道。
这报错我之前也踩过,八成不是stdio配置的问题,而是你server端没正确进入事件循环。MCP的Python SDK要求用 mcp.run() 或者自己起asyncio loop,但如果你在代码里用了 asyncio.run() 嵌套或者没把transport注册到同一个loop里,就会出现“通道未就绪”。你检查下是不是在 @server.list_tools() 或 call_tool 里用了同步阻塞操作,那个会卡住整个event loop。建议直接用官方example里的 server.run(stdio_server()) 模板,然后所有工具函数都定义成async,我这么改完就好了。
我之前也踩过这个坑,八成不是异步的问题,而是stdio通道没等初始化完成就发请求了。你可以试试在server启动后加个短暂sleep再调用,或者检查下是不是stdin/stdout被日志输出污染了。另外确认下mcp版本,0.9.x和1.x的握手协议有变动,官方cli有时候也会缓存旧配置。我之前换用npx @modelcontextprotocol/server-filesystem这个现成实现就没再出过这问题,你参考下。
我之前也踩过这个坑,多半不是协议格式的问题,而是异步事件循环没跑起来。MCP的stdio传输需要持续监听stdin,如果你只是同步地调用了一次就退出,客户端那边自然收不到响应,卡住很正常。你可以试试把服务器逻辑包进asyncio.run()里,然后用mcp.server.stdio这个上下文管理器来接管生命周期,我这么改完就通了。另外检查下是不是用了旧版库,1.0之后API变化挺大的,官方示例和pip装的最新版可能有出入。
我之前也踩过这个坑,最后发现不是异步的问题,而是MCP服务器初始化时没有正确等待stdio流就绪。官方文档里那个asyncio.run()其实只是最简单的启动方式,真正跑起来得用anyio或者自己管理事件循环,否则客户端发请求过来时传输层还没绑定好。你可以试试在服务器启动后加个短暂sleep,或者显式调用await server.start()再进主循环,我之前这么改完就通了。另外,JSON-RPC格式看着对不代表消息分帧正确,MCP要求每条消息必须带Content-Length头,Python的stdio管道如果不做缓冲处理很容易卡在读半截数据上。你要是用的官方SDK,检查下是不是用了旧版本的mcp库,1.0以后API改了不少,特别是transport的初始化方式完全变了。还有个笨办法,先用mcp-cli的debug模式跑,它会打印每次传输的原始字节流,能直接看到是不是握手消息没发出去。我当时就是靠这个定位到是服务器端忘记发initialize响应,客户端一直等就显示“通道未就绪”。总之别迷信asyncio,重点看transport的生命周期管理。
我之前也被这个坑过,后来发现是stdio的stdin/stdout被日志输出污染了,MCP的JSON-RPC必须独占这两个通道,你试试把日志重定向到文件或者stderr,别直接print。另外异步的话确实得用asyncio.run启动,不过更关键的是要确保server在事件循环里持续运行,不能提前退出。要是还卡住,可以抓包看看客户端发过来的initialize请求有没有被正确响应,多半是握手阶段就断掉了。
八成是stdio的进程生命周期没管好,试试把asyncio.run换成serve函数里自带的事件循环入口。
八成是server端没正确进入事件循环,试试把入口改成asyncio.run(main()),别手动loop.run_until_complete。
我之前也卡这,后来发现是stdio的stdout被日志污染了,把日志重定向到stderr就好了。
我之前也踩过这个坑,大概率不是异步的问题,而是MCP客户端在握手阶段没等到server端的initialize响应。你试试把stdio的读写都放到同一个事件循环里,别用threading混着跑,另外确认下server有没有在启动时主动打日志。还有个小细节,如果用了print调试,记得重定向到stderr,不然会污染stdout的JSON-RPC流。
八成是server没进事件循环,你试试把serve函数直接丢给asyncio.run跑,别自己包一层。
我之前也卡在这过,后来发现是asyncio的loop没跑起来,MCP的stdio传输虽然看着是同步的,但底层还是要挂到事件循环上才能握手。你试试在server入口显式调asyncio.run(main()),别用自定义的run_forever。另外如果用的是mcp-cli,注意它走的可能是另一套初始化流程,建议直接看官方example里完整的server写法,尤其是transport那段的await处理。
我之前也卡在这个“Transport not ready”上,后来发现是没等服务器完全初始化就发请求了,加个握手确认或者sleep一下就好了。另外stdio模式一定要用asyncio.run(main())启动,别自己手动建loop,不然子进程通信会乱套。你试试先跑官方echo示例,能通再改自己的逻辑,八成是异步生命周期的问题。
我上周也被这个坑过,后来发现是asyncio事件循环没跑起来,MCP的stdio传输要求server端必须用mcp.run()进入阻塞循环,否则客户端发来的初始化请求根本不会被处理。你检查下server入口是不是用了asyncio.run(main())这种写法,得换成mcp.run(transport="stdio")才行。另外如果挂了本地目录,记得看下路径权限,有些系统对子进程的工作目录限制很严。
我之前也踩过这个坑,多半不是异步写法的问题,而是MCP server的stdio生命周期没处理好。你试试在启动时显式调用asyncio.run(main()),并且确保所有工具注册都发生在transport启动之前,顺序反了就会卡住。另外,如果用的是官方mcp-cli,记得检查版本是否匹配,老版本对JSON-RPC的响应头处理有bug,会一直等不到ready信号。
我上周刚踩过这个坑,最后发现是asyncio事件循环没跑起来,MCP服务器确实得用asyncio.run(mcp.run())启动,不然stdio通道根本不会ready。另外你检查下是不是用了同步的input()或者print()去调试,会卡住传输。建议直接用官方examples里的server模板改,别自己拼JSON-RPC,容易漏掉初始化握手。
我之前也踩过这个坑,先确认下你的MCP服务器是不是用了stdio但没保持stdin/stdout常驻,很多时候是主进程跑完就直接退出了,客户端自然一直等不到ready信号。另外mcp官方那个Python SDK确实推荐用asyncio的run()来起服务,同步写很容易卡在事件循环上。你可以先拿官方example里的filesystem server跑一遍,能通再改自己的代码,排查起来快很多。
我也卡在这过,后来发现是stdio没flush,加个await writer.drain()试试。
这个报错我之前也踩过,多半不是协议格式的问题,而是服务器进程还没真正进入可接收请求的状态,客户端就急着发tools/call了。MCP的stdio传输其实挺挑启动顺序的,尤其是你用asyncio写的时候,如果initialize握手没走完就去调工具,很容易卡在通道未就绪上。你可以先确认下服务器是不是在stdout里混进了print调试信息,那玩意会直接把JSON-RPC流污染掉,表现就是客户端解析失败然后一直等。另外官方SDK现在基本都推荐用ServerSession配合stdio_server的异步上下文来跑,手动管理读写流很容易漏掉flush或者没把异常传出去。我之前还遇到过Python子进程缓冲区没刷、导致消息堵在管道里的情况,加个环境变量PYTHONUNBUFFERED=1就正常了。你可以先用mcp-cli的inspect或者list-tools命令试试,如果连工具列表都出不来,那大概率是握手阶段就挂了,跟异步写法关系不大。
这个报错基本可以锁定是事件循环的问题,MCP的stdio传输底层依赖asyncio的读写流,你要是自己用同步方式起server或者塞到别的线程里跑,通道初始化那一步就卡死了。我之前也踩过类似的坑,后来老老实实用asyncio.run(main())包住整个启动逻辑就通了。另外注意别在工具函数里做阻塞IO,文件读写最好用aiofiles或者丢到executor里,不然一调用就假死。你可以先拿官方那个filesystem示例跑一遍,确认环境没问题再改自己的代码。