MCP Configuration

Where the files live

FileLocation
User sidecar configmcp.yaml in the profile config directory — $XDG_CONFIG_HOME/sonny when set, otherwise the platform-native location
Project chain.sonny/mcp.yaml in every directory from the cwd up to the filesystem root
Allowlistmcp.allowlist.yaml, in the same profile config directory as the sidecar config

--mcp-config <path> on sonny run, sonny mcp list, sonny tools list and sonny recipe resolve overrides the user sidecar path for that invocation only. It does not move the project chain or the allowlist.

How layers merge

Four sources merge, then two gates run, in this exact order:

#LayerConflict rule
1Your user mcp.yamlthe base
2Pack-registry servers (the mcp_d/ directories of your recipe sources)only servers the active recipe’s mcp.allowed names explicitly join; "*" pulls none; each replacement is announced at startup, and a name two sources both define loads from neither
3The .sonny/mcp.yaml project chainouter-first; a deeper directory replaces a shallower same-name server, and a project server wins over a user server
4The active recipe’s own mcp.serversbeats all of the above — the recipe is the concrete packaged contract

After the merge, the recipe’s resolved mcp.allowed filters the set:

  • a list of names keeps only the servers it names, whichever layer declared them;
  • an explicit allowed: [] keeps none;
  • an omitted allowed, no mcp: block, or ["*"] with allow_wildcard: true keeps everything ("*" without allow_wildcard is ignored).

A server dropped here never reaches the allowlist, and every surface says what it dropped: the TUI startup alert, a sub-agent stderr note, a sonny recipe resolve warning, filtered by mcp.allowed rows in sonny mcp list --pack-file (JSON: filtered_by_allowed), and a single mcp_not_allowed summary line in a headless --result-json.

Then the two gates run — the integration gate and allowlist enforcement. They are covered in Trust model .

Warning
An unreadable or malformed user/project MCP file, project integration file, or allowlist is a startup error. Sonny does not skip the bad layer and does not restore a broader configuration from an earlier one. A single server that merely lacks approval is withheld and reported without blocking unrelated approved servers.

Server schema

A mcp.yaml file is one key holding a list of servers:

servers:
  - name: fs
    type: stdio
    command: /usr/bin/mcp-server-filesystem
    args: ["--root", "/tmp"]
    work_dir: /srv
    enabled: true
    auto_connect: true

  - name: web
    type: sse
    url: "https://mcp.example.com/sse"
    egress:
      allow_hosts: ["mcp.example.com"]
      allow_ports: [443]
    auth:
      type: bearer
      token: ${MCP_TOKEN}
FieldTypeNotes
namestringrequired; the merge key, the allowlist key, and the tool-name namespace
typestringstdio or sse. No http type exists; streamable_http is refused at load
commandstringrequired for stdio; the executable to spawn
argslistargv for the stdio command
urlstringrequired for sse; the SSE endpoint
work_dirstringworking directory for the spawned stdio process
envmapextra environment for the stdio process; values expand ${VAR}/$VAR
authobjectnone/api_key/bearer/basic, with api_key, api_key_header, api_key_env_var, token, username, password; only meaningful for sse
egressobjectrequired for sse; see below. A stdio server cannot declare it
enabledbooldefault true
auto_connectbooldefault true; when false the server loads but is not connected at session start
integrationstringSonny-only; classifies the server for the integration gate (jira, gitlab, github, …). Omit it and Sonny guesses from name
metadatamapSonny-only free-form labels (tenant, env, region, …)
from_packstringSonny-only; written by sonny mcp approve --write-config on an entry copied from a recipe that is not verified-signed

The standalone sidecar is parsed strictly: it accepts the fields above plus env_allowlist, env_inherit_all and timeout. Any other key — including the upstream oauth block and schema_version — is rejected as a configuration error rather than silently ignored. A recipe’s inline mcp.servers: decodes into the full schema type.

sse servers need an egress policy

Remote egress is deny-by-default. The MCP manager validates every remote server at registration and refuses one whose egress.allow_hosts does not name its URL host.

servers:
  - name: local-dev
    type: sse
    url: "http://localhost:3001/sse"
    egress:
      allow_hosts: ["localhost"]
      allow_plain_http: true

The full block is {allow_hosts, allow_ports, allow_plain_http, allow_private_networks, deny_private_networks, proxy_url} — the same block a pack’s mcp.servers: entry takes.

Environment expansion

env values expand ${VAR}/$VAR against the current environment, or a recipe secrets: export, at connect time. A name the environment leaves unset or empty is then read from the operator’s owner-only (0600) <config-dir>/.env, or the file SONNY_DOTENV names, on every connect path: the CLI, the TUI, and the desktop daemon. A connect that fails with a reference still unresolved names the variable and that file.

Under active isolation the host environment comes first, so a variable you set — even to an empty value — wins over a recipe secret of the same name. A server an untrusted recipe declares resolves references only against the session’s own values.

Introspection

sonny mcp list                      # what would load, and why anything was dropped
sonny mcp list --format json
sonny mcp list --pack-file pack.yaml
sonny mcp list --connect            # connect and enumerate tools (spawns processes)
FlagEffect
--mcp-config <path>use this sidecar config instead of standard discovery
--pack-file <path>merge this recipe’s inline mcp.servers over the config
--connectconnect to enabled servers and enumerate their tools
--connect-timeout <dur>per-server connect timeout (only used with --connect)
--format text|jsonoutput shape
Note
--timeout is deprecated in favour of --connect-timeout; it still works and warns.

Introspection goes through the same allowlist-gated core as a real session, so sonny mcp list --connect, sonny tools list --mcp-config and sonny recipe resolve --connect can never connect to an unapproved server either.