actionmcp 0.72.0 → 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 (52) 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/config/routes.rb +0 -13
  10. data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
  11. data/lib/action_mcp/client/streamable_http_transport.rb +1 -46
  12. data/lib/action_mcp/client.rb +2 -25
  13. data/lib/action_mcp/configuration.rb +51 -24
  14. data/lib/action_mcp/engine.rb +0 -7
  15. data/lib/action_mcp/filtered_logger.rb +2 -6
  16. data/lib/action_mcp/gateway_identifier.rb +187 -3
  17. data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
  18. data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
  19. data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
  20. data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
  21. data/lib/action_mcp/gateway_identifiers.rb +26 -0
  22. data/lib/action_mcp/server/base_session.rb +2 -0
  23. data/lib/action_mcp/version.rb +1 -1
  24. data/lib/action_mcp.rb +1 -6
  25. data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
  26. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
  27. data/lib/generators/action_mcp/install/install_generator.rb +1 -1
  28. data/lib/generators/action_mcp/install/templates/application_gateway.rb +80 -31
  29. data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
  30. metadata +13 -97
  31. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -265
  32. data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -125
  33. data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -201
  34. data/app/models/action_mcp/oauth_client.rb +0 -159
  35. data/app/models/action_mcp/oauth_token.rb +0 -142
  36. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -28
  37. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -44
  38. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -39
  39. data/lib/action_mcp/client/jwt_client_provider.rb +0 -135
  40. data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
  41. data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
  42. data/lib/action_mcp/jwt_decoder.rb +0 -28
  43. data/lib/action_mcp/jwt_identifier.rb +0 -28
  44. data/lib/action_mcp/none_identifier.rb +0 -19
  45. data/lib/action_mcp/o_auth_identifier.rb +0 -34
  46. data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
  47. data/lib/action_mcp/oauth/error.rb +0 -79
  48. data/lib/action_mcp/oauth/memory_storage.rb +0 -132
  49. data/lib/action_mcp/oauth/middleware.rb +0 -128
  50. data/lib/action_mcp/oauth/provider.rb +0 -406
  51. data/lib/action_mcp/oauth.rb +0 -12
  52. data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -162
@@ -1,159 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- # OAuth 2.0 Client model for storing registered clients
5
- # == Schema Information
6
- #
7
- # Table name: action_mcp_oauth_clients
8
- #
9
- # id :integer not null, primary key
10
- # active :boolean default(TRUE)
11
- # client_id_issued_at :integer
12
- # client_name :string
13
- # client_secret :string
14
- # client_secret_expires_at :integer
15
- # grant_types :json
16
- # metadata :json
17
- # redirect_uris :json
18
- # registration_access_token :string
19
- # response_types :json
20
- # scope :text
21
- # token_endpoint_auth_method :string default("client_secret_basic")
22
- # created_at :datetime not null
23
- # updated_at :datetime not null
24
- # client_id :string not null
25
- #
26
- # Indexes
27
- #
28
- # index_action_mcp_oauth_clients_on_active (active)
29
- # index_action_mcp_oauth_clients_on_client_id (client_id) UNIQUE
30
- # index_action_mcp_oauth_clients_on_client_id_issued_at (client_id_issued_at)
31
- #
32
- # Implements RFC 7591 Dynamic Client Registration
33
- class OAuthClient < ApplicationRecord
34
- self.table_name = "action_mcp_oauth_clients"
35
-
36
- # Validations
37
- validates :client_id, presence: true, uniqueness: true
38
- validates :token_endpoint_auth_method, inclusion: {
39
- in: %w[none client_secret_basic client_secret_post client_secret_jwt private_key_jwt]
40
- }
41
-
42
- # Scopes
43
- scope :active, -> { where(active: true) }
44
- scope :expired, lambda {
45
- where("client_secret_expires_at < ?", Time.current.to_i).where.not(client_secret_expires_at: [ nil, 0 ])
46
- }
47
-
48
- # Callbacks
49
- before_create :set_issued_at
50
-
51
- # Check if client secret is expired
52
- def secret_expired?
53
- return false if client_secret_expires_at.nil? || client_secret_expires_at.zero?
54
-
55
- Time.current.to_i > client_secret_expires_at
56
- end
57
-
58
- # Check if client is public (no authentication required)
59
- def public_client?
60
- token_endpoint_auth_method == "none"
61
- end
62
-
63
- # Check if client is confidential (authentication required)
64
- def confidential_client?
65
- !public_client?
66
- end
67
-
68
- # Validate redirect URI against registered URIs
69
- def valid_redirect_uri?(uri)
70
- return false if redirect_uris.blank?
71
-
72
- redirect_uris.include?(uri)
73
- end
74
-
75
- # Check if grant type is supported by this client
76
- def supports_grant_type?(grant_type)
77
- grant_types.include?(grant_type)
78
- end
79
-
80
- # Check if response type is supported by this client
81
- def supports_response_type?(response_type)
82
- response_types.include?(response_type)
83
- end
84
-
85
- # Check if scope is allowed for this client
86
- def valid_scope?(requested_scope)
87
- return true if scope.blank? # No scope restrictions
88
-
89
- requested_scopes = requested_scope.split(" ")
90
- allowed_scopes = scope.split(" ")
91
-
92
- # All requested scopes must be in allowed scopes
93
- (requested_scopes - allowed_scopes).empty?
94
- end
95
-
96
- # Convert to hash for API responses
97
- def to_api_response
98
- response = {
99
- client_id: client_id,
100
- client_id_issued_at: client_id_issued_at
101
- }
102
-
103
- # Include client secret for confidential clients
104
- if client_secret.present?
105
- response[:client_secret] = client_secret
106
- response[:client_secret_expires_at] = client_secret_expires_at || 0
107
- end
108
-
109
- # Include metadata fields
110
- %w[
111
- client_name redirect_uris grant_types response_types
112
- token_endpoint_auth_method scope
113
- ].each do |field|
114
- value = send(field)
115
- response[field.to_sym] = value if value.present?
116
- end
117
-
118
- # Include additional metadata
119
- response.merge!(metadata) if metadata.present?
120
-
121
- response
122
- end
123
-
124
- # Create from registration request
125
- def self.create_from_registration(client_metadata)
126
- client = new
127
-
128
- # Set basic fields
129
- client.client_id = "mcp_#{SecureRandom.hex(16)}"
130
-
131
- # Set metadata fields
132
- %w[
133
- client_name redirect_uris grant_types response_types
134
- token_endpoint_auth_method scope
135
- ].each do |field|
136
- client.send("#{field}=", client_metadata[field]) if client_metadata[field]
137
- end
138
-
139
- # Generate client secret for confidential clients
140
- client.client_secret = SecureRandom.urlsafe_base64(32) if client.confidential_client?
141
-
142
- # Store any additional metadata
143
- known_fields = %w[
144
- client_name redirect_uris grant_types response_types
145
- token_endpoint_auth_method scope
146
- ]
147
- additional_metadata = client_metadata.except(*known_fields)
148
- client.metadata = additional_metadata if additional_metadata.present?
149
-
150
- client
151
- end
152
-
153
- private
154
-
155
- def set_issued_at
156
- self.client_id_issued_at ||= Time.current.to_i
157
- end
158
- end
159
- end
@@ -1,142 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- # == Schema Information
5
- #
6
- # Table name: action_mcp_oauth_tokens
7
- #
8
- # id :integer not null, primary key
9
- # access_token :string
10
- # code_challenge :string
11
- # code_challenge_method :string
12
- # expires_at :datetime
13
- # metadata :json
14
- # redirect_uri :string
15
- # revoked :boolean default(FALSE)
16
- # scope :text
17
- # token :string not null
18
- # token_type :string not null
19
- # created_at :datetime not null
20
- # updated_at :datetime not null
21
- # client_id :string not null
22
- # user_id :string
23
- #
24
- # Indexes
25
- #
26
- # index_action_mcp_oauth_tokens_on_client_id (client_id)
27
- # index_action_mcp_oauth_tokens_on_expires_at (expires_at)
28
- # index_action_mcp_oauth_tokens_on_revoked (revoked)
29
- # index_action_mcp_oauth_tokens_on_token (token) UNIQUE
30
- # index_action_mcp_oauth_tokens_on_token_type (token_type)
31
- # index_action_mcp_oauth_tokens_on_token_type_and_expires_at (token_type,expires_at)
32
- # index_action_mcp_oauth_tokens_on_user_id (user_id)
33
- #
34
- # OAuth 2.0 Token model for storing access tokens, refresh tokens, and authorization codes
35
- class OAuthToken < ApplicationRecord
36
- self.table_name = "action_mcp_oauth_tokens"
37
-
38
- # Token types
39
- ACCESS_TOKEN = "access_token"
40
- REFRESH_TOKEN = "refresh_token"
41
- AUTHORIZATION_CODE = "authorization_code"
42
-
43
- # Validations
44
- validates :token, presence: true, uniqueness: true
45
- validates :token_type, presence: true, inclusion: { in: [ ACCESS_TOKEN, REFRESH_TOKEN, AUTHORIZATION_CODE ] }
46
- validates :client_id, presence: true
47
- validates :expires_at, presence: true
48
-
49
- # Scopes
50
- scope :active, -> { where(revoked: false).where("expires_at > ?", Time.current) }
51
- scope :expired, -> { where("expires_at <= ?", Time.current) }
52
- scope :access_tokens, -> { where(token_type: ACCESS_TOKEN) }
53
- scope :refresh_tokens, -> { where(token_type: REFRESH_TOKEN) }
54
- scope :authorization_codes, -> { where(token_type: AUTHORIZATION_CODE) }
55
-
56
- # Check if token is still valid
57
- def still_valid?
58
- !revoked? && !expired?
59
- end
60
-
61
- # Check if token is expired
62
- def expired?
63
- expires_at <= Time.current
64
- end
65
-
66
- # Revoke the token
67
- def revoke!
68
- update!(revoked: true)
69
- end
70
-
71
- # Convert to introspection response
72
- def to_introspection_response
73
- if still_valid?
74
- {
75
- active: true,
76
- scope: scope,
77
- client_id: client_id,
78
- username: user_id,
79
- token_type: token_type == ACCESS_TOKEN ? "Bearer" : token_type,
80
- exp: expires_at.to_i,
81
- iat: created_at.to_i,
82
- nbf: created_at.to_i,
83
- sub: user_id,
84
- aud: client_id,
85
- iss: ActionMCP.configuration.oauth_config&.dig("issuer_url")
86
- }.compact
87
- else
88
- { active: false }
89
- end
90
- end
91
-
92
- # Create authorization code
93
- def self.create_authorization_code(client_id:, user_id:, redirect_uri:, scope:, code_challenge: nil,
94
- code_challenge_method: nil)
95
- create!(
96
- token: SecureRandom.urlsafe_base64(32),
97
- token_type: AUTHORIZATION_CODE,
98
- client_id: client_id,
99
- user_id: user_id,
100
- redirect_uri: redirect_uri,
101
- scope: scope,
102
- code_challenge: code_challenge,
103
- code_challenge_method: code_challenge_method,
104
- expires_at: 10.minutes.from_now
105
- )
106
- end
107
-
108
- # Create access token
109
- def self.create_access_token(client_id:, user_id:, scope:)
110
- expires_in = ActionMCP.configuration.oauth_config&.dig("access_token_expires_in") || 3600
111
-
112
- create!(
113
- token: SecureRandom.urlsafe_base64(32),
114
- token_type: ACCESS_TOKEN,
115
- client_id: client_id,
116
- user_id: user_id,
117
- scope: scope,
118
- expires_at: expires_in.seconds.from_now
119
- )
120
- end
121
-
122
- # Create refresh token
123
- def self.create_refresh_token(client_id:, user_id:, scope:, access_token:)
124
- expires_in = ActionMCP.configuration.oauth_config&.dig("refresh_token_expires_in") || 7.days.to_i
125
-
126
- create!(
127
- token: SecureRandom.urlsafe_base64(32),
128
- token_type: REFRESH_TOKEN,
129
- client_id: client_id,
130
- user_id: user_id,
131
- scope: scope,
132
- access_token: access_token,
133
- expires_at: expires_in.seconds.from_now
134
- )
135
- end
136
-
137
- # Clean up expired tokens
138
- def self.cleanup_expired
139
- expired.delete_all
140
- end
141
- end
142
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class AddOAuthToSessions < ActiveRecord::Migration[8.0]
4
- def change
5
- # Use json for all databases (PostgreSQL, SQLite3, MySQL) for consistency
6
- json_type = :json
7
-
8
- add_column :action_mcp_sessions, :oauth_access_token, :string unless column_exists?(:action_mcp_sessions,
9
- :oauth_access_token)
10
- add_column :action_mcp_sessions, :oauth_refresh_token, :string unless column_exists?(:action_mcp_sessions,
11
- :oauth_refresh_token)
12
- add_column :action_mcp_sessions, :oauth_token_expires_at, :datetime unless column_exists?(:action_mcp_sessions,
13
- :oauth_token_expires_at)
14
- add_column :action_mcp_sessions, :oauth_user_context, json_type unless column_exists?(:action_mcp_sessions,
15
- :oauth_user_context)
16
- add_column :action_mcp_sessions, :authentication_method, :string, default: 'none' unless column_exists?(
17
- :action_mcp_sessions, :authentication_method
18
- )
19
-
20
- # Add indexes for performance
21
- add_index :action_mcp_sessions, :oauth_access_token, unique: true unless index_exists?(:action_mcp_sessions,
22
- :oauth_access_token)
23
- add_index :action_mcp_sessions, :oauth_token_expires_at unless index_exists?(:action_mcp_sessions,
24
- :oauth_token_expires_at)
25
- add_index :action_mcp_sessions, :authentication_method unless index_exists?(:action_mcp_sessions,
26
- :authentication_method)
27
- end
28
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateActionMCPOAuthClients < ActiveRecord::Migration[7.2]
4
- def change
5
- create_table :action_mcp_oauth_clients do |t|
6
- t.string :client_id, null: false, index: { unique: true }
7
- t.string :client_secret
8
- t.string :client_name
9
-
10
- # Store arrays as JSON for database compatibility
11
- if connection.adapter_name.downcase.include?('postgresql')
12
- t.text :redirect_uris, array: true, default: []
13
- t.text :grant_types, array: true, default: [ 'authorization_code' ]
14
- t.text :response_types, array: true, default: [ 'code' ]
15
- else
16
- # For SQLite and other databases, use JSON
17
- t.json :redirect_uris, default: []
18
- t.json :grant_types, default: [ 'authorization_code' ]
19
- t.json :response_types, default: [ 'code' ]
20
- end
21
-
22
- t.string :token_endpoint_auth_method, default: 'client_secret_basic'
23
- t.text :scope
24
- t.boolean :active, default: true
25
-
26
- # Registration metadata
27
- t.integer :client_id_issued_at
28
- t.integer :client_secret_expires_at
29
- t.string :registration_access_token # OAuth 2.1 Dynamic Client Registration
30
-
31
- # Additional metadata as JSON for database compatibility
32
- if connection.adapter_name.downcase.include?('postgresql')
33
- t.jsonb :metadata, default: {}
34
- else
35
- t.json :metadata, default: {}
36
- end
37
-
38
- t.timestamps
39
- end
40
-
41
- add_index :action_mcp_oauth_clients, :active
42
- add_index :action_mcp_oauth_clients, :client_id_issued_at
43
- end
44
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- class CreateActionMCPOAuthTokens < ActiveRecord::Migration[7.2]
4
- def change
5
- create_table :action_mcp_oauth_tokens do |t|
6
- t.string :token, null: false, index: { unique: true }
7
- t.string :token_type, null: false # 'access_token', 'refresh_token', 'authorization_code'
8
- t.string :client_id, null: false
9
- t.string :user_id
10
- t.text :scope
11
- t.datetime :expires_at
12
- t.boolean :revoked, default: false
13
-
14
- # For authorization codes
15
- t.string :redirect_uri
16
- t.string :code_challenge
17
- t.string :code_challenge_method
18
-
19
- # For refresh tokens
20
- t.string :access_token # Reference to associated access token
21
-
22
- # Additional data - use JSON for database compatibility
23
- if connection.adapter_name.downcase.include?('postgresql')
24
- t.jsonb :metadata, default: {}
25
- else
26
- t.json :metadata, default: {}
27
- end
28
-
29
- t.timestamps
30
- end
31
-
32
- add_index :action_mcp_oauth_tokens, :token_type
33
- add_index :action_mcp_oauth_tokens, :client_id
34
- add_index :action_mcp_oauth_tokens, :user_id
35
- add_index :action_mcp_oauth_tokens, :expires_at
36
- add_index :action_mcp_oauth_tokens, :revoked
37
- add_index :action_mcp_oauth_tokens, %i[token_type expires_at]
38
- end
39
- end
@@ -1,135 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "base64"
5
-
6
- module ActionMCP
7
- module Client
8
- # JWT client provider for MCP client authentication
9
- # Provides clean JWT token management for ActionMCP client connections
10
- class JwtClientProvider
11
- class AuthenticationError < StandardError; end
12
- class TokenExpiredError < StandardError; end
13
-
14
- attr_reader :storage
15
-
16
- def initialize(token: nil, storage: nil, logger: ActionMCP.logger)
17
- @storage = storage || MemoryStorage.new
18
- @logger = logger
19
-
20
- # If token provided during initialization, store it
21
- return unless token
22
-
23
- save_token(token)
24
- end
25
-
26
- # Check if client has valid authentication
27
- def authenticated?
28
- token = current_token
29
- return false unless token
30
-
31
- !token_expired?(token)
32
- end
33
-
34
- # Get authorization headers for HTTP requests
35
- def authorization_headers
36
- token = current_token
37
- return {} unless token
38
-
39
- if token_expired?(token)
40
- log_debug("JWT token expired")
41
- clear_tokens!
42
- return {}
43
- end
44
-
45
- { "Authorization" => "Bearer #{token}" }
46
- end
47
-
48
- # Set/update the JWT token
49
- def set_token(token)
50
- save_token(token)
51
- log_debug("JWT token updated")
52
- end
53
-
54
- # Clear stored tokens (logout)
55
- def clear_tokens!
56
- @storage.clear_token
57
- log_debug("Cleared JWT token")
58
- end
59
-
60
- # Get current valid token
61
- def access_token
62
- token = current_token
63
- return nil unless token
64
- return nil if token_expired?(token)
65
-
66
- token
67
- end
68
-
69
- private
70
-
71
- def current_token
72
- @storage.load_token
73
- end
74
-
75
- def save_token(token)
76
- @storage.save_token(token)
77
- end
78
-
79
- def token_expired?(token)
80
- return false unless token
81
-
82
- begin
83
- payload = decode_jwt_payload(token)
84
- exp = payload["exp"]
85
- return false unless exp
86
-
87
- # Add 30 second buffer for clock skew
88
- Time.at(exp) <= Time.now + 30
89
- rescue StandardError => e
90
- log_debug("Error checking token expiration: #{e.message}")
91
- true # Treat invalid tokens as expired
92
- end
93
- end
94
-
95
- def decode_jwt_payload(token)
96
- # Split JWT into parts
97
- parts = token.split(".")
98
- raise AuthenticationError, "Invalid JWT format" unless parts.length == 3
99
-
100
- # Decode payload (second part)
101
- payload_base64 = parts[1]
102
- # Add padding if needed
103
- payload_base64 += "=" * (4 - payload_base64.length % 4) if payload_base64.length % 4 != 0
104
-
105
- payload_json = Base64.urlsafe_decode64(payload_base64)
106
- JSON.parse(payload_json)
107
- rescue StandardError => e
108
- raise AuthenticationError, "Failed to decode JWT: #{e.message}"
109
- end
110
-
111
- def log_debug(message)
112
- @logger.debug("[ActionMCP::JwtClientProvider] #{message}")
113
- end
114
-
115
- # Simple memory storage for JWT tokens
116
- class MemoryStorage
117
- def initialize
118
- @token = nil
119
- end
120
-
121
- def save_token(token)
122
- @token = token
123
- end
124
-
125
- def load_token
126
- @token
127
- end
128
-
129
- def clear_token
130
- @token = nil
131
- end
132
- end
133
- end
134
- end
135
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActionMCP
4
- module Client
5
- class OauthClientProvider
6
- # Simple in-memory storage for development
7
- # In production, use persistent storage
8
- class MemoryStorage
9
- def initialize
10
- @data = {}
11
- end
12
-
13
- def save_tokens(tokens)
14
- @data[:tokens] = tokens
15
- end
16
-
17
- def load_tokens
18
- @data[:tokens]
19
- end
20
-
21
- def clear_tokens
22
- @data.delete(:tokens)
23
- end
24
-
25
- def save_code_verifier(verifier)
26
- @data[:code_verifier] = verifier
27
- end
28
-
29
- def load_code_verifier
30
- @data[:code_verifier]
31
- end
32
-
33
- def clear_code_verifier
34
- @data.delete(:code_verifier)
35
- end
36
-
37
- def save_client_information(info)
38
- @data[:client_information] = info
39
- end
40
-
41
- def load_client_information
42
- @data[:client_information]
43
- end
44
- end
45
- end
46
- end
47
- end