Packages
fnord
0.9.13
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}) 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
}
# 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"}}
defp await_callback(oauth, base_url, server, state, verifier, port, timeout_ms) do
cfg = Map.put(oauth, :redirect_uri, "http://127.0.0.1:#{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