Merge pull request #460 from koic/lowercase_response_header_names

Lowercase response header names in `StreamableHTTPTransport`
This commit is contained in:
Koichi ITO 2026-07-18 10:35:29 +09:00 • committed by GitHub
commit 0b51173441
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 196 additions and 154 deletions

View file

@ -32,6 +32,7 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
- Use frozen string literals - Use frozen string literals
- Follow Ruby community conventions - Follow Ruby community conventions
- Keep dependencies minimal - Keep dependencies minimal
- Use lowercase HTTP response header names (e.g. `mcp-session-id`); the Rack 3 SPEC requires this, and the MCP spec's `Mcp-Session-Id` casing is prose convention only
## Commit message conventions ## Commit message conventions

View file

@ -105,7 +105,7 @@ class McpSseLogger
@mcp_logger.error("Response error: #{parsed["error"]["message"]}") @mcp_logger.error("Response error: #{parsed["error"]["message"]}")
else else
@mcp_logger.info("Response: success (id: #{parsed["id"]})") @mcp_logger.info("Response: success (id: #{parsed["id"]})")
@sse_logger.info("Session created: #{headers["Mcp-Session-Id"]}") if headers["Mcp-Session-Id"] @sse_logger.info("Session created: #{headers["mcp-session-id"]}") if headers["mcp-session-id"]
end end
rescue JSON::ParserError rescue JSON::ParserError
@mcp_logger.warn("Invalid JSON in response") @mcp_logger.warn("Invalid JSON in response")

View file

@ -19,9 +19,9 @@ module MCP
class InvalidJsonError < StandardError; end class InvalidJsonError < StandardError; end
SSE_HEADERS = { SSE_HEADERS = {
"Content-Type" => "text/event-stream", "content-type" => "text/event-stream",
"Cache-Control" => "no-cache", "cache-control" => "no-cache",
"Connection" => "keep-alive", "connection" => "keep-alive",
}.freeze }.freeze
# Secure defaults for stateful mode. Without a finite idle timeout, sessions live until an explicit client DELETE, # Secure defaults for stateful mode. Without a finite idle timeout, sessions live until an explicit client DELETE,
@ -566,7 +566,7 @@ module MCP
end end
def handle_delete(request) def handle_delete(request)
success_response = [200, { "Content-Type" => "application/json" }, [{ success: true }.to_json]] success_response = [200, { "content-type" => "application/json" }, [{ success: true }.to_json]]
if @stateless if @stateless
protocol_version_error = validate_protocol_version_header(request) protocol_version_error = validate_protocol_version_header(request)
@ -762,7 +762,7 @@ module MCP
def json_rpc_error_response(status:, code:, message:) def json_rpc_error_response(status:, code:, message:)
body = { jsonrpc: "2.0", id: nil, error: { code: code, message: message } } body = { jsonrpc: "2.0", id: nil, error: { code: code, message: message } }
[status, { "Content-Type" => "application/json" }, [body.to_json]] [status, { "content-type" => "application/json" }, [body.to_json]]
end end
def notification?(body) def notification?(body)
@ -870,10 +870,10 @@ module MCP
end end
headers = { headers = {
"Content-Type" => "application/json", "content-type" => "application/json",
} }
headers["Mcp-Session-Id"] = session_id if session_id headers["mcp-session-id"] = session_id if session_id
[200, headers, [response]] [200, headers, [response]]
end end
@ -915,7 +915,7 @@ module MCP
# which would produce an empty body the client cannot parse as JSON. # which would produce an empty body the client cannot parse as JSON.
return handle_accepted if response.nil? return handle_accepted if response.nil?
[200, { "Content-Type" => "application/json" }, [response]] [200, { "content-type" => "application/json" }, [response]]
end end
end end
@ -1148,7 +1148,7 @@ module MCP
message: message, message: message,
}, },
} }
[400, { "Content-Type" => "application/json" }, [body.to_json]] [400, { "content-type" => "application/json" }, [body.to_json]]
end end
def session_already_connected_response def session_already_connected_response

View file

@ -28,7 +28,7 @@ module MCP
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json, { jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
) )
init_response = @transport.handle_request(init_request) init_response = @transport.handle_request(init_request)
session_id = init_response[1]["Mcp-Session-Id"] session_id = init_response[1]["mcp-session-id"]
# Connect with SSE # Connect with SSE
io = StringIO.new io = StringIO.new
@ -77,7 +77,7 @@ module MCP
{ jsonrpc: "2.0", method: "initialize", id: "123", params: initialize_params }.to_json, { jsonrpc: "2.0", method: "initialize", id: "123", params: initialize_params }.to_json,
) )
init_response1 = @transport.handle_request(init_request1) init_response1 = @transport.handle_request(init_request1)
session_id1 = init_response1[1]["Mcp-Session-Id"] session_id1 = init_response1[1]["mcp-session-id"]
init_request2 = create_rack_request( init_request2 = create_rack_request(
"POST", "POST",
@ -86,7 +86,7 @@ module MCP
{ jsonrpc: "2.0", method: "initialize", id: "456", params: initialize_params }.to_json, { jsonrpc: "2.0", method: "initialize", id: "456", params: initialize_params }.to_json,
) )
init_response2 = @transport.handle_request(init_request2) init_response2 = @transport.handle_request(init_request2)
session_id2 = init_response2[1]["Mcp-Session-Id"] session_id2 = init_response2[1]["mcp-session-id"]
# Connect both sessions with SSE # Connect both sessions with SSE
io1 = StringIO.new io1 = StringIO.new
@ -132,7 +132,7 @@ module MCP
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json, { jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
) )
init_response = @transport.handle_request(init_request) init_response = @transport.handle_request(init_request)
session_id = init_response[1]["Mcp-Session-Id"] session_id = init_response[1]["mcp-session-id"]
# Connect with SSE # Connect with SSE
io = StringIO.new io = StringIO.new
@ -168,7 +168,7 @@ module MCP
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json, { jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
) )
init_response = @transport.handle_request(init_request) init_response = @transport.handle_request(init_request)
session_id = init_response[1]["Mcp-Session-Id"] session_id = init_response[1]["mcp-session-id"]
# Connect with SSE # Connect with SSE
io = StringIO.new io = StringIO.new
@ -212,7 +212,7 @@ module MCP
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json, { jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
) )
init_response = @transport.handle_request(init_request) init_response = @transport.handle_request(init_request)
session_id = init_response[1]["Mcp-Session-Id"] session_id = init_response[1]["mcp-session-id"]
# Connect with SSE # Connect with SSE
io = StringIO.new io = StringIO.new

File diff suppressed because it is too large Load diff