actionmcp 0.60.2 → 0.71.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -59
  3. data/app/controllers/action_mcp/application_controller.rb +95 -28
  4. data/app/controllers/action_mcp/oauth/metadata_controller.rb +13 -13
  5. data/app/controllers/action_mcp/oauth/registration_controller.rb +206 -0
  6. data/app/models/action_mcp/oauth_client.rb +157 -0
  7. data/app/models/action_mcp/oauth_token.rb +141 -0
  8. data/app/models/action_mcp/session/message.rb +12 -12
  9. data/app/models/action_mcp/session/resource.rb +2 -2
  10. data/app/models/action_mcp/session/sse_event.rb +2 -2
  11. data/app/models/action_mcp/session/subscription.rb +2 -2
  12. data/app/models/action_mcp/session.rb +68 -43
  13. data/config/routes.rb +1 -0
  14. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +42 -0
  15. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +37 -0
  16. data/lib/action_mcp/capability.rb +2 -0
  17. data/lib/action_mcp/client/json_rpc_handler.rb +9 -9
  18. data/lib/action_mcp/client/jwt_client_provider.rb +134 -0
  19. data/lib/action_mcp/configuration.rb +90 -11
  20. data/lib/action_mcp/engine.rb +26 -1
  21. data/lib/action_mcp/filtered_logger.rb +32 -0
  22. data/lib/action_mcp/oauth/active_record_storage.rb +183 -0
  23. data/lib/action_mcp/oauth/memory_storage.rb +23 -1
  24. data/lib/action_mcp/oauth/middleware.rb +33 -0
  25. data/lib/action_mcp/oauth/provider.rb +49 -13
  26. data/lib/action_mcp/oauth.rb +12 -0
  27. data/lib/action_mcp/prompt.rb +14 -0
  28. data/lib/action_mcp/registry_base.rb +25 -4
  29. data/lib/action_mcp/resource_response.rb +110 -0
  30. data/lib/action_mcp/resource_template.rb +30 -2
  31. data/lib/action_mcp/server/capabilities.rb +3 -14
  32. data/lib/action_mcp/server/memory_session.rb +0 -1
  33. data/lib/action_mcp/server/prompts.rb +8 -1
  34. data/lib/action_mcp/server/resources.rb +9 -6
  35. data/lib/action_mcp/server/tools.rb +41 -20
  36. data/lib/action_mcp/server.rb +6 -3
  37. data/lib/action_mcp/sse_listener.rb +0 -7
  38. data/lib/action_mcp/test_helper.rb +5 -0
  39. data/lib/action_mcp/tool.rb +108 -4
  40. data/lib/action_mcp/tools_registry.rb +3 -0
  41. data/lib/action_mcp/version.rb +1 -1
  42. data/lib/generators/action_mcp/install/templates/mcp.yml +16 -16
  43. data/lib/tasks/action_mcp_tasks.rake +238 -0
  44. metadata +11 -1
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module OAuth
5
+ # OAuth 2.0 Dynamic Client Registration Controller (RFC 7591)
6
+ # Allows clients to dynamically register with the authorization server
7
+ class RegistrationController < ActionController::Base
8
+ protect_from_forgery with: :null_session
9
+ before_action :check_oauth_enabled
10
+ before_action :check_registration_enabled
11
+
12
+ # POST /oauth/register
13
+ # Dynamic client registration endpoint as per RFC 7591
14
+ def create
15
+ # Parse client metadata from request body
16
+ client_metadata = parse_client_metadata
17
+
18
+ # Validate required fields
19
+ validate_client_metadata(client_metadata)
20
+
21
+ # Generate client credentials
22
+ client_id = generate_client_id
23
+ client_secret = nil # Public clients by default
24
+
25
+ # Generate client secret for confidential clients
26
+ if client_metadata["token_endpoint_auth_method"] != "none"
27
+ client_secret = generate_client_secret
28
+ end
29
+
30
+ # Store client registration
31
+ client_info = {
32
+ client_id: client_id,
33
+ client_secret: client_secret,
34
+ client_id_issued_at: Time.current.to_i,
35
+ client_metadata: client_metadata,
36
+ created_at: Time.current
37
+ }
38
+
39
+ # Save client registration (delegated to provider)
40
+ ActionMCP::OAuth::Provider.register_client(client_info)
41
+
42
+ # Build response according to RFC 7591
43
+ response_data = {
44
+ client_id: client_id,
45
+ client_id_issued_at: client_info[:client_id_issued_at]
46
+ }
47
+
48
+ # Include client secret for confidential clients
49
+ if client_secret
50
+ response_data[:client_secret] = client_secret
51
+ response_data[:client_secret_expires_at] = 0 # Never expires
52
+ end
53
+
54
+ # Include all client metadata in response
55
+ response_data.merge!(client_metadata)
56
+
57
+ # Add registration management fields if enabled
58
+ if oauth_config[:enable_registration_management]
59
+ response_data[:registration_access_token] = generate_registration_access_token(client_id)
60
+ response_data[:registration_client_uri] = registration_client_url(client_id)
61
+ end
62
+
63
+ render json: response_data, status: :created
64
+ rescue ActionMCP::OAuth::Error => e
65
+ render_registration_error(e.oauth_error_code, e.message)
66
+ rescue StandardError => e
67
+ Rails.logger.error "Registration error: #{e.message}"
68
+ render_registration_error("invalid_client_metadata", "Invalid client metadata")
69
+ end
70
+
71
+ private
72
+
73
+ def check_oauth_enabled
74
+ auth_methods = ActionMCP.configuration.authentication_methods
75
+ unless auth_methods&.include?("oauth")
76
+ head :not_found
77
+ end
78
+ end
79
+
80
+ def check_registration_enabled
81
+ unless oauth_config[:enable_dynamic_registration]
82
+ head :not_found
83
+ end
84
+ end
85
+
86
+ def oauth_config
87
+ @oauth_config ||= HashWithIndifferentAccess.new(ActionMCP.configuration.oauth_config || {})
88
+ end
89
+
90
+ def parse_client_metadata
91
+ # RFC 7591 requires JSON request body
92
+ unless request.content_type&.include?("application/json")
93
+ raise ActionMCP::OAuth::InvalidRequestError, "Content-Type must be application/json"
94
+ end
95
+
96
+ JSON.parse(request.body.read)
97
+ rescue JSON::ParserError
98
+ raise ActionMCP::OAuth::InvalidRequestError, "Invalid JSON"
99
+ end
100
+
101
+ def validate_client_metadata(metadata)
102
+ # Validate redirect URIs (required for authorization code flow)
103
+ if metadata["grant_types"]&.include?("authorization_code") ||
104
+ metadata["response_types"]&.include?("code")
105
+ unless metadata["redirect_uris"].is_a?(Array) && metadata["redirect_uris"].any?
106
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "redirect_uris required for authorization code flow"
107
+ end
108
+
109
+ # Validate redirect URI format
110
+ metadata["redirect_uris"].each do |uri|
111
+ validate_redirect_uri(uri)
112
+ end
113
+ end
114
+
115
+ # Validate grant types
116
+ if metadata["grant_types"]
117
+ unsupported = metadata["grant_types"] - supported_grant_types
118
+ if unsupported.any?
119
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported grant types: #{unsupported.join(', ')}"
120
+ end
121
+ end
122
+
123
+ # Validate response types
124
+ if metadata["response_types"]
125
+ unsupported = metadata["response_types"] - supported_response_types
126
+ if unsupported.any?
127
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported response types: #{unsupported.join(', ')}"
128
+ end
129
+ end
130
+
131
+ # Validate token endpoint auth method
132
+ if metadata["token_endpoint_auth_method"]
133
+ unless supported_auth_methods.include?(metadata["token_endpoint_auth_method"])
134
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "Unsupported token endpoint auth method"
135
+ end
136
+ end
137
+ end
138
+
139
+ def validate_redirect_uri(uri)
140
+ parsed = URI.parse(uri)
141
+
142
+ # Must be absolute URI
143
+ unless parsed.absolute?
144
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must be absolute"
145
+ end
146
+
147
+ # For non-localhost, must use HTTPS
148
+ unless parsed.host == "localhost" || parsed.host == "127.0.0.1" || parsed.scheme == "https"
149
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "Redirect URI must use HTTPS"
150
+ end
151
+ rescue URI::InvalidURIError
152
+ raise ActionMCP::OAuth::InvalidClientMetadataError, "Invalid redirect URI format"
153
+ end
154
+
155
+ def generate_client_id
156
+ # Generate a unique client identifier
157
+ "mcp_#{SecureRandom.hex(16)}"
158
+ end
159
+
160
+ def generate_client_secret
161
+ # Generate a secure client secret
162
+ SecureRandom.urlsafe_base64(32)
163
+ end
164
+
165
+ def generate_registration_access_token(client_id)
166
+ # Generate a token for managing this registration
167
+ SecureRandom.urlsafe_base64(32)
168
+ end
169
+
170
+ def registration_client_url(client_id)
171
+ "#{request.base_url}/oauth/register/#{client_id}"
172
+ end
173
+
174
+ def supported_grant_types
175
+ grants = [ "authorization_code" ]
176
+ grants << "refresh_token" if oauth_config[:enable_refresh_tokens]
177
+ grants << "client_credentials" if oauth_config[:enable_client_credentials]
178
+ grants
179
+ end
180
+
181
+ def supported_response_types
182
+ [ "code" ]
183
+ end
184
+
185
+ def supported_auth_methods
186
+ methods = [ "client_secret_basic", "client_secret_post" ]
187
+ methods << "none" if oauth_config.fetch(:allow_public_clients, true)
188
+ methods
189
+ end
190
+
191
+ def render_registration_error(error_code, description)
192
+ render json: {
193
+ error: error_code,
194
+ error_description: description
195
+ }, status: :bad_request
196
+ end
197
+ end
198
+
199
+ # Custom error for invalid client metadata
200
+ class InvalidClientMetadataError < Error
201
+ def initialize(message = "Invalid client metadata")
202
+ super(message, "invalid_client_metadata")
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,157 @@
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, -> { where("client_secret_expires_at < ?", Time.current.to_i).where.not(client_secret_expires_at: [ nil, 0 ]) }
45
+
46
+ # Callbacks
47
+ before_create :set_issued_at
48
+
49
+ # Check if client secret is expired
50
+ def secret_expired?
51
+ return false if client_secret_expires_at.nil? || client_secret_expires_at == 0
52
+ Time.current.to_i > client_secret_expires_at
53
+ end
54
+
55
+ # Check if client is public (no authentication required)
56
+ def public_client?
57
+ token_endpoint_auth_method == "none"
58
+ end
59
+
60
+ # Check if client is confidential (authentication required)
61
+ def confidential_client?
62
+ !public_client?
63
+ end
64
+
65
+ # Validate redirect URI against registered URIs
66
+ def valid_redirect_uri?(uri)
67
+ return false if redirect_uris.blank?
68
+ redirect_uris.include?(uri)
69
+ end
70
+
71
+ # Check if grant type is supported by this client
72
+ def supports_grant_type?(grant_type)
73
+ grant_types.include?(grant_type)
74
+ end
75
+
76
+ # Check if response type is supported by this client
77
+ def supports_response_type?(response_type)
78
+ response_types.include?(response_type)
79
+ end
80
+
81
+ # Check if scope is allowed for this client
82
+ def valid_scope?(requested_scope)
83
+ return true if scope.blank? # No scope restrictions
84
+
85
+ requested_scopes = requested_scope.split(" ")
86
+ allowed_scopes = scope.split(" ")
87
+
88
+ # All requested scopes must be in allowed scopes
89
+ (requested_scopes - allowed_scopes).empty?
90
+ end
91
+
92
+ # Convert to hash for API responses
93
+ def to_api_response
94
+ response = {
95
+ client_id: client_id,
96
+ client_id_issued_at: client_id_issued_at
97
+ }
98
+
99
+ # Include client secret for confidential clients
100
+ if client_secret.present?
101
+ response[:client_secret] = client_secret
102
+ response[:client_secret_expires_at] = client_secret_expires_at || 0
103
+ end
104
+
105
+ # Include metadata fields
106
+ %w[
107
+ client_name redirect_uris grant_types response_types
108
+ token_endpoint_auth_method scope
109
+ ].each do |field|
110
+ value = send(field)
111
+ response[field.to_sym] = value if value.present?
112
+ end
113
+
114
+ # Include additional metadata
115
+ response.merge!(metadata) if metadata.present?
116
+
117
+ response
118
+ end
119
+
120
+ # Create from registration request
121
+ def self.create_from_registration(client_metadata)
122
+ client = new
123
+
124
+ # Set basic fields
125
+ client.client_id = "mcp_#{SecureRandom.hex(16)}"
126
+
127
+ # Set metadata fields
128
+ %w[
129
+ client_name redirect_uris grant_types response_types
130
+ token_endpoint_auth_method scope
131
+ ].each do |field|
132
+ client.send("#{field}=", client_metadata[field]) if client_metadata[field]
133
+ end
134
+
135
+ # Generate client secret for confidential clients
136
+ if client.confidential_client?
137
+ client.client_secret = SecureRandom.urlsafe_base64(32)
138
+ end
139
+
140
+ # Store any additional metadata
141
+ known_fields = %w[
142
+ client_name redirect_uris grant_types response_types
143
+ token_endpoint_auth_method scope
144
+ ]
145
+ additional_metadata = client_metadata.except(*known_fields)
146
+ client.metadata = additional_metadata if additional_metadata.present?
147
+
148
+ client
149
+ end
150
+
151
+ private
152
+
153
+ def set_issued_at
154
+ self.client_id_issued_at ||= Time.current.to_i
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,141 @@
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, code_challenge_method: nil)
94
+ create!(
95
+ token: SecureRandom.urlsafe_base64(32),
96
+ token_type: AUTHORIZATION_CODE,
97
+ client_id: client_id,
98
+ user_id: user_id,
99
+ redirect_uri: redirect_uri,
100
+ scope: scope,
101
+ code_challenge: code_challenge,
102
+ code_challenge_method: code_challenge_method,
103
+ expires_at: 10.minutes.from_now
104
+ )
105
+ end
106
+
107
+ # Create access token
108
+ def self.create_access_token(client_id:, user_id:, scope:)
109
+ expires_in = ActionMCP.configuration.oauth_config&.dig("access_token_expires_in") || 3600
110
+
111
+ create!(
112
+ token: SecureRandom.urlsafe_base64(32),
113
+ token_type: ACCESS_TOKEN,
114
+ client_id: client_id,
115
+ user_id: user_id,
116
+ scope: scope,
117
+ expires_at: expires_in.seconds.from_now
118
+ )
119
+ end
120
+
121
+ # Create refresh token
122
+ def self.create_refresh_token(client_id:, user_id:, scope:, access_token:)
123
+ expires_in = ActionMCP.configuration.oauth_config&.dig("refresh_token_expires_in") || 7.days.to_i
124
+
125
+ create!(
126
+ token: SecureRandom.urlsafe_base64(32),
127
+ token_type: REFRESH_TOKEN,
128
+ client_id: client_id,
129
+ user_id: user_id,
130
+ scope: scope,
131
+ access_token: access_token,
132
+ expires_at: expires_in.seconds.from_now
133
+ )
134
+ end
135
+
136
+ # Clean up expired tokens
137
+ def self.cleanup_expired
138
+ expired.delete_all
139
+ end
140
+ end
141
+ end
@@ -4,17 +4,17 @@
4
4
  #
5
5
  # Table name: action_mcp_session_messages
6
6
  #
7
- # id :bigint not null, primary key
8
- # direction(The message recipient) :string default("client"), not null
9
- # is_ping(Whether the message is a ping) :boolean default(FALSE), not null
10
- # message_json :json
11
- # message_type(The type of the message) :string not null
12
- # request_acknowledged :boolean default(FALSE), not null
13
- # request_cancelled :boolean default(FALSE), not null
14
- # created_at :datetime not null
15
- # updated_at :datetime not null
16
- # jsonrpc_id :string
17
- # session_id :string not null
7
+ # id :integer not null, primary key
8
+ # direction :string default("client"), not null
9
+ # is_ping :boolean default(FALSE), not null
10
+ # message_json :json
11
+ # message_type :string not null
12
+ # request_acknowledged :boolean default(FALSE), not null
13
+ # request_cancelled :boolean default(FALSE), not null
14
+ # created_at :datetime not null
15
+ # updated_at :datetime not null
16
+ # jsonrpc_id :string
17
+ # session_id :string not null
18
18
  #
19
19
  # Indexes
20
20
  #
@@ -22,7 +22,7 @@
22
22
  #
23
23
  # Foreign Keys
24
24
  #
25
- # fk_action_mcp_session_messages_session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
25
+ # session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
26
26
  #
27
27
  module ActionMCP
28
28
  class Session
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # Table name: action_mcp_session_resources
6
6
  #
7
- # id :bigint not null, primary key
7
+ # id :integer not null, primary key
8
8
  # created_by_tool :boolean default(FALSE)
9
9
  # description :text
10
10
  # last_accessed_at :datetime
@@ -22,7 +22,7 @@
22
22
  #
23
23
  # Foreign Keys
24
24
  #
25
- # fk_rails_... (session_id => action_mcp_sessions.id) ON DELETE => cascade
25
+ # session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade
26
26
  #
27
27
  module ActionMCP
28
28
  class Session
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # Table name: action_mcp_sse_events
6
6
  #
7
- # id :bigint not null, primary key
7
+ # id :integer not null, primary key
8
8
  # data :text not null
9
9
  # created_at :datetime not null
10
10
  # updated_at :datetime not null
@@ -19,7 +19,7 @@
19
19
  #
20
20
  # Foreign Keys
21
21
  #
22
- # fk_rails_... (session_id => action_mcp_sessions.id)
22
+ # session_id (session_id => action_mcp_sessions.id)
23
23
  #
24
24
  module ActionMCP
25
25
  class Session
@@ -4,7 +4,7 @@
4
4
  #
5
5
  # Table name: action_mcp_session_subscriptions
6
6
  #
7
- # id :bigint not null, primary key
7
+ # id :integer not null, primary key
8
8
  # last_notification_at :datetime
9
9
  # uri :string not null
10
10
  # created_at :datetime not null
@@ -17,7 +17,7 @@
17
17
  #
18
18
  # Foreign Keys
19
19
  #
20
- # fk_rails_... (session_id => action_mcp_sessions.id) ON DELETE => cascade
20
+ # session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade
21
21
  #
22
22
  module ActionMCP
23
23
  class Session