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,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "digest/sha2"
5
+ require "securerandom"
6
+ require "time"
7
+
8
+ module RubyLLM
9
+ module MCP
10
+ module Auth
11
+ # OAuth configuration constants
12
+ # Token refresh buffer time in seconds (5 minutes)
13
+ TOKEN_REFRESH_BUFFER = 300
14
+
15
+ # Default OAuth timeout in seconds (30 seconds)
16
+ DEFAULT_OAUTH_TIMEOUT = 30
17
+
18
+ # CSRF state parameter size in bytes (32 bytes)
19
+ CSRF_STATE_SIZE = 32
20
+
21
+ # PKCE code verifier size in bytes (32 bytes)
22
+ PKCE_VERIFIER_SIZE = 32
23
+
24
+ # Represents an OAuth 2.1 access token with expiration tracking
25
+ class Token
26
+ attr_reader :access_token, :token_type, :expires_in, :scope, :refresh_token, :expires_at
27
+
28
+ def initialize(access_token:, token_type: "Bearer", expires_in: nil, scope: nil, refresh_token: nil)
29
+ @access_token = access_token
30
+ @token_type = token_type
31
+ @expires_in = expires_in
32
+ @scope = scope
33
+ @refresh_token = refresh_token
34
+ @expires_at = expires_in ? Time.now + expires_in : nil
35
+ end
36
+
37
+ # Check if token has expired
38
+ # @return [Boolean] true if token is expired
39
+ def expired?
40
+ return false unless @expires_at
41
+
42
+ Time.now >= @expires_at
43
+ end
44
+
45
+ # Check if token expires soon (within configured buffer)
46
+ # This enables proactive token refresh
47
+ # @return [Boolean] true if token expires within the buffer period
48
+ def expires_soon?
49
+ return false unless @expires_at
50
+
51
+ Time.now >= (@expires_at - TOKEN_REFRESH_BUFFER)
52
+ end
53
+
54
+ # Format token for Authorization header
55
+ # @return [String] formatted as "Bearer {access_token}"
56
+ def to_header
57
+ "#{@token_type} #{@access_token}"
58
+ end
59
+
60
+ # Serialize token to hash
61
+ # @return [Hash] token data
62
+ def to_h
63
+ {
64
+ access_token: @access_token,
65
+ token_type: @token_type,
66
+ expires_in: @expires_in,
67
+ scope: @scope,
68
+ refresh_token: @refresh_token,
69
+ expires_at: @expires_at&.iso8601
70
+ }
71
+ end
72
+
73
+ # Deserialize token from hash
74
+ # @param data [Hash] token data
75
+ # @return [Token] new token instance
76
+ def self.from_h(data)
77
+ token = new(
78
+ access_token: data[:access_token] || data["access_token"],
79
+ token_type: data[:token_type] || data["token_type"] || "Bearer",
80
+ expires_in: data[:expires_in] || data["expires_in"],
81
+ scope: data[:scope] || data["scope"],
82
+ refresh_token: data[:refresh_token] || data["refresh_token"]
83
+ )
84
+
85
+ # Restore expires_at if present
86
+ expires_at_str = data[:expires_at] || data["expires_at"]
87
+ if expires_at_str
88
+ token.instance_variable_set(:@expires_at, Time.parse(expires_at_str))
89
+ end
90
+
91
+ token
92
+ end
93
+ end
94
+
95
+ # Client metadata for dynamic client registration (RFC 7591)
96
+ # Supports all optional parameters from the specification
97
+ class ClientMetadata
98
+ attr_reader :redirect_uris, :token_endpoint_auth_method, :grant_types, :response_types, :scope,
99
+ :client_name, :client_uri, :logo_uri, :contacts, :tos_uri, :policy_uri,
100
+ :jwks_uri, :jwks, :software_id, :software_version
101
+
102
+ def initialize( # rubocop:disable Metrics/ParameterLists
103
+ redirect_uris:,
104
+ token_endpoint_auth_method: "none",
105
+ grant_types: %w[authorization_code refresh_token],
106
+ response_types: ["code"],
107
+ scope: nil,
108
+ client_name: nil,
109
+ client_uri: nil,
110
+ logo_uri: nil,
111
+ contacts: nil,
112
+ tos_uri: nil,
113
+ policy_uri: nil,
114
+ jwks_uri: nil,
115
+ jwks: nil,
116
+ software_id: nil,
117
+ software_version: nil
118
+ )
119
+ @redirect_uris = redirect_uris
120
+ @token_endpoint_auth_method = token_endpoint_auth_method
121
+ @grant_types = grant_types
122
+ @response_types = response_types
123
+ @scope = scope
124
+ @client_name = client_name
125
+ @client_uri = client_uri
126
+ @logo_uri = logo_uri
127
+ @contacts = contacts
128
+ @tos_uri = tos_uri
129
+ @policy_uri = policy_uri
130
+ @jwks_uri = jwks_uri
131
+ @jwks = jwks
132
+ @software_id = software_id
133
+ @software_version = software_version
134
+ end
135
+
136
+ # Convert to hash for registration request
137
+ # @return [Hash] client metadata
138
+ def to_h
139
+ {
140
+ redirect_uris: @redirect_uris,
141
+ token_endpoint_auth_method: @token_endpoint_auth_method,
142
+ grant_types: @grant_types,
143
+ response_types: @response_types,
144
+ scope: @scope,
145
+ client_name: @client_name,
146
+ client_uri: @client_uri,
147
+ logo_uri: @logo_uri,
148
+ contacts: @contacts,
149
+ tos_uri: @tos_uri,
150
+ policy_uri: @policy_uri,
151
+ jwks_uri: @jwks_uri,
152
+ jwks: @jwks,
153
+ software_id: @software_id,
154
+ software_version: @software_version
155
+ }.compact
156
+ end
157
+ end
158
+
159
+ # Registered client information from authorization server
160
+ class ClientInfo
161
+ attr_reader :client_id, :client_secret, :client_id_issued_at, :client_secret_expires_at, :metadata
162
+
163
+ def initialize(client_id:, client_secret: nil, client_id_issued_at: nil, client_secret_expires_at: nil,
164
+ metadata: nil)
165
+ @client_id = client_id
166
+ @client_secret = client_secret
167
+ @client_id_issued_at = client_id_issued_at
168
+ @client_secret_expires_at = client_secret_expires_at
169
+ @metadata = metadata
170
+ end
171
+
172
+ # Check if client secret has expired
173
+ # @return [Boolean] true if client secret is expired
174
+ def client_secret_expired?
175
+ return false unless @client_secret_expires_at
176
+
177
+ Time.now.to_i >= @client_secret_expires_at
178
+ end
179
+
180
+ # Serialize to hash
181
+ # @return [Hash] client info
182
+ def to_h
183
+ {
184
+ client_id: @client_id,
185
+ client_secret: @client_secret,
186
+ client_id_issued_at: @client_id_issued_at,
187
+ client_secret_expires_at: @client_secret_expires_at,
188
+ metadata: @metadata&.to_h
189
+ }
190
+ end
191
+
192
+ # Deserialize from hash
193
+ # @param data [Hash] client info data
194
+ # @return [ClientInfo] new instance
195
+ def self.from_h(data)
196
+ metadata_data = data[:metadata] || data["metadata"]
197
+ metadata = if metadata_data
198
+ ClientMetadata.new(**metadata_data.transform_keys(&:to_sym))
199
+ end
200
+
201
+ new(
202
+ client_id: data[:client_id] || data["client_id"],
203
+ client_secret: data[:client_secret] || data["client_secret"],
204
+ client_id_issued_at: data[:client_id_issued_at] || data["client_id_issued_at"],
205
+ client_secret_expires_at: data[:client_secret_expires_at] || data["client_secret_expires_at"],
206
+ metadata: metadata
207
+ )
208
+ end
209
+ end
210
+
211
+ # OAuth Authorization Server Metadata (RFC 8414)
212
+ class ServerMetadata
213
+ attr_reader :issuer, :authorization_endpoint, :token_endpoint, :registration_endpoint,
214
+ :scopes_supported, :response_types_supported, :grant_types_supported
215
+
216
+ def initialize(issuer:, authorization_endpoint:, token_endpoint:, options: {})
217
+ @issuer = issuer
218
+ @authorization_endpoint = authorization_endpoint
219
+ @token_endpoint = token_endpoint
220
+ @registration_endpoint = options[:registration_endpoint] || options["registration_endpoint"]
221
+ @scopes_supported = options[:scopes_supported] || options["scopes_supported"]
222
+ @response_types_supported = options[:response_types_supported] || options["response_types_supported"]
223
+ @grant_types_supported = options[:grant_types_supported] || options["grant_types_supported"]
224
+ end
225
+
226
+ # Check if dynamic client registration is supported
227
+ # @return [Boolean] true if registration endpoint exists
228
+ def supports_registration?
229
+ !@registration_endpoint.nil?
230
+ end
231
+
232
+ # Serialize to hash
233
+ # @return [Hash] server metadata
234
+ def to_h
235
+ {
236
+ issuer: @issuer,
237
+ authorization_endpoint: @authorization_endpoint,
238
+ token_endpoint: @token_endpoint,
239
+ registration_endpoint: @registration_endpoint,
240
+ scopes_supported: @scopes_supported,
241
+ response_types_supported: @response_types_supported,
242
+ grant_types_supported: @grant_types_supported
243
+ }.compact
244
+ end
245
+
246
+ # Deserialize from hash
247
+ # @param data [Hash] server metadata
248
+ # @return [ServerMetadata] new instance
249
+ def self.from_h(data)
250
+ new(
251
+ issuer: data[:issuer] || data["issuer"],
252
+ authorization_endpoint: data[:authorization_endpoint] || data["authorization_endpoint"],
253
+ token_endpoint: data[:token_endpoint] || data["token_endpoint"],
254
+ registration_endpoint: data[:registration_endpoint] || data["registration_endpoint"],
255
+ scopes_supported: data[:scopes_supported] || data["scopes_supported"],
256
+ response_types_supported: data[:response_types_supported] || data["response_types_supported"],
257
+ grant_types_supported: data[:grant_types_supported] || data["grant_types_supported"]
258
+ )
259
+ end
260
+ end
261
+
262
+ # OAuth Protected Resource Metadata (RFC 9728)
263
+ # Used for authorization server delegation
264
+ class ResourceMetadata
265
+ attr_reader :resource, :authorization_servers
266
+
267
+ def initialize(resource:, authorization_servers:)
268
+ @resource = resource
269
+ @authorization_servers = authorization_servers
270
+ end
271
+
272
+ # Serialize to hash
273
+ # @return [Hash] resource metadata
274
+ def to_h
275
+ {
276
+ resource: @resource,
277
+ authorization_servers: @authorization_servers
278
+ }
279
+ end
280
+
281
+ # Deserialize from hash
282
+ # @param data [Hash] resource metadata
283
+ # @return [ResourceMetadata] new instance
284
+ def self.from_h(data)
285
+ new(
286
+ resource: data[:resource] || data["resource"],
287
+ authorization_servers: data[:authorization_servers] || data["authorization_servers"]
288
+ )
289
+ end
290
+ end
291
+
292
+ # Proof Key for Code Exchange (PKCE) implementation (RFC 7636)
293
+ # Required for OAuth 2.1 security
294
+ class PKCE
295
+ attr_reader :code_verifier, :code_challenge, :code_challenge_method
296
+
297
+ def initialize
298
+ @code_verifier = generate_code_verifier
299
+ @code_challenge = generate_code_challenge(@code_verifier)
300
+ @code_challenge_method = "S256" # SHA256 - only secure method for OAuth 2.1
301
+ end
302
+
303
+ # Serialize to hash
304
+ # @return [Hash] PKCE parameters
305
+ def to_h
306
+ {
307
+ code_verifier: @code_verifier,
308
+ code_challenge: @code_challenge,
309
+ code_challenge_method: @code_challenge_method
310
+ }
311
+ end
312
+
313
+ # Deserialize from hash
314
+ # @param data [Hash] PKCE data
315
+ # @return [PKCE] new instance
316
+ def self.from_h(data)
317
+ pkce = allocate
318
+ pkce.instance_variable_set(:@code_verifier, data[:code_verifier] || data["code_verifier"])
319
+ pkce.instance_variable_set(:@code_challenge, data[:code_challenge] || data["code_challenge"])
320
+ pkce.instance_variable_set(:@code_challenge_method,
321
+ data[:code_challenge_method] || data["code_challenge_method"] || "S256")
322
+ pkce
323
+ end
324
+
325
+ private
326
+
327
+ # Generate cryptographically secure code verifier
328
+ # @return [String] base64url-encoded random bytes
329
+ def generate_code_verifier
330
+ Base64.urlsafe_encode64(SecureRandom.random_bytes(PKCE_VERIFIER_SIZE), padding: false)
331
+ end
332
+
333
+ # Generate code challenge from verifier using SHA256
334
+ # @param verifier [String] code verifier
335
+ # @return [String] base64url-encoded SHA256 hash
336
+ def generate_code_challenge(verifier)
337
+ digest = Digest::SHA256.digest(verifier)
338
+ Base64.urlsafe_encode64(digest, padding: false)
339
+ end
340
+ end
341
+
342
+ # Factory method to create OAuth providers
343
+ # @param server_url [String] OAuth server URL
344
+ # @param type [Symbol] OAuth provider type (:standard or :browser)
345
+ # @param options [Hash] additional options passed to provider
346
+ # @return [OAuthProvider, BrowserOAuthProvider] OAuth provider instance
347
+ def self.create_oauth(server_url, type: :standard, **options)
348
+ case type
349
+ when :browser
350
+ BrowserOAuthProvider.new(server_url: server_url, **options)
351
+ when :standard
352
+ OAuthProvider.new(server_url: server_url, **options)
353
+ else
354
+ raise ArgumentError, "Unknown OAuth type: #{type}. Must be :standard or :browser"
355
+ end
356
+ end
357
+ end
358
+ end
359
+ end