actionmcp 0.71.1 → 0.80.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 (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +187 -16
  3. data/app/controllers/action_mcp/application_controller.rb +64 -49
  4. data/app/models/action_mcp/session/message.rb +31 -20
  5. data/app/models/action_mcp/session/resource.rb +35 -20
  6. data/app/models/action_mcp/session/sse_event.rb +23 -17
  7. data/app/models/action_mcp/session/subscription.rb +22 -15
  8. data/app/models/action_mcp/session.rb +71 -113
  9. data/config/routes.rb +0 -11
  10. data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
  11. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
  12. data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
  13. data/lib/action_mcp/base_response.rb +1 -1
  14. data/lib/action_mcp/client/base.rb +9 -11
  15. data/lib/action_mcp/client/elicitation.rb +4 -4
  16. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  17. data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
  18. data/lib/action_mcp/client.rb +6 -26
  19. data/lib/action_mcp/configuration.rb +65 -63
  20. data/lib/action_mcp/engine.rb +1 -10
  21. data/lib/action_mcp/filtered_logger.rb +3 -7
  22. data/lib/action_mcp/gateway.rb +7 -11
  23. data/lib/action_mcp/gateway_identifier.rb +187 -3
  24. data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
  25. data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
  26. data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
  27. data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
  28. data/lib/action_mcp/gateway_identifiers.rb +26 -0
  29. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  30. data/lib/action_mcp/prompt.rb +2 -0
  31. data/lib/action_mcp/renderable.rb +1 -1
  32. data/lib/action_mcp/resource_template.rb +6 -2
  33. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
  34. data/lib/action_mcp/server/base_session_store.rb +86 -0
  35. data/lib/action_mcp/server/capabilities.rb +2 -1
  36. data/lib/action_mcp/server/elicitation.rb +3 -9
  37. data/lib/action_mcp/server/error_handling.rb +14 -1
  38. data/lib/action_mcp/server/handlers/router.rb +31 -0
  39. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  40. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  41. data/lib/action_mcp/server/prompts.rb +4 -4
  42. data/lib/action_mcp/server/resources.rb +23 -4
  43. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  44. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  45. data/lib/action_mcp/server/tools.rb +62 -43
  46. data/lib/action_mcp/server/transport_handler.rb +2 -4
  47. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  48. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  49. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  50. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  51. data/lib/action_mcp/tool.rb +48 -37
  52. data/lib/action_mcp/types/float_array_type.rb +5 -3
  53. data/lib/action_mcp/version.rb +1 -1
  54. data/lib/action_mcp.rb +2 -7
  55. data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
  56. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
  57. data/lib/generators/action_mcp/install/install_generator.rb +1 -1
  58. data/lib/generators/action_mcp/install/templates/application_gateway.rb +86 -36
  59. data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
  60. data/lib/tasks/action_mcp_tasks.rake +7 -5
  61. metadata +18 -100
  62. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
  63. data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
  64. data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
  65. data/app/models/action_mcp/oauth_client.rb +0 -157
  66. data/app/models/action_mcp/oauth_token.rb +0 -141
  67. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
  68. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
  69. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
  70. data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
  71. data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
  72. data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
  73. data/lib/action_mcp/jwt_decoder.rb +0 -26
  74. data/lib/action_mcp/jwt_identifier.rb +0 -28
  75. data/lib/action_mcp/none_identifier.rb +0 -19
  76. data/lib/action_mcp/o_auth_identifier.rb +0 -34
  77. data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
  78. data/lib/action_mcp/oauth/error.rb +0 -79
  79. data/lib/action_mcp/oauth/memory_storage.rb +0 -134
  80. data/lib/action_mcp/oauth/middleware.rb +0 -133
  81. data/lib/action_mcp/oauth/provider.rb +0 -426
  82. data/lib/action_mcp/oauth.rb +0 -12
  83. data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
  84. data/lib/action_mcp/server/notifications.rb +0 -58
@@ -1,426 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "securerandom"
4
- require "digest"
5
- require "base64"
6
-
7
- module ActionMCP
8
- module OAuth
9
- # OAuth 2.1 Provider implementation
10
- # Handles authorization codes, access tokens, refresh tokens, and token validation
11
- class Provider
12
- class << self
13
- # Generate authorization code for OAuth flow
14
- # @param client_id [String] OAuth client identifier
15
- # @param redirect_uri [String] Client redirect URI
16
- # @param scope [String] Requested scope
17
- # @param code_challenge [String] PKCE code challenge
18
- # @param code_challenge_method [String] PKCE challenge method (S256, plain)
19
- # @param user_id [String] User identifier
20
- # @return [String] Authorization code
21
- def generate_authorization_code(client_id:, redirect_uri:, scope:, code_challenge: nil, code_challenge_method: nil, user_id:)
22
- # Validate scope
23
- validate_scope(scope) if scope
24
-
25
- code = SecureRandom.urlsafe_base64(32)
26
-
27
- # Store authorization code with metadata
28
- store_authorization_code(code, {
29
- client_id: client_id,
30
- redirect_uri: redirect_uri,
31
- scope: scope,
32
- code_challenge: code_challenge,
33
- code_challenge_method: code_challenge_method,
34
- user_id: user_id,
35
- created_at: Time.current,
36
- expires_at: 10.minutes.from_now
37
- })
38
-
39
- code
40
- end
41
-
42
- # Exchange authorization code for access token
43
- # @param code [String] Authorization code
44
- # @param client_id [String] OAuth client identifier
45
- # @param client_secret [String] OAuth client secret (optional for public clients)
46
- # @param redirect_uri [String] Client redirect URI
47
- # @param code_verifier [String] PKCE code verifier
48
- # @return [Hash] Token response with access_token, token_type, expires_in, scope
49
- def exchange_code_for_token(code:, client_id:, client_secret: nil, redirect_uri:, code_verifier: nil)
50
- # Retrieve and validate authorization code
51
- code_data = retrieve_authorization_code(code)
52
- raise InvalidGrantError, "Invalid authorization code" unless code_data
53
- raise InvalidGrantError, "Authorization code expired" if code_data[:expires_at] < Time.current
54
-
55
- # Validate client
56
- validate_client(client_id, client_secret)
57
-
58
- # Validate redirect URI matches
59
- unless code_data[:redirect_uri] == redirect_uri
60
- raise InvalidGrantError, "Redirect URI mismatch"
61
- end
62
-
63
- # Validate client ID matches
64
- unless code_data[:client_id] == client_id
65
- raise InvalidGrantError, "Client ID mismatch"
66
- end
67
-
68
- # Validate PKCE if challenge was provided during authorization
69
- if code_data[:code_challenge]
70
- validate_pkce(code_data[:code_challenge], code_data[:code_challenge_method], code_verifier)
71
- end
72
-
73
- # Generate access token
74
- access_token = generate_access_token(
75
- client_id: client_id,
76
- scope: code_data[:scope],
77
- user_id: code_data[:user_id]
78
- )
79
-
80
- # Generate refresh token if enabled
81
- refresh_token = nil
82
- if oauth_config[:enable_refresh_tokens]
83
- refresh_token = generate_refresh_token(
84
- client_id: client_id,
85
- scope: code_data[:scope],
86
- user_id: code_data[:user_id],
87
- access_token: access_token
88
- )
89
- end
90
-
91
- # Remove used authorization code
92
- remove_authorization_code(code)
93
-
94
- # Return token response
95
- response = {
96
- access_token: access_token,
97
- token_type: "Bearer",
98
- expires_in: token_expires_in,
99
- scope: code_data[:scope]
100
- }
101
- response[:refresh_token] = refresh_token if refresh_token
102
- response
103
- end
104
-
105
- # Refresh access token using refresh token
106
- # @param refresh_token [String] Refresh token
107
- # @param client_id [String] OAuth client identifier
108
- # @param client_secret [String] OAuth client secret
109
- # @param scope [String] Requested scope (optional, must be subset of original)
110
- # @return [Hash] New token response
111
- def refresh_access_token(refresh_token:, client_id:, client_secret: nil, scope: nil)
112
- # Retrieve refresh token data
113
- token_data = retrieve_refresh_token(refresh_token)
114
- raise InvalidGrantError, "Invalid refresh token" unless token_data
115
- raise InvalidGrantError, "Refresh token expired" if token_data[:expires_at] < Time.current
116
-
117
- # Validate client
118
- validate_client(client_id, client_secret)
119
-
120
- # Validate client ID matches
121
- unless token_data[:client_id] == client_id
122
- raise InvalidGrantError, "Client ID mismatch"
123
- end
124
-
125
- # Validate scope if provided
126
- if scope
127
- requested_scopes = scope.split(" ")
128
- original_scopes = token_data[:scope].split(" ")
129
- unless (requested_scopes - original_scopes).empty?
130
- raise InvalidScopeError, "Requested scope exceeds original scope"
131
- end
132
- else
133
- scope = token_data[:scope]
134
- end
135
-
136
- # Revoke old access token
137
- revoke_access_token(token_data[:access_token]) if token_data[:access_token]
138
-
139
- # Generate new access token
140
- access_token = generate_access_token(
141
- client_id: client_id,
142
- scope: scope,
143
- user_id: token_data[:user_id]
144
- )
145
-
146
- # Update refresh token with new access token
147
- update_refresh_token(refresh_token, access_token)
148
-
149
- {
150
- access_token: access_token,
151
- token_type: "Bearer",
152
- expires_in: token_expires_in,
153
- scope: scope
154
- }
155
- end
156
-
157
- # Validate access token and return token info
158
- # @param access_token [String] Access token to validate
159
- # @return [Hash] Token info with active, client_id, scope, user_id, exp
160
- def introspect_token(access_token)
161
- token_data = retrieve_access_token(access_token)
162
-
163
- unless token_data
164
- return { active: false }
165
- end
166
-
167
- if token_data[:expires_at] < Time.current
168
- remove_access_token(access_token)
169
- return { active: false }
170
- end
171
-
172
- {
173
- active: true,
174
- client_id: token_data[:client_id],
175
- scope: token_data[:scope],
176
- user_id: token_data[:user_id],
177
- exp: token_data[:expires_at].to_i,
178
- iat: token_data[:created_at].to_i,
179
- token_type: "Bearer"
180
- }
181
- end
182
-
183
- # Revoke access or refresh token
184
- # @param token [String] Token to revoke
185
- # @param token_type_hint [String] Type hint: "access_token" or "refresh_token"
186
- # @return [Boolean] True if token was revoked
187
- def revoke_token(token, token_type_hint: nil)
188
- revoked = false
189
-
190
- # Try access token first if hint suggests it or no hint provided
191
- if token_type_hint == "access_token" || token_type_hint.nil?
192
- if retrieve_access_token(token)
193
- revoke_access_token(token)
194
- revoked = true
195
- end
196
- end
197
-
198
- # Try refresh token if not revoked yet
199
- if !revoked && (token_type_hint == "refresh_token" || token_type_hint.nil?)
200
- if retrieve_refresh_token(token)
201
- revoke_refresh_token(token)
202
- revoked = true
203
- end
204
- end
205
-
206
- revoked
207
- end
208
-
209
- # Register a new OAuth client (Dynamic Client Registration)
210
- # @param client_info [Hash] Client registration information
211
- # @return [Hash] Registered client information
212
- def register_client(client_info)
213
- # Store client registration
214
- storage.store_client_registration(client_info[:client_id], client_info)
215
- client_info
216
- end
217
-
218
- # Retrieve registered client information
219
- # @param client_id [String] OAuth client identifier
220
- # @return [Hash, nil] Client information or nil if not found
221
- def get_client(client_id)
222
- storage.retrieve_client_registration(client_id)
223
- end
224
-
225
- # Client Credentials Grant (for server-to-server)
226
- # @param client_id [String] OAuth client identifier
227
- # @param client_secret [String] OAuth client secret
228
- # @param scope [String] Requested scope
229
- # @return [Hash] Token response
230
- def client_credentials_grant(client_id:, client_secret:, scope: nil)
231
- unless oauth_config[:enable_client_credentials]
232
- raise UnsupportedGrantTypeError, "Client credentials grant not supported"
233
- end
234
-
235
- # Validate client credentials
236
- validate_client(client_id, client_secret, require_secret: true)
237
-
238
- # Validate scope
239
- if scope
240
- validate_scope(scope)
241
- else
242
- scope = default_scope
243
- end
244
-
245
- # Generate access token (no user context for client credentials)
246
- access_token = generate_access_token(
247
- client_id: client_id,
248
- scope: scope,
249
- user_id: nil
250
- )
251
-
252
- {
253
- access_token: access_token,
254
- token_type: "Bearer",
255
- expires_in: token_expires_in,
256
- scope: scope
257
- }
258
- end
259
-
260
- private
261
-
262
- def oauth_config
263
- @oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
264
- end
265
-
266
- def validate_client(client_id, client_secret, require_secret: false)
267
- # First check if client is registered via dynamic registration
268
- client_info = get_client(client_id)
269
- if client_info
270
- # Validate client secret for confidential clients
271
- if client_info[:client_secret]
272
- unless client_secret == client_info[:client_secret]
273
- raise InvalidClientError, "Invalid client credentials"
274
- end
275
- elsif require_secret
276
- raise InvalidClientError, "Client authentication required"
277
- end
278
- return true
279
- end
280
-
281
- # Fall back to custom provider validation
282
- provider_class = oauth_config[:provider]
283
- if provider_class && provider_class.respond_to?(:validate_client)
284
- provider_class.validate_client(client_id, client_secret)
285
- elsif require_secret && client_secret.nil?
286
- raise InvalidClientError, "Client authentication required"
287
- else
288
- # In development, allow unregistered clients if configured
289
- if Rails.env.development? && oauth_config[:allow_unregistered_clients] != false
290
- return true
291
- end
292
- raise InvalidClientError, "Unknown client"
293
- end
294
- end
295
-
296
- def validate_pkce(code_challenge, method, code_verifier)
297
- raise InvalidGrantError, "Code verifier required" unless code_verifier
298
-
299
- case method
300
- when "S256"
301
- expected_challenge = Base64.urlsafe_encode64(
302
- Digest::SHA256.digest(code_verifier), padding: false
303
- )
304
- unless code_challenge == expected_challenge
305
- raise InvalidGrantError, "Invalid code verifier"
306
- end
307
- when "plain"
308
- unless oauth_config[:allow_plain_pkce]
309
- raise InvalidGrantError, "Plain PKCE not allowed"
310
- end
311
- unless code_challenge == code_verifier
312
- raise InvalidGrantError, "Invalid code verifier"
313
- end
314
- else
315
- raise InvalidGrantError, "Unsupported code challenge method"
316
- end
317
- end
318
-
319
- def validate_scope(scope)
320
- supported_scopes = oauth_config.fetch(:scopes_supported, [ "mcp:tools", "mcp:resources", "mcp:prompts" ])
321
- requested_scopes = scope.split(" ")
322
- unsupported = requested_scopes - supported_scopes
323
- if unsupported.any?
324
- raise InvalidScopeError, "Unsupported scopes: #{unsupported.join(', ')}"
325
- end
326
- end
327
-
328
- def default_scope
329
- oauth_config.fetch(:default_scope, "mcp:tools mcp:resources mcp:prompts")
330
- end
331
-
332
- def generate_access_token(client_id:, scope:, user_id:)
333
- token = SecureRandom.urlsafe_base64(32)
334
-
335
- store_access_token(token, {
336
- client_id: client_id,
337
- scope: scope,
338
- user_id: user_id,
339
- created_at: Time.current,
340
- expires_at: token_expires_in.seconds.from_now
341
- })
342
-
343
- token
344
- end
345
-
346
- def generate_refresh_token(client_id:, scope:, user_id:, access_token:)
347
- token = SecureRandom.urlsafe_base64(32)
348
-
349
- store_refresh_token(token, {
350
- client_id: client_id,
351
- scope: scope,
352
- user_id: user_id,
353
- access_token: access_token,
354
- created_at: Time.current,
355
- expires_at: refresh_token_expires_in.seconds.from_now
356
- })
357
-
358
- token
359
- end
360
-
361
- def token_expires_in
362
- oauth_config.fetch(:access_token_expires_in, 3600) # 1 hour
363
- end
364
-
365
- def refresh_token_expires_in
366
- oauth_config.fetch(:refresh_token_expires_in, 7.days.to_i) # 1 week
367
- end
368
-
369
- # Storage methods - these delegate to a configurable storage backend
370
- def storage
371
- @storage ||= begin
372
- # Default to ActiveRecord storage for production, memory for test
373
- default_storage = Rails.env.test? ? "ActionMCP::OAuth::MemoryStorage" : "ActionMCP::OAuth::ActiveRecordStorage"
374
- storage_class = oauth_config.fetch(:storage, default_storage)
375
- storage_class = storage_class.constantize if storage_class.is_a?(String)
376
- storage_class.new
377
- end
378
- end
379
-
380
- def store_authorization_code(code, data)
381
- storage.store_authorization_code(code, data)
382
- end
383
-
384
- def retrieve_authorization_code(code)
385
- storage.retrieve_authorization_code(code)
386
- end
387
-
388
- def remove_authorization_code(code)
389
- storage.remove_authorization_code(code)
390
- end
391
-
392
- def store_access_token(token, data)
393
- storage.store_access_token(token, data)
394
- end
395
-
396
- def retrieve_access_token(token)
397
- storage.retrieve_access_token(token)
398
- end
399
-
400
- def remove_access_token(token)
401
- storage.remove_access_token(token)
402
- end
403
-
404
- def revoke_access_token(token)
405
- storage.remove_access_token(token)
406
- end
407
-
408
- def store_refresh_token(token, data)
409
- storage.store_refresh_token(token, data)
410
- end
411
-
412
- def retrieve_refresh_token(token)
413
- storage.retrieve_refresh_token(token)
414
- end
415
-
416
- def update_refresh_token(token, new_access_token)
417
- storage.update_refresh_token(token, new_access_token)
418
- end
419
-
420
- def revoke_refresh_token(token)
421
- storage.remove_refresh_token(token)
422
- end
423
- end
424
- end
425
- end
426
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module OAuth
5
- # Load OAuth components
6
- autoload :Error, "action_mcp/oauth/error"
7
- autoload :Provider, "action_mcp/oauth/provider"
8
- autoload :Middleware, "action_mcp/oauth/middleware"
9
- autoload :MemoryStorage, "action_mcp/oauth/memory_storage"
10
- autoload :ActiveRecordStorage, "action_mcp/oauth/active_record_storage"
11
- end
12
- end
@@ -1,176 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "omniauth-oauth2"
4
-
5
- module ActionMCP
6
- module Omniauth
7
- # MCP-specific Omniauth strategy for OAuth 2.1 authentication
8
- # This strategy integrates with ActionMCP's configuration system and provider interface
9
- class MCPStrategy < ::OmniAuth::Strategies::OAuth2
10
- # Strategy name used in configuration
11
- option :name, "mcp"
12
-
13
- # Default OAuth options with MCP-specific settings
14
- option :client_options, {
15
- authorize_url: "/oauth/authorize",
16
- token_url: "/oauth/token",
17
- auth_scheme: :request_body
18
- }
19
-
20
- # OAuth 2.1 compliance - PKCE is required
21
- option :pkce, true
22
-
23
- # Default scopes for MCP access
24
- option :scope, "mcp:tools mcp:resources mcp:prompts"
25
-
26
- # Use authorization code grant flow
27
- option :response_type, "code"
28
-
29
- # OAuth server metadata discovery
30
- option :discovery, true
31
-
32
- def initialize(app, *args, &block)
33
- super
34
-
35
- # Load configuration from ActionMCP if available
36
- configure_from_mcp_config if defined?(ActionMCP)
37
- end
38
-
39
- # User info from OAuth token response or userinfo endpoint
40
- def raw_info
41
- @raw_info ||= begin
42
- if options.userinfo_url
43
- access_token.get(options.userinfo_url).parsed
44
- else
45
- # Extract user info from token response or use minimal info
46
- token_response = access_token.token
47
- {
48
- "sub" => access_token.params["user_id"] || access_token.token,
49
- "scope" => access_token.params["scope"] || options.scope
50
- }
51
- end
52
- end
53
- rescue ::OAuth2::Error => e
54
- log(:error, "Failed to fetch user info: #{e.message}")
55
- {}
56
- end
57
-
58
- # User ID for Omniauth
59
- uid { raw_info["sub"] || raw_info["user_id"] }
60
-
61
- # User info hash
62
- info do
63
- {
64
- name: raw_info["name"],
65
- email: raw_info["email"],
66
- username: raw_info["username"] || raw_info["preferred_username"]
67
- }
68
- end
69
-
70
- # Extra credentials and token info
71
- extra do
72
- {
73
- "raw_info" => raw_info,
74
- "scope" => access_token.params["scope"],
75
- "token_type" => access_token.params["token_type"] || "Bearer"
76
- }
77
- end
78
-
79
- # OAuth server metadata discovery
80
- def discovery_info
81
- @discovery_info ||= begin
82
- if options.discovery && options.client_options.site
83
- discovery_url = "#{options.client_options.site}/.well-known/oauth-authorization-server"
84
- response = client.request(:get, discovery_url)
85
- JSON.parse(response.body)
86
- end
87
- rescue StandardError => e
88
- log(:warn, "OAuth discovery failed: #{e.message}")
89
- {}
90
- end
91
- end
92
-
93
- # Override client to use discovered endpoints if available
94
- def client
95
- @client ||= begin
96
- if discovery_info.any?
97
- options.client_options.merge!(
98
- authorize_url: discovery_info["authorization_endpoint"],
99
- token_url: discovery_info["token_endpoint"]
100
- ) if discovery_info["authorization_endpoint"] && discovery_info["token_endpoint"]
101
- end
102
- super
103
- end
104
- end
105
-
106
- # Token validation for API requests (not callback flow)
107
- def self.validate_token(token, options = {})
108
- strategy = new(nil, options)
109
- strategy.validate_token(token)
110
- end
111
-
112
- def validate_token(token)
113
- # Validate access token with OAuth server
114
- return nil unless token
115
-
116
- begin
117
- response = client.request(:post, options.introspection_url || "/oauth/introspect", {
118
- body: { token: token },
119
- headers: { "Content-Type" => "application/x-www-form-urlencoded" }
120
- })
121
-
122
- token_info = JSON.parse(response.body)
123
- return nil unless token_info["active"]
124
-
125
- token_info
126
- rescue StandardError => e
127
- log(:error, "Token validation failed: #{e.message}")
128
- nil
129
- end
130
- end
131
-
132
- private
133
-
134
- # Configure strategy from ActionMCP configuration
135
- def configure_from_mcp_config
136
- oauth_config = ActionMCP.configuration.oauth_config
137
- return unless oauth_config.is_a?(Hash)
138
-
139
- # Set client options from MCP config
140
- if oauth_config["issuer_url"]
141
- options.client_options[:site] = oauth_config["issuer_url"]
142
- end
143
-
144
- if oauth_config["client_id"]
145
- options.client_id = oauth_config["client_id"]
146
- end
147
-
148
- if oauth_config["client_secret"]
149
- options.client_secret = oauth_config["client_secret"]
150
- end
151
-
152
- if oauth_config["scopes_supported"]
153
- options.scope = Array(oauth_config["scopes_supported"]).join(" ")
154
- end
155
-
156
- # Enable PKCE if required (OAuth 2.1 compliance)
157
- if oauth_config["pkce_required"]
158
- options.pkce = true
159
- end
160
-
161
- # Set userinfo endpoint if provided
162
- if oauth_config["userinfo_endpoint"]
163
- options.userinfo_url = oauth_config["userinfo_endpoint"]
164
- end
165
-
166
- # Set token introspection endpoint
167
- if oauth_config["introspection_endpoint"]
168
- options.introspection_url = oauth_config["introspection_endpoint"]
169
- end
170
- end
171
- end
172
- end
173
- end
174
-
175
- # Register the strategy with Omniauth
176
- OmniAuth.config.add_camelization "mcp", "MCP"
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Server
5
- module Notifications
6
- # Notify client that the resources list has changed
7
- def send_resources_list_changed_notification
8
- send_jsonrpc_notification("notifications/resources/list_changed")
9
- end
10
-
11
- # Notify client that a specific resource has been updated
12
- def send_resource_updated_notification(uri)
13
- send_jsonrpc_notification("notifications/resources/updated", { uri: })
14
- end
15
-
16
- # Notify client that the tools list has changed
17
- def send_tools_list_changed_notification
18
- send_jsonrpc_notification("notifications/tools/list_changed")
19
- end
20
-
21
- # Notify client that the prompts list has changed
22
- def send_prompts_list_changed_notification
23
- send_jsonrpc_notification("notifications/prompts/list_changed")
24
- end
25
-
26
- # Send a logging message to the client
27
- def send_logging_message_notification(level:, data:, logger: nil)
28
- params = {
29
- level: level,
30
- data: data
31
- }
32
- params[:logger] = logger if logger.present?
33
-
34
- send_jsonrpc_notification("notifications/logging/message", params)
35
- end
36
-
37
- # Updated to match MCP 2025-03-26 specification
38
- def send_progress_notification(progressToken:, progress:, total: nil, message: nil, **options)
39
- params = {
40
- progressToken: progressToken,
41
- progress: progress
42
- }
43
- # Only include total and message if they are present (not nil)
44
- params[:total] = total unless total.nil?
45
- params[:message] = message if message.present?
46
- params.merge!(options) if options.any?
47
-
48
- send_jsonrpc_notification("notifications/progress", params)
49
- end
50
-
51
- # Backward compatibility method for old API
52
- def send_progress_notification_legacy(token:, value:, message: nil)
53
- Rails.logger.warn("DEPRECATION: send_progress_notification with token/value is deprecated. Use progressToken/progress instead.")
54
- send_progress_notification(progressToken: token, progress: value, message: message)
55
- end
56
- end
57
- end
58
- end