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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2875f1eaab23887a9cafbff393229ab35a733822b4b20487cff5b4556cd602c9
4
- data.tar.gz: ac60fbba55e7e06960644c70f790cd0f4f8909d1b6d1137b3f002bdeda29575c
3
+ metadata.gz: ea96963047c8bb5e9fa9fd88163a14798c8780a531fa1600e3681bd92ca1b4fe
4
+ data.tar.gz: aae9fc897bd554fba0442735b184d9eac03a381c3b07e052869ce76ce714555d
5
5
  SHA512:
6
- metadata.gz: 9fe34b128238c04f1dcb8a08af77b17aafcf7c0bf5fd1ce670d0c8bfdd0aefcea6d284d1c223cb3572645e5101aa71021381eb1d0379851114c075102311c156
7
- data.tar.gz: 3817b2aa867d773afad8cb9db19fca5e1dc1d1425d1b02912a62cd15fba6cde28b3e7333114a98ccc8fe551c15d481f77d02c6eb549bf3ae971d95a2a2ff013b
6
+ metadata.gz: 47947a7bba2e39a33b1793cbf8af4a38dcf26e0eebcc19989b143d4279f8f71fc05c646169d4739dd8e52f23c2f34df5a232d4cd414e6872cc636ab4c5a22ae5
7
+ data.tar.gz: 8e2b06ddaf6b1174035dc3e4a6315aaf8d624ac5a096d647f252c40c533f5d20ebf02b3b8b5355d81617de98dfccf63d4342ec540558f8dbc5072043aee11c4a
data/README.md CHANGED
@@ -576,7 +576,7 @@ This will create:
576
576
 
577
577
  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.
578
578
 
579
- 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).
579
+ ActionMCP uses a Gateway pattern with pluggable identifiers for authentication. You can implement custom authentication strategies using session-based auth, API keys, bearer tokens, or integrate with existing authentication systems like Warden, Devise, or external OAuth providers.
580
580
 
581
581
  ### Creating an ApplicationGateway
582
582
 
@@ -28,7 +28,9 @@ module ActionMCP
28
28
  end
29
29
 
30
30
  # Handles GET requests for establishing server-initiated SSE streams (2025-03-26 spec).
31
- # @route GET /
31
+ # <rails-lens:routes:begin>
32
+ # ROUTE: /, name: mcp_get, via: GET
33
+ # <rails-lens:routes:end>
32
34
  def show
33
35
  unless request.accepts.any? { |type| type.to_s == "text/event-stream" }
34
36
  return render_not_acceptable("Client must accept 'text/event-stream' for GET requests.")
@@ -158,7 +160,9 @@ module ActionMCP
158
160
  end
159
161
 
160
162
  # Handles POST requests containing client JSON-RPC messages according to 2025-03-26 spec.
161
- # @route POST /mcp
163
+ # <rails-lens:routes:begin>
164
+ # ROUTE: /, name: mcp_post, via: POST
165
+ # <rails-lens:routes:end>
162
166
  def create
163
167
  unless post_accept_headers_valid?
164
168
  id = extract_jsonrpc_id_from_request
@@ -227,7 +231,9 @@ module ActionMCP
227
231
  end
228
232
 
229
233
  # Handles DELETE requests for session termination (2025-03-26 spec).
230
- # @route DELETE /
234
+ # <rails-lens:routes:begin>
235
+ # ROUTE: /, name: mcp_delete, via: DELETE
236
+ # <rails-lens:routes:end>
231
237
  def destroy
232
238
  session_id_from_header = extract_session_id
233
239
  return render_bad_request("Mcp-Session-Id header is required for DELETE requests.") unless session_id_from_header
@@ -521,21 +527,23 @@ module ActionMCP
521
527
  gateway_class = ActionMCP.configuration.gateway_class
522
528
  return unless gateway_class # Skip if no gateway configured
523
529
 
524
- gateway = gateway_class.new(request)
525
- gateway.call
526
- rescue ActionMCP::UnauthorizedError => e
527
- render_unauthorized(e.message)
530
+ begin
531
+ gateway = gateway_class.new(request)
532
+ gateway.call
533
+ rescue ActionMCP::UnauthorizedError => e
534
+ render_unauthorized(e.message)
535
+ rescue StandardError => e
536
+ Rails.logger.error "Gateway authentication error: #{e.class} - #{e.message}"
537
+ render_unauthorized("Authentication system error")
538
+ end
528
539
  end
529
540
 
530
541
  # Renders an unauthorized response
531
542
  def render_unauthorized(message = "Unauthorized", id = nil)
532
543
  id ||= extract_jsonrpc_id_from_request
533
544
 
534
- # Add WWW-Authenticate header for OAuth discovery as per spec
535
- auth_methods = ActionMCP.configuration.authentication_methods || []
536
- response.headers["WWW-Authenticate"] = 'Bearer realm="MCP API"' if auth_methods.include?("oauth")
537
-
538
- render json: { jsonrpc: "2.0", id: id, error: { code: -32_000, message: message } }, status: :unauthorized
545
+ # Return JSON-RPC error with 200 status as per MCP specification
546
+ render json: { jsonrpc: "2.0", id: id, error: { code: -32_000, message: message } }
539
547
  end
540
548
  end
541
549
  end
@@ -1,29 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # == Schema Information
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_session_messages"
5
+ # database_dialect = "SQLite"
4
6
  #
5
- # Table name: action_mcp_session_messages
7
+ # columns = [
8
+ # { name = "id", type = "integer", primary_key = true, nullable = false },
9
+ # { name = "session_id", type = "string", nullable = false },
10
+ # { name = "direction", type = "string", nullable = false, default = "client" },
11
+ # { name = "message_type", type = "string", nullable = false },
12
+ # { name = "jsonrpc_id", type = "string", nullable = true },
13
+ # { name = "message_json", type = "json", nullable = true },
14
+ # { name = "is_ping", type = "boolean", nullable = false, default = "0" },
15
+ # { name = "request_acknowledged", type = "boolean", nullable = false, default = "0" },
16
+ # { name = "request_cancelled", type = "boolean", nullable = false, default = "0" },
17
+ # { name = "created_at", type = "datetime", nullable = false },
18
+ # { name = "updated_at", type = "datetime", nullable = false }
19
+ # ]
6
20
  #
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
21
+ # indexes = [
22
+ # { name = "index_action_mcp_session_messages_on_session_id", columns = ["session_id"] }
23
+ # ]
18
24
  #
19
- # Indexes
20
- #
21
- # index_action_mcp_session_messages_on_session_id (session_id)
22
- #
23
- # Foreign Keys
24
- #
25
- # session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade ON UPDATE => cascade
25
+ # foreign_keys = [
26
+ # { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade", on_update = "cascade" }
27
+ # ]
26
28
  #
29
+ # == Notes
30
+ # - Column 'message_json' should probably have NOT NULL constraint
31
+ # - String column 'session_id' has no length limit - consider adding one
32
+ # - String column 'direction' has no length limit - consider adding one
33
+ # - String column 'message_type' has no length limit - consider adding one
34
+ # - String column 'jsonrpc_id' has no length limit - consider adding one
35
+ # - Column 'message_type' is commonly used in queries - consider adding an index
36
+ # - Column 'is_ping' uses non-conventional prefix - consider removing 'is_' or 'has_'
37
+ # <rails-lens:schema:end>
27
38
  module ActionMCP
28
39
  class Session
29
40
  #
@@ -1,29 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # == Schema Information
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_session_resources"
5
+ # database_dialect = "SQLite"
4
6
  #
5
- # Table name: action_mcp_session_resources
7
+ # columns = [
8
+ # { name = "id", type = "integer", primary_key = true, nullable = false },
9
+ # { name = "session_id", type = "string", nullable = false },
10
+ # { name = "uri", type = "string", nullable = false },
11
+ # { name = "name", type = "string", nullable = true },
12
+ # { name = "description", type = "text", nullable = true },
13
+ # { name = "mime_type", type = "string", nullable = false },
14
+ # { name = "created_by_tool", type = "boolean", nullable = true, default = "0" },
15
+ # { name = "last_accessed_at", type = "datetime", nullable = true },
16
+ # { name = "metadata", type = "json", nullable = true },
17
+ # { name = "created_at", type = "datetime", nullable = false },
18
+ # { name = "updated_at", type = "datetime", nullable = false }
19
+ # ]
6
20
  #
7
- # id :integer not null, primary key
8
- # created_by_tool :boolean default(FALSE)
9
- # description :text
10
- # last_accessed_at :datetime
11
- # metadata :json
12
- # mime_type :string not null
13
- # name :string
14
- # uri :string not null
15
- # created_at :datetime not null
16
- # updated_at :datetime not null
17
- # session_id :string not null
21
+ # indexes = [
22
+ # { name = "index_action_mcp_session_resources_on_session_id", columns = ["session_id"] }
23
+ # ]
18
24
  #
19
- # Indexes
20
- #
21
- # index_action_mcp_session_resources_on_session_id (session_id)
22
- #
23
- # Foreign Keys
24
- #
25
- # session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade
25
+ # foreign_keys = [
26
+ # { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade" }
27
+ # ]
26
28
  #
29
+ # == Notes
30
+ # - Association 'session' should specify inverse_of
31
+ # - Column 'name' should probably have NOT NULL constraint
32
+ # - Column 'description' should probably have NOT NULL constraint
33
+ # - Column 'created_by_tool' should probably have NOT NULL constraint
34
+ # - Column 'metadata' should probably have NOT NULL constraint
35
+ # - String column 'session_id' has no length limit - consider adding one
36
+ # - String column 'uri' has no length limit - consider adding one
37
+ # - String column 'name' has no length limit - consider adding one
38
+ # - String column 'mime_type' has no length limit - consider adding one
39
+ # - Large text column 'description' is frequently queried - consider separate storage
40
+ # - Column 'mime_type' is commonly used in queries - consider adding an index
41
+ # <rails-lens:schema:end>
27
42
  module ActionMCP
28
43
  class Session
29
44
  #
@@ -1,26 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # == Schema Information
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_sse_events"
5
+ # database_dialect = "SQLite"
4
6
  #
5
- # Table name: action_mcp_sse_events
7
+ # columns = [
8
+ # { name = "id", type = "integer", primary_key = true, nullable = false },
9
+ # { name = "session_id", type = "string", nullable = false },
10
+ # { name = "event_id", type = "integer", nullable = false },
11
+ # { name = "data", type = "text", nullable = false },
12
+ # { name = "created_at", type = "datetime", nullable = false },
13
+ # { name = "updated_at", type = "datetime", nullable = false }
14
+ # ]
6
15
  #
7
- # id :integer not null, primary key
8
- # data :text not null
9
- # created_at :datetime not null
10
- # updated_at :datetime not null
11
- # event_id :integer not null
12
- # session_id :string not null
16
+ # indexes = [
17
+ # { name = "index_action_mcp_sse_events_on_created_at", columns = ["created_at"] },
18
+ # { name = "index_action_mcp_sse_events_on_session_id_and_event_id", columns = ["session_id", "event_id"], unique = true },
19
+ # { name = "index_action_mcp_sse_events_on_session_id", columns = ["session_id"] }
20
+ # ]
13
21
  #
14
- # Indexes
15
- #
16
- # index_action_mcp_sse_events_on_created_at (created_at)
17
- # index_action_mcp_sse_events_on_session_id (session_id)
18
- # index_action_mcp_sse_events_on_session_id_and_event_id (session_id,event_id) UNIQUE
19
- #
20
- # Foreign Keys
21
- #
22
- # session_id (session_id => action_mcp_sessions.id)
22
+ # foreign_keys = [
23
+ # { column = "session_id", references_table = "action_mcp_sessions", references_column = "id" }
24
+ # ]
23
25
  #
26
+ # == Notes
27
+ # - Association 'session' should specify inverse_of
28
+ # - String column 'session_id' has no length limit - consider adding one
29
+ # <rails-lens:schema:end>
24
30
  module ActionMCP
25
31
  class Session
26
32
  # Represents a Server-Sent Event (SSE) in an MCP session
@@ -1,24 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # == Schema Information
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_session_subscriptions"
5
+ # database_dialect = "SQLite"
4
6
  #
5
- # Table name: action_mcp_session_subscriptions
7
+ # columns = [
8
+ # { name = "id", type = "integer", primary_key = true, nullable = false },
9
+ # { name = "session_id", type = "string", nullable = false },
10
+ # { name = "uri", type = "string", nullable = false },
11
+ # { name = "last_notification_at", type = "datetime", nullable = true },
12
+ # { name = "created_at", type = "datetime", nullable = false },
13
+ # { name = "updated_at", type = "datetime", nullable = false }
14
+ # ]
6
15
  #
7
- # id :integer not null, primary key
8
- # last_notification_at :datetime
9
- # uri :string not null
10
- # created_at :datetime not null
11
- # updated_at :datetime not null
12
- # session_id :string not null
16
+ # indexes = [
17
+ # { name = "index_action_mcp_session_subscriptions_on_session_id", columns = ["session_id"] }
18
+ # ]
13
19
  #
14
- # Indexes
15
- #
16
- # index_action_mcp_session_subscriptions_on_session_id (session_id)
17
- #
18
- # Foreign Keys
19
- #
20
- # session_id (session_id => action_mcp_sessions.id) ON DELETE => cascade
20
+ # foreign_keys = [
21
+ # { column = "session_id", references_table = "action_mcp_sessions", references_column = "id", on_delete = "cascade" }
22
+ # ]
21
23
  #
24
+ # == Notes
25
+ # - Consider adding counter cache for 'session'
26
+ # - String column 'session_id' has no length limit - consider adding one
27
+ # - String column 'uri' has no length limit - consider adding one
28
+ # <rails-lens:schema:end>
22
29
  module ActionMCP
23
30
  class Session
24
31
  #
@@ -1,39 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # == Schema Information
3
+ # <rails-lens:schema:begin>
4
+ # table = "action_mcp_sessions"
5
+ # database_dialect = "SQLite"
4
6
  #
5
- # Table name: action_mcp_sessions
6
- #
7
- # id :string not null, primary key
8
- # authentication_method :string default("none")
9
- # client_capabilities :json
10
- # client_info :json
11
- # consents :json not null
12
- # ended_at :datetime
13
- # initialized :boolean default(FALSE), not null
14
- # messages_count :integer default(0), not null
15
- # oauth_access_token :string
16
- # oauth_refresh_token :string
17
- # oauth_token_expires_at :datetime
18
- # oauth_user_context :json
19
- # prompt_registry :json
20
- # protocol_version :string
21
- # resource_registry :json
22
- # role :string default("server"), not null
23
- # server_capabilities :json
24
- # server_info :json
25
- # sse_event_counter :integer default(0), not null
26
- # status :string default("pre_initialize"), not null
27
- # tool_registry :json
28
- # created_at :datetime not null
29
- # updated_at :datetime not null
30
- #
31
- # Indexes
32
- #
33
- # index_action_mcp_sessions_on_authentication_method (authentication_method)
34
- # index_action_mcp_sessions_on_oauth_access_token (oauth_access_token) UNIQUE
35
- # index_action_mcp_sessions_on_oauth_token_expires_at (oauth_token_expires_at)
7
+ # columns = [
8
+ # { name = "id", type = "string", primary_key = true, nullable = false },
9
+ # { name = "role", type = "string", nullable = false, default = "server" },
10
+ # { name = "status", type = "string", nullable = false, default = "pre_initialize" },
11
+ # { name = "ended_at", type = "datetime", nullable = true },
12
+ # { name = "protocol_version", type = "string", nullable = true },
13
+ # { name = "server_capabilities", type = "json", nullable = true },
14
+ # { name = "client_capabilities", type = "json", nullable = true },
15
+ # { name = "server_info", type = "json", nullable = true },
16
+ # { name = "client_info", type = "json", nullable = true },
17
+ # { name = "initialized", type = "boolean", nullable = false, default = "0" },
18
+ # { name = "messages_count", type = "integer", nullable = false, default = "0" },
19
+ # { name = "sse_event_counter", type = "integer", nullable = false, default = "0" },
20
+ # { name = "tool_registry", type = "json", nullable = true, default = "[]" },
21
+ # { name = "prompt_registry", type = "json", nullable = true, default = "[]" },
22
+ # { name = "resource_registry", type = "json", nullable = true, default = "[]" },
23
+ # { name = "created_at", type = "datetime", nullable = false },
24
+ # { name = "updated_at", type = "datetime", nullable = false },
25
+ # { name = "consents", type = "json", nullable = false, default = "{}" }
26
+ # ]
36
27
  #
28
+ # == Notes
29
+ # - Association 'messages' has N+1 query risk. Consider using includes/preload
30
+ # - Association 'subscriptions' has N+1 query risk. Consider using includes/preload
31
+ # - Association 'resources' has N+1 query risk. Consider using includes/preload
32
+ # - Association 'sse_events' has N+1 query risk. Consider using includes/preload
33
+ # - Column 'protocol_version' should probably have NOT NULL constraint
34
+ # - Column 'server_capabilities' should probably have NOT NULL constraint
35
+ # - Column 'client_capabilities' should probably have NOT NULL constraint
36
+ # - Column 'server_info' should probably have NOT NULL constraint
37
+ # - Column 'client_info' should probably have NOT NULL constraint
38
+ # - Column 'tool_registry' should probably have NOT NULL constraint
39
+ # - Column 'prompt_registry' should probably have NOT NULL constraint
40
+ # - Column 'resource_registry' should probably have NOT NULL constraint
41
+ # - String column 'id' has no length limit - consider adding one
42
+ # - String column 'role' has no length limit - consider adding one
43
+ # - String column 'status' has no length limit - consider adding one
44
+ # - String column 'protocol_version' has no length limit - consider adding one
45
+ # - Column 'status' is commonly used in queries - consider adding an index
46
+ # <rails-lens:schema:end>
37
47
  module ActionMCP
38
48
  ##
39
49
  # Represents an MCP session, which is a connection between a client and a server.
@@ -362,93 +372,6 @@ module ActionMCP
362
372
  resource_registry == [ "*" ]
363
373
  end
364
374
 
365
- # OAuth Session Management
366
- # Required by MCP 2025-03-26 specification for session binding
367
-
368
- # Store OAuth token and user context in session
369
- def store_oauth_token(access_token:, expires_at:, refresh_token: nil, user_context: {})
370
- update!(
371
- oauth_access_token: access_token,
372
- oauth_refresh_token: refresh_token,
373
- oauth_token_expires_at: expires_at,
374
- oauth_user_context: user_context,
375
- authentication_method: "oauth"
376
- )
377
- end
378
-
379
- # Retrieve OAuth token information
380
- def oauth_token_info
381
- return nil unless oauth_access_token
382
-
383
- {
384
- access_token: oauth_access_token,
385
- refresh_token: oauth_refresh_token,
386
- expires_at: oauth_token_expires_at,
387
- user_context: oauth_user_context || {},
388
- authentication_method: authentication_method
389
- }
390
- end
391
-
392
- # Check if OAuth token is valid and not expired
393
- def oauth_token_valid?
394
- return false unless oauth_access_token
395
- return true unless oauth_token_expires_at
396
-
397
- oauth_token_expires_at > Time.current
398
- end
399
-
400
- # Clear OAuth token data
401
- def clear_oauth_token!
402
- update!(
403
- oauth_access_token: nil,
404
- oauth_refresh_token: nil,
405
- oauth_token_expires_at: nil,
406
- oauth_user_context: nil,
407
- authentication_method: "none"
408
- )
409
- end
410
-
411
- # Update OAuth token (for refresh flow)
412
- def update_oauth_token(access_token:, expires_at:, refresh_token: nil)
413
- update!(
414
- oauth_access_token: access_token,
415
- oauth_refresh_token: refresh_token,
416
- oauth_token_expires_at: expires_at
417
- )
418
- end
419
-
420
- # Get user information from OAuth context
421
- def oauth_user
422
- return nil unless oauth_user_context.is_a?(Hash)
423
-
424
- OpenStruct.new(oauth_user_context)
425
- end
426
-
427
- # Check if session is authenticated via OAuth
428
- def oauth_authenticated?
429
- authentication_method == "oauth" && oauth_token_valid?
430
- end
431
-
432
- # Find session by OAuth access token (class method)
433
- def self.find_by_oauth_token(access_token)
434
- find_by(oauth_access_token: access_token)
435
- end
436
-
437
- # Find sessions with expired OAuth tokens (class method)
438
- def self.with_expired_oauth_tokens
439
- where("oauth_token_expires_at IS NOT NULL AND oauth_token_expires_at < ?", Time.current)
440
- end
441
-
442
- # Cleanup expired OAuth tokens (class method)
443
- def self.cleanup_expired_oauth_tokens
444
- with_expired_oauth_tokens.update_all(
445
- oauth_access_token: nil,
446
- oauth_refresh_token: nil,
447
- oauth_token_expires_at: nil,
448
- oauth_user_context: nil,
449
- authentication_method: "none"
450
- )
451
- end
452
375
 
453
376
  # Consent management methods as per MCP specification
454
377
  # These methods manage user consents for tools and resources
data/config/routes.rb CHANGED
@@ -3,19 +3,6 @@
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",
8
- as: :oauth_authorization_server_metadata
9
- get "/.well-known/oauth-protected-resource", to: "oauth/metadata#protected_resource",
10
- as: :oauth_protected_resource_metadata
11
-
12
- # OAuth 2.1 endpoints
13
- get "/oauth/authorize", to: "oauth/endpoints#authorize", as: :oauth_authorize
14
- post "/oauth/token", to: "oauth/endpoints#token", as: :oauth_token
15
- post "/oauth/introspect", to: "oauth/endpoints#introspect", as: :oauth_introspect
16
- post "/oauth/revoke", to: "oauth/endpoints#revoke", as: :oauth_revoke
17
- post "/oauth/register", to: "oauth/registration#create", as: :oauth_register
18
-
19
6
  # MCP 2025-03-26 Spec routes
20
7
  get "/", to: "application#show", as: :mcp_get
21
8
  post "/", to: "application#create", as: :mcp_post
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveOauthSupport < ActiveRecord::Migration[8.0]
4
+ def change
5
+ # Remove OAuth tables
6
+ drop_table :action_mcp_oauth_clients, if_exists: true do |t|
7
+ t.string :client_id, null: false
8
+ t.string :client_secret
9
+ t.string :client_name
10
+ t.json :redirect_uris, default: []
11
+ t.json :grant_types, default: [ "authorization_code" ]
12
+ t.json :response_types, default: [ "code" ]
13
+ t.string :token_endpoint_auth_method, default: "client_secret_basic"
14
+ t.text :scope
15
+ t.boolean :active, default: true
16
+ t.integer :client_id_issued_at
17
+ t.integer :client_secret_expires_at
18
+ t.string :registration_access_token
19
+ t.json :metadata, default: {}
20
+ t.datetime :created_at, null: false
21
+ t.datetime :updated_at, null: false
22
+ end
23
+
24
+ drop_table :action_mcp_oauth_tokens, if_exists: true do |t|
25
+ t.string :token, null: false
26
+ t.string :token_type, null: false
27
+ t.string :client_id, null: false
28
+ t.string :user_id
29
+ t.text :scope
30
+ t.datetime :expires_at
31
+ t.boolean :revoked, default: false
32
+ t.string :redirect_uri
33
+ t.string :code_challenge
34
+ t.string :code_challenge_method
35
+ t.string :access_token
36
+ t.json :metadata, default: {}
37
+ t.datetime :created_at, null: false
38
+ t.datetime :updated_at, null: false
39
+ end
40
+
41
+ # Remove OAuth columns from sessions table
42
+ if table_exists?(:action_mcp_sessions)
43
+ remove_column :action_mcp_sessions, :oauth_access_token, :string if column_exists?(:action_mcp_sessions, :oauth_access_token)
44
+ remove_column :action_mcp_sessions, :oauth_refresh_token, :string if column_exists?(:action_mcp_sessions, :oauth_refresh_token)
45
+ remove_column :action_mcp_sessions, :oauth_token_expires_at, :datetime if column_exists?(:action_mcp_sessions, :oauth_token_expires_at)
46
+ remove_column :action_mcp_sessions, :oauth_user_context, :json if column_exists?(:action_mcp_sessions, :oauth_user_context)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def table_exists?(table_name)
53
+ ActiveRecord::Base.connection.table_exists?(table_name)
54
+ end
55
+
56
+ def column_exists?(table_name, column_name)
57
+ ActiveRecord::Base.connection.column_exists?(table_name, column_name)
58
+ end
59
+ end
@@ -15,12 +15,9 @@ module ActionMCP
15
15
 
16
16
  attr_reader :session_id, :last_event_id, :protocol_version
17
17
 
18
- def initialize(url, session_store:, session_id: nil, oauth_provider: nil, jwt_provider: nil,
19
- protocol_version: nil, **options)
18
+ def initialize(url, session_store:, session_id: nil, protocol_version: nil, **options)
20
19
  super(url, session_store: session_store, **options)
21
20
  @session_id = session_id
22
- @oauth_provider = oauth_provider
23
- @jwt_provider = jwt_provider
24
21
  @protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
25
22
  @negotiated_protocol_version = nil
26
23
  @last_event_id = nil
@@ -105,8 +102,6 @@ module ActionMCP
105
102
  # Add MCP-Protocol-Version header for GET requests when we have a negotiated version
106
103
  headers["MCP-Protocol-Version"] = @negotiated_protocol_version if @negotiated_protocol_version
107
104
 
108
- headers.merge!(oauth_headers)
109
- headers.merge!(jwt_headers)
110
105
  log_debug("Final GET headers: #{headers}")
111
106
  headers
112
107
  end
@@ -122,8 +117,6 @@ module ActionMCP
122
117
  # Only include when we have a negotiated version from previous handshake
123
118
  headers["MCP-Protocol-Version"] = @negotiated_protocol_version if @negotiated_protocol_version
124
119
 
125
- headers.merge!(oauth_headers)
126
- headers.merge!(jwt_headers)
127
120
  log_debug("Final POST headers: #{headers}")
128
121
  headers
129
122
  end
@@ -208,7 +201,6 @@ module ActionMCP
208
201
  # Accepted - message received, no immediate response
209
202
  log_debug("Message accepted (202)")
210
203
  when 401
211
- handle_authentication_error(response)
212
204
  raise AuthenticationError, "Authentication required"
213
205
  when 405
214
206
  # Method not allowed - server doesn't support this operation
@@ -305,43 +297,6 @@ module ActionMCP
305
297
  log_debug("Saved session state")
306
298
  end
307
299
 
308
- def oauth_headers
309
- return {} unless @oauth_provider&.authenticated?
310
-
311
- headers = @oauth_provider.authorization_headers
312
- log_debug("OAuth headers: #{headers}") unless headers.empty?
313
- headers
314
- rescue StandardError => e
315
- log_error("Failed to get OAuth headers: #{e.message}")
316
- {}
317
- end
318
-
319
- def jwt_headers
320
- return {} unless @jwt_provider&.authenticated?
321
-
322
- headers = @jwt_provider.authorization_headers
323
- log_debug("JWT headers: #{headers}") unless headers.empty?
324
- headers
325
- rescue StandardError => e
326
- log_error("Failed to get JWT headers: #{e.message}")
327
- {}
328
- end
329
-
330
- def handle_authentication_error(response)
331
- # Check for OAuth challenge in WWW-Authenticate header
332
- www_auth = response.headers["www-authenticate"]
333
- return unless www_auth&.include?("Bearer")
334
-
335
- if @oauth_provider
336
- log_debug("Received OAuth challenge, clearing OAuth tokens")
337
- @oauth_provider.clear_tokens!
338
- end
339
-
340
- return unless @jwt_provider
341
-
342
- log_debug("Received Bearer challenge, clearing JWT tokens")
343
- @jwt_provider.clear_tokens!
344
- end
345
300
 
346
301
  def user_agent
347
302
  "ActionMCP-StreamableHTTP/#{ActionMCP.gem_version}"