Skip to content

Commit c917028

Browse files
committed
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.
1 parent cf44475 commit c917028

2 files changed

Lines changed: 103 additions & 58 deletions

File tree

lib/mcp/server/transports/streamable_http_transport.rb

Lines changed: 50 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,11 @@ def handle_post(request)
389389
end
390390
rescue StandardError => e
391391
MCP.configuration.exception_reporter.call(e, { request: body_string })
392-
[500, { "Content-Type" => "application/json" }, [{ error: "Internal server error" }.to_json]]
392+
json_rpc_error_response(
393+
status: 500,
394+
code: JsonRpcHandler::ErrorCode::INTERNAL_ERROR,
395+
message: "Internal server error",
396+
)
393397
end
394398

395399
def handle_get(request)
@@ -513,19 +517,19 @@ def validate_content_type(request)
513517
media_type = content_type&.split(";")&.first&.strip&.downcase
514518
return if media_type == "application/json"
515519

516-
[
517-
415,
518-
{ "Content-Type" => "application/json" },
519-
[{ error: "Unsupported Media Type: Content-Type must be application/json" }.to_json],
520-
]
520+
json_rpc_error_response(
521+
status: 415,
522+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
523+
message: "Unsupported Media Type: Content-Type must be application/json",
524+
)
521525
end
522526

523527
def not_acceptable_response(required_types)
524-
[
525-
406,
526-
{ "Content-Type" => "application/json" },
527-
[{ error: "Not Acceptable: Accept header must include #{required_types.join(" and ")}" }.to_json],
528-
]
528+
json_rpc_error_response(
529+
status: 406,
530+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
531+
message: "Not Acceptable: Accept header must include #{required_types.join(" and ")}",
532+
)
529533
end
530534

531535
def parse_request_body(body_string)
@@ -535,7 +539,11 @@ def parse_request_body(body_string)
535539
end
536540

537541
def invalid_json_response
538-
[400, { "Content-Type" => "application/json" }, [{ error: "Invalid JSON" }.to_json]]
542+
json_rpc_error_response(
543+
status: 400,
544+
code: JsonRpcHandler::ErrorCode::PARSE_ERROR,
545+
message: "Parse error: Invalid JSON",
546+
)
539547
end
540548

541549
def initialize_request?(body)
@@ -548,15 +556,16 @@ def validate_protocol_version_header(request)
548556
return if MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(header_value)
549557

550558
supported = MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.join(", ")
551-
body = {
552-
jsonrpc: "2.0",
553-
id: nil,
554-
error: {
555-
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
556-
message: "Bad Request: Unsupported protocol version: #{header_value}. Supported versions: #{supported}",
557-
},
558-
}
559-
[400, { "Content-Type" => "application/json" }, [body.to_json]]
559+
json_rpc_error_response(
560+
status: 400,
561+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
562+
message: "Bad Request: Unsupported protocol version: #{header_value}. Supported versions: #{supported}",
563+
)
564+
end
565+
566+
def json_rpc_error_response(status:, code:, message:)
567+
body = { jsonrpc: "2.0", id: nil, error: { code: code, message: message } }
568+
[status, { "Content-Type" => "application/json" }, [body.to_json]]
560569
end
561570

562571
def notification?(body)
@@ -793,15 +802,27 @@ def session_active?(session_id)
793802
end
794803

795804
def method_not_allowed_response
796-
[405, { "Content-Type" => "application/json" }, [{ error: "Method not allowed" }.to_json]]
805+
json_rpc_error_response(
806+
status: 405,
807+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
808+
message: "Method not allowed",
809+
)
797810
end
798811

799812
def missing_session_id_response
800-
[400, { "Content-Type" => "application/json" }, [{ error: "Missing session ID" }.to_json]]
813+
json_rpc_error_response(
814+
status: 400,
815+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
816+
message: "Missing session ID",
817+
)
801818
end
802819

803820
def session_not_found_response
804-
[404, { "Content-Type" => "application/json" }, [{ error: "Session not found" }.to_json]]
821+
json_rpc_error_response(
822+
status: 404,
823+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
824+
message: "Session not found",
825+
)
805826
end
806827

807828
def already_initialized_response(request_id)
@@ -821,11 +842,11 @@ def invalid_request_response(message, request_id: nil)
821842
end
822843

823844
def session_already_connected_response
824-
[
825-
409,
826-
{ "Content-Type" => "application/json" },
827-
[{ error: "Conflict: Only one SSE stream is allowed per session" }.to_json],
828-
]
845+
json_rpc_error_response(
846+
status: 409,
847+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
848+
message: "Conflict: Only one SSE stream is allowed per session",
849+
)
829850
end
830851

831852
def setup_sse_stream(session_id)

0 commit comments

Comments
 (0)