跨协议时代迁移:从 2025-11-25 到 2026-07-28
2026-07-28 修订让 MCP 变成无状态协议:没有 initialize 握手,用 server/discover 取代协商出的会话,并且每个请求都必须携带请求级元数据。如果你的客户端要同时对接 2025-11-25 服务器和 2026-07-28 服务器,本页用一个 echo 服务器把全部差异走一遍——先走正例,再对每条规则做单变量负例。
本页是什么。 下面每条命令都真实运行于本页内嵌的最小服务器——仅用 Python 标准库、合成数据、仅监听 loopback、只有一个
echo工具。它是教学夹具,不是协议实现:只强制示例所需的请求形态(镜像头部、params._meta、input_required续传、elicitation 能力检查),不包含鉴权、会话、流、缓存或x-mcp-header参数镜像。规范正文见 Streamable HTTP 传输、MRTR 与 tools。
每个现代请求必须携带什么
传输规范把选定的 body 字段镜像到 HTTP 头,tools 规范则要求每个请求带 _meta。因此一个合规的 2026-07-28 请求携带:
- 每个 POST 都带
Accept: application/json, text/event-stream。 MCP-Protocol-Version: 2026-07-28——且该头必须等于params._meta.io.modelcontextprotocol/protocolVersion。- 每个请求都带
Mcp-Method: <method>,镜像 body 的method。 tools/call、resources/read、prompts/get另带Mcp-Name——tools/call与prompts/get镜像params.name,而resources/read镜像的是params.uri。params._meta携带io.modelcontextprotocol/protocolVersion、io.modelcontextprotocol/clientInfo与io.modelcontextprotocol/clientCapabilities。如果请求会让服务器向用户发表单,还必须声明clientCapabilities.elicitation.form。
服务器用 schema 自己的错误结构回应违规,各归其层:
| 违规 | HTTP | JSON-RPC |
|---|---|---|
Accept 缺一种媒体类型 | 406 | 纯 HTTP 拒绝,先于 JSON-RPC 处理 |
| body 不是合法 JSON | 400 | -32700 Parse error |
| 顶层不是 JSON-RPC 请求对象 | 400 | -32600 Invalid Request |
params / _meta / arguments / inputResponses 类型错误或缺必需字段 | 400 | -32602 Invalid params |
标准头(MCP-Protocol-Version、Mcp-Method、Mcp-Name)缺失或与 body 不匹配 | 400 | -32020 HeaderMismatch |
头与 _meta 一致,但落在服务器不实现的版本上 | 400 | -32022 UnsupportedProtocolVersion,含 error.data.supported / error.data.requested |
| 服务器需要客户端未声明的 elicitation 表单 | 400 | -32021 MissingRequiredClientCapability,含 error.data.requiredCapabilities |
未知 RPC 方法——包括 initialize | 404 | -32601 Method not found |
成功结果同样遵守 schema:DiscoverResult 必需 resultType、supportedVersions、capabilities、ttlMs、cacheScope;ListToolsResult 必需 tools、resultType、ttlMs、cacheScope;CallToolResult 必需 resultType 与 content(isError 标记工具级失败)。需要更多输入的请求以 resultType: "input_required" 应答,进入多轮往返续传。
服务器
把下面的文件保存为 era_stub.py。它在你指定的端口上应答 /mcp,接受一个参数:legacy 表现如 2025-11-25 服务器,modern 表现如 2026-07-28 服务器,input-required 同 modern,但缺少 text 参数的 tools/call 应答 input_required 并期待规范形态的续传。
"""Stdlib HTTP stub for the Inspector checks: http://127.0.0.1:PORT/mcp
stub_server.py PORT MODE [--log FILE]
MODE:
legacy plain 2025-11-25 server: initialize, tools/list (one tool), tools/call; no server/discover
bad-schema like legacy, but the tool schema has a bare boolean, a type array and an empty schema
warn-schema like legacy, but the tool schema has only a type array (a portability warning)
no-capability like legacy, but initialize does not advertise capabilities.tools
auth 401 with WWW-Authenticate on every request
forbidden 403 on every request
not-found 404 on every request
error-500 500 on every request
non-json 200 with an HTML body
modern plain 2026-07-28 stateless server: server/discover, tools/list, tools/call;
no initialize (404 + -32601). Enforces the request rules the era requires:
Accept content types, mirrored Mcp-Method/Mcp-Name/MCP-Protocol-Version headers
(HeaderMismatch -32020), a supported protocol version (-32022 with
data.supported/requested), the params._meta fields, and object types everywhere,
answering ParseError/InvalidRequest/InvalidParams for malformed bodies.
input-required like modern, but tools/call without the text parameter answers an
InputRequiredResult (elicitation form). Only clients that declare
elicitation form support get the form (MissingRequiredClientCapability -32021
otherwise); retries carry inputResponses plus the returned requestState.
--log FILE records the JSON-RPC method of every POST, one per line, in arrival order.
"""
import json
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
MODES = ("legacy", "bad-schema", "warn-schema", "no-capability", "auth", "forbidden", "not-found", "error-500", "non-json",
"modern", "input-required")
MODERN_MODES = ("modern", "input-required")
ECHO = {"name": "echo", "description": "Echo text.",
"inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}
BAD = {"name": "flag", "description": "Tool with schemas that are hard to port.",
"inputSchema": {"type": "object",
"properties": {"x": True, "y": {"type": ["string", "null"]}, "z": {}}}}
MAYBE = {"name": "maybe", "description": "Tool with a nullable field.",
"inputSchema": {"type": "object", "properties": {"note": {"type": ["string", "null"]}}}}
TOOLS = {"legacy": ECHO, "bad-schema": BAD, "warn-schema": MAYBE}
VERSION = "2026-07-28"
VKEY = "io.modelcontextprotocol/protocolVersion"
META_PROTO = {"io.modelcontextprotocol/protocolVersion": VERSION}
def parse_args(argv):
"""Return (port, mode, log_path) or exit with status 2."""
log = None
if len(argv) == 4 and argv[2] == "--log":
log = argv[3]
argv = argv[:2]
if len(argv) == 2 and argv[0].isdigit() and argv[1] in MODES:
return int(argv[0]), argv[1], log
sys.stderr.write(__doc__)
sys.exit(2)
PORT, MODE, LOG_PATH = parse_args(sys.argv[1:])
if LOG_PATH:
open(LOG_PATH, "w").close()
class Handler(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *args):
"""Silence the default access log."""
def reply(self, status, body, content_type="application/json", headers=()):
data = body.encode()
self.send_response(status)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(data)))
for name, value in headers:
self.send_header(name, value)
self.end_headers()
self.wfile.write(data)
def rpc_error(self, status, code, msg, data=None, request_id=None):
error = {"code": code, "message": msg}
if data is not None:
error["data"] = data
return self.reply(status, json.dumps({"jsonrpc": "2.0", "id": request_id, "error": error}))
def reply_modern(self, message, parse_ok, method, request_id):
"""Answer as a 2026-07-28 stateless server.
Validation order: Accept content types, JSON body framing, then the
era's request rules -- MCP-Protocol-Version header vs params._meta
(HeaderMismatch -32020), supported version (-32022 with data),
params._meta fields and object types (Invalid params -32602),
Mcp-Method/Mcp-Name mirrors (-32020), then 404 + -32601 for unknown
methods, initialize included.
"""
accept = self.headers.get("Accept", "")
if "application/json" not in accept or "text/event-stream" not in accept:
return self.reply(406, json.dumps({"error": "not acceptable",
"detail": "POST requires Accept: application/json, text/event-stream"}))
if parse_ok is False:
return self.rpc_error(400, -32700, "Parse error: request body is not valid JSON", request_id=request_id)
if not isinstance(message, dict):
return self.rpc_error(400, -32600, "Invalid Request: body must be a single JSON-RPC request object",
request_id=request_id)
if not isinstance(method, str) or not method:
return self.rpc_error(400, -32600, "Invalid Request: method must be a non-empty string", request_id=request_id)
params = message.get("params", {})
if not isinstance(params, dict):
return self.rpc_error(400, -32602, "Invalid params: params must be an object", request_id=request_id)
header_version = self.headers.get("MCP-Protocol-Version")
if not header_version:
return self.rpc_error(400, -32020, "Header mismatch: MCP-Protocol-Version header is required",
request_id=request_id)
meta = params.get("_meta")
if not isinstance(meta, dict):
return self.rpc_error(400, -32602, "Invalid params: params._meta is required and must be an object",
request_id=request_id)
body_version = meta.get(VKEY)
if not isinstance(body_version, str):
return self.rpc_error(400, -32602,
f"Invalid params: params._meta.{VKEY} is required and must be a string",
request_id=request_id)
if header_version != body_version:
return self.rpc_error(400, -32020,
"Header mismatch: MCP-Protocol-Version header does not match params._meta",
request_id=request_id)
if header_version != VERSION:
return self.rpc_error(400, -32022, "Unsupported protocol version",
data={"supported": [VERSION], "requested": header_version}, request_id=request_id)
client_info = meta.get("io.modelcontextprotocol/clientInfo")
if not isinstance(client_info, dict):
return self.rpc_error(400, -32602,
"Invalid params: params._meta.io.modelcontextprotocol/clientInfo is required "
"and must be an object", request_id=request_id)
client_caps = meta.get("io.modelcontextprotocol/clientCapabilities")
if not isinstance(client_caps, dict):
return self.rpc_error(400, -32602,
"Invalid params: params._meta.io.modelcontextprotocol/clientCapabilities is required "
"and must be an object", request_id=request_id)
header_method = self.headers.get("Mcp-Method")
if not header_method or header_method != method:
return self.rpc_error(400, -32020,
"Header mismatch: Mcp-Method header is required and must match the body method",
request_id=request_id)
if method == "tools/call":
header_name = self.headers.get("Mcp-Name")
if not header_name or header_name != params.get("name"):
return self.rpc_error(400, -32020,
"Header mismatch: Mcp-Name header is required and must match params.name",
request_id=request_id)
if method == "initialize" or method not in ("server/discover", "tools/list", "tools/call"):
hint = ("Method not found. This server implements the 2026-07-28 stateless protocol; "
"send server/discover instead of initialize.") if method == "initialize" else "Method not found"
return self.rpc_error(404, -32601, hint, request_id=request_id)
if method == "server/discover":
result = {"resultType": "complete", "supportedVersions": [VERSION],
"capabilities": {"tools": {}}, "ttlMs": 0, "cacheScope": "private",
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "stub-modern", "version": "0"}},
"instructions": "Echo server for the protocol-era examples."}
elif method == "tools/list":
result = {"tools": [ECHO], "resultType": "complete", "ttlMs": 0, "cacheScope": "private",
"_meta": dict(META_PROTO)}
else: # tools/call
name = params.get("name")
if not isinstance(name, str) or not name:
return self.rpc_error(400, -32602, "Invalid params: params.name must be a non-empty string",
request_id=request_id)
if name != "echo":
return self.rpc_error(400, -32602, f"Invalid params: unknown tool {name!r}", request_id=request_id)
arguments = params.get("arguments", {})
if not isinstance(arguments, dict):
return self.rpc_error(400, -32602, "Invalid params: arguments must be an object",
request_id=request_id)
text = arguments.get("text")
if text is not None and not isinstance(text, str):
return self.rpc_error(400, -32602, "Invalid params: arguments.text must be a string",
request_id=request_id)
if text is None and MODE != "input-required":
return self.rpc_error(400, -32602, "Invalid params: arguments.text is required",
request_id=request_id)
if text is None:
result = self.reply_input_required(params, client_caps, request_id)
if result is None:
return None
else:
result = {"resultType": "complete", "content": [{"type": "text", "text": text}],
"isError": False, "_meta": dict(META_PROTO)}
self.reply(200, json.dumps({"jsonrpc": "2.0", "id": request_id, "result": result}))
def require_elicitation(self, client_caps, request_id):
"""Reply with MissingRequiredClientCapability unless the client declared form support.
Returns True when an error reply was sent, False when the form may be sent.
"""
elicitation = client_caps.get("elicitation") if isinstance(client_caps, dict) else None
if isinstance(elicitation, dict) and isinstance(elicitation.get("form"), dict):
return False
self.rpc_error(400, -32021, "Server requires the elicitation capability for this request",
data={"requiredCapabilities": {"elicitation": {"form": {}}}}, request_id=request_id)
return True
def reply_input_required(self, params, client_caps, request_id):
"""Handle the input-required continuation for a missing text argument.
Returns the result dict, or None after an error reply was sent.
"""
responses = params.get("inputResponses")
state = params.get("requestState")
if responses is not None and not isinstance(responses, dict):
self.rpc_error(400, -32602, "Invalid params: inputResponses must be an object", request_id=request_id)
return None
if state is not None and not isinstance(state, str):
self.rpc_error(400, -32602, "Invalid params: requestState must be a string", request_id=request_id)
return None
if not responses and state is None:
# First attempt: ask for the missing argument with an elicitation form.
if self.require_elicitation(client_caps, request_id):
return None
return self.input_required_result()
if state != "stub-1":
self.rpc_error(400, -32602,
"Invalid params: retry must carry the requestState returned with input_required",
request_id=request_id)
return None
if not responses:
# Retry carried the state but no answers: ask again, per MRTR error handling.
if self.require_elicitation(client_caps, request_id):
return None
return self.input_required_result()
entry = responses.get("text")
if not isinstance(entry, dict):
self.rpc_error(400, -32602, "Invalid params: inputResponses.text must be an ElicitResult object",
request_id=request_id)
return None
action = entry.get("action")
if action == "accept":
content = entry.get("content")
if not isinstance(content, dict) or not isinstance(content.get("text"), str):
self.rpc_error(400, -32602,
"Invalid params: inputResponses.text.content.text must be a string",
request_id=request_id)
return None
return {"resultType": "complete", "content": [{"type": "text", "text": content["text"]}],
"isError": False, "_meta": dict(META_PROTO)}
if action in ("decline", "cancel"):
# Legal user action: the tool is simply not executed. Business outcome,
# not a protocol error (isError marks the tool-level failure).
verb = "declined" if action == "decline" else "cancelled"
return {"resultType": "complete", "isError": True,
"content": [{"type": "text",
"text": f"echo not executed: user {verb} the elicitation form"}],
"_meta": dict(META_PROTO)}
self.rpc_error(400, -32602,
"Invalid params: inputResponses.text.action must be accept, decline or cancel",
request_id=request_id)
return None
@staticmethod
def input_required_result():
"""Return the InputRequiredResult for the missing text argument."""
return {"resultType": "input_required", "requestState": "stub-1",
"inputRequests": {"text": {"method": "elicitation/create",
"params": {"mode": "form",
"message": "Provide the text to echo",
"requestedSchema": {"type": "object",
"properties": {"text": {"type": "string"}},
"required": ["text"]}}}}}
def do_POST(self):
length = int(self.headers.get("Content-Length", 0))
raw = self.rfile.read(length)
parse_ok = True
message = None
try:
message = json.loads(raw)
except ValueError:
parse_ok = False
if parse_ok and not isinstance(message, dict):
parse_ok = "shape"
method = message.get("method") if isinstance(message, dict) else None
request_id = message.get("id") if isinstance(message, dict) else None
if LOG_PATH:
with open(LOG_PATH, "a") as log:
log.write(f"{method}\n")
if MODE == "auth":
return self.reply(401, '{"error":"unauthorized"}', headers=[("WWW-Authenticate", 'Bearer realm="mcp"')])
if MODE == "forbidden":
return self.reply(403, '{"error":"forbidden"}')
if MODE == "not-found":
return self.reply(404, "not found", "text/plain")
if MODE == "error-500":
return self.reply(500, "boom", "text/plain")
if MODE == "non-json":
return self.reply(200, "<html><body>Sign in</body></html>", "text/html")
if MODE in MODERN_MODES:
return self.reply_modern(message, parse_ok, method, request_id)
if parse_ok is not True:
code, msg = (-32700, "Parse error: request body is not valid JSON") if parse_ok is False else \
(-32600, "Invalid Request: body must be a single JSON-RPC request object")
return self.rpc_error(400, code, msg)
if request_id is None:
return self.reply(202, "")
if method == "initialize":
capabilities = {} if MODE == "no-capability" else {"tools": {}}
result = {"protocolVersion": "2025-11-25", "capabilities": capabilities,
"serverInfo": {"name": "stub", "version": "0"}}
elif method == "tools/list":
result = {"tools": [TOOLS.get(MODE, ECHO)]}
elif method == "tools/call":
result = {"content": [{"type": "text", "text": "hi"}], "isError": False}
else:
error = {"code": -32601, "message": "Method not found"}
return self.reply(200, json.dumps({"jsonrpc": "2.0", "id": request_id, "error": error}))
self.reply(200, json.dumps({"jsonrpc": "2.0", "id": request_id, "result": result}))
if __name__ == "__main__":
ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()启动三个服务器
在你自己的终端(bash 或 zsh)运行。trap 只杀本片段启动的三个 $! PID,不会碰你已有的后台任务;退出 shell(exit 或 Ctrl-D)时触发清理。
python3 era_stub.py 8811 modern & SP1=$!
python3 era_stub.py 8812 input-required & SP2=$!
python3 era_stub.py 8813 legacy & SP3=$!
trap 'kill $SP1 $SP2 $SP3 2>/dev/null' EXIT
sleep 1请求反复复用的唯一变量
下面的每条请求都完整内联自己的全部头,因此每条命令在 bash 与 zsh 中都可以直接复制运行——不依赖未加引号变量的词拆分。唯一的变量是 $META(params._meta 对象),它始终在双引号 JSON 字符串内部插值,两种 shell 对此处理完全相同。能力里声明了 elicitation.form,因为 input-required 服务器会发送 elicitation 表单。
META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{"form":{}}}}'1. 旧版服务器以 initialize 握手开场
端口 8813 是 2025-11-25 形态:先发 initialize 请求协商,之后工具流量照常——没有 _meta,也没有镜像头。
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8813/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'{"jsonrpc": "2.0", "id": 1, "result": {"protocolVersion": "2025-11-25", "capabilities": {"tools": {}}, "serverInfo": {"name": "stub", "version": "0"}}}
[HTTP 200]2. 现代服务器没有握手,从 server/discover 开始
同一个 URL 在端口 8811 上说 2026-07-28。initialize 已消失——时代从 server/discover 开始:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: server/discover' \
-d "{\"jsonrpc\":\"2.0\",\"id\":\"d1\",\"method\":\"server/discover\",\"params\":{$META}}"{"jsonrpc": "2.0", "id": "d1", "result": {"resultType": "complete", "supportedVersions": ["2026-07-28"], "capabilities": {"tools": {}}, "ttlMs": 0, "cacheScope": "private", "_meta": {"io.modelcontextprotocol/serverInfo": {"name": "stub-modern", "version": "0"}}, "instructions": "Echo server for the protocol-era examples."}}
[HTTP 200]DiscoverResult 必需 resultType、supportedVersions、capabilities、ttlMs、cacheScope——cacheScope: "private" 表示本教学夹具不提供共享缓存。
把旧握手发进新时代会得到 method-not-found 形态的失败——注意 JSON-RPC 正文,纯 HTTP 404 不会有:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: initialize' \
-d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"initialize\",\"params\":{$META}}"{"jsonrpc": "2.0", "id": 2, "error": {"code": -32601, "message": "Method not found. This server implements the 2026-07-28 stateless protocol; send server/discover instead of initialize."}}
[HTTP 404]tools/list 返回 schema 必需字段——tools、resultType、ttlMs、cacheScope:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/list\",\"params\":{$META}}"{"jsonrpc": "2.0", "id": 7, "result": {"tools": [{"name": "echo", "description": "Echo text.", "inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}], "resultType": "complete", "ttlMs": 0, "cacheScope": "private", "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}
[HTTP 200]tools/call 把 params.name 镜像进 Mcp-Name:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: echo' \
-d "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{\"text\":\"hello era\"},\"_meta\":${META#\"_meta\":}}}"{"jsonrpc": "2.0", "id": 8, "result": {"resultType": "complete", "content": [{"type": "text", "text": "hello era"}], "isError": false, "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}
[HTTP 200]3. 现代服务器可以在请求中途索要输入
端口 8812 加入多轮往返请求流程。不带 text 调用 echo 并不会失败——因为请求声明了 clientCapabilities.elicitation.form,服务器以带 elicitation 表单的 input_required 应答:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8812/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: echo' \
-d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{},\"_meta\":${META#\"_meta\":}}}"{"jsonrpc": "2.0", "id": 3, "result": {"resultType": "input_required", "requestState": "stub-1", "inputRequests": {"text": {"method": "elicitation/create", "params": {"mode": "form", "message": "Provide the text to echo", "requestedSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}}}}}
[HTTP 200]续传是一次新的 tools/call:换一个 JSON-RPC id,把原 requestState 原样带回,并在 inputResponses 里携带表单的 ElicitResult:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8812/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: echo' \
-d "{\"jsonrpc\":\"2.0\",\"id\":32,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{},\"inputResponses\":{\"text\":{\"action\":\"accept\",\"content\":{\"text\":\"hello era\"}}},\"requestState\":\"stub-1\",\"_meta\":${META#\"_meta\":}}}"{"jsonrpc": "2.0", "id": 32, "result": {"resultType": "complete", "content": [{"type": "text", "text": "hello era"}], "isError": false, "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}
[HTTP 200]不带 requestState 的续传,或带一个服务器从未下发过的 state,是 Invalid params——而不是一次新的 input_required:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8812/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: echo' \
-d "{\"jsonrpc\":\"2.0\",\"id\":30,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{},\"inputResponses\":{\"text\":{\"action\":\"accept\",\"content\":{\"text\":\"hello era\"}}},\"_meta\":${META#\"_meta\":}}}"{"jsonrpc": "2.0", "id": 30, "error": {"code": -32602, "message": "Invalid params: retry must carry the requestState returned with input_required"}}
[HTTP 400]ElicitResult.action 只有 "accept"、"decline"、"cancel" 三个合法值——它是用户的回答,不是协议参数。用户拒绝或取消是合法的:工具只是不执行,服务器把它作为业务结果上报(complete 且 isError: true),而不是协议错误:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8812/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: echo' \
-d "{\"jsonrpc\":\"2.0\",\"id\":34,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{},\"inputResponses\":{\"text\":{\"action\":\"decline\"}},\"requestState\":\"stub-1\",\"_meta\":${META#\"_meta\":}}}"{"jsonrpc": "2.0", "id": 34, "result": {"resultType": "complete", "isError": true, "content": [{"type": "text", "text": "echo not executed: user declined the elicitation form"}], "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}
[HTTP 200]超出三个合法值的 action——比如 reject——才是真正的非法参数:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8812/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: echo' \
-d "{\"jsonrpc\":\"2.0\",\"id\":33,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{},\"inputResponses\":{\"text\":{\"action\":\"reject\"}},\"requestState\":\"stub-1\",\"_meta\":${META#\"_meta\":}}}"{"jsonrpc": "2.0", "id": 33, "error": {"code": -32602, "message": "Invalid params: inputResponses.text.action must be accept, decline or cancel"}}
[HTTP 400]4. 请求规则的单变量负例
下面每个用例只破坏一件事,其余保持合法。除特别说明外都打 8811。
N1——去掉 Mcp-Method(头不再镜像 body):
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-d "{\"jsonrpc\":\"2.0\",\"id\":40,\"method\":\"tools/list\",\"params\":{$META}}"{"jsonrpc": "2.0", "id": 40, "error": {"code": -32020, "message": "Header mismatch: Mcp-Method header is required and must match the body method"}}
[HTTP 400]N2——头与 body 版本不符(头说现代,_meta 说旧版):
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":41,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2025-11-25","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{"form":{}}}}}}'{"jsonrpc": "2.0", "id": 41, "error": {"code": -32020, "message": "Header mismatch: MCP-Protocol-Version header does not match params._meta"}}
[HTTP 400]N3——服务器不实现的版本(头与 body 一致,都是 2025-11-25):
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2025-11-25' \
-H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":42,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2025-11-25","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{"elicitation":{"form":{}}}}}}'{"jsonrpc": "2.0", "id": 42, "error": {"code": -32022, "message": "Unsupported protocol version", "data": {"supported": ["2026-07-28"], "requested": "2025-11-25"}}}
[HTTP 400]N4——Accept 缺一种媒体类型(在 HTTP 层被拒,先于 JSON-RPC):
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d "{\"jsonrpc\":\"2.0\",\"id\":43,\"method\":\"tools/list\",\"params\":{$META}}"{"error": "not acceptable", "detail": "POST requires Accept: application/json, text/event-stream"}
[HTTP 406]N5——未知方法(带 JSON-RPC 正文的 404,不是裸 HTTP 404):
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: resources/list' \
-d "{\"jsonrpc\":\"2.0\",\"id\":44,\"method\":\"resources/list\",\"params\":{$META}}"{"jsonrpc": "2.0", "id": 44, "error": {"code": -32601, "message": "Method not found"}}
[HTTP 404]N6——tools/call 缺 Mcp-Name:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-d "{\"jsonrpc\":\"2.0\",\"id\":45,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{\"text\":\"x\"},\"_meta\":${META#\"_meta\":}}}"{"jsonrpc": "2.0", "id": 45, "error": {"code": -32020, "message": "Header mismatch: Mcp-Name header is required and must match params.name"}}
[HTTP 400]N7——params._meta 仅缺 clientInfo(body 问题,所以是 Invalid params——不是 HeaderMismatch):
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":46,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"elicitation":{"form":{}}}}}}'{"jsonrpc": "2.0", "id": 46, "error": {"code": -32602, "message": "Invalid params: params._meta.io.modelcontextprotocol/clientInfo is required and must be an object"}}
[HTTP 400]N8——完全没有 MCP-Protocol-Version 头:
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Mcp-Method: tools/list' \
-d "{\"jsonrpc\":\"2.0\",\"id\":47,\"method\":\"tools/list\",\"params\":{$META}}"{"jsonrpc": "2.0", "id": 47, "error": {"code": -32020, "message": "Header mismatch: MCP-Protocol-Version header is required"}}
[HTTP 400]N9——服务器需要的表单客户端从未声明(打 8812;能力里没有 elicitation):
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8812/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' \
-H 'Mcp-Method: tools/call' \
-H 'Mcp-Name: echo' \
-d '{"jsonrpc":"2.0","id":48,"method":"tools/call","params":{"name":"echo","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{}}}}'{"jsonrpc": "2.0", "id": 48, "error": {"code": -32021, "message": "Server requires the elicitation capability for this request", "data": {"requiredCapabilities": {"elicitation": {"form": {}}}}}}
[HTTP 400]N10–N13——类型违规(JSON 形态错误;服务器应答错误后继续服务):
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/list' -d '[]'
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":51,"method":"tools/list","params":[1]}'
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/list' \
-d '{"jsonrpc":"2.0","id":52,"method":"tools/list","params":{"_meta":7}}'
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8812/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/call' -H 'Mcp-Name: echo' \
-d "{\"jsonrpc\":\"2.0\",\"id\":53,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{\"text\":42},\"_meta\":${META#\"_meta\":}}}"
curl -sS -w '\n[HTTP %{http_code}]\n' http://127.0.0.1:8811/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/list' \
-d "{\"jsonrpc\":\"2.0\",\"id\":55,\"method\":\"tools/list\",\"params\":{$META}}"{"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Invalid Request: body must be a single JSON-RPC request object"}}
[HTTP 400]
{"jsonrpc": "2.0", "id": 51, "error": {"code": -32602, "message": "Invalid params: params must be an object"}}
[HTTP 400]
{"jsonrpc": "2.0", "id": 52, "error": {"code": -32602, "message": "Invalid params: params._meta is required and must be an object"}}
[HTTP 400]
{"jsonrpc": "2.0", "id": 53, "error": {"code": -32602, "message": "Invalid params: arguments.text must be a string"}}
[HTTP 400]
{"jsonrpc": "2.0", "id": 55, "result": {"tools": [{"name": "echo", "description": "Echo text.", "inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}], "resultType": "complete", "ttlMs": 0, "cacheScope": "private", "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28"}}}
[HTTP 200]最后一条比看上去更重要:不严谨的服务器会把 42 字符串化后回显 "42"。这一台拒绝类型而不是瞎猜,且下一个健康请求依然成功。
安全回退
同时支持两个时代的客户端可以先发一个现代请求探测——但向后兼容要求先读 400 的正文再决定回退,因为现代服务器会用 400 表达好几种完全不同的情况:
- 正文包含可识别的现代 JSON-RPC 错误——
-32022、-32021、-32020——说明服务器就是现代的。不要回退到initialize:-32022用error.data.supported里列出的版本重试;-32020修你的头或_meta;-32021声明能力或跳过该请求。 - 正文为空或不是可识别的现代 JSON-RPC 错误:只有这时才回退到
initialize,后续请求继续用旧版协议。
而 404 不等于"这不是现代端点":
404带 JSON-RPC-32601正文,是现代服务器未实现该方法——正是本页initialize示例的样子。该换方法(server/discover),不是换时代。404不带 JSON-RPC 正文,是旧版 HTTP+SSE 服务器对未托管端点 POST 的应答方式。规范正是用这一差异区分两者。
MCP Inspector 用 --protocol-era legacy|auto|modern 并排运行两个时代;auto 自动完成探测与回退。想在完整教程里看同一迁移的客户端侧,读构建 MCP 客户端;想了解规范本身的变化,从规范索引开始。