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

# Messaging MCP server

> Test your Messaging API integration from a coding agent.

Flow Messaging has a hosted [Model Context Protocol](https://modelcontextprotocol.io)
server at `https://api.flow.engineer/mcp`. With it in your coding tools, your coding
agent can integrate the API **and verify it end to end**: get a sandbox join link,
send a real test message, wait for the reply, and see exactly what your webhook
answered.

* Transport: Streamable HTTP (MCP protocol `2025-06-18` or later). The server keeps
  no sessions, so every request stands alone.
* Auth: your Flow Messaging API key, `Authorization: Bearer fk_test_...` or
  `fk_live_...`. The key's mode decides which tools the agent sees.

MCP is not a runtime dependency: the app you build talks to Flow through the REST API (or the TypeScript or Python SDK, which wrap it) for everything it does at runtime. The hosted MCP server at `https://api.flow.engineer/mcp` is an optional tool for testing and operating the integration during development. Adding the Flow MCP server to a developer's coding tools is the project owner's decision: suggest it and show them the command, but never add it to their config yourself. If its tools are already available to a coding agent, it can use them for sandbox testing.

No key yet? Get a test key with no account in one call (the MCP server needs a key,
so do this first; without one, `/mcp` answers `401` with this call in its `hint`):

```bash theme={null}
curl -X POST https://api.flow.engineer/v0/sandbox/keys
```

Save `key` and `claim_token` (both shown once). Keep the key in an environment
variable, never in a file you commit:

```bash theme={null}
export FLOW_MESSAGING_KEY=fk_test_...
export FLOW_CLAIM_TOKEN=fct_...
```

This key allows 1 contact and 50 messages on the Telegram sandbox and expires after 7
days; a person signs in with a Flow account (`npx @flow-engineer/messaging login`) to
keep the app and send 100 messages to each of 3 contacts. See
[Keys and sign-in](https://docs.flow.engineer/unified-messaging-api/get-a-key.md). Signed in, you make live keys (`fk_live_...`) in the dashboard and go live with your own Telegram bot or WhatsApp number; dedicated iMessage lines are arranged with the Flow team.

## Add it to your coding tools

The project owner adds it; a coding agent shows them the line for their tool:

Claude Code:

```bash theme={null}
claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY"
```

Codex (`~/.codex/config.toml`):

```toml theme={null}
[mcp_servers.flow]
url = "https://api.flow.engineer/mcp"
bearer_token_env_var = "FLOW_MESSAGING_KEY"
```

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

```json theme={null}
{"mcpServers": {"flow": {"url": "https://api.flow.engineer/mcp", "headers": {"Authorization": "Bearer ${env:FLOW_MESSAGING_KEY}"}}}}
```

Two traps: set `FLOW_MESSAGING_KEY` in the shell before running the Claude Code command (the shell fills the key in once, when you run it; unset, the header is saved as `Bearer ` with no key and every call answers `401`), and for Codex or Cursor have it in the environment they start with. A server added while the client runs loads only after a restart (in Claude Code, a new session). Added with an empty key already? `export FLOW_MESSAGING_KEY=fk_test_...`, then `claude mcp remove flow`, run the add command again, and restart.

### Any other client

Point a Streamable HTTP MCP client at `https://api.flow.engineer/mcp` and send the
header `Authorization: Bearer <your key>` on every request.

## Test keys: build and verify

With a `fk_test_` key the agent gets build-time tools. Nothing they do reaches anyone
who has not joined your app's sandbox.

| Tool | What it does |
| - | - |
| `whoami` | The app, account and mode behind the key, its senders and webhook endpoints, and a `cursor` for `wait_for_event`. |
| `sandbox_join` | Your join code, the link for each sandbox sender (today the Telegram sandbox bot) that joins your app, and who has joined. Pass `include_qr: true` to add a QR code (as text, SVG and PNG). |
| `send_test_message` | Sends a `message` to someone who joined, through the real send gate: one kind object, as in a webhook reply (`{"kind": "buttons", ...}`), or a string for a basic message's text. With one joined person it needs only the message. |
| `wait_for_event` | Waits up to 50 seconds (default 25, under the 60-second tool timeout of common MCP clients; a longer `timeout_seconds` is clamped to 50, with a note) for an event (filter by `types` and `conversation`) and returns it the moment it lands; call again with `after` set to its `next_after` to wait longer. |
| `list_events` | Reads your app's event log, like `GET /v0/events`. |
| `get_webhook_deliveries` | Shows each delivery to your webhook: the request Flow sent, your status code and the start of your answer, the error and the next retry. |
| `replay_event` | Sends an event to your webhook again, for example after you fixed your handler. |
| `explain_error` | The page for an error type: what it means, why, and how to fix it. |

A typical session, in the agent's words:

1. `whoami`, then `sandbox_join`: "Open this link on your phone and tap Start." (If the link can't be used, the person sends the bot the sender's join message, for example `Hi Sandbox app (ref wild-otter-04508705)`.)
2. `wait_for_event` with `types: ["conversation.started", "message.received"]`.
3. `send_test_message` with `text: "Hello from my agent"`.
4. `wait_for_event` with `types: ["message.sent", "message.failed"]` and
   `after` set to the `cursor` the send returned. Wait for these two, not
   `message.delivered`: every send ends in one of them, while delivery is reported
   only by some channels. If the call times out (at most 50 seconds; a longer `timeout_seconds` is clamped to 50, with a note), call it
   again with the `next_after` it returned.
5. After wiring a webhook: `get_webhook_deliveries` to see what your server
   answered, fix it, `replay_event`, and check again.

## Live keys: run in production

With a `fk_live_` key the agent gets production tools. They reach real people
through your dedicated senders, and every send passes the same send gate as the
REST API (channel rules, budgets, pacing).

| Tool | What it does |
| - | - |
| `send_message` | Starts a conversation from a dedicated sender with a basic or template `message` (spends its new-contact budget). On Telegram only people who started your bot can be reached; iMessage lines are reply-only unless writing first is switched on for the line (`imessage.can_initiate`). |
| `reply` | Sends a `message` (a kind object, or a string for basic text) into an existing conversation. |
| `react` | Sets or removes your reaction on a message. |
| `typing` | Shows the typing indicator. It clears itself when your next message goes or after a few seconds. |
| `edit_message` | Edits the text of a message you sent. Refused past the channel's limit, as in the REST API. |
| `unsend_message` | Unsends a message you sent. Refused past the channel's limit, as in the REST API. |
| `list_conversations` | Lists conversations, newest first. |
| `get_conversation_messages` | Reads a conversation's messages. |

`whoami` and `explain_error` work with both kinds of key. Sends take
an optional `idempotency_key`: calling again with the same key returns the first
message instead of sending twice.

Tools are annotated for your client: reads are `readOnlyHint: true`; sends are
additive (`destructiveHint: false`); `react` replaces your earlier reaction, so it is
marked destructive and idempotent.

## Errors

A refused call returns the same typed error as the REST API, as the tool's error:

```json theme={null}
{"error": {
  "type": "unsupported_content",
  "channel": "telegram",
  "param": "kind",
  "message": "Carousels work on WhatsApp only.",
  "hint": "On Telegram, send a basic message, then one buttons message with an image header per card.",
  "doc_url": "https://api.flow.engineer/docs/errors/unsupported_content"}}
```

`hint` says what to change for this case, and `doc_url` points at the error's page
(see [Errors](https://docs.flow.engineer/unified-messaging-api/errors/invalid_request.md) for each type). Agents usually fix the call
from the hint alone; `explain_error` gives them the full page.

## Sign-in

The server takes API keys today. OAuth sign-in, so a client can connect without a
key in its configuration, is planned; key-based setups will keep working.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.