ruby_llm-mcp 0.7.0 → 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 +4 -4
- data/lib/generators/ruby_llm/mcp/{install_generator.rb → install/install_generator.rb} +4 -2
- 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/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 +115 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +41 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +539 -0
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +254 -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 +65 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +72 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +226 -0
- data/lib/ruby_llm/mcp/auth/security.rb +44 -0
- data/lib/ruby_llm/mcp/auth/session_manager.rb +56 -0
- data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +78 -0
- data/lib/ruby_llm/mcp/auth.rb +359 -0
- data/lib/ruby_llm/mcp/client.rb +49 -0
- data/lib/ruby_llm/mcp/configuration.rb +39 -13
- data/lib/ruby_llm/mcp/coordinator.rb +11 -0
- data/lib/ruby_llm/mcp/errors.rb +11 -0
- data/lib/ruby_llm/mcp/railtie.rb +2 -10
- data/lib/ruby_llm/mcp/tool.rb +1 -1
- data/lib/ruby_llm/mcp/transport.rb +94 -1
- data/lib/ruby_llm/mcp/transports/sse.rb +116 -22
- data/lib/ruby_llm/mcp/transports/stdio.rb +82 -79
- data/lib/ruby_llm/mcp/transports/streamable_http.rb +81 -79
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +10 -4
- metadata +40 -6
- /data/lib/generators/ruby_llm/mcp/{templates → install/templates}/initializer.rb +0 -0
- /data/lib/generators/ruby_llm/mcp/{templates → install/templates}/mcps.yml +0 -0
|
@@ -13,13 +13,14 @@ module RubyLLM
|
|
|
13
13
|
|
|
14
14
|
attr_reader :command, :stdin, :stdout, :stderr, :id, :coordinator
|
|
15
15
|
|
|
16
|
-
def initialize(command:, coordinator:, request_timeout:,
|
|
16
|
+
def initialize(command:, coordinator:, request_timeout:, options: {})
|
|
17
17
|
@request_timeout = request_timeout
|
|
18
18
|
@command = command
|
|
19
19
|
@coordinator = coordinator
|
|
20
|
-
@args = args
|
|
21
|
-
@env = env || {}
|
|
20
|
+
@args = options[:args] || options["args"] || []
|
|
21
|
+
@env = options[:env] || options["env"] || {}
|
|
22
22
|
@client_id = SecureRandom.uuid
|
|
23
|
+
# NOTE: Stdio transport doesn't use OAuth (local process communication)
|
|
23
24
|
|
|
24
25
|
@id_counter = 0
|
|
25
26
|
@id_mutex = Mutex.new
|
|
@@ -78,51 +79,23 @@ module RubyLLM
|
|
|
78
79
|
@running = true
|
|
79
80
|
end
|
|
80
81
|
|
|
81
|
-
def close
|
|
82
|
+
def close
|
|
82
83
|
@running = false
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
rescue
|
|
87
|
-
nil
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
begin
|
|
91
|
-
@wait_thread&.join(1)
|
|
92
|
-
rescue StandardError
|
|
85
|
+
[@stdin, @stdout, @stderr].each do |stream|
|
|
86
|
+
stream&.close
|
|
87
|
+
rescue IOError, Errno::EBADF
|
|
93
88
|
nil
|
|
94
89
|
end
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
rescue StandardError
|
|
99
|
-
nil
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
begin
|
|
103
|
-
@stderr&.close
|
|
91
|
+
[@wait_thread, @reader_thread, @stderr_thread].each do |thread|
|
|
92
|
+
thread&.join(1)
|
|
104
93
|
rescue StandardError
|
|
105
94
|
nil
|
|
106
95
|
end
|
|
107
96
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
rescue StandardError
|
|
111
|
-
nil
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
begin
|
|
115
|
-
@stderr_thread&.join(1)
|
|
116
|
-
rescue StandardError
|
|
117
|
-
nil
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
@stdin = nil
|
|
121
|
-
@stdout = nil
|
|
122
|
-
@stderr = nil
|
|
123
|
-
@wait_thread = nil
|
|
124
|
-
@reader_thread = nil
|
|
125
|
-
@stderr_thread = nil
|
|
97
|
+
@stdin = @stdout = @stderr = nil
|
|
98
|
+
@wait_thread = @reader_thread = @stderr_thread = nil
|
|
126
99
|
end
|
|
127
100
|
|
|
128
101
|
def set_protocol_version(version)
|
|
@@ -151,58 +124,88 @@ module RubyLLM
|
|
|
151
124
|
|
|
152
125
|
def start_reader_thread
|
|
153
126
|
@reader_thread = Thread.new do
|
|
154
|
-
|
|
155
|
-
begin
|
|
156
|
-
if @stdout.closed? || @wait_thread.nil? || !@wait_thread.alive?
|
|
157
|
-
sleep 1
|
|
158
|
-
restart_process if @running
|
|
159
|
-
next
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
line = @stdout.gets
|
|
163
|
-
next unless line && !line.strip.empty?
|
|
164
|
-
|
|
165
|
-
process_response(line.strip)
|
|
166
|
-
rescue IOError, Errno::EPIPE => e
|
|
167
|
-
RubyLLM::MCP.logger.error "Reader error: #{e.message}. Restarting in 1 second..."
|
|
168
|
-
sleep 1
|
|
169
|
-
restart_process if @running
|
|
170
|
-
rescue StandardError => e
|
|
171
|
-
RubyLLM::MCP.logger.error "Error in reader thread: #{e.message}, #{e.backtrace.join("\n")}"
|
|
172
|
-
sleep 1
|
|
173
|
-
end
|
|
174
|
-
end
|
|
127
|
+
read_stdout_loop
|
|
175
128
|
end
|
|
176
129
|
|
|
177
130
|
@reader_thread.abort_on_exception = true
|
|
178
131
|
end
|
|
179
132
|
|
|
133
|
+
def read_stdout_loop
|
|
134
|
+
while @running
|
|
135
|
+
begin
|
|
136
|
+
handle_stdout_read
|
|
137
|
+
rescue IOError, Errno::EPIPE => e
|
|
138
|
+
handle_stream_error(e, "Reader")
|
|
139
|
+
break unless @running
|
|
140
|
+
rescue StandardError => e
|
|
141
|
+
RubyLLM::MCP.logger.error "Error in reader thread: #{e.message}, #{e.backtrace.join("\n")}"
|
|
142
|
+
sleep 1
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def handle_stdout_read
|
|
148
|
+
if @stdout.closed? || @wait_thread.nil? || !@wait_thread.alive?
|
|
149
|
+
if @running
|
|
150
|
+
sleep 1
|
|
151
|
+
restart_process
|
|
152
|
+
end
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
line = @stdout.gets
|
|
157
|
+
return unless line && !line.strip.empty?
|
|
158
|
+
|
|
159
|
+
process_response(line.strip)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def handle_stream_error(error, stream_name)
|
|
163
|
+
# Check @running to distinguish graceful shutdown from unexpected errors.
|
|
164
|
+
# During shutdown, streams are closed intentionally and shouldn't trigger restarts.
|
|
165
|
+
if @running
|
|
166
|
+
RubyLLM::MCP.logger.error "#{stream_name} error: #{error.message}. Restarting in 1 second..."
|
|
167
|
+
sleep 1
|
|
168
|
+
restart_process
|
|
169
|
+
else
|
|
170
|
+
# Graceful shutdown in progress
|
|
171
|
+
RubyLLM::MCP.logger.debug "#{stream_name} thread exiting during shutdown"
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
180
175
|
def start_stderr_thread
|
|
181
176
|
@stderr_thread = Thread.new do
|
|
182
|
-
|
|
183
|
-
begin
|
|
184
|
-
if @stderr.closed? || @wait_thread.nil? || !@wait_thread.alive?
|
|
185
|
-
sleep 1
|
|
186
|
-
next
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
line = @stderr.gets
|
|
190
|
-
next unless line && !line.strip.empty?
|
|
191
|
-
|
|
192
|
-
RubyLLM::MCP.logger.info(line.strip)
|
|
193
|
-
rescue IOError, Errno::EPIPE => e
|
|
194
|
-
RubyLLM::MCP.logger.error "Stderr reader error: #{e.message}"
|
|
195
|
-
sleep 1
|
|
196
|
-
rescue StandardError => e
|
|
197
|
-
RubyLLM::MCP.logger.error "Error in stderr thread: #{e.message}"
|
|
198
|
-
sleep 1
|
|
199
|
-
end
|
|
200
|
-
end
|
|
177
|
+
read_stderr_loop
|
|
201
178
|
end
|
|
202
179
|
|
|
203
180
|
@stderr_thread.abort_on_exception = true
|
|
204
181
|
end
|
|
205
182
|
|
|
183
|
+
def read_stderr_loop
|
|
184
|
+
while @running
|
|
185
|
+
begin
|
|
186
|
+
handle_stderr_read
|
|
187
|
+
rescue IOError, Errno::EPIPE => e
|
|
188
|
+
handle_stream_error(e, "Stderr reader")
|
|
189
|
+
break unless @running
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
RubyLLM::MCP.logger.error "Error in stderr thread: #{e.message}"
|
|
192
|
+
sleep 1
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def handle_stderr_read
|
|
198
|
+
if @stderr.closed? || @wait_thread.nil? || !@wait_thread.alive?
|
|
199
|
+
sleep 1
|
|
200
|
+
return
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
line = @stderr.gets
|
|
204
|
+
return unless line && !line.strip.empty?
|
|
205
|
+
|
|
206
|
+
RubyLLM::MCP.logger.info(line.strip)
|
|
207
|
+
end
|
|
208
|
+
|
|
206
209
|
def process_response(line)
|
|
207
210
|
response = JSON.parse(line)
|
|
208
211
|
request_id = response["id"]&.to_s
|
|
@@ -27,21 +27,6 @@ module RubyLLM
|
|
|
27
27
|
end
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
class OAuthOptions
|
|
31
|
-
attr_reader :issuer, :client_id, :client_secret, :scope
|
|
32
|
-
|
|
33
|
-
def initialize(issuer:, client_id:, client_secret:, scopes:)
|
|
34
|
-
@issuer = issuer
|
|
35
|
-
@client_id = client_id
|
|
36
|
-
@client_secret = client_secret
|
|
37
|
-
@scope = scopes
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def enabled?
|
|
41
|
-
@issuer && @client_id && @client_secret && @scope
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
30
|
# Options for starting SSE connections
|
|
46
31
|
class StartSSEOptions
|
|
47
32
|
attr_reader :resumption_token, :on_resumption_token, :replay_message_id
|
|
@@ -57,52 +42,52 @@ module RubyLLM
|
|
|
57
42
|
class StreamableHTTP
|
|
58
43
|
include Support::Timeout
|
|
59
44
|
|
|
60
|
-
attr_reader :session_id, :protocol_version, :coordinator
|
|
61
|
-
|
|
62
|
-
def initialize(
|
|
63
|
-
url:,
|
|
64
|
-
request_timeout:,
|
|
65
|
-
coordinator:,
|
|
66
|
-
headers: {},
|
|
67
|
-
reconnection: {},
|
|
68
|
-
version: :http2,
|
|
69
|
-
oauth: nil,
|
|
70
|
-
rate_limit: nil,
|
|
71
|
-
reconnection_options: nil,
|
|
72
|
-
session_id: nil
|
|
73
|
-
)
|
|
45
|
+
attr_reader :session_id, :protocol_version, :coordinator, :oauth_provider
|
|
46
|
+
|
|
47
|
+
def initialize(url:, request_timeout:, coordinator:, options: {})
|
|
74
48
|
@url = URI(url)
|
|
75
49
|
@coordinator = coordinator
|
|
76
50
|
@request_timeout = request_timeout
|
|
77
|
-
@headers = headers || {}
|
|
78
|
-
@session_id = session_id
|
|
79
51
|
|
|
80
|
-
|
|
81
|
-
|
|
52
|
+
extract_options(options)
|
|
53
|
+
initialize_state_variables
|
|
54
|
+
initialize_mutexes
|
|
55
|
+
|
|
56
|
+
@connection = create_connection
|
|
57
|
+
|
|
58
|
+
RubyLLM::MCP.logger.debug "OAuth provider: #{@oauth_provider ? 'present' : 'none'}" if @oauth_provider
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def extract_options(options)
|
|
62
|
+
@headers = options[:headers] || options["headers"] || {}
|
|
63
|
+
@session_id = options[:session_id] || options["session_id"]
|
|
64
|
+
@oauth_provider = options[:oauth_provider] || options["oauth_provider"]
|
|
65
|
+
@version = options[:version] || options["version"] || :http2
|
|
82
66
|
@protocol_version = nil
|
|
83
|
-
@session_id = session_id
|
|
84
67
|
|
|
85
|
-
|
|
86
|
-
@
|
|
68
|
+
reconnection = options[:reconnection] || options["reconnection"] || {}
|
|
69
|
+
@reconnection_options = options[:reconnection_options] || ReconnectionOptions.new(**reconnection)
|
|
87
70
|
|
|
88
|
-
|
|
89
|
-
@oauth_options = OAuthOptions.new(**oauth) unless oauth.nil?
|
|
71
|
+
rate_limit = options[:rate_limit] || options["rate_limit"]
|
|
90
72
|
@rate_limiter = Support::RateLimiter.new(**rate_limit) if rate_limit
|
|
73
|
+
end
|
|
91
74
|
|
|
75
|
+
def initialize_state_variables
|
|
76
|
+
@resource_metadata_url = nil
|
|
77
|
+
@client_id = SecureRandom.uuid
|
|
92
78
|
@id_counter = 0
|
|
93
|
-
@id_mutex = Mutex.new
|
|
94
79
|
@pending_requests = {}
|
|
95
|
-
@pending_mutex = Mutex.new
|
|
96
80
|
@running = true
|
|
97
81
|
@abort_controller = nil
|
|
98
82
|
@sse_thread = nil
|
|
99
|
-
@sse_mutex = Mutex.new
|
|
100
|
-
|
|
101
|
-
# Thread-safe collection of all HTTPX clients
|
|
102
83
|
@clients = []
|
|
103
|
-
|
|
84
|
+
end
|
|
104
85
|
|
|
105
|
-
|
|
86
|
+
def initialize_mutexes
|
|
87
|
+
@id_mutex = Mutex.new
|
|
88
|
+
@pending_mutex = Mutex.new
|
|
89
|
+
@sse_mutex = Mutex.new
|
|
90
|
+
@clients_mutex = Mutex.new
|
|
106
91
|
end
|
|
107
92
|
|
|
108
93
|
def request(body, add_id: true, wait_for_response: true)
|
|
@@ -242,17 +227,6 @@ module RubyLLM
|
|
|
242
227
|
}
|
|
243
228
|
)
|
|
244
229
|
|
|
245
|
-
if @oauth_options&.enabled?
|
|
246
|
-
client = client.plugin(:oauth).oauth_auth(
|
|
247
|
-
issuer: @oauth_options.issuer,
|
|
248
|
-
client_id: @oauth_options.client_id,
|
|
249
|
-
client_secret: @oauth_options.client_secret,
|
|
250
|
-
scope: @oauth_options.scope
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
client.with_access_token
|
|
254
|
-
end
|
|
255
|
-
|
|
256
230
|
register_client(client)
|
|
257
231
|
end
|
|
258
232
|
|
|
@@ -262,7 +236,25 @@ module RubyLLM
|
|
|
262
236
|
headers["mcp-session-id"] = @session_id if @session_id
|
|
263
237
|
headers["mcp-protocol-version"] = @protocol_version if @protocol_version
|
|
264
238
|
headers["X-CLIENT-ID"] = @client_id
|
|
265
|
-
headers["Origin"] = @
|
|
239
|
+
headers["Origin"] = @url.to_s
|
|
240
|
+
|
|
241
|
+
# Apply OAuth authorization if available
|
|
242
|
+
if @oauth_provider
|
|
243
|
+
RubyLLM::MCP.logger.debug "OAuth provider present, attempting to get token..."
|
|
244
|
+
RubyLLM::MCP.logger.debug " Server URL: #{@oauth_provider.server_url}"
|
|
245
|
+
|
|
246
|
+
token = @oauth_provider.access_token
|
|
247
|
+
if token
|
|
248
|
+
headers["Authorization"] = token.to_header
|
|
249
|
+
RubyLLM::MCP.logger.debug "✓ Applied OAuth authorization header: #{token.to_header[0..30]}..."
|
|
250
|
+
else
|
|
251
|
+
RubyLLM::MCP.logger.warn "✗ OAuth provider present but no valid token available!"
|
|
252
|
+
RubyLLM::MCP.logger.warn " This means the token is not in storage or has expired"
|
|
253
|
+
RubyLLM::MCP.logger.warn " Check that authentication completed successfully"
|
|
254
|
+
end
|
|
255
|
+
else
|
|
256
|
+
RubyLLM::MCP.logger.debug "No OAuth provider configured for this transport"
|
|
257
|
+
end
|
|
266
258
|
|
|
267
259
|
headers
|
|
268
260
|
end
|
|
@@ -320,16 +312,6 @@ module RubyLLM
|
|
|
320
312
|
}
|
|
321
313
|
)
|
|
322
314
|
|
|
323
|
-
if @oauth_options&.enabled?
|
|
324
|
-
client = client.plugin(:oauth).oauth_auth(
|
|
325
|
-
issuer: @oauth_options.issuer,
|
|
326
|
-
client_id: @oauth_options.client_id,
|
|
327
|
-
client_secret: @oauth_options.client_secret,
|
|
328
|
-
scope: @oauth_options.scope
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
client.with_access_token
|
|
332
|
-
end
|
|
333
315
|
register_client(client)
|
|
334
316
|
end
|
|
335
317
|
|
|
@@ -348,8 +330,12 @@ module RubyLLM
|
|
|
348
330
|
handle_accepted_response(original_message)
|
|
349
331
|
when 404
|
|
350
332
|
handle_session_expired
|
|
351
|
-
when
|
|
352
|
-
|
|
333
|
+
when 401
|
|
334
|
+
raise Errors::AuthenticationRequiredError.new(
|
|
335
|
+
message: "OAuth authentication required. Server returned 401 Unauthorized.",
|
|
336
|
+
code: 401
|
|
337
|
+
)
|
|
338
|
+
when 405
|
|
353
339
|
# Method not allowed - acceptable for some endpoints
|
|
354
340
|
nil
|
|
355
341
|
when 400...500
|
|
@@ -405,23 +391,38 @@ module RubyLLM
|
|
|
405
391
|
end
|
|
406
392
|
|
|
407
393
|
def handle_client_error(response)
|
|
394
|
+
response_body = response.respond_to?(:body) ? response.body.to_s : "Unknown error"
|
|
395
|
+
status_code = response.respond_to?(:status) ? response.status : "Unknown"
|
|
396
|
+
|
|
408
397
|
begin
|
|
409
|
-
# Safely access response body
|
|
410
|
-
response_body = response.respond_to?(:body) ? response.body.to_s : "Unknown error"
|
|
411
398
|
error_body = JSON.parse(response_body)
|
|
412
399
|
|
|
413
400
|
if error_body.is_a?(Hash) && error_body["error"]
|
|
414
|
-
error_message = error_body["error"]["message"] || error_body["error"]["code"]
|
|
401
|
+
error_message = error_body["error"]["message"] || error_body["error"]["code"] || error_body["error"].to_s
|
|
402
|
+
|
|
403
|
+
# If we still don't have a message, include the full error object
|
|
404
|
+
if error_message.to_s.strip.empty?
|
|
405
|
+
error_message = "Empty error (full response: #{response_body})"
|
|
406
|
+
end
|
|
415
407
|
|
|
416
408
|
if error_message.to_s.downcase.include?("session")
|
|
417
409
|
raise Errors::TransportError.new(
|
|
418
|
-
code:
|
|
410
|
+
code: status_code,
|
|
419
411
|
message: "Server error: #{error_message} (Current session ID: #{@session_id || 'none'})"
|
|
420
412
|
)
|
|
421
413
|
end
|
|
422
414
|
|
|
415
|
+
# Special handling for 403 Forbidden with OAuth
|
|
416
|
+
if status_code == 403 && @oauth_provider
|
|
417
|
+
raise Errors::TransportError.new(
|
|
418
|
+
code: status_code,
|
|
419
|
+
message: "Authorization failed (403 Forbidden): #{error_message}. \
|
|
420
|
+
Check token scope and resource permissions at #{@oauth_provider.server_url}."
|
|
421
|
+
)
|
|
422
|
+
end
|
|
423
|
+
|
|
423
424
|
raise Errors::TransportError.new(
|
|
424
|
-
code:
|
|
425
|
+
code: status_code,
|
|
425
426
|
message: "Server error: #{error_message}"
|
|
426
427
|
)
|
|
427
428
|
end
|
|
@@ -429,10 +430,6 @@ module RubyLLM
|
|
|
429
430
|
# Fall through to generic error
|
|
430
431
|
end
|
|
431
432
|
|
|
432
|
-
# Safely access response attributes
|
|
433
|
-
response_body = response.respond_to?(:body) ? response.body.to_s : "Unknown error"
|
|
434
|
-
status_code = response.respond_to?(:status) ? response.status : "Unknown"
|
|
435
|
-
|
|
436
433
|
raise Errors::TransportError.new(
|
|
437
434
|
code: status_code,
|
|
438
435
|
message: "HTTP client error: #{status_code} - #{response_body}"
|
|
@@ -492,7 +489,12 @@ module RubyLLM
|
|
|
492
489
|
# SSE stream established successfully
|
|
493
490
|
RubyLLM::MCP.logger.debug "SSE stream established"
|
|
494
491
|
# Response will be processed through callbacks
|
|
495
|
-
when
|
|
492
|
+
when 401
|
|
493
|
+
raise Errors::AuthenticationRequiredError.new(
|
|
494
|
+
message: "OAuth authentication required. Server returned 401 Unauthorized.",
|
|
495
|
+
code: 401
|
|
496
|
+
)
|
|
497
|
+
when 405
|
|
496
498
|
# Server doesn't support SSE - this is acceptable
|
|
497
499
|
RubyLLM::MCP.logger.info "Server does not support SSE streaming"
|
|
498
500
|
nil
|
data/lib/ruby_llm/mcp/version.rb
CHANGED
data/lib/ruby_llm/mcp.rb
CHANGED
|
@@ -61,10 +61,10 @@ module RubyLLM
|
|
|
61
61
|
tools.uniq(&:name)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
def mcp_configurations
|
|
65
|
+
config.mcp_configuration.each_with_object({}) do |config, acc|
|
|
66
|
+
acc[config[:name]] = config
|
|
67
|
+
end
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def configure
|
|
@@ -92,5 +92,11 @@ loader.inflector.inflect("sse" => "SSE")
|
|
|
92
92
|
loader.inflector.inflect("openai" => "OpenAI")
|
|
93
93
|
loader.inflector.inflect("streamable_http" => "StreamableHTTP")
|
|
94
94
|
loader.inflector.inflect("http_client" => "HTTPClient")
|
|
95
|
+
loader.inflector.inflect("oauth_provider" => "OAuthProvider")
|
|
96
|
+
loader.inflector.inflect("browser_oauth" => "BrowserOAuth")
|
|
97
|
+
loader.inflector.inflect("browser_oauth_provider" => "BrowserOAuthProvider")
|
|
98
|
+
loader.inflector.inflect("http_server" => "HttpServer")
|
|
99
|
+
loader.inflector.inflect("callback_handler" => "CallbackHandler")
|
|
100
|
+
loader.inflector.inflect("callback_server" => "CallbackServer")
|
|
95
101
|
|
|
96
102
|
loader.setup
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_llm-mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick Vice
|
|
@@ -78,12 +78,46 @@ extra_rdoc_files: []
|
|
|
78
78
|
files:
|
|
79
79
|
- LICENSE
|
|
80
80
|
- README.md
|
|
81
|
-
- lib/generators/ruby_llm/mcp/install_generator.rb
|
|
82
|
-
- lib/generators/ruby_llm/mcp/templates/initializer.rb
|
|
83
|
-
- lib/generators/ruby_llm/mcp/templates/mcps.yml
|
|
81
|
+
- lib/generators/ruby_llm/mcp/install/install_generator.rb
|
|
82
|
+
- lib/generators/ruby_llm/mcp/install/templates/initializer.rb
|
|
83
|
+
- lib/generators/ruby_llm/mcp/install/templates/mcps.yml
|
|
84
|
+
- lib/generators/ruby_llm/mcp/oauth/install_generator.rb
|
|
85
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt
|
|
86
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt
|
|
87
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt
|
|
88
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt
|
|
89
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt
|
|
90
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt
|
|
91
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt
|
|
92
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt
|
|
93
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt
|
|
94
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt
|
|
95
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb
|
|
96
|
+
- lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb
|
|
84
97
|
- lib/ruby_llm/chat.rb
|
|
85
98
|
- lib/ruby_llm/mcp.rb
|
|
86
99
|
- lib/ruby_llm/mcp/attachment.rb
|
|
100
|
+
- lib/ruby_llm/mcp/auth.rb
|
|
101
|
+
- lib/ruby_llm/mcp/auth/browser/callback_handler.rb
|
|
102
|
+
- lib/ruby_llm/mcp/auth/browser/callback_server.rb
|
|
103
|
+
- lib/ruby_llm/mcp/auth/browser/http_server.rb
|
|
104
|
+
- lib/ruby_llm/mcp/auth/browser/opener.rb
|
|
105
|
+
- lib/ruby_llm/mcp/auth/browser/pages.rb
|
|
106
|
+
- lib/ruby_llm/mcp/auth/browser_oauth_provider.rb
|
|
107
|
+
- lib/ruby_llm/mcp/auth/client_registrar.rb
|
|
108
|
+
- lib/ruby_llm/mcp/auth/discoverer.rb
|
|
109
|
+
- lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb
|
|
110
|
+
- lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb
|
|
111
|
+
- lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb
|
|
112
|
+
- lib/ruby_llm/mcp/auth/grant_strategies/base.rb
|
|
113
|
+
- lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb
|
|
114
|
+
- lib/ruby_llm/mcp/auth/http_response_handler.rb
|
|
115
|
+
- lib/ruby_llm/mcp/auth/memory_storage.rb
|
|
116
|
+
- lib/ruby_llm/mcp/auth/oauth_provider.rb
|
|
117
|
+
- lib/ruby_llm/mcp/auth/security.rb
|
|
118
|
+
- lib/ruby_llm/mcp/auth/session_manager.rb
|
|
119
|
+
- lib/ruby_llm/mcp/auth/token_manager.rb
|
|
120
|
+
- lib/ruby_llm/mcp/auth/url_builder.rb
|
|
87
121
|
- lib/ruby_llm/mcp/client.rb
|
|
88
122
|
- lib/ruby_llm/mcp/completion.rb
|
|
89
123
|
- lib/ruby_llm/mcp/configuration.rb
|
|
@@ -145,7 +179,7 @@ metadata:
|
|
|
145
179
|
homepage_uri: https://www.rubyllm-mcp.com
|
|
146
180
|
source_code_uri: https://github.com/patvice/ruby_llm-mcp
|
|
147
181
|
changelog_uri: https://github.com/patvice/ruby_llm-mcp/commits/main
|
|
148
|
-
documentation_uri: https://www.rubyllm-mcp.com
|
|
182
|
+
documentation_uri: https://www.rubyllm-mcp.com/guides/
|
|
149
183
|
bug_tracker_uri: https://github.com/patvice/ruby_llm-mcp/issues
|
|
150
184
|
rubygems_mfa_required: 'true'
|
|
151
185
|
allowed_push_host: https://rubygems.org
|
|
@@ -163,7 +197,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
163
197
|
- !ruby/object:Gem::Version
|
|
164
198
|
version: '0'
|
|
165
199
|
requirements: []
|
|
166
|
-
rubygems_version: 3.6.
|
|
200
|
+
rubygems_version: 3.6.9
|
|
167
201
|
specification_version: 4
|
|
168
202
|
summary: A RubyLLM MCP Client
|
|
169
203
|
test_files: []
|
|
File without changes
|
|
File without changes
|