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.
- checksums.yaml +4 -4
- data/README.md +46 -59
- data/app/controllers/action_mcp/application_controller.rb +95 -28
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +13 -13
- data/app/controllers/action_mcp/oauth/registration_controller.rb +206 -0
- data/app/models/action_mcp/oauth_client.rb +157 -0
- data/app/models/action_mcp/oauth_token.rb +141 -0
- data/app/models/action_mcp/session/message.rb +12 -12
- data/app/models/action_mcp/session/resource.rb +2 -2
- data/app/models/action_mcp/session/sse_event.rb +2 -2
- data/app/models/action_mcp/session/subscription.rb +2 -2
- data/app/models/action_mcp/session.rb +68 -43
- data/config/routes.rb +1 -0
- data/db/migrate/20250708105124_create_action_mcp_oauth_clients.rb +42 -0
- data/db/migrate/20250708105226_create_action_mcp_oauth_tokens.rb +37 -0
- data/lib/action_mcp/capability.rb +2 -0
- data/lib/action_mcp/client/json_rpc_handler.rb +9 -9
- data/lib/action_mcp/client/jwt_client_provider.rb +134 -0
- data/lib/action_mcp/configuration.rb +90 -11
- data/lib/action_mcp/engine.rb +26 -1
- data/lib/action_mcp/filtered_logger.rb +32 -0
- data/lib/action_mcp/oauth/active_record_storage.rb +183 -0
- data/lib/action_mcp/oauth/memory_storage.rb +23 -1
- data/lib/action_mcp/oauth/middleware.rb +33 -0
- data/lib/action_mcp/oauth/provider.rb +49 -13
- data/lib/action_mcp/oauth.rb +12 -0
- data/lib/action_mcp/prompt.rb +14 -0
- data/lib/action_mcp/registry_base.rb +25 -4
- data/lib/action_mcp/resource_response.rb +110 -0
- data/lib/action_mcp/resource_template.rb +30 -2
- data/lib/action_mcp/server/capabilities.rb +3 -14
- data/lib/action_mcp/server/memory_session.rb +0 -1
- data/lib/action_mcp/server/prompts.rb +8 -1
- data/lib/action_mcp/server/resources.rb +9 -6
- data/lib/action_mcp/server/tools.rb +41 -20
- data/lib/action_mcp/server.rb +6 -3
- data/lib/action_mcp/sse_listener.rb +0 -7
- data/lib/action_mcp/test_helper.rb +5 -0
- data/lib/action_mcp/tool.rb +108 -4
- data/lib/action_mcp/tools_registry.rb +3 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/generators/action_mcp/install/templates/mcp.yml +16 -16
- data/lib/tasks/action_mcp_tasks.rake +238 -0
- metadata +11 -1
@@ -4,28 +4,28 @@
|
|
4
4
|
#
|
5
5
|
# Table name: action_mcp_sessions
|
6
6
|
#
|
7
|
-
# id
|
8
|
-
# authentication_method
|
9
|
-
# client_capabilities
|
10
|
-
# client_info
|
11
|
-
# ended_at
|
12
|
-
# initialized
|
13
|
-
# messages_count
|
14
|
-
# oauth_access_token
|
15
|
-
# oauth_refresh_token
|
16
|
-
# oauth_token_expires_at
|
17
|
-
# oauth_user_context
|
18
|
-
# prompt_registry
|
19
|
-
# protocol_version
|
20
|
-
# resource_registry
|
21
|
-
# role
|
22
|
-
# server_capabilities
|
23
|
-
# server_info
|
24
|
-
# sse_event_counter
|
25
|
-
# status
|
26
|
-
# tool_registry
|
27
|
-
# created_at
|
28
|
-
# updated_at
|
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
|
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
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
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
|
-
#
|
447
|
-
self.tool_registry =
|
448
|
-
self.prompt_registry =
|
449
|
-
self.resource_registry =
|
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
|
@@ -35,12 +35,12 @@ module ActionMCP
|
|
35
35
|
|
36
36
|
def handle_notification(notification)
|
37
37
|
# Handle server notifications to client
|
38
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
#
|
183
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/action_mcp/engine.rb
CHANGED
@@ -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
|
-
|
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, [ "/" ]
|