Exposes Marea's entire command tree as a Model Context
Protocol server, so AI assistants
(Claude Code, etc.) can drive builds, deploys, Helm operations and
AWS calls through a structured, auditable interface instead of
shelling out to marea ….
Always loaded (part of @base_plugins). No marea.yaml opt-in
required.
Why an MCP server
When an AI assistant runs Marea directly via the shell, every command goes through whatever sandbox the assistant runs under — broad permissions, no per-command granularity, no machine-readable schema for the available operations. With Marea exposed as an MCP server:
- Every
mareasubcommand becomes an MCP tool with a JSON-Schema-typedinputSchemaderived from its Optimus options and flags. - The host (
marea mcp serve) runs outside the assistant's sandbox, with whatever filesystem and network access Marea normally needs. - The assistant interacts only through JSON-RPC tool calls, which the
client can allowlist per tool. Destructive operations (
helm delete,helm rollback, AWS deletes) carrydestructiveHint: trueso the client can require explicit confirmation; read-only ones carryreadOnlyHint: trueand can be auto-approved.
Commands
marea mcp
├── serve
├── tools
└── call --tool NAME --json JSONmarea mcp serve
Runs a stdio JSON-RPC 2.0 server. Newline-delimited JSON on stdin/stdout, diagnostics on stderr.
Register it with Claude Code:
claude mcp add marea -- marea mcp serve
Then allowlist tools in ~/.claude/settings.json:
{
"permissions": {
"allow": [
"mcp__marea__helm_list",
"mcp__marea__helm_history",
"mcp__marea__helm_values",
"mcp__marea__aws_ecr_list",
"mcp__marea__aws_route53_list"
]
}
}Destructive tools (mcp__marea__helm_delete, mcp__marea__helm_rollback,
mcp__marea__aws_*_delete) will prompt for confirmation per call,
backed by the destructiveHint annotation in their descriptors.
marea mcp tools
Prints the full JSON descriptor of every tool the server would
expose — name, description, inputSchema, annotations, and the
internal subcommand path. Useful for inspecting how a given
marea.yaml + plugin set maps to MCP tools, without starting a real
client.
$ marea mcp tools | jq '.[] | .name'
"setup_init_release"
"build_local"
"build_release"
"build_docker"
"build_helm"
"run_local"
"run_release"
"run_docker"
"helm_chart"
"helm_list"
"helm_history"
"helm_values"
"helm_template"
"helm_upgrade"
"helm_delete"
"helm_rollback"
…
marea mcp call --tool NAME --json JSON
Invokes one tool with the given JSON arguments object and prints the
captured output. Same execution path as a real tools/call
request — useful for end-to-end debugging without writing JSON-RPC
by hand:
$ marea mcp call --tool helm_list --json '{"deploy": "staging"}'
How tools are derived
The Marea.Plugins.Base.marea_config_args/1 chain produces a
%{options:, flags:, subcommands:} map. Marea.Mcp.Tools.list/0
walks that map and emits one tool per leaf subcommand:
| Optimus spec | JSON Schema |
|---|---|
leaf path [:helm, :upgrade] | tool name "helm_upgrade" |
about: "..." | description |
options: [release: [long: "--release", parser: :integer, required: true, help: "...", default: ...]] | property release with type (integer/number/string), description, default, and entry in required |
flags: [debug: [long: "--debug", help: "..."]] | property debug: {type: "boolean", default: false} |
global: true options/flags | propagated into every descendant leaf |
Branch nodes (helm, aws, build) are not callable directly —
only leaves are tools. The mcp, schema and help namespaces are
excluded so they don't show up as user-facing tools.
Execution model
tools/call is realised by Marea.Mcp.Runner.run/2:
- Stops any currently running
Marea.Service. - Spawns a
Taskwhose group leader is swapped for aStringIO, so every byte that would normally hit stdout/stderr is captured. - Sets
Marea.Lib.set_stop_mode(:raise)soMarea.Lib.stop/1raisesMarea.Stopinstead of callingSystem.halt/1— long running servers can't tolerate a VM halt. - Synthesizes argv from the MCP
argumentsmap (each option becomes[--long, value], each true flag becomes[--long]) and callsMarea.main_inline/1. - If the command produced a deferred
{:exec, cmd}(Run / Build), the shell command is executed inline withSystem.shell/2,stderr_to_stdout: true, output appended to the capture. Returns
{captured_output, :ok | {:error, reason}}to the server, which packages it as a singletextcontent block and setsisErrorappropriately.
The service is stopped again after each call, so each invocation
sees a fresh marea.yaml and a clean Config.
stdio discipline
The server keeps its original group leader (so IO.read(:stdio, :line)
reads real stdin) and emits responses with IO.puts(:standard_io, …).
Diagnostics use :standard_error. The per-call StringIO swap in the
runner contains all command output, so nothing the underlying plugins
print can corrupt the JSON-RPC stream.
Caching
The tool list is computed once at server startup (read marea.yaml,
load plugins, walk the args tree). Changing marea.yaml or plugin
code requires restarting the server. Per-call execution always re-
reads marea.yaml and re-parses CLI args, so config edits between
two calls of the same tool are picked up.
Interactive commands
A handful of commands replace the parent shell with an interactive
TTY-attached process via Marea's
deferred-shell mechanism:
marea run local / release / docker, marea k8s console / shell / remote. These are exposed as MCP tools — their argv synthesis and
schema work fine — but calling them through marea mcp serve
captures their output rather than producing a real interactive
session. For an actual IEx prompt or pod shell, use the CLI.
Non-interactive commands (builds, Helm chart/list/history/values/ template/upgrade/delete/rollback, AWS operations, K8s describe/logs/ restart) work cleanly via MCP: their output is captured and returned as a single text content block.