Skip to content

Commit 618c2e2

Browse files
authored
Merge pull request #613 from koic/accept_any_string_as_a_request_id
Accept any string as a request id
2 parents 54885b2 + 5f82b63 commit 618c2e2

4 files changed

Lines changed: 72 additions & 3 deletions

File tree

‎lib/json_rpc_handler.rb‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,9 @@ def valid_version?(version)
185185

186186
def valid_id?(id, pattern = nil)
187187
return true if id.nil? || id.is_a?(Integer)
188-
return false unless id.is_a?(String)
188+
# A string that is not valid in its encoding cannot be written into the response,
189+
# and matching a pattern against it raises, so it is refused before the method runs.
190+
return false unless id.is_a?(String) && id.valid_encoding?
189191

190192
pattern ? id.match?(pattern) : true
191193
end

‎lib/mcp/server.rb‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def initialize(
288288
# handlers as `server_context.auth_info`.
289289
# @return [Hash, nil] The JSON-RPC response, or `nil` for notifications.
290290
def handle(request, session: nil, auth_info: nil)
291-
JsonRpcHandler.handle(request) do |method, request_id|
291+
JsonRpcHandler.handle(request, id_validation_pattern: nil) do |method, request_id|
292292
handle_request(request, method, session: session, related_request_id: request_id, auth_info: auth_info)
293293
end
294294
end
@@ -303,7 +303,7 @@ def handle(request, session: nil, auth_info: nil)
303303
# for the current request, as in `handle`.
304304
# @return [String, nil] The JSON-RPC response as JSON, or `nil` for notifications.
305305
def handle_json(request, session: nil, auth_info: nil)
306-
JsonRpcHandler.handle_json(request) do |method, request_id|
306+
JsonRpcHandler.handle_json(request, id_validation_pattern: nil) do |method, request_id|
307307
handle_request(request, method, session: session, related_request_id: request_id, auth_info: auth_info)
308308
end
309309
end

‎test/json_rpc_handler_test.rb‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,21 @@
134134
}
135135
end
136136

137+
it "returns an error and does not call the method when the request id is not valid UTF-8" do
138+
called = false
139+
register("add") { called = true }
140+
141+
handle jsonrpc: "2.0", id: [0xFF].pack("C").force_encoding(Encoding::UTF_8), method: "add", params: { a: 1, b: 2 }
142+
143+
assert_rpc_error expected_error: {
144+
code: -32600,
145+
message: "Invalid Request",
146+
data: "Request ID must be a string matching the validation pattern, or an integer",
147+
}
148+
assert_nil @response[:id]
149+
refute called
150+
end
151+
137152
it "accepts string id with alphanumerics, dashes, and underscores" do
138153
register("add") { |params| params[:a] + params[:b] }
139154
id = "request-123_ABC"

‎test/mcp/server_test.rb‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1960,6 +1960,58 @@ def read_resource_request(uri)
19601960
assert_equal ["custom"], seen
19611961
end
19621962

1963+
test "#handle accepts any string as the request id and echoes it" do
1964+
# MCP, like JSON-RPC 2.0, allows any string or integer id; the TypeScript and Python servers accept these.
1965+
["req.1", "call:42", "urn:uuid:6f1c4e0a-2b7d-4c1e-9a3f-0d2c8b1e5f77", "request 123", "<script>alert('x')</script>", ""].each do |id|
1966+
response = @server.handle({ jsonrpc: "2.0", id: id, method: "ping" })
1967+
1968+
assert_equal({ jsonrpc: "2.0", id: id, result: {} }, response, id.inspect)
1969+
end
1970+
end
1971+
1972+
test "#handle_json keeps a request id with a newline on one line of the response" do
1973+
json = @server.handle_json({ jsonrpc: "2.0", id: "line1\nline2", method: "ping" }.to_json)
1974+
1975+
refute_includes json, "\n"
1976+
assert_equal "line1\nline2", JSON.parse(json)["id"]
1977+
end
1978+
1979+
test "#handle echoes a string request id on an error response" do
1980+
response = @server.handle({ jsonrpc: "2.0", id: "req.1", method: "unsupported_method" })
1981+
1982+
assert_equal "req.1", response[:id]
1983+
assert_equal(-32601, response[:error][:code])
1984+
end
1985+
1986+
test "#handle still refuses a request id that is neither a string nor an integer" do
1987+
[3.14, true, false, { a: 1 }, [1]].each do |id|
1988+
response = @server.handle({ jsonrpc: "2.0", id: id, method: "ping" })
1989+
1990+
assert_nil response[:id], id.inspect
1991+
assert_equal(-32600, response[:error][:code], id.inspect)
1992+
end
1993+
end
1994+
1995+
test "#handle refuses a request id that is not valid UTF-8 without running the method" do
1996+
# Such an id cannot be written into the response; refused up front, the write it asks for does not run.
1997+
calls = 0
1998+
@server.define_tool(name: "count") do
1999+
calls += 1
2000+
Tool::Response.new([{ type: "text", text: "ok" }])
2001+
end
2002+
2003+
response = @server.handle({
2004+
jsonrpc: "2.0",
2005+
id: [0xFF].pack("C").force_encoding(Encoding::UTF_8),
2006+
method: "tools/call",
2007+
params: { name: "count", arguments: {} },
2008+
})
2009+
2010+
assert_nil response[:id]
2011+
assert_equal(-32600, response[:error][:code])
2012+
assert_equal 0, calls
2013+
end
2014+
19632015
test "#handle handles custom methods" do
19642016
@server.define_custom_method(method_name: "add") do |params|
19652017
params[:a] + params[:b]

0 commit comments

Comments
 (0)