# MCP servers

MCP (Model Context Protocol) servers extend AI agents with additional tools and context. An MCP server exposes capabilities -- file search, database queries, API integrations -- that an agent can invoke during a run.

Moat supports three types of MCP servers:

- **Remote MCP servers** -- External HTTPS services accessed through Moat's credential-injecting proxy
- **Host-local MCP servers** -- Services running on the host machine, bridged to the container through the proxy relay
- **Sandbox-local MCP servers** -- Child processes running inside the container

This guide covers configuring all three types, granting credentials, and troubleshooting common issues.

For prepackaged language servers (Go, TypeScript, Python), see [Language servers](./01-claude-code.md#language-servers) in the Claude Code guide.

## Prerequisites

- Moat installed
- An agent configured (Claude Code, Codex, or Gemini)
- For remote MCP servers: the server URL and any required credentials
- For sandbox-local MCP servers: the server package name or executable

## Remote MCP servers

Remote MCP servers are external HTTPS services declared at the top level of `moat.yaml`. Moat injects credentials at the network layer -- the agent never sees raw tokens.

### 1. Grant credentials

Store credentials for the MCP server with `moat grant mcp`:

```bash
$ moat grant mcp context7
Enter credential for MCP server 'context7': ***************
MCP credential 'mcp-context7' saved to ~/.moat/credentials/mcp-context7.enc
```

The credential is encrypted and stored locally. The grant name follows the pattern `mcp-<name>`.

For public MCP servers that do not require authentication, skip this step.

### 2. Configure in moat.yaml

Declare the MCP server at the top level of `moat.yaml` (not under `claude:`, `codex:`, or `gemini:`):

```yaml
mcp:
  - name: context7
    url: https://mcp.context7.com/mcp
    auth:
      grant: mcp-context7
      header: CONTEXT7_API_KEY
```

**Required fields:**

| Field | Description |
|-------|-------------|
| `name` | Unique identifier for the server |
| `url` | HTTPS endpoint for remote servers. HTTP is allowed for host-local servers (`localhost`, `127.0.0.1`, `[::1]`) |

**Optional fields:**

| Field | Description |
|-------|-------------|
| `auth.grant` | Grant name to use (format: `mcp-<name>`) |
| `auth.header` | HTTP header name for credential injection |

Omit the `auth` block for public MCP servers that do not require authentication:

```yaml
mcp:
  - name: public-tools
    url: https://public.example.com/mcp
```

### 3. Run the agent

```bash
moat claude ./my-project
```

No additional flags are needed. Moat reads the `mcp:` section from `moat.yaml` and configures the proxy relay automatically.

### Multiple remote servers

Configure multiple MCP servers in a single `moat.yaml`:

```yaml
mcp:
  - name: context7
    url: https://mcp.context7.com/mcp
    auth:
      grant: mcp-context7
      header: CONTEXT7_API_KEY

  - name: notion
    url: https://notion-mcp.example.com
    auth:
      grant: mcp-notion
      header: Notion-Token
```

Grant all required credentials before running:

```bash
moat grant mcp context7
moat grant mcp notion
```

## Host-local MCP servers

Host-local MCP servers run on your host machine (outside the container). Containers cannot reach `localhost` on the host directly, so Moat's proxy relay bridges the connection.

### Configure in moat.yaml

Declare host-local servers in the `mcp:` section with an `http://localhost` or `http://127.0.0.1` URL:

```yaml
mcp:
  - name: my-tools
    url: http://localhost:3000/mcp
```

Authentication is optional. If the host-local server requires credentials:

```yaml
mcp:
  - name: my-tools
    url: http://localhost:3000/mcp
    auth:
      grant: mcp-my-tools
      header: Authorization
```

### Start the MCP server on the host

Start your MCP server on the host before running the agent:

```bash
# Start your MCP server (example)
my-mcp-server --port 3000 &

# Run the agent
moat claude ./my-project
```

The proxy relay forwards requests from the container to `localhost:3000` on the host.

### Mixing remote and host-local servers

Remote and host-local servers can be configured together:

```yaml
mcp:
  - name: context7
    url: https://mcp.context7.com/mcp
    auth:
      grant: mcp-context7
      header: CONTEXT7_API_KEY

  - name: local-tools
    url: http://localhost:3000/mcp
```

## How the relay works

Some agent HTTP clients do not respect `HTTP_PROXY` for MCP connections, so Moat routes MCP traffic through a local relay endpoint on the proxy. The agent connects to the relay, the relay injects credentials from the grant store, and forwards requests to the real MCP server. For host-local servers, the relay forwards to `localhost` on the host, bridging the network boundary between the container and the host. The agent never has access to raw credentials, and all MCP traffic appears in network traces and audit logs.

See [Proxy architecture](/moat/concepts/proxy) for details on how the relay works.

## Policy enforcement

Restrict which MCP tool calls an agent can make with the `policy` field. Rules are evaluated by [Keep](https://github.com/majorcontext/keep) at the proxy layer before requests reach the MCP server.

### Starter packs

Starter packs are built-in policy sets for common MCP servers. Apply one by name:

```yaml
mcp:
  - name: linear
    url: https://mcp.linear.app/mcp
    auth:
      grant: mcp-linear
      header: Authorization
    policy: linear-readonly
```

This restricts the Linear MCP server to read-only operations. Write operations like `create_issue` or `delete_issue` are denied.

Available starter packs:

| Name | Description |
|------|-------------|
| `linear-readonly` | Allows read operations on Linear, denies all writes |

### Inline rules

For simple policies, define rules directly in `moat.yaml`:

```yaml
mcp:
  - name: linear
    url: https://mcp.linear.app/mcp
    policy:
      deny: [delete_issue, update_issue]
      mode: enforce
```

Listed operations are denied; unlisted operations are implicitly allowed.

The `mode` field controls enforcement:

| Mode | Behavior |
|------|----------|
| `enforce` | Deny blocked operations (default) |
| `audit` | Log policy decisions without blocking |

### File-based rules

For larger policies, store rules in a separate file. The `.keep/` directory in your workspace root is the conventional location:

```yaml
mcp:
  - name: linear
    url: https://mcp.linear.app/mcp
    policy: .keep/linear-rules.yaml
```

The file path is relative to the workspace root.

Keep rules files use Keep's native YAML format with `scope`, `mode`, and `rules`:

```yaml
# .keep/linear-rules.yaml
scope: linear
mode: enforce
rules:
  - name: deny-deletes
    match:
      operation: "delete_*"
    action: deny
    message: "Delete operations are not allowed."

  - name: deny-updates
    match:
      operation: "update_*"
    action: deny
    message: "Update operations are not allowed."
```

See the [Keep documentation](https://github.com/majorcontext/keep) for the full rule format, including pattern matching, CEL expressions, and redaction.

### Audit mode

Use `mode: audit` to observe what the agent does before enforcing restrictions. Policy decisions are logged to the run's audit trail without blocking any operations:

```yaml
mcp:
  - name: linear
    url: https://mcp.linear.app/mcp
    policy:
      deny: [delete_issue]
      mode: audit
```

Run the agent, then review audit entries:

```bash
$ moat audit <run-id>
```

Once you are confident in the rules, switch to `mode: enforce`.

## Sandbox-local MCP servers

Sandbox-local MCP servers run as child processes inside the container. The agent starts them directly -- no proxy is involved. Configure them under the agent-specific section of `moat.yaml`.

### 1. Install the MCP server

The server executable must be available inside the container. Use a `pre_run` hook to install it:

```yaml
hooks:
  pre_run: npm install -g @modelcontextprotocol/server-filesystem
```

Alternatively, include the server in your project's dependencies so it is installed alongside your code.

### 2. Configure in moat.yaml

Declare the MCP server under the agent section (`claude:`, `codex:`, or `gemini:`):

#### Claude Code

```yaml
claude:
  mcp:
    filesystem:
      command: npx
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
      cwd: /workspace
```

Moat writes the server configuration to `.claude.json` inside the container. Claude Code discovers it automatically.

#### Codex

```yaml
codex:
  mcp:
    filesystem:
      command: npx
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
      cwd: /workspace
```

Moat writes the server configuration to `.mcp.json` in the workspace directory.

#### Gemini

```yaml
gemini:
  mcp:
    filesystem:
      command: npx
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
      cwd: /workspace
```

Moat writes the server configuration to `.mcp.json` in the workspace directory.

### 3. Run the agent

```bash
moat claude ./my-project
```

The agent starts the MCP server process inside the container and connects to it over stdio.

### Injecting credentials

Use the `grant` field to inject a credential as an environment variable.
This is supported for Codex and Gemini agents:

```yaml
codex:
  mcp:
    github_server:
      command: /path/to/github-mcp-server
      grant: github
      cwd: /workspace
```

When `grant` is specified, Moat sets the corresponding environment variable automatically:

| Grant | Environment variable |
|-------|---------------------|
| `github` | `GITHUB_TOKEN` |
| `anthropic` | `ANTHROPIC_API_KEY` |
| `openai` | `OPENAI_API_KEY` |
| `gemini` | `GEMINI_API_KEY` |

The grant must also appear in the top-level `grants:` list.

### Environment variables

Pass environment variables directly:

```yaml
claude:
  mcp:
    my_server:
      command: /path/to/server
      env:
        API_KEY: my-api-key
        DEBUG: "true"
      cwd: /workspace
```

### Sandbox-local MCP server fields

| Field | Type | Description |
|-------|------|-------------|
| `command` | `string` | Server executable path (required) |
| `args` | `array[string]` | Command arguments |
| `env` | `map[string]string` | Environment variables |
| `grant` | `string` | Credential to inject as an environment variable (Codex and Gemini only) |
| `cwd` | `string` | Working directory for the server process |

## OAuth authentication

MCP servers that use OAuth 2.0 can authenticate through `moat grant oauth`. This acquires tokens via a browser-based authorization code flow with PKCE.

### Grant OAuth credentials

```bash
moat grant oauth notion --url https://mcp.notion.com/mcp
```

The `--url` flag triggers MCP OAuth discovery -- Moat fetches the server's OAuth metadata automatically. No manual endpoint configuration is needed.

For servers without MCP discovery, provide OAuth endpoints directly:

```bash
moat grant oauth linear \
    --auth-url https://linear.app/oauth/authorize \
    --token-url https://linear.app/api/oauth/token \
    --client-id your-client-id \
    --scopes "read write"
```

Alternatively, create a config file at `~/.moat/oauth/<name>.yaml`:

```yaml
auth_url: https://linear.app/oauth/authorize
token_url: https://linear.app/api/oauth/token
client_id: your-client-id
scopes: read write
```

Config resolution order: CLI flags, then config file (`~/.moat/oauth/<name>.yaml`), then MCP discovery from `--url`.

### Configure in moat.yaml

Reference the OAuth grant with the `oauth:<name>` prefix:

```yaml
grants:
  - oauth:notion

mcp:
  - name: notion
    url: https://mcp.notion.com/mcp
    auth:
      grant: oauth:notion
      header: Authorization
```

The proxy injects a `Bearer` prefix when injecting `oauth:` grants into the `Authorization` header.

### Token refresh

OAuth tokens are auto-refreshed during runs. When a token expires, the proxy uses the stored refresh token to obtain a new access token without interrupting the agent.

## Observability

All MCP traffic (both remote and host-local) flows through the proxy relay, so it appears in network traces:

```bash
$ moat trace --network

[10:23:44.512] GET https://mcp.context7.com/mcp 200 (89ms)
```

Credential injection events are recorded in audit logs:

```bash
$ moat audit

[10:23:44.500] credential.injected grant=mcp-context7 host=mcp.context7.com header=CONTEXT7_API_KEY
```

Sandbox-local MCP server output appears in container logs:

```bash
moat logs
```

## Troubleshooting

### MCP server not appearing in agent

- Verify the MCP server is declared in `moat.yaml` (remote servers at the top level, sandbox-local servers under the agent section)
- For remote servers, check that the grant exists: `moat grant list` should show `mcp-{name}`
- Check container logs for configuration errors: `moat logs`

### Authentication failures (401 or 403)

- Verify the grant exists and the credential is correct
- Revoke and re-grant: `moat revoke mcp-{name}` then `moat grant mcp {name}`
- Verify the `url` in `moat.yaml` matches the actual MCP server endpoint
- Verify the `header` name matches what the MCP server expects

### Stub credential in error messages

If you see `moat-stub-{grant}` in error output, the proxy did not replace the stub with a real credential. Check:

- The `url` in `moat.yaml` matches the host the agent is connecting to
- The `header` name matches what the agent sends
- The grant name in `auth.grant` matches the stored credential (`mcp-{name}`)

### Connection refused

- The proxy may not be running. Check `moat list` to verify the run is active
- For remote servers, verify the URL is reachable from your network
- For host-local servers, verify the server is running on the host and listening on the configured port
- For sandbox-local servers, verify the `command` path exists inside the container

### Sandbox-local server not starting

- Verify the server executable is installed inside the container. Use a `pre_run` hook or include it in your project dependencies.
- Check that `command` is an absolute path or available on `$PATH` inside the container
- Review container logs with `moat logs` for startup errors

### SSE streaming issues

Remote MCP servers that use SSE (Server-Sent Events) for streaming responses are supported through the proxy relay. If streaming appears stalled:

- Check that the MCP server URL is correct and the server supports SSE
- Review network traces with `moat trace --network` to see if requests reach the server
- Verify no intermediate firewalls or proxies are buffering SSE responses

## Agent-specific notes

### Claude Code

Remote and sandbox-local MCP servers are configured in the generated `.claude.json`. Remote servers use `type: http` with relay URLs pointing to the proxy. Sandbox-local servers use `type: stdio` with the command and arguments from `moat.yaml`. Claude Code discovers both types through this config file automatically. See [Running Claude Code](/moat/guides/claude-code) for other Claude Code configuration options.

### Codex

Sandbox-local MCP servers are configured under `codex.mcp:` in `moat.yaml`. Configuration is written to `.mcp.json` in the workspace. See [Running Codex](/moat/guides/codex) for Codex-specific configuration.

### Gemini

Sandbox-local MCP servers are configured under `gemini.mcp:` in `moat.yaml`. Configuration is written to `.mcp.json` in the workspace. See [Running Gemini](/moat/guides/gemini) for Gemini-specific configuration.

## Related guides

- [Credential management](/moat/concepts/credentials) -- How credential injection works
- [Observability](/moat/concepts/observability) -- Network traces and audit logs
- [moat.yaml reference](/moat/reference/moat-yaml) -- Full field reference for `mcp:`, `claude.mcp:`, `codex.mcp:`, and `gemini.mcp:`
- [CLI reference](/moat/reference/cli) -- `moat grant mcp` command details
- [Running Claude Code](/moat/guides/claude-code) -- Claude Code agent guide
- [Running Codex](/moat/guides/codex) -- Codex agent guide
- [Running Gemini](/moat/guides/gemini) -- Gemini agent guide

---
[← Moat documentation index](https://majorcontext.com/moat/llms.txt)
