actionmcp 0.52.2 → 0.54.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a11c68a5511cb3cafd2f90ae24aa31e2099f41c8b4ee3d357719574f598752d4
4
- data.tar.gz: 145398e069f03f6bd2697a16efca476d47983ccdcf84f3730f9ea0b0223eec03
3
+ metadata.gz: 8f3d6323b7f8ce694050a031c566f30e33d637116e2869cda61fbbaad0702411
4
+ data.tar.gz: ef8bfccea1b3109146b3e490340a44c4a6071e08c6aed3bd72ddecd5fd95638a
5
5
  SHA512:
6
- metadata.gz: 9bb02d28a5b99c70bde877ab7e481619c26d55f68b47fbebc40e384400fc445c4683e04e35ac6bc9f837028bb5af9ec84928662fb8fd93e80e3a7bcec446e6ad
7
- data.tar.gz: 78c11ff2bc200f7a482ffefc516b2ab2ad487399b37685d3fe49cd907de6b1e6a3aafd891ea584b3b2387c4567c4d58c1ed9db5d6aaca35b065ac613f505c4d8
6
+ metadata.gz: 2665d85224ce5b07a115936d1f1d159b70c8068a8a221e2f196bebd9564b9e753a73acd15b86241b04d277e70dc6dc5db818fecda505cfab9beb999d8483aecb
7
+ data.tar.gz: 56a568c5aca9693534a46d1013a9fdd8cb168a92e10b6c947dc02bb345f0c0a4f5b812edac990ddaa84c0ba589c500d47b7841f22b8542b1f36a120d2c8a1756
data/README.md CHANGED
@@ -476,16 +476,21 @@ This ensures all thread pools are properly terminated and tasks are completed.
476
476
 
477
477
  **ActionMCP** runs as a standalone Rack application. **Do not attempt to mount it in your application's `routes.rb`**—it is not designed to be mounted as an engine at a custom path. When you use `run ActionMCP::Engine` in your `mcp.ru`, the MCP endpoint is always available at the root path (`/`).
478
478
 
479
- ### Installing the Configuration Generator
479
+ ### Installing ActionMCP
480
480
 
481
- ActionMCP includes a generator to help you create the configuration file:
481
+ ActionMCP includes generators to help you set up your project quickly. The install generator creates all necessary base classes and configuration files:
482
482
 
483
483
  ```bash
484
- # Generate the mcp.yml configuration file
485
- bin/rails generate action_mcp:config
484
+ # Install ActionMCP with base classes and configuration
485
+ bin/rails generate action_mcp:install
486
486
  ```
487
487
 
488
- This will create `config/mcp.yml` with example configurations for all environments.
488
+ This will create:
489
+ - `app/mcp/prompts/application_mcp_prompt.rb` - Base prompt class
490
+ - `app/mcp/tools/application_mcp_tool.rb` - Base tool class
491
+ - `app/mcp/resource_templates/application_mcp_res_template.rb` - Base resource template class
492
+ - `app/mcp/application_gateway.rb` - Gateway for authentication
493
+ - `config/mcp.yml` - Configuration file with example settings for all environments
489
494
 
490
495
  > **Note:** Authentication and authorization are not included. You are responsible for securing the endpoint.
491
496
 
@@ -493,6 +498,8 @@ This will create `config/mcp.yml` with example configurations for all environmen
493
498
 
494
499
  ActionMCP provides a Gateway system similar to ActionCable's Connection for handling authentication. The Gateway allows you to authenticate users and make them available throughout your MCP components.
495
500
 
501
+ ActionMCP supports multiple authentication methods including OAuth 2.1, JWT tokens, and no authentication for development. For detailed OAuth 2.1 configuration and usage, see the [OAuth Authentication Guide](OAUTH.md).
502
+
496
503
  ### Creating an ApplicationGateway
497
504
 
498
505
  When you run the install generator, it creates an `ApplicationGateway` class:
@@ -645,14 +652,14 @@ location /mcp/ {
645
652
 
646
653
  ActionMCP includes Rails generators to help you quickly set up your MCP server components.
647
654
 
648
- You can generate the base classes for your MCP Prompt and Tool using the following command:
655
+ First, install ActionMCP to create base classes and configuration:
649
656
 
650
657
  ```bash
651
658
  bin/rails action_mcp:install:migrations # to copy the migrations
652
659
  bin/rails generate action_mcp:install
653
660
  ```
654
661
 
655
- This will create the base application classes in your app directory.
662
+ This will create the base application classes, configuration file, and authentication gateway in your app directory.
656
663
 
657
664
  ### Generate a New Prompt
658
665
 
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module ActionMCP
6
+ module OAuth
7
+ # OAuth 2.1 endpoints controller
8
+ # Handles authorization, token, introspection, and revocation endpoints
9
+ class EndpointsController < ActionController::Base
10
+ protect_from_forgery with: :null_session
11
+ before_action :check_oauth_enabled
12
+
13
+ # GET /oauth/authorize
14
+ # Authorization endpoint for OAuth 2.1 authorization code flow
15
+ def authorize
16
+ # Extract parameters
17
+ client_id = params[:client_id]
18
+ redirect_uri = params[:redirect_uri]
19
+ response_type = params[:response_type]
20
+ scope = params[:scope]
21
+ state = params[:state]
22
+ code_challenge = params[:code_challenge]
23
+ code_challenge_method = params[:code_challenge_method]
24
+
25
+ # Validate required parameters
26
+ if client_id.blank? || redirect_uri.blank? || response_type.blank?
27
+ return render_error("invalid_request", "Missing required parameters")
28
+ end
29
+
30
+ # Validate response type
31
+ unless response_type == "code"
32
+ return render_error("unsupported_response_type", "Only authorization code flow supported")
33
+ end
34
+
35
+ # Validate PKCE if required
36
+ if oauth_config["pkce_required"] && code_challenge.blank?
37
+ return render_error("invalid_request", "PKCE required")
38
+ end
39
+
40
+ # In a real implementation, this would show a consent page
41
+ # For now, we'll auto-approve for configured clients
42
+ if auto_approve_client?(client_id)
43
+ # Generate authorization code
44
+ user_id = current_user&.id || "anonymous"
45
+
46
+ begin
47
+ code = ActionMCP::OAuth::Provider.generate_authorization_code(
48
+ client_id: client_id,
49
+ redirect_uri: redirect_uri,
50
+ scope: scope || default_scope,
51
+ code_challenge: code_challenge,
52
+ code_challenge_method: code_challenge_method,
53
+ user_id: user_id
54
+ )
55
+
56
+ # Redirect back to client with authorization code
57
+ redirect_params = { code: code }
58
+ redirect_params[:state] = state if state
59
+ redirect_to "#{redirect_uri}?#{redirect_params.to_query}", allow_other_host: true
60
+ rescue ActionMCP::OAuth::Error => e
61
+ render_error(e.oauth_error_code, e.message)
62
+ end
63
+ else
64
+ # In production, show consent page
65
+ render_consent_page(client_id, redirect_uri, scope, state, code_challenge, code_challenge_method)
66
+ end
67
+ end
68
+
69
+ # POST /oauth/token
70
+ # Token endpoint for exchanging authorization codes and refreshing tokens
71
+ def token
72
+ grant_type = params[:grant_type]
73
+
74
+ case grant_type
75
+ when "authorization_code"
76
+ handle_authorization_code_grant
77
+ when "refresh_token"
78
+ handle_refresh_token_grant
79
+ when "client_credentials"
80
+ handle_client_credentials_grant
81
+ else
82
+ render_token_error("unsupported_grant_type", "Unsupported grant type")
83
+ end
84
+ rescue ActionMCP::OAuth::Error => e
85
+ render_token_error(e.oauth_error_code, e.message)
86
+ end
87
+
88
+ # POST /oauth/introspect
89
+ # Token introspection endpoint (RFC 7662)
90
+ def introspect
91
+ token = params[:token]
92
+ return render_introspection_error unless token
93
+
94
+ # Authenticate client for introspection
95
+ client_id, client_secret = extract_client_credentials
96
+ return render_introspection_error unless client_id
97
+
98
+ begin
99
+ token_info = ActionMCP::OAuth::Provider.introspect_token(token)
100
+ render json: token_info
101
+ rescue ActionMCP::OAuth::Error
102
+ render json: { active: false }
103
+ end
104
+ end
105
+
106
+ # POST /oauth/revoke
107
+ # Token revocation endpoint (RFC 7009)
108
+ def revoke
109
+ token = params[:token]
110
+ token_type_hint = params[:token_type_hint]
111
+
112
+ return head :bad_request unless token
113
+
114
+ # Authenticate client
115
+ client_id, client_secret = extract_client_credentials
116
+ return head :unauthorized unless client_id
117
+
118
+ begin
119
+ ActionMCP::OAuth::Provider.revoke_token(token, token_type_hint: token_type_hint)
120
+ head :ok
121
+ rescue ActionMCP::OAuth::Error
122
+ head :bad_request
123
+ end
124
+ end
125
+
126
+ private
127
+
128
+ def check_oauth_enabled
129
+ auth_methods = ActionMCP.configuration.authentication_methods
130
+ unless auth_methods&.include?("oauth")
131
+ head :not_found
132
+ end
133
+ end
134
+
135
+ def oauth_config
136
+ @oauth_config ||= ActionMCP.configuration.oauth_config || {}
137
+ end
138
+
139
+ def handle_authorization_code_grant
140
+ code = params[:code]
141
+ client_id = params[:client_id]
142
+ client_secret = params[:client_secret]
143
+ redirect_uri = params[:redirect_uri]
144
+ code_verifier = params[:code_verifier]
145
+
146
+ # Extract client credentials from Authorization header if not in params
147
+ if client_id.blank?
148
+ client_id, client_secret = extract_client_credentials
149
+ end
150
+
151
+ return render_token_error("invalid_request", "Missing required parameters") if code.blank? || client_id.blank?
152
+
153
+ token_response = ActionMCP::OAuth::Provider.exchange_code_for_token(
154
+ code: code,
155
+ client_id: client_id,
156
+ client_secret: client_secret,
157
+ redirect_uri: redirect_uri,
158
+ code_verifier: code_verifier
159
+ )
160
+
161
+ render json: token_response
162
+ end
163
+
164
+ def handle_refresh_token_grant
165
+ refresh_token = params[:refresh_token]
166
+ scope = params[:scope]
167
+
168
+ # Extract client credentials
169
+ client_id, client_secret = extract_client_credentials
170
+ client_id ||= params[:client_id]
171
+ client_secret ||= params[:client_secret]
172
+
173
+ return render_token_error("invalid_request", "Missing required parameters") if refresh_token.blank? || client_id.blank?
174
+
175
+ token_response = ActionMCP::OAuth::Provider.refresh_access_token(
176
+ refresh_token: refresh_token,
177
+ client_id: client_id,
178
+ client_secret: client_secret,
179
+ scope: scope
180
+ )
181
+
182
+ render json: token_response
183
+ end
184
+
185
+ def handle_client_credentials_grant
186
+ scope = params[:scope]
187
+
188
+ # Extract client credentials
189
+ client_id, client_secret = extract_client_credentials
190
+ client_id ||= params[:client_id]
191
+ client_secret ||= params[:client_secret]
192
+
193
+ return render_token_error("invalid_request", "Missing client credentials") if client_id.blank?
194
+
195
+ token_response = ActionMCP::OAuth::Provider.client_credentials_grant(
196
+ client_id: client_id,
197
+ client_secret: client_secret,
198
+ scope: scope
199
+ )
200
+
201
+ render json: token_response
202
+ end
203
+
204
+ def extract_client_credentials
205
+ auth_header = request.headers["Authorization"]
206
+ if auth_header&.start_with?("Basic ")
207
+ encoded = auth_header.split(" ", 2).last
208
+ decoded = Base64.decode64(encoded)
209
+ decoded.split(":", 2)
210
+ else
211
+ [ nil, nil ]
212
+ end
213
+ end
214
+
215
+ def auto_approve_client?(client_id)
216
+ # In development/testing, auto-approve known clients
217
+ # In production, this should check a proper client registry
218
+ Rails.env.development? || Rails.env.test? || oauth_config["auto_approve_clients"]&.include?(client_id)
219
+ end
220
+
221
+ def current_user
222
+ # This should be implemented by the application
223
+ # For now, return a default user for development
224
+ if Rails.env.development? || Rails.env.test?
225
+ OpenStruct.new(id: "dev_user", email: "dev@example.com")
226
+ else
227
+ # In production, this should integrate with your authentication system
228
+ nil
229
+ end
230
+ end
231
+
232
+ def default_scope
233
+ oauth_config["default_scope"] || "mcp:tools mcp:resources mcp:prompts"
234
+ end
235
+
236
+ def render_error(error_code, description)
237
+ render json: {
238
+ error: error_code,
239
+ error_description: description
240
+ }, status: :bad_request
241
+ end
242
+
243
+ def render_token_error(error_code, description)
244
+ render json: {
245
+ error: error_code,
246
+ error_description: description
247
+ }, status: :bad_request
248
+ end
249
+
250
+ def render_introspection_error
251
+ render json: { active: false }, status: :bad_request
252
+ end
253
+
254
+ def render_consent_page(client_id, redirect_uri, scope, state, code_challenge, code_challenge_method)
255
+ # In production, this would render a proper consent page
256
+ # For now, just auto-deny unknown clients
257
+ render json: {
258
+ error: "access_denied",
259
+ error_description: "User denied authorization"
260
+ }, status: :forbidden
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module OAuth
5
+ # Controller for OAuth 2.1 metadata endpoints
6
+ # Provides server discovery information as per RFC 8414
7
+ class MetadataController < ActionController::Base
8
+ before_action :check_oauth_enabled
9
+
10
+ # GET /.well-known/oauth-authorization-server
11
+ # Returns OAuth Authorization Server Metadata as per RFC 8414
12
+ def authorization_server
13
+ metadata = {
14
+ issuer: issuer_url,
15
+ authorization_endpoint: authorization_endpoint,
16
+ token_endpoint: token_endpoint,
17
+ introspection_endpoint: introspection_endpoint,
18
+ revocation_endpoint: revocation_endpoint,
19
+ response_types_supported: response_types_supported,
20
+ grant_types_supported: grant_types_supported,
21
+ token_endpoint_auth_methods_supported: token_endpoint_auth_methods_supported,
22
+ scopes_supported: scopes_supported,
23
+ code_challenge_methods_supported: code_challenge_methods_supported,
24
+ service_documentation: service_documentation
25
+ }
26
+
27
+ # Add optional fields based on configuration
28
+ if oauth_config["enable_dynamic_registration"]
29
+ metadata[:registration_endpoint] = registration_endpoint
30
+ end
31
+
32
+ if oauth_config["jwks_uri"]
33
+ metadata[:jwks_uri] = oauth_config["jwks_uri"]
34
+ end
35
+
36
+ render json: metadata
37
+ end
38
+
39
+ # GET /.well-known/oauth-protected-resource
40
+ # Returns Protected Resource Metadata as per RFC 8705
41
+ def protected_resource
42
+ metadata = {
43
+ resource: issuer_url,
44
+ authorization_servers: [ issuer_url ],
45
+ scopes_supported: scopes_supported,
46
+ bearer_methods_supported: [ "header" ],
47
+ resource_documentation: resource_documentation
48
+ }
49
+
50
+ render json: metadata
51
+ end
52
+
53
+ private
54
+
55
+ def check_oauth_enabled
56
+ auth_methods = ActionMCP.configuration.authentication_methods
57
+ unless auth_methods&.include?("oauth")
58
+ head :not_found
59
+ end
60
+ end
61
+
62
+ def oauth_config
63
+ @oauth_config ||= ActionMCP.configuration.oauth_config || {}
64
+ end
65
+
66
+ def issuer_url
67
+ @issuer_url ||= oauth_config["issuer_url"] || request.base_url
68
+ end
69
+
70
+ def authorization_endpoint
71
+ "#{issuer_url}/oauth/authorize"
72
+ end
73
+
74
+ def token_endpoint
75
+ "#{issuer_url}/oauth/token"
76
+ end
77
+
78
+ def introspection_endpoint
79
+ "#{issuer_url}/oauth/introspect"
80
+ end
81
+
82
+ def revocation_endpoint
83
+ "#{issuer_url}/oauth/revoke"
84
+ end
85
+
86
+ def registration_endpoint
87
+ "#{issuer_url}/oauth/register"
88
+ end
89
+
90
+ def response_types_supported
91
+ [ "code" ]
92
+ end
93
+
94
+ def grant_types_supported
95
+ grants = [ "authorization_code" ]
96
+ grants << "refresh_token" if oauth_config["enable_refresh_tokens"]
97
+ grants << "client_credentials" if oauth_config["enable_client_credentials"]
98
+ grants
99
+ end
100
+
101
+ def token_endpoint_auth_methods_supported
102
+ methods = [ "client_secret_basic", "client_secret_post" ]
103
+ methods << "none" if oauth_config["allow_public_clients"]
104
+ methods
105
+ end
106
+
107
+ def scopes_supported
108
+ oauth_config["scopes_supported"] || [ "mcp:tools", "mcp:resources", "mcp:prompts" ]
109
+ end
110
+
111
+ def code_challenge_methods_supported
112
+ methods = []
113
+ if oauth_config["pkce_required"] || oauth_config["pkce_supported"]
114
+ methods << "S256"
115
+ methods << "plain" if oauth_config["allow_plain_pkce"]
116
+ end
117
+ methods
118
+ end
119
+
120
+ def service_documentation
121
+ oauth_config["service_documentation"] || "#{request.base_url}/docs"
122
+ end
123
+
124
+ def resource_documentation
125
+ oauth_config["resource_documentation"] || "#{request.base_url}/docs/api"
126
+ end
127
+ end
128
+ end
129
+ 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 :jsonb
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 :bigint 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
+ # fk_rails_... (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
26
26
  #
27
27
  module ActionMCP
28
28
  class Session
@@ -4,23 +4,34 @@
4
4
  #
5
5
  # Table name: action_mcp_sessions
6
6
  #
7
- # id :string not null, primary key
8
- # client_capabilities(The capabilities of the client) :jsonb
9
- # client_info(The information about the client) :jsonb
10
- # ended_at(The time the session ended) :datetime
11
- # initialized :boolean default(FALSE), not null
12
- # messages_count :integer default(0), not null
13
- # prompt_registry :jsonb
14
- # protocol_version :string
15
- # resource_registry :jsonb
16
- # role(The role of the session) :string default("server"), not null
17
- # server_capabilities(The capabilities of the server) :jsonb
18
- # server_info(The information about the server) :jsonb
19
- # sse_event_counter :integer default(0), not null
20
- # status :string default("pre_initialize"), not null
21
- # tool_registry :jsonb
22
- # created_at :datetime not null
23
- # updated_at :datetime not null
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
+ #
30
+ # Indexes
31
+ #
32
+ # index_action_mcp_sessions_on_authentication_method (authentication_method)
33
+ # index_action_mcp_sessions_on_oauth_access_token (oauth_access_token) UNIQUE
34
+ # index_action_mcp_sessions_on_oauth_token_expires_at (oauth_token_expires_at)
24
35
  #
25
36
  module ActionMCP
26
37
  ##
@@ -323,6 +334,94 @@ module ActionMCP
323
334
  end
324
335
  end
325
336
 
337
+ # OAuth Session Management
338
+ # Required by MCP 2025-03-26 specification for session binding
339
+
340
+ # Store OAuth token and user context in session
341
+ def store_oauth_token(access_token:, refresh_token: nil, expires_at:, user_context: {})
342
+ update!(
343
+ oauth_access_token: access_token,
344
+ oauth_refresh_token: refresh_token,
345
+ oauth_token_expires_at: expires_at,
346
+ oauth_user_context: user_context,
347
+ authentication_method: "oauth"
348
+ )
349
+ end
350
+
351
+ # Retrieve OAuth token information
352
+ def oauth_token_info
353
+ return nil unless oauth_access_token
354
+
355
+ {
356
+ access_token: oauth_access_token,
357
+ refresh_token: oauth_refresh_token,
358
+ expires_at: oauth_token_expires_at,
359
+ user_context: oauth_user_context || {},
360
+ authentication_method: authentication_method
361
+ }
362
+ end
363
+
364
+ # Check if OAuth token is valid and not expired
365
+ def oauth_token_valid?
366
+ return false unless oauth_access_token
367
+ return true unless oauth_token_expires_at
368
+
369
+ oauth_token_expires_at > Time.current
370
+ end
371
+
372
+ # Clear OAuth token data
373
+ def clear_oauth_token!
374
+ update!(
375
+ oauth_access_token: nil,
376
+ oauth_refresh_token: nil,
377
+ oauth_token_expires_at: nil,
378
+ oauth_user_context: nil,
379
+ authentication_method: "none"
380
+ )
381
+ end
382
+
383
+ # Update OAuth token (for refresh flow)
384
+ def update_oauth_token(access_token:, refresh_token: nil, expires_at:)
385
+ update!(
386
+ oauth_access_token: access_token,
387
+ oauth_refresh_token: refresh_token,
388
+ oauth_token_expires_at: expires_at
389
+ )
390
+ end
391
+
392
+ # Get user information from OAuth context
393
+ def oauth_user
394
+ return nil unless oauth_user_context.is_a?(Hash)
395
+
396
+ OpenStruct.new(oauth_user_context)
397
+ end
398
+
399
+ # Check if session is authenticated via OAuth
400
+ def oauth_authenticated?
401
+ authentication_method == "oauth" && oauth_token_valid?
402
+ end
403
+
404
+ # Find session by OAuth access token (class method)
405
+ def self.find_by_oauth_token(access_token)
406
+ find_by(oauth_access_token: access_token)
407
+ end
408
+
409
+ # Find sessions with expired OAuth tokens (class method)
410
+ def self.with_expired_oauth_tokens
411
+ where("oauth_token_expires_at IS NOT NULL AND oauth_token_expires_at < ?", Time.current)
412
+ end
413
+
414
+ # Cleanup expired OAuth tokens (class method)
415
+ def self.cleanup_expired_oauth_tokens
416
+ with_expired_oauth_tokens.update_all(
417
+ oauth_access_token: nil,
418
+ oauth_refresh_token: nil,
419
+ oauth_token_expires_at: nil,
420
+ oauth_user_context: nil,
421
+ authentication_method: "none"
422
+ )
423
+ end
424
+
326
425
  private
327
426
 
328
427
  # if this session is from a server, the writer is the client
data/config/routes.rb CHANGED
@@ -3,6 +3,16 @@
3
3
  ActionMCP::Engine.routes.draw do
4
4
  get "/up", to: "/rails/health#show", as: :action_mcp_health_check
5
5
 
6
+ # OAuth 2.1 metadata endpoints
7
+ get "/.well-known/oauth-authorization-server", to: "oauth/metadata#authorization_server", as: :oauth_authorization_server_metadata
8
+ get "/.well-known/oauth-protected-resource", to: "oauth/metadata#protected_resource", as: :oauth_protected_resource_metadata
9
+
10
+ # OAuth 2.1 endpoints
11
+ get "/oauth/authorize", to: "oauth/endpoints#authorize", as: :oauth_authorize
12
+ post "/oauth/token", to: "oauth/endpoints#token", as: :oauth_token
13
+ post "/oauth/introspect", to: "oauth/endpoints#introspect", as: :oauth_introspect
14
+ post "/oauth/revoke", to: "oauth/endpoints#revoke", as: :oauth_revoke
15
+
6
16
  # MCP 2025-03-26 Spec routes
7
17
  get "/", to: "application#show", as: :mcp_get
8
18
  post "/", to: "application#create", as: :mcp_post