Inbound and outbound MCP
How Fermix connects to outside tool servers over MCP (the Model Context Protocol) to gain new tools (outbound), and how it can offer its own tools to other MCP apps (inbound, built but not yet turned on).
MCP (the Model Context Protocol) is an open standard for connecting AI agents to external tools. This page covers how Fermix uses it in both directions. Fermix implements both sides of the Model Context Protocol: outbound (Fermix as a client connecting to external MCP servers and surfacing their tools through the capability registry, Fermix’s internal catalog of tools the agent can use) and inbound (Fermix as a server exposing its own capabilities to other MCP clients). Outbound ships and runs on every boot. Inbound is fully implemented but not yet started by default.
Outbound MCP
When you configure one or more MCP servers in config.toml, Fermix runs a dedicated background process per server, restarting it if it crashes. On connect, it calls tools/list (the MCP request that asks a server what tools it offers), registers each discovered tool in the capability registry, and makes those tools available to the agent alongside built-in tools.
The pieces involved are:
Capabilities.MCP.Supervisorstarts and monitors oneCapabilities.MCP.Serverchild per configured server.Capabilities.MCP.Servermanages the lifecycle of one outbound connection.Capabilities.MCP.Caller(backed by the Anubis MCP client library) handles protocol-level calls.Capabilities.MCP.Discovererfetches the tool list (tools/list) on connect; theServerregisters each result.Capabilities.MCP.Namingprefixes each tool name asmcp_<server>_<tool>so names from different servers cannot collide.
Configuring MCP servers
Add [mcp.servers.<name>] blocks to ~/.fermix/config.toml. Each block maps to one supervised server process.
[mcp.servers.gmail]
command = "npx"
args = ["-y", "@modelcontextprotocol/server-gmail"]
env = { LOG_LEVEL = "info" }
pass_env = ["HOME", "PATH", "GMAIL_CREDENTIALS"]
[mcp.servers.gmail.tools.list_messages]
policy_class = "read_only"
hidden_from_agent = false
[mcp.servers.gmail.tools.send_message]
policy_class = "external_api"
hidden_from_agent = true
The top-level [mcp.servers.<name>] keys are:
| Key | Type | Description |
|---|---|---|
command |
string | Executable to launch the MCP server process |
args |
list of strings | Arguments passed to command |
env |
map | Environment variables injected into the child process. Values are passed through verbatim — there is no @keyring or other secret resolution on this path, so use env only for non-secret values (a literal "@keyring" would reach the server as the literal string). For a secret, declare it in [sandbox.env] and forward it with pass_env. A "$env:VAR" value here is rejected at load with a message pointing you to pass_env, and naming the same variable in both env and pass_env is an error. |
pass_env |
list of strings | Environment variable names forwarded into the child process from the daemon’s resolved environment (host env / [sandbox.env] source = "command" helpers). This is how secrets reach an MCP server — e.g. pass_env = ["GMAIL_CREDENTIALS"] forwards the resolved value. |
Per-tool overrides live under [mcp.servers.<name>.tools.<tool_name>]:
| Key | Type | Description |
|---|---|---|
policy_class |
string | Which permission rules (the sandbox policy, the limits on what a tool may touch) apply when the tool is called. See capabilities and tools. Defaults to external_api. |
hidden_from_agent |
bool | When true, the tool is registered but not included in the tool list sent to the model. Defaults to false. |
Discovered tools that have no explicit sub-block default to policy_class: external_api and hidden_from_agent: false.
Discovery and retry
If tools/list fails on first connect, the server retries with exponential backoff — up to five attempts in total, with waits of roughly 0.5s, 1s, 2s, then 4s between them. Tools only appear in the registry once discovery succeeds, so there is a window of up to roughly 8 seconds at boot during which the agent may see fewer MCP tools than configured while discovery is still settling.
If all five attempts fail, Fermix gives up on that server: its process — and the server subprocess it launched — is shut down and quarantined rather than respawned, until the next configuration or plugin change, when Fermix re-derives the server list and tries it again. A server that connected successfully and later crashes (a transport blip) is still restarted and re-runs discovery.
Inbound MCP
The inbound stack is fully implemented but the supervisor is not included in the application’s child list and is therefore not started. No changes to config.toml alone will enable it in the current release.
When wired, the inbound server will expose a filtered view of the capability registry to any MCP client such as Claude Desktop or Cursor.
The relevant modules are:
MCP.Inbound.Supervisor: supervisor for the inbound stack (not yet added to application children).MCP.Inbound.Server: Anubis-based MCP server accepting client connections.MCP.Inbound.Config: parses[mcp.inbound]fromconfig.toml.MCP.Inbound.Exposure: the rule that decides which registry tools to expose, according to operator policy.MCP.Inbound.CapabilityPort: bridges the inbound server to the registry.
Inbound configuration defaults
The [mcp.inbound] block is parsed but not acted upon until the supervisor is wired. The defaults are intentionally tight:
Write these as plain TOML — quoted strings and [ ... ] arrays, e.g. transport = "stdio" and expose_kinds = ["builtin"].
| Key | Default | Notes |
|---|---|---|
enabled |
false |
Must be set to true explicitly |
transport |
"stdio" |
How clients connect: "stdio" talks over the program’s standard input/output (for a client that launches Fermix directly). Alternative: "streamable_http" (connect over HTTP). |
expose_kinds |
["builtin"] |
Which kinds of tool may be exposed ("builtin", "skill", "mcp"). Skills and tools sourced from other MCP servers are excluded by default. |
expose_policy_classes |
["read_only"] |
Only the most restrictive policy class is exposed by default |
allowed_tools |
[] |
Empty list means deny all. You must name each tool explicitly to expose it. |
denied_tools |
[] |
Tools to block even if otherwise permitted |
http.path |
"/mcp" |
HTTP endpoint path ("streamable_http" transport only) |
http.auth_token |
(required) | Must be set when transport = "streamable_http" |
Individual tools can be overridden under [mcp.inbound.tools.<name>]: exposed = true force-exposes a tool even if the filters above would exclude it, exposed = false hides one, and description_override replaces the description sent to MCP clients. Three more keys set the advertised server identity and timeout: server_name (default "fermix"), server_version (defaults to the running Fermix version), and request_timeout_ms (default 30000).
allowed_tools: [] being deny-all is deliberate: nothing is exposed to outside MCP clients until you name it, so the inbound surface can’t accidentally hand out write-capable tools such as file_write or git_write once it is wired.
Wiring inbound MCP manually
To enable inbound MCP before it ships as a supported feature:
- Add
MCP.Inbound.Supervisorto the application children in the OTP application module. - For the
"streamable_http"transport, mount the authenticated/mcproute in the Phoenix router.
There is no shipped CLI entry point for inbound MCP in this release — no fermix mcp verb exists — so starting it requires the source-level wiring above. None of these steps are performed by fermix setup in the current release.
How MCP tools appear alongside built-ins
All MCP tools discovered through outbound connections are registered in the same FermixCore.Capabilities.Registry as built-in tools and skills. From the agent’s perspective they are ordinary capabilities, except:
- Their names are namespaced:
mcp_<server>_<tool>. - Their default
policy_classisexternal_api, which is more permissive thanread_onlybut subject to the active sandbox profile. - They can be individually hidden from the LLM via
hidden_from_agent = truewhile remaining callable if the operator invokes them by name through a skill.
Tool-schema deferral
By default, tool-schema deferral is enabled ([fermix_core.tools.tool_search] enabled absent means true). Deferral means Fermix does not send the model the full definition of every tool up front; instead the model looks them up only when needed. When it is on, MCP tool schemas (the detailed descriptions of each tool’s inputs) are left out of requests to the provider to save tokens. Tool names remain listed in the prompt. Three bridge tools are registered instead:
tool_search: keyword search over the deferred tool catalog by capability description.tool_describe: fetches one tool’s full schema on demand.tool_call: invokes a deferred tool by name; the agent loop unwraps the call so traces and policy enforcement see the real tool name. Direct calls by name also work.
Set enabled = false to disable deferral: all MCP tool schemas are then inlined in every request.
For the full list of policy classes and how the registry applies them, see capabilities and tools. For the relevant config.toml keys and environment variable overrides, see configuration.