actionmcp 0.70.0 → 0.71.1
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 -41
- data/app/controllers/action_mcp/application_controller.rb +67 -15
- 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 +22 -22
- 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/client/base.rb +3 -2
- data/lib/action_mcp/client/collection.rb +3 -3
- data/lib/action_mcp/client/jwt_client_provider.rb +134 -0
- data/lib/action_mcp/client/streamable_http_transport.rb +56 -10
- data/lib/action_mcp/client.rb +16 -4
- data/lib/action_mcp/configuration.rb +27 -4
- data/lib/action_mcp/engine.rb +7 -1
- data/lib/action_mcp/filtered_logger.rb +32 -0
- data/lib/action_mcp/gateway.rb +47 -133
- data/lib/action_mcp/gateway_identifier.rb +29 -0
- data/lib/action_mcp/jwt_identifier.rb +28 -0
- data/lib/action_mcp/none_identifier.rb +19 -0
- data/lib/action_mcp/o_auth_identifier.rb +34 -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/server/capabilities.rb +0 -3
- data/lib/action_mcp/server/resources.rb +1 -1
- data/lib/action_mcp/server/tools.rb +36 -24
- data/lib/action_mcp/sse_listener.rb +0 -7
- data/lib/action_mcp/test_helper.rb +5 -0
- data/lib/action_mcp/tool.rb +94 -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
- metadata +14 -1
@@ -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
|
@@ -23,11 +23,12 @@ module ActionMCP
|
|
23
23
|
|
24
24
|
delegate :connected?, :ready?, to: :transport
|
25
25
|
|
26
|
-
def initialize(transport:, logger: ActionMCP.logger, **options)
|
26
|
+
def initialize(transport:, logger: ActionMCP.logger, protocol_version: nil, **options)
|
27
27
|
@logger = logger
|
28
28
|
@transport = transport
|
29
29
|
@session = nil # Session will be created/loaded based on server response
|
30
30
|
@session_id = options[:session_id] # Optional session ID for resumption
|
31
|
+
@protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
|
31
32
|
@server_capabilities = nil
|
32
33
|
@connection_error = nil
|
33
34
|
@initialized = false
|
@@ -180,7 +181,7 @@ module ActionMCP
|
|
180
181
|
end
|
181
182
|
|
182
183
|
params = {
|
183
|
-
protocolVersion:
|
184
|
+
protocolVersion: @protocol_version,
|
184
185
|
capabilities: client_capabilities,
|
185
186
|
clientInfo: client_info
|
186
187
|
}
|
@@ -143,14 +143,14 @@ module ActionMCP
|
|
143
143
|
def silence_logs
|
144
144
|
return yield unless @silence_sql
|
145
145
|
|
146
|
-
original_log_level = Session.logger&.level
|
146
|
+
original_log_level = ActionMCP::Session.logger&.level
|
147
147
|
begin
|
148
148
|
# Temporarily increase log level to suppress SQL queries
|
149
|
-
Session.logger.level = Logger::WARN if Session.logger
|
149
|
+
ActionMCP::Session.logger.level = Logger::WARN if ActionMCP::Session.logger
|
150
150
|
yield
|
151
151
|
ensure
|
152
152
|
# Restore original log level
|
153
|
-
Session.logger.level = original_log_level if Session.logger
|
153
|
+
ActionMCP::Session.logger.level = original_log_level if ActionMCP::Session.logger && original_log_level
|
154
154
|
end
|
155
155
|
end
|
156
156
|
|
@@ -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
|
@@ -13,12 +13,15 @@ module ActionMCP
|
|
13
13
|
SSE_TIMEOUT = 10
|
14
14
|
ENDPOINT_TIMEOUT = 5
|
15
15
|
|
16
|
-
attr_reader :session_id, :last_event_id
|
16
|
+
attr_reader :session_id, :last_event_id, :protocol_version
|
17
17
|
|
18
|
-
def initialize(url, session_store:, session_id: nil, oauth_provider: nil, **options)
|
18
|
+
def initialize(url, session_store:, session_id: nil, oauth_provider: nil, jwt_provider: nil, protocol_version: nil, **options)
|
19
19
|
super(url, session_store: session_store, **options)
|
20
20
|
@session_id = session_id
|
21
21
|
@oauth_provider = oauth_provider
|
22
|
+
@jwt_provider = jwt_provider
|
23
|
+
@protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
|
24
|
+
@negotiated_protocol_version = nil
|
22
25
|
@last_event_id = nil
|
23
26
|
@buffer = +""
|
24
27
|
@current_event = nil
|
@@ -38,8 +41,9 @@ module ActionMCP
|
|
38
41
|
# Start SSE stream if server supports it
|
39
42
|
start_sse_stream
|
40
43
|
|
41
|
-
|
44
|
+
# Set ready first, then connected (so transport is ready when on_connect fires)
|
42
45
|
set_ready(true)
|
46
|
+
set_connected(true)
|
43
47
|
log_debug("StreamableHTTP connection established")
|
44
48
|
true
|
45
49
|
rescue StandardError => e
|
@@ -98,7 +102,15 @@ module ActionMCP
|
|
98
102
|
}
|
99
103
|
headers["mcp-session-id"] = @session_id if @session_id
|
100
104
|
headers["Last-Event-ID"] = @last_event_id if @last_event_id
|
105
|
+
|
106
|
+
# 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
|
110
|
+
|
101
111
|
headers.merge!(oauth_headers)
|
112
|
+
headers.merge!(jwt_headers)
|
113
|
+
log_debug("Final GET headers: #{headers}")
|
102
114
|
headers
|
103
115
|
end
|
104
116
|
|
@@ -108,7 +120,16 @@ module ActionMCP
|
|
108
120
|
"Accept" => "application/json, text/event-stream"
|
109
121
|
}
|
110
122
|
headers["mcp-session-id"] = @session_id if @session_id
|
123
|
+
|
124
|
+
# Add MCP-Protocol-Version header as per 2025-06-18 spec
|
125
|
+
# 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
|
129
|
+
|
111
130
|
headers.merge!(oauth_headers)
|
131
|
+
headers.merge!(jwt_headers)
|
132
|
+
log_debug("Final POST headers: #{headers}")
|
112
133
|
headers
|
113
134
|
end
|
114
135
|
|
@@ -218,6 +239,13 @@ module ActionMCP
|
|
218
239
|
def handle_json_response(response)
|
219
240
|
begin
|
220
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
|
248
|
+
|
221
249
|
handle_message(message)
|
222
250
|
rescue MultiJson::ParseError => e
|
223
251
|
log_error("Failed to parse JSON response: #{e}")
|
@@ -232,7 +260,7 @@ module ActionMCP
|
|
232
260
|
end
|
233
261
|
|
234
262
|
def handle_error_response(response)
|
235
|
-
error_msg = "HTTP #{response.status}: #{response.reason_phrase}"
|
263
|
+
error_msg = +"HTTP #{response.status}: #{response.reason_phrase}"
|
236
264
|
if response.body && !response.body.empty?
|
237
265
|
error_msg << " - #{response.body}"
|
238
266
|
end
|
@@ -280,7 +308,7 @@ module ActionMCP
|
|
280
308
|
id: @session_id,
|
281
309
|
last_event_id: @last_event_id,
|
282
310
|
session_data: {},
|
283
|
-
protocol_version:
|
311
|
+
protocol_version: @protocol_version
|
284
312
|
}
|
285
313
|
|
286
314
|
@session_store.save_session(@session_id, session_data)
|
@@ -290,20 +318,38 @@ module ActionMCP
|
|
290
318
|
def oauth_headers
|
291
319
|
return {} unless @oauth_provider&.authenticated?
|
292
320
|
|
293
|
-
@oauth_provider.authorization_headers
|
321
|
+
headers = @oauth_provider.authorization_headers
|
322
|
+
log_debug("OAuth headers: #{headers}") unless headers.empty?
|
323
|
+
headers
|
294
324
|
rescue StandardError => e
|
295
325
|
log_error("Failed to get OAuth headers: #{e.message}")
|
296
326
|
{}
|
297
327
|
end
|
298
328
|
|
299
|
-
def
|
300
|
-
return unless @
|
329
|
+
def jwt_headers
|
330
|
+
return {} unless @jwt_provider&.authenticated?
|
301
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)
|
302
341
|
# Check for OAuth challenge in WWW-Authenticate header
|
303
342
|
www_auth = response.headers["www-authenticate"]
|
304
343
|
if www_auth&.include?("Bearer")
|
305
|
-
|
306
|
-
|
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
|
307
353
|
end
|
308
354
|
end
|
309
355
|
|
data/lib/action_mcp/client.rb
CHANGED
@@ -4,6 +4,7 @@ require_relative "client/transport"
|
|
4
4
|
require_relative "client/session_store"
|
5
5
|
require_relative "client/streamable_http_transport"
|
6
6
|
require_relative "client/oauth_client_provider"
|
7
|
+
require_relative "client/jwt_client_provider"
|
7
8
|
|
8
9
|
module ActionMCP
|
9
10
|
# Creates a client appropriate for the given endpoint.
|
@@ -13,6 +14,8 @@ module ActionMCP
|
|
13
14
|
# @param session_store [Symbol] The session store type (:memory, :active_record)
|
14
15
|
# @param session_id [String] Optional session ID for resuming connections
|
15
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
|
+
# @param protocol_version [String] The MCP protocol version to use (defaults to ActionMCP::DEFAULT_PROTOCOL_VERSION)
|
16
19
|
# @param logger [Logger] The logger to use. Default is Logger.new($stdout).
|
17
20
|
# @param options [Hash] Additional options to pass to the client constructor.
|
18
21
|
#
|
@@ -46,7 +49,16 @@ module ActionMCP
|
|
46
49
|
# "http://127.0.0.1:3001/action_mcp",
|
47
50
|
# oauth_provider: oauth_provider
|
48
51
|
# )
|
49
|
-
|
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)
|
50
62
|
unless endpoint =~ %r{\Ahttps?://}
|
51
63
|
raise ArgumentError, "Only HTTP(S) endpoints are supported. STDIO and other transports are not supported."
|
52
64
|
end
|
@@ -55,11 +67,11 @@ module ActionMCP
|
|
55
67
|
store = Client::SessionStoreFactory.create(session_store, **options)
|
56
68
|
|
57
69
|
# Create transport
|
58
|
-
transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id, oauth_provider: oauth_provider, logger: logger, **options)
|
70
|
+
transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id, oauth_provider: oauth_provider, jwt_provider: jwt_provider, protocol_version: protocol_version, logger: logger, **options)
|
59
71
|
|
60
72
|
logger.info("Creating #{transport} client for endpoint: #{endpoint}")
|
61
|
-
# Pass session_id to the client
|
62
|
-
Client::Base.new(transport: transport_instance, logger: logger, session_id: session_id, **options)
|
73
|
+
# Pass session_id and protocol_version to the client
|
74
|
+
Client::Base.new(transport: transport_instance, logger: logger, session_id: session_id, protocol_version: protocol_version, **options)
|
63
75
|
end
|
64
76
|
|
65
77
|
private_class_method def self.create_transport(type, endpoint, **options)
|
@@ -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,
|
@@ -56,12 +57,13 @@ module ActionMCP
|
|
56
57
|
@logging_level = :info
|
57
58
|
@resources_subscribe = false
|
58
59
|
@elicitation_enabled = false
|
60
|
+
@verbose_logging = false
|
59
61
|
@active_profile = :primary
|
60
62
|
@profiles = default_profiles
|
61
63
|
|
62
64
|
# Authentication defaults
|
63
65
|
@authentication_methods = Rails.env.production? ? [ "jwt" ] : [ "none" ]
|
64
|
-
@oauth_config =
|
66
|
+
@oauth_config = HashWithIndifferentAccess.new
|
65
67
|
|
66
68
|
@sse_heartbeat_interval = 30
|
67
69
|
@post_response_preference = :json
|
@@ -73,6 +75,7 @@ module ActionMCP
|
|
73
75
|
|
74
76
|
# Gateway - default to ApplicationGateway if it exists, otherwise ActionMCP::Gateway
|
75
77
|
@gateway_class = defined?(::ApplicationGateway) ? ::ApplicationGateway : ActionMCP::Gateway
|
78
|
+
@gateway_class_name = nil
|
76
79
|
|
77
80
|
# Session Store
|
78
81
|
@session_store_type = Rails.env.production? ? :active_record : :volatile
|
@@ -88,6 +91,15 @@ module ActionMCP
|
|
88
91
|
@version || (has_rails_version ? Rails.application.version.to_s : "0.0.1")
|
89
92
|
end
|
90
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
|
+
|
91
103
|
# Get active profile (considering thread-local override)
|
92
104
|
def active_profile
|
93
105
|
ActionMCP.thread_profiles.value || @active_profile
|
@@ -111,7 +123,7 @@ module ActionMCP
|
|
111
123
|
|
112
124
|
# Extract OAuth configuration if present
|
113
125
|
if app_config["oauth"]
|
114
|
-
@oauth_config = app_config["oauth"]
|
126
|
+
@oauth_config = HashWithIndifferentAccess.new(app_config["oauth"])
|
115
127
|
end
|
116
128
|
|
117
129
|
# Extract other top-level configuration settings
|
@@ -121,9 +133,10 @@ module ActionMCP
|
|
121
133
|
if app_config["profiles"]
|
122
134
|
@profiles = app_config["profiles"]
|
123
135
|
end
|
124
|
-
rescue StandardError
|
136
|
+
rescue StandardError => e
|
125
137
|
# If the config file doesn't exist in the Rails app, just use the defaults
|
126
|
-
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
|
127
140
|
end
|
128
141
|
|
129
142
|
# Apply the active profile
|
@@ -294,6 +307,16 @@ module ActionMCP
|
|
294
307
|
@connects_to = app_config["connects_to"]
|
295
308
|
end
|
296
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
|
+
|
297
320
|
# Extract session store configuration
|
298
321
|
if app_config["session_store_type"]
|
299
322
|
@session_store_type = app_config["session_store_type"].to_sym
|
data/lib/action_mcp/engine.rb
CHANGED
@@ -61,9 +61,10 @@ module ActionMCP
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
-
# Configure autoloading for the mcp/tools directory
|
64
|
+
# Configure autoloading for the mcp/tools directory and identifiers
|
65
65
|
initializer "action_mcp.autoloading", before: :set_autoload_paths do |app|
|
66
66
|
mcp_path = app.root.join("app/mcp")
|
67
|
+
identifiers_path = app.root.join("app/identifiers")
|
67
68
|
|
68
69
|
if mcp_path.exist?
|
69
70
|
# First add the parent mcp directory
|
@@ -74,6 +75,11 @@ module ActionMCP
|
|
74
75
|
app.autoloaders.main.collapse(dir)
|
75
76
|
end
|
76
77
|
end
|
78
|
+
|
79
|
+
# Add identifiers directory for gateway identifiers
|
80
|
+
if identifiers_path.exist?
|
81
|
+
app.autoloaders.main.push_dir(identifiers_path, namespace: Object)
|
82
|
+
end
|
77
83
|
end
|
78
84
|
|
79
85
|
# Initialize the ActionMCP logger.
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
# Custom logger that filters out repetitive MCP requests
|
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
|
11
|
+
|
12
|
+
FILTERED_METHODS = [
|
13
|
+
"notifications/initialized",
|
14
|
+
"notifications/ping"
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
def add(severity, message = nil, progname = nil, &block)
|
18
|
+
# Filter out repetitive OAuth metadata requests
|
19
|
+
if message && message.is_a?(String)
|
20
|
+
return if FILTERED_PATHS.any? { |path| message.include?(path) && message.include?("200 OK") }
|
21
|
+
|
22
|
+
# Filter out repetitive MCP notifications
|
23
|
+
return if FILTERED_METHODS.any? { |method| message.include?(method) }
|
24
|
+
|
25
|
+
# Filter out MCP protocol version debug messages
|
26
|
+
return if message.include?("MCP-Protocol-Version header validation passed")
|
27
|
+
end
|
28
|
+
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|