阅读,与值得关注的内容Readance

MCP 服务怎么像 curl 一样做健康检查?一条命令查清死活

最近正在做一个MCP智能体项目,整体进入尾声,完成了功能测试和压力测试,准备部署生产环境。客户提出一个问题,有没有比较简单直接的方法,可以监控线上MCP服务状态? 我翻了翻的mcp接口测试脚本,简化了下。如果你正好需要,供参考。


原理如下:给 HTTP 服务探活,谁都会 curl 一下;MCP 服务却一直缺这么一把"curl"——直接 curl 过去,看不出死活。它跑在有状态的 JSON-RPC 协议上:得先 initialize 拿 session,再 tools/list 看看工具在不在,最后 tools/call 真调一次才算数。少一步,拿到的都是"假绿"。

最难查的就是这类故障:Agent 一直在收 error,用户无感,但业务已经崩了。

这篇就补上 MCP 缺的这一把——245 行、只依赖 requests 的检查脚本,运维、研发、测试都能上手:

  • 三层检查
    (连通性 → 可用性 → 端到端),fail-fast,一次 200ms 内出结果
  • 命令行 + JSON 双输出
    ,接进 CI 就是一个 step
  • 一张踩坑表
    :协议版本、SSE 解析、session 丢失,坑我都替你踩过了

curl 怎么用,它就怎么用:

  • 看服务在不在
    :python3 mcp_monitor.py -u http://host:8080/mcp
  • 连后端依赖一起查
    :末尾加 --full,真调一次工具验证全链路
  • 每 30 秒盯一次
    :末尾加 -i 30,Ctrl+C 停止
  • 接进 CI / 喂日志
    :加 --json,靠 exit code 判成败(0 健康 / 1 不健康)

最省事的一条是:

bash
python3 mcp_monitor.py -u http://127.0.0.1:8080/mcp --full

输出 HEALTHY 就是三层全通,exit code 0 可以直接接流水线。


一、MCP 为什么不能直接 curl

给 HTTP 服务探活,一行就搞定:

bash
curl -f http://localhost:8080/health || echo"服务挂了"

HTTP 是无状态的:发一个请求,看响应码,完事。

但 MCP 不行。它跑在 JSON-RPC 2.0 上,而且有状态:

  1. 先 initialize 握手,拿服务端返回的 Mcp-Session-Id
  2. 后续每个请求都要带上这个 session,否则 401/403
  3. 工具能不能用,得问 tools/list
  4. 工具背后的依赖(向量库、数据库、消息队列)活没活,只有真调一次才知道

所以一个能用的探针至少要覆盖这三层。端口 ping 通不算活着,那只证明 TCP 没断。


二、写之前必须知道的三个协议细节

这三个点不知道,代码一定写不对。

① 请求体固定三件套

json
{
"jsonrpc":"2.0",
"method":"initialize",
"params":{"protocolVersion":"0.1.0","capabilities":{},
"clientInfo":{"name":"mcp-monitor","version":"1.0.0"}},
"id":1
}

jsonrpc 和 id 一个都不能少——少了服务端会当成非法请求。

② 响应有两种格式,必须都兼容

MCP 服务可能返回 application/json,也可能是 text/event-stream(SSE,AI Agent 服务常用)。所以请求头要同时声明两种接受格式:

python
"Accept": "text/event-stream, application/json"

SSE 的响应体长这样,每条消息带 data: 前缀,解析时得先把前缀剥掉:

data: {"jsonrpc":"2.0","id":1,"result":{...}}

③ session 在响应头里,不在响应体里

这是最容易踩的一个坑。Mcp-Session-Id 是 header,不是 body 字段:

python
new_sid = resp.headers.get("Mcp-Session-Id") or session_id

第一版我把 session 从 body 里找,找了半小时才反应过来。


三、三层递进设计:fail-fast 是关键

┌─────────────────────────────────────────────┐
│  Layer 1: initialize(连通性)               │
│  TCP 通 + 协议握手成功 → 才算活着             │
│  失败 → 立即返回,后续检查无意义               │
└─────────────────────────────────────────────┘
              ↓ 通过,拿到 session_id
┌─────────────────────────────────────────────┐
│  Layer 2: tools/list(服务可用性)            │
│  能列出工具清单 → server 正常                  │
│  顺便打印工具名,方便排查"工具没注册"           │
└─────────────────────────────────────────────┘
              ↓ 通过
┌─────────────────────────────────────────────┐
│  Layer 3: tools/call(端到端 · 可选)         │
│  真调一个最轻量的工具,验证后端依赖全链路        │
│  需要 --full 才开(会打到后端)                │
└─────────────────────────────────────────────┘

这三层是严格递进的:Layer 1 挂了,session 都拿不到,再测 Layer 2 只是白等一个 timeout。所以 Layer 1 失败直接 return:

python
except Exception as e:
    result["checks"].append({"name": "initialize", "status": "fail", ...})
    result["healthy"] = False
return result          # ← 直接返回,不做后续检查

Layer 3 为什么单独开关? 因为它会真打到后端依赖。探活本身要轻——工具调用重、或有副作用,就别默认开,或者降低频率(30s / 60s 一次)。


四、核心代码:统一处理 JSON 和 SSE 两种响应

整个脚本的关键就是一个函数,把"发请求 + 解析响应 + 透传 session"包干净:

python
def_rpc_call(url, method, params=None, session_id=None, timeout=10):
"""发送 JSON-RPC 2.0 请求,返回 (result_dict, elapsed_ms, session_id)"""
    payload = {"jsonrpc": "2.0", "method": method,
"params": params or {}, "id": 1}
    headers = {
"Content-Type": "application/json",
"Accept": "text/event-stream, application/json",
    }
if session_id:
        headers["Mcp-Session-Id"] = session_id      # 后续请求必须带

    start = time.time()
    resp = requests.post(url, json=payload, headers=headers, timeout=timeout)
    elapsed_ms = round((time.time() - start) * 1000, 2)
    resp.raise_for_status()

    new_sid = resp.headers.get("Mcp-Session-Id") or session_id

# 两种响应格式都要兼容
if"text/event-stream"in resp.headers.get("Content-Type", ""):
for line in resp.text.strip().split("\n"):
if line.startswith("data:"):
return json.loads(line[5:]), elapsed_ms, new_sid
raise ValueError("SSE 响应中未找到 data 字段")
return resp.json(), elapsed_ms, new_sid

有了它,三层检查就是调它三次、每次换个 method。耗时顺手就算出来了——延迟本身就是监控指标。


五、跑起来

5.1 单次检查

bash
python3 mcp_monitor.py -u http://127.0.0.1:8080/mcp --full
==================================================
MCP 健康检查 ✅
==================================================
  地址: http://127.0.0.1:8080/mcp
  总延迟: 35.23 ms
  状态: HEALTHY
  ✅ initialize  (18.3 ms)
  ✅ tools/list  (14.64 ms)  tools=['memory_search']
  ✅ memory_search  (2.29 ms)
==================================================

一次 35ms,三层链路全查完。 挂掉时也一样清楚:

MCP 健康检查 ❌
  状态
:UNHEALTHY
❌ initialize  (2000 ms)  — 502 Server Error:Bad Gateway

一眼就知道是网络层还是协议层的问题。

5.2 持续监控 + JSON 输出

bash
# 每 30 秒完整检查一次,JSON 输出方便日志采集
python3 mcp_monitor.py -u http://127.0.0.1:8080/mcp -i 30 --full --json

5.3 接进 CI(靠 exit code)

bash
python3 mcp_monitor.py -u $MCP_URL --json --full
# exit 0 = 健康,1 = 不健康

放进 .gitlab-ci.yml 就是一个 step,挂了流水线直接红。


六、踩坑表:这五个坑我都踩过

坑
现象
解法
协议版本填错
连接通但 initialize 报"协议版本不兼容"
填 0.1.0,不是程序版本 1.0.0
SSE 解析失败
拿不到 data 字段
Accept
 头要同时声明 text/event-stream, application/json
第二个请求开始 401/403
只有第一个请求成功
Mcp-Session-Id
 在响应头里,后续每个请求都要带
tools/call
 报 method not found
工具明明在
先看 tools/list 返回的真实名字,可能带命名空间前缀
Layer 3 跑一次就 timeout
探活本身超时
查后端依赖:连接池、消息队列积压、索引重建中

最后一条值得展开:Layer 3 超时往往不是 MCP 服务的问题,而是它身后某个依赖在打摆子。 这正是端到端检查的价值——把"MCP 挂了"和"MCP 背后的东西挂了"区分开。


七、还能往哪扩

  • 加告警
    :用 exit code 包一层 shell,失败就发通知(连续失败 N 次再报,避免抖动误报)
  • 定时跑
    :crontab 一行,日志落文件
  • 接 Prometheus
    :把 latency / status 导出成 /metrics,进 Grafana 看趋势
  • 多实例巡检
    :shell 循环多个地址,一屏看完整个集群

先把它跑起来,再考虑扩展。这个脚本 245 行,pip install requests 就能跑。


八、写在最后

这个探针来自一个真实的 AI Agent 项目——服务经常"Agent 报 timeout,但运维看服务是绿的"。写完之后,单次 200ms 内把三层链路查清楚:网络、协议、还是后端依赖,一眼定位。

如果你也在做 MCP / AI Agent,这套三层检查思路可以直接搬;协议不是 MCP 也没关系,"递进 + fail-fast"一样适用。

建议收藏——下次 Agent 报 timeout,照着命令敲一遍,比在群里问"服务是不是挂了"快得多。

你在 MCP 探活上踩过什么坑? 比如 session 过期、SSE 流式响应解析,评论区说说,我挑几个一起看。


后台回复「源码」,获取完整配套源码包(245 行脚本 + README + FAQ,可直接跑)。


前往微信阅读全文

内容来自公众号,可前往微信查看原文。

查看作者的更多文章 →