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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/controllers/action_mcp/application_controller.rb +20 -12
- data/app/models/action_mcp/session/message.rb +31 -20
- data/app/models/action_mcp/session/resource.rb +35 -20
- data/app/models/action_mcp/session/sse_event.rb +23 -17
- data/app/models/action_mcp/session/subscription.rb +22 -15
- data/app/models/action_mcp/session.rb +42 -119
- data/config/routes.rb +0 -13
- data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
- data/lib/action_mcp/client/streamable_http_transport.rb +1 -46
- data/lib/action_mcp/client.rb +2 -25
- data/lib/action_mcp/configuration.rb +51 -24
- data/lib/action_mcp/engine.rb +0 -7
- data/lib/action_mcp/filtered_logger.rb +2 -6
- data/lib/action_mcp/gateway_identifier.rb +187 -3
- data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
- data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
- data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
- data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
- data/lib/action_mcp/gateway_identifiers.rb +26 -0
- data/lib/action_mcp/server/base_session.rb +2 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +1 -6
- data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
- data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
- data/lib/generators/action_mcp/install/install_generator.rb +1 -1
- data/lib/generators/action_mcp/install/templates/application_gateway.rb +80 -31
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
- metadata +13 -97
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -265
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -125
- data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -201
- data/app/models/action_mcp/oauth_client.rb +0 -159
- data/app/models/action_mcp/oauth_token.rb +0 -142
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -28
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -44
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -39
- data/lib/action_mcp/client/jwt_client_provider.rb +0 -135
- data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
- data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
- data/lib/action_mcp/jwt_decoder.rb +0 -28
- data/lib/action_mcp/jwt_identifier.rb +0 -28
- data/lib/action_mcp/none_identifier.rb +0 -19
- data/lib/action_mcp/o_auth_identifier.rb +0 -34
- data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
- data/lib/action_mcp/oauth/error.rb +0 -79
- data/lib/action_mcp/oauth/memory_storage.rb +0 -132
- data/lib/action_mcp/oauth/middleware.rb +0 -128
- data/lib/action_mcp/oauth/provider.rb +0 -406
- data/lib/action_mcp/oauth.rb +0 -12
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea96963047c8bb5e9fa9fd88163a14798c8780a531fa1600e3681bd92ca1b4fe
|
4
|
+
data.tar.gz: aae9fc897bd554fba0442735b184d9eac03a381c3b07e052869ce76ce714555d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|
-
#
|
535
|
-
|
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
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_session_messages"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
-
#
|
20
|
-
#
|
21
|
-
#
|
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
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_session_resources"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
-
#
|
20
|
-
#
|
21
|
-
#
|
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
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_sse_events"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_session_subscriptions"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
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
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
-
#
|
15
|
-
#
|
16
|
-
#
|
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
|
-
#
|
3
|
+
# <rails-lens:schema:begin>
|
4
|
+
# table = "action_mcp_sessions"
|
5
|
+
# database_dialect = "SQLite"
|
4
6
|
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
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,
|
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}"
|