ruby_llm_swarm-mcp 0.8.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
- 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/chat.rb +34 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
- data/lib/ruby_llm/mcp/attachment.rb +18 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -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 +280 -0
- data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
- data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -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 +90 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -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 +236 -0
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
- data/lib/ruby_llm/mcp/auth.rb +359 -0
- data/lib/ruby_llm/mcp/client.rb +401 -0
- data/lib/ruby_llm/mcp/completion.rb +16 -0
- data/lib/ruby_llm/mcp/configuration.rb +310 -0
- data/lib/ruby_llm/mcp/content.rb +28 -0
- data/lib/ruby_llm/mcp/elicitation.rb +48 -0
- data/lib/ruby_llm/mcp/error.rb +34 -0
- data/lib/ruby_llm/mcp/errors.rb +91 -0
- data/lib/ruby_llm/mcp/logging.rb +16 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
- data/lib/ruby_llm/mcp/native/client.rb +387 -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 +42 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
- data/lib/ruby_llm/mcp/native/messages.rb +36 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limit.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 +100 -0
- data/lib/ruby_llm/mcp/progress.rb +35 -0
- data/lib/ruby_llm/mcp/prompt.rb +132 -0
- data/lib/ruby_llm/mcp/railtie.rb +14 -0
- data/lib/ruby_llm/mcp/resource.rb +112 -0
- data/lib/ruby_llm/mcp/resource_template.rb +85 -0
- data/lib/ruby_llm/mcp/result.rb +108 -0
- data/lib/ruby_llm/mcp/roots.rb +45 -0
- data/lib/ruby_llm/mcp/sample.rb +152 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
- data/lib/ruby_llm/mcp/tool.rb +228 -0
- data/lib/ruby_llm/mcp/version.rb +7 -0
- data/lib/ruby_llm/mcp.rb +125 -0
- data/lib/tasks/release.rake +23 -0
- metadata +184 -0
|
@@ -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 RateLimit
|
|
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
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
class NotificationHandler
|
|
6
|
+
attr_reader :client
|
|
7
|
+
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def execute(notification)
|
|
13
|
+
case notification.type
|
|
14
|
+
when "notifications/tools/list_changed"
|
|
15
|
+
client.reset_tools!
|
|
16
|
+
when "notifications/resources/list_changed"
|
|
17
|
+
client.reset_resources!
|
|
18
|
+
when "notifications/resources/updated"
|
|
19
|
+
process_resource_updated(notification)
|
|
20
|
+
when "notifications/prompts/list_changed"
|
|
21
|
+
client.reset_prompts!
|
|
22
|
+
when "notifications/message"
|
|
23
|
+
process_logging_message(notification)
|
|
24
|
+
when "notifications/progress"
|
|
25
|
+
process_progress_message(notification)
|
|
26
|
+
when "notifications/cancelled"
|
|
27
|
+
process_cancelled_notification(notification)
|
|
28
|
+
else
|
|
29
|
+
process_unknown_notification(notification)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def process_resource_updated(notification)
|
|
36
|
+
uri = notification.params["uri"]
|
|
37
|
+
resource = client.resources.find { |r| r.uri == uri }
|
|
38
|
+
resource&.reset_content!
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def process_logging_message(notification)
|
|
42
|
+
if client.logging_handler_enabled?
|
|
43
|
+
client.on[:logging].call(notification)
|
|
44
|
+
else
|
|
45
|
+
default_process_logging_message(notification)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def process_progress_message(notification)
|
|
50
|
+
if client.tracking_progress?
|
|
51
|
+
progress_obj = RubyLLM::MCP::Progress.new(self, client.on[:progress], notification.params)
|
|
52
|
+
progress_obj.execute_progress_handler
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def default_process_logging_message(notification, logger: RubyLLM::MCP.logger)
|
|
57
|
+
level = notification.params["level"]
|
|
58
|
+
logger_message = notification.params["logger"]
|
|
59
|
+
message = notification.params["data"]
|
|
60
|
+
|
|
61
|
+
message = "#{logger_message}: #{message}"
|
|
62
|
+
|
|
63
|
+
case level
|
|
64
|
+
when "debug"
|
|
65
|
+
logger.debug(message["message"])
|
|
66
|
+
when "info", "notice"
|
|
67
|
+
logger.info(message["message"])
|
|
68
|
+
when "warning"
|
|
69
|
+
logger.warn(message["message"])
|
|
70
|
+
when "error", "critical"
|
|
71
|
+
logger.error(message["message"])
|
|
72
|
+
when "alert", "emergency"
|
|
73
|
+
logger.fatal(message["message"])
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def process_cancelled_notification(notification)
|
|
78
|
+
request_id = notification.params["requestId"]
|
|
79
|
+
reason = notification.params["reason"] || "No reason provided"
|
|
80
|
+
|
|
81
|
+
RubyLLM::MCP.logger.info(
|
|
82
|
+
"Received cancellation for request #{request_id}: #{reason}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
success = client.cancel_in_flight_request(request_id)
|
|
86
|
+
|
|
87
|
+
unless success
|
|
88
|
+
RubyLLM::MCP.logger.debug(
|
|
89
|
+
"Request #{request_id} was not found or already completed"
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def process_unknown_notification(notification)
|
|
95
|
+
message = "Unknown notification type: #{notification.type} params: #{notification.params.to_h}"
|
|
96
|
+
RubyLLM::MCP.logger.error(message)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
class Progress
|
|
6
|
+
attr_reader :progress_token, :progress, :total, :message, :client
|
|
7
|
+
|
|
8
|
+
def initialize(coordinator, progress_handler, progress_data)
|
|
9
|
+
@coordinator = coordinator
|
|
10
|
+
@client = coordinator.client
|
|
11
|
+
@progress_handler = progress_handler
|
|
12
|
+
|
|
13
|
+
@progress_token = progress_data["progressToken"]
|
|
14
|
+
@progress = progress_data["progress"]
|
|
15
|
+
@total = progress_data["total"]
|
|
16
|
+
@message = progress_data["message"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def execute_progress_handler
|
|
20
|
+
@progress_handler.call(self)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def to_h
|
|
24
|
+
{
|
|
25
|
+
progress_token: @progress_token,
|
|
26
|
+
progress: @progress,
|
|
27
|
+
total: @total,
|
|
28
|
+
message: @message
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias to_json to_h
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
class Prompt
|
|
6
|
+
class Argument
|
|
7
|
+
attr_reader :name, :description, :required
|
|
8
|
+
|
|
9
|
+
def initialize(name:, description:, required:)
|
|
10
|
+
@name = name
|
|
11
|
+
@description = description
|
|
12
|
+
@required = required
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{
|
|
17
|
+
name: @name,
|
|
18
|
+
description: @description,
|
|
19
|
+
required: @required
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
attr_reader :name, :description, :arguments, :adapter
|
|
25
|
+
|
|
26
|
+
def initialize(adapter, prompt)
|
|
27
|
+
@adapter = adapter
|
|
28
|
+
@name = prompt["name"]
|
|
29
|
+
@description = prompt["description"]
|
|
30
|
+
@arguments = parse_arguments(prompt["arguments"])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch(arguments = {})
|
|
34
|
+
fetch_prompt_messages(arguments)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def include(chat, arguments: {})
|
|
38
|
+
validate_arguments!(arguments)
|
|
39
|
+
messages = fetch_prompt_messages(arguments)
|
|
40
|
+
|
|
41
|
+
messages.each { |message| chat.add_message(message) }
|
|
42
|
+
chat
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def ask(chat, arguments: {}, &)
|
|
46
|
+
include(chat, arguments: arguments)
|
|
47
|
+
|
|
48
|
+
chat.complete(&)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
alias say ask
|
|
52
|
+
|
|
53
|
+
def complete(argument, value, context: nil)
|
|
54
|
+
if @adapter.capabilities.completion?
|
|
55
|
+
result = @adapter.completion_prompt(name: @name, argument: argument, value: value, context: context)
|
|
56
|
+
if result.error?
|
|
57
|
+
return result.to_error
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
response = result.value["completion"]
|
|
61
|
+
|
|
62
|
+
Completion.new(argument: argument, values: response["values"], total: response["total"],
|
|
63
|
+
has_more: response["hasMore"])
|
|
64
|
+
else
|
|
65
|
+
message = "Completion is not available for this MCP server"
|
|
66
|
+
raise Errors::Capabilities::CompletionNotAvailable.new(message: message)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def to_h
|
|
71
|
+
{
|
|
72
|
+
name: @name,
|
|
73
|
+
description: @description,
|
|
74
|
+
arguments: @arguments.map(&:to_h)
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
alias to_json to_h
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def fetch_prompt_messages(arguments)
|
|
83
|
+
result = @adapter.execute_prompt(
|
|
84
|
+
name: @name,
|
|
85
|
+
arguments: arguments
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
result.raise_error! if result.error?
|
|
89
|
+
|
|
90
|
+
result.value["messages"].map do |message|
|
|
91
|
+
content = create_content_for_message(message["content"])
|
|
92
|
+
|
|
93
|
+
RubyLLM::Message.new(
|
|
94
|
+
role: message["role"],
|
|
95
|
+
content: content
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate_arguments!(incoming_arguments)
|
|
101
|
+
@arguments.each do |arg|
|
|
102
|
+
if arg.required && incoming_arguments.key?(arg.name)
|
|
103
|
+
raise Errors::PromptArgumentError, "Argument #{arg.name} is required"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def create_content_for_message(content)
|
|
109
|
+
case content["type"]
|
|
110
|
+
when "text"
|
|
111
|
+
MCP::Content.new(text: content["text"])
|
|
112
|
+
when "image", "audio"
|
|
113
|
+
attachment = MCP::Attachment.new(content["content"], content["mime_type"])
|
|
114
|
+
MCP::Content.new(text: nil, attachments: [attachment])
|
|
115
|
+
when "resource"
|
|
116
|
+
resource = Resource.new(adapter, content["resource"])
|
|
117
|
+
resource.to_content
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def parse_arguments(arguments)
|
|
122
|
+
if arguments.nil?
|
|
123
|
+
[]
|
|
124
|
+
else
|
|
125
|
+
arguments.map do |arg|
|
|
126
|
+
Argument.new(name: arg["name"], description: arg["description"], required: arg["required"])
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
class Resource
|
|
6
|
+
attr_reader :uri, :name, :description, :mime_type, :adapter, :subscribed
|
|
7
|
+
|
|
8
|
+
def initialize(adapter, resource)
|
|
9
|
+
@adapter = adapter
|
|
10
|
+
@uri = resource["uri"]
|
|
11
|
+
@name = resource["name"]
|
|
12
|
+
@description = resource["description"]
|
|
13
|
+
@mime_type = resource["mimeType"]
|
|
14
|
+
if resource.key?("content_response")
|
|
15
|
+
@content_response = resource["content_response"]
|
|
16
|
+
@content = @content_response["text"] || @content_response["blob"]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
@subscribed = false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def content
|
|
23
|
+
return @content unless @content.nil?
|
|
24
|
+
|
|
25
|
+
result = read_response
|
|
26
|
+
result.raise_error! if result.error?
|
|
27
|
+
|
|
28
|
+
@content_response = result.value.dig("contents", 0)
|
|
29
|
+
@content = @content_response["text"] || @content_response["blob"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def content_loaded?
|
|
33
|
+
!@content.nil?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def subscribe!
|
|
37
|
+
if @adapter.capabilities.resource_subscribe?
|
|
38
|
+
@adapter.resources_subscribe(uri: @uri)
|
|
39
|
+
@subscribed = true
|
|
40
|
+
else
|
|
41
|
+
message = "Resource subscribe is not available for this MCP server"
|
|
42
|
+
raise Errors::Capabilities::ResourceSubscribeNotAvailable.new(message: message)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def reset_content!
|
|
47
|
+
@content = nil
|
|
48
|
+
@content_response = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def include(chat, **args)
|
|
52
|
+
message = RubyLLM::Message.new(
|
|
53
|
+
role: "user",
|
|
54
|
+
content: to_content(**args)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
chat.add_message(message)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def to_content
|
|
61
|
+
content = self.content
|
|
62
|
+
case content_type
|
|
63
|
+
when "text"
|
|
64
|
+
MCP::Content.new(text: "#{name}: #{description}\n\n#{content}")
|
|
65
|
+
when "blob"
|
|
66
|
+
attachment = MCP::Attachment.new(content, mime_type)
|
|
67
|
+
MCP::Content.new(text: "#{name}: #{description}", attachments: [attachment])
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_h
|
|
72
|
+
{
|
|
73
|
+
uri: @uri,
|
|
74
|
+
name: @name,
|
|
75
|
+
description: @description,
|
|
76
|
+
mime_type: @mime_type,
|
|
77
|
+
contented_loaded: content_loaded?,
|
|
78
|
+
content: @content
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
alias to_json to_h
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def content_type
|
|
87
|
+
return "text" if @content_response.nil?
|
|
88
|
+
|
|
89
|
+
if @content_response.key?("blob") && !@content_response["blob"].nil?
|
|
90
|
+
"blob"
|
|
91
|
+
else
|
|
92
|
+
"text"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def read_response(uri: @uri)
|
|
97
|
+
parsed = URI.parse(uri)
|
|
98
|
+
case parsed.scheme
|
|
99
|
+
when "http", "https"
|
|
100
|
+
fetch_uri_content(uri)
|
|
101
|
+
else # file:// or git://
|
|
102
|
+
@adapter.resource_read(uri: uri)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def fetch_uri_content(uri)
|
|
107
|
+
response = HTTPX.get(uri)
|
|
108
|
+
{ "result" => { "contents" => [{ "text" => response.body }] } }
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httpx"
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
module MCP
|
|
7
|
+
class ResourceTemplate
|
|
8
|
+
attr_reader :uri, :name, :description, :mime_type, :adapter, :template
|
|
9
|
+
|
|
10
|
+
def initialize(adapter, resource)
|
|
11
|
+
@adapter = adapter
|
|
12
|
+
@uri = resource["uriTemplate"]
|
|
13
|
+
@name = resource["name"]
|
|
14
|
+
@description = resource["description"]
|
|
15
|
+
@mime_type = resource["mimeType"]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def fetch_resource(arguments: {})
|
|
19
|
+
uri = apply_template(@uri, arguments)
|
|
20
|
+
result = read_response(uri)
|
|
21
|
+
content_response = result.value.dig("contents", 0)
|
|
22
|
+
|
|
23
|
+
Resource.new(adapter, {
|
|
24
|
+
"uri" => uri,
|
|
25
|
+
"name" => "#{@name} (#{uri})",
|
|
26
|
+
"description" => @description,
|
|
27
|
+
"mimeType" => @mime_type,
|
|
28
|
+
"content_response" => content_response
|
|
29
|
+
})
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_content(arguments: {})
|
|
33
|
+
fetch_resource(arguments: arguments).to_content
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def complete(argument, value, context: nil)
|
|
37
|
+
if @adapter.capabilities.completion?
|
|
38
|
+
result = @adapter.completion_resource(uri: @uri, argument: argument, value: value, context: context)
|
|
39
|
+
result.raise_error! if result.error?
|
|
40
|
+
|
|
41
|
+
response = result.value["completion"]
|
|
42
|
+
|
|
43
|
+
Completion.new(argument: argument, values: response["values"], total: response["total"],
|
|
44
|
+
has_more: response["hasMore"])
|
|
45
|
+
else
|
|
46
|
+
message = "Completion is not available for this MCP server"
|
|
47
|
+
raise Errors::Capabilities::CompletionNotAvailable.new(message: message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def content_type
|
|
54
|
+
if @content.key?("type")
|
|
55
|
+
@content["type"]
|
|
56
|
+
else
|
|
57
|
+
"text"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def read_response(uri)
|
|
62
|
+
parsed = URI.parse(uri)
|
|
63
|
+
case parsed.scheme
|
|
64
|
+
when "http", "https"
|
|
65
|
+
fetch_uri_content(uri)
|
|
66
|
+
else # file:// or git://
|
|
67
|
+
@adapter.resource_read(uri: uri)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def fetch_uri_content(uri)
|
|
72
|
+
response = HTTPX.get(uri)
|
|
73
|
+
{ "result" => { "contents" => [{ "text" => response.body }] } }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def apply_template(uri, arguments)
|
|
77
|
+
uri.gsub(/\{(\w+)\}/) do
|
|
78
|
+
arguments[::Regexp.last_match(1).to_s] ||
|
|
79
|
+
arguments[::Regexp.last_match(1).to_sym] ||
|
|
80
|
+
"{#{::Regexp.last_match(1)}}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|