actionmcp 0.71.1 → 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 +187 -16
- data/app/controllers/action_mcp/application_controller.rb +64 -49
- 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 +71 -113
- data/config/routes.rb +0 -11
- data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
- data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
- data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
- data/lib/action_mcp/base_response.rb +1 -1
- data/lib/action_mcp/client/base.rb +9 -11
- data/lib/action_mcp/client/elicitation.rb +4 -4
- data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
- data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
- data/lib/action_mcp/client.rb +6 -26
- data/lib/action_mcp/configuration.rb +65 -63
- data/lib/action_mcp/engine.rb +1 -10
- data/lib/action_mcp/filtered_logger.rb +3 -7
- data/lib/action_mcp/gateway.rb +7 -11
- 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/json_rpc_handler_base.rb +0 -2
- data/lib/action_mcp/prompt.rb +2 -0
- data/lib/action_mcp/renderable.rb +1 -1
- data/lib/action_mcp/resource_template.rb +6 -2
- data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
- data/lib/action_mcp/server/base_session_store.rb +86 -0
- data/lib/action_mcp/server/capabilities.rb +2 -1
- data/lib/action_mcp/server/elicitation.rb +3 -9
- data/lib/action_mcp/server/error_handling.rb +14 -1
- data/lib/action_mcp/server/handlers/router.rb +31 -0
- data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
- data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
- data/lib/action_mcp/server/prompts.rb +4 -4
- data/lib/action_mcp/server/resources.rb +23 -4
- data/lib/action_mcp/server/session_store_factory.rb +1 -1
- data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
- data/lib/action_mcp/server/tools.rb +62 -43
- data/lib/action_mcp/server/transport_handler.rb +2 -4
- data/lib/action_mcp/server/volatile_session_store.rb +1 -93
- data/lib/action_mcp/tagged_stream_logging.rb +2 -2
- data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
- data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
- data/lib/action_mcp/tool.rb +48 -37
- data/lib/action_mcp/types/float_array_type.rb +5 -3
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +2 -7
- 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 +86 -36
- data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
- data/lib/tasks/action_mcp_tasks.rake +7 -5
- metadata +18 -100
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
- data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
- data/app/models/action_mcp/oauth_client.rb +0 -157
- data/app/models/action_mcp/oauth_token.rb +0 -141
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
- data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
- 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 -26
- 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 -134
- data/lib/action_mcp/oauth/middleware.rb +0 -133
- data/lib/action_mcp/oauth/provider.rb +0 -426
- data/lib/action_mcp/oauth.rb +0 -12
- data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
- data/lib/action_mcp/server/notifications.rb +0 -58
@@ -1,29 +1,213 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActionMCP
|
4
|
+
# Base class for Gateway authentication identifiers.
|
5
|
+
#
|
6
|
+
# Gateway identifiers provide a clean interface for authentication by reading
|
7
|
+
# from request.env keys set by upstream middleware (like Warden, Devise, or custom auth).
|
8
|
+
#
|
9
|
+
# @example Warden/Devise Integration
|
10
|
+
# class WardenIdentifier < ActionMCP::GatewayIdentifier
|
11
|
+
# identifier :user
|
12
|
+
# authenticates :warden
|
13
|
+
#
|
14
|
+
# def resolve
|
15
|
+
# # Warden sets 'warden.user' in request.env after authentication
|
16
|
+
# user = user_from_middleware
|
17
|
+
# user || raise(Unauthorized, "No authenticated user found")
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# @example API Key Authentication
|
22
|
+
# class ApiKeyIdentifier < ActionMCP::GatewayIdentifier
|
23
|
+
# identifier :user
|
24
|
+
# authenticates :api_key
|
25
|
+
#
|
26
|
+
# def resolve
|
27
|
+
# # Check for API key in header or query param
|
28
|
+
# api_key = @request.env['HTTP_X_API_KEY'] ||
|
29
|
+
# @request.params['api_key']
|
30
|
+
# return raise(Unauthorized, "Missing API key") unless api_key
|
31
|
+
#
|
32
|
+
# user = User.find_by(api_key: api_key)
|
33
|
+
# user || raise(Unauthorized, "Invalid API key")
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# @example Session-based Authentication
|
38
|
+
# class SessionIdentifier < ActionMCP::GatewayIdentifier
|
39
|
+
# identifier :user
|
40
|
+
# authenticates :session
|
41
|
+
#
|
42
|
+
# def resolve
|
43
|
+
# user_id = session&.[]('user_id')
|
44
|
+
# return raise(Unauthorized, "No user session") unless user_id
|
45
|
+
#
|
46
|
+
# user = User.find_by(id: user_id)
|
47
|
+
# user || raise(Unauthorized, "Invalid session")
|
48
|
+
# end
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# @example Multi-tenant with Organization
|
52
|
+
# class TenantIdentifier < ActionMCP::GatewayIdentifier
|
53
|
+
# identifier :user
|
54
|
+
# authenticates :tenant
|
55
|
+
#
|
56
|
+
# def resolve
|
57
|
+
# # Get user from middleware
|
58
|
+
# user = user_from_middleware
|
59
|
+
# return raise(Unauthorized, "No user found") unless user
|
60
|
+
#
|
61
|
+
# # Check tenant header
|
62
|
+
# tenant_id = @request.env['HTTP_X_TENANT_ID']
|
63
|
+
# return raise(Unauthorized, "Missing tenant") unless tenant_id
|
64
|
+
#
|
65
|
+
# # Verify user has access to tenant
|
66
|
+
# unless user.tenants.exists?(id: tenant_id)
|
67
|
+
# raise Unauthorized, "Access denied for tenant"
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# # Set current tenant for the request
|
71
|
+
# Current.tenant = Tenant.find(tenant_id)
|
72
|
+
# user
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# @example Development/Testing Bypass
|
77
|
+
# class DevIdentifier < ActionMCP::GatewayIdentifier
|
78
|
+
# identifier :user
|
79
|
+
# authenticates :dev
|
80
|
+
#
|
81
|
+
# def resolve
|
82
|
+
# return raise(Unauthorized, "Dev auth disabled in production") unless development_env?
|
83
|
+
#
|
84
|
+
# # Create or find dev user
|
85
|
+
# User.find_or_create_by!(email: "dev@localhost") do |user|
|
86
|
+
# user.name = "Development User"
|
87
|
+
# end
|
88
|
+
# end
|
89
|
+
# end
|
4
90
|
class GatewayIdentifier
|
5
91
|
class Unauthorized < StandardError; end
|
6
92
|
|
7
93
|
class << self
|
8
|
-
# e.g
|
9
|
-
attr_reader :identifier_name
|
94
|
+
# @return [Symbol] The name of the identity this identifier provides (e.g., :user, :admin)
|
95
|
+
attr_reader :identifier_name
|
10
96
|
|
97
|
+
# @return [String] The authentication method this identifier handles (e.g., "session", "api_key")
|
98
|
+
attr_reader :auth_method
|
99
|
+
|
100
|
+
# Declares what identity attribute this identifier provides.
|
101
|
+
# This becomes the accessor name on the Gateway instance.
|
102
|
+
#
|
103
|
+
# @param name [Symbol, String] The identity name (e.g., :user, :admin)
|
104
|
+
# @example
|
105
|
+
# identifier :user
|
106
|
+
# # Gateway instance will have gateway.user accessor
|
11
107
|
def identifier(name)
|
12
108
|
@identifier_name = name.to_sym
|
13
109
|
end
|
14
110
|
|
111
|
+
# Declares what authentication method this identifier handles.
|
112
|
+
# This should match values in your authentication_methods configuration.
|
113
|
+
#
|
114
|
+
# @param method [Symbol, String] The auth method name (e.g., :session, :api_key)
|
115
|
+
# @example
|
116
|
+
# authenticates :api_key
|
117
|
+
# # Matches authentication_methods: ["api_key"] in config
|
15
118
|
def authenticates(method)
|
16
119
|
@auth_method = method.to_s
|
17
120
|
end
|
18
121
|
end
|
19
122
|
|
123
|
+
# @param request [ActionDispatch::Request] The request object containing env hash
|
20
124
|
def initialize(request)
|
21
125
|
@request = request
|
22
126
|
end
|
23
127
|
|
24
|
-
#
|
128
|
+
# Resolves the identity for this authentication method.
|
129
|
+
# Must return a truthy identity object, or raise Unauthorized.
|
130
|
+
#
|
131
|
+
# Common request.env keys set by popular auth middleware:
|
132
|
+
# - 'warden.user' - Warden (used by Devise)
|
133
|
+
# - 'devise.user' - Devise direct
|
134
|
+
# - 'rack.session' - Rack session hash
|
135
|
+
# - 'HTTP_AUTHORIZATION' - Authorization header
|
136
|
+
# - Custom keys set by your middleware
|
137
|
+
#
|
138
|
+
# @return [Object] The authenticated identity (User, Admin, etc.)
|
139
|
+
# @raise [Unauthorized] When authentication fails
|
140
|
+
# @abstract Subclasses must implement this method
|
25
141
|
def resolve
|
26
142
|
raise NotImplementedError, "#{self.class}#resolve must be implemented"
|
27
143
|
end
|
144
|
+
|
145
|
+
protected
|
146
|
+
|
147
|
+
# Helper method to extract Bearer token from Authorization header
|
148
|
+
# @return [String, nil] The token without "Bearer " prefix
|
149
|
+
def extract_bearer_token
|
150
|
+
header = @request.env["HTTP_AUTHORIZATION"] || ""
|
151
|
+
header[/\ABearer (.+)\z/, 1]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Helper method to extract Basic auth credentials from Authorization header
|
155
|
+
# @return [Array<String>, nil] [username, password] or nil if not Basic auth
|
156
|
+
def extract_basic_auth
|
157
|
+
header = @request.env["HTTP_AUTHORIZATION"] || ""
|
158
|
+
return nil unless header.start_with?("Basic ")
|
159
|
+
|
160
|
+
encoded = header[6..-1] # Remove "Basic " prefix
|
161
|
+
decoded = Base64.decode64(encoded)
|
162
|
+
decoded.split(":", 2) # Split on first colon only
|
163
|
+
rescue StandardError
|
164
|
+
nil
|
165
|
+
end
|
166
|
+
|
167
|
+
# Helper method to get user from common middleware env keys
|
168
|
+
# @return [Object, nil] User from Warden/Devise or nil
|
169
|
+
def user_from_middleware
|
170
|
+
@request.env["warden.user"] || @request.env["devise.user"]
|
171
|
+
end
|
172
|
+
|
173
|
+
# Helper method to get session hash
|
174
|
+
# @return [Hash, nil] Rack session hash or nil
|
175
|
+
def session
|
176
|
+
@request.env["rack.session"]
|
177
|
+
end
|
178
|
+
|
179
|
+
# Helper method to read custom env key with optional fallbacks
|
180
|
+
# @param keys [String, Array<String>] Primary key or array of keys to try
|
181
|
+
# @return [Object, nil] Value from request.env or nil
|
182
|
+
def env_value(*keys)
|
183
|
+
keys.flatten.each do |key|
|
184
|
+
value = @request.env[key]
|
185
|
+
return value if value
|
186
|
+
end
|
187
|
+
nil
|
188
|
+
end
|
189
|
+
|
190
|
+
# Helper method to extract API key from various common locations
|
191
|
+
# @param header_name [String] Custom header name (default: 'HTTP_X_API_KEY')
|
192
|
+
# @param param_name [String] Query/form parameter name (default: 'api_key')
|
193
|
+
# @return [String, nil] The API key or nil
|
194
|
+
def extract_api_key(header_name: "HTTP_X_API_KEY", param_name: "api_key")
|
195
|
+
# Try custom header first
|
196
|
+
api_key = @request.env[header_name]
|
197
|
+
return api_key if api_key
|
198
|
+
|
199
|
+
# Try Authorization header with "Bearer" prefix
|
200
|
+
bearer_token = extract_bearer_token
|
201
|
+
return bearer_token if bearer_token
|
202
|
+
|
203
|
+
# Try request parameters (query string or form data)
|
204
|
+
@request.params[param_name] if @request.respond_to?(:params)
|
205
|
+
end
|
206
|
+
|
207
|
+
# Helper method to check if user is in a development environment
|
208
|
+
# @return [Boolean] true if Rails.env.development?
|
209
|
+
def development_env?
|
210
|
+
Rails.env.development?
|
211
|
+
end
|
28
212
|
end
|
29
213
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module GatewayIdentifiers
|
5
|
+
# Example Gateway identifier for API key-based authentication.
|
6
|
+
#
|
7
|
+
# This identifier looks for API keys in various locations:
|
8
|
+
# - Authorization header (Bearer token)
|
9
|
+
# - Custom X-API-Key header
|
10
|
+
# - Query parameters
|
11
|
+
#
|
12
|
+
# @example Usage in ApplicationGateway
|
13
|
+
# class ApplicationGateway < ActionMCP::Gateway
|
14
|
+
# identified_by ActionMCP::GatewayIdentifiers::ApiKeyIdentifier
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Configuration
|
18
|
+
# # config/mcp.yml
|
19
|
+
# authentication_methods: ["api_key"]
|
20
|
+
#
|
21
|
+
# @example API Key usage
|
22
|
+
# # Via Authorization header:
|
23
|
+
# # Authorization: Bearer your-api-key-here
|
24
|
+
# #
|
25
|
+
# # Via custom header:
|
26
|
+
# # X-API-Key: your-api-key-here
|
27
|
+
# #
|
28
|
+
# # Via query parameter:
|
29
|
+
# # ?api_key=your-api-key-here
|
30
|
+
class ApiKeyIdentifier < ActionMCP::GatewayIdentifier
|
31
|
+
identifier :user
|
32
|
+
authenticates :api_key
|
33
|
+
|
34
|
+
def resolve
|
35
|
+
api_key = extract_api_key
|
36
|
+
raise Unauthorized, "Missing API key" unless api_key
|
37
|
+
|
38
|
+
# Look up user by API key
|
39
|
+
# Assumes you have an api_key or api_token field on your User model
|
40
|
+
user = User.find_by(api_key: api_key) || User.find_by(api_token: api_key)
|
41
|
+
raise Unauthorized, "Invalid API key" unless user
|
42
|
+
|
43
|
+
# Optional: Check if API key is still valid (not expired, user active, etc.)
|
44
|
+
if user.respond_to?(:api_key_expired?) && user.api_key_expired?
|
45
|
+
raise Unauthorized, "API key expired"
|
46
|
+
end
|
47
|
+
|
48
|
+
if user.respond_to?(:active?) && !user.active?
|
49
|
+
raise Unauthorized, "User account inactive"
|
50
|
+
end
|
51
|
+
|
52
|
+
user
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module GatewayIdentifiers
|
5
|
+
# Example Gateway identifier for direct Devise integration.
|
6
|
+
#
|
7
|
+
# This identifier looks for the user directly in request.env['devise.user'] which
|
8
|
+
# may be set by custom Devise middleware or helpers.
|
9
|
+
#
|
10
|
+
# @example Usage in ApplicationGateway
|
11
|
+
# class ApplicationGateway < ActionMCP::Gateway
|
12
|
+
# identified_by ActionMCP::GatewayIdentifiers::DeviseIdentifier
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @example Configuration
|
16
|
+
# # config/mcp.yml
|
17
|
+
# authentication_methods: ["devise"]
|
18
|
+
#
|
19
|
+
# @example Custom Devise middleware setup
|
20
|
+
# # In your application, you might set this in a before_action:
|
21
|
+
# # request.env['devise.user'] = current_user if user_signed_in?
|
22
|
+
class DeviseIdentifier < ActionMCP::GatewayIdentifier
|
23
|
+
identifier :user
|
24
|
+
authenticates :devise
|
25
|
+
|
26
|
+
def resolve
|
27
|
+
user = @request.env["devise.user"]
|
28
|
+
raise Unauthorized, "Not authenticated" unless user
|
29
|
+
|
30
|
+
user
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module GatewayIdentifiers
|
5
|
+
# Example Gateway identifier for custom request environment-based authentication.
|
6
|
+
#
|
7
|
+
# This identifier looks for user information in custom request headers or environment
|
8
|
+
# variables. Useful for authentication set up by upstream proxies, API gateways,
|
9
|
+
# or custom middleware.
|
10
|
+
#
|
11
|
+
# @example Usage in ApplicationGateway
|
12
|
+
# class ApplicationGateway < ActionMCP::Gateway
|
13
|
+
# identified_by ActionMCP::GatewayIdentifiers::RequestEnvIdentifier
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# @example Configuration
|
17
|
+
# # config/mcp.yml
|
18
|
+
# authentication_methods: ["request_env"]
|
19
|
+
#
|
20
|
+
# @example Nginx/Proxy setup
|
21
|
+
# # Your proxy/gateway might set headers like:
|
22
|
+
# # X-User-ID: 123
|
23
|
+
# # X-User-Email: user@example.com
|
24
|
+
# # X-User-Roles: admin,user
|
25
|
+
class RequestEnvIdentifier < ActionMCP::GatewayIdentifier
|
26
|
+
identifier :user
|
27
|
+
authenticates :request_env
|
28
|
+
|
29
|
+
def resolve
|
30
|
+
user_id = @request.env["HTTP_X_USER_ID"]
|
31
|
+
raise Unauthorized, "User ID header missing" unless user_id
|
32
|
+
|
33
|
+
# You might also want to get additional user info from headers
|
34
|
+
email = @request.env["HTTP_X_USER_EMAIL"]
|
35
|
+
roles = @request.env["HTTP_X_USER_ROLES"]&.split(",") || []
|
36
|
+
|
37
|
+
# Option 1: Find user in database
|
38
|
+
begin
|
39
|
+
user = User.find(user_id)
|
40
|
+
# Optional: verify email matches if provided
|
41
|
+
if email && user.email != email
|
42
|
+
raise Unauthorized, "User email mismatch"
|
43
|
+
end
|
44
|
+
user
|
45
|
+
rescue ActiveRecord::RecordNotFound
|
46
|
+
raise Unauthorized, "Invalid user"
|
47
|
+
end
|
48
|
+
|
49
|
+
# Option 2: Create a simple user object from headers (if you don't want DB lookup)
|
50
|
+
# OpenStruct.new(
|
51
|
+
# id: user_id,
|
52
|
+
# email: email,
|
53
|
+
# roles: roles
|
54
|
+
# )
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module GatewayIdentifiers
|
5
|
+
# Example Gateway identifier for Warden-based authentication.
|
6
|
+
#
|
7
|
+
# This identifier works with Warden middleware which is commonly used by Devise.
|
8
|
+
# Warden sets the authenticated user in request.env['warden.user'] after successful authentication.
|
9
|
+
#
|
10
|
+
# @example Usage in ApplicationGateway
|
11
|
+
# class ApplicationGateway < ActionMCP::Gateway
|
12
|
+
# identified_by ActionMCP::GatewayIdentifiers::WardenIdentifier
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# @example Configuration
|
16
|
+
# # config/mcp.yml
|
17
|
+
# authentication_methods: ["warden"]
|
18
|
+
#
|
19
|
+
# @example With Devise
|
20
|
+
# # In your controller or middleware, ensure Warden is configured:
|
21
|
+
# # devise_for :users
|
22
|
+
# # authenticate_user! # This sets up Warden env
|
23
|
+
class WardenIdentifier < ActionMCP::GatewayIdentifier
|
24
|
+
identifier :user
|
25
|
+
authenticates :warden
|
26
|
+
|
27
|
+
def resolve
|
28
|
+
warden = @request.env["warden"]
|
29
|
+
raise Unauthorized, "Warden not available" unless warden
|
30
|
+
|
31
|
+
user = warden.user
|
32
|
+
raise Unauthorized, "Not authenticated" unless user
|
33
|
+
|
34
|
+
user
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
# Example Gateway identifiers for common authentication patterns.
|
5
|
+
#
|
6
|
+
# These identifiers provide ready-to-use implementations for popular
|
7
|
+
# Rails authentication patterns. You can use them directly or as
|
8
|
+
# templates for your own custom identifiers.
|
9
|
+
#
|
10
|
+
# @example Using in ApplicationGateway
|
11
|
+
# class ApplicationGateway < ActionMCP::Gateway
|
12
|
+
# # Use multiple identifiers (tried in order)
|
13
|
+
# identified_by ActionMCP::GatewayIdentifiers::WardenIdentifier,
|
14
|
+
# ActionMCP::GatewayIdentifiers::ApiKeyIdentifier
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @example Configuration
|
18
|
+
# # config/mcp.yml
|
19
|
+
# authentication_methods: ["warden", "api_key"]
|
20
|
+
module GatewayIdentifiers
|
21
|
+
autoload :WardenIdentifier, "action_mcp/gateway_identifiers/warden_identifier"
|
22
|
+
autoload :DeviseIdentifier, "action_mcp/gateway_identifiers/devise_identifier"
|
23
|
+
autoload :RequestEnvIdentifier, "action_mcp/gateway_identifiers/request_env_identifier"
|
24
|
+
autoload :ApiKeyIdentifier, "action_mcp/gateway_identifiers/api_key_identifier"
|
25
|
+
end
|
26
|
+
end
|
data/lib/action_mcp/prompt.rb
CHANGED
@@ -28,6 +28,7 @@ module ActionMCP
|
|
28
28
|
# @return [String] The default prompt name.
|
29
29
|
def self.default_prompt_name
|
30
30
|
return "" if name.nil?
|
31
|
+
|
31
32
|
name.demodulize.underscore.sub(/_prompt$/, "")
|
32
33
|
end
|
33
34
|
|
@@ -55,6 +56,7 @@ module ActionMCP
|
|
55
56
|
def meta(data = nil)
|
56
57
|
if data
|
57
58
|
raise ArgumentError, "_meta must be a hash" unless data.is_a?(Hash)
|
59
|
+
|
58
60
|
self._meta = _meta.merge(data)
|
59
61
|
else
|
60
62
|
_meta
|
@@ -49,7 +49,7 @@ module ActionMCP
|
|
49
49
|
#
|
50
50
|
def render_resource_link(uri:, name: nil, description: nil, mime_type: nil, annotations: nil)
|
51
51
|
Content::ResourceLink.new(uri, name: name, description: description,
|
52
|
-
|
52
|
+
mime_type: mime_type, annotations: annotations)
|
53
53
|
end
|
54
54
|
end
|
55
55
|
end
|
@@ -27,7 +27,9 @@ module ActionMCP
|
|
27
27
|
def abstract!
|
28
28
|
@abstract = true
|
29
29
|
# Unregister from the appropriate registry if already registered
|
30
|
-
|
30
|
+
return unless ActionMCP::ResourceTemplatesRegistry.items.values.include?(self)
|
31
|
+
|
32
|
+
ActionMCP::ResourceTemplatesRegistry.unregister(self)
|
31
33
|
end
|
32
34
|
|
33
35
|
def inherited(subclass)
|
@@ -92,6 +94,7 @@ module ActionMCP
|
|
92
94
|
def meta(data = nil)
|
93
95
|
if data
|
94
96
|
raise ArgumentError, "_meta must be a hash" unless data.is_a?(Hash)
|
97
|
+
|
95
98
|
@_meta ||= {}
|
96
99
|
@_meta = @_meta.merge(data)
|
97
100
|
else
|
@@ -110,13 +113,14 @@ module ActionMCP
|
|
110
113
|
}.compact
|
111
114
|
|
112
115
|
# Add _meta if present
|
113
|
-
result[:_meta] = @_meta if @_meta
|
116
|
+
result[:_meta] = @_meta if @_meta&.any?
|
114
117
|
|
115
118
|
result
|
116
119
|
end
|
117
120
|
|
118
121
|
def capability_name
|
119
122
|
return "" if name.nil?
|
123
|
+
|
120
124
|
@capability_name ||= name.demodulize.underscore.sub(/_template$/, "")
|
121
125
|
end
|
122
126
|
|
@@ -2,14 +2,14 @@
|
|
2
2
|
|
3
3
|
module ActionMCP
|
4
4
|
module Server
|
5
|
-
#
|
6
|
-
class
|
5
|
+
# Base session object that mimics ActiveRecord Session with common functionality
|
6
|
+
class BaseSession
|
7
7
|
attr_accessor :id, :status, :initialized, :role, :messages_count,
|
8
8
|
:sse_event_counter, :protocol_version, :client_info,
|
9
9
|
:client_capabilities, :server_info, :server_capabilities,
|
10
10
|
:tool_registry, :prompt_registry, :resource_registry,
|
11
11
|
:created_at, :updated_at, :ended_at, :last_event_id,
|
12
|
-
:session_data
|
12
|
+
:session_data, :consents
|
13
13
|
|
14
14
|
def initialize(attributes = {}, store = nil)
|
15
15
|
@store = store
|
@@ -21,12 +21,15 @@ module ActionMCP
|
|
21
21
|
@message_counter = Concurrent::AtomicFixnum.new(0)
|
22
22
|
@new_record = true
|
23
23
|
|
24
|
+
# Initialize consents as empty hash if not provided
|
25
|
+
@consents = {}
|
26
|
+
|
24
27
|
attributes.each do |key, value|
|
25
28
|
send("#{key}=", value) if respond_to?("#{key}=")
|
26
29
|
end
|
27
30
|
end
|
28
31
|
|
29
|
-
#
|
32
|
+
# ActiveRecord-like interface
|
30
33
|
def new_record?
|
31
34
|
@new_record
|
32
35
|
end
|
@@ -37,7 +40,7 @@ module ActionMCP
|
|
37
40
|
|
38
41
|
def save
|
39
42
|
self.updated_at = Time.current
|
40
|
-
@store
|
43
|
+
@store&.save_session(self)
|
41
44
|
@new_record = false
|
42
45
|
true
|
43
46
|
end
|
@@ -58,7 +61,7 @@ module ActionMCP
|
|
58
61
|
end
|
59
62
|
|
60
63
|
def destroy
|
61
|
-
@store
|
64
|
+
@store&.delete_session(id)
|
62
65
|
end
|
63
66
|
|
64
67
|
def reload
|
@@ -133,32 +136,26 @@ module ActionMCP
|
|
133
136
|
event = { event_id: event_id, data: data, created_at: Time.current }
|
134
137
|
@sse_events << event
|
135
138
|
|
136
|
-
|
137
|
-
while @sse_events.size > max_events
|
138
|
-
@sse_events.shift
|
139
|
-
end
|
139
|
+
@sse_events.shift while @sse_events.size > max_events
|
140
140
|
|
141
141
|
event
|
142
142
|
end
|
143
143
|
|
144
144
|
def get_sse_events_after(last_event_id, limit = 50)
|
145
|
-
@sse_events.select { |e| e[:event_id] > last_event_id }
|
146
|
-
.first(limit)
|
145
|
+
@sse_events.select { |e| e[:event_id] > last_event_id }.first(limit)
|
147
146
|
end
|
148
147
|
|
149
148
|
def cleanup_old_sse_events(max_age = 15.minutes)
|
150
149
|
cutoff_time = Time.current - max_age
|
150
|
+
original_size = @sse_events.size
|
151
151
|
@sse_events.delete_if { |e| e[:created_at] < cutoff_time }
|
152
|
+
original_size - @sse_events.size
|
152
153
|
end
|
153
154
|
|
154
|
-
# Calculates the maximum number of SSE events to store based on configuration
|
155
|
-
# @return [Integer] The maximum number of events
|
156
155
|
def max_stored_sse_events
|
157
156
|
ActionMCP.configuration.max_stored_sse_events || 100
|
158
157
|
end
|
159
158
|
|
160
|
-
# Returns the SSE event retention period from configuration
|
161
|
-
# @return [ActiveSupport::Duration] The retention period (default: 15 minutes)
|
162
159
|
def sse_event_retention_period
|
163
160
|
ActionMCP.configuration.sse_event_retention_period || 15.minutes
|
164
161
|
end
|
@@ -196,9 +193,9 @@ module ActionMCP
|
|
196
193
|
|
197
194
|
# Subscription management
|
198
195
|
def resource_subscribe(uri)
|
199
|
-
|
200
|
-
|
201
|
-
|
196
|
+
return if @subscriptions.any? { |s| s[:uri] == uri }
|
197
|
+
|
198
|
+
@subscriptions << { uri: uri, created_at: Time.current }
|
202
199
|
end
|
203
200
|
|
204
201
|
def resource_unsubscribe(uri)
|
@@ -310,6 +307,29 @@ module ActionMCP
|
|
310
307
|
end
|
311
308
|
end
|
312
309
|
|
310
|
+
# Consent management methods
|
311
|
+
def consent_granted_for?(key)
|
312
|
+
consents_hash = consents.is_a?(String) ? JSON.parse(consents) : consents
|
313
|
+
consents_hash ||= {}
|
314
|
+
consents_hash[key] == true
|
315
|
+
end
|
316
|
+
|
317
|
+
def grant_consent(key)
|
318
|
+
consents_hash = consents.is_a?(String) ? JSON.parse(consents) : consents
|
319
|
+
consents_hash ||= {}
|
320
|
+
consents_hash[key] = true
|
321
|
+
self.consents = consents_hash
|
322
|
+
save!
|
323
|
+
end
|
324
|
+
|
325
|
+
def revoke_consent(key)
|
326
|
+
consents_hash = consents.is_a?(String) ? JSON.parse(consents) : consents
|
327
|
+
consents_hash ||= {}
|
328
|
+
consents_hash.delete(key)
|
329
|
+
self.consents = consents_hash
|
330
|
+
save!
|
331
|
+
end
|
332
|
+
|
313
333
|
private
|
314
334
|
|
315
335
|
def normalize_name(class_or_name, type)
|
@@ -352,7 +372,6 @@ module ActionMCP
|
|
352
372
|
end
|
353
373
|
|
354
374
|
def send_tools_list_changed_notification
|
355
|
-
# Only send if server capabilities allow it
|
356
375
|
return unless server_capabilities.dig("tools", "listChanged")
|
357
376
|
|
358
377
|
write(JSON_RPC::Notification.new(method: "notifications/tools/list_changed"))
|
@@ -370,9 +389,7 @@ module ActionMCP
|
|
370
389
|
write(JSON_RPC::Notification.new(method: "notifications/resources/list_changed"))
|
371
390
|
end
|
372
391
|
|
373
|
-
|
374
|
-
|
375
|
-
# Simple collection classes to mimic ActiveRecord associations
|
392
|
+
# Collection classes
|
376
393
|
class MessageCollection < Array
|
377
394
|
def create!(attributes)
|
378
395
|
self << attributes
|
@@ -380,7 +397,6 @@ module ActionMCP
|
|
380
397
|
end
|
381
398
|
|
382
399
|
def order(field)
|
383
|
-
# Simple ordering implementation
|
384
400
|
sort_by { |msg| msg[field] || msg[field.to_s] }
|
385
401
|
end
|
386
402
|
end
|
@@ -413,8 +429,7 @@ module ActionMCP
|
|
413
429
|
size
|
414
430
|
end
|
415
431
|
|
416
|
-
def where(
|
417
|
-
# Simple implementation for "event_id > ?" condition
|
432
|
+
def where(_condition, value)
|
418
433
|
select { |e| e[:event_id] > value }
|
419
434
|
end
|
420
435
|
|