actionmcp 0.52.2 → 0.54.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 +14 -7
- data/app/controllers/action_mcp/oauth/endpoints_controller.rb +264 -0
- data/app/controllers/action_mcp/oauth/metadata_controller.rb +129 -0
- data/app/models/action_mcp/session/message.rb +12 -12
- data/app/models/action_mcp/session.rb +116 -17
- data/config/routes.rb +10 -0
- data/db/migrate/20250512154359_consolidated_migration.rb +15 -12
- data/db/migrate/20250608112101_add_oauth_to_sessions.rb +19 -0
- data/lib/action_mcp/client/oauth_client_provider/memory_storage.rb +47 -0
- data/lib/action_mcp/client/oauth_client_provider.rb +234 -0
- data/lib/action_mcp/client/streamable_http_transport.rb +25 -1
- data/lib/action_mcp/client.rb +15 -2
- data/lib/action_mcp/configuration.rb +65 -6
- data/lib/action_mcp/engine.rb +8 -0
- data/lib/action_mcp/gateway.rb +95 -6
- data/lib/action_mcp/oauth/error.rb +79 -0
- data/lib/action_mcp/oauth/memory_storage.rb +112 -0
- data/lib/action_mcp/oauth/middleware.rb +100 -0
- data/lib/action_mcp/oauth/provider.rb +390 -0
- data/lib/action_mcp/omniauth/mcp_strategy.rb +176 -0
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +6 -1
- data/lib/generators/action_mcp/install/install_generator.rb +32 -1
- data/lib/generators/action_mcp/install/templates/mcp.yml +96 -19
- metadata +81 -3
- data/lib/generators/action_mcp/config/config_generator.rb +0 -28
- data/lib/generators/action_mcp/config/templates/mcp.yml +0 -36
@@ -11,16 +11,16 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
11
11
|
t.string :status, null: false, default: 'pre_initialize'
|
12
12
|
t.datetime :ended_at, comment: 'The time the session ended'
|
13
13
|
t.string :protocol_version
|
14
|
-
t.
|
15
|
-
t.
|
16
|
-
t.
|
17
|
-
t.
|
14
|
+
t.json :server_capabilities, comment: 'The capabilities of the server'
|
15
|
+
t.json :client_capabilities, comment: 'The capabilities of the client'
|
16
|
+
t.json :server_info, comment: 'The information about the server'
|
17
|
+
t.json :client_info, comment: 'The information about the client'
|
18
18
|
t.boolean :initialized, null: false, default: false
|
19
19
|
t.integer :messages_count, null: false, default: 0
|
20
20
|
t.integer :sse_event_counter, default: 0, null: false
|
21
|
-
t.
|
22
|
-
t.
|
23
|
-
t.
|
21
|
+
t.json :tool_registry, default: []
|
22
|
+
t.json :prompt_registry, default: []
|
23
|
+
t.json :resource_registry, default: []
|
24
24
|
t.timestamps
|
25
25
|
end
|
26
26
|
end
|
@@ -36,7 +36,7 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
36
36
|
t.string :direction, null: false, comment: 'The message recipient', default: 'client'
|
37
37
|
t.string :message_type, null: false, comment: 'The type of the message'
|
38
38
|
t.string :jsonrpc_id
|
39
|
-
t.
|
39
|
+
t.json :message_json
|
40
40
|
t.boolean :is_ping, default: false, null: false, comment: 'Whether the message is a ping'
|
41
41
|
t.boolean :request_acknowledged, default: false, null: false
|
42
42
|
t.boolean :request_cancelled, null: false, default: false
|
@@ -98,15 +98,15 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
98
98
|
end
|
99
99
|
|
100
100
|
unless column_exists?(:action_mcp_sessions, :tool_registry)
|
101
|
-
add_column :action_mcp_sessions, :tool_registry, :
|
101
|
+
add_column :action_mcp_sessions, :tool_registry, :json, default: []
|
102
102
|
end
|
103
103
|
|
104
104
|
unless column_exists?(:action_mcp_sessions, :prompt_registry)
|
105
|
-
add_column :action_mcp_sessions, :prompt_registry, :
|
105
|
+
add_column :action_mcp_sessions, :prompt_registry, :json, default: []
|
106
106
|
end
|
107
107
|
|
108
108
|
unless column_exists?(:action_mcp_sessions, :resource_registry)
|
109
|
-
add_column :action_mcp_sessions, :resource_registry, :
|
109
|
+
add_column :action_mcp_sessions, :resource_registry, :json, default: []
|
110
110
|
end
|
111
111
|
end
|
112
112
|
|
@@ -132,7 +132,10 @@ class ConsolidatedMigration < ActiveRecord::Migration[8.0]
|
|
132
132
|
|
133
133
|
return unless column_exists?(:action_mcp_session_messages, :direction)
|
134
134
|
|
135
|
-
|
135
|
+
# SQLite3 doesn't support changing column comments
|
136
|
+
if connection.adapter_name.downcase != 'sqlite'
|
137
|
+
change_column_comment :action_mcp_session_messages, :direction, 'The message recipient'
|
138
|
+
end
|
136
139
|
end
|
137
140
|
|
138
141
|
private
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class AddOAuthToSessions < ActiveRecord::Migration[8.0]
|
4
|
+
def change
|
5
|
+
# Use json for all databases (PostgreSQL, SQLite3, MySQL) for consistency
|
6
|
+
json_type = :json
|
7
|
+
|
8
|
+
add_column :action_mcp_sessions, :oauth_access_token, :string
|
9
|
+
add_column :action_mcp_sessions, :oauth_refresh_token, :string
|
10
|
+
add_column :action_mcp_sessions, :oauth_token_expires_at, :datetime
|
11
|
+
add_column :action_mcp_sessions, :oauth_user_context, json_type
|
12
|
+
add_column :action_mcp_sessions, :authentication_method, :string, default: "none"
|
13
|
+
|
14
|
+
# Add indexes for performance
|
15
|
+
add_index :action_mcp_sessions, :oauth_access_token, unique: true
|
16
|
+
add_index :action_mcp_sessions, :oauth_token_expires_at
|
17
|
+
add_index :action_mcp_sessions, :authentication_method
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActionMCP
|
4
|
+
module Client
|
5
|
+
class OauthClientProvider
|
6
|
+
# Simple in-memory storage for development
|
7
|
+
# In production, use persistent storage
|
8
|
+
class MemoryStorage
|
9
|
+
def initialize
|
10
|
+
@data = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def save_tokens(tokens)
|
14
|
+
@data[:tokens] = tokens
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_tokens
|
18
|
+
@data[:tokens]
|
19
|
+
end
|
20
|
+
|
21
|
+
def clear_tokens
|
22
|
+
@data.delete(:tokens)
|
23
|
+
end
|
24
|
+
|
25
|
+
def save_code_verifier(verifier)
|
26
|
+
@data[:code_verifier] = verifier
|
27
|
+
end
|
28
|
+
|
29
|
+
def load_code_verifier
|
30
|
+
@data[:code_verifier]
|
31
|
+
end
|
32
|
+
|
33
|
+
def clear_code_verifier
|
34
|
+
@data.delete(:code_verifier)
|
35
|
+
end
|
36
|
+
|
37
|
+
def save_client_information(info)
|
38
|
+
@data[:client_information] = info
|
39
|
+
end
|
40
|
+
|
41
|
+
def load_client_information
|
42
|
+
@data[:client_information]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,234 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "pkce_challenge"
|
5
|
+
require "securerandom"
|
6
|
+
require "uri"
|
7
|
+
require "json"
|
8
|
+
|
9
|
+
module ActionMCP
|
10
|
+
module Client
|
11
|
+
# OAuth client provider for MCP client authentication
|
12
|
+
# Implements OAuth 2.1 authorization code flow with PKCE
|
13
|
+
class OauthClientProvider
|
14
|
+
class AuthenticationError < StandardError; end
|
15
|
+
class TokenExpiredError < StandardError; end
|
16
|
+
attr_reader :redirect_url, :client_metadata, :authorization_server_url
|
17
|
+
|
18
|
+
def initialize(
|
19
|
+
authorization_server_url:,
|
20
|
+
redirect_url:,
|
21
|
+
client_metadata: {},
|
22
|
+
storage: nil,
|
23
|
+
logger: ActionMCP.logger
|
24
|
+
)
|
25
|
+
@authorization_server_url = URI(authorization_server_url)
|
26
|
+
@redirect_url = URI(redirect_url)
|
27
|
+
@client_metadata = default_client_metadata.merge(client_metadata)
|
28
|
+
@storage = storage || MemoryStorage.new
|
29
|
+
@logger = logger
|
30
|
+
@http_client = build_http_client
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get current access token for authorization headers
|
34
|
+
def access_token
|
35
|
+
tokens = current_tokens
|
36
|
+
return nil unless tokens
|
37
|
+
|
38
|
+
if token_expired?(tokens)
|
39
|
+
refresh_tokens! if tokens[:refresh_token]
|
40
|
+
tokens = current_tokens
|
41
|
+
end
|
42
|
+
|
43
|
+
tokens&.dig(:access_token)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Check if client has valid authentication
|
47
|
+
def authenticated?
|
48
|
+
!access_token.nil?
|
49
|
+
end
|
50
|
+
|
51
|
+
# Start OAuth authorization flow
|
52
|
+
def start_authorization_flow(scope: nil, state: nil)
|
53
|
+
# Generate PKCE challenge
|
54
|
+
pkce = PkceChallenge.challenge
|
55
|
+
code_verifier = pkce.code_verifier
|
56
|
+
code_challenge = pkce.code_challenge
|
57
|
+
@storage.save_code_verifier(code_verifier)
|
58
|
+
|
59
|
+
# Build authorization URL
|
60
|
+
auth_params = {
|
61
|
+
response_type: "code",
|
62
|
+
client_id: client_id,
|
63
|
+
redirect_uri: @redirect_url.to_s,
|
64
|
+
code_challenge: code_challenge,
|
65
|
+
code_challenge_method: "S256"
|
66
|
+
}
|
67
|
+
auth_params[:scope] = scope if scope
|
68
|
+
auth_params[:state] = state if state
|
69
|
+
|
70
|
+
authorization_url = build_url(server_metadata[:authorization_endpoint], auth_params)
|
71
|
+
|
72
|
+
log_debug("Starting OAuth flow: #{authorization_url}")
|
73
|
+
authorization_url
|
74
|
+
end
|
75
|
+
|
76
|
+
# Complete OAuth flow with authorization code
|
77
|
+
def complete_authorization_flow(authorization_code, state: nil)
|
78
|
+
code_verifier = @storage.load_code_verifier
|
79
|
+
raise AuthenticationError, "No code verifier found" unless code_verifier
|
80
|
+
|
81
|
+
# Exchange code for tokens
|
82
|
+
token_params = {
|
83
|
+
grant_type: "authorization_code",
|
84
|
+
code: authorization_code,
|
85
|
+
redirect_uri: @redirect_url.to_s,
|
86
|
+
code_verifier: code_verifier,
|
87
|
+
client_id: client_id
|
88
|
+
}
|
89
|
+
|
90
|
+
response = @http_client.post(server_metadata[:token_endpoint]) do |req|
|
91
|
+
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
92
|
+
req.headers["Accept"] = "application/json"
|
93
|
+
req.body = URI.encode_www_form(token_params)
|
94
|
+
end
|
95
|
+
|
96
|
+
handle_token_response(response)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Refresh access token using refresh token
|
100
|
+
def refresh_tokens!
|
101
|
+
tokens = current_tokens
|
102
|
+
refresh_token = tokens&.dig(:refresh_token)
|
103
|
+
raise TokenExpiredError, "No refresh token available" unless refresh_token
|
104
|
+
|
105
|
+
token_params = {
|
106
|
+
grant_type: "refresh_token",
|
107
|
+
refresh_token: refresh_token,
|
108
|
+
client_id: client_id
|
109
|
+
}
|
110
|
+
|
111
|
+
response = @http_client.post(server_metadata[:token_endpoint]) do |req|
|
112
|
+
req.headers["Content-Type"] = "application/x-www-form-urlencoded"
|
113
|
+
req.headers["Accept"] = "application/json"
|
114
|
+
req.body = URI.encode_www_form(token_params)
|
115
|
+
end
|
116
|
+
|
117
|
+
handle_token_response(response)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Clear stored tokens (logout)
|
121
|
+
def clear_tokens!
|
122
|
+
@storage.clear_tokens
|
123
|
+
@storage.clear_code_verifier if @storage.respond_to?(:clear_code_verifier)
|
124
|
+
log_debug("Cleared OAuth tokens and code verifier")
|
125
|
+
end
|
126
|
+
|
127
|
+
# Get client information for registration
|
128
|
+
def client_information
|
129
|
+
@storage.load_client_information
|
130
|
+
end
|
131
|
+
|
132
|
+
# Save client information after registration
|
133
|
+
def save_client_information(client_info)
|
134
|
+
@storage.save_client_information(client_info)
|
135
|
+
end
|
136
|
+
|
137
|
+
# Get authorization headers for HTTP requests
|
138
|
+
def authorization_headers
|
139
|
+
token = access_token
|
140
|
+
return {} unless token
|
141
|
+
|
142
|
+
{ "Authorization" => "Bearer #{token}" }
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def current_tokens
|
148
|
+
@storage.load_tokens
|
149
|
+
end
|
150
|
+
|
151
|
+
def save_tokens(tokens)
|
152
|
+
@storage.save_tokens(tokens)
|
153
|
+
end
|
154
|
+
|
155
|
+
def token_expired?(tokens)
|
156
|
+
expires_at = tokens[:expires_at]
|
157
|
+
return false unless expires_at
|
158
|
+
|
159
|
+
Time.at(expires_at) <= Time.now + 30 # 30 second buffer
|
160
|
+
end
|
161
|
+
|
162
|
+
def client_id
|
163
|
+
client_info = client_information
|
164
|
+
client_info&.dig(:client_id) || @client_metadata[:client_id]
|
165
|
+
end
|
166
|
+
|
167
|
+
def server_metadata
|
168
|
+
@server_metadata ||= fetch_server_metadata
|
169
|
+
end
|
170
|
+
|
171
|
+
def fetch_server_metadata
|
172
|
+
well_known_url = @authorization_server_url.dup
|
173
|
+
well_known_url.path = "/.well-known/oauth-authorization-server"
|
174
|
+
|
175
|
+
response = @http_client.get(well_known_url)
|
176
|
+
unless response.success?
|
177
|
+
raise AuthenticationError, "Failed to fetch server metadata: #{response.status}"
|
178
|
+
end
|
179
|
+
|
180
|
+
JSON.parse(response.body, symbolize_names: true)
|
181
|
+
end
|
182
|
+
|
183
|
+
def handle_token_response(response)
|
184
|
+
unless response.success?
|
185
|
+
error_body = JSON.parse(response.body) rescue {}
|
186
|
+
error_msg = error_body["error_description"] || error_body["error"] || "Token request failed"
|
187
|
+
raise AuthenticationError, "#{error_msg} (#{response.status})"
|
188
|
+
end
|
189
|
+
|
190
|
+
token_data = JSON.parse(response.body, symbolize_names: true)
|
191
|
+
|
192
|
+
# Calculate token expiration
|
193
|
+
if token_data[:expires_in]
|
194
|
+
token_data[:expires_at] = Time.now.to_i + token_data[:expires_in].to_i
|
195
|
+
end
|
196
|
+
|
197
|
+
save_tokens(token_data)
|
198
|
+
log_debug("OAuth tokens obtained successfully")
|
199
|
+
token_data
|
200
|
+
end
|
201
|
+
|
202
|
+
def build_url(base_url, params)
|
203
|
+
uri = URI(base_url)
|
204
|
+
uri.query = URI.encode_www_form(params)
|
205
|
+
uri.to_s
|
206
|
+
end
|
207
|
+
|
208
|
+
def build_http_client
|
209
|
+
Faraday.new do |f|
|
210
|
+
f.headers["User-Agent"] = "ActionMCP-OAuth/#{ActionMCP.gem_version}"
|
211
|
+
f.options.timeout = 30
|
212
|
+
f.options.open_timeout = 10
|
213
|
+
f.adapter :net_http
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def default_client_metadata
|
218
|
+
{
|
219
|
+
client_name: "ActionMCP Client",
|
220
|
+
client_uri: "https://github.com/anthropics/action_mcp",
|
221
|
+
redirect_uris: [ @redirect_url.to_s ],
|
222
|
+
grant_types: [ "authorization_code", "refresh_token" ],
|
223
|
+
response_types: [ "code" ],
|
224
|
+
token_endpoint_auth_method: "none", # Public client
|
225
|
+
code_challenge_methods_supported: [ "S256" ]
|
226
|
+
}
|
227
|
+
end
|
228
|
+
|
229
|
+
def log_debug(message)
|
230
|
+
@logger.debug("[ActionMCP::OAuthClientProvider] #{message}")
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
@@ -15,9 +15,10 @@ module ActionMCP
|
|
15
15
|
|
16
16
|
attr_reader :session_id, :last_event_id
|
17
17
|
|
18
|
-
def initialize(url, session_store:, session_id: nil, **options)
|
18
|
+
def initialize(url, session_store:, session_id: nil, oauth_provider: nil, **options)
|
19
19
|
super(url, session_store: session_store, **options)
|
20
20
|
@session_id = session_id
|
21
|
+
@oauth_provider = oauth_provider
|
21
22
|
@last_event_id = nil
|
22
23
|
@buffer = +""
|
23
24
|
@current_event = nil
|
@@ -97,6 +98,7 @@ module ActionMCP
|
|
97
98
|
}
|
98
99
|
headers["mcp-session-id"] = @session_id if @session_id
|
99
100
|
headers["Last-Event-ID"] = @last_event_id if @last_event_id
|
101
|
+
headers.merge!(oauth_headers)
|
100
102
|
headers
|
101
103
|
end
|
102
104
|
|
@@ -106,6 +108,7 @@ module ActionMCP
|
|
106
108
|
"Accept" => "application/json, text/event-stream"
|
107
109
|
}
|
108
110
|
headers["mcp-session-id"] = @session_id if @session_id
|
111
|
+
headers.merge!(oauth_headers)
|
109
112
|
headers
|
110
113
|
end
|
111
114
|
|
@@ -190,6 +193,7 @@ module ActionMCP
|
|
190
193
|
# Accepted - message received, no immediate response
|
191
194
|
log_debug("Message accepted (202)")
|
192
195
|
when 401
|
196
|
+
handle_authentication_error(response)
|
193
197
|
raise AuthenticationError, "Authentication required"
|
194
198
|
when 405
|
195
199
|
# Method not allowed - server doesn't support this operation
|
@@ -283,6 +287,26 @@ module ActionMCP
|
|
283
287
|
log_debug("Saved session state")
|
284
288
|
end
|
285
289
|
|
290
|
+
def oauth_headers
|
291
|
+
return {} unless @oauth_provider&.authenticated?
|
292
|
+
|
293
|
+
@oauth_provider.authorization_headers
|
294
|
+
rescue StandardError => e
|
295
|
+
log_error("Failed to get OAuth headers: #{e.message}")
|
296
|
+
{}
|
297
|
+
end
|
298
|
+
|
299
|
+
def handle_authentication_error(response)
|
300
|
+
return unless @oauth_provider
|
301
|
+
|
302
|
+
# Check for OAuth challenge in WWW-Authenticate header
|
303
|
+
www_auth = response.headers["www-authenticate"]
|
304
|
+
if www_auth&.include?("Bearer")
|
305
|
+
log_debug("Received OAuth challenge, clearing tokens")
|
306
|
+
@oauth_provider.clear_tokens!
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
286
310
|
def user_agent
|
287
311
|
"ActionMCP-StreamableHTTP/#{ActionMCP.gem_version}"
|
288
312
|
end
|
data/lib/action_mcp/client.rb
CHANGED
@@ -3,6 +3,7 @@
|
|
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"
|
6
7
|
|
7
8
|
module ActionMCP
|
8
9
|
# Creates a client appropriate for the given endpoint.
|
@@ -11,6 +12,7 @@ module ActionMCP
|
|
11
12
|
# @param transport [Symbol] The transport type to use (:streamable_http, :sse for legacy)
|
12
13
|
# @param session_store [Symbol] The session store type (:memory, :active_record)
|
13
14
|
# @param session_id [String] Optional session ID for resuming connections
|
15
|
+
# @param oauth_provider [ActionMCP::Client::OauthClientProvider] Optional OAuth provider for authentication
|
14
16
|
# @param logger [Logger] The logger to use. Default is Logger.new($stdout).
|
15
17
|
# @param options [Hash] Additional options to pass to the client constructor.
|
16
18
|
#
|
@@ -33,7 +35,18 @@ module ActionMCP
|
|
33
35
|
# "http://127.0.0.1:3001/action_mcp",
|
34
36
|
# session_store: :memory
|
35
37
|
# )
|
36
|
-
|
38
|
+
#
|
39
|
+
# @example With OAuth authentication
|
40
|
+
# oauth_provider = ActionMCP::Client::OauthClientProvider.new(
|
41
|
+
# authorization_server_url: "https://oauth.example.com",
|
42
|
+
# redirect_url: "http://localhost:3000/callback",
|
43
|
+
# client_metadata: { client_name: "My App" }
|
44
|
+
# )
|
45
|
+
# client = ActionMCP.create_client(
|
46
|
+
# "http://127.0.0.1:3001/action_mcp",
|
47
|
+
# oauth_provider: oauth_provider
|
48
|
+
# )
|
49
|
+
def self.create_client(endpoint, transport: :streamable_http, session_store: nil, session_id: nil, oauth_provider: nil, logger: Logger.new($stdout), **options)
|
37
50
|
unless endpoint =~ %r{\Ahttps?://}
|
38
51
|
raise ArgumentError, "Only HTTP(S) endpoints are supported. STDIO and other transports are not supported."
|
39
52
|
end
|
@@ -42,7 +55,7 @@ module ActionMCP
|
|
42
55
|
store = Client::SessionStoreFactory.create(session_store, **options)
|
43
56
|
|
44
57
|
# Create transport
|
45
|
-
transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id, logger: logger, **options)
|
58
|
+
transport_instance = create_transport(transport, endpoint, session_store: store, session_id: session_id, oauth_provider: oauth_provider, logger: logger, **options)
|
46
59
|
|
47
60
|
logger.info("Creating #{transport} client for endpoint: #{endpoint}")
|
48
61
|
# Pass session_id to the client
|
@@ -25,6 +25,9 @@ module ActionMCP
|
|
25
25
|
:logging_level,
|
26
26
|
:active_profile,
|
27
27
|
:profiles,
|
28
|
+
# --- Authentication Options ---
|
29
|
+
:authentication_methods,
|
30
|
+
:oauth_config,
|
28
31
|
# --- Transport Options ---
|
29
32
|
:sse_heartbeat_interval,
|
30
33
|
:post_response_preference, # :json or :sse
|
@@ -36,9 +39,15 @@ module ActionMCP
|
|
36
39
|
:max_stored_sse_events,
|
37
40
|
# --- Gateway Options ---
|
38
41
|
:gateway_class,
|
39
|
-
:current_class,
|
40
42
|
# --- Session Store Options ---
|
41
|
-
:session_store_type
|
43
|
+
:session_store_type,
|
44
|
+
# --- Pub/Sub and Thread Pool Options ---
|
45
|
+
:adapter,
|
46
|
+
:min_threads,
|
47
|
+
:max_threads,
|
48
|
+
:max_queue,
|
49
|
+
:polling_interval,
|
50
|
+
:connects_to
|
42
51
|
|
43
52
|
def initialize
|
44
53
|
@logging_enabled = true
|
@@ -47,6 +56,11 @@ module ActionMCP
|
|
47
56
|
@resources_subscribe = false
|
48
57
|
@active_profile = :primary
|
49
58
|
@profiles = default_profiles
|
59
|
+
|
60
|
+
# Authentication defaults
|
61
|
+
@authentication_methods = Rails.env.production? ? [ "jwt" ] : [ "none" ]
|
62
|
+
@oauth_config = {}
|
63
|
+
|
50
64
|
@sse_heartbeat_interval = 30
|
51
65
|
@post_response_preference = :json
|
52
66
|
@protocol_version = "2025-03-26"
|
@@ -58,7 +72,6 @@ module ActionMCP
|
|
58
72
|
|
59
73
|
# Gateway - default to ApplicationGateway if it exists, otherwise ActionMCP::Gateway
|
60
74
|
@gateway_class = defined?(::ApplicationGateway) ? ::ApplicationGateway : ActionMCP::Gateway
|
61
|
-
@current_class = nil
|
62
75
|
|
63
76
|
# Session Store
|
64
77
|
@session_store_type = Rails.env.production? ? :active_record : :volatile
|
@@ -77,7 +90,7 @@ module ActionMCP
|
|
77
90
|
ActionMCP.thread_profiles.value || @active_profile
|
78
91
|
end
|
79
92
|
|
80
|
-
# Load custom
|
93
|
+
# Load custom configuration from Rails configuration
|
81
94
|
def load_profiles
|
82
95
|
# First load defaults from the gem
|
83
96
|
@profiles = default_profiles
|
@@ -88,8 +101,23 @@ module ActionMCP
|
|
88
101
|
|
89
102
|
raise "Invalid MCP config file" unless app_config.is_a?(Hash)
|
90
103
|
|
91
|
-
#
|
92
|
-
|
104
|
+
# Extract authentication configuration if present
|
105
|
+
if app_config["authentication"]
|
106
|
+
@authentication_methods = Array(app_config["authentication"])
|
107
|
+
end
|
108
|
+
|
109
|
+
# Extract OAuth configuration if present
|
110
|
+
if app_config["oauth"]
|
111
|
+
@oauth_config = app_config["oauth"]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Extract other top-level configuration settings
|
115
|
+
extract_top_level_settings(app_config)
|
116
|
+
|
117
|
+
# Extract profiles configuration
|
118
|
+
if app_config["profiles"]
|
119
|
+
@profiles = app_config["profiles"]
|
120
|
+
end
|
93
121
|
rescue StandardError
|
94
122
|
# If the config file doesn't exist in the Rails app, just use the defaults
|
95
123
|
Rails.logger.debug "No MCP config found in Rails app, using defaults from gem"
|
@@ -197,6 +225,37 @@ module ActionMCP
|
|
197
225
|
}
|
198
226
|
end
|
199
227
|
|
228
|
+
def extract_top_level_settings(app_config)
|
229
|
+
# Extract adapter configuration
|
230
|
+
if app_config["adapter"]
|
231
|
+
# This will be handled by the pub/sub system, we just store it for now
|
232
|
+
@adapter = app_config["adapter"]
|
233
|
+
end
|
234
|
+
|
235
|
+
# Extract thread pool settings
|
236
|
+
if app_config["min_threads"]
|
237
|
+
@min_threads = app_config["min_threads"]
|
238
|
+
end
|
239
|
+
|
240
|
+
if app_config["max_threads"]
|
241
|
+
@max_threads = app_config["max_threads"]
|
242
|
+
end
|
243
|
+
|
244
|
+
if app_config["max_queue"]
|
245
|
+
@max_queue = app_config["max_queue"]
|
246
|
+
end
|
247
|
+
|
248
|
+
# Extract polling interval for solid_cable
|
249
|
+
if app_config["polling_interval"]
|
250
|
+
@polling_interval = app_config["polling_interval"]
|
251
|
+
end
|
252
|
+
|
253
|
+
# Extract connects_to setting
|
254
|
+
if app_config["connects_to"]
|
255
|
+
@connects_to = app_config["connects_to"]
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
200
259
|
def should_include_all?(type)
|
201
260
|
return false unless @profiles[active_profile]
|
202
261
|
|
data/lib/action_mcp/engine.rb
CHANGED
@@ -12,6 +12,7 @@ module ActionMCP
|
|
12
12
|
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
13
13
|
inflect.acronym "SSE"
|
14
14
|
inflect.acronym "MCP"
|
15
|
+
inflect.acronym "OAuth"
|
15
16
|
end
|
16
17
|
|
17
18
|
# Provide a configuration namespace for ActionMCP
|
@@ -28,6 +29,13 @@ module ActionMCP
|
|
28
29
|
ActionMCP.configuration.load_profiles
|
29
30
|
end
|
30
31
|
|
32
|
+
# Add OAuth middleware if OAuth is configured
|
33
|
+
initializer "action_mcp.oauth_middleware", after: "action_mcp.load_profiles" do
|
34
|
+
if ActionMCP.configuration.authentication_methods&.include?("oauth")
|
35
|
+
config.middleware.use ActionMCP::OAuth::Middleware
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
31
39
|
# Configure autoloading for the mcp/tools directory
|
32
40
|
initializer "action_mcp.autoloading", before: :set_autoload_paths do |app|
|
33
41
|
mcp_path = app.root.join("app/mcp")
|