Packages
fnord
0.8.78
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/mcp/oauth2/discovery.ex
defmodule MCP.OAuth2.Discovery do
@moduledoc """
OAuth2 server discovery and automatic configuration.
Implements RFC 8414 Authorization Server Metadata discovery.
"""
require Logger
alias MCP.OAuth2.Registration
@doc """
Discover OAuth2 configuration and set up authentication automatically.
## Parameters
- base_url: The MCP server's base URL
- opts: Configuration options
- :client_id - Use existing client_id (skip registration)
- :client_secret - Client secret (optional)
- :scope - List of scopes (default: auto-detect)
- :redirect_port - Port to use for redirect URI (for registration)
## Returns
- `{:ok, oauth_config}` - Ready-to-use OAuth configuration map
- `{:error, reason}` - Discovery or setup failed
## OAuth Config Structure
%{
"discovery_url" => String.t(),
"client_id" => String.t(),
"client_secret" => String.t() | nil,
"scopes" => [String.t()]
}
"""
@spec discover_and_setup(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def discover_and_setup(base_url, opts \\ []) do
with {:ok, discovery_url} <- build_discovery_url(base_url),
{:ok, metadata} <- fetch_metadata(discovery_url),
:ok <- validate_metadata(metadata),
{:ok, client_id, client_secret} <- ensure_client_credentials(metadata, opts),
{:ok, scopes} <- determine_scopes(metadata, opts) do
oauth_config = %{
"discovery_url" => discovery_url,
"client_id" => client_id,
"client_secret" => client_secret,
"scopes" => scopes
}
# Include redirect_port if it was provided (for exact URI matching)
oauth_with_port =
case opts[:redirect_port] do
port when is_integer(port) ->
Map.put(oauth_config, "redirect_port", port)
_ ->
oauth_config
end
log_setup_success(client_id, scopes)
{:ok, oauth_with_port}
end
end
# Build the well-known OAuth discovery URL
defp build_discovery_url(base_url) do
url =
base_url
|> String.trim_trailing("/")
|> Kernel.<>("/.well-known/oauth-authorization-server")
{:ok, url}
end
# Fetch OAuth authorization server metadata
defp fetch_metadata(discovery_url) do
MCP.Util.debug("MCP OAuth", "Fetching metadata from #{discovery_url}")
case HTTPoison.get(discovery_url, [], recv_timeout: 10_000, timeout: 10_000) do
{:ok, %{status_code: 200, body: body}} ->
case Jason.decode(body) do
{:ok, metadata} when is_map(metadata) ->
{:ok, metadata}
{:ok, _} ->
{:error, {:invalid_metadata, "Discovery response is not a JSON object"}}
{:error, reason} ->
{:error, {:invalid_json, reason}}
end
{:ok, %{status_code: 404}} ->
{:error, :discovery_not_found}
{:ok, %{status_code: code}} ->
{:error, {:http_error, code}}
{:error, reason} ->
{:error, {:network_error, reason}}
end
end
# Validate required OAuth metadata fields
defp validate_metadata(metadata) do
required = ["issuer", "authorization_endpoint", "token_endpoint"]
missing =
Enum.filter(required, fn field ->
!Map.has_key?(metadata, field) || is_nil(metadata[field])
end)
case missing do
[] ->
:ok
fields ->
{:error, {:incomplete_metadata, "Missing required fields: #{Enum.join(fields, ", ")}"}}
end
end
# Ensure we have client credentials (either provided or via registration)
defp ensure_client_credentials(metadata, opts) do
case opts[:client_id] do
nil ->
# No client_id provided, attempt dynamic registration
register_client(metadata, opts)
client_id ->
# Use provided client_id
MCP.Util.debug("MCP OAuth", "Using provided client_id: #{client_id}")
{:ok, client_id, opts[:client_secret]}
end
end
# Attempt dynamic client registration
defp register_client(%{"registration_endpoint" => endpoint}, opts) when is_binary(endpoint) do
MCP.Util.debug("MCP OAuth", "Attempting dynamic registration at #{endpoint}")
# If a redirect port is provided, use it for registration
registration_opts =
case opts[:redirect_port] do
port when is_integer(port) ->
[redirect_uris: ["http://localhost:#{port}/callback"]]
_ ->
[]
end
case Registration.register(endpoint, registration_opts) do
{:ok, %{client_id: client_id, client_secret: client_secret}} ->
{:ok, client_id, client_secret}
{:error, reason} ->
{:error, {:registration_failed, reason}}
end
end
defp register_client(_metadata, _opts) do
{:error, :no_registration_endpoint}
end
# Determine which scopes to use
defp determine_scopes(metadata, opts) do
scopes =
cond do
# User explicitly provided scopes
opts[:scope] && length(opts[:scope]) > 0 ->
MCP.Util.debug("MCP OAuth", "Using user-provided scopes: #{inspect(opts[:scope])}")
opts[:scope]
# Server supports mcp:access (recommended minimal scope)
has_scope?(metadata, "mcp:access") ->
MCP.Util.debug("MCP OAuth", "Using recommended scope: mcp:access")
["mcp:access"]
# Fall back to all supported scopes
true ->
supported = Map.get(metadata, "scopes_supported", [])
MCP.Util.debug("MCP OAuth", "Using all supported scopes: #{inspect(supported)}")
supported
end
{:ok, scopes}
end
defp has_scope?(%{"scopes_supported" => scopes}, scope) when is_list(scopes) do
scope in scopes
end
defp has_scope?(_, _), do: false
defp log_setup_success(client_id, scopes) do
# Only log in non-quiet mode (tests typically run with quiet mode)
unless UI.quiet?() do
UI.puts("✓ OAuth configured successfully")
UI.puts(" Client ID: #{client_id}")
UI.puts(" Scopes: #{Enum.join(scopes, ", ")}")
UI.puts("")
end
end
end