> For an agent: the map of this documentation is at https://escouade.io/en/mail/docs/llms.txt; every page exists in Markdown, at the same address with `.md` appended.

# MCP server

> MCP is Escouade Mail's only interface. The agents' address is `https://api.subagent.fr/mcp`, over Streamable HTTP. Without a key, the server only serves sign-up there. With a key, it serves the tools its permissions open. Online assistants connect to the same server through a second address, `https://mail.escouade.io/mcp`.

## In short

| Property | Value |
|---|---|
| Agents' address | `https://api.subagent.fr/mcp` |
| Online assistants' address | `https://mail.escouade.io/mcp`: OAuth sign-in, no key to copy. See [Connector](https://escouade.io/en/mail/docs/connecteur.md). |
| Transport | Streamable HTTP: JSON-RPC 2.0 requests over `POST`, responses as `application/json` |
| Authentication | `Authorization: Bearer sak_…` header. Without a header, at the agents' address: sign-up only. |
| Tools | Those of the [reference](https://escouade.io/en/mail/docs/reference/index.md), filtered by the key's permissions |
| Views | One workbench per conversation, opened by `open_workspace` then driven by `navigate_workspace`: see [Views](https://escouade.io/en/mail/docs/vues.md) |
| Rules given at connection | The `instructions` field of the response to `initialize`: under 2,000 characters |

## Connecting a client

The server is declared by its address, and the key by a header.

Claude Code:

```sh
claude mcp add \
  --transport http subagent \
  https://api.subagent.fr/mcp \
  --header "Authorization: Bearer $SUBAGENT_KEY"
```

Cursor (`.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "subagent": {
      "url": "https://api.subagent.fr/mcp",
      "headers": {
        "Authorization": "Bearer ${env:SUBAGENT_KEY}"
      }
    }
  }
}
```

Other client (`.mcp.json`):

```json
{
  "mcpServers": {
    "subagent": {
      "type": "http",
      "url": "https://api.subagent.fr/mcp",
      "headers": {
        "Authorization": "Bearer ${SUBAGENT_KEY}"
      }
    }
  }
}
```

No key yet: declare the address without a header, let the agent call `sign_up`, then add the header. See [Getting started](https://escouade.io/en/mail/docs/demarrer.md).

An online assistant (Claude, ChatGPT) declares no header: it connects to `https://mail.escouade.io/mcp`, where the human signs in with their Escouade account. See [Connector](https://escouade.io/en/mail/docs/connecteur.md). Client by client: [Connect my agent](https://escouade.io/en/mail/docs/brancher.md).

## Without a key

At `https://api.subagent.fr/mcp`, without an authorisation header, the server still answers, for sign-up. At `https://mail.escouade.io/mcp`, it answers 401: the assistant then opens the sign-in page.

- It serves `initialize`, `ping`, `tools/list` and `tools/call`.
- `tools/list` returns a single tool: `sign_up`.
- It opens no session and serves no views.
- Calling another tool is refused with the code `unauthorized`. Any other method gets the HTTP status 401.

The server reads the header on every request. After `sign_up`, the connection opened without a key still only shows `sign_up`: the key must be presented. See [Keys and permissions](https://escouade.io/en/mail/docs/cles.md#apres-sign-up).

## With a key

- `tools/list` returns the tools the key's permissions open. `sign_up` is no longer among them.
- Once the space is verified, `verify` and `attach_human` leave the list.
- A key that is presented but unknown or revoked gets the HTTP status 401.

## A single workbench

`open_workspace` is the visual entry point. Once it is open, the model reuses the `instance_id` from this conversation's context and asks for a page with `navigate_workspace`: audiences, contacts, campaigns or performance. Business tools keep returning their data without asking for a new card.

The view receives requests with `workspace_events`, reserved for the app by `_meta.ui.visibility: ["app"]`. It waits 18 seconds at most per call and keeps a single wait in progress. The model must not call this tool to keep the link alive. `navigate_workspace` remains visible to the model and to the app.

A `pending` receipt means the navigation has been requested. Input in progress can delay or prevent its display: the view lets the human choose. See [Workbench navigation](https://escouade.io/en/mail/docs/vues.md#navigation).

## A tool's result

A successful call returns three things.

Result of tools/call:

```json
{
  "content": [{ "type": "text", "text": "Message msg_… en file dans le fil thr_… : …" }],
  "structuredContent": { "message": {}, "thread": {}, "inbox": {}, "replayed": false, "estimated_send_at": "2026-10-13T14:31:00+02:00" },
  "isError": false
}
```

- **`content`**: a readable text in French. It says who, what, when, and the remaining quota.
- **`structuredContent`**: the tool's object, whose schema is published in `https://api.subagent.fr/openapi.json`. It is what a program reads.
- **`isError`**: `false`.

The result also carries `_meta["subagent/requestId"]`, a diagnostic identifier. The space's context metadata can accompany the business object; it serves to partition and refresh the view's data.

The readable text cuts a message's body at 2,000 characters. The whole body is in `structuredContent`.

## A refusal

A refusal by the service is not a protocol error. It is a tool result marked `isError`, with a text: the message, the code in brackets, then the lead after an arrow.

```text
approve_message demande la permission approve, que cette clé n’a pas. (code forbidden)
→ La validation est un geste humain : votre humain l’accomplit depuis son assistant (Claude, ChatGPT…), branché à l’adresse https://mail.escouade.io/mcp où il se connecte avec son e-mail.
```

Over MCP, the same refusal also appears in `structuredContent.error`: `code`, `message` and, when there is one, the lead `hint`. A client should read these fields; the text remains available for the human. Over HTTP, the object is `{"error": {"code", "message", "hint"}}`. See [Errors](https://escouade.io/en/mail/docs/erreurs.md).

## What each tool announces

In `tools/list`, each tool carries:

- `name`, `title` and `description`, in French;
- `inputSchema`: its parameters. An unknown parameter is refused. `tools/list` carries no output schema (`outputSchema`);
- `annotations`: `readOnlyHint` (the tool only reads), `destructiveHint` (its effect cannot be undone), `idempotentHint`;
- `_meta.ui.resourceUri` and `_meta["openai/outputTemplate"]` for the visual entry point `open_workspace`;
- `_meta.ui.visibility`: `model` and `app` in general, `app` only for `workspace_events`;
- `_meta["openai/widgetAccessible"]: true` for the view's calls, including tools without a new card.

The schema of each tool's structured object can be read in `https://api.subagent.fr/openapi.json`.

The workbench uses the resource `ui://subagent/workspace-v12.html`. The addresses of the old views remain readable for conversations already open. After a connector update, the host must read the tool descriptors and their resource address again; an old cached descriptor may still ask for the previous cards.

## Protocol

| Topic | What the server does |
|---|---|
| Versions | `2026-07-28`, `2025-11-25`, `2025-06-18`, `2025-03-26`. Any other version in the `Mcp-Protocol-Version` header is refused. |
| Sessions | `initialize` opens a session and returns the `Mcp-Session-Id` header. It lasts 24 hours after the last request. The session is optional: a request without this header is served statelessly. An expired session is erased after 30 days. |
| Ending a session | `DELETE` on `https://api.subagent.fr/mcp`, with the `Mcp-Session-Id` header. |
| `GET` | Refused with the status 405: the server opens no stream. |
| Batches | Refused: one JSON-RPC message per request. |
| Size | A request is 1 MiB at most. |
| Origin | A request coming from another site's web page (`Origin` header) is refused. |
| Rate | 60 requests per minute without a key, 600 with a key, per client address. See [Limits and quotas](https://escouade.io/en/mail/docs/limites.md#debit). |

## Without MCP

Every tool also exists as `POST https://api.subagent.fr/v1/{operation}`, with the same arguments and the same object in return. See [HTTP API](https://escouade.io/en/mail/docs/api-http.md).
