Reach sidecar MCP servers from the harness#15
Merged
Conversation
The launcher needs to know which harness it is wrapping so it can emit a tong's MCP server config in that harness's shape (OpenCode merges a fragment into opencode.json; Claude Code reads a --mcp-config file). Plumb the harness name from the Make targets that already know it (run_opencode, run_claude) through a new --harness option into the parsed launcher options. The option is stored but not yet consumed; nothing reads it until the MCP emitter is wired into the launch path, so this is inert on its own.
An `mcp` tong is an HTTP MCP server the agent calls so credentials stay in the tong's process and never enter the anvil. The launcher now wires one up: it generates the per-harness MCP config for the discovered `mcp` tongs (an opencode.json `mcp` fragment for OpenCode, an `mcpServers` document for Claude Code), writes it to a host temp file mounted read-only into the anvil, and points the harness at it -- OpenCode's entrypoint merges the fragment via SWARMFORGE_TONG_MCP_FILE, while Claude Code is passed `--mcp-config <path>`. To do that the launcher selects the emitter from the harness it is wrapping and stops refusing `mcp` tongs as unsupported. Because an `mcp` tong's canonical alias is its declared interface.name rather than its filename, two tongs can now claim the same network alias; the launch refuses such a set up front, since a shared alias would make DNS (and so readiness, env, and MCP wiring) nondeterministic. The generated `mcp` fragment replaces a whole `mcp.<server>` entry rather than deep-merging into one that a lower-precedence opencode.json already defined under the same name. A tong's remote server and a pre-existing local server of the same name describe different transports, so merging their keys would yield a server carrying both a `url` and a `command` -- a contradictory config. The merge logic moves out of the entrypoint heredoc into anvil/merge_opencode_json.py (unit tested) so the whole-entry replacement is opt-in for the tong fragment while the ordinary config layers keep deep-merging. Selecting the emitter from the harness means a harness with no emitter would otherwise start the tong but emit no config, leaving the anvil unable to discover it. The launch instead refuses an `mcp` tong up front unless the harness has an emitter, turning a silent misconfiguration into a clear error before any docker work. With no `mcp` tongs discovered nothing is generated, mounted, or appended, so the anvil argv is unchanged and the empty-discovery launch stays byte-identical to the direct docker run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A sidecar can hold credentials in its own process and expose an MCP server the
agent calls, so the secret never enters the harness container. This teaches the
host launcher (
scripts/run_anvil.py) to actually wire up such a sidecar:it generates the MCP server config, makes it reachable from the harness, and
points the harness at it.
What this does
run_opencode/run_claudepass a--harnessselector (
opencode/claude) to the launcher, which records it in theparsed options so the MCP config is emitted in that harness's shape.
MCP-interface sidecar, the launcher builds the config pointing at the sidecar's
canonical alias and port, writes it to a host temp file mounted read-only into
the harness container, and points the harness at it:
opencode.jsonviaSWARMFORGE_TONG_MCP_FILE(merged last, so sidecar servers take precedence).--mcp-config <path>.opencode.jsonalreadydefines an MCP server under the same name, the generated entry replaces it
wholesale instead of deep-merging. A sidecar's remote server and a pre-existing
local server of that name describe different transports, so merging their keys
would yield a server carrying both a
urland acommand. The merge logicmoves out of the entrypoint heredoc into
anvil/merge_opencode_json.py(unittested), where whole-entry replacement is opt-in for the sidecar fragment while
ordinary config layers keep deep-merging.
interface name rather than its filename, so two sidecars can now claim the same
network alias. The launch refuses such a set up front, since a shared alias
would make DNS -- and so readiness, environment, and MCP wiring --
nondeterministic.
unknown or omitted harness would run the sidecar but emit no config, leaving the
agent unable to discover it. The launch fails fast with a clear error instead.