outrig mcp
outrig mcp turns an outrig image-config into one MCP server for an external
client. It starts or attaches to the selected container, launches every
[images.<name>.mcp] backing server inside it, and republishes their tools over this
process’s stdio by default, or over Streamable HTTP when --listen is set.
Use outrig run when you want outrig to be the LLM client: it resolves an agent,
builds a Rig agent, and opens the built-in REPL. Use outrig mcp when another
program is the LLM client – Claude Code, Cursor, Zed, or any MCP-capable editor –
and you want that program to drive the tools inside your outrig container.
Synopsis
outrig mcp [--image <name-or-local-ref>]
[--attach <session-id-or-container-name>]
[--listen <addr>]
[--network <default|audit|filter>]
[--session-dir <path>]
[--config <path>]
[--global-config <path>]
[--session-root <path>]
[--volume <host:container[:ro|rw]>]
[--verbose]
outrig mcp show-merged [--image <name-or-local-ref>]
[--attach <session-id-or-container-name>]
outrig mcp self
--image <name-or-local-ref>(default:default-image): selects a[images.<name>]block. If an explicit value does not match config, it is treated as a local Podman image ref and is never pulled. Required with--attach <podman-name>.--attach <session-id-or-container-name>(default: off): reuse an existing container instead of starting one.--listen <addr>(default: off): serve Streamable HTTP at/mcpinstead of stdio. Accepts TCP socket addresses such as127.0.0.1:7331or0.0.0.0:7331, plus Unix sockets asunix:/tmp/outrig.sock.--network <default|audit|filter>(default: config[network].mode, elsedefault): choose Podman’s default networking, network audit logging, or global network filtering for this fresh session.--session-dir <path>(default:<session-root>/<sid>): writes to a known path, which must be empty or not exist yet.--config <path>(default: walks up from cwd; if not found, run config-less): read the repo config from this file instead. See Reference -> CLI for which repo the session runs against.--global-config <path>(default:~/.outrig/config.toml, skipped if missing): read the global config from this file, which has to exist.--session-root <path>(default: global config, then XDG data directory): root for all sessions.--volume <host:container[:ro|rw]>(repeatable): bind an extra host directory into the container, on top of the default workspace mount. Read-only unless:rwis given; the host directory must exist. A relative host path resolves against the directory you ran outrig from, not against the repo root. Rejected with--attach.--verbose(default: off): adds buildah/podman command transcripts to stderr andcontainer.log.
outrig mcp self is different from the session MCP server. It does not resolve a repo config,
start a container, or create a session. It serves OutRig’s own docs, schema, suggestions,
and advisory validators so an external AI tool can design an image-config. See
AI-assisted design.
There is no --agent flag. outrig mcp has no agent, so it never consults
default-agent, agent.image, [agents], [models], [providers], or provider
API keys. Image selection is only:
--image <name-or-local-ref>- top-level
default-image - outrig’s built-in default image-config
With neither of the first two set, startup does not fail. The session falls through to the built-in default and marks it on stderr:
[outrig] no --image, agent image, or default-image configured; using outrig's built-in default
[outrig] image-config: outrig-default (built-in default)
Declaring a reserved name yourself vetoes the injection, and outrig steps aside with a note
saying so. Five names do it: [images.outrig-default], [images.outrig-default-fs],
[images.outrig-default-shell], [sidecars.outrig-default-fs], and
[sidecars.outrig-default-shell]. [sidecars.outrig-default] is not reserved – the
built-in declares no sidecar by that name, so yours cannot clash with it.
What a veto leaves behind depends on which name you took. An [images.outrig-default] of your
own still resolves, so the session runs on it. Any of the other four leaves nothing to fall
through to, and startup ends there – so a repo that declares only
[images.outrig-default-fs] and no default-image fails rather than falling back.
default-image must name a config block. The raw local-image fallback applies
only to explicit --image values and to raw image refs saved in session records.
outrig mcp can also run in a directory with no .agents/outrig/config.toml (and no
--config): it uses the current directory as the workspace root and merges in the global
config. With no agent it resolves no model and no provider either, so a working podman is all
it needs – the built-in default supplies the image and two MCP servers. Pass
--image <local-ref> to proxy a different image instead; its proxied MCP servers come from
that image’s org.outrig.mcp labels. As with outrig run, the current directory can’t be your
home directory, one above it, or one that contains the global config, unless the global config
declares [workspace] host-path – so an MCP client that starts outrig in / or ~ needs
--config naming a repo.
With --attach, image-config selection is different:
- If the attach value matches an exact session id under the resolved session root,
outrig reuses that session row’s
container_nameandimage_config_name. - If
--image <name-or-local-ref>is also passed, it overrides the session row’simage_config_name. - If the attach value is not a known session id, outrig treats it as a podman
container name and requires
--image <name-or-local-ref>. - If the session row predates
image_config_namethere is nothing to inherit, so--image <name-or-local-ref>is required there too.
--network audit and --network filter are rejected with --attach; borrowed containers are
not retrofitted with a new interceptor.
The primary’s label is read off the image its container is running, by the
engine’s id for that image, not off whatever its tag names at the time of the read. The two
differ once a tag moves: a pull of a re-pushed registry/x:latest re-points the ref while
every container started from the old one keeps running it. This matters most with --attach,
which borrows a container someone else may have started long ago – the servers merged are the
ones that container can actually run. A repo-local build’s tag is content-addressed and cannot
move, so nothing changes there.
The selected image must expose at least one backing MCP server after image
org.outrig.mcp label entries and [images.<name>.mcp] overrides are merged. Repo-local
build cache images already stamp that merged table into their labels on cache misses, but startup
still applies the same merge. An image with no merged entries has nothing to proxy, so outrig mcp
exits before the client sees an MCP initialize response.
Minimal Config
outrig mcp can run from an image-only repo config:
default-image = "coding"
[images.coding]
dockerfile = ".agents/outrig/images/coding/Dockerfile"
context = ".agents/outrig/images/coding"
[images.coding.mcp]
fs = ["mcp-server-filesystem", "/workspace"]
shell = ["bash", "-lc", "exec shell-mcp-command"]
The MCP server binaries still have to exist inside the image. Install them in the
container Dockerfile just as you would for outrig run. In the example above,
shell-mcp-command stands for whichever shell MCP server package you choose to install.
An image can also carry the same [mcp] table in its org.outrig.mcp OCI label.
Use that when a shared image owns the default tool set, then keep only repo-specific
overrides in .agents/outrig/config.toml. A ${VAR} in a label entry’s env is held to the
global config’s [repo] env-allow list, as a repo config’s
references are, and a session whose label names an unlisted variable is refused. See
Concepts -> MCP Servers.
To inspect the effective table without serving MCP:
outrig mcp show-merged --image coding
In fresh mode this starts the selected container, reads the org.outrig.mcp label off the
primary image and every named sidecar image, applies config.toml overrides, prints the merged
[mcp] table to stdout, then stops the container. Sidecar containers are not launched for the
read. A label that names an unlisted host variable is refused here too, as at a session start.
Each server carries a comment naming its placement and provenance:
[mcp]
# fs: primary (image label org.outrig.mcp)
fs = ["mcp-server-filesystem", "/workspace"]
# search: sidecar "tools" (config.toml)
search = { command = ["mcp-search"], sidecar = "tools" }
For repo-local build images, the cache tag was already stamped with that merged label when it
was built (placement-bearing entries excluded – they belong to other containers). With
--attach, it borrows the existing container for the same read and leaves it running.
Attach Mode
Use attach mode when a container is already running and you want an external MCP client to share its workspace, installed tools, and environment:
outrig mcp --attach 20260504T141907-a83f
outrig mcp --attach outrig-20260504T141907-a83f --image coding
The first form resolves an existing outrig session id. The second form borrows a
podman container directly, which is useful for containers not started by outrig run.
Attach mode cannot own containers, so a config that declares sidecars (or places MCP
entries in one) is rejected with --attach; start a fresh session instead.
Attach mode shares the container, not the MCP protocol processes. The attacher starts
its own podman exec -i children for each merged MCP server, so the external client
has independent MCP state and independent stderr logs. Existing MCP children from the
host session keep running.
Every attach invocation writes its own fresh session row and log directory. outrig logs <attached-session> <server> therefore works the same as it does for a fresh-container
outrig mcp session.
MCP servers used with attach mode should be reentrant-safe. Servers that bind a fixed port, write a global pidfile, or take an exclusive lock can conflict with the host session’s copy or with another attacher.
Client Configuration
Put outrig on PATH, or use an absolute path to the binary in each client config.
MCP clients may start servers with a different working directory than your shell, so
passing the absolute path of the repo’s .agents/outrig/config.toml as --config is the least
surprising setup.
Claude Code can add a stdio server from the command line:
claude mcp add --transport stdio outrig -- outrig mcp \
--config /path/to/repo/.agents/outrig/config.toml \
--image coding
Claude Code and Cursor both understand an mcpServers JSON shape. For Cursor, place
this in .cursor/mcp.json for project scope or ~/.cursor/mcp.json for global scope.
For Claude Code project scope, use .mcp.json in the project root.
{
"mcpServers": {
"outrig": {
"type": "stdio",
"command": "outrig",
"args": [
"mcp",
"--config",
"/path/to/repo/.agents/outrig/config.toml",
"--image",
"coding"
],
"env": {}
}
}
}
Zed uses context_servers in its settings:
{
"context_servers": {
"outrig": {
"command": "outrig",
"args": [
"mcp",
"--config",
"/path/to/repo/.agents/outrig/config.toml",
"--image",
"coding"
],
"env": {}
}
}
}
Streamable HTTP
Use --listen when you want one long-lived outrig mcp process that multiple
MCP clients can connect to:
outrig mcp --listen 127.0.0.1:7331 --image coding
The MCP endpoint is /mcp, so clients should connect to:
http://127.0.0.1:7331/mcp
Loopback TCP is the intended default deployment shape. Binding 0.0.0.0:7331
or any other non-loopback address is allowed, but outrig prints a warning because
v1 has no built-in authentication and anything that can reach the port can call the
container’s tools. Put an authenticated reverse proxy in front if you expose it beyond
the local machine.
For local multi-process access without a TCP port, use a Unix socket:
outrig mcp --listen unix:/tmp/outrig.sock --image coding
Socket filesystem permissions are the access boundary, so the socket is created
0600 – only the user running outrig mcp can connect, whatever the process
umask is. HTTP clients still use the Streamable HTTP protocol and the /mcp path
over that socket.
A socket file left behind by a process that is gone is removed and the path
reused. A path another outrig mcp is still serving is refused instead, naming
it: taking it over would leave that instance running and holding its container
with nothing able to reach it. Stop it, or pick another path. A socket that
answers neither way – one whose server has stopped accepting, or that this user
cannot connect to – is also refused, since the risk of cutting off a running
instance outweighs an error message.
Point --listen at a directory only you can write. In a shared one without the
sticky bit, another user can replace the socket’s directory entry; outrig refuses
the path instead of following the replacement, but it cannot stop the entry from
being taken.
Browser defenses
Two request headers are checked before anything is dispatched, and both are about pages in a browser rather than about MCP clients:
Hostguards against DNS rebinding. A loopback TCP listener accepts onlylocalhost,127.0.0.1, and::1, so a name that resolves to the loopback address does not reach the tools under a hostname outrig does not serve. A non-loopback bind and a Unix socket are reached under names outrig cannot enumerate – whatever a reverse proxy publishes – so the check is off for them.Originguards against a web page driving the listener, which the MCP transport spec requires a local server to do. A loopback listener accepts the origins that name itself, port included (http://localhost:7331,http://127.0.0.1:7331,http://[::1]:7331); every other origin is refused with403, including another local port, because a page onhttp://localhost:3000is as cross-origin as one on the internet. A non-loopback bind and a Unix socket recognize no origin as their own and refuse every one.
A request that carries no Origin header is served in all cases. That is what every
non-browser client sends – an editor, a CLI, a script, rmcp’s own HTTP transport – so these
rules cost an ordinary client nothing. A browser is what attaches the header, and a browser is
what the rules are for.
Neither check is authentication. They keep a page a user did not open from reaching tools that the user’s own machine can; anything that can send a request without a browser attaching headers to it is still limited only by what can reach the port or the socket.
What Happens, in Order
- Locate config. Walks up from the current directory until
.agents/outrig/config.tomlis found, else runs config-less there. The MCP host’scwdtherefore needs to be the repo, or pass--config <repo>/.agents/outrig/config.tomlexplicitly. A config found that another user owns is refused, not read (see Reference -> CLI). Startup names the config file it read and the workspace it will mount on stderr before anything is built or started. - Resolve image. Uses explicit
--imagefirst. Config entries win; an unknown explicit value is treated as a local Podman image ref. Without explicit--image, top-leveldefault-imagestill names a config block. - Prepare the container. Fresh mode builds, cache-hits, pulls configured
image-namerefs, or probes raw local refs, then startspodman run -d --rm --name outrig-<sid> .... Attach mode probes the existing container withpodman inspect, verifies that it is running, and does not build, start, stop, or remove it. - Merge MCP config and start sidecars. Read the primary image’s
org.outrig.mcplabel if present, then overlay[images.<name>.mcp]from config by server name. Each declared sidecar’s image is resolved, its label merged (scoped to that sidecar), andstart = "auto"sidecars are started asoutrig-<sid>-<sc>. A label whoseenvreferences a host variable the global config’s[repo]env-allowdoes not list is a startup error, before any sidecar starts. An entrypoint-stdio sidecar (inlineimage, nocommand) is instead created and initialized with its ENTRYPOINT held un-executed, env baked in viapodman create --env(a${VAR}value by name, from podman’s own environment). Sidecar failures follow the block’son-failurekey. Astart = "manual"sidecar stays planned but unstarted – its servers are skipped with a notice.outrig mcphas no mid-session start surface; manual sidecars are started from theoutrig runREPL (/sidecar add <name>) or the library API. - Start network interception, if enabled. The interceptor attaches to the primary
and every sidecar – including created-but-not-started entrypoint sidecars, whose
first packet is therefore already subject to policy. Fresh sessions can write
<session_dir>/logs/network.jsonland filter mode can enforce global policy; attach mode cannot install a new interceptor. - Connect MCP servers. For each merged entry,
podman exec -ithe configured command in the container its placement names – or, for entrypoint-stdio servers,podman start --attach --interactivethe held container, whose lifetime now equals the server’s – and run the MCPinitializehandshake. A server that has not answered within 120 seconds fails startup, with its stderr in the error. - Build the proxy. outrig advertises one merged tool list to its client, with
each tool namespaced
<server>__<tool>. See Tool Names below. Each backing server’stools/listgets 120 seconds too. - Serve MCP. Without
--listen, rmcp’s stdio transport reads JSON-RPC frames from the process stdin and writes responses to stdout. With--listen, rmcp’s Streamable HTTP service accepts POST/SSE traffic at/mcp. The proxy dispatchestools/callto the right backing server in both modes, under that server’scall-timeout-secs(10 minutes unless configured). A client’snotifications/cancelledfor a call is forwarded to the backing server, and so is outrig’s own when a call runs past its deadline; the client gets a timeout error.
If anything before step 8 fails, outrig mcp prints the error on stderr and exits
non-zero without ever advertising a tool list.
Startup Banner
After the image is ready, the container is running, and all backing MCP servers have
answered tools/list, outrig mcp prints one banner to stderr:
[outrig] image-config: coding
[outrig] image: outrig:coding-1f3a2b
[outrig] container started: outrig-coding-2026-05-04-a83f
[outrig] mcp fs: initialized (3 tools)
[outrig] mcp shell: initialized (1 tool)
[outrig] tools available: fs__list_directory, fs__read_file, fs__write_file, shell__exec
[outrig] session id: 20260504T141907-a83f
[outrig] transport: stdio
[outrig] mcp server ready
Everything in that banner is on stderr. The client should treat stdout as protocol bytes only for stdio transport.
Attach mode prints container attached: in the same position.
With --listen, the banner says transport: streamable-http, then prints the bound
endpoint before the ready line:
[outrig] transport: streamable-http
[outrig] listen: http://127.0.0.1:7331/mcp
[outrig] mcp server ready
Transport Discipline
By default, outrig mcp serves MCP over stdio. Its stdout is reserved for JSON-RPC
messages to the client; all other process output goes somewhere else:
- startup banner: stderr
- a cache-miss image build or pull, as
[buildah]/[podman]lines: stderr - outrig tracing controlled by
OUTRIG_LOG, orRUST_LOGwhen unset: stderr - top-level startup errors: stderr
- backing server stderr:
<session_dir>/logs/<server>.stderr
This split is load-bearing. If a wrapper, shell hook, or debug print writes anything non-JSON to stdout before or during the MCP exchange, the external client may fail the handshake or drop the server.
With --listen, stdout is not the protocol channel, but outrig keeps the same
stderr-first discipline so wrapper behavior remains predictable.
Tool Names
Outrig publishes tools under <server>__<tool> names. If the backing fs server
advertises a tool named read_file, the external client sees:
fs__read_file
Some MCP clients display that relationship as fs.read_file in their UI. The actual
tool name on the wire is still fs__read_file; outrig looks that name up in a table it
built at startup and dispatches the call to the original read_file tool on the fs
backing server.
A tool whose name outrig had to change to satisfy ^[a-zA-Z0-9_-]{1,64}$ – one carrying
a character outside that set, one too long to fit, or one on a server whose own name ends
in _ or contains __ – is advertised with a short hash suffix instead, as
fs__read_file_5d2270. The suffix depends only on the server and tool names, so a client
reconnecting to the same servers sees the names it saw before, whatever order those servers
list their tools in. That hash-suffixed name keeps the <server>__ prefix only while the
server’s name is 54 characters or fewer; past that the cut can fall inside the server name,
which is why the proxy’s instructions tell a client to treat names as opaque and call each
exactly as listed.
Sixty-four characters cannot encode every possible pair, so two tools can still want one
name. outrig widens one side’s suffix rather than dropping either, and picks which side
moves from the two tools’ identities rather than from listing order. A widened name does
depend on the rest of the session’s tool set, so it is the one kind of name worth re-reading
instead of caching when the servers in play change. outrig run applies the same rule, so a
tool set gets the same names on either surface. The one difference is /sidecar add: names the
model has already seen cannot move, so a tool added mid-session is the one widened.
See Concepts -> MCP Servers for the collision and sanitization rules.
Tool Results
A tools/call answer is an ordered list of content blocks, and outrig forwards it whole:
every block, in the order the backing server sent it, with its boundaries, payload,
annotations, and _meta intact. structuredContent and result-level _meta come through
beside the blocks. A client connected to outrig mcp therefore sees an image as an image.
Two things are outrig’s rather than the backing server’s:
- The tool name.
tools/listre-advertises the upstream descriptor unchanged – title, description, both schemas, behavioral hints, icons,_meta– under the namespaced name. resultType. outrig answerscompletefor every dispatch. The other values promisetasks/*follow-up methods the proxy does not implement, so relaying one would advertise a surface that is not there.
A block kind newer than outrig’s MCP SDK never arrives: the SDK rejects it before outrig sees it. A kind the SDK knows and outrig does not is carried through as the JSON it arrived as, so the proxy is not the thing that drops it.
The rendered view
A language model’s tool-result channel is a string, so outrig run gives the model a
rendering of the blocks rather than the blocks themselves. It is the library’s
McpToolResult::render_text, and it is what tool-result-max caps – see
Reference -> Config. Blocks are joined with newlines:
| Block | Rendering |
|---|---|
| text | the text |
| embedded text resource | the text |
| image | [image: {mime}, {n} base64 bytes] |
| audio | [audio: {mime}, {n} base64 bytes] |
| embedded binary resource | [blob: {mime}, {n} base64 bytes] |
| resource link | [resource link: {uri}] |
| anything else | [unsupported content block: {kind}] |
{n} counts base64 characters, not decoded bytes – it describes what arrived, not what
it decodes to. structuredContent is not rendered; a server that wants a model to read it
is required by the spec to also send it as text.
So the two exits differ on purpose. An external client reaching outrig through
outrig mcp gets the protocol’s data; the agent loop inside outrig run gets the
rendering, because that is the shape its tool channel has.
Lifecycle
outrig mcp has four graceful shutdown triggers:
- stdio stdin EOF, which usually means the external MCP client disconnected
- SIGINT, such as Ctrl-C in the terminal that launched the process
- SIGTERM, such as a supervisor asking the process to stop
- SIGHUP, such as the terminal that launched the process closing
All paths cancel the rmcp service, wait for the dispatcher to settle, shut down each
backing MCP server, and finalize the session record. Fresh-container mode then stops the
containers – sidecars first, primary last. Attach mode leaves the borrowed container
running. A signal is how a server is stopped, so once startup has finished it exits 0.
Under --listen, a signal gives requests still in flight five seconds to finish. Anything
unfinished after that – a request whose body has stalled, say – is abandoned so teardown can
proceed, and a second signal ends the wait early.
A signal during startup – while the image builds, the container starts, a backing server
initializes, or a stdio client has yet to send initialize – stops whatever had started and
finalizes the record too, and exits with 128 plus the signal number: 130, 143, 129. A
SIGINT or SIGTERM while the containers are being stopped stops the waiting: outrig finalizes
the record, exits, and leaves the remaining containers to a forced removal in the background.
Sessions with sidecars also watch the primary container: if it dies out from under outrig
(a manual podman kill, the OOM killer), the sidecars are reaped and the process exits
non-zero. A sidecar dying mid-session only degrades the tool set – its tools return
errors and the session continues.
HTTP/SSE mode is daemon-shaped: client disconnects close only that MCP session. The
outrig mcp --listen process stays alive until SIGINT, SIGTERM, SIGHUP, or
attached-container shutdown.
If an attached host session stops the container while outrig mcp --attach is live, the
attacher cancels its proxy, shuts down its MCP children, finalizes its session with a
non-zero exit, and reports that the attached container stopped.
Sessions and Logs
Every outrig mcp invocation creates a normal outrig session, including attach mode.
It appears in outrig ls, its backing-server stderr is readable with outrig logs, and
outrig discard removes it like any other session.
Because no agent participates, the in-memory session row has agent_name = None.
On disk, new outrig mcp session JSON omits agent_name; older JSON with
"agent_name": null still represents the same no-agent state when read.
$ outrig ls
ID STARTED DURATION IMAGE EXIT
20260504T141907-a83f 2026-05-04 14:19:07 0m44s coding 0
$ outrig logs 20260504T141907-a83f fs
[mcp-server-filesystem] starting; root=/workspace
[mcp-server-filesystem] tools/list: 3 tools advertised
See Sessions for session-root resolution, --session-dir, and log
inspection.
Future Work
The v0 HTTP surface is intentionally unauthenticated and minimal. Deferred work includes
adding built-in auth, exposing a tool-call audit log, proxying MCP prompts/* and
resources/*, surfacing backing-server stderr as MCP resources, and paginating
tools/list.
See Also
- outrig run – outrig as the LLM client and REPL.
- Sessions – session records and server stderr logs.
- Concepts -> MCP Servers – configuration and routing.
- Reference -> CLI – all flags and exit behavior.