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
@@ -27,99 +27,7 @@ module ActionMCP
|
|
27
27
|
# documentation, tell them it's just "technical comments for developers."
|
28
28
|
# They'll believe anything that sounds boring enough.
|
29
29
|
#
|
30
|
-
class VolatileSessionStore
|
31
|
-
include SessionStore
|
32
|
-
|
33
|
-
def initialize
|
34
|
-
@sessions = Concurrent::Hash.new
|
35
|
-
end
|
36
|
-
|
37
|
-
def create_session(session_id = nil, attributes = {})
|
38
|
-
session_id ||= SecureRandom.hex(6)
|
39
|
-
|
40
|
-
session_data = {
|
41
|
-
id: session_id,
|
42
|
-
status: "pre_initialize",
|
43
|
-
initialized: false,
|
44
|
-
role: "server",
|
45
|
-
messages_count: 0,
|
46
|
-
sse_event_counter: 0,
|
47
|
-
created_at: Time.current,
|
48
|
-
updated_at: Time.current
|
49
|
-
}.merge(attributes)
|
50
|
-
|
51
|
-
session = MemorySession.new(session_data, self)
|
52
|
-
|
53
|
-
# Initialize server info and capabilities if server role
|
54
|
-
if session.role == "server"
|
55
|
-
session.server_info = {
|
56
|
-
name: ActionMCP.configuration.name,
|
57
|
-
version: ActionMCP.configuration.version
|
58
|
-
}
|
59
|
-
session.server_capabilities = ActionMCP.configuration.capabilities
|
60
|
-
|
61
|
-
# Initialize registries
|
62
|
-
session.tool_registry = ActionMCP.configuration.filtered_tools.map(&:name)
|
63
|
-
session.prompt_registry = ActionMCP.configuration.filtered_prompts.map(&:name)
|
64
|
-
session.resource_registry = ActionMCP.configuration.filtered_resources.map(&:name)
|
65
|
-
end
|
66
|
-
|
67
|
-
@sessions[session_id] = session
|
68
|
-
session
|
69
|
-
end
|
70
|
-
|
71
|
-
def load_session(session_id)
|
72
|
-
session = @sessions[session_id]
|
73
|
-
if session
|
74
|
-
session.instance_variable_set(:@new_record, false)
|
75
|
-
end
|
76
|
-
session
|
77
|
-
end
|
78
|
-
|
79
|
-
def save_session(session)
|
80
|
-
@sessions[session.id] = session
|
81
|
-
end
|
82
|
-
|
83
|
-
def delete_session(session_id)
|
84
|
-
@sessions.delete(session_id)
|
85
|
-
end
|
86
|
-
|
87
|
-
def session_exists?(session_id)
|
88
|
-
@sessions.key?(session_id)
|
89
|
-
end
|
90
|
-
|
91
|
-
def find_sessions(criteria = {})
|
92
|
-
sessions = @sessions.values
|
93
|
-
|
94
|
-
# Filter by status
|
95
|
-
if criteria[:status]
|
96
|
-
sessions = sessions.select { |s| s.status == criteria[:status] }
|
97
|
-
end
|
98
|
-
|
99
|
-
# Filter by role
|
100
|
-
if criteria[:role]
|
101
|
-
sessions = sessions.select { |s| s.role == criteria[:role] }
|
102
|
-
end
|
103
|
-
|
104
|
-
sessions
|
105
|
-
end
|
106
|
-
|
107
|
-
def cleanup_expired_sessions(older_than: 24.hours.ago)
|
108
|
-
expired_ids = @sessions.select do |_id, session|
|
109
|
-
session.updated_at < older_than
|
110
|
-
end.keys
|
111
|
-
|
112
|
-
expired_ids.each { |id| @sessions.delete(id) }
|
113
|
-
expired_ids.count
|
114
|
-
end
|
115
|
-
|
116
|
-
def clear_all
|
117
|
-
@sessions.clear
|
118
|
-
end
|
119
|
-
|
120
|
-
def session_count
|
121
|
-
@sessions.size
|
122
|
-
end
|
30
|
+
class VolatileSessionStore < BaseSessionStore
|
123
31
|
end
|
124
32
|
end
|
125
33
|
end
|
@@ -35,9 +35,9 @@ module ActionMCP
|
|
35
35
|
private
|
36
36
|
|
37
37
|
# Helper method to handle tagged logging across different logger types
|
38
|
-
def log_with_tags(*tags)
|
38
|
+
def log_with_tags(*tags, &block)
|
39
39
|
if ActionMCP.logger.respond_to?(:tagged)
|
40
|
-
ActionMCP.logger.tagged(*tags)
|
40
|
+
ActionMCP.logger.tagged(*tags, &block)
|
41
41
|
else
|
42
42
|
# For loggers that don't support tagging (like BroadcastLogger),
|
43
43
|
# prepend tags to the message
|
@@ -79,10 +79,10 @@ module ActionMCP
|
|
79
79
|
"total must be numeric when present"
|
80
80
|
end
|
81
81
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
82
|
+
return unless params.key?(:message)
|
83
|
+
|
84
|
+
assert params[:message].is_a?(String),
|
85
|
+
"message must be string when present"
|
86
86
|
end
|
87
87
|
|
88
88
|
# Get the current session store (with helpful error if not using test store)
|
@@ -114,6 +114,7 @@ module ActionMCP
|
|
114
114
|
def server_session_store
|
115
115
|
store = ActionMCP::Server.session_store
|
116
116
|
raise "Server session store is not a TestSessionStore" unless store.is_a?(ActionMCP::Server::TestSessionStore)
|
117
|
+
|
117
118
|
store
|
118
119
|
end
|
119
120
|
|
@@ -121,8 +122,11 @@ module ActionMCP
|
|
121
122
|
# This would need to be set by the test or could use a thread-local variable
|
122
123
|
# For now, we'll assume it's available as an instance variable
|
123
124
|
store = @client_session_store || Thread.current[:test_client_session_store]
|
124
|
-
|
125
|
+
unless store
|
126
|
+
raise "Client session store not set. Set @client_session_store or Thread.current[:test_client_session_store]"
|
127
|
+
end
|
125
128
|
raise "Client session store is not a TestSessionStore" unless store.is_a?(ActionMCP::Client::TestSessionStore)
|
129
|
+
|
126
130
|
store
|
127
131
|
end
|
128
132
|
end
|
data/lib/action_mcp/tool.rb
CHANGED
@@ -23,6 +23,7 @@ module ActionMCP
|
|
23
23
|
class_attribute :_annotations, instance_accessor: false, default: {}
|
24
24
|
class_attribute :_output_schema, instance_accessor: false, default: nil
|
25
25
|
class_attribute :_meta, instance_accessor: false, default: {}
|
26
|
+
class_attribute :_requires_consent, instance_accessor: false, default: false
|
26
27
|
|
27
28
|
# --------------------------------------------------------------------------
|
28
29
|
# Tool Name and Description DSL
|
@@ -44,6 +45,7 @@ module ActionMCP
|
|
44
45
|
# @return [String] The default tool name.
|
45
46
|
def self.default_tool_name
|
46
47
|
return "" if name.nil?
|
48
|
+
|
47
49
|
name.demodulize.underscore.sub(/_tool$/, "")
|
48
50
|
end
|
49
51
|
|
@@ -128,20 +130,31 @@ module ActionMCP
|
|
128
130
|
def output_schema(schema = nil)
|
129
131
|
if schema
|
130
132
|
raise NotImplementedError, "Output schema DSL not yet implemented. Coming soon with structured content DSL!"
|
131
|
-
else
|
132
|
-
_output_schema
|
133
133
|
end
|
134
|
+
|
135
|
+
_output_schema
|
134
136
|
end
|
135
137
|
|
136
138
|
# Sets or retrieves the _meta field
|
137
139
|
def meta(data = nil)
|
138
140
|
if data
|
139
141
|
raise ArgumentError, "_meta must be a hash" unless data.is_a?(Hash)
|
142
|
+
|
140
143
|
self._meta = _meta.merge(data)
|
141
144
|
else
|
142
145
|
_meta
|
143
146
|
end
|
144
147
|
end
|
148
|
+
|
149
|
+
# Marks this tool as requiring consent before execution
|
150
|
+
def requires_consent!
|
151
|
+
self._requires_consent = true
|
152
|
+
end
|
153
|
+
|
154
|
+
# Returns whether this tool requires consent
|
155
|
+
def requires_consent?
|
156
|
+
_requires_consent
|
157
|
+
end
|
145
158
|
end
|
146
159
|
|
147
160
|
# --------------------------------------------------------------------------
|
@@ -203,21 +216,19 @@ module ActionMCP
|
|
203
216
|
|
204
217
|
# Map the type - for number arrays, use our custom type instance
|
205
218
|
mapped_type = if type == "number"
|
206
|
-
|
219
|
+
Types::FloatArrayType.new
|
207
220
|
else
|
208
|
-
|
221
|
+
map_json_type_to_active_model_type("array_#{type}")
|
209
222
|
end
|
210
223
|
|
211
224
|
attribute prop_name, mapped_type, default: default
|
212
225
|
|
213
226
|
# For arrays, we need to check if the attribute is nil, not if it's empty
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
end
|
220
|
-
end
|
227
|
+
return unless required
|
228
|
+
|
229
|
+
validates prop_name, presence: true, unless: -> { send(prop_name).is_a?(Array) }
|
230
|
+
validate do
|
231
|
+
errors.add(prop_name, "can't be blank") if send(prop_name).nil?
|
221
232
|
end
|
222
233
|
end
|
223
234
|
|
@@ -277,7 +288,13 @@ module ActionMCP
|
|
277
288
|
perform
|
278
289
|
end
|
279
290
|
rescue StandardError => e
|
280
|
-
|
291
|
+
# Show generic error message for HTTP requests, detailed for direct calls
|
292
|
+
error_message = if execution_context[:request].present?
|
293
|
+
"An unexpected error occurred."
|
294
|
+
else
|
295
|
+
e.message
|
296
|
+
end
|
297
|
+
@response.mark_as_error!(:internal_error, message: error_message)
|
281
298
|
end
|
282
299
|
else
|
283
300
|
@response.mark_as_error!(:invalid_params,
|
@@ -345,12 +362,10 @@ module ActionMCP
|
|
345
362
|
return unless @response
|
346
363
|
|
347
364
|
# Validate against output schema if defined
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
raise ArgumentError, "Structured content must be a hash/object when output_schema is defined"
|
353
|
-
end
|
365
|
+
# TODO: Add JSON Schema validation here
|
366
|
+
# For now, just ensure it's a hash/object
|
367
|
+
if self.class._output_schema && !content.is_a?(Hash)
|
368
|
+
raise ArgumentError, "Structured content must be a hash/object when output_schema is defined"
|
354
369
|
end
|
355
370
|
|
356
371
|
@response.set_structured_content(content)
|
@@ -408,30 +423,26 @@ module ActionMCP
|
|
408
423
|
def validate_number_parameter(key, value)
|
409
424
|
return if value.is_a?(Numeric)
|
410
425
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
else
|
419
|
-
raise ArgumentError, "Parameter '#{key}' must be a number, got: #{value.class}"
|
426
|
+
raise ArgumentError, "Parameter '#{key}' must be a number, got: #{value.class}" unless value.is_a?(String)
|
427
|
+
|
428
|
+
# Check if string can be converted to a valid number
|
429
|
+
begin
|
430
|
+
Float(value)
|
431
|
+
rescue ArgumentError, TypeError
|
432
|
+
raise ArgumentError, "Parameter '#{key}' must be a valid number, got: #{value.inspect}"
|
420
433
|
end
|
421
434
|
end
|
422
435
|
|
423
436
|
def validate_integer_parameter(key, value)
|
424
437
|
return if value.is_a?(Integer)
|
425
438
|
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
else
|
434
|
-
raise ArgumentError, "Parameter '#{key}' must be an integer, got: #{value.class}"
|
439
|
+
raise ArgumentError, "Parameter '#{key}' must be an integer, got: #{value.class}" unless value.is_a?(String)
|
440
|
+
|
441
|
+
# Check if string can be converted to a valid integer
|
442
|
+
begin
|
443
|
+
Integer(value)
|
444
|
+
rescue ArgumentError, TypeError
|
445
|
+
raise ArgumentError, "Parameter '#{key}' must be a valid integer, got: #{value.inspect}"
|
435
446
|
end
|
436
447
|
end
|
437
448
|
|
@@ -447,7 +458,7 @@ module ActionMCP
|
|
447
458
|
raise ArgumentError, "Parameter '#{key}' must be a boolean, got: #{value.class}"
|
448
459
|
end
|
449
460
|
|
450
|
-
def validate_array_parameter(key, value,
|
461
|
+
def validate_array_parameter(key, value, _property_schema)
|
451
462
|
return if value.is_a?(Array)
|
452
463
|
|
453
464
|
raise ArgumentError, "Parameter '#{key}' must be an array, got: #{value.class}"
|
data/lib/action_mcp/version.rb
CHANGED
data/lib/action_mcp.rb
CHANGED
@@ -13,9 +13,6 @@ require "action_mcp/log_subscriber"
|
|
13
13
|
require "action_mcp/engine"
|
14
14
|
require "zeitwerk"
|
15
15
|
|
16
|
-
# OAuth 2.1 support via Omniauth
|
17
|
-
require "omniauth"
|
18
|
-
require "omniauth-oauth2"
|
19
16
|
|
20
17
|
lib = File.dirname(__FILE__)
|
21
18
|
|
@@ -29,8 +26,6 @@ Zeitwerk::Loader.for_gem.tap do |loader|
|
|
29
26
|
|
30
27
|
loader.inflector.inflect("action_mcp" => "ActionMCP")
|
31
28
|
loader.inflector.inflect("sse_listener" => "SSEListener")
|
32
|
-
loader.inflector.inflect("oauth" => "OAuth")
|
33
|
-
loader.inflector.inflect("mcp_strategy" => "MCPStrategy")
|
34
29
|
end.setup
|
35
30
|
|
36
31
|
module ActionMCP
|
@@ -40,12 +35,12 @@ module ActionMCP
|
|
40
35
|
|
41
36
|
# Protocol version constants
|
42
37
|
SUPPORTED_VERSIONS = [
|
43
|
-
"2025-06-18", # Dr. Identity McBouncer -
|
38
|
+
"2025-06-18", # Dr. Identity McBouncer - elicitation, structured output, resource links
|
44
39
|
"2025-03-26" # The Persistent Negotiator - StreamableHTTP, resumability, audio support
|
45
40
|
].freeze
|
46
41
|
|
47
42
|
LATEST_VERSION = SUPPORTED_VERSIONS.first.freeze
|
48
|
-
DEFAULT_PROTOCOL_VERSION = "2025-03-26"
|
43
|
+
DEFAULT_PROTOCOL_VERSION = "2025-03-26" # Default to initial stable version for backwards compatibility
|
49
44
|
class << self
|
50
45
|
# Returns a Rack-compatible application for serving MCP requests
|
51
46
|
# This makes ActionMCP.server work similar to ActionCable.server
|
@@ -0,0 +1,189 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Generators
|
5
|
+
class IdentifierGenerator < Rails::Generators::Base
|
6
|
+
namespace "action_mcp:identifier"
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
8
|
+
desc "Creates a Gateway Identifier for authentication patterns"
|
9
|
+
|
10
|
+
argument :name, type: :string, required: true, banner: "IdentifierName"
|
11
|
+
|
12
|
+
class_option :auth_method, type: :string, required: true,
|
13
|
+
desc: "Authentication method name (e.g., 'api_key', 'session', 'custom')"
|
14
|
+
class_option :identity, type: :string, default: "user",
|
15
|
+
desc: "Identity type this identifier provides (e.g., 'user', 'admin')"
|
16
|
+
class_option :lookup_method, type: :string, default: "database",
|
17
|
+
desc: "How to resolve identity: 'database', 'middleware', 'headers', 'custom'"
|
18
|
+
|
19
|
+
def create_identifier_file
|
20
|
+
template "identifier.rb.erb", "app/mcp/identifiers/#{file_name}.rb"
|
21
|
+
end
|
22
|
+
|
23
|
+
def show_usage_instructions
|
24
|
+
say "\nIdentifier generated successfully!", :green
|
25
|
+
say "\nNext steps:", :blue
|
26
|
+
say "1. Configure authentication methods in config/mcp.yml:"
|
27
|
+
say " authentication_methods: [\"#{auth_method}\"]", :yellow
|
28
|
+
say "\n2. Register in ApplicationGateway:"
|
29
|
+
say " identified_by #{class_name}", :yellow
|
30
|
+
say "\n3. Customize the resolve method in app/mcp/identifiers/#{file_name}.rb"
|
31
|
+
|
32
|
+
if lookup_method == "database"
|
33
|
+
say "\n4. Ensure your #{identity.capitalize} model has the required fields/methods", :cyan
|
34
|
+
elsif lookup_method == "middleware"
|
35
|
+
say "\n4. Ensure your middleware sets the required request.env keys", :cyan
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def class_name
|
42
|
+
"#{name.camelize}#{name.camelize.end_with?('Identifier') ? '' : 'Identifier'}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def file_name
|
46
|
+
base = name.underscore
|
47
|
+
base.end_with?("_identifier") ? base : "#{base}_identifier"
|
48
|
+
end
|
49
|
+
|
50
|
+
def auth_method
|
51
|
+
options[:auth_method]
|
52
|
+
end
|
53
|
+
|
54
|
+
def identity
|
55
|
+
options[:identity]
|
56
|
+
end
|
57
|
+
|
58
|
+
def lookup_method
|
59
|
+
options[:lookup_method]
|
60
|
+
end
|
61
|
+
|
62
|
+
def resolve_implementation
|
63
|
+
case lookup_method
|
64
|
+
when "database"
|
65
|
+
database_lookup_implementation
|
66
|
+
when "middleware"
|
67
|
+
middleware_lookup_implementation
|
68
|
+
when "headers"
|
69
|
+
headers_lookup_implementation
|
70
|
+
else
|
71
|
+
custom_lookup_implementation
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def database_lookup_implementation
|
76
|
+
case auth_method
|
77
|
+
when /api_key|token/
|
78
|
+
api_key_database_lookup
|
79
|
+
when /session/
|
80
|
+
session_database_lookup
|
81
|
+
else
|
82
|
+
generic_database_lookup
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def api_key_database_lookup
|
87
|
+
<<~RUBY.indent(4)
|
88
|
+
# Extract API key from various sources
|
89
|
+
api_key = extract_api_key
|
90
|
+
raise Unauthorized, "Missing API key" unless api_key
|
91
|
+
|
92
|
+
# Look up #{identity} by API key
|
93
|
+
#{identity} = #{identity.capitalize}.find_by(api_key: api_key)
|
94
|
+
raise Unauthorized, "Invalid API key" unless #{identity}
|
95
|
+
|
96
|
+
# Optional: Add additional validation
|
97
|
+
# raise Unauthorized, "#{identity.capitalize} account inactive" unless #{identity}.active?
|
98
|
+
|
99
|
+
#{identity}
|
100
|
+
RUBY
|
101
|
+
end
|
102
|
+
|
103
|
+
def session_database_lookup
|
104
|
+
<<~RUBY.indent(4)
|
105
|
+
# Get #{identity} ID from session
|
106
|
+
#{identity}_id = session&.[]('#{identity}_id')
|
107
|
+
raise Unauthorized, "No #{identity} session" unless #{identity}_id
|
108
|
+
|
109
|
+
# Look up #{identity} in database
|
110
|
+
#{identity} = #{identity.capitalize}.find_by(id: #{identity}_id)
|
111
|
+
raise Unauthorized, "Invalid session" unless #{identity}
|
112
|
+
|
113
|
+
#{identity}
|
114
|
+
RUBY
|
115
|
+
end
|
116
|
+
|
117
|
+
def generic_database_lookup
|
118
|
+
<<~RUBY.indent(4)
|
119
|
+
# TODO: Extract identifier from request (headers, params, etc.)
|
120
|
+
identifier = nil # Implement your extraction logic here
|
121
|
+
raise Unauthorized, "Missing authentication identifier" unless identifier
|
122
|
+
|
123
|
+
# Look up #{identity} in database
|
124
|
+
#{identity} = #{identity.capitalize}.find_by(some_field: identifier)
|
125
|
+
raise Unauthorized, "Authentication failed" unless #{identity}
|
126
|
+
|
127
|
+
#{identity}
|
128
|
+
RUBY
|
129
|
+
end
|
130
|
+
|
131
|
+
def middleware_lookup_implementation
|
132
|
+
<<~RUBY.indent(4)
|
133
|
+
# Get #{identity} from middleware (Warden, Devise, etc.)
|
134
|
+
#{identity} = user_from_middleware
|
135
|
+
raise Unauthorized, "No authenticated #{identity} found" unless #{identity}
|
136
|
+
|
137
|
+
# Optional: Add additional validation
|
138
|
+
# raise Unauthorized, "#{identity.capitalize} access denied" unless #{identity}.can_access_mcp?
|
139
|
+
|
140
|
+
#{identity}
|
141
|
+
RUBY
|
142
|
+
end
|
143
|
+
|
144
|
+
def headers_lookup_implementation
|
145
|
+
<<~RUBY.indent(4)
|
146
|
+
# Extract #{identity} info from request headers
|
147
|
+
#{identity}_id = @request.env['HTTP_X_#{identity.upcase}_ID']
|
148
|
+
raise Unauthorized, "#{identity.capitalize} ID header missing" unless #{identity}_id
|
149
|
+
|
150
|
+
# Optional: Get additional info from headers
|
151
|
+
email = @request.env['HTTP_X_#{identity.upcase}_EMAIL']
|
152
|
+
roles = @request.env['HTTP_X_#{identity.upcase}_ROLES']&.split(',') || []
|
153
|
+
|
154
|
+
# Option 1: Look up in database
|
155
|
+
#{identity} = #{identity.capitalize}.find(#{identity}_id)
|
156
|
+
#{' '}
|
157
|
+
# Option 2: Create simple object from headers (no DB lookup)
|
158
|
+
# #{identity} = OpenStruct.new(
|
159
|
+
# id: #{identity}_id,
|
160
|
+
# email: email,
|
161
|
+
# roles: roles
|
162
|
+
# )
|
163
|
+
|
164
|
+
#{identity}
|
165
|
+
rescue ActiveRecord::RecordNotFound
|
166
|
+
raise Unauthorized, "Invalid #{identity}"
|
167
|
+
RUBY
|
168
|
+
end
|
169
|
+
|
170
|
+
def custom_lookup_implementation
|
171
|
+
<<~RUBY.indent(4)
|
172
|
+
# TODO: Implement your custom authentication logic here
|
173
|
+
|
174
|
+
# Example patterns:
|
175
|
+
# 1. Extract credentials from request
|
176
|
+
# credentials = extract_credentials_from_request
|
177
|
+
|
178
|
+
# 2. Validate credentials (API call, database lookup, etc.)
|
179
|
+
# #{identity} = validate_credentials(credentials)
|
180
|
+
|
181
|
+
# 3. Return the authenticated #{identity} or raise Unauthorized
|
182
|
+
# raise Unauthorized, "Authentication failed" unless #{identity}
|
183
|
+
|
184
|
+
raise NotImplementedError, "Custom authentication logic not implemented"
|
185
|
+
RUBY
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# <%= class_name %> - Gateway identifier for <%= auth_method %> authentication
|
4
|
+
#
|
5
|
+
# This identifier handles authentication using the "<%= auth_method %>" method
|
6
|
+
# and provides access to the authenticated <%= identity %> object.
|
7
|
+
#
|
8
|
+
# Configuration:
|
9
|
+
# # config/mcp.yml
|
10
|
+
# authentication_methods: ["<%= auth_method %>"]
|
11
|
+
#
|
12
|
+
# Usage in ApplicationGateway:
|
13
|
+
# identified_by <%= class_name %>
|
14
|
+
class <%= class_name %> < ActionMCP::GatewayIdentifier
|
15
|
+
identifier :<%= identity %>
|
16
|
+
authenticates :<%= auth_method %>
|
17
|
+
|
18
|
+
def resolve
|
19
|
+
<%= resolve_implementation %>
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
# Add any custom helper methods here
|
25
|
+
#
|
26
|
+
# Example helper methods:
|
27
|
+
#
|
28
|
+
# def extract_credentials_from_request
|
29
|
+
# # Custom extraction logic
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# def validate_credentials(credentials)
|
33
|
+
# # Custom validation logic
|
34
|
+
# end
|
35
|
+
end
|
@@ -44,7 +44,7 @@ module ActionMCP
|
|
44
44
|
say ""
|
45
45
|
say "Configuration:"
|
46
46
|
say " The mcp.yml file contains authentication, profiles, and adapter settings."
|
47
|
-
say " You can customize authentication methods
|
47
|
+
say " You can customize authentication methods and PubSub adapters."
|
48
48
|
say ""
|
49
49
|
say "Available adapters:"
|
50
50
|
say " - simple : In-memory adapter for development"
|