Skip to content

MCP servers (mcp.json)

mast connects agents to tools over the Model Context Protocol (MCP). Server definitions live in a catalog file, mcp.json; a workload bundle references them by name via tool_catalog.mcp[].server. mast wires each referenced server generically, dispatched by transport kind — no server is special-cased.

  • Directory mode (--workload=<path>) — next to the workload: <path>/mcp.json.
  • Name mode (--workload=<name>) — at the config root selected by .agents/ discovery.

A workload that references a server absent from mcp.json is a fatal load error — mast refuses to start rather than silently drop a tool.

{
"version": 1,
"command_allowlist": ["/usr/local/bin/fs-mcp-server"],
"servers": {
"gke": {
"transport": "http",
"url": "https://container.googleapis.com/mcp",
"auth": {
"google_oauth": {
"scopes": ["https://www.googleapis.com/auth/cloud-platform"]
}
}
},
"filesystem": {
"transport": "stdio",
"command": "/usr/local/bin/fs-mcp-server",
"args": ["--root", "${WORKSPACE}/data"],
"env_mode": "clean",
"env_passthrough": ["PATH", "HOME"],
"env": {
"FS_MCP_TOKEN": "${FS_MCP_TOKEN}"
}
}
}
}
FieldTypeNotes
versionintRequired. Must be 1 — the only schema version this build accepts.
command_allowlistlist of stringsOptional, catalog-level. When non-empty, every stdio server’s resolved command must appear here or the catalog fails to load. Both sides are ${VAR}-expanded before comparison. Empty (the default) imposes no restriction.
serversmapServer name → definition. The map key is the name workloads reference; it must be non-empty.
servers.<name>.transportstringRequired. http or stdio.
servers.<name>.urlstringRequired for http. The streamable-HTTP endpoint.
servers.<name>.auth.google_oauth.scopeslist of stringshttp only. When present, requests carry an Application Default Credentials (ADC) bearer token with these scopes. Omit for an unauthenticated endpoint. Empty scopes default to cloud-platform.
servers.<name>.commandstringRequired for stdio. Executable to launch — a bare name resolved on PATH or an absolute path.
servers.<name>.argslist of stringsstdio only. Command arguments.
servers.<name>.env_modestringstdio only. inherit (default) — the child inherits the full daemon environment. clean — the child starts from an empty environment and receives only env_passthrough variables plus env.
servers.<name>.env_passthroughlist of stringsstdio only, and only under env_mode: "clean". Names of daemon environment variables to copy through to the child (each copied only if set). Rejected under inherit, where the child already sees everything.
servers.<name>.envmapstdio only. Environment variables layered on top (they override an inherited or passed-through variable of the same name).

Unknown fields are tolerated (forward-compatibility with richer catalogs); the loader validates the version, each server name, and the per-transport required fields, reporting the first problem it finds.

Speaks MCP over a streamable-HTTP endpoint. When the entry declares auth.google_oauth, mast attaches an ADC bearer token and fails fast at startup if credentials cannot be loaded — surfacing a misconfiguration as a clear load error rather than a mid-run tool failure. Without an auth block the endpoint is called unauthenticated.

mast launches command (with args and env) as a local child process and speaks MCP over its stdin/stdout. This is the path for local or sidecar MCP servers that authenticate through their own environment rather than a bearer token.

  • Variable expansion. ${VAR} references in command, each args entry, and each env value are expanded against the daemon environment. Expansion follows Go’s os.ExpandEnv, so a bare $name expands too and there is no escape for a literal $ — a value that must contain a literal dollar (for example a secret) should be passed through the inherited daemon environment rather than written into the catalog.
  • Environment. Under the default env_mode: "inherit" the child inherits the daemon’s environment and the configured env entries are layered on top (they override, applied in a deterministic order). Under env_mode: "clean" the child starts from an empty environment and sees only the env_passthrough daemon variables that are set, plus env — so a local tool server never receives the daemon’s provider API keys or cloud credentials unless you name them explicitly.
  • Command allowlist. The catalog-level command_allowlist, when non-empty, restricts which executables stdio servers may launch: an out-of-allowlist command is a fatal load error. This bounds the blast radius of an edited catalog even where the file itself is not gate-protected. The match is a literal string comparison after ${VAR} expansion — it does not resolve PATH or canonicalize symlinks, so list a command exactly as it is written in the server entry (a bare node and an absolute /usr/bin/node are distinct entries).
  • Lifecycle. The process is launched lazily on first tool use, not at startup. It then lives for the duration of the daemon (mast holds the toolset for the process lifetime and does not tear individual toolsets down); the child exits when it closes its own stdio or the daemon exits.

An MCP server that answers 4xx or 5xx usually says why in the response body, and that sentence is normally the whole fix — the IAM permission to grant, the quota metric that ran out. mast reads it and puts it in the error, so a denial reaches the log, the model, and the operator as

403 Forbidden: Permission 'mcp.googleapis.com/tools.call' denied on
resource '//container.googleapis.com/mcp/projects/example' (or it may
not exist).

rather than the bare Forbidden the status line alone gives you. This is automatic for every http server; there is nothing to configure.

Two limits worth knowing. The body is only read when the response declares a JSON content type and stays under 32 KiB — anything else (an HTML error page from a proxy in front of the server, an oversized response) passes through untouched and you get the status line. And an extracted error does not tear down the MCP session: the call fails, the model sees the reason, and the next call reuses the same connection.

MCP is not wired under the default echo model, which never emits tool calls — wiring it there would be pure startup cost and would surface credential problems as workload-load failures. The scripted model and real providers do wire MCP. Because a stdio server needs no cloud credentials, you can drive real tool calls fully offline with --model scripted.

Per-workload and per-specialist allowlists narrow which of a server’s tools an agent may call; see tool_catalog. MCP tools default to mutating for the recorded-effect outbox unless a tool_catalog.tools[] override marks them read-only.

A specialist’s allowlist names servers by the key they are declared under in mcp.json:

tools:
mcp:
- server: gke # the key in mcp.json's "servers" map
tools: [get_k8s_resource, get_k8s_logs]
- server: prometheus # no tools: — the whole server

Presence is significant, per axis. No mcp: key inherits every server the workload catalogs; mcp: [] denies them all. They are one character apart and mean opposite things, so write the empty list deliberately — it is how a specialist that needs no cluster access (a synthesizer, a classifier) says so, and since specialists are read-only by default, it is also how such a specialist avoids inheriting a catalog that contains write tools.