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,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
|