actionmcp 0.52.2 → 0.53.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 +2 -0
- 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.rb +99 -0
- data/config/routes.rb +10 -0
- 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/config/templates/mcp.yml +71 -3
- metadata +81 -1
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "omniauth-oauth2"
|
4
|
+
|
5
|
+
module ActionMCP
|
6
|
+
module Omniauth
|
7
|
+
# MCP-specific Omniauth strategy for OAuth 2.1 authentication
|
8
|
+
# This strategy integrates with ActionMCP's configuration system and provider interface
|
9
|
+
class MCPStrategy < ::OmniAuth::Strategies::OAuth2
|
10
|
+
# Strategy name used in configuration
|
11
|
+
option :name, "mcp"
|
12
|
+
|
13
|
+
# Default OAuth options with MCP-specific settings
|
14
|
+
option :client_options, {
|
15
|
+
authorize_url: "/oauth/authorize",
|
16
|
+
token_url: "/oauth/token",
|
17
|
+
auth_scheme: :request_body
|
18
|
+
}
|
19
|
+
|
20
|
+
# OAuth 2.1 compliance - PKCE is required
|
21
|
+
option :pkce, true
|
22
|
+
|
23
|
+
# Default scopes for MCP access
|
24
|
+
option :scope, "mcp:tools mcp:resources mcp:prompts"
|
25
|
+
|
26
|
+
# Use authorization code grant flow
|
27
|
+
option :response_type, "code"
|
28
|
+
|
29
|
+
# OAuth server metadata discovery
|
30
|
+
option :discovery, true
|
31
|
+
|
32
|
+
def initialize(app, *args, &block)
|
33
|
+
super
|
34
|
+
|
35
|
+
# Load configuration from ActionMCP if available
|
36
|
+
configure_from_mcp_config if defined?(ActionMCP)
|
37
|
+
end
|
38
|
+
|
39
|
+
# User info from OAuth token response or userinfo endpoint
|
40
|
+
def raw_info
|
41
|
+
@raw_info ||= begin
|
42
|
+
if options.userinfo_url
|
43
|
+
access_token.get(options.userinfo_url).parsed
|
44
|
+
else
|
45
|
+
# Extract user info from token response or use minimal info
|
46
|
+
token_response = access_token.token
|
47
|
+
{
|
48
|
+
"sub" => access_token.params["user_id"] || access_token.token,
|
49
|
+
"scope" => access_token.params["scope"] || options.scope
|
50
|
+
}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
rescue ::OAuth2::Error => e
|
54
|
+
log(:error, "Failed to fetch user info: #{e.message}")
|
55
|
+
{}
|
56
|
+
end
|
57
|
+
|
58
|
+
# User ID for Omniauth
|
59
|
+
uid { raw_info["sub"] || raw_info["user_id"] }
|
60
|
+
|
61
|
+
# User info hash
|
62
|
+
info do
|
63
|
+
{
|
64
|
+
name: raw_info["name"],
|
65
|
+
email: raw_info["email"],
|
66
|
+
username: raw_info["username"] || raw_info["preferred_username"]
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Extra credentials and token info
|
71
|
+
extra do
|
72
|
+
{
|
73
|
+
"raw_info" => raw_info,
|
74
|
+
"scope" => access_token.params["scope"],
|
75
|
+
"token_type" => access_token.params["token_type"] || "Bearer"
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
# OAuth server metadata discovery
|
80
|
+
def discovery_info
|
81
|
+
@discovery_info ||= begin
|
82
|
+
if options.discovery && options.client_options.site
|
83
|
+
discovery_url = "#{options.client_options.site}/.well-known/oauth-authorization-server"
|
84
|
+
response = client.request(:get, discovery_url)
|
85
|
+
JSON.parse(response.body)
|
86
|
+
end
|
87
|
+
rescue StandardError => e
|
88
|
+
log(:warn, "OAuth discovery failed: #{e.message}")
|
89
|
+
{}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Override client to use discovered endpoints if available
|
94
|
+
def client
|
95
|
+
@client ||= begin
|
96
|
+
if discovery_info.any?
|
97
|
+
options.client_options.merge!(
|
98
|
+
authorize_url: discovery_info["authorization_endpoint"],
|
99
|
+
token_url: discovery_info["token_endpoint"]
|
100
|
+
) if discovery_info["authorization_endpoint"] && discovery_info["token_endpoint"]
|
101
|
+
end
|
102
|
+
super
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Token validation for API requests (not callback flow)
|
107
|
+
def self.validate_token(token, options = {})
|
108
|
+
strategy = new(nil, options)
|
109
|
+
strategy.validate_token(token)
|
110
|
+
end
|
111
|
+
|
112
|
+
def validate_token(token)
|
113
|
+
# Validate access token with OAuth server
|
114
|
+
return nil unless token
|
115
|
+
|
116
|
+
begin
|
117
|
+
response = client.request(:post, options.introspection_url || "/oauth/introspect", {
|
118
|
+
body: { token: token },
|
119
|
+
headers: { "Content-Type" => "application/x-www-form-urlencoded" }
|
120
|
+
})
|
121
|
+
|
122
|
+
token_info = JSON.parse(response.body)
|
123
|
+
return nil unless token_info["active"]
|
124
|
+
|
125
|
+
token_info
|
126
|
+
rescue StandardError => e
|
127
|
+
log(:error, "Token validation failed: #{e.message}")
|
128
|
+
nil
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
# Configure strategy from ActionMCP configuration
|
135
|
+
def configure_from_mcp_config
|
136
|
+
oauth_config = ActionMCP.configuration.oauth_config
|
137
|
+
return unless oauth_config.is_a?(Hash)
|
138
|
+
|
139
|
+
# Set client options from MCP config
|
140
|
+
if oauth_config["issuer_url"]
|
141
|
+
options.client_options[:site] = oauth_config["issuer_url"]
|
142
|
+
end
|
143
|
+
|
144
|
+
if oauth_config["client_id"]
|
145
|
+
options.client_id = oauth_config["client_id"]
|
146
|
+
end
|
147
|
+
|
148
|
+
if oauth_config["client_secret"]
|
149
|
+
options.client_secret = oauth_config["client_secret"]
|
150
|
+
end
|
151
|
+
|
152
|
+
if oauth_config["scopes_supported"]
|
153
|
+
options.scope = Array(oauth_config["scopes_supported"]).join(" ")
|
154
|
+
end
|
155
|
+
|
156
|
+
# Enable PKCE if required (OAuth 2.1 compliance)
|
157
|
+
if oauth_config["pkce_required"]
|
158
|
+
options.pkce = true
|
159
|
+
end
|
160
|
+
|
161
|
+
# Set userinfo endpoint if provided
|
162
|
+
if oauth_config["userinfo_endpoint"]
|
163
|
+
options.userinfo_url = oauth_config["userinfo_endpoint"]
|
164
|
+
end
|
165
|
+
|
166
|
+
# Set token introspection endpoint
|
167
|
+
if oauth_config["introspection_endpoint"]
|
168
|
+
options.introspection_url = oauth_config["introspection_endpoint"]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
# Register the strategy with Omniauth
|
176
|
+
OmniAuth.config.add_camelization "mcp", "MCP"
|
data/lib/action_mcp/version.rb
CHANGED
data/lib/action_mcp.rb
CHANGED
@@ -13,6 +13,10 @@ require "action_mcp/log_subscriber"
|
|
13
13
|
require "action_mcp/engine"
|
14
14
|
require "zeitwerk"
|
15
15
|
|
16
|
+
# OAuth 2.1 support via Omniauth
|
17
|
+
require "omniauth"
|
18
|
+
require "omniauth-oauth2"
|
19
|
+
|
16
20
|
lib = File.dirname(__FILE__)
|
17
21
|
|
18
22
|
Zeitwerk::Loader.for_gem.tap do |loader|
|
@@ -25,8 +29,9 @@ Zeitwerk::Loader.for_gem.tap do |loader|
|
|
25
29
|
|
26
30
|
loader.inflector.inflect("action_mcp" => "ActionMCP")
|
27
31
|
loader.inflector.inflect("sse_client" => "SSEClient")
|
28
|
-
loader.inflector.inflect("sse_server" => "SSEServer")
|
29
32
|
loader.inflector.inflect("sse_listener" => "SSEListener")
|
33
|
+
loader.inflector.inflect("oauth" => "OAuth")
|
34
|
+
loader.inflector.inflect("mcp_strategy" => "MCPStrategy")
|
30
35
|
end.setup
|
31
36
|
|
32
37
|
module ActionMCP
|
@@ -1,9 +1,41 @@
|
|
1
1
|
# ActionMCP Configuration
|
2
|
-
# This file contains configuration for the ActionMCP
|
3
|
-
#
|
2
|
+
# This file contains configuration for the ActionMCP server including
|
3
|
+
# authentication, profiles, and pub/sub system settings.
|
4
4
|
|
5
5
|
development:
|
6
|
-
#
|
6
|
+
# Authentication configuration - array of methods to try in order
|
7
|
+
authentication: ["none"] # No authentication required for development
|
8
|
+
|
9
|
+
# OAuth configuration (if using OAuth authentication)
|
10
|
+
# oauth:
|
11
|
+
# provider: "demo_oauth_provider"
|
12
|
+
# scopes_supported: ["mcp:tools", "mcp:resources", "mcp:prompts"]
|
13
|
+
# enable_dynamic_registration: true
|
14
|
+
# enable_token_revocation: true
|
15
|
+
# pkce_required: true
|
16
|
+
|
17
|
+
# MCP capability profiles
|
18
|
+
profiles:
|
19
|
+
primary:
|
20
|
+
tools: ["all"]
|
21
|
+
prompts: ["all"]
|
22
|
+
resources: ["all"]
|
23
|
+
options:
|
24
|
+
list_changed: false
|
25
|
+
logging_enabled: true
|
26
|
+
resources_subscribe: false
|
27
|
+
|
28
|
+
minimal:
|
29
|
+
tools: []
|
30
|
+
prompts: []
|
31
|
+
resources: []
|
32
|
+
options:
|
33
|
+
list_changed: false
|
34
|
+
logging_enabled: false
|
35
|
+
logging_level: :warn
|
36
|
+
resources_subscribe: false
|
37
|
+
|
38
|
+
# Pub/sub adapter configuration
|
7
39
|
adapter: simple
|
8
40
|
# Thread pool configuration (optional)
|
9
41
|
# min_threads: 5 # Minimum number of threads in the pool
|
@@ -11,10 +43,46 @@ development:
|
|
11
43
|
# max_queue: 100 # Maximum number of tasks that can be queued
|
12
44
|
|
13
45
|
test:
|
46
|
+
# JWT authentication for testing
|
47
|
+
authentication: ["jwt"]
|
48
|
+
|
49
|
+
profiles:
|
50
|
+
primary:
|
51
|
+
tools: ["all"]
|
52
|
+
prompts: ["all"]
|
53
|
+
resources: ["all"]
|
54
|
+
|
14
55
|
# Test adapter for testing
|
15
56
|
adapter: test
|
16
57
|
|
17
58
|
production:
|
59
|
+
# Multiple authentication methods - try OAuth first, fallback to JWT
|
60
|
+
authentication: ["oauth", "jwt"]
|
61
|
+
|
62
|
+
# OAuth configuration for production
|
63
|
+
oauth:
|
64
|
+
provider: "application_oauth_provider" # Your custom provider class
|
65
|
+
scopes_supported: ["mcp:tools", "mcp:resources", "mcp:prompts"]
|
66
|
+
enable_dynamic_registration: true
|
67
|
+
enable_token_revocation: true
|
68
|
+
pkce_required: true
|
69
|
+
# issuer_url: <%= ENV.fetch("OAUTH_ISSUER_URL") { "https://yourapp.com" } %>
|
70
|
+
|
71
|
+
profiles:
|
72
|
+
primary:
|
73
|
+
tools: ["all"]
|
74
|
+
prompts: ["all"]
|
75
|
+
resources: ["all"]
|
76
|
+
options:
|
77
|
+
list_changed: false
|
78
|
+
logging_enabled: true
|
79
|
+
resources_subscribe: false
|
80
|
+
|
81
|
+
external_clients:
|
82
|
+
tools: ["WeatherForecastTool"] # Limited tool access for external clients
|
83
|
+
prompts: []
|
84
|
+
resources: []
|
85
|
+
|
18
86
|
# Choose one of the following adapters:
|
19
87
|
|
20
88
|
# 1. Database-backed adapter (recommended)
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: actionmcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.53.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Abdelkader Boudih
|
@@ -107,6 +107,76 @@ dependencies:
|
|
107
107
|
- - "~>"
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: '2.10'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: omniauth
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '2.1'
|
117
|
+
type: :runtime
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '2.1'
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: omniauth-oauth2
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '1.7'
|
131
|
+
type: :runtime
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '1.7'
|
138
|
+
- !ruby/object:Gem::Dependency
|
139
|
+
name: ostruct
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
type: :runtime
|
146
|
+
prerelease: false
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: faraday
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '2.7'
|
159
|
+
type: :runtime
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - "~>"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '2.7'
|
166
|
+
- !ruby/object:Gem::Dependency
|
167
|
+
name: pkce_challenge
|
168
|
+
requirement: !ruby/object:Gem::Requirement
|
169
|
+
requirements:
|
170
|
+
- - "~>"
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
version: '1.0'
|
173
|
+
type: :runtime
|
174
|
+
prerelease: false
|
175
|
+
version_requirements: !ruby/object:Gem::Requirement
|
176
|
+
requirements:
|
177
|
+
- - "~>"
|
178
|
+
- !ruby/object:Gem::Version
|
179
|
+
version: '1.0'
|
110
180
|
description: It offers base classes and helpers for creating MCP applications, making
|
111
181
|
it easier to integrate your Ruby/Rails application with the MCP standard
|
112
182
|
email:
|
@@ -120,6 +190,8 @@ files:
|
|
120
190
|
- README.md
|
121
191
|
- Rakefile
|
122
192
|
- app/controllers/action_mcp/application_controller.rb
|
193
|
+
- app/controllers/action_mcp/oauth/endpoints_controller.rb
|
194
|
+
- app/controllers/action_mcp/oauth/metadata_controller.rb
|
123
195
|
- app/models/action_mcp.rb
|
124
196
|
- app/models/action_mcp/application_record.rb
|
125
197
|
- app/models/action_mcp/session.rb
|
@@ -131,6 +203,7 @@ files:
|
|
131
203
|
- app/models/concerns/mcp_message_inspect.rb
|
132
204
|
- config/routes.rb
|
133
205
|
- db/migrate/20250512154359_consolidated_migration.rb
|
206
|
+
- db/migrate/20250608112101_add_oauth_to_sessions.rb
|
134
207
|
- exe/actionmcp_cli
|
135
208
|
- lib/action_mcp.rb
|
136
209
|
- lib/action_mcp/base_response.rb
|
@@ -145,6 +218,8 @@ files:
|
|
145
218
|
- lib/action_mcp/client/json_rpc_handler.rb
|
146
219
|
- lib/action_mcp/client/logging.rb
|
147
220
|
- lib/action_mcp/client/messaging.rb
|
221
|
+
- lib/action_mcp/client/oauth_client_provider.rb
|
222
|
+
- lib/action_mcp/client/oauth_client_provider/memory_storage.rb
|
148
223
|
- lib/action_mcp/client/prompt_book.rb
|
149
224
|
- lib/action_mcp/client/prompts.rb
|
150
225
|
- lib/action_mcp/client/request_timeouts.rb
|
@@ -181,6 +256,11 @@ files:
|
|
181
256
|
- lib/action_mcp/jwt_decoder.rb
|
182
257
|
- lib/action_mcp/log_subscriber.rb
|
183
258
|
- lib/action_mcp/logging.rb
|
259
|
+
- lib/action_mcp/oauth/error.rb
|
260
|
+
- lib/action_mcp/oauth/memory_storage.rb
|
261
|
+
- lib/action_mcp/oauth/middleware.rb
|
262
|
+
- lib/action_mcp/oauth/provider.rb
|
263
|
+
- lib/action_mcp/omniauth/mcp_strategy.rb
|
184
264
|
- lib/action_mcp/prompt.rb
|
185
265
|
- lib/action_mcp/prompt_response.rb
|
186
266
|
- lib/action_mcp/prompts_registry.rb
|