Packages
fnord
0.9.40
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/cmd/config/mcp/login.ex
defmodule Cmd.Config.MCP.Login do
@moduledoc """
MCP OAuth2 login entrypoint under the config namespace.
Performs the Authorization Code + PKCE flow using the configured adapter,
opens the authorization URL in a browser, awaits the loopback callback, and
persists tokens in the credentials store.
"""
@spec run(map(), list(), list()) :: :ok
def run(opts, [:mcp, :login], [server]) when is_binary(server) do
if opts[:project] do
Settings.set_project(opts[:project])
end
settings = Settings.new()
cfgs = Settings.MCP.effective_config(settings)
with {:ok, srv_cfg} <- fetch_server(cfgs, server),
{:ok, oauth} <- fetch_oauth(cfgs[server]),
{:ok, port, state, verifier, auth_url, _redirect_uri} <-
adapter().start_flow(oauth) do
# Open browser for authentication
UI.info("Opening browser for authentication...")
case browser().open(auth_url) do
:ok ->
UI.info("Waiting for authorization callback...")
case await_callback(
oauth,
srv_cfg["base_url"],
server,
state,
verifier,
port,
opts[:timeout] || 120_000
) do
{:ok, _token} ->
UI.info("Authentication successful!")
# Brief delay to allow HTTP response to be sent to browser before process exits
Process.sleep(3000)
# Start MCP supervisor and check server status
UI.info("Checking server status...")
Services.MCP.start()
Process.sleep(2000)
UI.puts("")
case Services.MCP.check_single_server(server) do
{:ok, status} ->
Cmd.Config.MCP.CheckFormatter.format_results(status)
{:error, :not_found} ->
# Fallback if server check fails (shouldn't happen)
UI.info("Authentication successful", server)
end
:ok
{:error, reason} ->
handle_auth_error(reason)
end
error ->
handle_auth_error(error)
end
else
{:error, :not_found} ->
UI.error("Server not found in config", server)
{:error, {:oauth_missing, reason}} ->
UI.error("OAuth configuration error", reason)
end
end
defp handle_auth_error(:timeout) do
UI.error(
"Timed out waiting for authorization callback",
"The OAuth flow did not complete within the timeout period. Please try again."
)
end
defp handle_auth_error({:provider_rejected_redirect, uri}) do
UI.error("OAuth provider rejected redirect_uri", uri)
end
defp handle_auth_error({:network_error, %HTTPoison.Error{reason: :nxdomain}}) do
UI.error(
"No internet connection",
"Unable to resolve DNS for OAuth provider. Check your network connection."
)
end
defp handle_auth_error(error) do
UI.error("Authentication error", inspect(error))
end
defp fetch_server(cfgs, server) do
case Map.fetch(cfgs, server) do
{:ok, cfg} -> {:ok, cfg}
:error -> {:error, :not_found}
end
end
defp fetch_oauth(%{"oauth" => oauth} = srv_cfg) when is_map(oauth) do
with discovery when is_binary(discovery) <- Map.get(oauth, "discovery_url"),
client_id when is_binary(client_id) <- Map.get(oauth, "client_id"),
scopes when is_list(scopes) <- Map.get(oauth, "scopes") do
oauth_map = %{
discovery_url: discovery,
client_id: client_id,
client_secret: Map.get(oauth, "client_secret"),
scopes: scopes,
# RFC 8707 resource indicator: the MCP server's own URL. Servers
# like Linear bind the grant to this and reject flows without it.
# Sent for any transport whose config carries a base_url (http AND
# websocket - a wss:// URL is a legal absolute URI and identifies
# the resource the grant is for); nil when absent (stdio), in which
# case the Client omits the param.
resource: Map.get(srv_cfg, "base_url")
}
# Include redirect_port if present (for exact URI matching with OAuth provider)
oauth_with_port =
case Map.get(oauth, "redirect_port") do
port when is_integer(port) -> Map.put(oauth_map, :redirect_port, port)
_ -> oauth_map
end
{:ok, oauth_with_port}
else
_ ->
{:error, {:oauth_missing, "OAuth config requires discovery_url, client_id, and scopes"}}
end
end
defp fetch_oauth(_), do: {:error, {:oauth_missing, "No OAuth config for this server"}}
# The token exchange must present the byte-identical redirect_uri used on
# the authorization request (RFC 6749 ยง4.1.3). The adapter and dynamic
# registration both use the `localhost` host form, so the exchange must
# too - `127.0.0.1` here would be rejected as a redirect_uri mismatch even
# though both resolve to the same loopback.
defp await_callback(oauth, base_url, server, state, verifier, port, timeout_ms) do
cfg = Map.put(oauth, :redirect_uri, "http://localhost:#{port}/callback")
MCP.OAuth2.Loopback.run(cfg, base_url, server, state, verifier, port, timeout_ms)
end
defp adapter, do: Application.get_env(:fnord, :mcp_oauth_adapter, MCP.OAuth2.Adapter.Default)
defp browser, do: Application.get_env(:fnord, :browser, Browser.Default)
end