Skip to content

Reach sidecar MCP servers from the harness#15

Merged
CrypticSwarm merged 2 commits into
masterfrom
tongs-phase-7e-mcp-injection
Jun 19, 2026
Merged

Reach sidecar MCP servers from the harness#15
CrypticSwarm merged 2 commits into
masterfrom
tongs-phase-7e-mcp-injection

Conversation

@CrypticSwarm

Copy link
Copy Markdown
Owner

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

  • Selects the harness. run_opencode / run_claude pass a --harness
    selector (opencode / claude) to the launcher, which records it in the
    parsed options so the MCP config is emitted in that harness's shape.
  • Generates and injects per-harness MCP config. For each discovered
    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's entrypoint merges the fragment into opencode.json via
      SWARMFORGE_TONG_MCP_FILE (merged last, so sidecar servers take precedence).
    • Claude Code is passed --mcp-config <path>.
  • Replaces, not blends, a colliding server entry. When opencode.json already
    defines 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 url and a command. The merge logic
    moves out of the entrypoint heredoc into anvil/merge_opencode_json.py (unit
    tested), where whole-entry replacement is opt-in for the sidecar fragment while
    ordinary config layers keep deep-merging.
  • Refuses ambiguous aliases. An MCP sidecar's canonical alias is its declared
    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.
  • Refuses an MCP sidecar with no emitter for the harness. Starting one for an
    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.

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.
@CrypticSwarm CrypticSwarm merged commit fb85cfb into master Jun 19, 2026
1 check passed
@CrypticSwarm CrypticSwarm deleted the tongs-phase-7e-mcp-injection branch June 19, 2026 09:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant