> 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/config-file.md).

# Config file reference

## Location

Default: `~/.config/mcp/servers.json`

Override: `MCP_CONFIG_PATH` environment variable.

## Schema

```json
{
  "mcpServers": {
    "<name>": <ServerConfig>,
    ...
  }
}
```

## ServerConfig

Three variants, distinguished by their fields:

### Stdio server

```json
{
  "command": "npx",
  "args": ["-y", "package-name"],
  "env": {
    "KEY": "value"
  }
}
```

| Field              | Type      | Default      | Description                                                                                |
| ------------------ | --------- | ------------ | ------------------------------------------------------------------------------------------ |
| `command`          | string    | *required*   | Executable to spawn                                                                        |
| `args`             | string\[] | `[]`         | Arguments passed to the command                                                            |
| `env`              | object    | `{}`         | Environment variables for the process                                                      |
| `tool_acl`         | object    | `null`       | Manual read/write classification overrides (see [Tool ACL overrides](#tool-acl-overrides)) |
| `idle_timeout`     | string    | `"adaptive"` | Idle shutdown policy (see [Idle timeout](#idle-timeout))                                   |
| `min_idle_timeout` | string    | `"1m"`       | Minimum idle timeout for adaptive mode                                                     |
| `max_idle_timeout` | string    | `"5m"`       | Maximum idle timeout for adaptive mode                                                     |

### HTTP server

```json
{
  "url": "https://example.com/mcp",
  "headers": {
    "Authorization": "Bearer token"
  }
}
```

| Field              | Type   | Default      | Description                                                                                                |
| ------------------ | ------ | ------------ | ---------------------------------------------------------------------------------------------------------- |
| `url`              | string | *required*   | Server endpoint URL                                                                                        |
| `headers`          | object | `{}`         | HTTP headers for every request                                                                             |
| `forward_identity` | object | `null`       | Send the **caller's** identity per request (see [Forwarding caller identity](#forwarding-caller-identity)) |
| `tool_acl`         | object | `null`       | Manual read/write classification overrides (see [Tool ACL overrides](#tool-acl-overrides))                 |
| `idle_timeout`     | string | `"adaptive"` | Idle shutdown policy (see [Idle timeout](#idle-timeout))                                                   |
| `min_idle_timeout` | string | `"1m"`       | Minimum idle timeout for adaptive mode                                                                     |
| `max_idle_timeout` | string | `"5m"`       | Maximum idle timeout for adaptive mode                                                                     |

#### Forwarding caller identity

The proxy authenticates the caller and enforces the ACL, then talks to the backend with **one credential resolved at startup**. That is the right model when the backend's data has no per-row owner — Sentry, Slack, Databricks: the shared token *is* the service account, and the proxy decides who may reach it.

It stops being the right model when the backend's data **is** owned per user: an internal app catalogue, a notes service, anything that records an author. There a shared credential makes every write land under the same name, and it does not fail loudly — it records the wrong owner, silently.

`forward_identity` sends the authenticated caller's `subject` to the backend as a header, per request:

```json
{
  "url": "http://hub.internal/mcp",
  "headers": { "Authorization": "Bearer ${HUB_SERVICE_TOKEN}" },
  "forward_identity": { "header": "X-MCP-Subject" }
}
```

| Field          | Type   | Default           | Description                                              |
| -------------- | ------ | ----------------- | -------------------------------------------------------- |
| `header`       | string | `"X-MCP-Subject"` | Header carrying the caller's `subject`                   |
| `roles_header` | string | `null`            | Optional header with the caller's roles, comma-separated |

> ⚠️ **The backend must not be reachable by anyone who could set this header themselves.** If it is, the header is a complete authentication bypass — the caller just claims to be someone else. In practice: the backend accepts it only from the proxy's address, over a private link. This is the condition the proxy cannot verify, which is why the feature is opt-in per server instead of on by default.

The identity travels on every method that reaches the backend: `tools/call`, `resources/read` and `prompts/get`. A resource read is where per-user data comes *back*, so leaving it out would have a per-user backend answer every caller as the service account.

Notes:

* **HTTP backends only.** A stdio/CLI backend is a process the proxy owns, and there is no per-request channel to carry a header into a long-lived stdin pipe.
* **The caller must be authenticated.** With no `serverAuth.providers` configured every caller is `anonymous`, and forwarding that would have the backend record `anonymous` as the author and believe it. Such a call is refused; configure a provider, or drop `forward_identity` from the server.
* A `subject` (or role) that is **empty** or contains a **control character** is refused, not sent without identity: falling back to the shared credential is exactly the silent-wrong-owner outcome this feature removes. An empty header asserts nothing, and most backends read it as no caller at all.
* Neither header may be one the HTTP transport writes itself: `Authorization`, `Mcp-Session-Id`, `Mcp-Method`, `Mcp-Name`, `Mcp-Protocol-Version`, `traceparent`, `tracestate`. `Authorization` is the one worth naming. The transport re-adds a saved OAuth token whenever the config map has no usable one, and the 401 retry path removes the config entry before doing so, so a forwarded header of that name would travel beside a bearer token that the `headers` check below never sees.
* `roles_header` is off by default — most backends only need to know *who*, and sending roles a backend does not read is avoidable exposure.
* `roles_header` **may not equal** `header` (compared case-insensitively). Two same-named headers with unrelated values leave the choice to the receiver, which could read the roles value as the subject; the call is refused instead.
* Neither header may share a name with an entry in this server's static `headers` (case-insensitively). Both would be sent, as two lines of one field, and the backend would pick the winner. This is what a migration off a hardcoded subject header looks like when the old entry is left behind, so the call is refused rather than sent ambiguous.
* A backend's own `x-mcp-header` annotation **cannot claim** the identity header's name. Argument-derived headers are always emitted as `Mcp-Param-{Name}`, so a tool annotating `X-MCP-Subject` produces `Mcp-Param-X-MCP-Subject` and lands beside the identity header rather than over it. The proxy additionally drops an argument-derived header whose full name collides with a forwarded one, which is reachable only if you configure `header` under the `Mcp-Param-` prefix yourself.

### CLI server

```json
{
  "command": "kubectl",
  "cli": true,
  "cli_help": "--help",
  "cli_depth": 2,
  "cli_only": ["get", "describe", "logs"]
}
```

| Field              | Type      | Default      | Description                                                                                |
| ------------------ | --------- | ------------ | ------------------------------------------------------------------------------------------ |
| `command`          | string    | *required*   | CLI executable to wrap                                                                     |
| `cli`              | bool      | *required*   | Must be `true` — marks this as a CLI server                                                |
| `cli_help`         | string    | `"--help"`   | Flag used to discover subcommands and options                                              |
| `cli_depth`        | number    | `2`          | How deep to recurse into subcommands for flag discovery                                    |
| `cli_only`         | string\[] | `[]` (all)   | Whitelist of subcommands to expose                                                         |
| `args`             | string\[] | `[]`         | Base arguments prepended to every invocation                                               |
| `env`              | object    | `{}`         | Environment variables for the CLI process                                                  |
| `tools`            | array     | `[]`         | Preset tool definitions (skips auto-discovery when set)                                    |
| `tool_acl`         | object    | `null`       | Manual read/write classification overrides (see [Tool ACL overrides](#tool-acl-overrides)) |
| `idle_timeout`     | string    | `"adaptive"` | Idle shutdown policy (see [Idle timeout](#idle-timeout))                                   |
| `min_idle_timeout` | string    | `"1m"`       | Minimum idle timeout for adaptive mode                                                     |
| `max_idle_timeout` | string    | `"5m"`       | Maximum idle timeout for adaptive mode                                                     |

See the [CLI as MCP guide](/guides/cli-as-mcp.md) for discovery details and examples.

## Idle timeout

Controls when the proxy shuts down idle backend connections to reclaim resources. Applies to both stdio and HTTP backends in proxy mode (`mcp serve`).

### Policy values

| Value                  | Behavior                                                                              |
| ---------------------- | ------------------------------------------------------------------------------------- |
| `"adaptive"` (default) | Timeout adjusts based on usage frequency — frequently used backends stay alive longer |
| `"never"`              | Never shut down — backend stays connected for the entire proxy lifetime               |
| `"<duration>"`         | Fixed timeout (e.g. `"3m"`, `"30s"`, `"1h"`)                                          |

Duration format: number followed by `s` (seconds), `m` (minutes), or `h` (hours). Plain numbers are treated as seconds.

### Adaptive mode

When `idle_timeout` is `"adaptive"` (the default), the proxy tracks how often each backend is used and assigns a timeout tier:

| Usage tier | Requests/hour | Idle timeout |
| ---------- | ------------- | ------------ |
| Hot        | > 20          | 5 min        |
| Warm       | 5–20          | 3 min        |
| Cold       | < 5           | 1 min        |

The tier is computed from the backend's total request count divided by its uptime. The result is clamped between `min_idle_timeout` (default `1m`) and `max_idle_timeout` (default `5m`).

When a backend is shut down due to inactivity, its tools remain visible in `tools/list`. On the next `tools/call`, the proxy reconnects automatically (lazy initialization). Usage history is preserved across reconnections so the adaptive algorithm has continuity.

### Examples

```json
{
  "mcpServers": {
    "slack": {
      "command": "npx",
      "args": ["@anthropic/mcp-slack"],
      "idle_timeout": "adaptive",
      "min_idle_timeout": "30s",
      "max_idle_timeout": "5m"
    },
    "sentry": {
      "url": "https://mcp.sentry.io",
      "idle_timeout": "never"
    },
    "github": {
      "command": "npx",
      "args": ["@modelcontextprotocol/server-github"],
      "idle_timeout": "2m"
    }
  }
}
```

## Tool ACL overrides

The proxy ships with an automatic classifier that labels every tool of every upstream MCP as `read`, `write`, or `ambiguous` (treated as write, fail-safe). The classifier is auditable — run `mcp acl classify` to see the verdict, confidence, source, and reasons for each tool.

When the classifier is wrong (or when you just want to be explicit), add `tool_acl` to any server and pin individual tools to `read` or `write` using the same glob syntax as the ACL rules (`*`, prefix, suffix, contains).

```json
{
  "mcpServers": {
    "grafana": {
      "command": "mcp-grafana",
      "tool_acl": {
        "read":  ["get_*", "list_*", "search_*", "find_*", "query_*", "generate_deeplink"],
        "write": ["update_dashboard", "create_*", "alerting_manage_*"]
      }
    },
    "databricks": {
      "command": "databricks-mcp",
      "tool_acl": {
        "read":  ["execute_sql_read_only", "poll_sql_result"],
        "write": ["execute_sql"]
      }
    }
  }
}
```

Semantics:

* Both `read` and `write` are optional — omit either or both.
* Overrides run **before** the classifier. A tool that matches an override is never scored.
* The same pattern string may not appear in both `read` and `write` for the same server — that fails loudly at load time.
* If two different globs on the same server both match a single tool name (e.g. `get_*` in `read` and `*_thing` in `write` both match `get_thing`), the proxy fails safe to `write` at classification time. Narrow your globs to avoid this.
* Overrides are **never cached** — they are re-read from the config on every startup.

For the full redesign plan and the token/description dictionaries the classifier uses, see [`docs/acl-redesign-plan.md`](https://github.com/avelino/mcp/tree/main/docs/acl-redesign-plan.md).

## Type detection

The config uses serde's untagged enum deserialization. The type is inferred from the fields:

* Has `command` + `cli: true` → CLI
* Has `command` (without `cli`) → Stdio
* Has `url` → HTTP

CLI is checked first, then Stdio, then HTTP.

## Environment variable substitution

Any `${VAR_NAME}` in a string value is replaced with the env var's value at load time.

```json
{
  "env": { "TOKEN": "${MY_SECRET}" },
  "headers": { "Authorization": "Bearer ${API_KEY}" },
  "url": "https://${HOST}/mcp"
}
```

Missing env vars resolve to empty string `""`.

## Reserved names

These names cannot be used as server names:

* `search`
* `add`
* `remove`
* `update`
* `list`
* `help`
* `version`
* `serve`
* `logs`
* `acl`
* `config`
* `completions`
* `healthcheck`
* `reload`

Using a reserved name won't break the config, but you'll get a warning and the server may be shadowed by built-in commands.

## Server authentication (`serverAuth`)

Optional. Configures authentication for `mcp serve --http`. Ignored for direct CLI usage.

```json
{
  "mcpServers": { ... },
  "serverAuth": {
    "providers": ["<provider>", "<provider>"],
    "bearer": { ... },
    "forwarded": { ... },
    "oauthAs": { ... },
    "acl": { ... },
    "adminRoles": ["<role>"]
  }
}
```

### Providers

`providers` is an array of provider names, evaluated in order as a chain. The first provider that accepts the request wins; if all reject, the chain returns the error of the *first* provider configured (oracle-resistant). Empty array (or omitted) is equivalent to anonymous access.

| Value         | Description                                                                                                                                                                                            |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `"none"`      | Anonymous identity (`subject: "anonymous"`, no roles). Useful for testing.                                                                                                                             |
| `"bearer"`    | Static bearer token validation. Reads from `bearer` sub-config.                                                                                                                                        |
| `"forwarded"` | Trust reverse proxy header (e.g. `X-Forwarded-User`). Reads from `forwarded` sub-config.                                                                                                               |
| `"oauth_as"`  | OAuth 2.0 Authorization Server with Dynamic Client Registration — the route required by Claude.ai / ChatGPT / Cursor. Reads from `oauthAs` sub-config. See the [OAuth AS how-to](/how-to/oauth-as.md). |

> **Schema change.** Earlier versions used a single `provider: "..."` string. The new schema is the array `providers: [...]`. Configs carrying the legacy field deserialize into an empty providers list and boot as `NoAuth` — silently weakening auth, so an explicit migration is required. Booting with a missing sub-config (e.g. `"bearer"` listed without a `bearer` block) fails at startup rather than degrading.

### Bearer config

Required when `"bearer"` is in `providers`. Each entry in `tokens` accepts two shapes:

* **Legacy (string):** `"<token>": "<subject>"` — subject only, no roles.
* **Extended (object):** `"<token>": { "subject": "<subject>", "roles": ["<role>", ...] }` — subject plus roles used by ACL evaluation.

Both forms can coexist in the same file.

```json
{
  "bearer": {
    "tokens": {
      "secret-abc": "alice",
      "secret-def": { "subject": "bob", "roles": ["dev", "oncall"] }
    }
  }
}
```

| Field    | Type   | Description                                                    |
| -------- | ------ | -------------------------------------------------------------- |
| `tokens` | object | Map of token → subject string **or** `{subject, roles}` object |

Each extended entry:

| Field     | Type      | Default    | Description                                         |
| --------- | --------- | ---------- | --------------------------------------------------- |
| `subject` | string    | *required* | User identity for this token                        |
| `roles`   | string\[] | `[]`       | Roles assigned to this identity (used by ACL rules) |

### Forwarded config

Optional when `"forwarded"` is in `providers`. Reads the authenticated user from a header set by a trusted reverse proxy, and optionally reads a groups header to populate roles.

```json
{
  "forwarded": {
    "header": "x-forwarded-user",
    "groups_header": "x-forwarded-groups"
  }
}
```

| Field           | Type   | Default                | Description                                                               |
| --------------- | ------ | ---------------------- | ------------------------------------------------------------------------- |
| `header`        | string | `"x-forwarded-user"`   | Header name to read the authenticated user from                           |
| `groups_header` | string | `"x-forwarded-groups"` | Header name to read roles from (comma-separated, oauth2-proxy convention) |

Groups header value is parsed as a comma-separated list: each entry is trimmed and empty entries are dropped. Missing header yields empty roles (not an error). Role matching is case-sensitive.

> Only use `forwarded` behind a trusted reverse proxy. The proxy **must** strip these headers from incoming client requests — otherwise a client could forge identity and roles.

### OAuth AS config (`oauthAs`)

Required when `"oauth_as"` is in `providers`. Turns `mcp serve` into an OAuth 2.0 Authorization Server with Dynamic Client Registration so Claude.ai, ChatGPT, Cursor and other AI clients can connect. User authentication is delegated to a trusted reverse proxy (oauth2-proxy / Cloudflare Access / Pomerium) — `mcp serve` never handles passwords.

```json
{
  "oauthAs": {
    "issuerUrl": "https://mcp.example.com",
    "jwtSecret": "${MCP_OAUTH_AS_JWT_SECRET}",
    "trustedUserHeader": "x-forwarded-user",
    "trustedGroupsHeader": "x-forwarded-groups",
    "trustedSourceCidrs": ["10.0.0.0/8"],
    "accessTokenTtlSeconds": 3600,
    "refreshTokenTtlSeconds": 2592000,
    "authorizationCodeTtlSeconds": 60,
    "scopesSupported": ["mcp"],
    "redirectUriAllowlist": [
      "https://claude.ai/api/mcp/auth_callback",
      "https://chat.openai.com/aip/*/oauth/callback"
    ],
    "injectedRoles": ["oauth-user"]
  }
}
```

| Field                         | Type      | Required | Default                | Description                                                                                                                       |
| ----------------------------- | --------- | -------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `issuerUrl`                   | string    | yes      | —                      | Public origin the AS advertises in metadata and embeds as `iss` in JWTs. Must match what clients reach.                           |
| `jwtSecret`                   | string    | yes      | —                      | HMAC-SHA256 signing key. **Must be ≥ 32 bytes** — boot fails otherwise.                                                           |
| `trustedUserHeader`           | string    | no       | `"x-forwarded-user"`   | Header read at `/authorize` to identify the human.                                                                                |
| `trustedGroupsHeader`         | string    | no       | `"x-forwarded-groups"` | Comma-separated → JWT `groups` claim.                                                                                             |
| `trustedSourceCidrs`          | string\[] | yes      | —                      | CIDRs allowed to reach `/authorize`. **Empty list rejected at boot** — without it any client could spoof the trusted user header. |
| `accessTokenTtlSeconds`       | number    | no       | `3600`                 | JWT lifetime.                                                                                                                     |
| `refreshTokenTtlSeconds`      | number    | no       | `2592000` (30d)        | Refresh-token lifetime.                                                                                                           |
| `authorizationCodeTtlSeconds` | number    | no       | `60`                   | Authorization-code lifetime.                                                                                                      |
| `scopesSupported`             | string\[] | no       | `[]`                   | Scopes advertised in metadata.                                                                                                    |
| `redirectUriAllowlist`        | string\[] | yes      | —                      | Patterns clients may register. Trailing `*` allowed (for ChatGPT-style URIs).                                                     |
| `injectedRoles`               | string\[] | no       | `[]`                   | Roles always added to issued JWTs — useful as a "came in via OAuth" marker for ACL discrimination.                                |

State (registered clients via DCR + refresh tokens) persists to `auth_server.json` in the config directory; override with `MCP_AUTH_SERVER_PATH` or inline via `MCP_AUTH_SERVER_CONFIG`. See the [OAuth AS how-to](/how-to/oauth-as.md) for setup, security notes, and troubleshooting.

### Admin roles (`adminRoles`)

Optional. Array of role names allowed to call `POST /reload` on the proxy. Default `[]`, which leaves the endpoint off (it answers `404`). A config without the key behaves exactly as before.

```json
{
  "serverAuth": {
    "providers": ["bearer"],
    "bearer": {
      "tokens": {
        "secret-abc": { "subject": "alice", "roles": ["dev"] },
        "secret-ops": { "subject": "ops-bot", "roles": ["admin"] }
      }
    },
    "adminRoles": ["admin"]
  }
}
```

Here only `ops-bot` can reload the proxy. `alice` gets `403`.

* Only `adminRoles` grants reload. A tool or resource grant in the ACL never implies it, including a `*` grant.
* `"*"` in `adminRoles` is not a wildcard. Roles are compared exactly, so it only matches a role literally named `*`. A wildcard would hand the proxy to any role an upstream identity provider happens to emit.
* An anonymous identity is always denied, whatever the roles say.
* The list is part of `serverAuth`, so changing it takes effect on the next [reload](/reference/cli.md#mcp-reload).

See [Reloading a running proxy](/guides/proxy-mode.md#reloading-a-running-proxy) for the endpoint.

### ACL config

Optional. Controls which users can access which tools. Supports two schemas: **role-based** (recommended) and **legacy** (backward compatible). Detection is automatic — see [schema detection](#schema-detection).

#### Role-based schema (recommended)

```json
{
  "acl": {
    "default": "deny",
    "strictClassification": false,
    "roles": {
      "admin": [{ "server": "*", "access": "*" }],
      "dev": [
        { "server": ["github", "grafana"], "access": "read" },
        { "server": "github", "access": "write", "tools": ["gh_pr", "gh_issue"] }
      ],
      "readonly": [{ "server": "*", "access": "read" }]
    },
    "subjects": {
      "alice": { "roles": ["admin"] },
      "bob":   { "roles": ["dev"] },
      "charlie": {
        "roles": ["readonly"],
        "extra": [{ "server": "sentry", "access": "read" }]
      }
    }
  }
}
```

| Field                  | Type                  | Default   | Description                                                |
| ---------------------- | --------------------- | --------- | ---------------------------------------------------------- |
| `default`              | `"allow"` \| `"deny"` | `"allow"` | Policy when no grant matches                               |
| `strictClassification` | bool                  | `false`   | Block ambiguous tools entirely (require explicit override) |
| `roles`                | object                | `{}`      | Map of role name → list of grants                          |
| `subjects`             | object                | `{}`      | Map of subject → `{ roles, extra }`                        |

#### Grant

| Field       | Type                           | Default              | Description                                                       |
| ----------- | ------------------------------ | -------------------- | ----------------------------------------------------------------- |
| `server`    | string or string\[]            | *required*           | Server alias(es) to match (`"*"` = any)                           |
| `access`    | `"read"` \| `"write"` \| `"*"` | *required*           | Access level                                                      |
| `tools`     | string\[]                      | `[]` (all tools)     | Tool name globs to narrow the grant                               |
| `resources` | string\[]                      | `[]` (all resources) | Resource URI globs to narrow the grant. Same `*` syntax as tools. |
| `prompts`   | string\[]                      | `[]` (all prompts)   | Prompt name globs to narrow the grant. Same `*` syntax as tools.  |
| `deny`      | bool                           | `false`              | Turns grant into explicit deny (always wins over allows)          |

#### Subject config

| Field   | Type      | Default | Description                                              |
| ------- | --------- | ------- | -------------------------------------------------------- |
| `roles` | string\[] | `[]`    | Roles assigned to this subject (merged with token roles) |
| `extra` | Grant\[]  | `[]`    | Additional per-subject grants                            |

#### Access expansion

| `access`  | Read tools | Write tools | Ambiguous tools | Ambiguous (strict) |
| --------- | ---------- | ----------- | --------------- | ------------------ |
| `"read"`  | allowed    | denied      | denied          | denied             |
| `"write"` | denied     | allowed     | allowed         | **denied**         |
| `"*"`     | allowed    | allowed     | allowed         | **denied**         |

#### Evaluation model

1. Collect all grants from all roles (token roles + subject config roles) + `extra`
2. Filter to grants matching the target server and tool
3. If any matching grant has `deny: true` → **deny**
4. If any matching allow grant covers the access level → **allow**
5. No match → apply `default`

Union-based, order-independent. Deny always wins.

#### Legacy schema

```json
{
  "acl": {
    "default": "allow",
    "rules": [
      {
        "subjects": ["bob"],
        "roles": ["viewer"],
        "tools": ["sentry__*"],
        "policy": "deny"
      }
    ]
  }
}
```

| Field     | Type                  | Default   | Description                                  |
| --------- | --------------------- | --------- | -------------------------------------------- |
| `default` | `"allow"` \| `"deny"` | `"allow"` | Default policy when no rule matches          |
| `rules`   | array                 | `[]`      | Ordered list of ACL rules (first match wins) |

#### Legacy ACL rule

| Field      | Type                  | Default          | Description                                                                    |
| ---------- | --------------------- | ---------------- | ------------------------------------------------------------------------------ |
| `subjects` | string\[]             | `[]` (match all) | User subjects to match (`*` = any)                                             |
| `roles`    | string\[]             | `[]` (match all) | Roles to match (`*` = any)                                                     |
| `tools`    | string\[]             | *required*       | Tool name patterns (supports `*` wildcards — prefix, suffix, middle, multiple) |
| `policy`   | `"allow"` \| `"deny"` | *required*       | Action when rule matches                                                       |

Both `subjects` and `roles` must match for a rule to apply. Empty means "match all".

#### Schema detection

| JSON keys present                             | Schema used               |
| --------------------------------------------- | ------------------------- |
| `roles` (as object) or `subjects` (as object) | Role-based                |
| `rules` (as array)                            | Legacy                    |
| Both `rules` and `roles`/`subjects`           | Config error              |
| Neither                                       | Legacy with default allow |

#### Tool pattern glob syntax

The `tools` field supports glob patterns with `*` wildcards (both schemas):

| Pattern              | Matches                           | Example                                |
| -------------------- | --------------------------------- | -------------------------------------- |
| `sentry__*`          | Anything starting with `sentry__` | `sentry__search_issues`                |
| `*_issues`           | Anything ending with `_issues`    | `search_issues`, `sentry__list_issues` |
| `*admin*`            | Anything containing `admin`       | `admin_panel`, `user_admin_tools`      |
| `sentry__*_admin__*` | Multiple wildcards                | `sentry__team_admin__delete`           |
| `my_tool`            | Exact match (no wildcards)        | `my_tool`                              |
| `*`                  | Everything                        | any tool                               |

## Auth store

Tokens and OAuth client registrations are stored separately in:

```
~/.config/mcp/auth.json
```

```json
{
  "clients": {
    "https://server-url": {
      "client_id": "registered-client-id",
      "client_secret": "optional-secret"
    }
  },
  "tokens": {
    "https://server-url": {
      "access_token": "the-token",
      "refresh_token": "optional-refresh-token",
      "expires_at": 1710000000
    }
  }
}
```

Keys are normalized server URLs (trailing slash removed).


---

# 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/config-file.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.
