Skip to content

Commit 693ff07

Browse files
authored
Merge pull request #99 from patvice/rpc-message-refactor
RPC Message Refactor + JSON RPC Error Codes
2 parents b067fc4 + 7634cfc commit 693ff07

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4186
-1362
lines changed

.rubocop.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,27 @@ Naming/AccessorMethodName:
3737
Layout/LineLength:
3838
Max: 120
3939

40-
Metrics/MethodLength:
41-
Max: 35
42-
4340
Metrics/AbcSize:
4441
Enabled: false
4542

43+
Metrics/BlockLength:
44+
Exclude:
45+
- 'spec/**/*'
46+
4647
Metrics/ClassLength:
4748
Enabled: false
4849

4950
Metrics/CyclomaticComplexity:
5051
Enabled: false
5152

52-
Metrics/PerceivedComplexity:
53-
Enabled: false
53+
Metrics/MethodLength:
54+
Max: 35
5455

55-
Metrics/BlockLength:
56-
Exclude:
57-
- 'spec/**/*'
56+
Metrics/ModuleLength:
57+
Enabled: false
5858

59+
Metrics/PerceivedComplexity:
60+
Enabled: false
5961

6062
RSpec/DescribedClass:
6163
Enabled: false

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ group :development do
1414
gem "bundler", ">= 2.0"
1515
gem "debug"
1616
gem "dotenv", ">= 3.0"
17+
gem "json_schemer"
1718
gem "rake", ">= 13.0"
1819
gem "rdoc", "~> 6.15"
1920
gem "reline"

lib/ruby_llm/mcp/adapters/base_adapter.rb

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ def initialize(client, transport_type:, config: {})
3838
@config = config
3939
end
4040

41-
# Instance methods
4241
def supports?(feature)
4342
self.class.support?(feature)
4443
end
@@ -54,7 +53,6 @@ def validate_transport!(transport_type)
5453
end
5554
end
5655

57-
# Lifecycle methods
5856
def start
5957
raise NotImplementedError, "#{self.class.name} must implement #start"
6058
end
@@ -75,7 +73,6 @@ def ping
7573
raise NotImplementedError, "#{self.class.name} must implement #ping"
7674
end
7775

78-
# Capabilities
7976
def capabilities
8077
raise NotImplementedError, "#{self.class.name} must implement #capabilities"
8178
end
@@ -84,7 +81,6 @@ def client_capabilities
8481
raise NotImplementedError, "#{self.class.name} must implement #client_capabilities"
8582
end
8683

87-
# Core MCP methods - Tools
8884
def tool_list(cursor: nil)
8985
raise NotImplementedError, "#{self.class.name} must implement #tool_list"
9086
end
@@ -93,7 +89,6 @@ def execute_tool(name:, parameters:)
9389
raise NotImplementedError, "#{self.class.name} must implement #execute_tool"
9490
end
9591

96-
# Core MCP methods - Resources
9792
def resource_list(cursor: nil)
9893
raise NotImplementedError, "#{self.class.name} must implement #resource_list"
9994
end
@@ -102,7 +97,6 @@ def resource_read(uri:)
10297
raise NotImplementedError, "#{self.class.name} must implement #resource_read"
10398
end
10499

105-
# Core MCP methods - Prompts
106100
def prompt_list(cursor: nil)
107101
raise NotImplementedError, "#{self.class.name} must implement #prompt_list"
108102
end
@@ -111,12 +105,10 @@ def execute_prompt(name:, arguments:)
111105
raise NotImplementedError, "#{self.class.name} must implement #execute_prompt"
112106
end
113107

114-
# Optional features - Resource Templates
115108
def resource_template_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
116109
raise_unsupported_feature(:resource_templates)
117110
end
118111

119-
# Optional features - Completions
120112
def completion_resource(uri:, argument:, value:, context: nil) # rubocop:disable Lint/UnusedMethodArgument
121113
raise_unsupported_feature(:completions)
122114
end
@@ -125,17 +117,14 @@ def completion_prompt(name:, argument:, value:, context: nil) # rubocop:disable
125117
raise_unsupported_feature(:completions)
126118
end
127119

128-
# Optional features - Logging
129120
def set_logging(level:) # rubocop:disable Lint/UnusedMethodArgument
130121
raise_unsupported_feature(:logging)
131122
end
132123

133-
# Optional features - Subscriptions
134124
def resources_subscribe(uri:) # rubocop:disable Lint/UnusedMethodArgument
135125
raise_unsupported_feature(:subscriptions)
136126
end
137127

138-
# Optional features - Notifications
139128
def initialize_notification
140129
raise_unsupported_feature(:notifications)
141130
end
@@ -148,7 +137,6 @@ def roots_list_change_notification
148137
raise_unsupported_feature(:notifications)
149138
end
150139

151-
# Optional features - Responses
152140
def ping_response(id:) # rubocop:disable Lint/UnusedMethodArgument
153141
raise_unsupported_feature(:responses)
154142
end
@@ -169,7 +157,6 @@ def elicitation_response(id:, elicitation:) # rubocop:disable Lint/UnusedMethodA
169157
raise_unsupported_feature(:elicitation)
170158
end
171159

172-
# Helper for resource registration
173160
def register_resource(_resource)
174161
raise_unsupported_feature(:resource_registration)
175162
end

lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,12 @@ def start
3333
return if @mcp_client
3434

3535
transport = build_transport
36-
37-
# Explicitly start the transport BEFORE creating the MCP client
38-
# This ensures the stdio process is running before the client tries to use it
3936
transport.start if transport.respond_to?(:start)
4037

4138
@mcp_client = ::MCP::Client.new(transport: transport)
4239
end
4340

4441
def stop
45-
# Close the transport if it has a close method
4642
if @mcp_client && @mcp_client.transport.respond_to?(:close)
4743
@mcp_client.transport.close
4844
end
@@ -59,7 +55,6 @@ def alive?
5955
end
6056

6157
def ping # rubocop:disable Naming/PredicateMethod
62-
# Auto-start if not started
6358
ensure_started
6459
alive?
6560
end
@@ -107,33 +102,28 @@ def resource_read(uri:)
107102
end
108103

109104
def prompt_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
110-
# The MCP gem does not support prompts
111105
[]
112106
end
113107

114108
def execute_prompt(name:, arguments:)
115-
# The MCP gem does not support prompts
116109
raise NotImplementedError, "Prompts are not supported by the MCP SDK (gem 'mcp')"
117110
end
118111

119112
def resource_template_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
120-
# The MCP gem does not support resource templates
121113
[]
122114
end
123115

124-
# Notifications
125116
def cancelled_notification(reason:, request_id:)
126117
return unless @mcp_client&.transport.respond_to?(:native_transport)
127118

128119
native_transport = @mcp_client.transport.native_transport
129120
return unless native_transport
130121

131-
# Use the Native::Notifications::Cancelled class directly
132-
RubyLLM::MCP::Native::Notifications::Cancelled.new(
133-
native_transport,
134-
reason: reason,
135-
request_id: request_id
136-
).call
122+
body = RubyLLM::MCP::Native::Messages::Notifications.cancelled(
123+
request_id: request_id,
124+
reason: reason
125+
)
126+
native_transport.request(body, wait_for_response: false)
137127
end
138128

139129
# These methods remain as NotImplementedError from base class:
@@ -198,7 +188,6 @@ def build_transport
198188
request_timeout: @config[:request_timeout] || 10_000
199189
)
200190
when :streamable, :streamable_http
201-
# Prepare OAuth provider if OAuth config is present
202191
config_copy = @config.dup
203192
oauth_provider = Auth::TransportOauthHelper.create_oauth_provider(config_copy) if Auth::TransportOauthHelper.oauth_config_present?(config_copy)
204193

@@ -233,7 +222,6 @@ def transform_tool(tool)
233222
end
234223

235224
def transform_resource(resource)
236-
# MCP gem returns resources as hashes, not objects
237225
{
238226
"name" => resource["name"],
239227
"uri" => resource["uri"],
@@ -253,12 +241,10 @@ def transform_tool_result(result)
253241
[{ "type" => "text", "text" => result.to_s }]
254242
end
255243

256-
# Extract isError flag if present
257244
is_error = if result.is_a?(Hash) && result["result"]
258245
result["result"]["isError"]
259246
end
260247

261-
# Result expects response["result"] to contain the data
262248
result_data = { "content" => content }
263249
result_data["isError"] = is_error unless is_error.nil?
264250

@@ -279,14 +265,12 @@ def transform_content_item(item)
279265
end
280266

281267
def transform_resource_content(result)
282-
# The MCP gem returns an array of content hashes
283268
contents = if result.is_a?(Array)
284269
result.map { |r| transform_single_resource_content(r) }
285270
else
286271
[transform_single_resource_content(result)]
287272
end
288273

289-
# Result expects response["result"] to contain the data
290274
Result.new({
291275
"result" => {
292276
"contents" => contents
@@ -295,7 +279,6 @@ def transform_resource_content(result)
295279
end
296280

297281
def transform_single_resource_content(result)
298-
# MCP gem returns resource contents as hashes, not objects
299282
{
300283
"uri" => result["uri"],
301284
"mimeType" => result["mimeType"],

lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,10 @@ def initialize(url:, headers: {}, version: :http2, request_timeout: 10_000)
2121
)
2222
end
2323

24-
# Start the SSE connection
2524
def start
2625
@native_transport.start
2726
end
2827

29-
# Close the SSE connection
3028
def close
3129
@native_transport.close
3230
end
@@ -37,14 +35,13 @@ def close
3735
# @param request [Hash] A JSON-RPC request object
3836
# @return [Hash] A JSON-RPC response object
3937
def send_request(request:)
40-
# Ensure transport is started
4138
start unless @native_transport.alive?
4239

43-
# The native transport expects the body without "jsonrpc" key added yet
44-
# and will add IDs automatically
45-
result = @native_transport.request(request, add_id: true, wait_for_response: true)
40+
unless request["id"] || request[:id]
41+
request["id"] = SecureRandom.uuid
42+
end
43+
result = @native_transport.request(request, wait_for_response: true)
4644

47-
# Convert Result object to hash expected by MCP::Client
4845
if result.is_a?(RubyLLM::MCP::Result)
4946
result.response
5047
else

lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,13 @@ def initialize(command:, args: [], env: {}, request_timeout: 10_000)
1818
request_timeout: request_timeout
1919
)
2020

21-
# Set transport reference so coordinator can send notifications
2221
@coordinator.transport = @native_transport
2322
end
2423

25-
# Start the stdio process
2624
def start
2725
@native_transport.start
2826
end
2927

30-
# Close the stdio process
3128
def close
3229
@native_transport.close
3330
end
@@ -38,14 +35,13 @@ def close
3835
# @param request [Hash] A JSON-RPC request object
3936
# @return [Hash] A JSON-RPC response object
4037
def send_request(request:)
41-
# Ensure transport is started
4238
start unless @native_transport.alive?
4339

44-
# The native transport expects the body without "jsonrpc" key added yet
45-
# and will add IDs automatically
46-
result = @native_transport.request(request, add_id: false, wait_for_response: true)
40+
unless request["id"] || request[:id]
41+
request["id"] = SecureRandom.uuid
42+
end
43+
result = @native_transport.request(request, wait_for_response: true)
4744

48-
# Convert Result object to hash expected by MCP::Client
4945
if result.is_a?(RubyLLM::MCP::Result)
5046
result.response
5147
else

lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,13 @@ def initialize(url:, headers: {}, version: :http2, request_timeout: 10_000, # ru
2424
session_id: session_id
2525
)
2626

27-
# Set the transport reference on the coordinator so it can send requests
2827
@coordinator.transport = @native_transport
2928
end
3029

31-
# Start the streamable HTTP connection
32-
# This only initializes the transport layer, not the MCP protocol
3330
def start
3431
@native_transport.start
3532
end
3633

37-
# Close the streamable HTTP connection
3834
def close
3935
@initialized = false
4036
@native_transport.close
@@ -52,11 +48,11 @@ def send_request(request:)
5248
perform_initialization
5349
end
5450

55-
# Pass the request through to native transport
56-
# add_id: true because MCP SDK provides requests without IDs
57-
result = @native_transport.request(request, add_id: true, wait_for_response: true)
51+
unless request["id"] || request[:id]
52+
request["id"] = SecureRandom.uuid
53+
end
54+
result = @native_transport.request(request, wait_for_response: true)
5855

59-
# Convert Result object to hash expected by MCP::Client
6056
if result.is_a?(RubyLLM::MCP::Result)
6157
result.response
6258
else
@@ -68,8 +64,11 @@ def send_request(request:)
6864

6965
def perform_initialization
7066
# Send initialization request
71-
init_request = RubyLLM::MCP::Native::Requests::Initialization.new(@coordinator).initialize_body
72-
result = @native_transport.request(init_request, add_id: true, wait_for_response: true)
67+
init_request = RubyLLM::MCP::Native::Messages::Requests.initialize(
68+
protocol_version: @coordinator.protocol_version,
69+
capabilities: @coordinator.client_capabilities
70+
)
71+
result = @native_transport.request(init_request, wait_for_response: true)
7372

7473
if result.is_a?(RubyLLM::MCP::Result) && result.error?
7574
raise RubyLLM::MCP::Errors::TransportError.new(
@@ -78,11 +77,8 @@ def perform_initialization
7877
)
7978
end
8079

81-
initialized_notification = {
82-
"jsonrpc" => "2.0",
83-
"method" => "notifications/initialized"
84-
}
85-
@native_transport.request(initialized_notification, add_id: false, wait_for_response: false)
80+
initialized_notification = RubyLLM::MCP::Native::Messages::Notifications.initialized
81+
@native_transport.request(initialized_notification, wait_for_response: false)
8682

8783
@initialized = true
8884
end

0 commit comments

Comments
 (0)