Packages
fnord
0.8.84
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.ex
defmodule Cmd.Config.MCP do
@moduledoc """
Aggregator for MCP commands. Directly handles list, check, add, update, and remove operations,
and delegates login and status commands to specialized submodules.
"""
alias Cmd.Config.Utils
@spec run(map(), list(), list()) :: :ok
def run(opts, [:mcp, :list], _unknown) do
settings = Settings.new()
cond do
opts[:effective] ->
Settings.MCP.effective_config(settings)
|> Jason.encode!(pretty: true)
|> UI.puts()
opts[:global] ->
Settings.MCP.get_config(settings, :global)
|> Jason.encode!(pretty: true)
|> UI.puts()
# project scope explicitly via --project
opts[:project] ->
# verify project configuration exists in settings
case Settings.get_project_data(settings, opts[:project]) do
nil ->
UI.error("Project not specified or not found")
_proj_data ->
# activate project context and print config
Settings.set_project(opts[:project])
Settings.MCP.get_config(settings, :project)
|> Jason.encode!(pretty: true)
|> UI.puts()
end
# default to current project in settings
true ->
case Settings.get_selected_project() do
{:ok, _proj} ->
Settings.MCP.get_config(settings, :project)
|> Jason.encode!(pretty: true)
|> UI.puts()
{:error, _} ->
UI.error("Project not specified or not found")
end
end
end
def run(opts, [:mcp, :check], _unknown) do
if opts[:project] do
Settings.set_project(opts[:project])
end
Services.MCP.start()
Services.MCP.test(with_discovery: true)
|> Cmd.Config.MCP.CheckFormatter.format_results()
end
# Unified entry for add, update, remove
def run(opts, [:mcp, action], args) when action in [:add, :update, :remove] do
if opts[:project] do
Settings.set_project(opts[:project])
end
case Utils.require_key(opts, args, :name, "Server name") do
{:error, msg} ->
UI.error(msg)
{:ok, name} ->
do_mcp_action(opts, action, name)
end
end
# Delegate login and status to submodules
def run(opts, [:mcp, :login], args) do
case Utils.require_key(opts, args, :server, "Server name") do
{:error, msg} ->
UI.error(msg)
{:ok, server} ->
Cmd.Config.MCP.Login.run(opts, [:mcp, :login], [server])
end
end
def run(opts, [:mcp, :status], args) do
case Utils.require_key(opts, args, :server, "Server name") do
{:error, msg} ->
UI.error(msg)
{:ok, server} ->
Cmd.Config.MCP.Status.run(opts, [:mcp, :status], [server])
end
end
# ----------------------------------------------------------------------------
# Helpers
# ----------------------------------------------------------------------------
@spec reserve_ephemeral_port() :: {:ok, non_neg_integer()}
defp reserve_ephemeral_port do
# Open a socket to get an ephemeral port, then close it
# The port will remain available for immediate reuse
{:ok, socket} =
:gen_tcp.listen(0, [:binary, {:ip, {127, 0, 0, 1}}, {:active, false}, {:reuseaddr, true}])
{:ok, {_addr, port}} = :inet.sockname(socket)
:gen_tcp.close(socket)
{:ok, port}
end
@spec build_server_config_from_opts(map()) :: map()
defp build_server_config_from_opts(opts) do
%{}
|> maybe_put("transport", opts[:transport] || "stdio")
|> maybe_put("command", opts[:command])
|> maybe_put("args", opts[:arg] || [])
|> maybe_put("base_url", opts[:url])
|> maybe_put("mcp_path", opts[:mcp_path])
|> maybe_put("headers", parse_kv_list(opts[:header] || []))
|> maybe_put("env", parse_kv_list(opts[:env] || []))
|> maybe_put("timeout_ms", opts[:timeout_ms])
end
@spec maybe_put(map(), String.t(), any()) :: map()
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
@spec parse_kv_list(nil | [String.t()]) :: map()
defp parse_kv_list(nil), do: %{}
defp parse_kv_list(list) when is_list(list) do
Enum.reduce(list, %{}, fn kv, acc ->
case String.split(kv, "=", parts: 2) do
[k, v] -> Map.put(acc, k, v)
_ -> acc
end
end)
end
# Internal dispatcher for MCP add/update/remove actions
defp do_mcp_action(opts, :add, name) do
base_config = build_server_config_from_opts(opts)
settings = Settings.new()
scope =
if opts[:global] do
:global
else
:project
end
# If --oauth flag present, run auto-discovery and setup
config_with_oauth =
if opts[:oauth] do
case opts[:url] do
nil ->
UI.error("--url is required when using --oauth")
exit({:shutdown, 1})
url ->
# Reserve a port for the loopback callback first
{:ok, port} = reserve_ephemeral_port()
discovery_opts = [
client_id: opts[:client_id],
client_secret: opts[:client_secret],
scope: opts[:scope],
redirect_port: port
]
case MCP.OAuth2.Discovery.discover_and_setup(url, discovery_opts) do
{:ok, oauth_config} ->
# oauth_config already includes redirect_port from discovery_opts
Map.put(base_config, "oauth", oauth_config)
{:error, :discovery_not_found} ->
UI.error(
"OAuth discovery failed (404)",
"""
Server does not support OAuth auto-discovery
Try: fnord config mcp add #{name} --url #{url} --client-id YOUR_CLIENT_ID
"""
)
exit({:shutdown, 1})
{:error, :no_registration_endpoint} ->
UI.error(
"OAuth registration not available",
"""
Server requires pre-registered client
Get a client_id from the provider, then:
fnord config mcp add #{name} --url #{url} --oauth --client-id YOUR_CLIENT_ID
"""
)
exit({:shutdown, 1})
{:error, {:incomplete_metadata, msg}} ->
UI.error(
"Invalid discovery document",
"""
#{msg}
Server OAuth configuration is incomplete. Contact server administrator.
"""
)
exit({:shutdown, 1})
{:error, reason} ->
UI.error("OAuth setup failed", inspect(reason))
exit({:shutdown, 1})
end
end
else
base_config
end
case Settings.MCP.add_server(settings, scope, name, config_with_oauth) do
{:ok, upd} ->
server_config = Settings.MCP.list_servers(upd, scope)[name]
%{name => server_config} |> Jason.encode!(pretty: true) |> UI.puts()
# If OAuth was configured, prompt user to login
if opts[:oauth] do
UI.puts("")
UI.puts(" Ready for login: fnord config mcp login #{name}")
end
{:error, :exists} ->
UI.error("Server already exists", name)
{:error, err} ->
UI.error("Add failed", inspect(err))
end
end
defp do_mcp_action(opts, :update, name) do
raw = build_server_config_from_opts(opts)
settings = Settings.new()
scope =
if opts[:global] do
:global
else
:project
end
case Settings.MCP.update_server(settings, scope, name, raw) do
{:ok, upd} ->
%{name => Settings.MCP.list_servers(upd, scope)[name]}
|> Jason.encode!(pretty: true)
|> UI.puts()
{:error, :not_found} ->
UI.error("Server not found", name)
{:error, err} ->
UI.error("Update failed", inspect(err))
end
end
defp do_mcp_action(opts, :remove, name) do
settings = Settings.new()
scope =
if opts[:global] do
:global
else
:project
end
case Settings.MCP.remove_server(settings, scope, name) do
{:ok, upd} ->
Settings.MCP.list_servers(upd, scope)
|> Jason.encode!(pretty: true)
|> UI.puts()
{:error, :not_found} ->
UI.error("Server not found", name)
end
end
end