Toolnexus.Mcp.parse_config
Elixir · package toolnexus · SPEC §2 · elixir/lib/toolnexus/mcp.ex
def parse_config(input) when is_map(input)def parse_config(input) when is_binary(input)Reads MCP server configuration and normalises it into a flat map of server name → config. It
connects to nothing — no ports, no HTTP. That is the whole point: it is the cheap check you can
run before paying for Toolnexus.Mcp.load/1.
When to use it
Section titled “When to use it”- Validate at startup or in a test, so a typo in
mcp.jsonfails immediately rather than halfway through connecting to five servers. - Inspect or modify config before loading — filter servers by environment, inject a header, disable one in CI.
- Accept config from somewhere other than a file — a database, an env var, an API response.
Why this and not just calling load
Section titled “Why this and not just calling load”The map clause round-trips through Jason.encode!/1 and Jason.decode!/1, which normalises atom
keys to the JSON string shape — so %{mcpServers: %{...}} and %{"mcpServers" => %{...}} both
work and both give you string keys back.
Examples
Section titled “Examples”1. Parse the shared fixture from disk
Section titled “1. Parse the shared fixture from disk”This is examples/mcp.json, the fixture every port is tested against.
alias Toolnexus.Mcp
# TOOLNEXUS_REPO is set by the docs test runner; in your own code just use a path.repo = System.get_env("TOOLNEXUS_REPO") || "."fixture = Path.join([repo, "examples", "mcp.json"])
config = Mcp.parse_config(fixture)
# The `mcpServers` wrapper is unwrapped — you get the servers directly.names = config |> Map.keys() |> Enum.sort()true = names == ["everything", "example-remote"]
local = config["everything"]true = local["type"] == "local"true = local["command"] == ["npx", "-y", "@modelcontextprotocol/server-everything"]true = local["timeout"] == 30_000
remote = config["example-remote"]true = remote["url"] == "https://example.com/mcp"# Disabled servers are still returned — parsing does not filter.true = remote["enabled"] == false
IO.puts("ok: #{Enum.join(names, ", ")}")2. Wrapper spellings, raw JSON, and atom keys
Section titled “2. Wrapper spellings, raw JSON, and atom keys”mcpServers, servers and mcp all mean the same thing.
alias Toolnexus.Mcp
server = %{"type" => "local", "command" => ["echo", "hi"]}
for wrapper <- ["mcpServers", "servers", "mcp"] do config = Mcp.parse_config(%{wrapper => %{"a" => server}}) true = Map.keys(config) == ["a"] true = config["a"]["command"] == ["echo", "hi"]end
# Raw JSON in a binary is detected by the leading brace.from_json = Mcp.parse_config(~s({"mcpServers":{"a":{"type":"local","command":["echo","hi"]}}}))true = Map.keys(from_json) == ["a"]
# Atom keys are normalised to strings on the way through.from_atoms = Mcp.parse_config(%{mcpServers: %{a: %{type: "local", command: ["echo", "hi"]}}})true = Map.keys(from_atoms) == ["a"]true = from_atoms["a"]["type"] == "local"
IO.puts("ok: 3 spellings + raw json + atom keys -> a")That last case is the one to remember: you get string keys back regardless of what you put in.
3. Validate before loading, and fail loudly
Section titled “3. Validate before loading, and fail loudly”The pattern this function exists for — check the config, then decide whether to connect.
alias Toolnexus.Mcp
validate = fn raw -> config = Mcp.parse_config(raw)
Enum.reduce(config, {[], []}, fn {name, cfg}, {enabled, problems} -> # Disabled either way round: `enabled: false` or `disabled: true`. if cfg["disabled"] == true or cfg["enabled"] == false do {enabled, problems} else problem = cond do cfg["type"] == "remote" and (cfg["url"] in [nil, ""]) -> "#{name}: remote server without a url"
cfg["type"] != "remote" and (cfg["command"] in [nil, []]) -> "#{name}: local server without a command"
true -> nil end
{[name | enabled], if(problem, do: [problem | problems], else: problems)} end end) |> then(fn {e, p} -> {Enum.sort(e), Enum.sort(p)} end)end
{good_enabled, good_problems} = validate.(~s({"mcpServers":{ "ok_local":{"type":"local","command":["npx","server"]}, "ok_remote":{"type":"remote","url":"https://example.com/mcp"}, "off":{"type":"local","command":["x"],"enabled":false}}}))
true = good_enabled == ["ok_local", "ok_remote"]true = good_problems == []
{_, bad_problems} = validate.(~s({"mcpServers":{ "broken_remote":{"type":"remote"}, "broken_local":{"type":"local","command":[]}}}))
true = length(bad_problems) == 2
# Malformed JSON raises rather than returning a half-config.true = try do Mcp.parse_config(~s({not json)) false rescue _ -> true end
IO.puts("ok: #{Enum.join(good_enabled, ",")} | problems: #{length(bad_problems)}")Accepted input
Section titled “Accepted input”| Input | Behaviour |
|---|---|
"./mcp.json" |
A binary not starting with { — read from disk with File.read!/1. |
~s({...}) |
A binary starting with { — decoded directly. |
%{...} |
A map — round-tripped through JSON, normalising atom keys to strings. |
Wrapped under mcpServers, servers or mcp — all three are unwrapped.
See also
Section titled “See also”Toolnexus.Mcp.load— parse and connectToolnexus.Mcp.list_tools— what each server would exposeToolnexus.create_toolkit— takes:mcp_configin these forms