ruby_llm-mcp 0.7.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -162
  3. data/lib/generators/ruby_llm/mcp/{install_generator.rb → install/install_generator.rb} +4 -2
  4. data/lib/generators/ruby_llm/mcp/{templates → install/templates}/initializer.rb +21 -4
  5. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
  6. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  16. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  17. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  18. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  19. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +215 -0
  20. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +413 -0
  21. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +41 -0
  22. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +56 -0
  23. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +56 -0
  24. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +90 -0
  25. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +216 -0
  26. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  27. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +36 -0
  28. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
  29. data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
  30. data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
  31. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +427 -0
  32. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  33. data/lib/ruby_llm/mcp/auth/discoverer.rb +255 -0
  34. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +122 -0
  35. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +67 -0
  36. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  37. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  38. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  39. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
  40. data/lib/ruby_llm/mcp/auth/memory_storage.rb +91 -0
  41. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +341 -0
  42. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  43. data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
  44. data/lib/ruby_llm/mcp/auth/token_manager.rb +307 -0
  45. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
  46. data/lib/ruby_llm/mcp/auth/url_builder.rb +135 -0
  47. data/lib/ruby_llm/mcp/auth.rb +371 -0
  48. data/lib/ruby_llm/mcp/client.rb +312 -35
  49. data/lib/ruby_llm/mcp/configuration.rb +199 -24
  50. data/lib/ruby_llm/mcp/elicitation.rb +261 -14
  51. data/lib/ruby_llm/mcp/errors.rb +29 -0
  52. data/lib/ruby_llm/mcp/extensions/apps/constants.rb +28 -0
  53. data/lib/ruby_llm/mcp/extensions/apps/resource_metadata.rb +24 -0
  54. data/lib/ruby_llm/mcp/extensions/apps/tool_metadata.rb +45 -0
  55. data/lib/ruby_llm/mcp/extensions/configuration.rb +72 -0
  56. data/lib/ruby_llm/mcp/extensions/constants.rb +16 -0
  57. data/lib/ruby_llm/mcp/extensions/registry.rb +85 -0
  58. data/lib/ruby_llm/mcp/handlers/approval_decision.rb +90 -0
  59. data/lib/ruby_llm/mcp/handlers/async_response.rb +181 -0
  60. data/lib/ruby_llm/mcp/handlers/concerns/approval_actions.rb +42 -0
  61. data/lib/ruby_llm/mcp/handlers/concerns/async_execution.rb +80 -0
  62. data/lib/ruby_llm/mcp/handlers/concerns/elicitation_actions.rb +42 -0
  63. data/lib/ruby_llm/mcp/handlers/concerns/error_handling.rb +29 -0
  64. data/lib/ruby_llm/mcp/handlers/concerns/guard_checks.rb +72 -0
  65. data/lib/ruby_llm/mcp/handlers/concerns/lifecycle.rb +84 -0
  66. data/lib/ruby_llm/mcp/handlers/concerns/logging.rb +19 -0
  67. data/lib/ruby_llm/mcp/handlers/concerns/model_filtering.rb +36 -0
  68. data/lib/ruby_llm/mcp/handlers/concerns/options.rb +83 -0
  69. data/lib/ruby_llm/mcp/handlers/concerns/registry_integration.rb +54 -0
  70. data/lib/ruby_llm/mcp/handlers/concerns/sampling_actions.rb +84 -0
  71. data/lib/ruby_llm/mcp/handlers/concerns/timeouts.rb +52 -0
  72. data/lib/ruby_llm/mcp/handlers/concerns/tool_filtering.rb +50 -0
  73. data/lib/ruby_llm/mcp/handlers/elicitation_handler.rb +58 -0
  74. data/lib/ruby_llm/mcp/handlers/elicitation_registry.rb +203 -0
  75. data/lib/ruby_llm/mcp/handlers/human_in_the_loop_handler.rb +93 -0
  76. data/lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb +271 -0
  77. data/lib/ruby_llm/mcp/handlers/promise.rb +192 -0
  78. data/lib/ruby_llm/mcp/handlers/sampling_handler.rb +64 -0
  79. data/lib/ruby_llm/mcp/handlers.rb +14 -0
  80. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +94 -0
  81. data/lib/ruby_llm/mcp/native/client.rb +551 -0
  82. data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
  83. data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
  84. data/lib/ruby_llm/mcp/native/messages/notifications.rb +60 -0
  85. data/lib/ruby_llm/mcp/native/messages/requests.rb +267 -0
  86. data/lib/ruby_llm/mcp/native/messages/responses.rb +114 -0
  87. data/lib/ruby_llm/mcp/native/messages.rb +43 -0
  88. data/lib/ruby_llm/mcp/native/notification.rb +16 -0
  89. data/lib/ruby_llm/mcp/native/protocol.rb +79 -0
  90. data/lib/ruby_llm/mcp/native/response_handler.rb +220 -0
  91. data/lib/ruby_llm/mcp/native/task_registry.rb +62 -0
  92. data/lib/ruby_llm/mcp/native/transport.rb +88 -0
  93. data/lib/ruby_llm/mcp/native/transports/sse.rb +655 -0
  94. data/lib/ruby_llm/mcp/native/transports/stdio.rb +367 -0
  95. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +1024 -0
  96. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
  97. data/lib/ruby_llm/mcp/native/transports/support/rate_limiter.rb +49 -0
  98. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
  99. data/lib/ruby_llm/mcp/native.rb +12 -0
  100. data/lib/ruby_llm/mcp/notification_handler.rb +43 -5
  101. data/lib/ruby_llm/mcp/prompt.rb +7 -7
  102. data/lib/ruby_llm/mcp/railtie.rb +7 -13
  103. data/lib/ruby_llm/mcp/resource.rb +17 -8
  104. data/lib/ruby_llm/mcp/resource_template.rb +8 -7
  105. data/lib/ruby_llm/mcp/result.rb +8 -4
  106. data/lib/ruby_llm/mcp/roots.rb +4 -4
  107. data/lib/ruby_llm/mcp/sample.rb +83 -13
  108. data/lib/ruby_llm/mcp/schema_validator.rb +33 -0
  109. data/lib/ruby_llm/mcp/server_capabilities.rb +41 -0
  110. data/lib/ruby_llm/mcp/task.rb +65 -0
  111. data/lib/ruby_llm/mcp/tool.rb +33 -27
  112. data/lib/ruby_llm/mcp/version.rb +1 -1
  113. data/lib/ruby_llm/mcp.rb +37 -7
  114. data/lib/tasks/smoke.rake +66 -0
  115. metadata +115 -39
  116. data/lib/generators/ruby_llm/mcp/templates/mcps.yml +0 -9
  117. data/lib/ruby_llm/mcp/coordinator.rb +0 -293
  118. data/lib/ruby_llm/mcp/notifications/cancelled.rb +0 -32
  119. data/lib/ruby_llm/mcp/notifications/initialize.rb +0 -24
  120. data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +0 -26
  121. data/lib/ruby_llm/mcp/protocol.rb +0 -34
  122. data/lib/ruby_llm/mcp/requests/completion_prompt.rb +0 -50
  123. data/lib/ruby_llm/mcp/requests/completion_resource.rb +0 -50
  124. data/lib/ruby_llm/mcp/requests/initialization.rb +0 -34
  125. data/lib/ruby_llm/mcp/requests/logging_set_level.rb +0 -28
  126. data/lib/ruby_llm/mcp/requests/ping.rb +0 -24
  127. data/lib/ruby_llm/mcp/requests/prompt_call.rb +0 -32
  128. data/lib/ruby_llm/mcp/requests/prompt_list.rb +0 -31
  129. data/lib/ruby_llm/mcp/requests/resource_list.rb +0 -31
  130. data/lib/ruby_llm/mcp/requests/resource_read.rb +0 -30
  131. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +0 -31
  132. data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +0 -30
  133. data/lib/ruby_llm/mcp/requests/shared/meta.rb +0 -32
  134. data/lib/ruby_llm/mcp/requests/shared/pagination.rb +0 -17
  135. data/lib/ruby_llm/mcp/requests/tool_call.rb +0 -35
  136. data/lib/ruby_llm/mcp/requests/tool_list.rb +0 -31
  137. data/lib/ruby_llm/mcp/response_handler.rb +0 -67
  138. data/lib/ruby_llm/mcp/responses/elicitation.rb +0 -33
  139. data/lib/ruby_llm/mcp/responses/error.rb +0 -33
  140. data/lib/ruby_llm/mcp/responses/ping.rb +0 -28
  141. data/lib/ruby_llm/mcp/responses/roots_list.rb +0 -31
  142. data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +0 -50
  143. data/lib/ruby_llm/mcp/transport.rb +0 -58
  144. data/lib/ruby_llm/mcp/transports/sse.rb +0 -341
  145. data/lib/ruby_llm/mcp/transports/stdio.rb +0 -230
  146. data/lib/ruby_llm/mcp/transports/streamable_http.rb +0 -723
  147. data/lib/ruby_llm/mcp/transports/support/http_client.rb +0 -28
  148. data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +0 -47
  149. data/lib/ruby_llm/mcp/transports/support/timeout.rb +0 -34
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Native
6
+ module Transports
7
+ class Stdio
8
+ include Support::Timeout
9
+
10
+ attr_reader :command, :stdin, :stdout, :stderr, :id, :coordinator
11
+
12
+ # Default environment that merges with user-provided env
13
+ # This ensures PATH and other critical env vars are preserved
14
+ DEFAULT_ENV = ENV.to_h.freeze
15
+
16
+ def initialize(command:, coordinator:, request_timeout:, args: [], env: {})
17
+ @request_timeout = request_timeout
18
+ @command = command
19
+ @coordinator = coordinator
20
+ @args = args
21
+ # Merge provided env with default environment (user env takes precedence)
22
+ @env = DEFAULT_ENV.merge(env || {})
23
+ @client_id = SecureRandom.uuid
24
+
25
+ @id_counter = 0
26
+ @id_mutex = Mutex.new
27
+ @pending_requests = {}
28
+ @pending_mutex = Mutex.new
29
+ @state_mutex = Mutex.new
30
+ @running = false
31
+ @reader_thread = nil
32
+ @stderr_thread = nil
33
+ end
34
+
35
+ def request(body, wait_for_response: true)
36
+ request_id = prepare_request_id(body, wait_for_response)
37
+ response_queue = register_pending_request(request_id, wait_for_response)
38
+
39
+ send_request(body, request_id)
40
+
41
+ return unless wait_for_response
42
+
43
+ wait_for_request_response(request_id, response_queue)
44
+ end
45
+
46
+ def alive?
47
+ running?
48
+ end
49
+
50
+ def running?
51
+ @state_mutex.synchronize { @running }
52
+ end
53
+
54
+ def start
55
+ @state_mutex.synchronize do
56
+ return if @running
57
+
58
+ @running = true
59
+ end
60
+ start_process
61
+ end
62
+
63
+ def close
64
+ @state_mutex.synchronize do
65
+ return unless @running
66
+
67
+ @running = false
68
+ end
69
+ shutdown_process
70
+ fail_pending_requests!(RubyLLM::MCP::Errors::TransportError.new(message: "Transport closed"))
71
+ end
72
+
73
+ def set_protocol_version(version)
74
+ @protocol_version = version
75
+ end
76
+
77
+ private
78
+
79
+ def prepare_request_id(body, wait_for_response)
80
+ request_id = body["id"] || body[:id]
81
+
82
+ if wait_for_response && request_id.nil?
83
+ raise ArgumentError, "Request ID must be provided in message body when wait_for_response is true"
84
+ end
85
+
86
+ request_id
87
+ end
88
+
89
+ def register_pending_request(request_id, wait_for_response)
90
+ return nil unless wait_for_response
91
+
92
+ response_queue = Queue.new
93
+ @pending_mutex.synchronize do
94
+ @pending_requests[request_id.to_s] = response_queue
95
+ end
96
+ response_queue
97
+ end
98
+
99
+ def send_request(body, request_id)
100
+ body = JSON.generate(body)
101
+ RubyLLM::MCP.logger.debug "Sending Request: #{body}"
102
+ stdin = @state_mutex.synchronize { @stdin }
103
+ unless stdin
104
+ raise RubyLLM::MCP::Errors::TransportError.new(message: "Transport is not connected")
105
+ end
106
+
107
+ stdin.puts(body)
108
+ stdin.flush
109
+ rescue IOError, Errno::EPIPE => e
110
+ @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) } if request_id
111
+ raise RubyLLM::MCP::Errors::TransportError.new(message: e.message, error: e)
112
+ rescue RubyLLM::MCP::Errors::TransportError => e
113
+ @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) } if request_id
114
+ raise e
115
+ end
116
+
117
+ def wait_for_request_response(request_id, response_queue)
118
+ result = with_timeout(@request_timeout / 1000, request_id: request_id) do
119
+ response_queue.pop
120
+ end
121
+ raise result if result.is_a?(RubyLLM::MCP::Errors::TransportError)
122
+
123
+ result
124
+ rescue RubyLLM::MCP::Errors::TimeoutError => e
125
+ @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
126
+ log_message = "Stdio request timeout (ID: #{request_id}) after #{@request_timeout / 1000} seconds"
127
+ RubyLLM::MCP.logger.error(log_message)
128
+ raise e
129
+ end
130
+
131
+ def start_process
132
+ shutdown_process if @stdin || @stdout || @stderr || @wait_thread
133
+
134
+ # Always pass env - it now includes defaults merged with user overrides
135
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@env, @command, *@args)
136
+
137
+ start_reader_thread
138
+ start_stderr_thread
139
+ end
140
+
141
+ def shutdown_process
142
+ close_stdin
143
+ terminate_child_process
144
+ close_output_streams
145
+ join_reader_threads
146
+ clear_process_handles
147
+ end
148
+
149
+ def close_stdin
150
+ @stdin&.close
151
+ rescue IOError, Errno::EBADF
152
+ # Already closed
153
+ end
154
+
155
+ def terminate_child_process
156
+ return unless @wait_thread
157
+
158
+ @wait_thread.join(1) if @wait_thread.alive? # 1s grace period
159
+ send_signal_to_process("TERM", 2) if @wait_thread.alive?
160
+ send_signal_to_process("KILL", 0) if @wait_thread.alive?
161
+ end
162
+
163
+ def send_signal_to_process(signal, wait_time)
164
+ Process.kill(signal, @wait_thread.pid)
165
+ @wait_thread.join(wait_time) if wait_time.positive?
166
+ rescue StandardError => e
167
+ RubyLLM::MCP.logger.debug "Error sending #{signal}: #{e.message}"
168
+ end
169
+
170
+ def close_output_streams
171
+ [@stdout, @stderr].each do |stream|
172
+ stream&.close
173
+ rescue IOError, Errno::EBADF
174
+ # Already closed
175
+ end
176
+ end
177
+
178
+ def join_reader_threads
179
+ [@reader_thread, @stderr_thread].each do |thread|
180
+ next unless thread&.alive?
181
+ next if Thread.current == thread # Avoid self-join deadlock
182
+
183
+ thread.join(1)
184
+ rescue StandardError => e
185
+ RubyLLM::MCP.logger.debug "Error joining thread: #{e.message}"
186
+ end
187
+ end
188
+
189
+ def clear_process_handles
190
+ @stdin = @stdout = @stderr = nil
191
+ @wait_thread = @reader_thread = @stderr_thread = nil
192
+ end
193
+
194
+ def fail_pending_requests!(error)
195
+ @pending_mutex.synchronize do
196
+ @pending_requests.each_value do |queue|
197
+ queue.push(error)
198
+ end
199
+ @pending_requests.clear
200
+ end
201
+ end
202
+
203
+ def safe_close_with_error(error)
204
+ fail_pending_requests!(error)
205
+ close
206
+ end
207
+
208
+ def start_reader_thread
209
+ @reader_thread = Thread.new do
210
+ read_stdout_loop
211
+ end
212
+ end
213
+
214
+ def read_stdout_loop
215
+ while running?
216
+ begin
217
+ handle_stdout_read
218
+ rescue IOError, Errno::EPIPE => e
219
+ handle_stream_error(e, "Reader")
220
+ break unless running?
221
+ rescue StandardError => e
222
+ RubyLLM::MCP.logger.error "Error in reader thread: #{e.message}, #{e.backtrace.join("\n")}"
223
+ sleep 1
224
+ end
225
+ end
226
+ end
227
+
228
+ def handle_stdout_read
229
+ if @stdout.closed? || @wait_thread.nil? || !@wait_thread.alive?
230
+ # Process is dead - if we're still running, this is an error
231
+ if running?
232
+ error = RubyLLM::MCP::Errors::TransportError.new(
233
+ message: "Process terminated unexpectedly"
234
+ )
235
+ safe_close_with_error(error)
236
+ end
237
+ return
238
+ end
239
+
240
+ line = @stdout.gets
241
+ return unless line && !line.strip.empty?
242
+
243
+ process_response(line.strip)
244
+ end
245
+
246
+ def handle_stream_error(error, stream_name)
247
+ if running?
248
+ RubyLLM::MCP.logger.error "#{stream_name} error: #{error.message}. Closing transport."
249
+ safe_close_with_error(error)
250
+ else
251
+ RubyLLM::MCP.logger.debug "#{stream_name} thread exiting during shutdown"
252
+ end
253
+ end
254
+
255
+ def start_stderr_thread
256
+ @stderr_thread = Thread.new do
257
+ read_stderr_loop
258
+ end
259
+ end
260
+
261
+ def read_stderr_loop
262
+ while running?
263
+ begin
264
+ handle_stderr_read
265
+ rescue IOError, Errno::EPIPE => e
266
+ handle_stream_error(e, "Stderr reader")
267
+ break unless running?
268
+ rescue StandardError => e
269
+ RubyLLM::MCP.logger.error "Error in stderr thread: #{e.message}"
270
+ sleep 1
271
+ end
272
+ end
273
+ end
274
+
275
+ def handle_stderr_read
276
+ if @stderr.closed? || @wait_thread.nil? || !@wait_thread.alive?
277
+ return
278
+ end
279
+
280
+ line = @stderr.gets
281
+ return unless line && !line.strip.empty?
282
+
283
+ RubyLLM::MCP.logger.info(line.strip)
284
+ end
285
+
286
+ def process_response(line)
287
+ response = parse_and_validate_envelope(line)
288
+ return unless response
289
+
290
+ request_id = response["id"]&.to_s
291
+ result = RubyLLM::MCP::Result.new(response)
292
+ RubyLLM::MCP.logger.debug "Result Received: #{result.inspect}"
293
+
294
+ result = @coordinator.process_result(result)
295
+ return if result.nil?
296
+
297
+ @pending_mutex.synchronize do
298
+ if result.matching_id?(request_id) && @pending_requests.key?(request_id)
299
+ response_queue = @pending_requests.delete(request_id)
300
+ response_queue&.push(result)
301
+ end
302
+ end
303
+ end
304
+
305
+ def parse_and_validate_envelope(line)
306
+ response = JSON.parse(line)
307
+
308
+ # Validate JSON-RPC envelope
309
+ validator = Native::JsonRpc::EnvelopeValidator.new(response)
310
+ unless validator.valid?
311
+ RubyLLM::MCP.logger.error("Invalid JSON-RPC envelope: #{validator.error_message}\nRaw: #{line}")
312
+
313
+ # If this is a request with an id, send an error response
314
+ if response.is_a?(Hash) && response["id"]
315
+ send_invalid_request_error(response["id"], validator.error_message)
316
+ end
317
+
318
+ return nil
319
+ end
320
+
321
+ response
322
+ rescue JSON::ParserError => e
323
+ RubyLLM::MCP.logger.error("JSON parse error: #{e.message}\nRaw response: #{line}")
324
+
325
+ # JSON-RPC 2.0 §5.1: Parse error should return error with id: null
326
+ send_parse_error(e.message)
327
+ nil
328
+ end
329
+
330
+ def send_invalid_request_error(id, detail)
331
+ error_body = Native::Messages::Responses.error(
332
+ id: id,
333
+ message: "Invalid Request",
334
+ code: Native::JsonRpc::ErrorCodes::INVALID_REQUEST,
335
+ data: { detail: detail }
336
+ )
337
+
338
+ begin
339
+ body_json = JSON.generate(error_body)
340
+ @stdin.puts(body_json)
341
+ @stdin.flush
342
+ rescue IOError, Errno::EPIPE => e
343
+ RubyLLM::MCP.logger.error("Failed to send invalid request error: #{e.message}")
344
+ end
345
+ end
346
+
347
+ def send_parse_error(detail)
348
+ error_body = Native::Messages::Responses.error(
349
+ id: nil,
350
+ message: "Parse error",
351
+ code: Native::JsonRpc::ErrorCodes::PARSE_ERROR,
352
+ data: { detail: detail }
353
+ )
354
+
355
+ begin
356
+ body_json = JSON.generate(error_body)
357
+ @stdin.puts(body_json)
358
+ @stdin.flush
359
+ rescue IOError, Errno::EPIPE => e
360
+ RubyLLM::MCP.logger.error("Failed to send parse error: #{e.message}")
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end
366
+ end
367
+ end