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.
- checksums.yaml +4 -4
- data/README.md +144 -162
- data/lib/generators/ruby_llm/mcp/{install_generator.rb → install/install_generator.rb} +4 -2
- data/lib/generators/ruby_llm/mcp/{templates → install/templates}/initializer.rb +21 -4
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
- data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +215 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +413 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +41 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +56 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +56 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +90 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +216 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +36 -0
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +427 -0
- data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
- data/lib/ruby_llm/mcp/auth/discoverer.rb +255 -0
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +122 -0
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +67 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +91 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +341 -0
- data/lib/ruby_llm/mcp/auth/security.rb +44 -0
- data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
- data/lib/ruby_llm/mcp/auth/token_manager.rb +307 -0
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +135 -0
- data/lib/ruby_llm/mcp/auth.rb +371 -0
- data/lib/ruby_llm/mcp/client.rb +312 -35
- data/lib/ruby_llm/mcp/configuration.rb +199 -24
- data/lib/ruby_llm/mcp/elicitation.rb +261 -14
- data/lib/ruby_llm/mcp/errors.rb +29 -0
- data/lib/ruby_llm/mcp/extensions/apps/constants.rb +28 -0
- data/lib/ruby_llm/mcp/extensions/apps/resource_metadata.rb +24 -0
- data/lib/ruby_llm/mcp/extensions/apps/tool_metadata.rb +45 -0
- data/lib/ruby_llm/mcp/extensions/configuration.rb +72 -0
- data/lib/ruby_llm/mcp/extensions/constants.rb +16 -0
- data/lib/ruby_llm/mcp/extensions/registry.rb +85 -0
- data/lib/ruby_llm/mcp/handlers/approval_decision.rb +90 -0
- data/lib/ruby_llm/mcp/handlers/async_response.rb +181 -0
- data/lib/ruby_llm/mcp/handlers/concerns/approval_actions.rb +42 -0
- data/lib/ruby_llm/mcp/handlers/concerns/async_execution.rb +80 -0
- data/lib/ruby_llm/mcp/handlers/concerns/elicitation_actions.rb +42 -0
- data/lib/ruby_llm/mcp/handlers/concerns/error_handling.rb +29 -0
- data/lib/ruby_llm/mcp/handlers/concerns/guard_checks.rb +72 -0
- data/lib/ruby_llm/mcp/handlers/concerns/lifecycle.rb +84 -0
- data/lib/ruby_llm/mcp/handlers/concerns/logging.rb +19 -0
- data/lib/ruby_llm/mcp/handlers/concerns/model_filtering.rb +36 -0
- data/lib/ruby_llm/mcp/handlers/concerns/options.rb +83 -0
- data/lib/ruby_llm/mcp/handlers/concerns/registry_integration.rb +54 -0
- data/lib/ruby_llm/mcp/handlers/concerns/sampling_actions.rb +84 -0
- data/lib/ruby_llm/mcp/handlers/concerns/timeouts.rb +52 -0
- data/lib/ruby_llm/mcp/handlers/concerns/tool_filtering.rb +50 -0
- data/lib/ruby_llm/mcp/handlers/elicitation_handler.rb +58 -0
- data/lib/ruby_llm/mcp/handlers/elicitation_registry.rb +203 -0
- data/lib/ruby_llm/mcp/handlers/human_in_the_loop_handler.rb +93 -0
- data/lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb +271 -0
- data/lib/ruby_llm/mcp/handlers/promise.rb +192 -0
- data/lib/ruby_llm/mcp/handlers/sampling_handler.rb +64 -0
- data/lib/ruby_llm/mcp/handlers.rb +14 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +94 -0
- data/lib/ruby_llm/mcp/native/client.rb +551 -0
- data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +60 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +267 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +114 -0
- data/lib/ruby_llm/mcp/native/messages.rb +43 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +79 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +220 -0
- data/lib/ruby_llm/mcp/native/task_registry.rb +62 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +655 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +367 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +1024 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limiter.rb +49 -0
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
- data/lib/ruby_llm/mcp/native.rb +12 -0
- data/lib/ruby_llm/mcp/notification_handler.rb +43 -5
- data/lib/ruby_llm/mcp/prompt.rb +7 -7
- data/lib/ruby_llm/mcp/railtie.rb +7 -13
- data/lib/ruby_llm/mcp/resource.rb +17 -8
- data/lib/ruby_llm/mcp/resource_template.rb +8 -7
- data/lib/ruby_llm/mcp/result.rb +8 -4
- data/lib/ruby_llm/mcp/roots.rb +4 -4
- data/lib/ruby_llm/mcp/sample.rb +83 -13
- data/lib/ruby_llm/mcp/schema_validator.rb +33 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +41 -0
- data/lib/ruby_llm/mcp/task.rb +65 -0
- data/lib/ruby_llm/mcp/tool.rb +33 -27
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +37 -7
- data/lib/tasks/smoke.rake +66 -0
- metadata +115 -39
- data/lib/generators/ruby_llm/mcp/templates/mcps.yml +0 -9
- data/lib/ruby_llm/mcp/coordinator.rb +0 -293
- data/lib/ruby_llm/mcp/notifications/cancelled.rb +0 -32
- data/lib/ruby_llm/mcp/notifications/initialize.rb +0 -24
- data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +0 -26
- data/lib/ruby_llm/mcp/protocol.rb +0 -34
- data/lib/ruby_llm/mcp/requests/completion_prompt.rb +0 -50
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +0 -50
- data/lib/ruby_llm/mcp/requests/initialization.rb +0 -34
- data/lib/ruby_llm/mcp/requests/logging_set_level.rb +0 -28
- data/lib/ruby_llm/mcp/requests/ping.rb +0 -24
- data/lib/ruby_llm/mcp/requests/prompt_call.rb +0 -32
- data/lib/ruby_llm/mcp/requests/prompt_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resource_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resource_read.rb +0 -30
- data/lib/ruby_llm/mcp/requests/resource_template_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +0 -30
- data/lib/ruby_llm/mcp/requests/shared/meta.rb +0 -32
- data/lib/ruby_llm/mcp/requests/shared/pagination.rb +0 -17
- data/lib/ruby_llm/mcp/requests/tool_call.rb +0 -35
- data/lib/ruby_llm/mcp/requests/tool_list.rb +0 -31
- data/lib/ruby_llm/mcp/response_handler.rb +0 -67
- data/lib/ruby_llm/mcp/responses/elicitation.rb +0 -33
- data/lib/ruby_llm/mcp/responses/error.rb +0 -33
- data/lib/ruby_llm/mcp/responses/ping.rb +0 -28
- data/lib/ruby_llm/mcp/responses/roots_list.rb +0 -31
- data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +0 -50
- data/lib/ruby_llm/mcp/transport.rb +0 -58
- data/lib/ruby_llm/mcp/transports/sse.rb +0 -341
- data/lib/ruby_llm/mcp/transports/stdio.rb +0 -230
- data/lib/ruby_llm/mcp/transports/streamable_http.rb +0 -723
- data/lib/ruby_llm/mcp/transports/support/http_client.rb +0 -28
- data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +0 -47
- data/lib/ruby_llm/mcp/transports/support/timeout.rb +0 -34
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
# Native MCP protocol client implementation
|
|
7
|
+
# This is the core protocol implementation that handles all MCP operations
|
|
8
|
+
# It's self-contained and could potentially be extracted as a separate gem
|
|
9
|
+
class Client
|
|
10
|
+
TOOL_CALL_CANCELLED_MESSAGE = "Tool call was cancelled by the client"
|
|
11
|
+
|
|
12
|
+
attr_reader :name, :transport_type, :config, :capabilities, :protocol_version, :elicitation_callback,
|
|
13
|
+
:sampling_callback, :human_in_the_loop_registry, :registry_owner_id, :task_registry
|
|
14
|
+
|
|
15
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
16
|
+
name:,
|
|
17
|
+
transport_type:,
|
|
18
|
+
transport_config: {},
|
|
19
|
+
human_in_the_loop_callback: nil,
|
|
20
|
+
roots_callback: nil,
|
|
21
|
+
logging_enabled: false,
|
|
22
|
+
logging_level: nil,
|
|
23
|
+
elicitation_enabled: false,
|
|
24
|
+
elicitation_callback: nil,
|
|
25
|
+
progress_tracking_enabled: false,
|
|
26
|
+
sampling_callback: nil,
|
|
27
|
+
notification_callback: nil,
|
|
28
|
+
extensions_capabilities: nil,
|
|
29
|
+
protocol_version: nil,
|
|
30
|
+
request_timeout: nil
|
|
31
|
+
)
|
|
32
|
+
@name = name
|
|
33
|
+
@transport_type = transport_type
|
|
34
|
+
@config = transport_config.merge(request_timeout: request_timeout || MCP.config.request_timeout)
|
|
35
|
+
@requested_protocol_version = protocol_version || MCP.config.protocol_version || Native::Protocol.latest_version
|
|
36
|
+
@protocol_version = @requested_protocol_version
|
|
37
|
+
@extensions_capabilities = extensions_capabilities || {}
|
|
38
|
+
|
|
39
|
+
# Callbacks
|
|
40
|
+
@human_in_the_loop_callback = human_in_the_loop_callback
|
|
41
|
+
@roots_callback = roots_callback
|
|
42
|
+
@logging_enabled = logging_enabled
|
|
43
|
+
@logging_level = logging_level
|
|
44
|
+
@elicitation_enabled = elicitation_enabled
|
|
45
|
+
@elicitation_callback = elicitation_callback
|
|
46
|
+
@progress_tracking_enabled = progress_tracking_enabled
|
|
47
|
+
@sampling_callback = sampling_callback
|
|
48
|
+
@notification_callback = notification_callback
|
|
49
|
+
|
|
50
|
+
@transport = nil
|
|
51
|
+
@capabilities = nil
|
|
52
|
+
@task_registry = Native::TaskRegistry.new
|
|
53
|
+
|
|
54
|
+
# Track in-flight server-initiated requests for cancellation
|
|
55
|
+
@in_flight_requests = {}
|
|
56
|
+
@in_flight_mutex = Mutex.new
|
|
57
|
+
|
|
58
|
+
# Human-in-the-loop approvals are scoped per client lifecycle.
|
|
59
|
+
@registry_owner_id = "native-client-#{SecureRandom.uuid}"
|
|
60
|
+
@human_in_the_loop_registry = Handlers::HumanInTheLoopRegistry.for_owner(@registry_owner_id)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def request(body, **options)
|
|
64
|
+
transport.request(body, **options)
|
|
65
|
+
rescue RubyLLM::MCP::Errors::TimeoutError => e
|
|
66
|
+
if transport&.alive? && !e.request_id.nil?
|
|
67
|
+
cancelled_notification(reason: "Request timed out", request_id: e.request_id)
|
|
68
|
+
end
|
|
69
|
+
raise e
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def process_result(result)
|
|
73
|
+
if result.notification?
|
|
74
|
+
process_notification(result)
|
|
75
|
+
return nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
if result.request?
|
|
79
|
+
process_request(result) if alive?
|
|
80
|
+
return nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
if result.response?
|
|
84
|
+
return result
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def start
|
|
91
|
+
return unless capabilities.nil?
|
|
92
|
+
|
|
93
|
+
transport.start
|
|
94
|
+
|
|
95
|
+
initialize_response = initialize_request
|
|
96
|
+
initialize_response.raise_error! if initialize_response.error?
|
|
97
|
+
|
|
98
|
+
# Extract and store the negotiated protocol version
|
|
99
|
+
negotiated_version = initialize_response.value["protocolVersion"]
|
|
100
|
+
|
|
101
|
+
if negotiated_version && !Native::Protocol.supported_version?(negotiated_version)
|
|
102
|
+
raise Errors::UnsupportedProtocolVersion.new(
|
|
103
|
+
message: <<~MESSAGE
|
|
104
|
+
Unsupported protocol version, and could not negotiate a supported version: #{negotiated_version}.
|
|
105
|
+
Supported versions: #{Native::Protocol.supported_versions.join(', ')}
|
|
106
|
+
MESSAGE
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@protocol_version = negotiated_version if negotiated_version
|
|
111
|
+
|
|
112
|
+
# Set the protocol version on the transport for subsequent requests
|
|
113
|
+
if @transport.respond_to?(:set_protocol_version)
|
|
114
|
+
@transport.set_protocol_version(@protocol_version)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
@capabilities = RubyLLM::MCP::ServerCapabilities.new(initialize_response.value["capabilities"])
|
|
118
|
+
initialize_notification
|
|
119
|
+
|
|
120
|
+
if @logging_enabled && @logging_level
|
|
121
|
+
set_logging(level: @logging_level)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def stop
|
|
126
|
+
@transport&.close
|
|
127
|
+
@capabilities = nil
|
|
128
|
+
@transport = nil
|
|
129
|
+
@task_registry = Native::TaskRegistry.new
|
|
130
|
+
@protocol_version = @requested_protocol_version || MCP.config.protocol_version || Native::Protocol.latest_version
|
|
131
|
+
Handlers::HumanInTheLoopRegistry.release(@registry_owner_id)
|
|
132
|
+
@human_in_the_loop_registry = Handlers::HumanInTheLoopRegistry.for_owner(@registry_owner_id)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def restart!
|
|
136
|
+
@initialize_response = nil
|
|
137
|
+
stop
|
|
138
|
+
start
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def alive?
|
|
142
|
+
!!@transport&.alive?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ping
|
|
146
|
+
body = Native::Messages::Requests.ping(tracking_progress: tracking_progress?)
|
|
147
|
+
if alive?
|
|
148
|
+
result = request(body)
|
|
149
|
+
else
|
|
150
|
+
transport.start
|
|
151
|
+
|
|
152
|
+
result = request(body)
|
|
153
|
+
@transport = nil
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
result.value == {}
|
|
157
|
+
rescue RubyLLM::MCP::Errors::TimeoutError, RubyLLM::MCP::Errors::TransportError
|
|
158
|
+
false
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def process_notification(result)
|
|
162
|
+
notification = result.notification
|
|
163
|
+
@notification_callback&.call(notification)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def process_request(result)
|
|
167
|
+
Native::ResponseHandler.new(self).execute(result)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def initialize_request
|
|
171
|
+
body = Native::Messages::Requests.initialize(
|
|
172
|
+
protocol_version: protocol_version,
|
|
173
|
+
capabilities: client_capabilities
|
|
174
|
+
)
|
|
175
|
+
request(body)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def tool_list(cursor: nil)
|
|
179
|
+
body = Native::Messages::Requests.tool_list(cursor: cursor, tracking_progress: tracking_progress?)
|
|
180
|
+
result = request(body)
|
|
181
|
+
result.raise_error! if result.error?
|
|
182
|
+
|
|
183
|
+
if result.next_cursor?
|
|
184
|
+
result.value["tools"] + tool_list(cursor: result.next_cursor)
|
|
185
|
+
else
|
|
186
|
+
result.value["tools"]
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def execute_tool(name:, parameters:)
|
|
191
|
+
if @human_in_the_loop_callback
|
|
192
|
+
approved = evaluate_tool_approval(name: name, parameters: parameters)
|
|
193
|
+
return create_cancelled_result unless approved
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
body = Native::Messages::Requests.tool_call(name: name, parameters: parameters,
|
|
197
|
+
tracking_progress: tracking_progress?)
|
|
198
|
+
request(body)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def resource_list(cursor: nil)
|
|
202
|
+
body = Native::Messages::Requests.resource_list(cursor: cursor, tracking_progress: tracking_progress?)
|
|
203
|
+
result = request(body)
|
|
204
|
+
result.raise_error! if result.error?
|
|
205
|
+
|
|
206
|
+
if result.next_cursor?
|
|
207
|
+
result.value["resources"] + resource_list(cursor: result.next_cursor)
|
|
208
|
+
else
|
|
209
|
+
result.value["resources"]
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def resource_read(uri:)
|
|
214
|
+
body = Native::Messages::Requests.resource_read(uri: uri, tracking_progress: tracking_progress?)
|
|
215
|
+
request(body)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def resource_template_list(cursor: nil)
|
|
219
|
+
body = Native::Messages::Requests.resource_template_list(cursor: cursor,
|
|
220
|
+
tracking_progress: tracking_progress?)
|
|
221
|
+
result = request(body)
|
|
222
|
+
result.raise_error! if result.error?
|
|
223
|
+
|
|
224
|
+
if result.next_cursor?
|
|
225
|
+
result.value["resourceTemplates"] + resource_template_list(cursor: result.next_cursor)
|
|
226
|
+
else
|
|
227
|
+
result.value["resourceTemplates"]
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def resources_subscribe(uri:)
|
|
232
|
+
body = Native::Messages::Requests.resources_subscribe(uri: uri, tracking_progress: tracking_progress?)
|
|
233
|
+
request(body, wait_for_response: false)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def resources_unsubscribe(uri:)
|
|
237
|
+
body = Native::Messages::Requests.resources_unsubscribe(uri: uri, tracking_progress: tracking_progress?)
|
|
238
|
+
request(body, wait_for_response: false)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def prompt_list(cursor: nil)
|
|
242
|
+
body = Native::Messages::Requests.prompt_list(cursor: cursor, tracking_progress: tracking_progress?)
|
|
243
|
+
result = request(body)
|
|
244
|
+
result.raise_error! if result.error?
|
|
245
|
+
|
|
246
|
+
if result.next_cursor?
|
|
247
|
+
result.value["prompts"] + prompt_list(cursor: result.next_cursor)
|
|
248
|
+
else
|
|
249
|
+
result.value["prompts"]
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def execute_prompt(name:, arguments:)
|
|
254
|
+
body = Native::Messages::Requests.prompt_call(name: name, arguments: arguments,
|
|
255
|
+
tracking_progress: tracking_progress?)
|
|
256
|
+
request(body)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def completion_resource(uri:, argument:, value:, context: nil)
|
|
260
|
+
body = Native::Messages::Requests.completion_resource(uri: uri, argument: argument, value: value,
|
|
261
|
+
context: context, tracking_progress: tracking_progress?)
|
|
262
|
+
request(body)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def completion_prompt(name:, argument:, value:, context: nil)
|
|
266
|
+
body = Native::Messages::Requests.completion_prompt(name: name, argument: argument, value: value,
|
|
267
|
+
context: context, tracking_progress: tracking_progress?)
|
|
268
|
+
request(body)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def set_logging(level:)
|
|
272
|
+
body = Native::Messages::Requests.logging_set_level(level: level, tracking_progress: tracking_progress?)
|
|
273
|
+
request(body)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def tasks_list(cursor: nil)
|
|
277
|
+
body = Native::Messages::Requests.tasks_list(cursor: cursor, tracking_progress: tracking_progress?)
|
|
278
|
+
result = request(body)
|
|
279
|
+
result.raise_error! if result.error?
|
|
280
|
+
|
|
281
|
+
task_registry.upsert_many(result.value["tasks"])
|
|
282
|
+
|
|
283
|
+
if result.next_cursor?
|
|
284
|
+
result.value["tasks"] + tasks_list(cursor: result.next_cursor)
|
|
285
|
+
else
|
|
286
|
+
result.value["tasks"] || []
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def task_get(task_id:)
|
|
291
|
+
body = Native::Messages::Requests.task_get(task_id: task_id, tracking_progress: tracking_progress?)
|
|
292
|
+
result = request(body)
|
|
293
|
+
result.raise_error! if result.error?
|
|
294
|
+
|
|
295
|
+
task_registry.upsert(result.value)
|
|
296
|
+
result
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def task_result(task_id:)
|
|
300
|
+
body = Native::Messages::Requests.task_result(task_id: task_id, tracking_progress: tracking_progress?)
|
|
301
|
+
result = request(body)
|
|
302
|
+
result.raise_error! if result.error?
|
|
303
|
+
|
|
304
|
+
task_registry.store_payload(task_id, result.value)
|
|
305
|
+
result
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def task_cancel(task_id:)
|
|
309
|
+
body = Native::Messages::Requests.task_cancel(task_id: task_id, tracking_progress: tracking_progress?)
|
|
310
|
+
result = request(body)
|
|
311
|
+
result.raise_error! if result.error?
|
|
312
|
+
|
|
313
|
+
task_registry.upsert(result.value)
|
|
314
|
+
result
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def task_status_notification(task:)
|
|
318
|
+
task_registry.upsert(task)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def set_progress_tracking(enabled:)
|
|
322
|
+
@progress_tracking_enabled = enabled
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def set_elicitation_enabled(enabled:)
|
|
326
|
+
@elicitation_enabled = enabled
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
## Notifications
|
|
330
|
+
#
|
|
331
|
+
def initialize_notification
|
|
332
|
+
body = Native::Messages::Notifications.initialized
|
|
333
|
+
request(body, wait_for_response: false)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def cancelled_notification(reason:, request_id:)
|
|
337
|
+
body = Native::Messages::Notifications.cancelled(request_id: request_id, reason: reason)
|
|
338
|
+
request(body, wait_for_response: false)
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def roots_list_change_notification
|
|
342
|
+
body = Native::Messages::Notifications.roots_list_changed
|
|
343
|
+
request(body, wait_for_response: false)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
## Responses
|
|
347
|
+
#
|
|
348
|
+
def ping_response(id:)
|
|
349
|
+
body = Native::Messages::Responses.ping(id: id)
|
|
350
|
+
request(body, wait_for_response: false)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def roots_list_response(id:)
|
|
354
|
+
body = Native::Messages::Responses.roots_list(id: id, roots_paths: roots_paths)
|
|
355
|
+
request(body, wait_for_response: false)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def result_response(id:, value:)
|
|
359
|
+
body = Native::Messages::Responses.result(id: id, value: value)
|
|
360
|
+
request(body, wait_for_response: false)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def sampling_create_message_response(id:, model:, message:, **_options)
|
|
364
|
+
body = Native::Messages::Responses.sampling_create_message(id: id, model: model, message: message)
|
|
365
|
+
request(body, wait_for_response: false)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def error_response(id:, message:, code: Native::JsonRpc::ErrorCodes::SERVER_ERROR, data: nil)
|
|
369
|
+
body = Native::Messages::Responses.error(id: id, message: message, code: code, data: data)
|
|
370
|
+
request(body, wait_for_response: false)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def elicitation_response(id:, elicitation:)
|
|
374
|
+
body = Native::Messages::Responses.elicitation(id: id, action: elicitation[:action],
|
|
375
|
+
content: elicitation[:content])
|
|
376
|
+
request(body, wait_for_response: false)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def client_capabilities
|
|
380
|
+
capabilities_hash = {}
|
|
381
|
+
|
|
382
|
+
if @roots_callback&.call&.any?
|
|
383
|
+
capabilities_hash[:roots] = {
|
|
384
|
+
listChanged: true
|
|
385
|
+
}
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
if MCP.config.sampling.enabled?
|
|
389
|
+
sampling_capabilities = {}
|
|
390
|
+
sampling_capabilities[:tools] = {} if MCP.config.sampling.tools
|
|
391
|
+
sampling_capabilities[:context] = {} if MCP.config.sampling.context
|
|
392
|
+
capabilities_hash[:sampling] = sampling_capabilities
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
if @elicitation_enabled
|
|
396
|
+
elicitation_capabilities = {}
|
|
397
|
+
elicitation_capabilities[:form] = {} if MCP.config.elicitation.form
|
|
398
|
+
elicitation_capabilities[:url] = {} if MCP.config.elicitation.url
|
|
399
|
+
capabilities_hash[:elicitation] = elicitation_capabilities unless elicitation_capabilities.empty?
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
if MCP.config.respond_to?(:tasks) && MCP.config.tasks.enabled?
|
|
403
|
+
capabilities_hash[:tasks] = {
|
|
404
|
+
list: {},
|
|
405
|
+
cancel: {}
|
|
406
|
+
}
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
if @extensions_capabilities.any? && Native::Protocol.extensions_supported?(@protocol_version)
|
|
410
|
+
capabilities_hash[:extensions] = @extensions_capabilities
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
capabilities_hash
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def roots_paths
|
|
417
|
+
@roots_callback&.call || []
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def tracking_progress?
|
|
421
|
+
@progress_tracking_enabled
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def sampling_callback_enabled?
|
|
425
|
+
!@sampling_callback.nil?
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def transport
|
|
429
|
+
@transport ||= Native::Transport.new(@transport_type, self, config: @config)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Register a server-initiated request that can be cancelled
|
|
433
|
+
# @param request_id [String] The ID of the request
|
|
434
|
+
# @param cancellable_operation [CancellableOperation, nil] The operation that can be cancelled
|
|
435
|
+
def register_in_flight_request(request_id, cancellable_operation = nil)
|
|
436
|
+
@in_flight_mutex.synchronize do
|
|
437
|
+
@in_flight_requests[request_id.to_s] = cancellable_operation
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Unregister a completed or cancelled request
|
|
442
|
+
# @param request_id [String] The ID of the request
|
|
443
|
+
def unregister_in_flight_request(request_id)
|
|
444
|
+
@in_flight_mutex.synchronize do
|
|
445
|
+
@in_flight_requests.delete(request_id.to_s)
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# Cancel an in-flight server-initiated request
|
|
450
|
+
# @param request_id [String] The ID of the request to cancel
|
|
451
|
+
# @return [Symbol] cancellation outcome
|
|
452
|
+
# :cancelled, :already_cancelled, :already_completed, :not_found, :not_cancellable, :failed
|
|
453
|
+
def cancel_in_flight_request(request_id)
|
|
454
|
+
operation = nil
|
|
455
|
+
@in_flight_mutex.synchronize do
|
|
456
|
+
operation = @in_flight_requests[request_id.to_s]
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
unless operation
|
|
460
|
+
RubyLLM::MCP.logger.debug("Request #{request_id} was not found for cancellation")
|
|
461
|
+
return :not_found
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
unless operation.respond_to?(:cancel)
|
|
465
|
+
RubyLLM::MCP.logger.warn("Request #{request_id} cannot be cancelled or was already completed")
|
|
466
|
+
return :not_cancellable
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
outcome = normalize_cancellation_outcome(operation.cancel)
|
|
470
|
+
if %i[cancelled already_cancelled already_completed].include?(outcome)
|
|
471
|
+
unregister_in_flight_request(request_id)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
outcome
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
private
|
|
478
|
+
|
|
479
|
+
def evaluate_tool_approval(name:, parameters:)
|
|
480
|
+
decision = @human_in_the_loop_callback.call(name, parameters)
|
|
481
|
+
unless decision.is_a?(Handlers::ApprovalDecision)
|
|
482
|
+
RubyLLM::MCP.logger.error(
|
|
483
|
+
"Human-in-the-loop callback must return ApprovalDecision, got #{decision.class}"
|
|
484
|
+
)
|
|
485
|
+
return false
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
return true if decision.approved?
|
|
489
|
+
return false if decision.denied?
|
|
490
|
+
return wait_for_deferred_approval(decision) if decision.deferred?
|
|
491
|
+
|
|
492
|
+
RubyLLM::MCP.logger.error(
|
|
493
|
+
"Human-in-the-loop callback returned unknown decision status '#{decision.status.inspect}'"
|
|
494
|
+
)
|
|
495
|
+
false
|
|
496
|
+
rescue Errors::InvalidApprovalDecision => e
|
|
497
|
+
RubyLLM::MCP.logger.error("Invalid approval decision: #{e.message}")
|
|
498
|
+
false
|
|
499
|
+
rescue StandardError => e
|
|
500
|
+
RubyLLM::MCP.logger.error("Error evaluating tool approval: #{e.message}")
|
|
501
|
+
false
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def wait_for_deferred_approval(decision)
|
|
505
|
+
unless decision.promise
|
|
506
|
+
RubyLLM::MCP.logger.error("Deferred approval #{decision.approval_id} missing promise")
|
|
507
|
+
return false
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
approved = decision.promise.wait(timeout: decision.timeout)
|
|
511
|
+
approved == true
|
|
512
|
+
rescue Timeout::Error
|
|
513
|
+
RubyLLM::MCP.logger.warn(
|
|
514
|
+
"Deferred approval #{decision.approval_id} timed out after #{decision.timeout} seconds"
|
|
515
|
+
)
|
|
516
|
+
human_in_the_loop_registry.deny(
|
|
517
|
+
decision.approval_id,
|
|
518
|
+
reason: "Timed out waiting for approval"
|
|
519
|
+
)
|
|
520
|
+
false
|
|
521
|
+
rescue StandardError => e
|
|
522
|
+
RubyLLM::MCP.logger.error("Deferred approval #{decision.approval_id} failed: #{e.message}")
|
|
523
|
+
false
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def normalize_cancellation_outcome(raw_outcome)
|
|
527
|
+
case raw_outcome
|
|
528
|
+
when Symbol
|
|
529
|
+
raw_outcome
|
|
530
|
+
when true
|
|
531
|
+
:cancelled
|
|
532
|
+
else
|
|
533
|
+
:failed
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Create a result for cancelled tool execution
|
|
538
|
+
def create_cancelled_result
|
|
539
|
+
Result.new(
|
|
540
|
+
{
|
|
541
|
+
"result" => {
|
|
542
|
+
"isError" => true,
|
|
543
|
+
"content" => [{ "type" => "text", "text" => TOOL_CALL_CANCELLED_MESSAGE }]
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
)
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|