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.
Where mcp.json lives
Section titled “Where mcp.json lives”- 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.
Schema
Section titled “Schema”{ "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}" } } }}| Field | Type | Notes |
|---|---|---|
version | int | Required. Must be 1 — the only schema version this build accepts. |
command_allowlist | list of strings | Optional, 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. |
servers | map | Server name → definition. The map key is the name workloads reference; it must be non-empty. |
servers.<name>.transport | string | Required. http or stdio. |
servers.<name>.url | string | Required for http. The streamable-HTTP endpoint. |
servers.<name>.auth.google_oauth.scopes | list of strings | http 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>.command | string | Required for stdio. Executable to launch — a bare name resolved on PATH or an absolute path. |
servers.<name>.args | list of strings | stdio only. Command arguments. |
servers.<name>.env_mode | string | stdio 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_passthrough | list of strings | stdio 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>.env | map | stdio 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.
Transports
Section titled “Transports”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 incommand, eachargsentry, and eachenvvalue are expanded against the daemon environment. Expansion follows Go’sos.ExpandEnv, so a bare$nameexpands 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 configuredenventries are layered on top (they override, applied in a deterministic order). Underenv_mode: "clean"the child starts from an empty environment and sees only theenv_passthroughdaemon variables that are set, plusenv— 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-allowlistcommandis 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 resolvePATHor canonicalize symlinks, so list acommandexactly as it is written in the server entry (a barenodeand an absolute/usr/bin/nodeare 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.
When an HTTP server rejects a call
Section titled “When an HTTP server rejects a call”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 onresource '//container.googleapis.com/mcp/projects/example' (or it maynot 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.
How wiring interacts with --model
Section titled “How wiring interacts with --model”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.
Tool policy
Section titled “Tool policy”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 serverPresence 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.