fix(mcp): reject non-object requests with protocol errors

This commit is contained in:
jxxghp
2026-10-17 03:23:40 +08:00
parent 33a14f85eb
commit ac64d4b9ca
3 changed files with 56 additions and 6 deletions

View File

@@ -109,13 +109,14 @@ def create_jsonrpc_error(
"""
创建 JSON-RPC 错误响应
"""
error_detail: Dict[str, object] = {"code": code, "message": message}
error = {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": code, "message": message},
"error": error_detail,
}
if data is not None:
error["error"]["data"] = data
error_detail["data"] = data
return error
@@ -164,7 +165,7 @@ async def _dispatch_jsonrpc_method(
},
)
async def mcp_jsonrpc(
request: Request, _: Annotated[str, Depends(verify_apikey)] = None
request: Request, _: Annotated[Union[str, None], Depends(verify_apikey)] = None
) -> Union[JSONResponse, Response]:
"""
MCP 标准 JSON-RPC 2.0 端点
@@ -184,7 +185,9 @@ async def mcp_jsonrpc(
if not isinstance(body, dict) or body.get("jsonrpc") != "2.0":
return JSONResponse(
status_code=400,
content=create_jsonrpc_error(body.get("id"), -32600, "Invalid Request"),
content=create_jsonrpc_error(
body.get("id") if isinstance(body, dict) else None, -32600, "Invalid Request"
),
)
method = body.get("method")
@@ -300,7 +303,7 @@ async def handle_tools_call(params: Dict[str, Any]) -> Dict[str, Any]:
},
)
async def delete_mcp_session(
_: Annotated[str, Depends(verify_apikey)] = None,
_: Annotated[Union[str, None], Depends(verify_apikey)] = None,
) -> Union[JSONResponse, Response]:
"""
终止 MCP 会话(无状态模式下仅返回成功)
@@ -347,7 +350,7 @@ async def list_tools(_: Annotated[str, Depends(verify_apikey)], page: Compatible
summary="调用工具",
response_model=_SchemaResponse[_SchemaToolCallData],
)
async def call_tool(request: _SchemaToolCallRequest, _: Annotated[str, Depends(verify_apikey)] = None) -> Any:
async def call_tool(request: _SchemaToolCallRequest, _: Annotated[Union[str, None], Depends(verify_apikey)] = None) -> Any:
"""
调用指定的工具

View File

@@ -51,6 +51,10 @@ Fake-IP 等非公网解析地址需要通过 `IMAGE_PROXY_ALLOWED_PRIVATE_RANGES
### 端点
**POST** `/api/v1/mcp`
请求体须为单个 JSON-RPC 2.0 对象。非对象值(包括数组批量请求)或错误的协议版本返回
HTTP 400、JSON-RPC 错误码 `-32600`;非对象请求的 `id` 为 `null`。无法解析的 JSON 返回
HTTP 400、错误码 `-32700`。
### 支持的方法
- `initialize`: 初始化会话,协商协议版本和能力。
- `notifications/initialized`: 客户端确认初始化完成。

View File

@@ -0,0 +1,43 @@
"""MCP JSON-RPC 请求格式错误的协议回归。"""
import json
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api.endpoints import mcp
@pytest.mark.parametrize(
("body", "request_id"),
[
(None, None),
([], None),
([{"jsonrpc": "2.0", "method": "ping", "id": 1}], None),
("invalid", None),
(1, None),
(True, None),
({}, None),
({"jsonrpc": "1.0", "id": 7}, 7),
],
)
def test_invalid_request_returns_jsonrpc_error(body: object, request_id: object) -> None:
"""非对象及错误版本的请求须返回 400 协议错误,不能在取 id 时崩溃。"""
app = FastAPI()
app.include_router(mcp.router, prefix="/mcp")
app.dependency_overrides[mcp.verify_apikey] = lambda: "test-api-key"
with TestClient(app) as client:
response = client.post(
"/mcp",
content=json.dumps(body),
headers={"Content-Type": "application/json"},
)
assert response.status_code == 400
assert response.json() == {
"jsonrpc": "2.0",
"id": request_id,
"error": {"code": -32600, "message": "Invalid Request"},
}