Docs/Interfaces/Inbound and outbound MCP

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.Supervisor starts and monitors one Capabilities.MCP.Server child per configured server.
  • Capabilities.MCP.Server manages the lifecycle of one outbound connection.
  • Capabilities.MCP.Caller (backed by the Anubis MCP client library) handles protocol-level calls.
  • Capabilities.MCP.Discoverer fetches the tool list (tools/list) on connect; the Server registers each result.
  • Capabilities.MCP.Naming prefixes each tool name as mcp_<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] from config.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:

  1. Add MCP.Inbound.Supervisor to the application children in the OTP application module.
  2. For the "streamable_http" transport, mount the authenticated /mcp route 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_class is external_api, which is more permissive than read_only but subject to the active sandbox profile.
  • They can be individually hidden from the LLM via hidden_from_agent = true while 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.

Next steps