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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Native
6
+ class Notification
7
+ attr_reader :type, :params
8
+
9
+ def initialize(response)
10
+ @type = response["method"]
11
+ @params = response["params"]
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Native
6
+ module Protocol
7
+ module_function
8
+
9
+ LATEST_PROTOCOL_VERSION = "2025-11-25"
10
+ EXTENSIONS_PROTOCOL_VERSION = "2025-06-18"
11
+ DRAFT_PROTOCOL_VERSION = "2026-01-26"
12
+ DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"
13
+ SUPPORTED_PROTOCOL_VERSIONS = [
14
+ DRAFT_PROTOCOL_VERSION,
15
+ LATEST_PROTOCOL_VERSION,
16
+ EXTENSIONS_PROTOCOL_VERSION,
17
+ "2025-03-26",
18
+ "2024-11-05",
19
+ "2024-10-07"
20
+ ].freeze
21
+
22
+ def supported_version?(version)
23
+ SUPPORTED_PROTOCOL_VERSIONS.include?(version)
24
+ end
25
+
26
+ def supported_versions
27
+ SUPPORTED_PROTOCOL_VERSIONS
28
+ end
29
+
30
+ def latest_version
31
+ LATEST_PROTOCOL_VERSION
32
+ end
33
+
34
+ def draft_version
35
+ DRAFT_PROTOCOL_VERSION
36
+ end
37
+
38
+ def default_negotiated_version
39
+ DEFAULT_NEGOTIATED_PROTOCOL_VERSION
40
+ end
41
+
42
+ def date_version?(value)
43
+ return false unless value.is_a?(String)
44
+
45
+ /\A\d{4}-\d{2}-\d{2}\z/.match?(value)
46
+ end
47
+
48
+ def compare_date_versions(version_a, version_b)
49
+ return nil unless date_version?(version_a) && date_version?(version_b)
50
+
51
+ Date.iso8601(version_a) <=> Date.iso8601(version_b)
52
+ rescue Date::Error
53
+ nil
54
+ end
55
+
56
+ def draft_or_newer?(version)
57
+ return false if version.nil?
58
+
59
+ normalized = version.to_s
60
+ return true if normalized.start_with?("DRAFT-")
61
+
62
+ comparison = compare_date_versions(normalized, DRAFT_PROTOCOL_VERSION)
63
+ !comparison.nil? && comparison >= 0
64
+ end
65
+
66
+ # Extensions are part of the stable protocol track from 2025-06-18 onward.
67
+ def extensions_supported?(version)
68
+ return false if version.nil?
69
+
70
+ normalized = version.to_s
71
+ return true if normalized.start_with?("DRAFT-")
72
+
73
+ comparison = compare_date_versions(normalized, EXTENSIONS_PROTOCOL_VERSION)
74
+ !comparison.nil? && comparison >= 0
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Native
6
+ class ResponseHandler
7
+ attr_reader :coordinator
8
+
9
+ def initialize(coordinator)
10
+ @coordinator = coordinator
11
+ end
12
+
13
+ def execute(result)
14
+ operation = CancellableOperation.new(result.id)
15
+ coordinator.register_in_flight_request(result.id, operation)
16
+ is_deferred = false
17
+
18
+ begin
19
+ # Execute in a separate thread that can be terminated on cancellation
20
+ operation.execute do
21
+ handled, deferred = dispatch_request(result)
22
+ is_deferred = deferred
23
+ handled
24
+ end
25
+ rescue Errors::RequestCancelled => e
26
+ RubyLLM::MCP.logger.info("Request #{result.id} was cancelled: #{e.message}")
27
+ # Don't send response - cancellation means result is unused
28
+ # Clean up if this was a deferred elicitation
29
+ Handlers::ElicitationRegistry.remove(result.id) if is_deferred
30
+ true
31
+ ensure
32
+ # Only unregister if not deferred (async operations stay registered)
33
+ coordinator.unregister_in_flight_request(result.id) unless is_deferred
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def dispatch_request(result)
40
+ case result.method
41
+ when Native::Messages::METHOD_PING
42
+ coordinator.ping_response(id: result.id)
43
+ [true, false]
44
+ when "roots/list"
45
+ handle_roots_response(result)
46
+ [true, false]
47
+ when "sampling/createMessage"
48
+ handle_sampling_response(result)
49
+ [true, false]
50
+ when "elicitation/create"
51
+ [true, handle_elicitation_response(result)]
52
+ when Native::Messages::METHOD_TASKS_LIST
53
+ handle_tasks_list_response(result)
54
+ [true, false]
55
+ when Native::Messages::METHOD_TASKS_GET
56
+ handle_task_get_response(result)
57
+ [true, false]
58
+ when Native::Messages::METHOD_TASKS_RESULT
59
+ handle_task_result_response(result)
60
+ [true, false]
61
+ when Native::Messages::METHOD_TASKS_CANCEL
62
+ handle_task_cancel_response(result)
63
+ [true, false]
64
+ else
65
+ handle_unknown_request(result)
66
+ RubyLLM::MCP.logger.error("MCP client was sent unknown method type and \
67
+ could not respond: #{result.inspect}.")
68
+ [false, false]
69
+ end
70
+ end
71
+
72
+ def handle_roots_response(result)
73
+ RubyLLM::MCP.logger.info("Roots request: #{result.inspect}")
74
+ roots_paths = coordinator.roots_paths
75
+ if roots_paths&.any?
76
+ coordinator.roots_list_response(id: result.id)
77
+ else
78
+ coordinator.error_response(
79
+ id: result.id,
80
+ message: "Roots are not enabled",
81
+ code: Native::JsonRpc::ErrorCodes::SERVER_ERROR
82
+ )
83
+ end
84
+ rescue StandardError => e
85
+ RubyLLM::MCP.logger.error("Error in roots request: #{e.message}\n#{e.backtrace.join("\n")}")
86
+ coordinator.error_response(
87
+ id: result.id,
88
+ message: "Internal error processing roots request",
89
+ code: Native::JsonRpc::ErrorCodes::INTERNAL_ERROR,
90
+ data: { detail: e.message }
91
+ )
92
+ end
93
+
94
+ def handle_sampling_response(result)
95
+ unless MCP.config.sampling.enabled?
96
+ RubyLLM::MCP.logger.info("Sampling is disabled, yet server requested sampling")
97
+ coordinator.error_response(
98
+ id: result.id,
99
+ message: "Sampling is disabled",
100
+ code: Native::JsonRpc::ErrorCodes::SERVER_ERROR
101
+ )
102
+ return
103
+ end
104
+
105
+ RubyLLM::MCP.logger.info("Sampling request: #{result.inspect}")
106
+ Sample.new(result, coordinator).execute
107
+ rescue StandardError => e
108
+ RubyLLM::MCP.logger.error("Error in sampling request: #{e.message}\n#{e.backtrace.join("\n")}")
109
+ coordinator.error_response(
110
+ id: result.id,
111
+ message: "Internal error processing sampling request",
112
+ code: Native::JsonRpc::ErrorCodes::INTERNAL_ERROR,
113
+ data: { detail: e.message }
114
+ )
115
+ end
116
+
117
+ def handle_elicitation_response(result)
118
+ RubyLLM::MCP.logger.info("Elicitation request: #{result.inspect}")
119
+ elicitation = Elicitation.new(coordinator, result)
120
+ elicitation.execute
121
+
122
+ # Return true if this elicitation is deferred (async)
123
+ elicitation.instance_variable_get(:@deferred)
124
+ rescue StandardError => e
125
+ RubyLLM::MCP.logger.error("Error in elicitation request: #{e.message}\n#{e.backtrace.join("\n")}")
126
+ coordinator.error_response(
127
+ id: result.id,
128
+ message: "Internal error processing elicitation request",
129
+ code: Native::JsonRpc::ErrorCodes::INTERNAL_ERROR,
130
+ data: { detail: e.message }
131
+ )
132
+ false
133
+ end
134
+
135
+ def handle_unknown_request(result)
136
+ coordinator.error_response(
137
+ id: result.id,
138
+ message: "Method not found: #{result.method}",
139
+ code: Native::JsonRpc::ErrorCodes::METHOD_NOT_FOUND
140
+ )
141
+ end
142
+
143
+ def handle_tasks_list_response(result)
144
+ coordinator.result_response(
145
+ id: result.id,
146
+ value: { tasks: coordinator.task_registry.tasks }
147
+ )
148
+ end
149
+
150
+ def handle_task_get_response(result)
151
+ task_id = result.params["taskId"]
152
+ return error_invalid_task_id(result.id) if task_id.nil? || task_id.empty?
153
+
154
+ task = coordinator.task_registry.task(task_id)
155
+ if task.nil?
156
+ error_unknown_task(result.id, task_id)
157
+ else
158
+ coordinator.result_response(id: result.id, value: task)
159
+ end
160
+ end
161
+
162
+ def handle_task_result_response(result)
163
+ task_id = result.params["taskId"]
164
+ return error_invalid_task_id(result.id) if task_id.nil? || task_id.empty?
165
+
166
+ payload = coordinator.task_registry.payload(task_id)
167
+ if payload.nil?
168
+ error_unknown_task(result.id, task_id)
169
+ else
170
+ coordinator.result_response(id: result.id, value: payload)
171
+ end
172
+ end
173
+
174
+ def handle_task_cancel_response(result)
175
+ task_id = result.params["taskId"]
176
+ return error_invalid_task_id(result.id) if task_id.nil? || task_id.empty?
177
+
178
+ task = coordinator.task_registry.update_status(
179
+ task_id,
180
+ status: "cancelled",
181
+ status_message: "Cancelled by server request"
182
+ )
183
+
184
+ coordinator.result_response(
185
+ id: result.id,
186
+ value: task || build_missing_task(task_id, "cancelled", "Task not found; treated as cancelled")
187
+ )
188
+ end
189
+
190
+ def error_invalid_task_id(request_id)
191
+ coordinator.error_response(
192
+ id: request_id,
193
+ message: "Invalid task request: taskId is required",
194
+ code: Native::JsonRpc::ErrorCodes::INVALID_PARAMS
195
+ )
196
+ end
197
+
198
+ def error_unknown_task(request_id, task_id)
199
+ coordinator.error_response(
200
+ id: request_id,
201
+ message: "Task not found: #{task_id}",
202
+ code: Native::JsonRpc::ErrorCodes::INVALID_PARAMS
203
+ )
204
+ end
205
+
206
+ def build_missing_task(task_id, status, status_message)
207
+ timestamp = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
208
+ {
209
+ "taskId" => task_id,
210
+ "status" => status,
211
+ "statusMessage" => status_message,
212
+ "createdAt" => timestamp,
213
+ "lastUpdatedAt" => timestamp,
214
+ "ttl" => 0
215
+ }
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Native
6
+ # In-memory task state cache used by the native client.
7
+ # This keeps task metadata/status synchronized across list/get/cancel calls
8
+ # and out-of-band status notifications.
9
+ class TaskRegistry
10
+ def initialize
11
+ @tasks = {}
12
+ @payloads = {}
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def upsert(task_hash)
17
+ return if task_hash.nil? || task_hash["taskId"].nil?
18
+
19
+ @mutex.synchronize do
20
+ @tasks[task_hash["taskId"]] = task_hash
21
+ end
22
+ end
23
+
24
+ def upsert_many(task_hashes)
25
+ Array(task_hashes).each { |task| upsert(task) }
26
+ end
27
+
28
+ def store_payload(task_id, payload)
29
+ return if task_id.nil?
30
+
31
+ @mutex.synchronize do
32
+ @payloads[task_id] = payload
33
+ end
34
+ end
35
+
36
+ def task(task_id)
37
+ @mutex.synchronize { @tasks[task_id] }
38
+ end
39
+
40
+ def payload(task_id)
41
+ @mutex.synchronize { @payloads[task_id] }
42
+ end
43
+
44
+ def tasks
45
+ @mutex.synchronize { @tasks.values }
46
+ end
47
+
48
+ def update_status(task_id, status:, status_message: nil)
49
+ @mutex.synchronize do
50
+ task = @tasks[task_id]
51
+ return nil unless task
52
+
53
+ task["status"] = status
54
+ task["statusMessage"] = status_message unless status_message.nil?
55
+ task["lastUpdatedAt"] = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
56
+ task
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Native
6
+ class Transport
7
+ class << self
8
+ def transports
9
+ @transports ||= {}
10
+ end
11
+
12
+ def register_transport(transport_type, transport_class)
13
+ transports[transport_type] = transport_class
14
+ end
15
+ end
16
+
17
+ extend Forwardable
18
+
19
+ register_transport(:sse, RubyLLM::MCP::Native::Transports::SSE)
20
+ register_transport(:stdio, RubyLLM::MCP::Native::Transports::Stdio)
21
+ register_transport(:streamable, RubyLLM::MCP::Native::Transports::StreamableHTTP)
22
+ register_transport(:streamable_http, RubyLLM::MCP::Native::Transports::StreamableHTTP)
23
+
24
+ attr_reader :transport_type, :coordinator, :config, :pid
25
+
26
+ def initialize(transport_type, coordinator, config:)
27
+ @transport_type = transport_type
28
+ @coordinator = coordinator
29
+ @config = config
30
+ @pid = Process.pid
31
+ end
32
+
33
+ def_delegators :transport_protocol, :request, :alive?, :close, :start, :set_protocol_version
34
+
35
+ def transport_protocol
36
+ if @pid != Process.pid
37
+ @pid = Process.pid
38
+ @transport_protocol = nil
39
+ @transport_protocol = build_transport
40
+ end
41
+
42
+ @transport_protocol ||= build_transport
43
+ end
44
+
45
+ private
46
+
47
+ def build_transport
48
+ unless RubyLLM::MCP::Native::Transport.transports.key?(transport_type)
49
+ supported_types = RubyLLM::MCP::Native::Transport.transports.keys.join(", ")
50
+ message = "Invalid transport type: :#{transport_type}. Supported types are #{supported_types}"
51
+ raise Errors::InvalidTransportType.new(message: message)
52
+ end
53
+
54
+ # Extract and merge options if present (from OAuth helper preparation)
55
+ transport_config = config.dup
56
+ if transport_config[:options]
57
+ options = transport_config.delete(:options)
58
+ transport_config.merge!(options)
59
+ end
60
+
61
+ # Handle SSE transport specially - it uses options hash pattern
62
+ if transport_type == :sse
63
+ url = transport_config.delete(:url) || transport_config.delete("url")
64
+ request_timeout = transport_config.delete(:request_timeout) ||
65
+ transport_config.delete("request_timeout") ||
66
+ MCP.config.request_timeout
67
+ # Everything else goes into options
68
+ options_hash = transport_config.dup
69
+ transport_config.clear
70
+ transport_config[:url] = url
71
+ transport_config[:request_timeout] = request_timeout
72
+ transport_config[:options] = options_hash
73
+ end
74
+
75
+ # Remove OAuth-specific params from transports that don't support them
76
+ # This allows other arbitrary params (like timeout) to pass through for testing
77
+ unless %i[streamable streamable_http sse].include?(transport_type)
78
+ transport_config.delete(:oauth_provider)
79
+ transport_config.delete(:oauth)
80
+ end
81
+
82
+ transport_klass = RubyLLM::MCP::Native::Transport.transports[transport_type]
83
+ transport_klass.new(coordinator: coordinator, **transport_config)
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end