actionmcp 0.72.0 → 0.80.1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/app/controllers/action_mcp/application_controller.rb +20 -12
  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 +42 -119
  9. data/app/models/concerns/{mcp_console_helpers.rb → action_mcp/mcp_console_helpers.rb} +4 -3
  10. data/app/models/concerns/{mcp_message_inspect.rb → action_mcp/mcp_message_inspect.rb} +4 -3
  11. data/config/routes.rb +0 -13
  12. data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
  13. data/lib/action_mcp/client/streamable_http_transport.rb +1 -46
  14. data/lib/action_mcp/client.rb +2 -25
  15. data/lib/action_mcp/configuration.rb +51 -24
  16. data/lib/action_mcp/engine.rb +0 -7
  17. data/lib/action_mcp/filtered_logger.rb +2 -6
  18. data/lib/action_mcp/gateway_identifier.rb +187 -3
  19. data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
  20. data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
  21. data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
  22. data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
  23. data/lib/action_mcp/gateway_identifiers.rb +26 -0
  24. data/lib/action_mcp/resource_template.rb +1 -0
  25. data/lib/action_mcp/server/base_session.rb +2 -0
  26. data/lib/action_mcp/server/resources.rb +8 -7
  27. data/lib/action_mcp/version.rb +1 -1
  28. data/lib/action_mcp.rb +1 -6
  29. data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
  30. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
  31. data/lib/generators/action_mcp/install/install_generator.rb +1 -1
  32. data/lib/generators/action_mcp/install/templates/application_gateway.rb +80 -31
  33. data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
  34. metadata +15 -99
  35. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -265
  36. data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -125
  37. data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -201
  38. data/app/models/action_mcp/oauth_client.rb +0 -159
  39. data/app/models/action_mcp/oauth_token.rb +0 -142
  40. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -28
  41. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -44
  42. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -39
  43. data/lib/action_mcp/client/jwt_client_provider.rb +0 -135
  44. data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
  45. data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
  46. data/lib/action_mcp/jwt_decoder.rb +0 -28
  47. data/lib/action_mcp/jwt_identifier.rb +0 -28
  48. data/lib/action_mcp/none_identifier.rb +0 -19
  49. data/lib/action_mcp/o_auth_identifier.rb +0 -34
  50. data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
  51. data/lib/action_mcp/oauth/error.rb +0 -79
  52. data/lib/action_mcp/oauth/memory_storage.rb +0 -132
  53. data/lib/action_mcp/oauth/middleware.rb +0 -128
  54. data/lib/action_mcp/oauth/provider.rb +0 -406
  55. data/lib/action_mcp/oauth.rb +0 -12
  56. data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -162
@@ -1,183 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module OAuth
5
- # ActiveRecord storage for OAuth tokens and codes
6
- # This is suitable for production multi-server environments
7
- class ActiveRecordStorage
8
- # Authorization code storage
9
- def store_authorization_code(code, data)
10
- OAuthToken.create!(
11
- token: code,
12
- token_type: OAuthToken::AUTHORIZATION_CODE,
13
- client_id: data[:client_id],
14
- user_id: data[:user_id],
15
- redirect_uri: data[:redirect_uri],
16
- scope: data[:scope],
17
- code_challenge: data[:code_challenge],
18
- code_challenge_method: data[:code_challenge_method],
19
- expires_at: data[:expires_at],
20
- metadata: data.except(:client_id, :user_id, :redirect_uri, :scope,
21
- :code_challenge, :code_challenge_method, :expires_at)
22
- )
23
- end
24
-
25
- def retrieve_authorization_code(code)
26
- token = OAuthToken.authorization_codes.active.find_by(token: code)
27
- return nil unless token
28
-
29
- {
30
- client_id: token.client_id,
31
- user_id: token.user_id,
32
- redirect_uri: token.redirect_uri,
33
- scope: token.scope,
34
- code_challenge: token.code_challenge,
35
- code_challenge_method: token.code_challenge_method,
36
- expires_at: token.expires_at,
37
- created_at: token.created_at
38
- }.merge(token.metadata || {})
39
- end
40
-
41
- def remove_authorization_code(code)
42
- OAuthToken.authorization_codes.where(token: code).destroy_all
43
- end
44
-
45
- # Access token storage
46
- def store_access_token(token, data)
47
- OAuthToken.create!(
48
- token: token,
49
- token_type: OAuthToken::ACCESS_TOKEN,
50
- client_id: data[:client_id],
51
- user_id: data[:user_id],
52
- scope: data[:scope],
53
- expires_at: data[:expires_at],
54
- metadata: data.except(:client_id, :user_id, :scope, :expires_at)
55
- )
56
- end
57
-
58
- def retrieve_access_token(token)
59
- token_record = OAuthToken.access_tokens.find_by(token: token)
60
- return nil unless token_record
61
-
62
- {
63
- client_id: token_record.client_id,
64
- user_id: token_record.user_id,
65
- scope: token_record.scope,
66
- expires_at: token_record.expires_at,
67
- created_at: token_record.created_at,
68
- active: token_record.still_valid?
69
- }.merge(token_record.metadata || {})
70
- end
71
-
72
- def remove_access_token(token)
73
- OAuthToken.access_tokens.where(token: token).destroy_all
74
- end
75
-
76
- # Refresh token storage
77
- def store_refresh_token(token, data)
78
- OAuthToken.create!(
79
- token: token,
80
- token_type: OAuthToken::REFRESH_TOKEN,
81
- client_id: data[:client_id],
82
- user_id: data[:user_id],
83
- scope: data[:scope],
84
- access_token: data[:access_token],
85
- expires_at: data[:expires_at],
86
- metadata: data.except(:client_id, :user_id, :scope, :access_token, :expires_at)
87
- )
88
- end
89
-
90
- def retrieve_refresh_token(token)
91
- token_record = OAuthToken.refresh_tokens.active.find_by(token: token)
92
- return nil unless token_record
93
-
94
- {
95
- client_id: token_record.client_id,
96
- user_id: token_record.user_id,
97
- scope: token_record.scope,
98
- access_token: token_record.access_token,
99
- expires_at: token_record.expires_at,
100
- created_at: token_record.created_at
101
- }.merge(token_record.metadata || {})
102
- end
103
-
104
- def update_refresh_token(token, new_access_token)
105
- token_record = OAuthToken.refresh_tokens.find_by(token: token)
106
- token_record&.update!(access_token: new_access_token)
107
- end
108
-
109
- def remove_refresh_token(token)
110
- OAuthToken.refresh_tokens.where(token: token).destroy_all
111
- end
112
-
113
- # Client registration storage
114
- def store_client_registration(client_id, data)
115
- client = OAuthClient.new
116
-
117
- # Map data fields to model attributes
118
- client.client_id = client_id
119
- client.client_secret = data[:client_secret]
120
- client.client_id_issued_at = data[:client_id_issued_at]
121
- client.registration_access_token = data[:registration_access_token]
122
-
123
- # Handle client metadata
124
- metadata = data[:client_metadata] || {}
125
- %w[
126
- client_name redirect_uris grant_types response_types
127
- token_endpoint_auth_method scope
128
- ].each do |field|
129
- client.send("#{field}=", metadata[field]) if metadata.key?(field)
130
- end
131
-
132
- # Store any additional metadata
133
- known_fields = %w[
134
- client_name redirect_uris grant_types response_types
135
- token_endpoint_auth_method scope
136
- ]
137
- additional_metadata = metadata.except(*known_fields)
138
- client.metadata = additional_metadata if additional_metadata.present?
139
-
140
- client.save!
141
- data
142
- end
143
-
144
- def retrieve_client_registration(client_id)
145
- client = OAuthClient.active.find_by(client_id: client_id)
146
- return nil unless client
147
-
148
- {
149
- client_id: client.client_id,
150
- client_secret: client.client_secret,
151
- client_id_issued_at: client.client_id_issued_at,
152
- registration_access_token: client.registration_access_token,
153
- client_metadata: client.to_api_response
154
- }
155
- end
156
-
157
- def remove_client_registration(client_id)
158
- OAuthClient.where(client_id: client_id).destroy_all
159
- end
160
-
161
- # Cleanup expired tokens
162
- def cleanup_expired
163
- OAuthToken.cleanup_expired
164
- end
165
-
166
- # Statistics (for debugging/monitoring)
167
- def stats
168
- {
169
- authorization_codes: OAuthToken.authorization_codes.active.count,
170
- access_tokens: OAuthToken.access_tokens.active.count,
171
- refresh_tokens: OAuthToken.refresh_tokens.active.count,
172
- client_registrations: OAuthClient.active.count
173
- }
174
- end
175
-
176
- # Clear all data (for testing)
177
- def clear_all
178
- OAuthToken.delete_all
179
- OAuthClient.delete_all
180
- end
181
- end
182
- end
183
- end
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module OAuth
5
- # Base OAuth error class
6
- class Error < StandardError
7
- attr_reader :oauth_error_code
8
-
9
- def initialize(message, oauth_error_code = "invalid_request")
10
- super(message)
11
- @oauth_error_code = oauth_error_code
12
- end
13
- end
14
-
15
- # OAuth 2.1 standard error types
16
- class InvalidRequestError < Error
17
- def initialize(message = "Invalid request")
18
- super(message, "invalid_request")
19
- end
20
- end
21
-
22
- class InvalidClientError < Error
23
- def initialize(message = "Invalid client")
24
- super(message, "invalid_client")
25
- end
26
- end
27
-
28
- class InvalidGrantError < Error
29
- def initialize(message = "Invalid grant")
30
- super(message, "invalid_grant")
31
- end
32
- end
33
-
34
- class UnauthorizedClientError < Error
35
- def initialize(message = "Unauthorized client")
36
- super(message, "unauthorized_client")
37
- end
38
- end
39
-
40
- class UnsupportedGrantTypeError < Error
41
- def initialize(message = "Unsupported grant type")
42
- super(message, "unsupported_grant_type")
43
- end
44
- end
45
-
46
- class InvalidScopeError < Error
47
- def initialize(message = "Invalid scope")
48
- super(message, "invalid_scope")
49
- end
50
- end
51
-
52
- class InvalidTokenError < Error
53
- def initialize(message = "Invalid token")
54
- super(message, "invalid_token")
55
- end
56
- end
57
-
58
- class InsufficientScopeError < Error
59
- attr_reader :required_scope
60
-
61
- def initialize(message = "Insufficient scope", required_scope = nil)
62
- super(message, "insufficient_scope")
63
- @required_scope = required_scope
64
- end
65
- end
66
-
67
- class ServerError < Error
68
- def initialize(message = "Server error")
69
- super(message, "server_error")
70
- end
71
- end
72
-
73
- class TemporarilyUnavailableError < Error
74
- def initialize(message = "Temporarily unavailable")
75
- super(message, "temporarily_unavailable")
76
- end
77
- end
78
- end
79
- end
@@ -1,132 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module OAuth
5
- # In-memory storage for OAuth tokens and codes
6
- # This is suitable for development and testing, but not for production
7
- class MemoryStorage
8
- def initialize
9
- @authorization_codes = {}
10
- @access_tokens = {}
11
- @refresh_tokens = {}
12
- @client_registrations = {}
13
- @mutex = Mutex.new
14
- end
15
-
16
- # Authorization code storage
17
- def store_authorization_code(code, data)
18
- @mutex.synchronize do
19
- @authorization_codes[code] = data
20
- end
21
- end
22
-
23
- def retrieve_authorization_code(code)
24
- @mutex.synchronize do
25
- @authorization_codes[code]
26
- end
27
- end
28
-
29
- def remove_authorization_code(code)
30
- @mutex.synchronize do
31
- @authorization_codes.delete(code)
32
- end
33
- end
34
-
35
- # Access token storage
36
- def store_access_token(token, data)
37
- @mutex.synchronize do
38
- @access_tokens[token] = data
39
- end
40
- end
41
-
42
- def retrieve_access_token(token)
43
- @mutex.synchronize do
44
- @access_tokens[token]
45
- end
46
- end
47
-
48
- def remove_access_token(token)
49
- @mutex.synchronize do
50
- @access_tokens.delete(token)
51
- end
52
- end
53
-
54
- # Refresh token storage
55
- def store_refresh_token(token, data)
56
- @mutex.synchronize do
57
- @refresh_tokens[token] = data
58
- end
59
- end
60
-
61
- def retrieve_refresh_token(token)
62
- @mutex.synchronize do
63
- @refresh_tokens[token]
64
- end
65
- end
66
-
67
- def update_refresh_token(token, new_access_token)
68
- @mutex.synchronize do
69
- @refresh_tokens[token][:access_token] = new_access_token if @refresh_tokens[token]
70
- end
71
- end
72
-
73
- def remove_refresh_token(token)
74
- @mutex.synchronize do
75
- @refresh_tokens.delete(token)
76
- end
77
- end
78
-
79
- # Client registration storage
80
- def store_client_registration(client_id, data)
81
- @mutex.synchronize do
82
- @client_registrations[client_id] = data
83
- end
84
- end
85
-
86
- def retrieve_client_registration(client_id)
87
- @mutex.synchronize do
88
- @client_registrations[client_id]
89
- end
90
- end
91
-
92
- def remove_client_registration(client_id)
93
- @mutex.synchronize do
94
- @client_registrations.delete(client_id)
95
- end
96
- end
97
-
98
- # Cleanup expired tokens (optional utility method)
99
- def cleanup_expired
100
- current_time = Time.current
101
-
102
- @mutex.synchronize do
103
- @authorization_codes.reject! { |_, data| data[:expires_at] < current_time }
104
- @access_tokens.reject! { |_, data| data[:expires_at] < current_time }
105
- @refresh_tokens.reject! { |_, data| data[:expires_at] < current_time }
106
- end
107
- end
108
-
109
- # Statistics (for debugging/monitoring)
110
- def stats
111
- @mutex.synchronize do
112
- {
113
- authorization_codes: @authorization_codes.size,
114
- access_tokens: @access_tokens.size,
115
- refresh_tokens: @refresh_tokens.size,
116
- client_registrations: @client_registrations.size
117
- }
118
- end
119
- end
120
-
121
- # Clear all data (for testing)
122
- def clear_all
123
- @mutex.synchronize do
124
- @authorization_codes.clear
125
- @access_tokens.clear
126
- @refresh_tokens.clear
127
- @client_registrations.clear
128
- end
129
- end
130
- end
131
- end
132
- end
@@ -1,128 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "error"
4
-
5
- module ActionMCP
6
- module OAuth
7
- # OAuth middleware that integrates with Omniauth for request authentication
8
- # Handles Bearer token validation for API requests
9
- class Middleware
10
- def initialize(app)
11
- @app = app
12
- end
13
-
14
- def call(env)
15
- request = ActionDispatch::Request.new(env)
16
-
17
- # Skip OAuth processing for non-MCP requests or if OAuth not configured
18
- return @app.call(env) unless should_process_oauth?(request)
19
-
20
- # Skip OAuth processing for metadata endpoints
21
- return @app.call(env) if request.path.start_with?("/.well-known/") || request.path.start_with?("/oauth/")
22
-
23
- # Skip OAuth processing for initialization-related requests
24
- return @app.call(env) if initialization_related_request?(request)
25
-
26
- # Validate Bearer token for API requests
27
- if (bearer_token = extract_bearer_token(request))
28
- validate_oauth_token(request, bearer_token)
29
- end
30
-
31
- @app.call(env)
32
- rescue ActionMCP::OAuth::Error => e
33
- oauth_error_response(e)
34
- end
35
-
36
- private
37
-
38
- def should_process_oauth?(_request)
39
- # Check if OAuth is enabled in configuration
40
- auth_methods = ActionMCP.configuration.authentication_methods
41
- return false unless auth_methods&.include?("oauth")
42
-
43
- # Process all MCP requests (ActionMCP serves at root "/") and OAuth-related paths
44
- true
45
- end
46
-
47
- def initialization_related_request?(request)
48
- # Only check JSON-RPC POST requests to MCP endpoints
49
- # The path might include the mount path (e.g., /action_mcp/ or just /)
50
- return false unless request.post? && request.content_type&.include?("application/json")
51
-
52
- # Check if this is an MCP endpoint (ends with / or is the root)
53
- path = request.path
54
- return false unless path == "/" || path.match?(%r{/action_mcp/?$})
55
-
56
- # Read and parse the request body
57
- body = request.body.read
58
- request.body.rewind # Reset for subsequent reads
59
-
60
- json = JSON.parse(body)
61
- method = json["method"]
62
-
63
- # Check if it's an initialization-related method
64
- %w[initialize notifications/initialized].include?(method)
65
- rescue JSON::ParserError, StandardError
66
- false
67
- end
68
-
69
- def extract_bearer_token(request)
70
- auth_header = request.headers["Authorization"] || request.headers["authorization"]
71
- return nil unless auth_header&.start_with?("Bearer ")
72
-
73
- auth_header.split(" ", 2).last
74
- end
75
-
76
- def validate_oauth_token(request, token)
77
- # Use the OAuth provider for token introspection
78
- token_info = ActionMCP::OAuth::Provider.introspect_token(token)
79
-
80
- unless token_info && token_info[:active]
81
- raise ActionMCP::OAuth::InvalidTokenError, "Invalid or expired OAuth token"
82
- end
83
-
84
- # Store OAuth token info in request environment for Gateway
85
- request.env["action_mcp.oauth_token_info"] = token_info
86
- request.env["action_mcp.oauth_token"] = token
87
- end
88
-
89
- def oauth_error_response(error)
90
- status = case error
91
- when ActionMCP::OAuth::InvalidTokenError
92
- 401
93
- when ActionMCP::OAuth::InsufficientScopeError
94
- 403
95
- else
96
- 400
97
- end
98
-
99
- headers = {
100
- "Content-Type" => "application/json",
101
- "WWW-Authenticate" => www_authenticate_header(error)
102
- }
103
-
104
- body = {
105
- error: error.oauth_error_code,
106
- error_description: error.message
107
- }.to_json
108
-
109
- [ status, headers, [ body ] ]
110
- end
111
-
112
- def www_authenticate_header(error)
113
- params = []
114
- params << 'realm="MCP API"'
115
-
116
- case error
117
- when ActionMCP::OAuth::InvalidTokenError
118
- params << 'error="invalid_token"'
119
- when ActionMCP::OAuth::InsufficientScopeError
120
- params << 'error="insufficient_scope"'
121
- params << "scope=\"#{error.required_scope}\"" if error.required_scope
122
- end
123
-
124
- "Bearer #{params.join(', ')}"
125
- end
126
- end
127
- end
128
- end