actionmcp 0.60.2 → 0.71.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +46 -59
  3. data/app/controllers/action_mcp/application_controller.rb +95 -28
  4. data/app/controllers/action_mcp/oauth/metadata_controller.rb +13 -13
  5. data/app/controllers/action_mcp/oauth/registration_controller.rb +206 -0
  6. data/app/models/action_mcp/oauth_client.rb +157 -0
  7. data/app/models/action_mcp/oauth_token.rb +141 -0
  8. data/app/models/action_mcp/session/message.rb +12 -12
  9. data/app/models/action_mcp/session/resource.rb +2 -2
  10. data/app/models/action_mcp/session/sse_event.rb +2 -2
  11. data/app/models/action_mcp/session/subscription.rb +2 -2
  12. data/app/models/action_mcp/session.rb +68 -43
  13. data/config/routes.rb +1 -0
  14. data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +42 -0
  15. data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +37 -0
  16. data/lib/action_mcp/capability.rb +2 -0
  17. data/lib/action_mcp/client/json_rpc_handler.rb +9 -9
  18. data/lib/action_mcp/client/jwt_client_provider.rb +134 -0
  19. data/lib/action_mcp/configuration.rb +90 -11
  20. data/lib/action_mcp/engine.rb +26 -1
  21. data/lib/action_mcp/filtered_logger.rb +32 -0
  22. data/lib/action_mcp/oauth/active_record_storage.rb +183 -0
  23. data/lib/action_mcp/oauth/memory_storage.rb +23 -1
  24. data/lib/action_mcp/oauth/middleware.rb +33 -0
  25. data/lib/action_mcp/oauth/provider.rb +49 -13
  26. data/lib/action_mcp/oauth.rb +12 -0
  27. data/lib/action_mcp/prompt.rb +14 -0
  28. data/lib/action_mcp/registry_base.rb +25 -4
  29. data/lib/action_mcp/resource_response.rb +110 -0
  30. data/lib/action_mcp/resource_template.rb +30 -2
  31. data/lib/action_mcp/server/capabilities.rb +3 -14
  32. data/lib/action_mcp/server/memory_session.rb +0 -1
  33. data/lib/action_mcp/server/prompts.rb +8 -1
  34. data/lib/action_mcp/server/resources.rb +9 -6
  35. data/lib/action_mcp/server/tools.rb +41 -20
  36. data/lib/action_mcp/server.rb +6 -3
  37. data/lib/action_mcp/sse_listener.rb +0 -7
  38. data/lib/action_mcp/test_helper.rb +5 -0
  39. data/lib/action_mcp/tool.rb +108 -4
  40. data/lib/action_mcp/tools_registry.rb +3 -0
  41. data/lib/action_mcp/version.rb +1 -1
  42. data/lib/generators/action_mcp/install/templates/mcp.yml +16 -16
  43. data/lib/tasks/action_mcp_tasks.rake +238 -0
  44. metadata +11 -1
@@ -4,28 +4,28 @@
4
4
  #
5
5
  # Table name: action_mcp_sessions
6
6
  #
7
- # id :string not null, primary key
8
- # authentication_method :string default("none")
9
- # client_capabilities(The capabilities of the client) :json
10
- # client_info(The information about the client) :json
11
- # ended_at(The time the session ended) :datetime
12
- # initialized :boolean default(FALSE), not null
13
- # messages_count :integer default(0), not null
14
- # oauth_access_token :string
15
- # oauth_refresh_token :string
16
- # oauth_token_expires_at :datetime
17
- # oauth_user_context :json
18
- # prompt_registry :json
19
- # protocol_version :string
20
- # resource_registry :json
21
- # role(The role of the session) :string default("server"), not null
22
- # server_capabilities(The capabilities of the server) :json
23
- # server_info(The information about the server) :json
24
- # sse_event_counter :integer default(0), not null
25
- # status :string default("pre_initialize"), not null
26
- # tool_registry :json
27
- # created_at :datetime not null
28
- # updated_at :datetime not null
7
+ # id :string not null, primary key
8
+ # authentication_method :string default("none")
9
+ # client_capabilities :json
10
+ # client_info :json
11
+ # ended_at :datetime
12
+ # initialized :boolean default(FALSE), not null
13
+ # messages_count :integer default(0), not null
14
+ # oauth_access_token :string
15
+ # oauth_refresh_token :string
16
+ # oauth_token_expires_at :datetime
17
+ # oauth_user_context :json
18
+ # prompt_registry :json
19
+ # protocol_version :string
20
+ # resource_registry :json
21
+ # role :string default("server"), not null
22
+ # server_capabilities :json
23
+ # server_info :json
24
+ # sse_event_counter :integer default(0), not null
25
+ # status :string default("pre_initialize"), not null
26
+ # tool_registry :json
27
+ # created_at :datetime not null
28
+ # updated_at :datetime not null
29
29
  #
30
30
  # Indexes
31
31
  #
@@ -75,9 +75,7 @@ module ActionMCP
75
75
  before_create :set_server_info, if: -> { role == "server" }
76
76
  before_create :set_server_capabilities, if: -> { role == "server" }
77
77
 
78
- validates :protocol_version, inclusion: { in: ActionMCP::SUPPORTED_VERSIONS }, allow_nil: true, unless: lambda {
79
- ActionMCP.configuration.vibed_ignore_version
80
- }
78
+ validates :protocol_version, inclusion: { in: ActionMCP::SUPPORTED_VERSIONS }, allow_nil: true
81
79
 
82
80
  def close!
83
81
  dummy_callback = ->(*) { } # this callback seem broken
@@ -115,8 +113,6 @@ module ActionMCP
115
113
  end
116
114
 
117
115
  def set_protocol_version(version)
118
- # If vibed_ignore_version is true, always use the latest supported version
119
- version = PROTOCOL_VERSION if ActionMCP.configuration.vibed_ignore_version
120
116
  update(protocol_version: version)
121
117
  end
122
118
 
@@ -311,29 +307,58 @@ module ActionMCP
311
307
 
312
308
  # Get registered items for this session
313
309
  def registered_tools
314
- (self.tool_registry || []).filter_map do |tool_name|
315
- ActionMCP::ToolsRegistry.find(tool_name)
316
- rescue StandardError
317
- nil
310
+ # Special case: ['*'] means use all available tools dynamically
311
+ if tool_registry == [ "*" ]
312
+ # filtered_tools returns a RegistryScope with Item objects, need to extract the klass
313
+ ActionMCP.configuration.filtered_tools.map(&:klass)
314
+ else
315
+ (self.tool_registry || []).filter_map do |tool_name|
316
+ ActionMCP::ToolsRegistry.find(tool_name)
317
+ rescue StandardError
318
+ nil
319
+ end
318
320
  end
319
321
  end
320
322
 
321
323
  def registered_prompts
322
- (self.prompt_registry || []).filter_map do |prompt_name|
323
- ActionMCP::PromptsRegistry.find(prompt_name)
324
- rescue StandardError
325
- nil
324
+ if prompt_registry == [ "*" ]
325
+ # filtered_prompts returns a RegistryScope with Item objects, need to extract the klass
326
+ ActionMCP.configuration.filtered_prompts.map(&:klass)
327
+ else
328
+ (self.prompt_registry || []).filter_map do |prompt_name|
329
+ ActionMCP::PromptsRegistry.find(prompt_name)
330
+ rescue StandardError
331
+ nil
332
+ end
326
333
  end
327
334
  end
328
335
 
329
336
  def registered_resource_templates
330
- (self.resource_registry || []).filter_map do |template_name|
331
- ActionMCP::ResourceTemplatesRegistry.find(template_name)
332
- rescue StandardError
333
- nil
337
+ if resource_registry == [ "*" ]
338
+ # filtered_resources returns a RegistryScope with Item objects, need to extract the klass
339
+ ActionMCP.configuration.filtered_resources.map(&:klass)
340
+ else
341
+ (self.resource_registry || []).filter_map do |template_name|
342
+ ActionMCP::ResourceTemplatesRegistry.find(template_name)
343
+ rescue StandardError
344
+ nil
345
+ end
334
346
  end
335
347
  end
336
348
 
349
+ # Helper methods to check if using all capabilities
350
+ def uses_all_tools?
351
+ tool_registry == [ "*" ]
352
+ end
353
+
354
+ def uses_all_prompts?
355
+ prompt_registry == [ "*" ]
356
+ end
357
+
358
+ def uses_all_resources?
359
+ resource_registry == [ "*" ]
360
+ end
361
+
337
362
  # OAuth Session Management
338
363
  # Required by MCP 2025-03-26 specification for session binding
339
364
 
@@ -443,10 +468,10 @@ module ActionMCP
443
468
  end
444
469
 
445
470
  def initialize_registries
446
- # Start with default registries from configuration
447
- self.tool_registry = ActionMCP.configuration.filtered_tools.map(&:name)
448
- self.prompt_registry = ActionMCP.configuration.filtered_prompts.map(&:name)
449
- self.resource_registry = ActionMCP.configuration.filtered_resources.map(&:name)
471
+ # Default to using all available capabilities with '*'
472
+ self.tool_registry = [ "*" ]
473
+ self.prompt_registry = [ "*" ]
474
+ self.resource_registry = [ "*" ]
450
475
  end
451
476
 
452
477
  def normalize_name(class_or_name, type)
data/config/routes.rb CHANGED
@@ -12,6 +12,7 @@ ActionMCP::Engine.routes.draw do
12
12
  post "/oauth/token", to: "oauth/endpoints#token", as: :oauth_token
13
13
  post "/oauth/introspect", to: "oauth/endpoints#introspect", as: :oauth_introspect
14
14
  post "/oauth/revoke", to: "oauth/endpoints#revoke", as: :oauth_revoke
15
+ post "/oauth/register", to: "oauth/registration#create", as: :oauth_register
15
16
 
16
17
  # MCP 2025-03-26 Spec routes
17
18
  get "/", to: "application#show", as: :mcp_get
@@ -0,0 +1,42 @@
1
+ class CreateActionMCPOAuthClients < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :action_mcp_oauth_clients do |t|
4
+ t.string :client_id, null: false, index: { unique: true }
5
+ t.string :client_secret
6
+ t.string :client_name
7
+
8
+ # Store arrays as JSON for database compatibility
9
+ if connection.adapter_name.downcase.include?('postgresql')
10
+ t.text :redirect_uris, array: true, default: []
11
+ t.text :grant_types, array: true, default: [ "authorization_code" ]
12
+ t.text :response_types, array: true, default: [ "code" ]
13
+ else
14
+ # For SQLite and other databases, use JSON
15
+ t.json :redirect_uris, default: []
16
+ t.json :grant_types, default: [ "authorization_code" ]
17
+ t.json :response_types, default: [ "code" ]
18
+ end
19
+
20
+ t.string :token_endpoint_auth_method, default: "client_secret_basic"
21
+ t.text :scope
22
+ t.boolean :active, default: true
23
+
24
+ # Registration metadata
25
+ t.integer :client_id_issued_at
26
+ t.integer :client_secret_expires_at
27
+ t.string :registration_access_token # OAuth 2.1 Dynamic Client Registration
28
+
29
+ # Additional metadata as JSON for database compatibility
30
+ if connection.adapter_name.downcase.include?('postgresql')
31
+ t.jsonb :metadata, default: {}
32
+ else
33
+ t.json :metadata, default: {}
34
+ end
35
+
36
+ t.timestamps
37
+ end
38
+
39
+ add_index :action_mcp_oauth_clients, :active
40
+ add_index :action_mcp_oauth_clients, :client_id_issued_at
41
+ end
42
+ end
@@ -0,0 +1,37 @@
1
+ class CreateActionMCPOAuthTokens < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :action_mcp_oauth_tokens do |t|
4
+ t.string :token, null: false, index: { unique: true }
5
+ t.string :token_type, null: false # 'access_token', 'refresh_token', 'authorization_code'
6
+ t.string :client_id, null: false
7
+ t.string :user_id
8
+ t.text :scope
9
+ t.datetime :expires_at
10
+ t.boolean :revoked, default: false
11
+
12
+ # For authorization codes
13
+ t.string :redirect_uri
14
+ t.string :code_challenge
15
+ t.string :code_challenge_method
16
+
17
+ # For refresh tokens
18
+ t.string :access_token # Reference to associated access token
19
+
20
+ # Additional data - use JSON for database compatibility
21
+ if connection.adapter_name.downcase.include?('postgresql')
22
+ t.jsonb :metadata, default: {}
23
+ else
24
+ t.json :metadata, default: {}
25
+ end
26
+
27
+ t.timestamps
28
+ end
29
+
30
+ add_index :action_mcp_oauth_tokens, :token_type
31
+ add_index :action_mcp_oauth_tokens, :client_id
32
+ add_index :action_mcp_oauth_tokens, :user_id
33
+ add_index :action_mcp_oauth_tokens, :expires_at
34
+ add_index :action_mcp_oauth_tokens, :revoked
35
+ add_index :action_mcp_oauth_tokens, [ :token_type, :expires_at ]
36
+ end
37
+ end
@@ -49,6 +49,8 @@ module ActionMCP
49
49
  # @return [void]
50
50
  def self.abstract!
51
51
  self.abstract_capability = true
52
+ # Unregister from the appropriate registry if already registered
53
+ unregister_from_registry
52
54
  end
53
55
 
54
56
  # Returns whether this tool is abstract.
@@ -35,12 +35,12 @@ module ActionMCP
35
35
 
36
36
  def handle_notification(notification)
37
37
  # Handle server notifications to client
38
- puts "\e[33mReceived notification: #{notification.method}\e[0m"
38
+ client.log_debug("Received notification: #{notification.method}")
39
39
  end
40
40
 
41
41
  def handle_response(response)
42
42
  # Handle server responses to client requests
43
- puts "\e[32mReceived response: #{response.id} - #{response.result ? 'success' : 'error'}\e[0m"
43
+ client.log_debug("Received response: #{response.id} - #{response.result ? 'success' : 'error'}")
44
44
  end
45
45
 
46
46
  protected
@@ -60,7 +60,7 @@ module ActionMCP
60
60
  else
61
61
  common_result = handle_common_methods(rpc_method, id, params)
62
62
  if common_result.nil?
63
- puts "\e[31mUnknown server method: #{rpc_method} #{id} #{params}\e[0m"
63
+ client.log_warn("Unknown server method: #{rpc_method} #{id} #{params}")
64
64
  end
65
65
  end
66
66
  end
@@ -94,19 +94,19 @@ module ActionMCP
94
94
  def process_notifications(rpc_method, params)
95
95
  case rpc_method
96
96
  when "notifications/resources/updated" # Resource update notification
97
- puts "\e[31m Resource #{params['uri']} was updated\e[0m"
97
+ client.log_debug("Resource #{params['uri']} was updated")
98
98
  # Handle resource update notification
99
99
  # TODO: fetch updated resource or mark it as stale
100
100
  when "notifications/tools/list_changed" # Tool list change notification
101
- puts "\e[31m Tool list has changed\e[0m"
101
+ client.log_debug("Tool list has changed")
102
102
  # Handle tool list change notification
103
103
  # TODO: fetch new tools or mark them as stale
104
104
  when "notifications/prompts/list_changed" # Prompt list change notification
105
- puts "\e[31m Prompt list has changed\e[0m"
105
+ client.log_debug("Prompt list has changed")
106
106
  # Handle prompt list change notification
107
107
  # TODO: fetch new prompts or mark them as stale
108
108
  when "notifications/resources/list_changed" # Resource list change notification
109
- puts "\e[31m Resource list has changed\e[0m"
109
+ client.log_debug("Resource list has changed")
110
110
  # Handle resource list change notification
111
111
  # TODO: fetch new resources or mark them as stale
112
112
  else
@@ -153,12 +153,12 @@ module ActionMCP
153
153
  return true
154
154
  end
155
155
 
156
- puts "\e[31mUnknown response: #{id} #{result}\e[0m"
156
+ client.log_warn("Unknown response: #{id} #{result}")
157
157
  end
158
158
 
159
159
  def process_error(id, error)
160
160
  # Do something ?
161
- puts "\e[31mUnknown error: #{id} #{error}\e[0m"
161
+ client.log_error("Unknown error: #{id} #{error}")
162
162
  end
163
163
 
164
164
  def handle_initialize_response(request_id, result)
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "base64"
5
+
6
+ module ActionMCP
7
+ module Client
8
+ # JWT client provider for MCP client authentication
9
+ # Provides clean JWT token management for ActionMCP client connections
10
+ class JwtClientProvider
11
+ class AuthenticationError < StandardError; end
12
+ class TokenExpiredError < StandardError; end
13
+
14
+ attr_reader :storage
15
+
16
+ def initialize(token: nil, storage: nil, logger: ActionMCP.logger)
17
+ @storage = storage || MemoryStorage.new
18
+ @logger = logger
19
+
20
+ # If token provided during initialization, store it
21
+ if token
22
+ save_token(token)
23
+ end
24
+ end
25
+
26
+ # Check if client has valid authentication
27
+ def authenticated?
28
+ token = current_token
29
+ return false unless token
30
+
31
+ !token_expired?(token)
32
+ end
33
+
34
+ # Get authorization headers for HTTP requests
35
+ def authorization_headers
36
+ token = current_token
37
+ return {} unless token
38
+
39
+ if token_expired?(token)
40
+ log_debug("JWT token expired")
41
+ clear_tokens!
42
+ return {}
43
+ end
44
+
45
+ { "Authorization" => "Bearer #{token}" }
46
+ end
47
+
48
+ # Set/update the JWT token
49
+ def set_token(token)
50
+ save_token(token)
51
+ log_debug("JWT token updated")
52
+ end
53
+
54
+ # Clear stored tokens (logout)
55
+ def clear_tokens!
56
+ @storage.clear_token
57
+ log_debug("Cleared JWT token")
58
+ end
59
+
60
+ # Get current valid token
61
+ def access_token
62
+ token = current_token
63
+ return nil unless token
64
+ return nil if token_expired?(token)
65
+ token
66
+ end
67
+
68
+ private
69
+
70
+ def current_token
71
+ @storage.load_token
72
+ end
73
+
74
+ def save_token(token)
75
+ @storage.save_token(token)
76
+ end
77
+
78
+ def token_expired?(token)
79
+ return false unless token
80
+
81
+ begin
82
+ payload = decode_jwt_payload(token)
83
+ exp = payload["exp"]
84
+ return false unless exp
85
+
86
+ # Add 30 second buffer for clock skew
87
+ Time.at(exp) <= Time.now + 30
88
+ rescue => e
89
+ log_debug("Error checking token expiration: #{e.message}")
90
+ true # Treat invalid tokens as expired
91
+ end
92
+ end
93
+
94
+ def decode_jwt_payload(token)
95
+ # Split JWT into parts
96
+ parts = token.split(".")
97
+ raise AuthenticationError, "Invalid JWT format" unless parts.length == 3
98
+
99
+ # Decode payload (second part)
100
+ payload_base64 = parts[1]
101
+ # Add padding if needed
102
+ payload_base64 += "=" * (4 - payload_base64.length % 4) if payload_base64.length % 4 != 0
103
+
104
+ payload_json = Base64.urlsafe_decode64(payload_base64)
105
+ JSON.parse(payload_json)
106
+ rescue => e
107
+ raise AuthenticationError, "Failed to decode JWT: #{e.message}"
108
+ end
109
+
110
+ def log_debug(message)
111
+ @logger.debug("[ActionMCP::JwtClientProvider] #{message}")
112
+ end
113
+
114
+ # Simple memory storage for JWT tokens
115
+ class MemoryStorage
116
+ def initialize
117
+ @token = nil
118
+ end
119
+
120
+ def save_token(token)
121
+ @token = token
122
+ end
123
+
124
+ def load_token
125
+ @token
126
+ end
127
+
128
+ def clear_token
129
+ @token = nil
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -26,6 +26,7 @@ module ActionMCP
26
26
  :active_profile,
27
27
  :profiles,
28
28
  :elicitation_enabled,
29
+ :verbose_logging,
29
30
  # --- Authentication Options ---
30
31
  :authentication_methods,
31
32
  :oauth_config,
@@ -33,8 +34,6 @@ module ActionMCP
33
34
  :sse_heartbeat_interval,
34
35
  :post_response_preference, # :json or :sse
35
36
  :protocol_version,
36
- # --- VibedIgnoreVersion Option ---
37
- :vibed_ignore_version,
38
37
  # --- SSE Resumability Options ---
39
38
  :sse_event_retention_period,
40
39
  :max_stored_sse_events,
@@ -58,17 +57,17 @@ module ActionMCP
58
57
  @logging_level = :info
59
58
  @resources_subscribe = false
60
59
  @elicitation_enabled = false
60
+ @verbose_logging = false
61
61
  @active_profile = :primary
62
62
  @profiles = default_profiles
63
63
 
64
64
  # Authentication defaults
65
65
  @authentication_methods = Rails.env.production? ? [ "jwt" ] : [ "none" ]
66
- @oauth_config = {}
66
+ @oauth_config = HashWithIndifferentAccess.new
67
67
 
68
68
  @sse_heartbeat_interval = 30
69
69
  @post_response_preference = :json
70
70
  @protocol_version = "2025-03-26" # Default to legacy for backwards compatibility
71
- @vibed_ignore_version = false
72
71
 
73
72
  # Resumability defaults
74
73
  @sse_event_retention_period = 15.minutes
@@ -76,6 +75,7 @@ module ActionMCP
76
75
 
77
76
  # Gateway - default to ApplicationGateway if it exists, otherwise ActionMCP::Gateway
78
77
  @gateway_class = defined?(::ApplicationGateway) ? ::ApplicationGateway : ActionMCP::Gateway
78
+ @gateway_class_name = nil
79
79
 
80
80
  # Session Store
81
81
  @session_store_type = Rails.env.production? ? :active_record : :volatile
@@ -91,6 +91,15 @@ module ActionMCP
91
91
  @version || (has_rails_version ? Rails.application.version.to_s : "0.0.1")
92
92
  end
93
93
 
94
+ def gateway_class
95
+ if @gateway_class_name
96
+ klass = @gateway_class_name.constantize
97
+ klass
98
+ else
99
+ @gateway_class
100
+ end
101
+ end
102
+
94
103
  # Get active profile (considering thread-local override)
95
104
  def active_profile
96
105
  ActionMCP.thread_profiles.value || @active_profile
@@ -114,7 +123,7 @@ module ActionMCP
114
123
 
115
124
  # Extract OAuth configuration if present
116
125
  if app_config["oauth"]
117
- @oauth_config = app_config["oauth"]
126
+ @oauth_config = HashWithIndifferentAccess.new(app_config["oauth"])
118
127
  end
119
128
 
120
129
  # Extract other top-level configuration settings
@@ -124,9 +133,10 @@ module ActionMCP
124
133
  if app_config["profiles"]
125
134
  @profiles = app_config["profiles"]
126
135
  end
127
- rescue StandardError
136
+ rescue StandardError => e
128
137
  # If the config file doesn't exist in the Rails app, just use the defaults
129
- Rails.logger.debug "No MCP config found in Rails app, using defaults from gem"
138
+ Rails.logger.warn "[Configuration] Failed to load MCP config: #{e.class} - #{e.message}"
139
+ # No MCP config found in Rails app, using defaults from gem
130
140
  end
131
141
 
132
142
  # Apply the active profile
@@ -178,15 +188,25 @@ module ActionMCP
178
188
  # Returns capabilities based on active profile
179
189
  def capabilities
180
190
  capabilities = {}
191
+ profile = @profiles[active_profile]
181
192
 
182
- # Only include capabilities if the corresponding filtered registry is non-empty
183
- capabilities[:tools] = { listChanged: @list_changed } if filtered_tools.any?
193
+ # Check profile configuration instead of registry contents
194
+ # If profile includes tools (either "all" or specific tools), advertise tools capability
195
+ if profile && profile[:tools] && profile[:tools].any?
196
+ capabilities[:tools] = { listChanged: @list_changed }
197
+ end
184
198
 
185
- capabilities[:prompts] = { listChanged: @list_changed } if filtered_prompts.any?
199
+ # If profile includes prompts, advertise prompts capability
200
+ if profile && profile[:prompts] && profile[:prompts].any?
201
+ capabilities[:prompts] = { listChanged: @list_changed }
202
+ end
186
203
 
187
204
  capabilities[:logging] = {} if @logging_enabled
188
205
 
189
- capabilities[:resources] = { subscribe: @resources_subscribe } if filtered_resources.any?
206
+ # If profile includes resources, advertise resources capability
207
+ if profile && profile[:resources] && profile[:resources].any?
208
+ capabilities[:resources] = { subscribe: @resources_subscribe }
209
+ end
190
210
 
191
211
  capabilities[:elicitation] = {} if @elicitation_enabled
192
212
 
@@ -214,6 +234,20 @@ module ActionMCP
214
234
  @resources_subscribe = options[:resources_subscribe] unless options[:resources_subscribe].nil?
215
235
  end
216
236
 
237
+ def eager_load_if_needed
238
+ profile = @profiles[active_profile]
239
+ return unless profile
240
+
241
+ # Check if any component type includes "all"
242
+ needs_eager_load = profile[:tools]&.include?("all") ||
243
+ profile[:prompts]&.include?("all") ||
244
+ profile[:resources]&.include?("all")
245
+
246
+ if needs_eager_load
247
+ ensure_mcp_components_loaded
248
+ end
249
+ end
250
+
217
251
  private
218
252
 
219
253
  def default_profiles
@@ -273,6 +307,16 @@ module ActionMCP
273
307
  @connects_to = app_config["connects_to"]
274
308
  end
275
309
 
310
+ # Extract verbose logging setting
311
+ if app_config.key?("verbose_logging")
312
+ @verbose_logging = app_config["verbose_logging"]
313
+ end
314
+
315
+ # Extract gateway class configuration
316
+ if app_config["gateway_class"]
317
+ @gateway_class_name = app_config["gateway_class"]
318
+ end
319
+
276
320
  # Extract session store configuration
277
321
  if app_config["session_store_type"]
278
322
  @session_store_type = app_config["session_store_type"].to_sym
@@ -303,6 +347,41 @@ module ActionMCP
303
347
  rescue LoadError
304
348
  false
305
349
  end
350
+
351
+ def ensure_mcp_components_loaded
352
+ # Only load if we haven't loaded yet - but in development, always reload
353
+ return if @mcp_components_loaded && !Rails.env.development?
354
+
355
+ # Use Zeitwerk eager loading if available (in to_prepare phase)
356
+ mcp_path = Rails.root.join("app/mcp")
357
+ if mcp_path.exist? && Rails.autoloaders.main.respond_to?(:eager_load_dir)
358
+ # This will trigger all inherited hooks properly
359
+ Rails.autoloaders.main.eager_load_dir(mcp_path)
360
+ elsif mcp_path.exist?
361
+ # Fallback for initialization phase - use require_dependency
362
+ # Load base classes first in specific order
363
+ base_files = [
364
+ mcp_path.join("application_gateway.rb"),
365
+ mcp_path.join("tools/application_mcp_tool.rb"),
366
+ mcp_path.join("prompts/application_mcp_prompt.rb"),
367
+ mcp_path.join("resource_templates/application_mcp_res_template.rb"),
368
+ # Load ArithmeticTool before other tools that inherit from it
369
+ mcp_path.join("tools/arithmetic_tool.rb")
370
+ ]
371
+
372
+ base_files.each do |file|
373
+ require_dependency file.to_s if file.exist?
374
+ end
375
+
376
+ # Then load all other files
377
+ Dir.glob(mcp_path.join("**/*.rb")).sort.each do |file|
378
+ # Skip base classes we already loaded
379
+ next if base_files.any? { |base| file == base.to_s }
380
+ require_dependency file
381
+ end
382
+ end
383
+ @mcp_components_loaded = true unless Rails.env.development?
384
+ end
306
385
  end
307
386
 
308
387
  class << self
@@ -18,8 +18,33 @@ module ActionMCP
18
18
  # Provide a configuration namespace for ActionMCP
19
19
  config.action_mcp = ActionMCP.configuration
20
20
 
21
+ # Create the ActiveSupport load hooks
22
+ ActiveSupport.on_load(:action_mcp_tool) do
23
+ # Register the tool when it's loaded
24
+ ActionMCP::ToolsRegistry.register(self) unless abstract?
25
+ end
26
+
27
+ ActiveSupport.on_load(:action_mcp_prompt) do
28
+ # Register the prompt when it's loaded
29
+ ActionMCP::PromptsRegistry.register(self) unless abstract?
30
+ end
31
+
32
+ ActiveSupport.on_load(:action_mcp_resource_template) do
33
+ # Register the resource template when it's loaded
34
+ ActionMCP::ResourceTemplatesRegistry.register(self) unless abstract?
35
+ end
36
+
21
37
  config.to_prepare do
22
- ActionMCP::ResourceTemplate.registered_templates.clear
38
+ # Only clear registries if we're in development mode
39
+ if Rails.env.development?
40
+ ActionMCP::ResourceTemplate.registered_templates.clear
41
+ ActionMCP::ToolsRegistry.clear!
42
+ ActionMCP::PromptsRegistry.clear!
43
+ end
44
+
45
+ # Eager load MCP components if profile includes "all"
46
+ # This runs after Zeitwerk is fully set up
47
+ ActionMCP.configuration.eager_load_if_needed
23
48
  end
24
49
 
25
50
  config.middleware.use JSONRPC_Rails::Middleware::Validator, [ "/" ]