actionmcp 0.70.0 → 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.
- checksums.yaml +4 -4
- data/README.md +46 -41
- data/app/controllers/action_mcp/application_controller.rb +67 -15
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +13 -13
- data/app/controllers/action_mcp/oauth/registration_controller.rb +206 -0
- data/app/models/action_mcp/oauth_client.rb +157 -0
- data/app/models/action_mcp/oauth_token.rb +141 -0
- data/app/models/action_mcp/session/message.rb +12 -12
- data/app/models/action_mcp/session/resource.rb +2 -2
- data/app/models/action_mcp/session/sse_event.rb +2 -2
- data/app/models/action_mcp/session/subscription.rb +2 -2
- data/app/models/action_mcp/session.rb +22 -22
- data/config/routes.rb +1 -0
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +42 -0
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +37 -0
- data/lib/action_mcp/client/jwt_client_provider.rb +134 -0
- data/lib/action_mcp/configuration.rb +27 -4
- data/lib/action_mcp/filtered_logger.rb +32 -0
- data/lib/action_mcp/oauth/active_record_storage.rb +183 -0
- data/lib/action_mcp/oauth/memory_storage.rb +23 -1
- data/lib/action_mcp/oauth/middleware.rb +33 -0
- data/lib/action_mcp/oauth/provider.rb +49 -13
- data/lib/action_mcp/oauth.rb +12 -0
- data/lib/action_mcp/server/capabilities.rb +0 -3
- data/lib/action_mcp/server/resources.rb +1 -1
- data/lib/action_mcp/server/tools.rb +36 -24
- data/lib/action_mcp/sse_listener.rb +0 -7
- data/lib/action_mcp/test_helper.rb +5 -0
- data/lib/action_mcp/tool.rb +94 -4
- data/lib/action_mcp/tools_registry.rb +3 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/generators/action_mcp/install/templates/mcp.yml +16 -16
- metadata +10 -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
|
8
|
-
# direction
|
9
|
-
# is_ping
|
10
|
-
# message_json
|
11
|
-
# message_type
|
12
|
-
# request_acknowledged
|
13
|
-
# request_cancelled
|
14
|
-
# created_at
|
15
|
-
# updated_at
|
16
|
-
# jsonrpc_id
|
17
|
-
# session_id
|
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
|
-
#
|
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 :
|
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
|
-
#
|
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 :
|
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
|
-
#
|
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 :
|
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
|
-
#
|
20
|
+
# session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade
|
21
21
|
#
|
22
22
|
module ActionMCP
|
23
23
|
class Session
|
@@ -4,28 +4,28 @@
|
|
4
4
|
#
|
5
5
|
# Table name: action_mcp_sessions
|
6
6
|
#
|
7
|
-
# id
|
8
|
-
# authentication_method
|
9
|
-
# client_capabilities
|
10
|
-
# client_info
|
11
|
-
# ended_at
|
12
|
-
# initialized
|
13
|
-
# messages_count
|
14
|
-
# oauth_access_token
|
15
|
-
# oauth_refresh_token
|
16
|
-
# oauth_token_expires_at
|
17
|
-
# oauth_user_context
|
18
|
-
# prompt_registry
|
19
|
-
# protocol_version
|
20
|
-
# resource_registry
|
21
|
-
# role
|
22
|
-
# server_capabilities
|
23
|
-
# server_info
|
24
|
-
# sse_event_counter
|
25
|
-
# status
|
26
|
-
# tool_registry
|
27
|
-
# created_at
|
28
|
-
# updated_at
|
7
|
+
# id :string not null, primary key
|
8
|
+
# authentication_method :string default("none")
|
9
|
+
# client_capabilities :json
|
10
|
+
# client_info :json
|
11
|
+
# ended_at :datetime
|
12
|
+
# initialized :boolean default(FALSE), not null
|
13
|
+
# messages_count :integer default(0), not null
|
14
|
+
# oauth_access_token :string
|
15
|
+
# oauth_refresh_token :string
|
16
|
+
# oauth_token_expires_at :datetime
|
17
|
+
# oauth_user_context :json
|
18
|
+
# prompt_registry :json
|
19
|
+
# protocol_version :string
|
20
|
+
# resource_registry :json
|
21
|
+
# role :string default("server"), not null
|
22
|
+
# server_capabilities :json
|
23
|
+
# server_info :json
|
24
|
+
# sse_event_counter :integer default(0), not null
|
25
|
+
# status :string default("pre_initialize"), not null
|
26
|
+
# tool_registry :json
|
27
|
+
# created_at :datetime not null
|
28
|
+
# updated_at :datetime not null
|
29
29
|
#
|
30
30
|
# Indexes
|
31
31
|
#
|
data/config/routes.rb
CHANGED
@@ -12,6 +12,7 @@ ActionMCP::Engine.routes.draw do
|
|
12
12
|
post "/oauth/token", to: "oauth/endpoints#token", as: :oauth_token
|
13
13
|
post "/oauth/introspect", to: "oauth/endpoints#introspect", as: :oauth_introspect
|
14
14
|
post "/oauth/revoke", to: "oauth/endpoints#revoke", as: :oauth_revoke
|
15
|
+
post "/oauth/register", to: "oauth/registration#create", as: :oauth_register
|
15
16
|
|
16
17
|
# MCP 2025-03-26 Spec routes
|
17
18
|
get "/", to: "application#show", as: :mcp_get
|