从零手写一个MCP Agent服务:stdio与SSE两种连接模式的踩坑实录

从零手写一个MCP Agent服务:stdio与SSE两种连接模式的踩坑实录

上周我在给公司的内部知识库做一个Agent入口,需要把几个自研工具(文档检索、SQL查询、发通知)暴露给Claude Desktop和自建的Agent框架同时使用。本来想直接抄官方示例了事,结果发现一旦涉及"同一个MCP服务要同时被本地客户端和远程Agent调用"这个需求,stdio和SSE的选型就没那么简单了。这篇记录一下我踩过的具体坑。

一、先搞清楚MCP到底在传什么

MCP(Model Context Protocol)本质是JSON-RPC 2.0的一层约定。客户端和服务端之间交换三类消息:请求(带id)、响应(带对应id)、通知(无id)。工具注册走的是tools/list,调用走tools/call

理解这一点很重要,因为後面stdio和SSE的差异,本质上只是"这层JSON-RPC消息怎么在传输层流动"的差异。

二、stdio模式:本地跑起来最省事,但有个致命细节

stdio模式下,MCP服务是一个被客户端作为子进程启动 的可执行程序,双方通过标准输入输出通信。协议规定:一行一条JSON-RPC消息

我第一版用的是Python的mcp库,按文档写了大概这样:

python 复制代码
from mcp.server import Server
from mcp.server.stdio import stdio_server
import mcp.types as types

app = Server("kb-server")

@app.list_tools()
async def list_tools():
    return [
        types.Tool(
            name="search_docs",
            description="搜索内部知识库文档",
            inputSchema={
                "type": "object",
                "properties": {"query": {"type": "string"}},
                "required": ["query"],
            },
        )
    ]

@app.call_tool()
async def call_tool(name, arguments):
    if name == "search_docs":
        result = await do_search(arguments["query"])
        return [types.TextContent(type="text", text=result)]

async def main():
    async with stdio_server() as (read, write):
        await app.run(read, write, app.create_initialization_options())

看起来没问题,但我踩的第一个坑是日志 。我在工具函数里随手写了个print(f"query={query}"),结果客户端直接报JSON解析错误。原因是stdout是协议通道,任何非JSON输出都会污染它。所有调试输出必须走stderr ,也就是print(..., file=sys.stderr)或者用logging配置到stderr。这个坑我查了半小时才反应过来。

第二个坑是进程生命周期 。stdio模式下服务由客户端拉起,客户端退出服务就没了。我一开始想在里面维护一个长连接的向量库客户端,结果每次重启都要重新建连,冷启动能到两三秒。后来改成在on_initialized回调里做预热,才勉强压到800ms左右。

三、SSE模式:能远程,但会话隔离要自己管

因为还有一部分Agent跑在服务器上,不可能让它去启动本地进程,所以我又加了一个SSE入口。

SSE模式的结构是:客户端先GET /sse建立一个长连接,服务端通过这条连接推送消息,同时返回一个endpoint事件告诉客户端往哪个URL发POST请求。之后的JSON-RPC请求都用POST发过去。

用mcp库大致这样:

python 复制代码
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route, Mount

sse = SseServerTransport("/messages/")

async def handle_sse(request):
    async with sse.connect_sse(
        request.scope, request.receive, request._send
    ) as streams:
        await app.run(streams[0], streams[1], app.create_initialization_options())
    return Response()

starlette_app = Starlette(
    routes=[
        Route("/sse", endpoint=handle_sse),
        Mount("/messages/", app=sse.handle_post_message),
    ]
)

这里我踩了最惨的一个坑 :一开始我把app(Server实例)定义成全局单例,想着所有连接共享。结果多个客户端同时连进来时,会话状态互相串了------A客户端的初始化参数会被B客户端看到,tools/call的返回偶尔发错连接。

翻源码才明白:每个SSE连接必须对应一个独立的Server会话 。正确做法是在handle_sse里为每个连接创建一个新的会话实例,或者至少保证create_initialization_options()返回的状态是per-connection的。我改了之后并发测试5个客户端才稳定。

另一个坑是代理超时 。我们前面挂了nginx,默认proxy_read_timeout是60秒。SSE连接空闲超过60秒就被nginx掐断,客户端重连时又拿不到原来的session id,工具调用直接失败。解决办法是在nginx配置里针对/sse路径设proxy_read_timeout 3600s并关闭proxy_buffering,同时在服务端每30秒发一个心跳注释行:\n\n保活。

四、两种模式到底怎么选

结合这次的实际使用,我的判断标准是这样的:

选stdio:客户端和服务在同一台机器、服务是本地工具(文件操作、本地数据库)、不关心多客户端并发。优点是零网络配置、天然进程隔离、安全性好(不暴露端口)。

选SSE:服务要远程部署、要被多个Agent或用户共享、需要集中做鉴权和限流。代价是必须自己处理会话隔离、心跳保活、反向代理配置这些事。

我最后的方案是同一份工具逻辑,两个入口tools/目录放纯业务函数,stdio_server.pysse_server.py分别只做传输层封装。这样改工具逻辑只改一处,两个入口都不会漏。

五、几个容易忽略的点

  1. 错误返回要用isError: true,而不是抛异常。抛异常客户端收到的是协议层错误,Agent无法把它当成工具执行结果来处理,会直接中断对话。
  2. tools/list返回的inputSchema要写全 ,尤其是required。我第一次漏了,Claude调用时经常传空参数进来。
  3. SSE的session id不要复用。断线重连要当成新会话,旧会话的资源(比如数据库连接)要显式清理,否则连接池会被耗光。
  4. stdio下不要用asyncio.run嵌套 。mcp库自己管事件循环,你在工具函数里再起一个loop会直接报RuntimeError: This event loop is already running

这次做完最大的感受是:MCP的协议本身不复杂,真正花时间的是传输层的那些"工程细节"------日志通道、会话隔离、超时保活。这些在示例代码里都不会写,但线上跑起来一个都躲不掉。

相关推荐
SpiderCodeJ1 小时前
【UE5】- UE MCP :在UE5.8编辑器中内置链接Codex
ue5·codex·智能体·mcp
多云行者1 小时前
什么是LLM Gateway?定义、技术栈与落地方式详解
网关·llm·gateway·api·传统
Java的搬运工1 小时前
Agent 记忆系统难在取舍
agent
YDS8291 小时前
AI Agent 脚手架 —— Service层和Trigger层接口实现
ai·agent·spring ai
能不能静下心来看2 小时前
手搓三种 Agent 范式后,一次翻车让我看穿了它的本质
agent
星野云联AIoT技术洞察2 小时前
物联网平台源码交付、私有化部署和 SaaS 怎么选:先算清责任与退出成本
llm·私有化部署·saas·dify·物联网平台·iot平台·设备管理平台
小年糕是糕手2 小时前
【AI】中国 AI:从跟随,到并肩
ai·chatgpt·agent·codex·deepseek
DolphinScheduler社区2 小时前
Apache DolphinScheduler 3.4.3 发布!权限安全与稳定性全面增强,调度补火即将上线
开源·agent·海豚调度·大数据工作流调度
prog_61032 小时前
【笔记】用agent手搓agent(一)
人工智能·llm·大语言模型·agent