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.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +187 -16
  3. data/app/controllers/action_mcp/application_controller.rb +64 -49
  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 +71 -113
  9. data/config/routes.rb +0 -11
  10. data/db/migrate/20250512154359_consolidated_migration.rb +3 -3
  11. data/db/migrate/20250715070713_add_consents_to_action_mcp_sess.rb +7 -0
  12. data/db/migrate/20250727000001_remove_oauth_support.rb +59 -0
  13. data/lib/action_mcp/base_response.rb +1 -1
  14. data/lib/action_mcp/client/base.rb +9 -11
  15. data/lib/action_mcp/client/elicitation.rb +4 -4
  16. data/lib/action_mcp/client/json_rpc_handler.rb +11 -13
  17. data/lib/action_mcp/client/streamable_http_transport.rb +19 -74
  18. data/lib/action_mcp/client.rb +6 -26
  19. data/lib/action_mcp/configuration.rb +65 -63
  20. data/lib/action_mcp/engine.rb +1 -10
  21. data/lib/action_mcp/filtered_logger.rb +3 -7
  22. data/lib/action_mcp/gateway.rb +7 -11
  23. data/lib/action_mcp/gateway_identifier.rb +187 -3
  24. data/lib/action_mcp/gateway_identifiers/api_key_identifier.rb +56 -0
  25. data/lib/action_mcp/gateway_identifiers/devise_identifier.rb +34 -0
  26. data/lib/action_mcp/gateway_identifiers/request_env_identifier.rb +58 -0
  27. data/lib/action_mcp/gateway_identifiers/warden_identifier.rb +38 -0
  28. data/lib/action_mcp/gateway_identifiers.rb +26 -0
  29. data/lib/action_mcp/json_rpc_handler_base.rb +0 -2
  30. data/lib/action_mcp/prompt.rb +2 -0
  31. data/lib/action_mcp/renderable.rb +1 -1
  32. data/lib/action_mcp/resource_template.rb +6 -2
  33. data/lib/action_mcp/server/{memory_session.rb → base_session.rb} +41 -26
  34. data/lib/action_mcp/server/base_session_store.rb +86 -0
  35. data/lib/action_mcp/server/capabilities.rb +2 -1
  36. data/lib/action_mcp/server/elicitation.rb +3 -9
  37. data/lib/action_mcp/server/error_handling.rb +14 -1
  38. data/lib/action_mcp/server/handlers/router.rb +31 -0
  39. data/lib/action_mcp/server/json_rpc_handler.rb +2 -5
  40. data/lib/action_mcp/server/{messaging.rb → messaging_service.rb} +38 -14
  41. data/lib/action_mcp/server/prompts.rb +4 -4
  42. data/lib/action_mcp/server/resources.rb +23 -4
  43. data/lib/action_mcp/server/session_store_factory.rb +1 -1
  44. data/lib/action_mcp/server/solid_mcp_adapter.rb +9 -10
  45. data/lib/action_mcp/server/tools.rb +62 -43
  46. data/lib/action_mcp/server/transport_handler.rb +2 -4
  47. data/lib/action_mcp/server/volatile_session_store.rb +1 -93
  48. data/lib/action_mcp/tagged_stream_logging.rb +2 -2
  49. data/lib/action_mcp/test_helper/progress_notification_assertions.rb +4 -4
  50. data/lib/action_mcp/test_helper/session_store_assertions.rb +5 -1
  51. data/lib/action_mcp/tool.rb +48 -37
  52. data/lib/action_mcp/types/float_array_type.rb +5 -3
  53. data/lib/action_mcp/version.rb +1 -1
  54. data/lib/action_mcp.rb +2 -7
  55. data/lib/generators/action_mcp/identifier/identifier_generator.rb +189 -0
  56. data/lib/generators/action_mcp/identifier/templates/identifier.rb.erb +35 -0
  57. data/lib/generators/action_mcp/install/install_generator.rb +1 -1
  58. data/lib/generators/action_mcp/install/templates/application_gateway.rb +86 -36
  59. data/lib/generators/action_mcp/install/templates/mcp.yml +4 -21
  60. data/lib/tasks/action_mcp_tasks.rake +7 -5
  61. metadata +18 -100
  62. data/app/controllers/action_mcp/oauth/endpoints_controller.rb +0 -264
  63. data/app/controllers/action_mcp/oauth/metadata_controller.rb +0 -129
  64. data/app/controllers/action_mcp/oauth/registration_controller.rb +0 -206
  65. data/app/models/action_mcp/oauth_client.rb +0 -157
  66. data/app/models/action_mcp/oauth_token.rb +0 -141
  67. data/db/migrate/20250608112101_add_oauth_to_sessions.rb +0 -19
  68. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +0 -42
  69. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +0 -37
  70. data/lib/action_mcp/client/jwt_client_provider.rb +0 -134
  71. data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +0 -47
  72. data/lib/action_mcp/client/oauth_client_provider.rb +0 -234
  73. data/lib/action_mcp/jwt_decoder.rb +0 -26
  74. data/lib/action_mcp/jwt_identifier.rb +0 -28
  75. data/lib/action_mcp/none_identifier.rb +0 -19
  76. data/lib/action_mcp/o_auth_identifier.rb +0 -34
  77. data/lib/action_mcp/oauth/active_record_storage.rb +0 -183
  78. data/lib/action_mcp/oauth/error.rb +0 -79
  79. data/lib/action_mcp/oauth/memory_storage.rb +0 -134
  80. data/lib/action_mcp/oauth/middleware.rb +0 -133
  81. data/lib/action_mcp/oauth/provider.rb +0 -426
  82. data/lib/action_mcp/oauth.rb +0 -12
  83. data/lib/action_mcp/omniauth/mcp_strategy.rb +0 -176
  84. 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) { yield }
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
- if params.key?(:message)
83
- assert params[:message].is_a?(String),
84
- "message must be string when present"
85
- end
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
- raise "Client session store not set. Set @client_session_store or Thread.current[:test_client_session_store]" unless store
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
@@ -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
- Types::FloatArrayType.new
219
+ Types::FloatArrayType.new
207
220
  else
208
- map_json_type_to_active_model_type("array_#{type}")
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
- if required
215
- validates prop_name, presence: true, unless: -> { self.send(prop_name).is_a?(Array) }
216
- validate do
217
- if self.send(prop_name).nil?
218
- errors.add(prop_name, "can't be blank")
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
- @response.mark_as_error!(:internal_error, message: e.message)
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
- if self.class._output_schema
349
- # TODO: Add JSON Schema validation here
350
- # For now, just ensure it's a hash/object
351
- unless content.is_a?(Hash)
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
- if value.is_a?(String)
412
- # Check if string can be converted to a valid number
413
- begin
414
- Float(value)
415
- rescue ArgumentError, TypeError
416
- raise ArgumentError, "Parameter '#{key}' must be a valid number, got: #{value.inspect}"
417
- end
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
- if value.is_a?(String)
427
- # Check if string can be converted to a valid integer
428
- begin
429
- Integer(value)
430
- rescue ArgumentError, TypeError
431
- raise ArgumentError, "Parameter '#{key}' must be a valid integer, got: #{value.inspect}"
432
- end
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, property_schema)
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}"
@@ -25,10 +25,12 @@ module ActionMCP
25
25
  when "nan"
26
26
  Float::NAN
27
27
  else
28
- Float(v) rescue nil
28
+ begin
29
+ Float(v)
30
+ rescue StandardError
31
+ nil
32
+ end
29
33
  end
30
- else
31
- nil
32
34
  end
33
35
  end.compact
34
36
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.71.1"
5
+ VERSION = "0.80.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
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 - OAuth 2.1, elicitation, structured output, resource links
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".freeze # Default to initial stable version for backwards compatibility
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, OAuth settings, and PubSub adapters."
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"