ruby_llm-mcp 0.7.1 → 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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/lib/generators/ruby_llm/mcp/{install_generator.rb → install/install_generator.rb} +4 -2
  3. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  4. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  5. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  6. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  16. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  17. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
  18. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +115 -0
  19. data/lib/ruby_llm/mcp/auth/browser/opener.rb +41 -0
  20. data/lib/ruby_llm/mcp/auth/browser/pages.rb +539 -0
  21. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +254 -0
  22. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  23. data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
  24. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
  25. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
  26. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  27. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  28. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  29. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +65 -0
  30. data/lib/ruby_llm/mcp/auth/memory_storage.rb +72 -0
  31. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +226 -0
  32. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  33. data/lib/ruby_llm/mcp/auth/session_manager.rb +56 -0
  34. data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
  35. data/lib/ruby_llm/mcp/auth/url_builder.rb +78 -0
  36. data/lib/ruby_llm/mcp/auth.rb +359 -0
  37. data/lib/ruby_llm/mcp/client.rb +49 -0
  38. data/lib/ruby_llm/mcp/configuration.rb +39 -13
  39. data/lib/ruby_llm/mcp/coordinator.rb +11 -0
  40. data/lib/ruby_llm/mcp/errors.rb +11 -0
  41. data/lib/ruby_llm/mcp/railtie.rb +2 -10
  42. data/lib/ruby_llm/mcp/tool.rb +1 -1
  43. data/lib/ruby_llm/mcp/transport.rb +94 -1
  44. data/lib/ruby_llm/mcp/transports/sse.rb +116 -22
  45. data/lib/ruby_llm/mcp/transports/stdio.rb +4 -3
  46. data/lib/ruby_llm/mcp/transports/streamable_http.rb +81 -79
  47. data/lib/ruby_llm/mcp/version.rb +1 -1
  48. data/lib/ruby_llm/mcp.rb +10 -4
  49. metadata +40 -5
  50. /data/lib/generators/ruby_llm/mcp/{templates → install/templates}/initializer.rb +0 -0
  51. /data/lib/generators/ruby_llm/mcp/{templates → install/templates}/mcps.yml +0 -0
@@ -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( # rubocop:disable Metrics/ParameterLists
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
- @version = version
81
- @reconnection_options = reconnection_options || ReconnectionOptions.new
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
- @resource_metadata_url = nil
86
- @client_id = SecureRandom.uuid
68
+ reconnection = options[:reconnection] || options["reconnection"] || {}
69
+ @reconnection_options = options[:reconnection_options] || ReconnectionOptions.new(**reconnection)
87
70
 
88
- @reconnection_options = ReconnectionOptions.new(**reconnection)
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
- @clients_mutex = Mutex.new
84
+ end
104
85
 
105
- @connection = create_connection
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"] = @uri.to_s
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 405, 401
352
- # TODO: Implement 401 handling this once we are adding authorization
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: response.status,
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: response.status,
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 405, 401
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module MCP
5
- VERSION = "0.7.1"
5
+ VERSION = "0.8.0"
6
6
  end
7
7
  end
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 support_complex_parameters!
65
- warn "[DEPRECATION] RubyLLM::MCP.support_complex_parameters! is no longer needed " \
66
- "and will be removed in version 0.8.0"
67
- # No-op: Complex parameters are now supported by default
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.7.1
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,6 +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
182
+ documentation_uri: https://www.rubyllm-mcp.com/guides/
148
183
  bug_tracker_uri: https://github.com/patvice/ruby_llm-mcp/issues
149
184
  rubygems_mfa_required: 'true'
150
185
  allowed_push_host: https://rubygems.org
@@ -162,7 +197,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
197
  - !ruby/object:Gem::Version
163
198
  version: '0'
164
199
  requirements: []
165
- rubygems_version: 3.6.7
200
+ rubygems_version: 3.6.9
166
201
  specification_version: 4
167
202
  summary: A RubyLLM MCP Client
168
203
  test_files: []