# 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](https://modelcontextprotocol.io): 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](/docs/capabilities-and-tools), 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.

```toml
[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](/docs/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](/docs/capabilities-and-tools). For the relevant `config.toml` keys and environment variable overrides, see [configuration](/docs/configuration).
