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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +277 -0
  4. data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
  5. data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
  6. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  16. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  17. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  18. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  19. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  20. data/lib/ruby_llm/chat.rb +34 -0
  21. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
  22. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
  23. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
  24. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
  25. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
  26. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
  27. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
  28. data/lib/ruby_llm/mcp/attachment.rb +18 -0
  29. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  30. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
  31. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
  32. data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
  33. data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
  34. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +280 -0
  35. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  36. data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
  37. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
  38. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
  39. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  40. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  41. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  42. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
  43. data/lib/ruby_llm/mcp/auth/memory_storage.rb +90 -0
  44. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -0
  45. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  46. data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
  47. data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
  48. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
  49. data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
  50. data/lib/ruby_llm/mcp/auth.rb +359 -0
  51. data/lib/ruby_llm/mcp/client.rb +401 -0
  52. data/lib/ruby_llm/mcp/completion.rb +16 -0
  53. data/lib/ruby_llm/mcp/configuration.rb +310 -0
  54. data/lib/ruby_llm/mcp/content.rb +28 -0
  55. data/lib/ruby_llm/mcp/elicitation.rb +48 -0
  56. data/lib/ruby_llm/mcp/error.rb +34 -0
  57. data/lib/ruby_llm/mcp/errors.rb +91 -0
  58. data/lib/ruby_llm/mcp/logging.rb +16 -0
  59. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
  60. data/lib/ruby_llm/mcp/native/client.rb +387 -0
  61. data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
  62. data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
  63. data/lib/ruby_llm/mcp/native/messages/notifications.rb +42 -0
  64. data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
  65. data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
  66. data/lib/ruby_llm/mcp/native/messages.rb +36 -0
  67. data/lib/ruby_llm/mcp/native/notification.rb +16 -0
  68. data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
  69. data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
  70. data/lib/ruby_llm/mcp/native/transport.rb +88 -0
  71. data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
  72. data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
  73. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
  74. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
  75. data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +49 -0
  76. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
  77. data/lib/ruby_llm/mcp/native.rb +12 -0
  78. data/lib/ruby_llm/mcp/notification_handler.rb +100 -0
  79. data/lib/ruby_llm/mcp/progress.rb +35 -0
  80. data/lib/ruby_llm/mcp/prompt.rb +132 -0
  81. data/lib/ruby_llm/mcp/railtie.rb +14 -0
  82. data/lib/ruby_llm/mcp/resource.rb +112 -0
  83. data/lib/ruby_llm/mcp/resource_template.rb +85 -0
  84. data/lib/ruby_llm/mcp/result.rb +108 -0
  85. data/lib/ruby_llm/mcp/roots.rb +45 -0
  86. data/lib/ruby_llm/mcp/sample.rb +152 -0
  87. data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
  88. data/lib/ruby_llm/mcp/tool.rb +228 -0
  89. data/lib/ruby_llm/mcp/version.rb +7 -0
  90. data/lib/ruby_llm/mcp.rb +125 -0
  91. data/lib/tasks/release.rake +23 -0
  92. 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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "native/messages"
4
+
5
+ module RubyLLM
6
+ module MCP
7
+ # Native protocol implementation namespace
8
+ # This module contains the native Ruby MCP protocol implementation
9
+ module Native
10
+ end
11
+ end
12
+ 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