ruby_llm-mcp 0.8.0 → 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/templates/initializer.rb +21 -4
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +20 -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_server.rb +7 -1
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +0 -3
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +0 -2
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +100 -32
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +230 -57
- data/lib/ruby_llm/mcp/auth/discoverer.rb +157 -26
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +19 -2
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +3 -2
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +0 -2
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +31 -12
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +124 -9
- data/lib/ruby_llm/mcp/auth/session_manager.rb +0 -2
- data/lib/ruby_llm/mcp/auth/token_manager.rb +74 -3
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +72 -15
- data/lib/ruby_llm/mcp/auth.rb +19 -7
- data/lib/ruby_llm/mcp/client.rb +267 -39
- data/lib/ruby_llm/mcp/configuration.rb +161 -12
- data/lib/ruby_llm/mcp/elicitation.rb +261 -14
- data/lib/ruby_llm/mcp/errors.rb +18 -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 +8 -6
- 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 +31 -7
- data/lib/tasks/smoke.rake +66 -0
- metadata +77 -36
- data/lib/ruby_llm/mcp/coordinator.rb +0 -304
- 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 -151
- data/lib/ruby_llm/mcp/transports/sse.rb +0 -435
- data/lib/ruby_llm/mcp/transports/stdio.rb +0 -231
- data/lib/ruby_llm/mcp/transports/streamable_http.rb +0 -725
- 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,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
module Transports
|
|
7
|
+
module Support
|
|
8
|
+
class HTTPClient
|
|
9
|
+
CONNECTION_KEY = :ruby_llm_mcp_client_connection
|
|
10
|
+
|
|
11
|
+
def self.connection
|
|
12
|
+
Thread.current[CONNECTION_KEY] ||= build_connection
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.build_connection
|
|
16
|
+
HTTPX.with(
|
|
17
|
+
pool_options: {
|
|
18
|
+
max_connections: RubyLLM::MCP.config.max_connections,
|
|
19
|
+
pool_timeout: RubyLLM::MCP.config.pool_timeout
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
module Transports
|
|
7
|
+
module Support
|
|
8
|
+
class RateLimiter
|
|
9
|
+
def initialize(limit: 10, interval: 1000)
|
|
10
|
+
@limit = limit
|
|
11
|
+
@interval = interval
|
|
12
|
+
@timestamps = []
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def exceeded?
|
|
17
|
+
now = current_time
|
|
18
|
+
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
purge_old(now)
|
|
21
|
+
@timestamps.size >= @limit
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add
|
|
26
|
+
now = current_time
|
|
27
|
+
|
|
28
|
+
@mutex.synchronize do
|
|
29
|
+
purge_old(now)
|
|
30
|
+
@timestamps << now
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def current_time
|
|
37
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def purge_old(now)
|
|
41
|
+
cutoff = now - @interval
|
|
42
|
+
@timestamps.reject! { |t| t < cutoff }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
module Transports
|
|
7
|
+
module Support
|
|
8
|
+
module Timeout
|
|
9
|
+
def with_timeout(seconds, request_id: nil)
|
|
10
|
+
result = nil
|
|
11
|
+
exception = nil
|
|
12
|
+
|
|
13
|
+
worker = Thread.new do
|
|
14
|
+
result = yield
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
exception = e
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
if worker.join(seconds)
|
|
20
|
+
raise exception if exception
|
|
21
|
+
|
|
22
|
+
result
|
|
23
|
+
else
|
|
24
|
+
worker.kill # stop the thread (can still have some risk if shared resources)
|
|
25
|
+
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
|
26
|
+
message: "Request timed out after #{seconds} seconds",
|
|
27
|
+
request_id: request_id
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module MCP
|
|
5
5
|
class NotificationHandler
|
|
6
|
-
attr_reader :
|
|
6
|
+
attr_reader :client
|
|
7
7
|
|
|
8
|
-
def initialize(
|
|
9
|
-
@
|
|
10
|
-
@client = coordinator.client
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
def execute(notification)
|
|
@@ -25,7 +24,11 @@ module RubyLLM
|
|
|
25
24
|
when "notifications/progress"
|
|
26
25
|
process_progress_message(notification)
|
|
27
26
|
when "notifications/cancelled"
|
|
28
|
-
|
|
27
|
+
process_cancelled_notification(notification)
|
|
28
|
+
when "notifications/tasks/status"
|
|
29
|
+
process_task_status_notification(notification)
|
|
30
|
+
when "notifications/elicitation/complete"
|
|
31
|
+
process_elicitation_complete_notification(notification)
|
|
29
32
|
else
|
|
30
33
|
process_unknown_notification(notification)
|
|
31
34
|
end
|
|
@@ -75,10 +78,45 @@ module RubyLLM
|
|
|
75
78
|
end
|
|
76
79
|
end
|
|
77
80
|
|
|
81
|
+
def process_cancelled_notification(notification)
|
|
82
|
+
request_id = notification.params["requestId"]
|
|
83
|
+
reason = notification.params["reason"] || "No reason provided"
|
|
84
|
+
|
|
85
|
+
RubyLLM::MCP.logger.info(
|
|
86
|
+
"Received cancellation for request #{request_id}: #{reason}"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
outcome = client.cancel_in_flight_request(request_id)
|
|
90
|
+
|
|
91
|
+
case outcome
|
|
92
|
+
when :cancelled, :already_cancelled, :already_completed
|
|
93
|
+
RubyLLM::MCP.logger.debug("Cancellation outcome for #{request_id}: #{outcome}")
|
|
94
|
+
when :not_found
|
|
95
|
+
RubyLLM::MCP.logger.debug("Request #{request_id} was not found or already completed")
|
|
96
|
+
when :not_cancellable
|
|
97
|
+
RubyLLM::MCP.logger.warn("Request #{request_id} is not cancellable")
|
|
98
|
+
else
|
|
99
|
+
RubyLLM::MCP.logger.warn("Cancellation for #{request_id} returned unexpected outcome: #{outcome.inspect}")
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
78
103
|
def process_unknown_notification(notification)
|
|
79
104
|
message = "Unknown notification type: #{notification.type} params: #{notification.params.to_h}"
|
|
80
105
|
RubyLLM::MCP.logger.error(message)
|
|
81
106
|
end
|
|
107
|
+
|
|
108
|
+
def process_task_status_notification(notification)
|
|
109
|
+
return unless client.adapter.respond_to?(:task_status_notification)
|
|
110
|
+
|
|
111
|
+
client.adapter.task_status_notification(task: notification.params)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def process_elicitation_complete_notification(notification)
|
|
115
|
+
elicitation_id = notification.params["elicitationId"]
|
|
116
|
+
return if elicitation_id.nil?
|
|
117
|
+
|
|
118
|
+
Handlers::ElicitationRegistry.remove(elicitation_id)
|
|
119
|
+
end
|
|
82
120
|
end
|
|
83
121
|
end
|
|
84
122
|
end
|
data/lib/ruby_llm/mcp/prompt.rb
CHANGED
|
@@ -21,10 +21,10 @@ module RubyLLM
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
attr_reader :name, :description, :arguments, :
|
|
24
|
+
attr_reader :name, :description, :arguments, :adapter
|
|
25
25
|
|
|
26
|
-
def initialize(
|
|
27
|
-
@
|
|
26
|
+
def initialize(adapter, prompt)
|
|
27
|
+
@adapter = adapter
|
|
28
28
|
@name = prompt["name"]
|
|
29
29
|
@description = prompt["description"]
|
|
30
30
|
@arguments = parse_arguments(prompt["arguments"])
|
|
@@ -51,8 +51,8 @@ module RubyLLM
|
|
|
51
51
|
alias say ask
|
|
52
52
|
|
|
53
53
|
def complete(argument, value, context: nil)
|
|
54
|
-
if @
|
|
55
|
-
result = @
|
|
54
|
+
if @adapter.capabilities.completion?
|
|
55
|
+
result = @adapter.completion_prompt(name: @name, argument: argument, value: value, context: context)
|
|
56
56
|
if result.error?
|
|
57
57
|
return result.to_error
|
|
58
58
|
end
|
|
@@ -80,7 +80,7 @@ module RubyLLM
|
|
|
80
80
|
private
|
|
81
81
|
|
|
82
82
|
def fetch_prompt_messages(arguments)
|
|
83
|
-
result = @
|
|
83
|
+
result = @adapter.execute_prompt(
|
|
84
84
|
name: @name,
|
|
85
85
|
arguments: arguments
|
|
86
86
|
)
|
|
@@ -113,7 +113,7 @@ module RubyLLM
|
|
|
113
113
|
attachment = MCP::Attachment.new(content["content"], content["mime_type"])
|
|
114
114
|
MCP::Content.new(text: nil, attachments: [attachment])
|
|
115
115
|
when "resource"
|
|
116
|
-
resource = Resource.new(
|
|
116
|
+
resource = Resource.new(adapter, content["resource"])
|
|
117
117
|
resource.to_content
|
|
118
118
|
end
|
|
119
119
|
end
|
data/lib/ruby_llm/mcp/railtie.rb
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
if defined?(Rails::Railtie)
|
|
4
|
+
module RubyLLM
|
|
5
|
+
module MCP
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
generators do
|
|
8
|
+
require_relative "../../generators/ruby_llm/mcp/install/install_generator"
|
|
9
|
+
require_relative "../../generators/ruby_llm/mcp/oauth/install_generator"
|
|
10
|
+
end
|
|
9
11
|
end
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "httpx"
|
|
4
|
-
|
|
5
3
|
module RubyLLM
|
|
6
4
|
module MCP
|
|
7
5
|
class Resource
|
|
8
|
-
attr_reader :uri, :name, :description, :mime_type, :
|
|
6
|
+
attr_reader :uri, :name, :description, :mime_type, :adapter, :subscribed, :apps_metadata
|
|
9
7
|
|
|
10
|
-
def initialize(
|
|
11
|
-
@
|
|
8
|
+
def initialize(adapter, resource)
|
|
9
|
+
@adapter = adapter
|
|
12
10
|
@uri = resource["uri"]
|
|
13
11
|
@name = resource["name"]
|
|
14
12
|
@description = resource["description"]
|
|
15
13
|
@mime_type = resource["mimeType"]
|
|
14
|
+
@apps_metadata = Extensions::Apps::ResourceMetadata.new(resource[Extensions::Apps::Constants::META_KEY])
|
|
16
15
|
if resource.key?("content_response")
|
|
17
16
|
@content_response = resource["content_response"]
|
|
18
17
|
@content = @content_response["text"] || @content_response["blob"]
|
|
@@ -36,8 +35,8 @@ module RubyLLM
|
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
def subscribe!
|
|
39
|
-
if @
|
|
40
|
-
@
|
|
38
|
+
if @adapter.capabilities.resource_subscribe?
|
|
39
|
+
@adapter.resources_subscribe(uri: @uri)
|
|
41
40
|
@subscribed = true
|
|
42
41
|
else
|
|
43
42
|
message = "Resource subscribe is not available for this MCP server"
|
|
@@ -45,6 +44,16 @@ module RubyLLM
|
|
|
45
44
|
end
|
|
46
45
|
end
|
|
47
46
|
|
|
47
|
+
def unsubscribe!
|
|
48
|
+
if @adapter.capabilities.resource_subscribe?
|
|
49
|
+
@adapter.resources_unsubscribe(uri: @uri)
|
|
50
|
+
@subscribed = false
|
|
51
|
+
else
|
|
52
|
+
message = "Resource unsubscribe is not available for this MCP server"
|
|
53
|
+
raise Errors::Capabilities::ResourceSubscribeNotAvailable.new(message: message)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
48
57
|
def reset_content!
|
|
49
58
|
@content = nil
|
|
50
59
|
@content_response = nil
|
|
@@ -101,7 +110,7 @@ module RubyLLM
|
|
|
101
110
|
when "http", "https"
|
|
102
111
|
fetch_uri_content(uri)
|
|
103
112
|
else # file:// or git://
|
|
104
|
-
@
|
|
113
|
+
@adapter.resource_read(uri: uri)
|
|
105
114
|
end
|
|
106
115
|
end
|
|
107
116
|
|
|
@@ -5,14 +5,15 @@ require "httpx"
|
|
|
5
5
|
module RubyLLM
|
|
6
6
|
module MCP
|
|
7
7
|
class ResourceTemplate
|
|
8
|
-
attr_reader :uri, :name, :description, :mime_type, :
|
|
8
|
+
attr_reader :uri, :name, :description, :mime_type, :adapter, :template, :apps_metadata
|
|
9
9
|
|
|
10
|
-
def initialize(
|
|
11
|
-
@
|
|
10
|
+
def initialize(adapter, resource)
|
|
11
|
+
@adapter = adapter
|
|
12
12
|
@uri = resource["uriTemplate"]
|
|
13
13
|
@name = resource["name"]
|
|
14
14
|
@description = resource["description"]
|
|
15
15
|
@mime_type = resource["mimeType"]
|
|
16
|
+
@apps_metadata = Extensions::Apps::ResourceMetadata.new(resource[Extensions::Apps::Constants::META_KEY])
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
def fetch_resource(arguments: {})
|
|
@@ -20,7 +21,7 @@ module RubyLLM
|
|
|
20
21
|
result = read_response(uri)
|
|
21
22
|
content_response = result.value.dig("contents", 0)
|
|
22
23
|
|
|
23
|
-
Resource.new(
|
|
24
|
+
Resource.new(adapter, {
|
|
24
25
|
"uri" => uri,
|
|
25
26
|
"name" => "#{@name} (#{uri})",
|
|
26
27
|
"description" => @description,
|
|
@@ -34,8 +35,8 @@ module RubyLLM
|
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def complete(argument, value, context: nil)
|
|
37
|
-
if @
|
|
38
|
-
result = @
|
|
38
|
+
if @adapter.capabilities.completion?
|
|
39
|
+
result = @adapter.completion_resource(uri: @uri, argument: argument, value: value, context: context)
|
|
39
40
|
result.raise_error! if result.error?
|
|
40
41
|
|
|
41
42
|
response = result.value["completion"]
|
|
@@ -64,7 +65,7 @@ module RubyLLM
|
|
|
64
65
|
when "http", "https"
|
|
65
66
|
fetch_uri_content(uri)
|
|
66
67
|
else # file:// or git://
|
|
67
|
-
@
|
|
68
|
+
@adapter.resource_read(uri: uri)
|
|
68
69
|
end
|
|
69
70
|
end
|
|
70
71
|
|
data/lib/ruby_llm/mcp/result.rb
CHANGED
|
@@ -32,6 +32,10 @@ module RubyLLM
|
|
|
32
32
|
|
|
33
33
|
@result_is_error = response.dig("result", "isError") || false
|
|
34
34
|
@next_cursor = response.dig("result", "nextCursor")
|
|
35
|
+
|
|
36
|
+
# Track whether result/error keys exist (for JSON-RPC detection)
|
|
37
|
+
@has_result = response.key?("result")
|
|
38
|
+
@has_error = response.key?("error")
|
|
35
39
|
end
|
|
36
40
|
|
|
37
41
|
REQUEST_METHODS.each do |method_name, method_value|
|
|
@@ -65,7 +69,7 @@ module RubyLLM
|
|
|
65
69
|
end
|
|
66
70
|
|
|
67
71
|
def notification?
|
|
68
|
-
@
|
|
72
|
+
@id.nil? && !@method.nil?
|
|
69
73
|
end
|
|
70
74
|
|
|
71
75
|
def next_cursor?
|
|
@@ -73,15 +77,15 @@ module RubyLLM
|
|
|
73
77
|
end
|
|
74
78
|
|
|
75
79
|
def request?
|
|
76
|
-
!@
|
|
80
|
+
!@id.nil? && !@method.nil?
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
def response?
|
|
80
|
-
!@id.nil? && (@
|
|
84
|
+
!@id.nil? && @method.nil? && (@has_result || @has_error)
|
|
81
85
|
end
|
|
82
86
|
|
|
83
87
|
def success?
|
|
84
|
-
|
|
88
|
+
@has_result
|
|
85
89
|
end
|
|
86
90
|
|
|
87
91
|
def tool_success?
|
data/lib/ruby_llm/mcp/roots.rb
CHANGED
|
@@ -5,9 +5,9 @@ module RubyLLM
|
|
|
5
5
|
class Roots
|
|
6
6
|
attr_reader :paths
|
|
7
7
|
|
|
8
|
-
def initialize(paths: [],
|
|
8
|
+
def initialize(paths: [], adapter: nil)
|
|
9
9
|
@paths = paths
|
|
10
|
-
@
|
|
10
|
+
@adapter = adapter
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def active?
|
|
@@ -16,12 +16,12 @@ module RubyLLM
|
|
|
16
16
|
|
|
17
17
|
def add(path)
|
|
18
18
|
@paths << path
|
|
19
|
-
@
|
|
19
|
+
@adapter.roots_list_change_notification
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def remove(path)
|
|
23
23
|
@paths.delete(path)
|
|
24
|
-
@
|
|
24
|
+
@adapter.roots_list_change_notification
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def to_request
|
data/lib/ruby_llm/mcp/sample.rb
CHANGED
|
@@ -47,15 +47,14 @@ module RubyLLM
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def execute
|
|
50
|
-
|
|
50
|
+
# Check if handler is a class or block
|
|
51
|
+
handler = @coordinator.sampling_callback
|
|
51
52
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
id: @id, message: chat_message, model: model
|
|
58
|
-
)
|
|
53
|
+
if Handlers.handler_class?(handler)
|
|
54
|
+
execute_with_handler_class(handler)
|
|
55
|
+
else
|
|
56
|
+
execute_with_block
|
|
57
|
+
end
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def message
|
|
@@ -75,15 +74,86 @@ module RubyLLM
|
|
|
75
74
|
|
|
76
75
|
private
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
# Execute using handler class
|
|
78
|
+
def execute_with_handler_class(handler_class)
|
|
79
|
+
handler_instance = handler_class.new(
|
|
80
|
+
sample: self,
|
|
81
|
+
coordinator: @coordinator
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
result = handler_instance.call
|
|
80
85
|
|
|
81
|
-
|
|
86
|
+
# Handle different return types
|
|
87
|
+
case result
|
|
88
|
+
when Hash
|
|
89
|
+
handle_handler_hash_result(result)
|
|
90
|
+
when TrueClass, FalseClass
|
|
91
|
+
handle_handler_boolean_result(result)
|
|
92
|
+
else
|
|
93
|
+
# Unexpected return type
|
|
94
|
+
RubyLLM::MCP.logger.error("Handler returned unexpected type: #{result.class}")
|
|
95
|
+
@coordinator.error_response(id: @id, message: "Internal error in sampling handler")
|
|
96
|
+
end
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
RubyLLM::MCP.logger.error("Error in sampling handler: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
99
|
+
@coordinator.error_response(id: @id, message: "Error executing sampling request: #{e.message}")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Handle hash result from handler
|
|
103
|
+
def handle_handler_hash_result(result)
|
|
104
|
+
if result[:accepted] == false
|
|
105
|
+
@coordinator.error_response(id: @id, message: result[:message] || REJECTED_MESSAGE)
|
|
106
|
+
elsif result[:accepted] == true && result[:response]
|
|
107
|
+
# Handler provided the response directly
|
|
108
|
+
model = preferred_model
|
|
109
|
+
return unless model
|
|
110
|
+
|
|
111
|
+
@coordinator.sampling_create_message_response(
|
|
112
|
+
id: @id, message: result[:response], model: model
|
|
113
|
+
)
|
|
114
|
+
else
|
|
115
|
+
# Invalid hash structure
|
|
116
|
+
@coordinator.error_response(id: @id, message: "Invalid handler response")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Handle boolean result from handler
|
|
121
|
+
def handle_handler_boolean_result(result)
|
|
122
|
+
unless result
|
|
82
123
|
@coordinator.error_response(id: @id, message: REJECTED_MESSAGE)
|
|
83
|
-
return
|
|
124
|
+
return
|
|
84
125
|
end
|
|
85
126
|
|
|
86
|
-
|
|
127
|
+
model = preferred_model
|
|
128
|
+
return unless model
|
|
129
|
+
|
|
130
|
+
chat_message = chat(model)
|
|
131
|
+
@coordinator.sampling_create_message_response(
|
|
132
|
+
id: @id, message: chat_message, model: model
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Execute using block (legacy/backward compatible)
|
|
137
|
+
def execute_with_block
|
|
138
|
+
callback_result = run_sampling_callback
|
|
139
|
+
|
|
140
|
+
case callback_result
|
|
141
|
+
when Hash
|
|
142
|
+
handle_handler_hash_result(callback_result)
|
|
143
|
+
when TrueClass, FalseClass
|
|
144
|
+
handle_handler_boolean_result(callback_result)
|
|
145
|
+
else
|
|
146
|
+
# Legacy compatibility: any truthy callback result means "allow sampling"
|
|
147
|
+
handle_handler_boolean_result(!!callback_result)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def run_sampling_callback
|
|
152
|
+
return true unless @coordinator.sampling_callback_enabled?
|
|
153
|
+
|
|
154
|
+
callback_result = @coordinator.sampling_callback&.call(self)
|
|
155
|
+
# If callback returns nil, it means no guard was configured - allow it
|
|
156
|
+
callback_result.nil? || callback_result
|
|
87
157
|
rescue StandardError => e
|
|
88
158
|
RubyLLM::MCP.logger.error("Error in callback guard: #{e.message}, #{e.backtrace.join("\n")}")
|
|
89
159
|
@coordinator.error_response(id: @id, message: "Error executing sampling request")
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json_schemer"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module MCP
|
|
7
|
+
module SchemaValidator
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def valid?(schema, data)
|
|
11
|
+
return true if schema.nil?
|
|
12
|
+
|
|
13
|
+
schemer(schema).valid?(data)
|
|
14
|
+
rescue StandardError
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def valid_schema?(schema)
|
|
19
|
+
return true if schema.nil?
|
|
20
|
+
|
|
21
|
+
schemer(schema)
|
|
22
|
+
true
|
|
23
|
+
rescue StandardError
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def schemer(schema)
|
|
28
|
+
JSONSchemer.schema(schema)
|
|
29
|
+
end
|
|
30
|
+
private_class_method :schemer
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -44,6 +44,47 @@ module RubyLLM
|
|
|
44
44
|
def logging?
|
|
45
45
|
!@capabilities["logging"].nil?
|
|
46
46
|
end
|
|
47
|
+
|
|
48
|
+
def tasks?
|
|
49
|
+
!@capabilities["tasks"].nil?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def tasks_list?
|
|
53
|
+
!@capabilities.dig("tasks", "list").nil?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def tasks_cancel?
|
|
57
|
+
!@capabilities.dig("tasks", "cancel").nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def task_augmented_tool_call?
|
|
61
|
+
!@capabilities.dig("tasks", "requests", "tools", "call").nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def extensions
|
|
65
|
+
value = @capabilities["extensions"]
|
|
66
|
+
value.is_a?(Hash) ? value : {}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def extension?(id)
|
|
70
|
+
!extension_capability(id).nil?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def extension_capability(id)
|
|
74
|
+
canonical_id = Extensions::Registry.canonicalize_id(id)
|
|
75
|
+
return nil if canonical_id.nil?
|
|
76
|
+
|
|
77
|
+
normalized_extensions[canonical_id]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def normalized_extensions
|
|
83
|
+
extensions.each_with_object({}) do |(id, value), acc|
|
|
84
|
+
canonical_id = Extensions::Registry.canonicalize_id(id) || id
|
|
85
|
+
acc[canonical_id] = value
|
|
86
|
+
end
|
|
87
|
+
end
|
|
47
88
|
end
|
|
48
89
|
end
|
|
49
90
|
end
|