MCP
magus serves its tools as an MCP (Model Context Protocol) server, so agents and IDE plugins that speak MCP (Claude Desktop, Cursor, VS Code Copilot, and others) can call them directly instead of shelling out. There are two ways to reach it:
- stdio (
magus mcp): the host launches magus and talks to it over its stdin and stdout. It opens the workspace it is launched in and needs no server and no token. Start here. - Streamable HTTP (
magus server): one long-lived server athttp://127.0.0.1:7391/mcpthat several clients share, authenticated with a bearer token.
Both serve the same tools. magus prints what a host needs (magus mcp --help) and never writes a host's config file; the snippets below are for you to place.
For the full agent surface built on top of MCP - the installable skills, MAGUS.md routing, durable memory, and the drift check - see Agents.
stdio: the host launches magus
Register magus with your MCP client as a stdio server. Most clients take a command and its arguments:
{
"command": "magus",
"args": ["mcp"]
}
The host starts magus mcp in the workspace it opens, and it serves that workspace until the host closes stdin. Stdout carries only protocol frames; logs, and one line saying what is being served, go to stderr, which most hosts keep as the server's log.
There is no token to mint. The caller is the local process the host started, running as you, so every tool call is admitted with the stdio credential, which holds mcp=write and nothing past it: the same grant a connector token holds (see Tokens and grants). The activity trail records each call with that credential and the client's name.
To check the wiring by hand, pipe a handshake in:
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | magus mcp
Each line of output is one JSON-RPC reply. Typed at a terminal with nothing piped in, magus mcp waits for a host to speak; press Ctrl+C to stop it.
A host that launches one process per workspace gets one magus mcp per workspace. When several clients should share one warm server instead, use magus server.
A stdio server that runs a target asks the broker for host capacity like any other run.
Streamable HTTP: the server serves MCP
When magus server is running, it also exposes the MCP server over Streamable HTTP. MCP is always compiled in; it is a runtime layer you turn off with mcp.enabled=false (see Enabling and disabling) when you do not want it.
You don't need a separate process. Start the server as usual:
magus server start
The MCP endpoint comes up alongside it:
http://127.0.0.1:7391/mcp
magus doctor reports whether MCP is reachable and prints the endpoint URL.
Is MCP actually reachable?
An agent host connects to the MCP endpoint over HTTP; nothing starts that endpoint on
its own, so if the server is not running the tools silently disappear from the host.
magus status reports the endpoint's live health as its own block, checked independently
of the server's job socket:
mcp endpoint
url http://127.0.0.1:7391/mcp
state serving
The state is one of:
| state | meaning |
|---|---|
serving |
listening and a workspace is loaded - the tools are reachable |
not-ready |
listening, but no workspace is loaded yet |
unreachable |
nothing is listening; start the server with magus server start |
disabled |
turned off by mcp.enabled=false |
For scripts and container probes, magus status --probe=<kind> exits 0 healthy / 1
unhealthy. The kinds are liveness (the server answers), readiness (a workspace is
loaded), and mcp (this endpoint is reachable) - and they are comma-combinable, failing
if any listed check does:
magus status --probe=mcp # fail if the tools are unreachable
magus status --probe=liveness,mcp # fail if the server OR the endpoint is down
The server also serves /livez, /readyz, and /healthz on the same port. If state is
unreachable even though you expect a server, see
Keeping the server running.
With mcp.enabled: false the server still keeps the knowledge graph and symbol indexes
current; turning off MCP turns off the endpoint and nothing else.
Which transport when
The same tools answer on each transport. What differs is who can reach them and what proves who they are:
| Transport | Where | Credential | Use it for |
|---|---|---|---|
| stdio | magus mcp, launched by the host |
none; the host launched the process as you | one host driving the workspace it opens, with no server running |
| Streamable HTTP, socket | /mcp on the server's own server.sock |
the kernel's word that the peer runs as you | a local client that speaks HTTP over a unix socket and should share the server's warm graph |
| Streamable HTTP, loopback | http://127.0.0.1:7391/mcp |
a bearer token holding mcp=write |
a client that only takes a URL, runs as another user, or reaches the server through a tunnel or TLS |
Whichever the transport, each tool call is held to mcp=write again, and a caller
below it gets MGS9015 as a tool error.
MCP over the server socket
The server has one unix socket, server.sock in the private (0700) runtime directory,
and it speaks HTTP. /mcp is one path on it, beside the Connect APIs and the control
operations the CLI uses (see the server's socket):
$XDG_RUNTIME_DIR/magus/server.sock # else <user cache dir>/magus/run/, else /tmp/magus-<uid>/
It is Streamable HTTP, like the loopback endpoint, carried over the socket instead of TCP,
and it takes no token. What admits a caller is the user it runs as: for every connection
the server asks the kernel for the peer's uid (SO_PEERCRED on Linux, LOCAL_PEERCRED on
macOS) and admits only its own. Any other peer, root included, gets 403
MGS9022. An admitted call carries the
socket-peer credential, which holds mcp=write and console=write but never token
management, and the activity trail records each call under it. On a platform where magus
cannot read a peer's uid, the socket carries the control operations alone and MCP stays
on loopback.
To check it by hand, send a handshake with curl:
curl --unix-socket "$XDG_RUNTIME_DIR/magus/server.sock" http://magus/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}'
mcp.enabled: false takes /mcp off the socket and off loopback alike.
Available tools
Both transports expose these tools. This list is authoritative at the time of writing;
magus describe mcp-tools (or the magus_describe tool with kind: mcp_tools) prints
the live set with full parameters, so trust that over this table if they ever differ.
The catalog itself is generated from the std.Magus module descriptor, the same
declaration the Buzz bindings, the checker declarations and
the magus module reference come from. Declare an
MCPTool there to add one; nothing in the handler package is hand-listed.
Discover:
| Tool | Purpose |
|---|---|
magus_describe |
Describe a concept and list its entities: spells, targets, projects, workspaces, mcp_tools (pass name for one entity's detail) |
magus_describe_file |
Classify paths against declared globs: owning project, per-target claims, dependency edges, and declarations covering several paths |
magus_where |
Resolve a fuzzy project name to its absolute path |
magus_config_get |
Read the resolved workspace config (read-only) |
Run:
| Tool | Purpose |
|---|---|
magus_run_target |
Run a target (build, test, lint, ci, ...) for one or more projects |
magus_run_affected |
Run a target on only the VCS-affected projects |
magus_affected_plan |
Emit a provider-neutral CI shard plan for the affected set |
magus_affected_explain |
Explain why a project is in the affected set |
magus_buzz |
Run a Buzz program (magus buzz) over stdin and return its output |
magus_buzz is the tool for transforming another tool's output, in place of a shell
one-liner. Pass the earlier result as stdin and the script reads it with
io\stdin.readAll(); its main receives args. This call keeps the spell ids from a
magus_query result:
{
"script": "import \"std\"; import \"io\"; import \"encoding/json\";\nfun main(args: [str]) > void !> any {\n final q = json\\parse(io\\stdin.readAll() ?? \"null\") as {str: any};\n final ids = mut [<str>];\n foreach (m in q[\"matches\"] as [any]) {\n final hit = m as {str: any};\n if (hit[\"kind\"] == args[0]) { ids.append(hit[\"id\"] as str); }\n }\n std\\print(json\\stringify({\"kind\": args[0], \"ids\": ids}));\n}",
"args": "spell",
"stdin": "<the magus_query result text>",
"write": true
}
The reply carries the script's stdout and, because that stdout is JSON, the same value
parsed under json:
{"exit_code":0,"stdout":"{\"ids\":[\"spell:go\",\"spell:golangci\"],\"kind\":\"spell\"}\n","json":{"ids":["spell:go","spell:golangci"],"kind":"spell"}}
A compile or runtime error comes back as a tool error carrying the diagnostic, such as
[BZZ1005] buzz: line 1:32: .... magus buzz has no read-only mode, so a script
reaches whatever its fs, proc and http modules can; every call must pass
write: true to accept that, and one without it is refused before anything runs. A
run is bounded by target_timeout, or five minutes when that is unset.
Inspect:
| Tool | Purpose |
|---|---|
magus_doctor |
Validate workspace health (config, cache, cycles, tool availability) |
magus_status |
Report telemetry/cache settings and the live proc-server pool state |
magus_output |
Fetch one target execution's exact captured output by its out... ref |
magus_insight |
Lenses: hotspots, files, affinity, ownership, trend, unreferenced |
magus_vcs_checkpoint |
Resolve the working state's identity: revision, branch, dirty, patch digest; writes nothing |
Knowledge graph:
| Tool | Purpose |
|---|---|
magus_query |
Search the graph and return ranked matches plus their neighborhood |
magus_explain |
Show one node's data, edges with provenance, and how many nodes reach it |
magus_path |
Shortest path between two nodes: how two entities relate |
magus_refs |
Where a code symbol is defined and every file that references it (SCIP) |
magus_stats |
Graph shape: god nodes, orphans, doc coverage |
Review:
| Tool | Purpose |
|---|---|
magus_diff |
Join the review session a person has open and pair with them on it: op=state (default) returns the annotated changeset, comment, suggest, and resolve write to it, addressed by workspace-relative path and 0-based hunk digest |
Memory and scratch:
| Tool | Purpose |
|---|---|
magus_memory |
User-owned per-repo memory: list/get/put/delete/verify named entries shared across worktrees |
magus_job |
The orchestrating agent's declared jobs (list/fork/exec/exit/wait), recorded for humans to see; magus never enforces them |
| Tool | Purpose |
|---|---|
magus_console_present |
Return a tokenless link to a local console surface when the user asks to see dashboard status or output |
magus_console_present does not open a browser or hand a client a token. A compatible
desktop client may render its link as an action. Other clients can return the link as text.
Its open field is a shell command that opens the link signed in, for example
open "http://127.0.0.1:7391/console/dashboard/#code=$(magus config console token create --code --expires 12h)";
the code is a substitution the person's shell expands, so it never appears in the reply,
and it is a one-time code the console trades within a minute for a console token that
expires in 12 hours, never the operator token. The guard refuses that command to an
agent session, so a person runs it.
The console must be enabled and bound locally.
Config mutation is not exposed over MCP. Use the CLI for magus config set and related commands.
Enabling and disabling
MCP is on by default. To disable it:
# magus.yaml
mcp:
enabled: false
Or set MAGUS_MCP_ENABLED=0 in the environment before starting the server.
To change the listen address:
# magus.yaml
mcp:
address: "127.0.0.1:9000"
Or MAGUS_MCP_ADDRESS=127.0.0.1:9000.
A non-loopback address (0.0.0.0:7391 for a Kubernetes health probe, say) sends
every bearer token in cleartext, so the server refuses to start on one unless you
also set mcp.insecure_bind: true (or MAGUS_MCP_INSECURE_BIND=true). Front such
a listener with TLS or a tunnel.
Security: keep this local
Warning: Reaching the MCP endpoint is equivalent to having shell access to your build workspace. Any authenticated caller can execute arbitrary build targets, which in turn invoke arbitrary toolchain commands defined in your magusfiles.
magus mcp opens no listener: only the process that launched it can reach it, through its pipes. Everything below is about the daemon's HTTP endpoint.
The endpoint requires a bearer token whose grant includes mcp=write (see
Tokens and grants). Two kinds hold it:
- A connector token (
mgs_...) - a named, hashed-at-rest token you mint per external client (a Claude connector, an IDE). It holdsmcp=writeand nothing else. Only its SHA-256 is stored, so it is shown once at creation; rotate by minting a new one. It always expires: 90 days by default, at most 366. - The operator token (
mgo_...) - the one retrievable secret the server generates on first start and stores0600at$XDG_STATE_HOME/magus/mcp_token. It holds every surface, token management included, so give an MCP client a connector token instead. An agent session is deniedmagus config token printandgenerateby the guard.
Every /mcp request must carry Authorization: Bearer <token>. A request without one, or with a token that is wrong, expired or revoked, gets 401; a valid token without mcp=write (a console token) gets 403 MGS9015. Manage connector tokens with:
magus config mcp connector create --name claude # mint one (prints the secret once)
magus config mcp connector create --expires 366d # the longest a token lives; "never" is refused
magus config mcp connector ls # names, ids, grants, and expiry
magus config mcp connector revoke <name|id>
magus doctor warns 14 days before a stored token expires, so a client is
re-minted before it starts failing.
The token must be presented in the Authorization header; the /mcp endpoint
does not accept a token in the URL query string (RFC 6750 keeps secrets out of
logs and history). How you connect depends on the client:
-
Claude Code connects to the loopback endpoint directly with a header. Mint a connector token, then register the server at
userscope so every workspace the server serves shares one connection (the server binds one loopback port for all of them):magus config mcp connector create --name claude-code --expires 366d claude mcp add --transport http --scope user magus http://127.0.0.1:7391/mcp \ --header "Authorization: Bearer <token>"The token expires in a year;
magus doctornames it two weeks before, and the fix is the same two commands with a new token.claude mcp listshould then reportmagus ... - Connected. Restart the Claude Code session afterward: a session only discovers MCP tools (and skills installed bymagus agent install .claude/skills) at launch, so an already-open session will not see them until it is restarted. -
Cursor owns its MCP client config.
magus agent harness apply --id cursorprints a short setup hint and a docs pointer; it does not write.cursor/mcp.json. Register Magus under Settings -> Tools & MCP (or hand-write.cursor/mcp.json/~/.cursor/mcp.json) athttp://127.0.0.1:7391/mcp, preferably withAuthorization: Bearer ${env:MAGUS_MCP_TOKEN}so the secret stays out of the file. ExportMAGUS_MCP_TOKENwhere the Cursor GUI process inherits it (macOS Dock launches often miss shell-profile exports), then restart Cursor or toggle Magus under Tools & MCP. Confirm with Output -> MCP Logs andmagus status --probe=mcp. -
Codex uses user-level
~/.codex/config.tomlto register the local Streamable HTTP endpoint. Do not commit this client configuration. It contains no secret; the token comes from the process environment:[mcp_servers.magus] url = "http://127.0.0.1:7391/mcp" bearer_token_env_var = "MAGUS_MCP_TOKEN" enabled = trueFor Codex CLI, start the server, mint a connector token and store it as
MAGUS_MCP_TOKENin your local secret manager (it is shown once), export it in the shell that will launch Codex, then check registration and endpoint health:magus server start magus config mcp connector create --name codex --expires 366d codex mcp list magus status --probe=liveness,mcpFor the ChatGPT desktop app or Codex IDE extension, set the variable through the OS environment before launching or restarting the client; exporting it in a terminal does not configure an already-running app. Start a new task after the server comes up.
codex mcp listconfirms configuration, whilemagus status --probe=liveness,mcpconfirms the endpoint is live. If you changemcp.address, update the URL in~/.codex/config.tomltoo. In the desktop app,/mcpshows connected servers. Install matching guidance withmagus agent install .agents/skills, then paste theAGENTS.mdblock it prints; see Codex for why Codex wants both locations. -
Claude Desktop / other IDE plugins that take a Streamable-HTTP URL plus headers use the same shape:
{ "type": "streamable-http", "url": "http://127.0.0.1:7391/mcp", "headers": { "Authorization": "Bearer <token>" } }Prefer binding the token through the workspace secret provider (built-in environment provider: the ref
MAGUS_MCP_TOKEN) rather than pasting a plaintext secret into a committed file. Harness spells declare that ref viaharness_mcp;magus agent harness applyprints a host CLI command sketch and/or a docs pointer only - Magus does not write host MCP client config. Resolve the ref withmagus\secret.read("MAGUS_MCP_TOKEN")inside a magusfile when a spell needs the value; hosts read the env var directly.Clients whose connector UI only speaks OAuth (no static-header option) reach a loopback server through the
mcp-remotestdio bridge:npx -y mcp-remote http://127.0.0.1:7391/mcp --header "Authorization: Bearer <token>". -
The Claude API "MCP connector" cannot reach this server: it requires a public
https://URL and rejectshttp://and loopback addresses. Front the server with a TLS tunnel first if you need that path.
The server socket reads no token; the kernel's report of the peer's uid stands in for it, so anything running as you reaches it, just as it could read your operator token.
Treat the token as defense in depth, and still keep the port closed. The server binds to 127.0.0.1 by default, refuses any other address without mcp.insecure_bind: true, and validates the Host and Origin headers on every /mcp request, returning 403 Forbidden for non-loopback values to block browser-based DNS-rebinding attacks. Anyone who reads the token gains the same workspace access, so keep it local.
Do not expose it over:
- Tailscale, Zerotier, or similar overlay networks where other devices can reach it
- ngrok, localtunnel, or other public tunnels
- SSH
-Lport-forwards shared with others - Kubernetes
port-forwardin shared clusters - Any network ACL that admits untrusted hosts
If you need to drive magus remotely, run the CLI over SSH instead.