MCP RUNBOOK
MCP Runbook
这页是 vhttpd 的 MCP 手工验证 runbook。
目标不是覆盖 MCP 全规范,而是快速验证这条主链是否正常:
initializeMcp-Session-IdGET /mcpSSE session- queued notification
- queued
sampling/createMessage /admin/runtime/mcp- client capability snapshot
samplingsoft warning metric
Start
启动示例:
cd https://github.com/bullsoft/vphpext/tree/main/vhttpd
./vhttpd --config /vphp/vhttpd-docs/examples/config/mcp.toml
默认端口:
- data plane:
http://127.0.0.1:19895 - admin plane:
http://127.0.0.1:19995
Step 1: Initialize With Client Capabilities
curl --noproxy '*' -s \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-05' \
-H 'Origin: http://127.0.0.1:19895' \
-D /tmp/vhttpd_mcp_headers.txt \
--data-binary '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-05","capabilities":{"sampling":{},"roots":{"listChanged":true}}}}' \
http://127.0.0.1:19895/mcp | jq .
预期:
- 返回
200 result.capabilities至少包含:loggingsamplingtoolsresourcesprompts
Step 2: Extract Mcp-Session-Id
注意:头文件里当前实际写出的是小写 mcp-session-id:,提取时按实际输出匹配最稳。
SESSION_ID=$(awk '/^mcp-session-id:/ {print $2}' /tmp/vhttpd_mcp_headers.txt | tr -d '\r')
echo "$SESSION_ID"
Step 3: Verify Client Capability Snapshot
curl --noproxy '*' \
"http://127.0.0.1:19995/admin/runtime/mcp?details=1&session_id=${SESSION_ID}" | jq .
只看 capability 快照:
curl --noproxy '*' \
"http://127.0.0.1:19995/admin/runtime/mcp?details=1&session_id=${SESSION_ID}" \
| jq -r '.sessions[0].client_capabilities_json'
预期类似:
{"sampling":{},"roots":{"listChanged":true}}
Step 4: Open SSE Session
另开一个终端:
curl --noproxy '*' -N \
-H 'Origin: http://127.0.0.1:19895' \
-H "Mcp-Session-Id: ${SESSION_ID}" \
http://127.0.0.1:19895/mcp
预期先看到:
: connected
Step 5: Verify Queued Notification
curl --noproxy '*' -s \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-05' \
-H 'Origin: http://127.0.0.1:19895' \
-H "Mcp-Session-Id: ${SESSION_ID}" \
--data-binary '{"jsonrpc":"2.0","id":2,"method":"debug/notify","params":{"text":"hello mcp"}}' \
http://127.0.0.1:19895/mcp | jq .
预期:
- 这条
POST会立刻返回 - body 里是
{ "queued": true } - SSE 终端会收到:
event: message
data: {"jsonrpc":"2.0","method":"notifications/message",...}
Step 6: Verify Queued Sampling Request
curl --noproxy '*' -s \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-05' \
-H 'Origin: http://127.0.0.1:19895' \
-H "Mcp-Session-Id: ${SESSION_ID}" \
--data-binary '{"jsonrpc":"2.0","id":3,"method":"debug/sample","params":{"topic":"runtime contract"}}' \
http://127.0.0.1:19895/mcp | jq .
预期:
POST立刻返回{ "queued": true }- SSE 终端收到:
event: message
data: {"jsonrpc":"2.0","id":"sample-3","method":"sampling/createMessage",...}
Step 7: Verify Soft Warning For Missing Client Sampling Capability
先创建一个不声明 sampling 的 session:
curl --noproxy '*' -s \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-05' \
-H 'Origin: http://127.0.0.1:19895' \
-D /tmp/vhttpd_mcp_headers_no_sampling.txt \
--data-binary '{"jsonrpc":"2.0","id":11,"method":"initialize","params":{"protocolVersion":"2025-11-05","capabilities":{"roots":{"listChanged":true}}}}' \
http://127.0.0.1:19895/mcp | jq .
SESSION_ID_NO_SAMPLING=$(awk '/^mcp-session-id:/ {print $2}' /tmp/vhttpd_mcp_headers_no_sampling.txt | tr -d '\r')
echo "$SESSION_ID_NO_SAMPLING"
先看 warning 值:
curl --noproxy '*' http://127.0.0.1:19995/admin/runtime | jq '.stats.mcp_sampling_capability_warnings_total'
再触发一次 debug/sample:
curl --noproxy '*' -s \
-X POST \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-05' \
-H 'Origin: http://127.0.0.1:19895' \
-H "Mcp-Session-Id: ${SESSION_ID_NO_SAMPLING}" \
--data-binary '{"jsonrpc":"2.0","id":12,"method":"debug/sample","params":{"topic":"warning path"}}' \
http://127.0.0.1:19895/mcp | jq .
再看一次:
curl --noproxy '*' http://127.0.0.1:19995/admin/runtime | jq '.stats.mcp_sampling_capability_warnings_total'
预期:
- 第一次是
0 - 第二次变成
1
Step 8: Delete Session
curl --noproxy '*' -s \
-X DELETE \
-H 'Origin: http://127.0.0.1:19895' \
-H "Mcp-Session-Id: ${SESSION_ID}" \
http://127.0.0.1:19895/mcp | jq .
预期:
{"deleted":true}
Common Pitfalls
1. mcp-session-id 提取错了
头文件当前实际写出的是小写:
mcp-session-id: mcp_xxx
所以推荐:
awk '/^mcp-session-id:/ {print $2}'
2. GET /mcp 用了空 session id
如果 SSE 这条请求没带上正确的 Mcp-Session-Id,会看到:
400 Missing Mcp-Session-Id- 或
404 Unknown Mcp-Session-Id
3. 改了代码但没重启 vhttpd
MCP 这条线最近修过几次真实 runtime bug。改完代码后一定重启:
kill $(cat /tmp/vhttpd_mcp.pid) 2>/dev/null || true
rm -f /tmp/vhttpd_mcp.pid
rm -f /tmp/vhttpd_mcp_worker*.sock
再重新启动示例。
4. 看错 warning 字段路径
warning 字段在:
.stats.mcp_sampling_capability_warnings_total
不是顶层字段。
5. sampling policy 会影响行为
mcp.toml 现在支持:
[mcp]
sampling_capability_policy = "warn"
可选值:
warn- 允许入队
- 增加
mcp_sampling_capability_warnings_total
drop- 不入队
- 增加
mcp_sampling_capability_dropped_total
error- 直接拒绝本次
POST /mcp - 返回
409 - 增加
mcp_sampling_capability_errors_total
- 直接拒绝本次