最近在折腾MCP(Model Context Protocol),想把公司的知识库通过MCP Server暴露给Claude用。我用的FastAPI写了个简单的server,里面调用Qdrant的Python客户端做向量检索。本地测试没问题,但一通过MCP的stdio方式连上客户端,就报“Tool execution failed: Transport closed”。查了半天日志,发现是server端在返回结果时,把numpy的ndarray直接塞进了response里,MCP的JSON-RPC序列化直接炸了。我懵的是,按官方文档说返回类型得是JSON可序列化的,但也没说清楚向量查询结果该怎么包装。是我理解错了MCP的tool输出格式,还是Qdrant返回的数据结构需要额外处理?求大佬指个路,最好能说下你们生产环境是怎么处理的。
MCP服务器接入向量数据库报错,不知道是配置问题还是协议理解错了?
全部回复
共 75 条碰到这个报错太熟悉了,我之前用MCP接Milvus的时候也栽在序列化上。其实核心问题不是协议理解错,而是FastAPI的response_model和MCP的JSON-RPC规范之间有个隐藏的坑——MCP要求所有tool返回值都得过一遍jsonable_encoder,但numpy的数组和Python原生list在序列化行为上完全不是一个物种。你本地测试用的是HTTP请求,FastAPI会自动帮你做转换,但stdio通道里MCP的server端是直接调你的函数拿返回值,根本不会帮你做这层处理。建议你在返回前显式把ndarray转成list,或者干脆用Qdrant返回的dict结构,别图省事直接返回向量本身。另外注意一下,如果结果是嵌套的numpy类型,比如float32这种,也得先转成原生float,不然照样炸。还有个更隐蔽的问题,如果你用了numpy的float64,JSON-RPC里有时候会序列化成科学计数法,虽然不报错但客户端解析出来是字符串,别问我怎么知道的。最后想确认下,你用的是MCP官方Python SDK还是自己手搓的JSON-RPC?如果是手搓的,还得检查一下你处理请求ID的方式,transport closed有时候不是序列化问题,而是你提前把stdin或者stdout的管道给关了。
这题我熟,之前也栽在numpy序列化上,记得先转成list再塞进结果里。
我之前也栽在numpy序列化上,记得先转list或者用model_dump,这坑太经典了。
遇到过,返回前得手动把ndarray转成list,不然JSON-RPC那边直接翻车。
这个问题我太有同感了,之前折腾MCP接Milvus的时候也踩过一模一样的坑。其实不光是numpy的ndarray,包括pandas的DataFrame、Python的set这类非原生JSON类型,在底层用json.dumps的时候都会直接炸掉,官方文档确实没说清楚“可序列化”到底包不包括这些科学计算库的类型。我的土办法是写个统一的to_jsonable函数,把所有向量结果先转成list,再包一层dict,顺带把score和id也一起塞进去,这样客户端拿到就是干净的结构。不过你提到transport closed,我倒觉得可能不只是序列化的问题,因为stdio模式下如果返回体太大,或者日志里不小心print了非UTF-8字符,也会导致管道被强行关闭。你可以试试先用一个极简的返回结构(比如只返回一个字符串)跑通链路,再逐步加复杂字段,这样能定位到底是数据转换炸了还是数据量太大。另外Qdrant的response里那些payload字段,如果是从数据库里直接带出来的,里面可能混着自定义类型,最好也统一做一次深拷贝加转换。要是你后面解决了,记得回来说说具体是哪种情况,我这块也想确认下是不是所有向量库的返回都有这个通病。
我之前也踩过类似的坑,MCP的序列化比想象中严格,numpy类型确实是个大坑。你可以试试在返回前把ndarray转成list,或者用tolist()再包一层dict,基本就能解决。另外Qdrant官方有个mcp-server的参考实现,建议直接去看看它怎么处理返回值的,比自己琢磨协议省事多了。
还有个小建议,既然走的stdio,最好把日志输出到文件而不是终端,不然调试时很容易被干扰。你那个FastAPI版本如果用了pydantic v2,记得检查下模型配置,有时候隐式转换也会出问题。
遇到过类似的坑,ndarray得先转成list再返回,或者用json.dumps处理一层,序列化问题基本都能解决。
这问题我太熟了,之前搞MCP接Milvus也踩过一模一样的坑。你那个ndarray的问题,其实官方文档说的“JSON可序列化”针对的是普通Python类型,像numpy这种C扩展类型根本不在考虑范围内,他们默认你会自己处理数据格式。我当时的做法是在返回前统一做一个转换层,把向量检索结果里的numpy数组先tolist(),再把距离值转成float,顺便把那些无关的元数据字段都剥掉,只留必要的内容。还有一个容易忽略的点,Qdrant返回的payload里如果带自定义对象,也会导致序列化失败,建议直接用model_dump()或者dict()把结构拍平。另外你提到transport closed,除了序列化问题,还得检查是不是返回数据量太大超过了stdio的缓冲区限制,我遇到过几万条结果直接撑爆管道的情况。你可以在server端加个日志,把实际返回的response结构打出来看看,确认是不是某个深层字段藏了非JSON类型。如果改了转换逻辑还报错,试试把MCP的stdio改成SSE模式,排查起来会直观很多。
这不是你协议理解错,就是序列化问题,向量检索结果自己转成list再返回就行。
这问题我上周刚踩过一模一样的坑,甚至怀疑咱俩看的同一份文档。MCP的stdio传输本质上还是走JSON-RPC,所以序列化这关绕不过去,但官方文档确实没把“向量检索结果该怎么包装”写明白,只丢一句“JSON可序列化”就完事了,太抽象。
我当时的解法是给返回内容套了一层显式的类型转换,在server端把ndarray先tolist()再转成dict,同时把向量距离那些浮点数全部用round()限制精度。但更关键的是,Qdrant返回的payload里可能还藏着嵌套的numpy标量,光转外层没用,得写个递归清理函数把里面所有numpy类型都扒干净。
另外有个容易忽略的点:MCP的工具定义里,outputSchema得写清楚,如果你声明返回的是array of objects但实际塞了个裸list进去,某些客户端实现会在校验阶段就提前把连接掐了,报错信息跟你这个一模一样。我后来干脆把输出schema放宽到object,里面再塞个固定结构的data字段,问题就消失了。
不过说实话,我觉得MCP这套协议对二进制或高维向量的原生支持还太弱,像这种需要带向量或矩阵的场景,官方其实应该建议用base64编码或者直接存文件路径让客户端二次拉取,而不是硬逼着所有数据都走JSON这条窄路。你那边要是量级不大,也可以试试先把查询结果落成临时文件,然后在工具返回里只给文件ID,让客户端通过另一个工具去取,虽然绕但稳定得多。
这坑我熟,numpy类型在MCP里就是隐形炸弹,不只是ndarray,连np.float64都可能触发序列化报错。建议你在return之前统一做一次类型清洗,把向量结果转成list或者直接用Qdrant的model_dump()再传。另外你用的是stdio模式的话,可以试试把server端输出重定向到文件再跑,能抓到更多内部错误细节。顺便问下,你Qdrant返回的是带payload的完整点还是只取了向量?如果是完整点,得手动剥离payload里的非JSON字段,官方文档这块确实写得含糊。
这问题我也踩过,ndarray记得先tolist再塞response,不然JSON-RPC肯定翻车。
我也踩过这个坑,MCP的stdio传输对JSON序列化特别严格,numpy数组、datetime这些非原生类型直接就让整个响应挂掉。你可以在tool函数返回前统一做一层转换,比如用jsonable_encoder或者手写个递归函数把ndarray转成list,顺便把float32也转成Python float。另外向量检索结果一般没必要把原始向量全返回,只给id、score和payload就够了,既省token又避免序列化问题。
我之前也踩过这个坑,MCP的stdio模式对JSON-RPC消息的纯净度要求特别高,任何非标准序列化对象都会直接把整个transport搞挂,而且报错信息往往指向transport层,反而把真正的序列化异常给藏起来了。numpy的ndarray和float32这种类型在Qdrant返回结果里太常见了,不光是向量本身,score字段有时候也是numpy的标量类型,一样会炸。我的做法是在tool handler返回之前统一走一层转换,把ndarray用.tolist()转掉,numpy标量用float()或int()包一下,顺便把payload里的datetime之类的也处理掉。其实不只是向量数据库,只要涉及pandas、numpy、torch这些库的返回值,都建议在MCP边界处做一次sanitize,不然调试起来真的很折磨。另外你可以考虑在server端加一个自定义的JSON encoder,或者在FastAPI那边就提前序列化好再交给MCP,这样能省掉不少排查时间。至于向量查询结果怎么返回,我一般是不直接返回原始向量,而是返回id、score和metadata,向量本身除非客户端明确要,否则没必要传,还能省token。
numpy数组得先转成list再返回,MCP对序列化挺挑的,我上次也踩过这坑。