> For the complete documentation index, see [llms.txt](https://mcp.avelino.run/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://mcp.avelino.run/reference/cli.md).

# CLI reference

Complete reference for all `mcp` commands.

## Output format

By default, `mcp` detects the output context:

* **Interactive terminal** — human-readable tables with colors
* **Piped or redirected** — JSON (for scripting with `jq`, etc.)

Use `--json` anywhere to force JSON output regardless of context:

```bash
mcp --list --json          # JSON even in terminal
mcp sentry --list --json   # JSON tool list
```

## Global commands

### `mcp --help`, `mcp -h`

Show usage information.

### `mcp --list`

List all configured servers.

```bash
mcp --list
```

Interactive output:

```
Server     Type   Endpoint
sentry     http   https://mcp.sentry.dev/sse
slack      stdio  npx -y slack-mcp-server@latest
grafana    stdio  uvx mcp-grafana

3 server(s) configured
```

JSON output (`--json` or piped):

```json
[
  { "name": "sentry", "type": "http", "url": "https://mcp.sentry.dev/sse" },
  { "name": "slack", "type": "stdio", "command": "npx", "args": ["-y", "slack-mcp-server@latest"] }
]
```

#### Backends that cannot start here

A `stdio` or `cli` backend whose `command` is not executable in this environment is reported without spawning anything. This is the common case inside a container, where a binary installed on your host is absent.

```json
[
  { "name": "notes", "type": "stdio", "command": "/opt/homebrew/bin/notes-tui", "args": [],
    "available": false,
    "unavailable_reason": "command does not exist: /opt/homebrew/bin/notes-tui" }
]
```

| Field                | When it appears                                        |
| -------------------- | ------------------------------------------------------ |
| `available`          | Only on a backend that cannot start, always `false`    |
| `unavailable_reason` | Alongside `available`, one of the three messages below |

A runnable backend carries **neither** key, so `select(.available)` matches nothing even when everything is healthy. Filter on the reason instead:

```bash
mcp --list --json | jq -r '.[] | select(.unavailable_reason) | "\(.name): \(.unavailable_reason)"'
```

The reasons:

| Message                             | Meaning                                          |
| ----------------------------------- | ------------------------------------------------ |
| `command not found in PATH: <name>` | Bare name, no `PATH` entry provides it           |
| `command does not exist: <path>`    | Explicit path, nothing there                     |
| `command is not executable: <path>` | Exists, but is a directory or has no execute bit |

An `http` backend is never marked unavailable: reachability needs a request, and `--list` reads config without touching the network. A backend that pins its own `PATH` in `env` is resolved against that `PATH`, not the one `mcp` runs with.

In the table output, a `Status` column appears only when at least one backend is unavailable, and the footer becomes `N server(s) configured, M unavailable here`.

## Global flags

### `--json`

Force JSON output. Can be placed anywhere in the command:

```bash
mcp --json --list
mcp sentry --list --json
mcp sentry search_issues '{"query": "..."}' --json
```

## Server commands

### `mcp <server> --list`

Connect to the server and list available tools.

```bash
mcp sentry --list
```

Interactive output:

```
Tool                  Description
search_issues         Search for issues in Sentry
get_issue_details     Get details of a specific issue
search_events         Search events in a project

3 tool(s) available
```

If the server name alone is provided (no flags, no tool), this is the default behavior:

```bash
mcp sentry          # same as mcp sentry --list
```

### `mcp <server> --info`

Like `--list`, but includes parameter details for each tool.

```bash
mcp sentry --info
```

Interactive output:

```
search_issues
  Search for issues in Sentry
  Parameters:
    query string — The search query (required)
    project string — Project slug
    sort string — Sort order

get_issue_details
  Get details of a specific issue
  Parameters:
    issue_id string — The issue ID (required)

2 tool(s) available
```

JSON output includes full JSON Schema for each tool's input parameters.

### `mcp <server> --health`

Connect to the server and disconnect. Exits non-zero if the connection fails.

```bash
mcp sentry --health
```

Interactive output:

```
sentry: ok
```

JSON output also reports the MCP protocol revision the two sides agreed on — the quickest way to see whether a backend negotiated `2026-07-28` or fell back to the legacy `initialize` handshake:

```bash
mcp sentry --health --json
```

```json
{"server":"sentry","status":"ok","protocolVersion":"2025-11-25"}
```

With `MCP_PROXY_URL` set, the connection goes through the running `mcp serve` proxy, so `protocolVersion` is the revision negotiated with the *proxy*, not with the backend behind it.

### `mcp <server> <tool> [json]`

Call a tool on the server. The optional `json` argument is a JSON object with the tool's parameters.

```bash
mcp sentry search_issues '{"query": "is:unresolved"}'
```

If `json` is omitted:

* **Interactive terminal** — Uses `{}` (empty object)
* **Piped input** — Reads JSON from stdin

Interactive output prints text content directly:

```
Found 23 issues matching query "is:unresolved level:error"
```

Errors are prefixed with `error:` on stderr.

JSON output wraps content in the MCP protocol format:

```json
{
  "content": [
    { "type": "text", "text": "Found 23 issues..." }
  ],
  "isError": false
}
```

Content items have a `type` field:

* `"text"` — Text content in the `text` field
* `"image"` — Base64-encoded image in `data` field, with `mimeType`
* `"resource"` — Embedded resource content

## Proxy commands

### `mcp serve`

Start a proxy server that aggregates all configured backends into a single MCP endpoint.

```bash
mcp serve               # stdio mode (default)
mcp serve --http        # HTTP mode on 127.0.0.1:8080
mcp serve --http :9090  # HTTP mode on custom port
mcp serve --http 0.0.0.0:8080 --insecure  # HTTP on all interfaces
```

The proxy connects to every server in `servers.json`, merges their tool lists with namespaced names (`server__tool`), and routes `tools/call` requests to the correct backend.

#### Stdio mode (default)

Designed to be used as a stdio transport in any MCP client:

```json
{
  "mcpServers": {
    "all": {
      "command": "mcp",
      "args": ["serve"]
    }
  }
}
```

Diagnostics are logged to stderr. Protocol messages use stdin/stdout.

#### HTTP mode (`--http`)

Exposes the proxy over HTTP with the following endpoints:

| Method | Path       | Description                                   |
| ------ | ---------- | --------------------------------------------- |
| `POST` | `/mcp`     | JSON-RPC 2.0 request/response endpoint        |
| `GET`  | `/mcp/sse` | SSE endpoint for streaming (per MCP spec)     |
| `GET`  | `/health`  | Health check (returns JSON status, see below) |

Default bind address is `127.0.0.1:8080` (localhost only). To bind to a different address:

```bash
mcp serve --http 127.0.0.1:9090
mcp serve --http :3000              # shorthand for 0.0.0.0:3000 — requires --insecure
```

#### `--insecure`

Allow binding to non-loopback addresses without TLS. Required when using addresses like `0.0.0.0`, `192.168.x.x`, etc. Without this flag, the server refuses to start on non-loopback interfaces to prevent accidental plaintext exposure.

#### Health check (`GET /health`)

Returns the proxy status as JSON:

```json
{
  "status": "ok",
  "backends_configured": 9,
  "backends_connected": 3,
  "active_clients": 5,
  "tools": 213,
  "version": "0.4.3"
}
```

| Field                 | Description                                                                |
| --------------------- | -------------------------------------------------------------------------- |
| `status`              | Always `"ok"`                                                              |
| `backends_configured` | Total servers in `servers.json`                                            |
| `backends_connected`  | Backends currently running (others are idle-shutdown or not yet connected) |
| `active_clients`      | Number of SSE sessions currently registered                                |
| `tools`               | Total tools across all backends (including idle ones — tools are cached)   |
| `version`             | `mcp` binary version                                                       |

`backends_connected` should **not** grow with `active_clients` — that's the proxy doing its job (N clients sharing M backends). If they grow together, clients may be bypassing the proxy.

#### Graceful shutdown

The HTTP server handles `SIGTERM` and `SIGINT` (Ctrl+C) gracefully: stops accepting new connections, finishes in-flight requests, and disconnects all backends.

See [**Proxy mode guide**](/guides/proxy-mode.md) for full details, client configuration examples, and team setup.

### `mcp reload`

Apply a config change to proxies that are already running.

```bash
mcp reload                          # validate, refresh caches, signal local proxies
mcp reload --url http://host:8080   # also reload a remote proxy via POST /reload
mcp reload --json                   # report as JSON
```

`--url` sends `MCP_ADMIN_TOKEN` as a bearer token. Over plain HTTP to a host that is not loopback, that send is refused: the token administers a proxy fronting every configured backend, so leaking it is worse than a failed reload. Use `https`, or pass `--insecure` to accept the risk.

`mcp serve` reads `servers.json` once, at boot. `mcp add foo` or an edit to the file does not reach a proxy that is already running. Restarting it works, but kills every stdio backend child and every open session. `mcp reload` avoids that.

In order. Only step 1 aborts the command, because every later step assumes a valid config. Steps 2 to 5 all run, and failures among them are reported together at the end:

1. **Validate** the config and report the server count and any [reserved-name](/reference/config-file.md#reserved-names) conflicts. An invalid config exits non-zero and nothing else runs.
2. **Evict stale cache entries.** Tool-cache entries are dropped when their config hash no longer matches, or when their server left the config. Classifier cache entries go with them, plus any entry for a server the config no longer knows: a classification outlives its tool cache entry, so without that second sweep a removed server would keep entries in `tool-classification.json` forever.
3. **Rewrite installed completions.** Only files that already exist (fish and zsh). It never creates one, and never touches `.bashrc`.
4. **Signal every local proxy** with `SIGHUP`.
5. With `--url`, **`POST <url>/reload`** to a remote proxy, with the token from [`MCP_ADMIN_TOKEN`](/reference/environment-variables.md#mcp_admin_token). `--url` adds to the local step, it does not replace it. See [Reloading a running proxy](/guides/proxy-mode.md#reloading-a-running-proxy) for the endpoint.

#### What a running proxy reloads

* **Servers.** The proxy diffs the new config against the old one. A backend whose config did not change is not touched: same child process, same usage stats. Added backends are discovered, changed ones are shut down and reconnected with the new config, removed ones are shut down.
* **`serverAuth`.** Providers, ACL and `adminRoles` are swapped in one step. If the new `serverAuth` fails to build, the reload is rejected and the old auth stays in place.
* **Tool cache and classifier cache**, for backends that changed or were removed.

If the new config is invalid, the proxy keeps serving the old one.

#### What still needs a restart

Some state is fixed at boot. A reload applies everything else and lists these under `requires_restart` instead of claiming it applied them:

| Reported as                     | Change                        | Why                                                 |
| ------------------------------- | ----------------------------- | --------------------------------------------------- |
| `serverAuth.providers.oauth_as` | Turning `oauth_as` on or off  | Its router is mounted at boot                       |
| `audit.output`                  | A different audit destination | The database pool and the logger are opened at boot |

The bind address is not in that list: it comes from `--http`, not from the config file, so reloading the file can never change it.

#### Filters are kept

A proxy started with `--only` or `--exclude` re-applies the same filter on every reload. A reload never makes it publish a backend it was told to hide.

#### How local proxies are found

Each `mcp serve` writes `<pid>.json` in `~/.config/mcp/instances/` (mode `0700`) and holds an `flock` on it until it exits. The lock proves the proxy is alive, not the pid: the kernel drops the lock when the process dies, however it dies. A leftover file whose lock is free is removed, and a recycled pid never gets a signal. That matters because the default action of `SIGHUP` is to terminate the process.

Signal-based reload is Unix only. On other platforms use `--url`.

#### Output

```
config     /Users/you/.config/mcp/servers.json (9 servers)
caches     2 evicted: roam, grafana, 14 classifications dropped
shell      fish /Users/you/.config/fish/completions/mcp.fish
instance   http 127.0.0.1:7332 pid 4121 reloaded
instance   http 127.0.0.1:7333 pid 4188 reloaded
remote     http://proxy.internal:8080/reload +1 ~2 -0, 148 tools
```

The `remote` summary reads `+added ~changed -removed`. When the proxy reports `requires_restart`, it appends `, N change(s) need a restart`. The `shell` line appears once per rewritten file. With no proxy running the `instance` line reads `none running`.

With `--json`:

```json
{
  "config_path": "/Users/you/.config/mcp/servers.json",
  "servers": 9,
  "caches": {
    "evicted": ["roam", "grafana"],
    "classifications_forgotten": 14
  },
  "completions": [
    { "shell": "fish", "path": "/Users/you/.config/fish/completions/mcp.fish" }
  ],
  "instances": [
    { "pid": 4121, "target": "http 127.0.0.1:7332", "reloaded": true }
  ],
  "remote": {
    "url": "http://proxy.internal:8080/reload",
    "authenticated": true,
    "status": 200,
    "body": {
      "reloaded": true,
      "added": ["novo"],
      "changed": ["roam", "grafana"],
      "removed": [],
      "tools": 148,
      "requires_restart": []
    }
  }
}
```

`name_conflicts`, `completions` and `remote` are left out when empty or unused. `reloaded: false` on an instance carries an `error` string. `authenticated` says whether the request carried a token, which is what tells a 401 apart from a rejected token.

`SIGHUP` is asynchronous: `reloaded: true` means the signal was delivered, not that the proxy accepted the new config. A proxy that rejects the config logs it and keeps serving the old one. Check with `mcp reload --url` (which returns the result) or `GET /health`.

#### Exit status

* `0`: config is valid and every targeted proxy was reached. Finding **no** running proxy is a success: caches and completions were still refreshed, and the next `mcp serve` reads the new config.
* non-zero: the config is invalid, a local proxy could not be signalled, or `--url` failed to connect or answered a non-2xx status. A non-2xx answer is explained in the error: `401` names `MCP_ADMIN_TOKEN`, `403` names `serverAuth.adminRoles`, and `404` says reload is disabled on that proxy.

## Registry commands

### `mcp search <query>`

Search the MCP server registry.

```bash
mcp search filesystem
mcp search "database sql"
```

Interactive output shows a table of matching servers. JSON output returns full server metadata including repository URL and install instructions.

### `mcp add <name>`

Add a server from the registry. Looks up the server by name, generates a config entry, and writes it to `servers.json`.

```bash
mcp add filesystem
```

Fails if:

* Server not found in registry
* Server already exists in config
* Name is reserved (`search`, `add`, `remove`, `update`, `list`, `help`, `version`, `serve`, `logs`, `acl`, `config`, `completions`, `healthcheck`, `reload`)

### `mcp add --url <url> <name>`

Add an HTTP server manually.

```bash
mcp add --url https://api.example.com/mcp my-server
```

### `mcp remove <name>`

Remove a server from the config file.

```bash
mcp remove filesystem
```

Fails if the server is not in the config.

### `mcp update <name>`

Refresh a server's config entry from the registry, preserving your customizations.

```bash
mcp update github
```

Use this when the registry metadata for a server changed (new package version, new env vars, updated args) and you want to pull the changes without losing what you customized locally.

**What gets refreshed (from the registry):**

* `command` and `args`
* `url` (HTTP servers)
* `env` schema — new vars are added as `${VAR_NAME}` placeholders, vars removed from the registry are dropped

**What is preserved (your customizations):**

* Filled-in `env` values (anything that isn't a `${VAR_NAME}` placeholder)
* `idle_timeout`, `min_idle_timeout`, `max_idle_timeout`
* `headers`, `forward_identity` (HTTP servers)
* `tool_acl`
* `cli`, `cli_help`, `cli_depth`, `cli_only`, `tools`

If the entry already matches the registry, the file is not rewritten and `mcp` reports `already up to date`. New env vars introduced by the update are listed at the end so you know what to fill in.

Fails if:

* Server is not in the local config (use `mcp add <name>` first)
* Server is not in the registry

> If the server changed type in the registry (stdio ↔ http), `mcp update` warns and drops type-specific fields that no longer apply.

## Config commands

### `mcp config path`

Print the resolved path to the config file (`servers.json`).

```bash
mcp config path
# /Users/you/.config/mcp/servers.json
```

With `--json`, includes metadata:

```json
{
  "path": "/Users/you/.config/mcp/servers.json",
  "exists": true,
  "dir": "/Users/you/.config/mcp"
}
```

### `mcp config edit`

Open the config file in your editor. Uses `$EDITOR`, falls back to `$VISUAL`, then `vi` (or `notepad` on Windows).

```bash
mcp config edit
```

If the config file doesn't exist, it's created with `{}` before opening.

## Shell completions

### `mcp completions <shell>`

Generate shell completion scripts. Supported shells: `bash`, `zsh`, `fish`.

```bash
# Fish (recommended for the current session)
mcp completions fish | source

# Fish (persistent)
mcp completions fish > ~/.config/fish/completions/mcp.fish

# Bash
mcp completions bash >> ~/.bashrc
source ~/.bashrc

# Zsh
mcp completions zsh > "${fpath[1]}/_mcp"
autoload -Uz compinit && compinit
```

Completions cover all subcommands, flags, and nested subcommands (`config path|edit`, `acl classify|check`, `logs` flags, etc.).

## ACL commands

### `mcp acl classify`

Classify every tool of every configured backend as `read`, `write`, or `ambiguous`, using the automatic classifier combined with manual `tool_acl` overrides from `servers.json`. This is metadata only — no enforcement path changes yet.

```bash
mcp acl classify                      # all servers, table output
mcp acl classify --server grafana     # one server
mcp acl classify --format json        # machine-readable
```

**Flags:**

* `--server <alias>` — restrict to one backend
* `--format table|json` — override the auto-detected output format

**Table columns:**

| Column    | Meaning                                                               |
| --------- | --------------------------------------------------------------------- |
| `SERVER`  | Backend alias                                                         |
| `TOOL`    | Upstream tool name (not namespaced)                                   |
| `KIND`    | `read`, `write`, or `ambiguous`                                       |
| `CONF`    | Classifier confidence (0.00–1.00)                                     |
| `SOURCE`  | `override`, `annotation`, `classifier`, or `fallback`                 |
| `reasons` | Which signals fired (name tokens, description patterns, schema hints) |

Rows prefixed with `[!]` are **ambiguous** — the classifier could not decide. They are treated as `write` at runtime (fail-safe). Add a `tool_acl` entry in `servers.json` to pin them explicitly.

**Example (JSON):**

```bash
mcp acl classify --server databricks --format json | jq '.[] | {tool, kind}'
```

The JSON form is an array of `{server, tool, kind, confidence, source, reasons}` objects, suitable for scripting or diffing across config changes.

See [Tool ACL overrides](/reference/config-file.md#tool-acl-overrides) for how to pin tools manually, and [`docs/acl-redesign-plan.md`](https://github.com/avelino/mcp/tree/main/docs/acl-redesign-plan.md) for the full redesign context.

### `mcp acl check`

Test an ACL decision without starting the proxy. Useful for validating policy changes before rolling them out.

```bash
mcp acl check --subject alice --server grafana --tool query_prometheus
mcp acl check --subject bob --server databricks --tool execute_sql --access write
mcp acl check --role dev --server github --all-tools
mcp acl check --subject alice --server github --all-tools --format json
mcp acl check --subject alice --server sentry --resource "issue://123"
mcp acl check --subject bob --server ai --prompt summarize
```

**Flags:**

* `--subject <name>` — subject to check (looks up roles from `subjects` map in ACL config)
* `--server <alias>` — backend server alias (required)
* `--tool <name>` — single tool to check
* `--resource <uri>` — resource URI to check (mutually exclusive with `--tool` and `--prompt`)
* `--prompt <name>` — prompt name to check (mutually exclusive with `--tool` and `--resource`)
* `--access read|write` — override the tool classification (if omitted, the CLI connects to the backend to classify the tool automatically; not applicable to resources/prompts)
* `--role <name>` — check a hypothetical role (creates a synthetic identity)
* `--all-tools` — connect to the backend, list all tools, and check each one
* `--format table|json` — override the auto-detected output format

One of `--tool`, `--resource`, `--prompt`, or `--all-tools` is required.

**Single-tool output:**

```
ALLOW  via dev[0]  access=read  classification=classifier:read (confidence 0.72)
```

**Multi-tool output (`--all-tools`):**

```
TOOL                                     DECISION RULE                      ACCESS KIND       SOURCE      CONF
query_prometheus                         ALLOW  dev[0]                    read   read       classifier  0.72
update_dashboard                         DENY   default                   -      write      classifier  0.81
```

**Exit code:** `0` for allow, `1` for deny (single-tool mode only). `--all-tools` always exits `0` since mixed results are expected.

**Examples:**

```bash
# CI pre-deploy check: ensure dev role can read grafana
mcp acl check --role dev --server grafana --tool query_prometheus || echo "BLOCKED"

# Audit what a role can reach on a server
mcp acl check --role dev --server github --all-tools --format json | jq '.[] | select(.decision=="DENY")'
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://mcp.avelino.run/reference/cli.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
