Packages

fnord

0.9.13

AI code archaeology

Current section

Files

Jump to
fnord lib cmd config mcp login.ex
Raw

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