Merge pull request #207 from keisku/add-Accept-header-validation

Add Accept header validation
This commit is contained in:
Koichi ITO 2026-01-08 02:49:19 +09:00 • committed by GitHub
commit b92dbe4c28
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 313 additions and 2 deletions

View file

@ -3,6 +3,8 @@
module MCP
class Client
class HTTP
ACCEPT_HEADER = "application/json, text/event-stream"
attr_reader :url
def initialize(url:, headers: {})
@ -14,7 +16,9 @@ module MCP
method = request[:method] || request["method"]
params = request[:params] || request["params"]
client.post("", request).body
response = client.post("", request)
validate_response_content_type!(response, method, params)
response.body
rescue Faraday::BadRequestError => e
raise RequestHandlerError.new(
"The #{method} request is invalid",
@ -70,6 +74,7 @@ module MCP
faraday.response(:json)
faraday.response(:raise_error)
faraday.headers["Accept"] = ACCEPT_HEADER
headers.each do |key, value|
faraday.headers[key] = value
end
@ -83,6 +88,17 @@ module MCP
"Add it to your Gemfile: gem 'faraday', '>= 2.0'" \
"See https://rubygems.org/gems/faraday for more details."
end
def validate_response_content_type!(response, method, params)
content_type = response.headers["Content-Type"]
return if content_type&.include?("application/json")
raise RequestHandlerError.new(
"Unsupported Content-Type: #{content_type.inspect}. This client only supports JSON responses.",
{ method: method, params: params },
error_type: :unsupported_media_type,
)
end
end
end
end

View file

@ -17,6 +17,9 @@ module MCP
@stateless = stateless
end
REQUIRED_POST_ACCEPT_TYPES = ["application/json", "text/event-stream"].freeze
REQUIRED_GET_ACCEPT_TYPES = ["text/event-stream"].freeze
def handle_request(request)
case request.env["REQUEST_METHOD"]
when "POST"
@ -105,6 +108,9 @@ module MCP
end
def handle_post(request)
accept_error = validate_accept_header(request, REQUIRED_POST_ACCEPT_TYPES)
return accept_error if accept_error
body_string = request.body.read
session_id = extract_session_id(request)
@ -128,6 +134,9 @@ module MCP
return method_not_allowed_response
end
accept_error = validate_accept_header(request, REQUIRED_GET_ACCEPT_TYPES)
return accept_error if accept_error
session_id = extract_session_id(request)
return missing_session_id_response unless session_id
@ -178,6 +187,31 @@ module MCP
request.env["HTTP_MCP_SESSION_ID"]
end
def validate_accept_header(request, required_types)
accept_header = request.env["HTTP_ACCEPT"]
return not_acceptable_response(required_types) unless accept_header
accepted_types = parse_accept_header(accept_header)
missing_types = required_types - accepted_types
return not_acceptable_response(required_types) unless missing_types.empty?
nil
end
def parse_accept_header(header)
header.split(",").map do |part|
part.split(";").first.strip
end
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],
]
end
def parse_request_body(body_string)
JSON.parse(body_string)
rescue JSON::ParserError, TypeError