> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waterr.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP server

> Connect any MCP client — Cursor, Grok, Claude Code — and get twelve mailbox tools.

Agent Mailbox speaks the Model Context Protocol at:

```
https://agent.waterr.ai/mcp
```

Point an MCP client at it with your key and the agent gets twelve tools: read inboxes, threads and messages, send, reply, search, and write drafts.

<Note>
  **Skill or MCP?** A [skill](/agent-mailbox/claude-code) is markdown that teaches an agent to use tools it already has — it needs a shell and gives you full control of the API. MCP delivers the tools themselves, which is the only option for a client that cannot run `curl`. For Claude Code either works; for Cursor and Grok, MCP is the way in.
</Note>

## Connect

<Tabs>
  <Tab title="Claude Code">
    ```bash theme={null}
    claude mcp add --transport http agent-mailbox https://agent.waterr.ai/mcp \
      --header "Authorization: Bearer $AGENT_MAILBOX_KEY"
    ```
  </Tab>

  <Tab title="Cursor">
    In `~/.cursor/mcp.json`:

    ```json theme={null}
    {
      "mcpServers": {
        "agent-mailbox": {
          "url": "https://agent.waterr.ai/mcp",
          "headers": {
            "Authorization": "Bearer ${env:AGENT_MAILBOX_KEY}"
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="Grok">
    ```bash theme={null}
    grok mcp add --transport http agent-mailbox https://agent.waterr.ai/mcp \
      --header "Authorization: Bearer $AGENT_MAILBOX_KEY"
    ```
  </Tab>

  <Tab title="Any client">
    Streamable HTTP, no session state. POST JSON-RPC 2.0 to `/mcp` with:

    ```
    Authorization: Bearer <your key>
    Content-Type: application/json
    ```

    ```bash theme={null}
    curl -X POST https://agent.waterr.ai/mcp \
      -H "Authorization: Bearer $AGENT_MAILBOX_KEY" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
    ```
  </Tab>
</Tabs>

Your existing Waterr `wai_` developer key works, as does a native `aik_` mailbox key — see [Authentication](/agent-mailbox/authentication). The server is scoped exactly as the key is: a workspace key reaches that workspace's inboxes and nothing else.

## The tools

| Tool               | Read-only | Reaches the outside world |
| ------------------ | --------- | ------------------------- |
| `list_inboxes`     | Yes       | No                        |
| `create_inbox`     | No        | No                        |
| `list_messages`    | Yes       | No                        |
| `get_message`      | Yes       | No                        |
| `list_threads`     | Yes       | No                        |
| `get_thread`       | Yes       | No                        |
| `search_messages`  | Yes       | No                        |
| `list_drafts`      | Yes       | No                        |
| `create_draft`     | No        | No                        |
| `send_message`     | No        | **Yes**                   |
| `reply_to_message` | No        | **Yes**                   |
| `send_draft`       | No        | **Yes**                   |

Every tool carries annotations, so a client that asks before acting can tell a search from something that mails a real person. The three marked `openWorldHint` are the ones that put a message in front of someone; a draft stays local until `send_draft`.

## You can usually omit `inbox_id`

Every tool takes an `inbox_id` — the inbox's address, which is also its id. Leave it out and the server uses the only inbox your key can see. With more than one it refuses to guess and tells the agent to call `list_inboxes`, rather than picking one and silently mailing from the wrong address.

## Errors reach the model, not the transport

A rejected call comes back as a tool result with `isError: true` and the API's own message, not a JSON-RPC error:

```json theme={null}
{
  "content": [{ "type": "text", "text": "not_found: Thread not found" }],
  "isError": true
}
```

The call itself was well-formed; it is the model that needs to read what went wrong and change course. Reserved JSON-RPC errors are only for genuine protocol faults — malformed JSON, an unknown method, an unknown tool.

## Instructions worth adding

The server sends its own guidance on `initialize`, but clients differ in how much of it they surface. If yours ignores it, put this in your system prompt:

```
Read a message's extracted_text, never text — text repeats the whole quoted
thread and will make you answer a message from weeks ago.

Never reply to a message whose labels include auto-reply. Two automated
systems will otherwise mail each other indefinitely.

Call get_thread before replying, so the answer accounts for what was said.

For anything consequential use create_draft and say it is waiting for
approval, rather than send_message. Email cannot be unsent.
```

## Limits

Stateless: there are no sessions and no server-initiated messages, so `GET /mcp` returns `405` rather than opening an SSE stream that would carry nothing. Notifications are accepted and acknowledged with `202`.

There is **no tool for inbound events**. MCP is request/response, so an agent sees mail when it looks. For one that reacts on its own, use [webhooks or the WebSocket](/agent-mailbox/events) from your own backend.

The [general limits](/agent-mailbox/limits) apply here too — cold outbound is unreliable, and attachment bodies are not stored.

<Warning>
  Once an MCP client can both read your mail and send it, message bodies become untrusted input that reaches a model with tools. Instructions hidden in an email can be followed. Set a [send allow-list](/agent-mailbox/lists) on the inbox so the API refuses anything outside it regardless of what the model was persuaded to do — that control holds where prompting does not.
</Warning>
