# MCP servers

Connect local and remote MCP servers with built-in tools, resources, OAuth, and exposure controls.

zot includes Model Context Protocol (MCP) support in its binary. No bridge installation or separate zot helper executable is required. Configured servers connect at CLI session startup, including interactive, print, stream, JSON, RPC, swarm, and continuous worker sessions.

> Built-in MCP requires a zot build containing commit `ee5c586`: [https://github.com/patriceckhart/zot/commit/ee5c586](https://github.com/patriceckhart/zot/commit/ee5c586). Older builds can use the optional bridge described in [Extensions](/docs/extensions).

## Quick setup

Run `/mcp` in the interactive TUI and press `a`, or run `/mcp add`, to open the add wizard. Choose the user or project configuration file, local command or remote URL, a server name, optional bearer-token environment variable, and tool exposure. Review the entry, then save and connect.

For manual configuration, create `$ZOT_HOME/mcp.json` for personal servers or `.zot/mcp.json` in the session working directory for project servers. When `ZOT_HOME` is unset, zot uses its normal state directory.

```json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
    },
    "docs": {
      "url": "https://example.com/mcp",
      "headers": { "Authorization": "Bearer ${DOCS_TOKEN}" }
    }
  }
}
```

The filesystem example requires Node.js and `npx` for that server, not for zot itself. Set `DOCS_TOKEN` before starting zot if you configure the example remote server. Run `/mcp reload` after editing configuration.

> **Trust configured servers**
>
> Loading MCP configuration can execute programs and make network requests before the first prompt. Per-tool confirmation does not authorize or sandbox server startup. Only load configuration from projects and servers you trust. Use `--no-mcp` to skip built-in MCP loading in an untrusted project.

## Configuration fields

A `command` selects stdio. A `url` selects streamable HTTP. `transport`, or its alias `type`, can explicitly select `stdio`, `streamable-http`, `http` (an alias for streamable HTTP), or legacy `sse`.

| Field | Meaning |
| --- | --- |
| command | One executable for stdio, not a shell expression. |
| args | Arguments passed directly to the executable. |
| env | Extra environment values for stdio. The process also inherits zot's environment. |
| cwd | Working directory for stdio. Defaults to the session directory. Relative values resolve against it. |
| url | Remote HTTP or SSE endpoint. |
| headers | HTTP headers. Prefer environment references over literal credentials. |
| enabled | Set to false to retain an entry without connecting. |
| connectTimeout | Maximum seconds for connection, initialization, and tool discovery. Default 30. |
| requestTimeout | Maximum seconds per tool invocation. Default 60. timeout is an alias. |
| exposure | deferred (default), direct, or hidden. |
| toolExposure | Per-tool exposure overrides by exact name or wildcard pattern. |
| oauth | Optional registered-client and authorization-server settings. |

Servers connect in parallel. Startup waits for discovery, bounded by each server's connection timeout. Invalid entries and failed servers are reported independently without discarding healthy connections. Continuous workers also report MCP failures without refusing to start.

Server names accept ASCII letters, digits, underscores, and hyphens. Names that normalize to the same namespace, such as `my-server` and `my_server`, conflict. The later name in lexical order is skipped. Tool names become `mcp__<server>__<tool>`, replacing other characters with underscores. Names longer than 64 characters get a stable hash suffix. Ambiguous normalized tool names are disabled rather than routed to the wrong tool.

## Project overrides and environment variables

A project entry with its own `command`, `url`, `type`, or `transport` replaces the user entry of the same name. Without those fields, it overrides only `enabled`, `exposure`, and `toolExposure`, retaining the user's connection and credentials. For example, `{"enabled": false}` disables a user server in one project.

`${NAME}` and `${NAME:-fallback}` expand once in `command`, `args`, `env` values, `cwd`, `url`, `headers` values, and `oauth.clientId`, `oauth.clientSecret`, and `oauth.authServerMetadataUrl`. Fallbacks apply only to unset variables. Explicitly empty values remain empty. Missing required variables disable the affected server and report the variable name, not its value.

Bare `$NAME`, shell commands, and references to sibling `env` entries are not expanded. Configuration files are not rewritten during expansion.

## Tool discovery and exposure

By default, the model sees `mcp__search_tools`. Searching by server name, tool name, or description loads matching deferred definitions for subsequent model calls. Search is local and never executes a remote tool. It loads up to 8 tools by default, with a maximum of 20.

| Exposure | Behavior |
| --- | --- |
| deferred | Discover tools through mcp__search_tools without advertising every schema up front. |
| direct | Always advertise tool definitions. Useful for small tool sets. |
| hidden | Connect and discover the server, but do not register its tools unless an individual override exposes them. |

`toolExposure` keys are exact remote tool names or patterns where `*` matches any characters. Exact names win, then the longest matching pattern, with lexical order breaking ties. A hidden server can expose only selected tools:

```json
{
  "mcpServers": {
    "github": {
      "url": "https://api.example.com/mcp",
      "exposure": "hidden",
      "toolExposure": { "issue_*": "deferred", "search_code": "direct" }
    }
  }
}
```

The tools view in `/mcp` shows effective exposure. Tool calls use the normal agent execution path, including confirmation and extension interception. Cancellation and request deadlines propagate to MCP requests. Text, images, structured content, and MCP error results are preserved. Server annotations do not grant permissions or replay rights. Input schemas that do not describe an object are advertised with an empty object schema because providers reject other types.

## Resources

Servers can offer resources such as files, documentation, or database schemas without implementing tools. When a connected, non-hidden server offers resources, zot registers:

- `mcp__list_resources`: list resource metadata with its server and URI. With `server`, return one page and an optional `nextCursor`. Without it, list all servers and pages, reporting individual failures without hiding healthy results.
- `mcp__list_resource_templates`: list URI templates for resources that servers do not list directly.
- `mcp__read_resource`: read content by `server` and `uri`. Text arrives as text. PNG, JPEG, GIF, and WebP images up to 512 KiB arrive inline. Other binary content is saved to a private temporary file and its path is returned.

Text from tool results and resource reads is limited to 64 KiB per content block. Longer text is saved whole to a temporary file, with a preview and path returned to the model. This is a per-block limit, not a total response or memory limit. Temporary files are removed when the MCP manager closes.

Resource tools reach only connected servers that offer resources. Hidden servers remain unreachable. They use the widest exposure among eligible servers, direct when any is direct and deferred otherwise. Interactive app resources with `ui://` URIs are left out because zot does not render them.

## Sign in with OAuth

HTTP servers that require OAuth need no credentials in `mcp.json`. A server answering 401 appears as `sign-in required` in `/mcp`. Select it and press `l`. zot opens the browser, waits for a local callback, stores the token, and reconnects. The dialog also shows the authorization URL. Press `esc` to cancel.

Over SSH or with a browser on another machine, the final redirect to `127.0.0.1` cannot reach zot. Copy the address from the browser and paste it into the dialog's redirect field, then press enter. A bare authorization code or `code#state` also works. A URL from another sign-in attempt is refused. Text-mode `/mcp login <server>` reads pasted redirects from the controlling terminal, never standard input.

zot discovers authorization-server metadata, supports dynamic client registration, uses PKCE, and refreshes access tokens automatically. Authorization failures during tool calls, resource requests, or discovery withdraw capabilities and offer sign-in again, even if a rejected token is still stored.

For servers requiring a pre-registered client:

```json
{
  "mcpServers": {
    "example": {
      "url": "https://mcp.example.com/mcp",
      "oauth": {
        "clientId": "my-client",
        "clientSecret": "${EXAMPLE_SECRET}",
        "callbackPort": 8765,
        "scopes": ["read"]
      }
    }
  }
}
```

`callbackPort` fixes the redirect URI to [http://127.0.0.1:<port>/callback](http://127.0.0.1:<port>/callback). Without it, zot selects a free port and registers a client for that URI. `authServerMetadataUrl` can override authorization-server discovery. A static `Authorization` header disables OAuth for that server.

Credentials and registered clients are stored in `$ZOT_HOME/mcp-auth.json`, keyed by server name and URL, with private file permissions where supported. Advertised resource-metadata URLs are checked against the resource origin before reuse. Concurrent requests within one runtime share token refreshes. Separate zot processes do not coordinate refreshes or logout.

Press `o` to sign out and delete stored credentials. Sign-out invalidates older connections and pending sign-ins within the runtime, so a stale refresh or authorization exchange cannot restore deleted credentials. It does not revoke tokens at the authorization server.

## Manage servers

The interactive `/mcp` manager lists server state, tool count, exposure, and defining file. Failed and invalid servers come first. Diagnostics omit raw transport details that could reveal URLs, headers, or arguments.

| Key | Action |
| --- | --- |
| up / down | Select a server. |
| enter / right | Open its detail view. |
| a | Add a server. |
| r | Reload all servers from the list, reconnect only the selected server from its detail view. |
| l | Sign in to the selected OAuth server. |
| o | Sign out. |
| t | Show a connected server's tools and effective exposure. |
| x | Change server exposure. |
| e | Enable or disable. |
| d | Remove after confirmation. |
| esc | Return to the previous view or close. |

Edits update the file defining the server, preserving unrelated fields and servers. Symlinked configurations are written through the link and existing file permissions are retained. Partial project overrides keep explicit enabled and deferred values when needed to override user settings.

Enabling, disabling, changing exposure, and signing in reconnect only the affected server. Adding or removing a server reloads MCP. The active turn is cancelled before reconnecting. The add wizard stores bearer tokens as environment-variable references, not pasted token values. In the wizard, `esc` steps back without losing input.

## Commands and opt-out flags

| Command or flag | Behavior |
| --- | --- |
| /mcp | Open the interactive manager. Text status is also available in print, JSON, and RPC modes. |
| /mcp add | Open the interactive add wizard. |
| /mcp help | Show text status and configuration paths. |
| /mcp reload | Reread configuration, reconnect servers, and rebuild the catalog without restarting extensions. |
| /mcp reconnect <server> | Reconnect one server. |
| /mcp login <server> | Sign in, printing the authorization URL to stderr in text modes. |
| /mcp logout <server> | Delete credentials and reconnect without them. |
| /reload-ext | Reload subprocess extensions, leaving built-in MCP connections alone. |
| --no-mcp | Skip built-in MCP loading for this run. |
| --no-tools | Also skip built-in MCP loading. |
| --no-ext | Disable subprocess extension discovery, not built-in MCP. |

Changing directory with `/cd` reconnects MCP for the new directory, applying its `.zot/mcp.json` and closing the previous project's servers.

## Connection behavior

Dropped stdio or SSE connections reconnect on the next tool call. Closing stderr alone does not disconnect a stdio server. Ended streamable HTTP sessions are reinitialized and the unaccepted request is sent once more. Resource calls recover ended sessions the same way. After reconnecting, zot checks the current catalog before invoking a tool or reading resources.

Network errors and HTTP 408, 429, and 5xx responses get up to two retries for initialization and discovery, with bounded backoff and `Retry-After` support. Tool calls are not replayed after network failures because they might already have run. Concurrent calls share a reconnect, while a waiting caller can cancel independently.

Servers announcing `tools.listChanged` update the catalog without a reload. Streamable HTTP also needs an open event stream. Without usable notifications, zot refreshes lists before a new prompt when they are older than 30 seconds. Cancellation stops this pre-turn discovery. Withdrawn tools are no longer callable.

Connections close at shutdown or MCP reload. For stdio, zot closes stdin, waits up to two seconds, sends `SIGTERM` to the process group, waits again, then kills the process group. Windows kills only the direct server process after the first wait, without an interrupt signal. Descendant cleanup is not guaranteed on Windows.

## Existing bridge installations

An enabled extension named `mcp` takes precedence over built-in support, avoiding duplicate connections. Remove it and restart zot, or run `/reload-ext` followed by `/mcp reload`, to use the built-in manager. Existing transport, command, environment, header, and request-timeout configuration works without conversion.

The optional bridge still provides setup templates described in [Extensions](/docs/extensions). `/mcp setup`, `/mcp start`, `/mcp stop`, `/mcp restart`, and `/mcp refresh` are bridge-only commands. Built-in support discovers tools at startup and keeps connections open until shutdown or reload. It does not use the bridge's tool cache or idle reaper, and ignores `idleTimeout`.

## Limitations

[Jail mode](/docs/jail) does not confine an MCP server's filesystem or network access. MCP servers run with the permissions of their process, regardless of local tool guardrails. Per-tool confirmation remains separate from configuration loading and server startup.

MCP prompts, server-initiated sampling, and interactive app rendering are not supported. The Go SDK does not automatically load MCP configuration. Extension and RPC wire formats are unchanged.
