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,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
@@ -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
- def self.create_client(endpoint, transport: :streamable_http, session_store: nil, session_id: nil, logger: Logger.new($stdout), **options)
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 profiles from Rails configuration
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
- # Merge with defaults so user config overrides gem defaults
92
- @profiles = app_config
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
 
@@ -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")
@@ -49,13 +49,22 @@ module ActionMCP
49
49
  protected
50
50
 
51
51
  def authenticate!
52
- token = extract_bearer_token
53
- raise UnauthorizedError, "Missing token" unless token
52
+ auth_methods = ActionMCP.configuration.authentication_methods || [ "jwt" ]
53
+
54
+ auth_methods.each do |method|
55
+ case method
56
+ when "none"
57
+ return default_user_identity
58
+ when "jwt"
59
+ result = jwt_authenticate
60
+ return result if result
61
+ when "oauth"
62
+ result = oauth_authenticate
63
+ return result if result
64
+ end
65
+ end
54
66
 
55
- payload = ActionMCP::JwtDecoder.decode(token)
56
- resolve_user(payload)
57
- rescue ActionMCP::JwtDecoder::DecodeError => e
58
- raise UnauthorizedError, e.message
67
+ raise UnauthorizedError, "No valid authentication found"
59
68
  end
60
69
 
61
70
  def extract_bearer_token
@@ -81,5 +90,85 @@ module ActionMCP
81
90
  def reject_unauthorized_connection
82
91
  raise UnauthorizedError, "Unauthorized"
83
92
  end
93
+
94
+ # Default user identity for "none" authentication
95
+ def default_user_identity
96
+ # Return a hash with all identified_by attributes set to a default user
97
+ self.class.identifiers.each_with_object({}) do |identifier, hash|
98
+ if identifier == :user
99
+ # Create or find a default user for development
100
+ hash[identifier] = find_or_create_default_user
101
+ end
102
+ # Add support for other identifiers as needed
103
+ end
104
+ end
105
+
106
+ # JWT authentication (existing implementation)
107
+ def jwt_authenticate
108
+ token = extract_bearer_token
109
+ unless token
110
+ raise UnauthorizedError, "Missing token" if ActionMCP.configuration.authentication_methods == [ "jwt" ]
111
+ return nil
112
+ end
113
+
114
+ payload = ActionMCP::JwtDecoder.decode(token)
115
+ result = resolve_user(payload)
116
+ unless result
117
+ raise UnauthorizedError, "Unauthorized" if ActionMCP.configuration.authentication_methods == [ "jwt" ]
118
+ return nil
119
+ end
120
+ result
121
+ rescue ActionMCP::JwtDecoder::DecodeError => e
122
+ if ActionMCP.configuration.authentication_methods == [ "jwt" ]
123
+ raise UnauthorizedError, "Invalid token"
124
+ else
125
+ nil # Let it try other authentication methods
126
+ end
127
+ end
128
+
129
+ # OAuth authentication via middleware
130
+ def oauth_authenticate
131
+ return nil unless oauth_enabled?
132
+
133
+ # Check if OAuth middleware has already validated the token
134
+ token_info = request.env["action_mcp.oauth_token_info"]
135
+ return nil unless token_info && token_info["active"]
136
+
137
+ resolve_user_from_oauth(token_info)
138
+ rescue ActionMCP::OAuth::Error
139
+ nil # Let it try other authentication methods
140
+ end
141
+
142
+ def oauth_enabled?
143
+ ActionMCP.configuration.authentication_methods&.include?("oauth") &&
144
+ ActionMCP.configuration.oauth_config.present?
145
+ end
146
+
147
+ def resolve_user_from_oauth(token_info)
148
+ return nil unless token_info.is_a?(Hash)
149
+
150
+ user_id = token_info["sub"] || token_info["user_id"]
151
+ return nil unless user_id
152
+
153
+ user = User.find_by(id: user_id) || User.find_by(oauth_subject: user_id)
154
+ return nil unless user
155
+
156
+ # Return a hash with all identified_by attributes
157
+ self.class.identifiers.each_with_object({}) do |identifier, hash|
158
+ hash[identifier] = user if identifier == :user
159
+ # Add support for other identifiers as needed
160
+ end
161
+ end
162
+
163
+ def find_or_create_default_user
164
+ # Only for development/testing with "none" authentication
165
+ return nil unless Rails.env.development? || Rails.env.test?
166
+
167
+ if defined?(User)
168
+ User.find_or_create_by(email: "dev@localhost") do |user|
169
+ user.name = "Development User" if user.respond_to?(:name=)
170
+ end
171
+ end
172
+ end
84
173
  end
85
174
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMCP
4
+ module OAuth
5
+ # Base OAuth error class
6
+ class Error < StandardError
7
+ attr_reader :oauth_error_code
8
+
9
+ def initialize(message, oauth_error_code = "invalid_request")
10
+ super(message)
11
+ @oauth_error_code = oauth_error_code
12
+ end
13
+ end
14
+
15
+ # OAuth 2.1 standard error types
16
+ class InvalidRequestError < Error
17
+ def initialize(message = "Invalid request")
18
+ super(message, "invalid_request")
19
+ end
20
+ end
21
+
22
+ class InvalidClientError < Error
23
+ def initialize(message = "Invalid client")
24
+ super(message, "invalid_client")
25
+ end
26
+ end
27
+
28
+ class InvalidGrantError < Error
29
+ def initialize(message = "Invalid grant")
30
+ super(message, "invalid_grant")
31
+ end
32
+ end
33
+
34
+ class UnauthorizedClientError < Error
35
+ def initialize(message = "Unauthorized client")
36
+ super(message, "unauthorized_client")
37
+ end
38
+ end
39
+
40
+ class UnsupportedGrantTypeError < Error
41
+ def initialize(message = "Unsupported grant type")
42
+ super(message, "unsupported_grant_type")
43
+ end
44
+ end
45
+
46
+ class InvalidScopeError < Error
47
+ def initialize(message = "Invalid scope")
48
+ super(message, "invalid_scope")
49
+ end
50
+ end
51
+
52
+ class InvalidTokenError < Error
53
+ def initialize(message = "Invalid token")
54
+ super(message, "invalid_token")
55
+ end
56
+ end
57
+
58
+ class InsufficientScopeError < Error
59
+ attr_reader :required_scope
60
+
61
+ def initialize(message = "Insufficient scope", required_scope = nil)
62
+ super(message, "insufficient_scope")
63
+ @required_scope = required_scope
64
+ end
65
+ end
66
+
67
+ class ServerError < Error
68
+ def initialize(message = "Server error")
69
+ super(message, "server_error")
70
+ end
71
+ end
72
+
73
+ class TemporarilyUnavailableError < Error
74
+ def initialize(message = "Temporarily unavailable")
75
+ super(message, "temporarily_unavailable")
76
+ end
77
+ end
78
+ end
79
+ end