Skip to content

Commit 3f5f6f8

Browse files
authored
Internalize json_rpc_handler along with an XSS vulnerability fix (#175)
* Copy-paste of files from original gem * Update copied code to match repo style * Use the lib instead of the gem
1 parent c152f15 commit 3f5f6f8

File tree

5 files changed

+816
-2
lines changed

5 files changed

+816
-2
lines changed

lib/json_rpc_handler.rb

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# frozen_string_literal: true
2+
3+
require "json"
4+
5+
module JsonRpcHandler
6+
class Version
7+
V1_0 = "1.0"
8+
V2_0 = "2.0"
9+
end
10+
11+
class ErrorCode
12+
INVALID_REQUEST = -32600
13+
METHOD_NOT_FOUND = -32601
14+
INVALID_PARAMS = -32602
15+
INTERNAL_ERROR = -32603
16+
PARSE_ERROR = -32700
17+
end
18+
19+
DEFAULT_ALLOWED_ID_CHARACTERS = /\A[a-zA-Z0-9_-]+\z/
20+
21+
extend self
22+
23+
def handle(request, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
24+
if request.is_a?(Array)
25+
return error_response(id: :unknown_id, id_validation_pattern:, error: {
26+
code: ErrorCode::INVALID_REQUEST,
27+
message: "Invalid Request",
28+
data: "Request is an empty array",
29+
}) if request.empty?
30+
31+
# Handle batch requests
32+
responses = request.map { |req| process_request(req, id_validation_pattern:, &method_finder) }.compact
33+
34+
# A single item is hoisted out of the array
35+
return responses.first if responses.one?
36+
37+
# An empty array yields nil
38+
responses if responses.any?
39+
elsif request.is_a?(Hash)
40+
# Handle single request
41+
process_request(request, id_validation_pattern:, &method_finder)
42+
else
43+
error_response(id: :unknown_id, id_validation_pattern:, error: {
44+
code: ErrorCode::INVALID_REQUEST,
45+
message: "Invalid Request",
46+
data: "Request must be an array or a hash",
47+
})
48+
end
49+
end
50+
51+
def handle_json(request_json, id_validation_pattern: DEFAULT_ALLOWED_ID_CHARACTERS, &method_finder)
52+
begin
53+
request = JSON.parse(request_json, symbolize_names: true)
54+
response = handle(request, id_validation_pattern:, &method_finder)
55+
rescue JSON::ParserError
56+
response = error_response(id: :unknown_id, id_validation_pattern:, error: {
57+
code: ErrorCode::PARSE_ERROR,
58+
message: "Parse error",
59+
data: "Invalid JSON",
60+
})
61+
end
62+
63+
response&.to_json
64+
end
65+
66+
def process_request(request, id_validation_pattern:, &method_finder)
67+
id = request[:id]
68+
69+
error = if !valid_version?(request[:jsonrpc])
70+
"JSON-RPC version must be 2.0"
71+
elsif !valid_id?(request[:id], id_validation_pattern)
72+
"Request ID must match validation pattern, or be an integer or null"
73+
elsif !valid_method_name?(request[:method])
74+
'Method name must be a string and not start with "rpc."'
75+
end
76+
77+
return error_response(id: :unknown_id, id_validation_pattern:, error: {
78+
code: ErrorCode::INVALID_REQUEST,
79+
message: "Invalid Request",
80+
data: error,
81+
}) if error
82+
83+
method_name = request[:method]
84+
params = request[:params]
85+
86+
unless valid_params?(params)
87+
return error_response(id:, id_validation_pattern:, error: {
88+
code: ErrorCode::INVALID_PARAMS,
89+
message: "Invalid params",
90+
data: "Method parameters must be an array or an object or null",
91+
})
92+
end
93+
94+
begin
95+
method = method_finder.call(method_name)
96+
97+
if method.nil?
98+
return error_response(id:, id_validation_pattern:, error: {
99+
code: ErrorCode::METHOD_NOT_FOUND,
100+
message: "Method not found",
101+
data: method_name,
102+
})
103+
end
104+
105+
result = method.call(params)
106+
107+
success_response(id:, result:)
108+
rescue StandardError => e
109+
error_response(id:, id_validation_pattern:, error: {
110+
code: ErrorCode::INTERNAL_ERROR,
111+
message: "Internal error",
112+
data: e.message,
113+
})
114+
end
115+
end
116+
117+
def valid_version?(version)
118+
version == Version::V2_0
119+
end
120+
121+
def valid_id?(id, pattern = nil)
122+
return true if id.nil? || id.is_a?(Integer)
123+
return false unless id.is_a?(String)
124+
125+
pattern ? id.match?(pattern) : true
126+
end
127+
128+
def valid_method_name?(method)
129+
method.is_a?(String) && !method.start_with?("rpc.")
130+
end
131+
132+
def valid_params?(params)
133+
params.nil? || params.is_a?(Array) || params.is_a?(Hash)
134+
end
135+
136+
def success_response(id:, result:)
137+
{
138+
jsonrpc: Version::V2_0,
139+
id:,
140+
result:,
141+
} unless id.nil?
142+
end
143+
144+
def error_response(id:, id_validation_pattern:, error:)
145+
{
146+
jsonrpc: Version::V2_0,
147+
id: valid_id?(id, id_validation_pattern) ? id : nil,
148+
error: error.compact,
149+
} unless id.nil?
150+
end
151+
end

lib/mcp.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# frozen_string_literal: true
22

3+
require_relative "json_rpc_handler"
34
require_relative "mcp/configuration"
45
require_relative "mcp/content"
56
require_relative "mcp/instrumentation"

lib/mcp/server.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# frozen_string_literal: true
22

3-
require "json_rpc_handler"
3+
require_relative "../json_rpc_handler"
44
require_relative "instrumentation"
55
require_relative "methods"
66

mcp.gemspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,5 @@ Gem::Specification.new do |spec|
2929
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
3030
spec.require_paths = ["lib"]
3131

32-
spec.add_dependency("json_rpc_handler", "~> 0.1")
3332
spec.add_dependency("json-schema", ">= 4.1")
3433
end

0 commit comments

Comments
 (0)