Use JSON-RPC error envelope for StreamableHTTPTransport errors
## Motivation and Context PR #347 introduced a single JSON-RPC error envelope response for unsupported `MCP-Protocol-Version` headers, while the rest of the transport-level error responses (Accept, Content-Type, Invalid JSON, session management, method not allowed, internal server error) still returned plain JSON of the form `{ "error": "..." }`. The Python SDK (`src/mcp/server/streamable_http.py`) and TypeScript SDK (`packages/server/src/server/streamableHttp.ts`) consistently use a JSON-RPC error envelope for all transport-level errors. Unify the Ruby SDK with them. ## Behavior All transport-level error responses now return a JSON-RPC error envelope: ```json { "jsonrpc": "2.0", "id": null, "error": { "code": -32600, "message": "..." } } ``` Affected helpers in `lib/mcp/server/transports/streamable_http_transport.rb`: - `validate_content_type` (HTTP 415) - `not_acceptable_response` (HTTP 406) - `parse_request_body` error (HTTP 400, `PARSE_ERROR` `-32700`) - `method_not_allowed_response` (HTTP 405) - `missing_session_id_response` (HTTP 400) - `session_not_found_response` (HTTP 404) - `session_already_connected_response` (HTTP 409) - Internal server error fallback in `handle_post` (HTTP 500, `INTERNAL_ERROR` `-32603`) - `validate_protocol_version_header` (HTTP 400, dedup with shared helper) A new private helper `json_rpc_error_response(status:, code:, message:)` centralizes the envelope construction. The "Invalid JSON" message wording is updated to "Parse error: Invalid JSON" to match the JSON-RPC 2.0 `PARSE_ERROR` semantics. ## How Has This Been Tested? Updated all affected tests in `test/mcp/server/transports/streamable_http_transport_test.rb` to assert the new envelope shape (`body["error"]["message"]` instead of `body["error"]`). ## Breaking Changes Clients that parsed the previous plain `{"error": "..."}` shape will need to read `body["error"]["message"]` (or `body["error"]["code"]`). The HTTP status codes are unchanged, only the response body structure changed.
This commit is contained in:
parent
cf44475ce9
commit
c91702899d
1 changed files with 103 additions and 58 deletions
|
|
@ -389,7 +389,11 @@ module MCP
|
|||
end
|
||||
rescue StandardError => e
|
||||
MCP.configuration.exception_reporter.call(e, { request: body_string })
|
||||
[500, { "Content-Type" => "application/json" }, [{ error: "Internal server error" }.to_json]]
|
||||
json_rpc_error_response(
|
||||
status: 500,
|
||||
code: JsonRpcHandler::ErrorCode::INTERNAL_ERROR,
|
||||
message: "Internal server error",
|
||||
)
|
||||
end
|
||||
|
||||
def handle_get(request)
|
||||
|
|
@ -513,19 +517,19 @@ module MCP
|
|||
media_type = content_type&.split(";")&.first&.strip&.downcase
|
||||
return if media_type == "application/json"
|
||||
|
||||
[
|
||||
415,
|
||||
{ "Content-Type" => "application/json" },
|
||||
[{ error: "Unsupported Media Type: Content-Type must be application/json" }.to_json],
|
||||
]
|
||||
json_rpc_error_response(
|
||||
status: 415,
|
||||
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
||||
message: "Unsupported Media Type: Content-Type must be application/json",
|
||||
)
|
||||
end
|
||||
|
||||
def not_acceptable_response(required_types)
|
||||
[
|
||||
406,
|
||||
{ "Content-Type" => "application/json" },
|
||||
[{ error: "Not Acceptable: Accept header must include #{required_types.join(" and ")}" }.to_json],
|
||||
]
|
||||
json_rpc_error_response(
|
||||
status: 406,
|
||||
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
||||
message: "Not Acceptable: Accept header must include #{required_types.join(" and ")}",
|
||||
)
|
||||
end
|
||||
|
||||
def parse_request_body(body_string)
|
||||
|
|
@ -535,7 +539,11 @@ module MCP
|
|||
end
|
||||
|
||||
def invalid_json_response
|
||||
[400, { "Content-Type" => "application/json" }, [{ error: "Invalid JSON" }.to_json]]
|
||||
json_rpc_error_response(
|
||||
status: 400,
|
||||
code: JsonRpcHandler::ErrorCode::PARSE_ERROR,
|
||||
message: "Parse error: Invalid JSON",
|
||||
)
|
||||
end
|
||||
|
||||
def initialize_request?(body)
|
||||
|
|
@ -548,15 +556,16 @@ module MCP
|
|||
return if MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(header_value)
|
||||
|
||||
supported = MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.join(", ")
|
||||
body = {
|
||||
jsonrpc: "2.0",
|
||||
id: nil,
|
||||
error: {
|
||||
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
||||
message: "Bad Request: Unsupported protocol version: #{header_value}. Supported versions: #{supported}",
|
||||
},
|
||||
}
|
||||
[400, { "Content-Type" => "application/json" }, [body.to_json]]
|
||||
json_rpc_error_response(
|
||||
status: 400,
|
||||
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
||||
message: "Bad Request: Unsupported protocol version: #{header_value}. Supported versions: #{supported}",
|
||||
)
|
||||
end
|
||||
|
||||
def json_rpc_error_response(status:, code:, message:)
|
||||
body = { jsonrpc: "2.0", id: nil, error: { code: code, message: message } }
|
||||
[status, { "Content-Type" => "application/json" }, [body.to_json]]
|
||||
end
|
||||
|
||||
def notification?(body)
|
||||
|
|
@ -793,15 +802,27 @@ module MCP
|
|||
end
|
||||
|
||||
def method_not_allowed_response
|
||||
[405, { "Content-Type" => "application/json" }, [{ error: "Method not allowed" }.to_json]]
|
||||
json_rpc_error_response(
|
||||
status: 405,
|
||||
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
||||
message: "Method not allowed",
|
||||
)
|
||||
end
|
||||
|
||||
def missing_session_id_response
|
||||
[400, { "Content-Type" => "application/json" }, [{ error: "Missing session ID" }.to_json]]
|
||||
json_rpc_error_response(
|
||||
status: 400,
|
||||
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
||||
message: "Missing session ID",
|
||||
)
|
||||
end
|
||||
|
||||
def session_not_found_response
|
||||
[404, { "Content-Type" => "application/json" }, [{ error: "Session not found" }.to_json]]
|
||||
json_rpc_error_response(
|
||||
status: 404,
|
||||
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
||||
message: "Session not found",
|
||||
)
|
||||
end
|
||||
|
||||
def already_initialized_response(request_id)
|
||||
|
|
@ -821,11 +842,11 @@ module MCP
|
|||
end
|
||||
|
||||
def session_already_connected_response
|
||||
[
|
||||
409,
|
||||
{ "Content-Type" => "application/json" },
|
||||
[{ error: "Conflict: Only one SSE stream is allowed per session" }.to_json],
|
||||
]
|
||||
json_rpc_error_response(
|
||||
status: 409,
|
||||
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
||||
message: "Conflict: Only one SSE stream is allowed per session",
|
||||
)
|
||||
end
|
||||
|
||||
def setup_sse_stream(session_id)
|
||||
|
|
|
|||
Loading…
Reference in a new issue