Messaging MCP server
Development MCPTest your Messaging API integration from a coding agent.
Flow Messaging has a hosted Model Context Protocol
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-18or later). The server keeps no sessions, so every request stands alone. - Auth: your Flow Messaging API key,
Authorization: Bearer fk_test_...orfk_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):
curl -X POST https://api.flow.engineer/v0/sandbox/keysSave key and claim_token (both shown once). Keep the key in an environment
variable, never in a file you commit:
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. 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
Section titled “Add it to your coding tools”The project owner adds it; a coding agent shows them the line for their tool:
Claude Code:
claude mcp add --transport http flow https://api.flow.engineer/mcp --header "Authorization: Bearer $FLOW_MESSAGING_KEY"Codex (~/.codex/config.toml):
[mcp_servers.flow]url = "https://api.flow.engineer/mcp"bearer_token_env_var = "FLOW_MESSAGING_KEY"Cursor (~/.cursor/mcp.json):
{"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
Section titled “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
Section titled “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:
whoami, thensandbox_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 exampleHi Sandbox app (ref wild-otter-04508705).)wait_for_eventwithtypes: ["conversation.started", "message.received"].send_test_messagewithtext: "Hello from my agent".wait_for_eventwithtypes: ["message.sent", "message.failed"]andafterset to thecursorthe send returned. Wait for these two, notmessage.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 longertimeout_secondsis clamped to 50, with a note), call it again with thenext_afterit returned.- After wiring a webhook:
get_webhook_deliveriesto see what your server answered, fix it,replay_event, and check again.
Live keys: run in production
Section titled “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
Section titled “Errors”A refused call returns the same typed error as the REST API, as the tool’s error:
{"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 for each type). Agents usually fix the call
from the hint alone; explain_error gives them the full page.
Sign-in
Section titled “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.
Thanks, we read every comment.