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
@@ -15,11 +15,9 @@ module ActionMCP
|
|
15
15
|
|
16
16
|
attr_reader :session_id, :last_event_id, :protocol_version
|
17
17
|
|
18
|
-
def initialize(url, session_store:, session_id: nil,
|
18
|
+
def initialize(url, session_store:, session_id: nil, protocol_version: nil, **options)
|
19
19
|
super(url, session_store: session_store, **options)
|
20
20
|
@session_id = session_id
|
21
|
-
@oauth_provider = oauth_provider
|
22
|
-
@jwt_provider = jwt_provider
|
23
21
|
@protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
|
24
22
|
@negotiated_protocol_version = nil
|
25
23
|
@last_event_id = nil
|
@@ -66,9 +64,7 @@ module ActionMCP
|
|
66
64
|
end
|
67
65
|
|
68
66
|
def send_message(message)
|
69
|
-
unless ready?
|
70
|
-
raise ConnectionError, "Transport not ready"
|
71
|
-
end
|
67
|
+
raise ConnectionError, "Transport not ready" unless ready?
|
72
68
|
|
73
69
|
headers = build_post_headers
|
74
70
|
json_data = message.is_a?(String) ? message : message.to_json
|
@@ -104,12 +100,8 @@ module ActionMCP
|
|
104
100
|
headers["Last-Event-ID"] = @last_event_id if @last_event_id
|
105
101
|
|
106
102
|
# Add MCP-Protocol-Version header for GET requests when we have a negotiated version
|
107
|
-
if @negotiated_protocol_version
|
108
|
-
headers["MCP-Protocol-Version"] = @negotiated_protocol_version
|
109
|
-
end
|
103
|
+
headers["MCP-Protocol-Version"] = @negotiated_protocol_version if @negotiated_protocol_version
|
110
104
|
|
111
|
-
headers.merge!(oauth_headers)
|
112
|
-
headers.merge!(jwt_headers)
|
113
105
|
log_debug("Final GET headers: #{headers}")
|
114
106
|
headers
|
115
107
|
end
|
@@ -123,12 +115,8 @@ module ActionMCP
|
|
123
115
|
|
124
116
|
# Add MCP-Protocol-Version header as per 2025-06-18 spec
|
125
117
|
# Only include when we have a negotiated version from previous handshake
|
126
|
-
if @negotiated_protocol_version
|
127
|
-
headers["MCP-Protocol-Version"] = @negotiated_protocol_version
|
128
|
-
end
|
118
|
+
headers["MCP-Protocol-Version"] = @negotiated_protocol_version if @negotiated_protocol_version
|
129
119
|
|
130
|
-
headers.merge!(oauth_headers)
|
131
|
-
headers.merge!(jwt_headers)
|
132
120
|
log_debug("Final POST headers: #{headers}")
|
133
121
|
headers
|
134
122
|
end
|
@@ -154,6 +142,7 @@ module ActionMCP
|
|
154
142
|
@http_client.get(@url, nil, headers) do |req|
|
155
143
|
req.options.on_data = proc do |chunk, _bytes|
|
156
144
|
break if @stop_requested
|
145
|
+
|
157
146
|
process_sse_chunk(chunk)
|
158
147
|
end
|
159
148
|
end
|
@@ -182,9 +171,9 @@ module ActionMCP
|
|
182
171
|
|
183
172
|
lines.each do |line|
|
184
173
|
if line.start_with?("id:")
|
185
|
-
event_id = line[3
|
174
|
+
event_id = line[3..].strip
|
186
175
|
elsif line.start_with?("data:")
|
187
|
-
data_lines << line[5
|
176
|
+
data_lines << line[5..].strip
|
188
177
|
end
|
189
178
|
end
|
190
179
|
|
@@ -201,11 +190,9 @@ module ActionMCP
|
|
201
190
|
end
|
202
191
|
end
|
203
192
|
|
204
|
-
def handle_post_response(response,
|
193
|
+
def handle_post_response(response, _original_message)
|
205
194
|
# Extract session ID from response headers
|
206
|
-
if response.headers["mcp-session-id"]
|
207
|
-
@session_id = response.headers["mcp-session-id"]
|
208
|
-
end
|
195
|
+
@session_id = response.headers["mcp-session-id"] if response.headers["mcp-session-id"]
|
209
196
|
|
210
197
|
case response.status
|
211
198
|
when 200
|
@@ -214,7 +201,6 @@ module ActionMCP
|
|
214
201
|
# Accepted - message received, no immediate response
|
215
202
|
log_debug("Message accepted (202)")
|
216
203
|
when 401
|
217
|
-
handle_authentication_error(response)
|
218
204
|
raise AuthenticationError, "Authentication required"
|
219
205
|
when 405
|
220
206
|
# Method not allowed - server doesn't support this operation
|
@@ -237,19 +223,17 @@ module ActionMCP
|
|
237
223
|
end
|
238
224
|
|
239
225
|
def handle_json_response(response)
|
240
|
-
|
241
|
-
message = MultiJson.load(response.body)
|
242
|
-
|
243
|
-
# Check if this is an initialize response to capture negotiated protocol version
|
244
|
-
if message.is_a?(Hash) && message["result"] && message["result"]["protocolVersion"]
|
245
|
-
@negotiated_protocol_version = message["result"]["protocolVersion"]
|
246
|
-
log_debug("Negotiated protocol version: #{@negotiated_protocol_version}")
|
247
|
-
end
|
226
|
+
message = MultiJson.load(response.body)
|
248
227
|
|
249
|
-
|
250
|
-
|
251
|
-
|
228
|
+
# Check if this is an initialize response to capture negotiated protocol version
|
229
|
+
if message.is_a?(Hash) && message["result"] && message["result"]["protocolVersion"]
|
230
|
+
@negotiated_protocol_version = message["result"]["protocolVersion"]
|
231
|
+
log_debug("Negotiated protocol version: #{@negotiated_protocol_version}")
|
252
232
|
end
|
233
|
+
|
234
|
+
handle_message(message)
|
235
|
+
rescue MultiJson::ParseError => e
|
236
|
+
log_error("Failed to parse JSON response: #{e}")
|
253
237
|
end
|
254
238
|
|
255
239
|
def handle_sse_response_stream(response)
|
@@ -261,9 +245,7 @@ module ActionMCP
|
|
261
245
|
|
262
246
|
def handle_error_response(response)
|
263
247
|
error_msg = +"HTTP #{response.status}: #{response.reason_phrase}"
|
264
|
-
if response.body && !response.body.empty?
|
265
|
-
error_msg << " - #{response.body}"
|
266
|
-
end
|
248
|
+
error_msg << " - #{response.body}" if response.body && !response.body.empty?
|
267
249
|
raise ConnectionError, error_msg
|
268
250
|
end
|
269
251
|
|
@@ -315,43 +297,6 @@ module ActionMCP
|
|
315
297
|
log_debug("Saved session state")
|
316
298
|
end
|
317
299
|
|
318
|
-
def oauth_headers
|
319
|
-
return {} unless @oauth_provider&.authenticated?
|
320
|
-
|
321
|
-
headers = @oauth_provider.authorization_headers
|
322
|
-
log_debug("OAuth headers: #{headers}") unless headers.empty?
|
323
|
-
headers
|
324
|
-
rescue StandardError => e
|
325
|
-
log_error("Failed to get OAuth headers: #{e.message}")
|
326
|
-
{}
|
327
|
-
end
|
328
|
-
|
329
|
-
def jwt_headers
|
330
|
-
return {} unless @jwt_provider&.authenticated?
|
331
|
-
|
332
|
-
headers = @jwt_provider.authorization_headers
|
333
|
-
log_debug("JWT headers: #{headers}") unless headers.empty?
|
334
|
-
headers
|
335
|
-
rescue StandardError => e
|
336
|
-
log_error("Failed to get JWT headers: #{e.message}")
|
337
|
-
{}
|
338
|
-
end
|
339
|
-
|
340
|
-
def handle_authentication_error(response)
|
341
|
-
# Check for OAuth challenge in WWW-Authenticate header
|
342
|
-
www_auth = response.headers["www-authenticate"]
|
343
|
-
if www_auth&.include?("Bearer")
|
344
|
-
if @oauth_provider
|
345
|
-
log_debug("Received OAuth challenge, clearing OAuth tokens")
|
346
|
-
@oauth_provider.clear_tokens!
|
347
|
-
end
|
348
|
-
|
349
|
-
if @jwt_provider
|
350
|
-
log_debug("Received Bearer challenge, clearing JWT tokens")
|
351
|
-
@jwt_provider.clear_tokens!
|
352
|
-
end
|
353
|
-
end
|
354
|
-
end
|
355
300
|
|
356
301
|
def user_agent
|
357
302
|
"ActionMCP-StreamableHTTP/#{ActionMCP.gem_version}"
|
data/lib/action_mcp/client.rb
CHANGED
@@ -3,8 +3,6 @@
|
|
3
3
|
require_relative "client/transport"
|
4
4
|
require_relative "client/session_store"
|
5
5
|
require_relative "client/streamable_http_transport"
|
6
|
-
require_relative "client/oauth_client_provider"
|
7
|
-
require_relative "client/jwt_client_provider"
|
8
6
|
|
9
7
|
module ActionMCP
|
10
8
|
# Creates a client appropriate for the given endpoint.
|
@@ -13,8 +11,6 @@ module ActionMCP
|
|
13
11
|
# @param transport [Symbol] The transport type to use (:streamable_http, :sse for legacy)
|
14
12
|
# @param session_store [Symbol] The session store type (:memory, :active_record)
|
15
13
|
# @param session_id [String] Optional session ID for resuming connections
|
16
|
-
# @param oauth_provider [ActionMCP::Client::OauthClientProvider] Optional OAuth provider for authentication
|
17
|
-
# @param jwt_provider [ActionMCP::Client::JwtClientProvider] Optional JWT provider for authentication
|
18
14
|
# @param protocol_version [String] The MCP protocol version to use (defaults to ActionMCP::DEFAULT_PROTOCOL_VERSION)
|
19
15
|
# @param logger [Logger] The logger to use. Default is Logger.new($stdout).
|
20
16
|
# @param options [Hash] Additional options to pass to the client constructor.
|
@@ -39,26 +35,8 @@ module ActionMCP
|
|
39
35
|
# session_store: :memory
|
40
36
|
# )
|
41
37
|
#
|
42
|
-
|
43
|
-
|
44
|
-
# authorization_server_url: "https://oauth.example.com",
|
45
|
-
# redirect_url: "http://localhost:3000/callback",
|
46
|
-
# client_metadata: { client_name: "My App" }
|
47
|
-
# )
|
48
|
-
# client = ActionMCP.create_client(
|
49
|
-
# "http://127.0.0.1:3001/action_mcp",
|
50
|
-
# oauth_provider: oauth_provider
|
51
|
-
# )
|
52
|
-
#
|
53
|
-
# @example With JWT authentication
|
54
|
-
# jwt_provider = ActionMCP::Client::JwtClientProvider.new(
|
55
|
-
# token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9..."
|
56
|
-
# )
|
57
|
-
# client = ActionMCP.create_client(
|
58
|
-
# "http://127.0.0.1:3001/action_mcp",
|
59
|
-
# jwt_provider: jwt_provider
|
60
|
-
# )
|
61
|
-
def self.create_client(endpoint, transport: :streamable_http, session_store: nil, session_id: nil, oauth_provider: nil, jwt_provider: nil, protocol_version: nil, logger: Logger.new($stdout), **options)
|
38
|
+
def self.create_client(endpoint, transport: :streamable_http, session_store: nil, session_id: nil,
|
39
|
+
protocol_version: nil, logger: Logger.new($stdout), **options)
|
62
40
|
unless endpoint =~ %r{\Ahttps?://}
|
63
41
|
raise ArgumentError, "Only HTTP(S) endpoints are supported. STDIO and other transports are not supported."
|
64
42
|
end
|
@@ -67,11 +45,13 @@ module ActionMCP
|
|
67
45
|
store = Client::SessionStoreFactory.create(session_store, **options)
|
68
46
|
|
69
47
|
# Create transport
|
70
|
-
transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id,
|
48
|
+
transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id,
|
49
|
+
protocol_version: protocol_version, logger: logger, **options)
|
71
50
|
|
72
51
|
logger.info("Creating #{transport} client for endpoint: #{endpoint}")
|
73
52
|
# Pass session_id and protocol_version to the client
|
74
|
-
Client::Base.new(transport: transport_instance, logger: logger, session_id: session_id,
|
53
|
+
Client::Base.new(transport: transport_instance, logger: logger, session_id: session_id,
|
54
|
+
protocol_version: protocol_version, **options)
|
75
55
|
end
|
76
56
|
|
77
57
|
private_class_method def self.create_transport(type, endpoint, **options)
|
@@ -29,7 +29,6 @@ module ActionMCP
|
|
29
29
|
:verbose_logging,
|
30
30
|
# --- Authentication Options ---
|
31
31
|
:authentication_methods,
|
32
|
-
:oauth_config,
|
33
32
|
# --- Transport Options ---
|
34
33
|
:sse_heartbeat_interval,
|
35
34
|
:post_response_preference, # :json or :sse
|
@@ -53,7 +52,7 @@ module ActionMCP
|
|
53
52
|
|
54
53
|
def initialize
|
55
54
|
@logging_enabled = true
|
56
|
-
@list_changed =
|
55
|
+
@list_changed = true
|
57
56
|
@logging_level = :info
|
58
57
|
@resources_subscribe = false
|
59
58
|
@elicitation_enabled = false
|
@@ -61,13 +60,12 @@ module ActionMCP
|
|
61
60
|
@active_profile = :primary
|
62
61
|
@profiles = default_profiles
|
63
62
|
|
64
|
-
# Authentication defaults
|
65
|
-
@authentication_methods =
|
66
|
-
@oauth_config = HashWithIndifferentAccess.new
|
63
|
+
# Authentication defaults - empty means all configured identifiers will be tried
|
64
|
+
@authentication_methods = []
|
67
65
|
|
68
66
|
@sse_heartbeat_interval = 30
|
69
67
|
@post_response_preference = :json
|
70
|
-
@protocol_version = "2025-03-26"
|
68
|
+
@protocol_version = "2025-03-26" # Default to legacy for backwards compatibility
|
71
69
|
|
72
70
|
# Resumability defaults
|
73
71
|
@sse_event_retention_period = 15.minutes
|
@@ -93,8 +91,8 @@ module ActionMCP
|
|
93
91
|
|
94
92
|
def gateway_class
|
95
93
|
if @gateway_class_name
|
96
|
-
|
97
|
-
|
94
|
+
@gateway_class_name.constantize
|
95
|
+
|
98
96
|
else
|
99
97
|
@gateway_class
|
100
98
|
end
|
@@ -110,6 +108,9 @@ module ActionMCP
|
|
110
108
|
# First load defaults from the gem
|
111
109
|
@profiles = default_profiles
|
112
110
|
|
111
|
+
# Preserve any settings that were already set via Rails config
|
112
|
+
preserved_name = @name
|
113
|
+
|
113
114
|
# Try to load from config/mcp.yml in the Rails app using Rails.config_for
|
114
115
|
begin
|
115
116
|
app_config = Rails.application.config_for(:mcp)
|
@@ -117,21 +118,27 @@ module ActionMCP
|
|
117
118
|
raise "Invalid MCP config file" unless app_config.is_a?(Hash)
|
118
119
|
|
119
120
|
# Extract authentication configuration if present
|
120
|
-
|
121
|
-
|
122
|
-
end
|
123
|
-
|
124
|
-
# Extract OAuth configuration if present
|
125
|
-
if app_config["oauth"]
|
126
|
-
@oauth_config = HashWithIndifferentAccess.new(app_config["oauth"])
|
127
|
-
end
|
121
|
+
# Handle both symbol and string keys
|
122
|
+
@authentication_methods = Array(app_config[:authentication] || app_config["authentication"]) if app_config[:authentication] || app_config["authentication"]
|
128
123
|
|
129
124
|
# Extract other top-level configuration settings
|
130
125
|
extract_top_level_settings(app_config)
|
131
126
|
|
132
|
-
# Extract profiles configuration
|
133
|
-
|
134
|
-
|
127
|
+
# Extract profiles configuration - merge with defaults instead of replacing
|
128
|
+
# Rails.config_for returns OrderedOptions which uses symbol keys
|
129
|
+
if app_config[:profiles] || app_config["profiles"]
|
130
|
+
# Get profiles with either symbol or string key
|
131
|
+
app_profiles = app_config[:profiles] || app_config["profiles"]
|
132
|
+
|
133
|
+
# Convert to regular hash and deep symbolize keys
|
134
|
+
if app_profiles.is_a?(ActiveSupport::OrderedOptions)
|
135
|
+
app_profiles = app_profiles.to_h.deep_symbolize_keys
|
136
|
+
elsif app_profiles.respond_to?(:deep_symbolize_keys)
|
137
|
+
app_profiles = app_profiles.deep_symbolize_keys
|
138
|
+
end
|
139
|
+
|
140
|
+
Rails.logger.debug "[Configuration] Merging profiles: #{app_profiles.inspect}"
|
141
|
+
@profiles = @profiles.deep_merge(app_profiles)
|
135
142
|
end
|
136
143
|
rescue StandardError => e
|
137
144
|
# If the config file doesn't exist in the Rails app, just use the defaults
|
@@ -140,8 +147,13 @@ module ActionMCP
|
|
140
147
|
end
|
141
148
|
|
142
149
|
# Apply the active profile
|
150
|
+
Rails.logger.info "[ActionMCP] Loaded profiles: #{@profiles.keys.join(', ')}"
|
151
|
+
Rails.logger.info "[ActionMCP] Using profile: #{@active_profile}"
|
143
152
|
use_profile(@active_profile)
|
144
153
|
|
154
|
+
# Restore preserved settings
|
155
|
+
@name = preserved_name if preserved_name
|
156
|
+
|
145
157
|
self
|
146
158
|
end
|
147
159
|
|
@@ -190,22 +202,21 @@ module ActionMCP
|
|
190
202
|
capabilities = {}
|
191
203
|
profile = @profiles[active_profile]
|
192
204
|
|
205
|
+
Rails.logger.debug "[ActionMCP] Generating capabilities for profile: #{active_profile}"
|
206
|
+
Rails.logger.debug "[ActionMCP] Profile config: #{profile.inspect}"
|
207
|
+
|
193
208
|
# Check profile configuration instead of registry contents
|
194
209
|
# If profile includes tools (either "all" or specific tools), advertise tools capability
|
195
|
-
|
196
|
-
capabilities[:tools] = { listChanged: @list_changed }
|
197
|
-
end
|
210
|
+
capabilities[:tools] = { listChanged: @list_changed } if profile && profile[:tools]&.any?
|
198
211
|
|
199
212
|
# If profile includes prompts, advertise prompts capability
|
200
|
-
|
201
|
-
capabilities[:prompts] = { listChanged: @list_changed }
|
202
|
-
end
|
213
|
+
capabilities[:prompts] = { listChanged: @list_changed } if profile && profile[:prompts]&.any?
|
203
214
|
|
204
215
|
capabilities[:logging] = {} if @logging_enabled
|
205
216
|
|
206
217
|
# If profile includes resources, advertise resources capability
|
207
|
-
if profile && profile[:resources]
|
208
|
-
capabilities[:resources] = { subscribe: @resources_subscribe }
|
218
|
+
if profile && profile[:resources]&.any?
|
219
|
+
capabilities[:resources] = { subscribe: @resources_subscribe, listChanged: @list_changed }
|
209
220
|
end
|
210
221
|
|
211
222
|
capabilities[:elicitation] = {} if @elicitation_enabled
|
@@ -240,12 +251,12 @@ module ActionMCP
|
|
240
251
|
|
241
252
|
# Check if any component type includes "all"
|
242
253
|
needs_eager_load = profile[:tools]&.include?("all") ||
|
243
|
-
|
244
|
-
|
254
|
+
profile[:prompts]&.include?("all") ||
|
255
|
+
profile[:resources]&.include?("all")
|
245
256
|
|
246
|
-
|
247
|
-
|
248
|
-
|
257
|
+
return unless needs_eager_load
|
258
|
+
|
259
|
+
ensure_mcp_components_loaded
|
249
260
|
end
|
250
261
|
|
251
262
|
private
|
@@ -278,58 +289,48 @@ module ActionMCP
|
|
278
289
|
end
|
279
290
|
|
280
291
|
def extract_top_level_settings(app_config)
|
292
|
+
# Create a wrapper that handles both symbol and string keys
|
293
|
+
config = HashWithIndifferentAccess.new(app_config)
|
294
|
+
|
281
295
|
# Extract adapter configuration
|
282
|
-
if
|
296
|
+
if config["adapter"]
|
283
297
|
# This will be handled by the pub/sub system, we just store it for now
|
284
|
-
@adapter =
|
298
|
+
@adapter = config["adapter"]
|
285
299
|
end
|
286
300
|
|
287
301
|
# Extract thread pool settings
|
288
|
-
if
|
289
|
-
@min_threads = app_config["min_threads"]
|
290
|
-
end
|
302
|
+
@min_threads = config["min_threads"] if config["min_threads"]
|
291
303
|
|
292
|
-
if
|
293
|
-
@max_threads = app_config["max_threads"]
|
294
|
-
end
|
304
|
+
@max_threads = config["max_threads"] if config["max_threads"]
|
295
305
|
|
296
|
-
if
|
297
|
-
@max_queue = app_config["max_queue"]
|
298
|
-
end
|
306
|
+
@max_queue = config["max_queue"] if config["max_queue"]
|
299
307
|
|
300
308
|
# Extract polling interval for solid_cable
|
301
|
-
if
|
302
|
-
@polling_interval = app_config["polling_interval"]
|
303
|
-
end
|
309
|
+
@polling_interval = config["polling_interval"] if config["polling_interval"]
|
304
310
|
|
305
311
|
# Extract connects_to setting
|
306
|
-
if
|
307
|
-
@connects_to = app_config["connects_to"]
|
308
|
-
end
|
312
|
+
@connects_to = config["connects_to"] if config["connects_to"]
|
309
313
|
|
310
314
|
# Extract verbose logging setting
|
311
|
-
if app_config.key?("verbose_logging")
|
312
|
-
@verbose_logging = app_config["verbose_logging"]
|
313
|
-
end
|
315
|
+
@verbose_logging = config["verbose_logging"] if app_config.key?("verbose_logging")
|
314
316
|
|
315
317
|
# Extract gateway class configuration
|
316
|
-
if
|
317
|
-
|
318
|
-
|
318
|
+
@gateway_class_name = config["gateway_class"] if config["gateway_class"]
|
319
|
+
|
320
|
+
# Extract active profile setting
|
321
|
+
@active_profile = config["profile"].to_sym if config["profile"]
|
319
322
|
|
320
323
|
# Extract session store configuration
|
321
|
-
if
|
322
|
-
@session_store_type = app_config["session_store_type"].to_sym
|
323
|
-
end
|
324
|
+
@session_store_type = config["session_store_type"].to_sym if config["session_store_type"]
|
324
325
|
|
325
326
|
# Extract client and server session store types
|
326
|
-
if
|
327
|
-
@client_session_store_type =
|
327
|
+
if config["client_session_store_type"]
|
328
|
+
@client_session_store_type = config["client_session_store_type"].to_sym
|
328
329
|
end
|
329
330
|
|
330
|
-
|
331
|
-
|
332
|
-
|
331
|
+
return unless config["server_session_store_type"]
|
332
|
+
|
333
|
+
@server_session_store_type = config["server_session_store_type"].to_sym
|
333
334
|
end
|
334
335
|
|
335
336
|
def should_include_all?(type)
|
@@ -377,6 +378,7 @@ module ActionMCP
|
|
377
378
|
Dir.glob(mcp_path.join("**/*.rb")).sort.each do |file|
|
378
379
|
# Skip base classes we already loaded
|
379
380
|
next if base_files.any? { |base| file == base.to_s }
|
381
|
+
|
380
382
|
require_dependency file
|
381
383
|
end
|
382
384
|
end
|
data/lib/action_mcp/engine.rb
CHANGED
@@ -12,7 +12,6 @@ module ActionMCP
|
|
12
12
|
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
13
13
|
inflect.acronym "SSE"
|
14
14
|
inflect.acronym "MCP"
|
15
|
-
inflect.acronym "OAuth"
|
16
15
|
end
|
17
16
|
|
18
17
|
# Provide a configuration namespace for ActionMCP
|
@@ -54,12 +53,6 @@ module ActionMCP
|
|
54
53
|
ActionMCP.configuration.load_profiles
|
55
54
|
end
|
56
55
|
|
57
|
-
# Add OAuth middleware if OAuth is configured
|
58
|
-
initializer "action_mcp.oauth_middleware", after: "action_mcp.load_profiles" do
|
59
|
-
if ActionMCP.configuration.authentication_methods&.include?("oauth")
|
60
|
-
config.middleware.use ActionMCP::OAuth::Middleware
|
61
|
-
end
|
62
|
-
end
|
63
56
|
|
64
57
|
# Configure autoloading for the mcp/tools directory and identifiers
|
65
58
|
initializer "action_mcp.autoloading", before: :set_autoload_paths do |app|
|
@@ -77,9 +70,7 @@ module ActionMCP
|
|
77
70
|
end
|
78
71
|
|
79
72
|
# Add identifiers directory for gateway identifiers
|
80
|
-
if identifiers_path.exist?
|
81
|
-
app.autoloaders.main.push_dir(identifiers_path, namespace: Object)
|
82
|
-
end
|
73
|
+
app.autoloaders.main.push_dir(identifiers_path, namespace: Object) if identifiers_path.exist?
|
83
74
|
end
|
84
75
|
|
85
76
|
# Initialize the ActionMCP logger.
|
@@ -3,11 +3,7 @@
|
|
3
3
|
module ActionMCP
|
4
4
|
# Custom logger that filters out repetitive MCP requests
|
5
5
|
class FilteredLogger < ActiveSupport::Logger
|
6
|
-
FILTERED_PATHS = [
|
7
|
-
"/oauth/authorize",
|
8
|
-
"/.well-known/oauth-protected-resource",
|
9
|
-
"/.well-known/oauth-authorization-server"
|
10
|
-
].freeze
|
6
|
+
FILTERED_PATHS = [].freeze
|
11
7
|
|
12
8
|
FILTERED_METHODS = [
|
13
9
|
"notifications/initialized",
|
@@ -15,8 +11,8 @@ module ActionMCP
|
|
15
11
|
].freeze
|
16
12
|
|
17
13
|
def add(severity, message = nil, progname = nil, &block)
|
18
|
-
# Filter out
|
19
|
-
if message
|
14
|
+
# Filter out specific paths
|
15
|
+
if message.is_a?(String)
|
20
16
|
return if FILTERED_PATHS.any? { |path| message.include?(path) && message.include?("200 OK") }
|
21
17
|
|
22
18
|
# Filter out repetitive MCP notifications
|
data/lib/action_mcp/gateway.rb
CHANGED
@@ -31,21 +31,17 @@ module ActionMCP
|
|
31
31
|
def authenticate!
|
32
32
|
active_identifiers = filter_active_identifiers
|
33
33
|
|
34
|
-
if active_identifiers.empty?
|
35
|
-
raise ActionMCP::UnauthorizedError, "No authentication methods available"
|
36
|
-
end
|
34
|
+
raise ActionMCP::UnauthorizedError, "No authentication methods available" if active_identifiers.empty?
|
37
35
|
|
38
36
|
# Try identifiers in order, use the first one that succeeds
|
39
37
|
last_error = nil
|
40
38
|
active_identifiers.each do |klass|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
next
|
48
|
-
end
|
39
|
+
result = klass.new(@request).resolve
|
40
|
+
return { klass.identifier_name => result }
|
41
|
+
rescue ActionMCP::GatewayIdentifier::Unauthorized => e
|
42
|
+
last_error = e
|
43
|
+
# Try next identifier
|
44
|
+
next
|
49
45
|
end
|
50
46
|
|
51
47
|
# If we get here, all identifiers failed
|