Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1403,7 +1403,7 @@ def call_tool(request, session: nil, related_request_id: nil, cancellation: nil,
end
end

progress_token = request.dig(:_meta, :progressToken)
progress_token = request.dig(:_meta, :progressToken) if request[:_meta].is_a?(Hash)

response = call_tool_with_args(
tool,
Expand Down Expand Up @@ -1915,7 +1915,7 @@ def handler_declares_server_context?(handler)
# (progress, cancellation, nested server-to-client requests).
def build_server_context(request:, session:, related_request_id:, cancellation:, envelope: nil, auth_info: nil)
meta_source = request.is_a?(Hash) ? request : {}
progress_token = meta_source.dig(:_meta, :progressToken)
progress_token = meta_source.dig(:_meta, :progressToken) if meta_source[:_meta].is_a?(Hash)
progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
retry_fields = mrtr_retry_fields(meta_source)
ServerContext.new(
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/server/transports/streamable_http_transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -980,7 +980,7 @@ def handle_modern(request, header_version, body_string: nil)
# converge on enforcement.
def validate_modern_headers(request, body, header_version)
params = body[:params]
meta_version = params.is_a?(Hash) ? params.dig(:_meta, :"io.modelcontextprotocol/protocolVersion") : nil
meta_version = params.is_a?(Hash) && params[:_meta].is_a?(Hash) ? params.dig(:_meta, :"io.modelcontextprotocol/protocolVersion") : nil
if meta_version && meta_version != header_version
return header_mismatch_response(
"MCP-Protocol-Version header value '#{header_version}' does not match body value '#{meta_version}'",
Expand Down
11 changes: 11 additions & 0 deletions test/mcp/server/transports/streamable_http_transport_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6505,6 +6505,17 @@ def string
assert_equal(-32602, JSON.parse(response[2][0]).dig("error", "code"))
end

test "modern POST rejects a _meta that is not an object with 400 and -32602" do
["not-an-object", []].each do |meta|
response = @transport.handle_request(modern_rack_request(
{ jsonrpc: "2.0", method: "tools/list", id: 1, params: { _meta: meta } }.to_json,
))

assert_equal 400, response[0]
assert_equal(-32602, JSON.parse(response[2][0]).dig("error", "code"))
end
end

test "modern POST rejects an envelope missing clientCapabilities with 400 and -32602" do
body = {
jsonrpc: "2.0",
Expand Down
14 changes: 14 additions & 0 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4321,6 +4321,20 @@ def server_context
end
end

test "#handle tools/call and prompts/get ignore a _meta that is not an object" do
@server.define_tool(name: "echo_tool") { Tool::Response.new([]) }

["not-an-object", []].each do |meta|
tool = @server.handle({ jsonrpc: "2.0", method: "tools/call", id: 1, params: { name: "echo_tool", _meta: meta } })
prompt = @server.handle({
jsonrpc: "2.0", method: "prompts/get", id: 1, params: { name: "test_prompt", arguments: { test_argument: "x" }, _meta: meta },
})

assert_nil tool[:error]
assert_nil prompt[:error]
end
end

test "#handle tools/call mirrors non-object structuredContent when content is omitted or nil" do
# Per SEP-2106, `structuredContent` may be any JSON value. Older clients may only read `content`,
# so the server adds a serialized fallback when the tool provided no content blocks.
Expand Down
Loading