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.
@@ -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"
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "gem_version"
4
4
  module ActionMCP
5
- VERSION = "0.52.2"
5
+ VERSION = "0.53.0"
6
6
 
7
7
  class << self
8
8
  alias version gem_version
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 pub/sub system.
3
- # Different environments can use different adapters.
2
+ # This file contains configuration for the ActionMCP server including
3
+ # authentication, profiles, and pub/sub system settings.
4
4
 
5
5
  development:
6
- # In-memory adapter for development
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.52.2
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