---
title: "Run Orchestrator Zero from an MCP client"
description: "Ask Claude Code or another MCP client why a job waits, which node is unhealthy, and have it block the node, with your rights and your confirmation."
canonical_url: "https://docs.oz0.ai/fleet/mcp-client"
---
# Run Orchestrator Zero from an MCP client

> Ask Claude Code or another MCP client why a job waits, which node is unhealthy, and have it block the node, with your rights and your confirmation.

For: cluster operators.

Connect a client with your operator identity or permitted tenant key. Inspect the changes it proposes and confirm those you intend to apply.

Orchestrator Zero has an MCP server for operators. An MCP client, such as Claude Code, lists nodes and their health, explains why a job waits, reads traces, costs and the audit log, and acts: it blocks a node, cancels a job, rolls back a rollout or decides an approval. Every call runs with your role, which the APIs check as they do for the CLI, and every change asks you first.

This is not the [MCP server for agents](https://docs.oz0.ai/build/apps/mcp-server), which runs a tenant's agents as tools on the edge. This one operates the cluster, on the management port. Which MCP clients were run against the agents' server, and what each did, is in the [interop matrix](https://docs.oz0.ai/build/apps/interop).

## Connect

### In one command: `oz0 setup`

`oz0 setup` writes what a coding assistant reads to use Orchestrator Zero, in the project's own files, or in your own configuration with `--global`: this server's entry where the tool reads one, and the [build kit's](https://docs.oz0.ai/build/plugins/claude-code) skills in `.agents/skills` for every tool:

```bash [Terminal]
oz0 setup claude-code      # .mcp.json, and the kit in .agents/skills and .claude/skills
oz0 setup codex            # .codex/config.toml, a section of AGENTS.md, the kit in .agents/skills
oz0 setup gemini           # .gemini/settings.json, a section of GEMINI.md, the kit
oz0 setup cursor           # .cursor/mcp.json, the kit
oz0 setup copilot          # .mcp.json (GitHub Copilot's CLI, coding agent and VS Code), the kit
oz0 setup opencode         # opencode.json, the kit
oz0 setup goose --global   # ~/.config/goose/config.yaml, the kit in ~/.agents/skills
```

<table>
<thead>
  <tr>
    <th>
      Assistant
    </th>
    
    <th>
      The MCP server
    </th>
    
    <th>
      Also
    </th>
    
    <th>
      With <code>
        --global
      </code>
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        claude-code
      </code>
    </td>
    
    <td>
      <code>
        .mcp.json
      </code>
    </td>
    
    <td>
      The kit in <code>
        .claude/skills
      </code>
      
       as well, which Claude Code reads instead of <code>
        .agents/skills
      </code>
    </td>
    
    <td>
      <code>
        ~/.claude.json
      </code>
      
      , <code>
        ~/.agents/skills
      </code>
      
       and <code>
        ~/.claude/skills
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        codex
      </code>
    </td>
    
    <td>
      <code>
        .codex/config.toml
      </code>
      
      , <code>
        [mcp_servers.orchestrator-zero]
      </code>
    </td>
    
    <td>
      A section of <code>
        AGENTS.md
      </code>
    </td>
    
    <td>
      <code>
        $CODEX_HOME/config.toml
      </code>
      
       and <code>
        $CODEX_HOME/AGENTS.md
      </code>
      
       (<code>
        ~/.codex
      </code>
      
       by default)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        gemini
      </code>
    </td>
    
    <td>
      <code>
        .gemini/settings.json
      </code>
    </td>
    
    <td>
      A section of <code>
        GEMINI.md
      </code>
    </td>
    
    <td>
      <code>
        ~/.gemini/settings.json
      </code>
      
       and <code>
        ~/.gemini/GEMINI.md
      </code>
      
       (<code>
        $GEMINI_CLI_HOME
      </code>
      
       instead of <code>
        ~
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cursor
      </code>
    </td>
    
    <td>
      <code>
        .cursor/mcp.json
      </code>
    </td>
    
    <td>
      
    </td>
    
    <td>
      <code>
        ~/.cursor/mcp.json
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        copilot
      </code>
    </td>
    
    <td>
      <code>
        .mcp.json
      </code>
      
      , which VS Code reads too
    </td>
    
    <td>
      
    </td>
    
    <td>
      <code>
        $COPILOT_HOME/mcp-config.json
      </code>
      
       (<code>
        ~/.copilot
      </code>
      
       by default)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        opencode
      </code>
    </td>
    
    <td>
      <code>
        opencode.json
      </code>
      
      , under <code>
        mcp
      </code>
    </td>
    
    <td>
      
    </td>
    
    <td>
      <code>
        ~/.config/opencode/opencode.json
      </code>
      
       (<code>
        $XDG_CONFIG_HOME
      </code>
      
       instead of <code>
        ~/.config
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        goose
      </code>
    </td>
    
    <td>
      none: goose reads MCP servers only from your own configuration
    </td>
    
    <td>
      
    </td>
    
    <td>
      <code>
        ~/.config/goose/config.yaml
      </code>
      
      , under <code>
        extensions
      </code>
    </td>
  </tr>
</tbody>
</table>

The server it adds is `orchestrator-zero`, which runs `oz0 mcp`: the bridge below, which reads your CLI context when the assistant starts it. No token, key or password is written into any file, so the files can be committed and shared; each person's own context decides what the tools may do. `oz0 setup --context <file>` names a context file (only its path goes into the entry), and `--command` the binary to start, such as `/usr/local/bin/oz0`.

The section of `AGENTS.md` or `GEMINI.md` says what the server is for, that a secret never goes into a file or a prompt (`oz0 secret set` reads it from standard input), and where the documentation for assistants is: `llms.txt` lists every page, and each page is also served as Markdown under `/raw/`, such as `/raw/fleet/mcp-client.md`. `--docs-url` points it at a mirror of these docs.

- Safe to run again. A second run changes nothing. A file it changes is copied to `<file>.oz0-backup-<time>` first; the rest of the file (your other servers, your own instructions) stays as it is.
- `--update` also replaces the kit's skills of another version that nobody changed since setup wrote them, and says what changed.
- `--dry-run` prints the changes as a diff and writes nothing.
- `--remove` takes out only what setup wrote: its server entry, its marked section, the skills as it wrote them. A skill you edited, or an `orchestrator-zero` entry that does not start `oz0 mcp`, stays; setup says so.
- It only writes files. It never runs the assistant, installs, downloads or signs in to anything. A project file that is a symbolic link is refused; in your home folder a link is followed.

Claude Code also writes `~/.claude.json` itself, so run `oz0 setup claude-code --global` while no Claude Code session runs, or use the project's `.mcp.json`. The assistant asks before it uses a project's MCP server the first time: Claude Code asks you to approve the project's servers, and Codex reads a project's `.codex/config.toml` only in a project you trust; Gemini CLI and GitHub Copilot read a project's skills and servers once you trust its folder.

### Claude Code on your machine, with your operator certificate

The CLI serves the tools on stdio and sends each request to the management port with the operator certificate of its context (`OZ0_CONTEXT`, or `--context`), as every other command does:

```bash [Terminal]
claude mcp add orchestrator-zero -- oz0 mcp
```

With another context, or another client, run the same command, `oz0 mcp`, as the server. For a client's JSON configuration:

```json [.mcp.json]
{
  "mcpServers": {
    "orchestrator-zero": {
      "command": "orchestrator-zero",
      "args": ["mcp"],
      "env": { "OZ0_CONTEXT": "/home/ada/.config/orchestrator-zero/prod/context.json" }
    }
  }
}
```

An operator certificate is a cluster admin's, so the tools can do what `orchestrator-zero` can. Logs go to stderr; stdout carries only MCP.

### Over HTTP, with a tenant API key

A client that connects to remote servers uses `https://<management>:8443/admin/mcp` with a tenant [API key](https://docs.oz0.ai/build/apps/runtime-api/authentication#authentication) as the bearer token. The key's role is its role here:

<table>
<thead>
  <tr>
    <th>
      Role
    </th>
    
    <th>
      Can
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        reader
      </code>
    </td>
    
    <td>
      Read the tenant: nodes, jobs, why they wait, plugins, rollouts, costs, quotas, findings
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        operator
      </code>
    </td>
    
    <td>
      Also cancel and retry jobs and decide approvals
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        admin
      </code>
    </td>
    
    <td>
      Also block and unblock nodes, start and roll back rollouts, set kill switches, read the audit log
    </td>
  </tr>
</tbody>
</table>

```bash [Terminal]
oz0 apikey create claude-ops --tenant acme --role admin --expires 30d
claude mcp add --transport http orchestrator-zero https://mgmt.example.com:8443/admin/mcp \
  --header "Authorization: Bearer oz0_..."
```

An [admin key](https://docs.oz0.ai/build/apps/runtime-api/authentication#admin-keys) works here too, with the permissions it names instead of a role: each tool's call needs the permission its admin API call does, so a key with `audit.read` reads the audit log and is refused the rest. A tool that reaches the runtime API (starting, cancelling or retrying jobs, deciding approvals) needs a runtime key of the role above.

```bash [Terminal]
oz0 apikey create claude-audit --tenant acme --admin --permission audit.read --permission tenant.read --expires 30d
```

The management port's certificate comes from the cluster's CA: the client must trust `ca.crt` (for Node.js clients, `NODE_EXTRA_CA_CERTS=ca.crt`). A key works only for its own tenant, and a runtime key reaches the admin API only through this endpoint. A console session and a CLI session (`oz0 login`) do not work here: the endpoint refuses them.

## Tools

Reading tools are annotated `readOnlyHint: true`; acting tools are not, and those that stop, cancel or replace something are `destructiveHint: true`, so your client can ask before it calls them.

<table>
<thead>
  <tr>
    <th>
      Tool
    </th>
    
    <th>
      What it does
    </th>
    
    <th>
      Needs
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        list_tenants
      </code>
    </td>
    
    <td>
      The tenants you may see, with budgets and this month's spend
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list_nodes
      </code>
      
      , <code>
        get_node
      </code>
    </td>
    
    <td>
      Nodes: state, online, health and why, labels, runtime, queues
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list_jobs
      </code>
      
      , <code>
        get_job
      </code>
    </td>
    
    <td>
      Recent jobs from Temporal, running ones too; one job's status, cost and approvals
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        why_waiting
      </code>
    </td>
    
    <td>
      What a job waits for, most important first: a node, a person, a budget, a retry, a child job
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        get_job_trace
      </code>
      
      , <code>
        get_job_step
      </code>
    </td>
    
    <td>
      A job's steps and findings; what one step read and wrote
    </td>
    
    <td>
      reader; the step needs content
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list_approvals
      </code>
    </td>
    
    <td>
      What waits for a person, with deadlines
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list_findings
      </code>
    </td>
    
    <td>
      Recent jobs with warnings, such as a possible loop
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list_plugins
      </code>
      
      , <code>
        list_rollouts
      </code>
      
      , <code>
        get_rollout
      </code>
      
      , <code>
        list_runtimes
      </code>
    </td>
    
    <td>
      Plugins, rollouts and their gates, runtime versions
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        get_costs
      </code>
      
      , <code>
        list_quotas
      </code>
    </td>
    
    <td>
      Costs by model and agent; quotas and what they have used
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list_kill_switches
      </code>
    </td>
    
    <td>
      Kill switches, active and released
    </td>
    
    <td>
      reader
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        list_audit
      </code>
    </td>
    
    <td>
      The audit log, newest first: 25 entries a page, <code>
        limit
      </code>
      
       up to 100, and <code>
        page_token
      </code>
      
       for the next
    </td>
    
    <td>
      admin
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cancel_job
      </code>
    </td>
    
    <td>
      Cancel a job and the jobs it delegated to
    </td>
    
    <td>
      operator, confirmed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        retry_job
      </code>
    </td>
    
    <td>
      Run an ended job again under a new ID
    </td>
    
    <td>
      operator
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        decide_approval
      </code>
    </td>
    
    <td>
      Approve, deny, or answer a question
    </td>
    
    <td>
      operator, confirmed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        block_node
      </code>
      
      , <code>
        unblock_node
      </code>
    </td>
    
    <td>
      Block a node, or let it work again
    </td>
    
    <td>
      admin; blocking is confirmed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        start_rollout
      </code>
      
      , <code>
        roll_back_rollout
      </code>
    </td>
    
    <td>
      Move nodes to a runtime version step by step, or roll a rollout back
    </td>
    
    <td>
      admin, confirmed
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        set_kill_switch
      </code>
      
      , <code>
        release_kill_switch
      </code>
    </td>
    
    <td>
      Stop a tenant, an agent or a tool across the fleet, or release it
    </td>
    
    <td>
      admin; setting is confirmed
    </td>
  </tr>
</tbody>
</table>

Tools that act in a tenant take `tenant`; without it, an API key acts in its own tenant and an operator in `default`. Answers are the APIs' own, as JSON text and as `structuredContent`, with the proto field names.

Every caller sees every tool. When your role is not enough, the tool answers with an error that says which role it needs, for example `permission_denied: apikey:3f9c... is a reader in tenant acme, and blocking a node needs an admin`. What jobs read and wrote, prompts, answers and tool data, needs the [content permission](https://docs.oz0.ai/admin/tenants): without it, `get_job` leaves the answer out and `get_job_step` is refused.

## Confirmations

A destructive tool requires confirmation through the client before it acts:

- Clients that can ask (MCP 2026-07-28 with form elicitation, such as the Python SDK, or any client through `oz0 mcp`) show a question such as *Block node node-3f2a? It stops taking work and its certificate stops working. Reason: disk is failing*, with one checkbox. Only a yes carries it out; a no answers *The person did not confirm block_node: nothing was done*. These clients are asked even when the call says `confirm: true`.
- Clients that cannot ask (over HTTP in the 2025 revisions) get the question back as an error, and must call again with `"confirm": true` once you agreed. The client must obtain your agreement before sending that flag.

The server verifies the client's response; it cannot prove that a person saw the question or answered it. Use a client that presents the confirmation to you and prevents the model from supplying the answer itself.

An elicitation confirmation is good for ten minutes, for the same caller, tool and arguments.

## The audit log

Every call is in the audit log as `mcp.call`, by you (`operator:<name>` or `apikey:<id>`), with `via: mcp`, the tenant, the tool, the IDs it acted on and the outcome: `ok`, `denied`, `error`, `invalid`, `confirmation_asked`, `declined` or `unconfirmed`, and for a change whether you confirmed it when asked or with the flag. The entries the change itself writes, such as `node.block`, `job.retry` and an approval's `run.approval.decide`, say `via: mcp` too. When management runs apart from the edges, the entries an edge writes (`run.approval.decide`, `run.start`, `job.retry`) do not, and the `mcp.call` entry is the record.

The entry goes to the tenant the call names when you may read that tenant, and otherwise to your own (an API key's tenant, or the cluster's entries for an operator). It keeps the IDs the tool takes, up to 200 bytes each, and no other argument: an approval's comment stays out of the audit log.

```bash [Terminal]
oz0 audit --action mcp.
oz0 audit --action node.block
```

Arguments are not recorded, so an approval's comment or answer stays out of the log.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
