# AI agents (MCP)

> Connect Claude, Cursor, or any MCP client to your Sendai account — six tools over hosted HTTP, authenticated with your API key, nothing to install.

Sendai runs a hosted [MCP](https://modelcontextprotocol.io) server, so an AI agent can send
and track messages **from your account**: the same API key, the same prepaid balance, the
same delivery record and message log as everything else you do. There is nothing to install
and nothing to run — you configure a URL and a key, and every message the agent sends shows
up in your dashboard exactly as if your own code had sent it.

| Host | Use |
| --- | --- |
| `https://mcp.sendai.co.zw` | Production |
| `https://mcp-staging.sendai.co.zw` | Staging |

> **Agents send real messages.** `send_sms` and `send_bulk_sms` deliver to real handsets and
> charge your prepaid balance, exactly as if you had called the REST API yourself. Both are
> marked destructive in the MCP metadata, so well-behaved clients ask before running them —
> but the enforcement that matters (approved sender, sufficient balance) happens server-side.

## How it works

The server speaks MCP over plain **HTTP**: your client POSTs JSON-RPC to the host and gets
the response back in the same request. There is no session and no state — each request
carries your API key as a bearer token, the server forwards it to the REST API for the life
of that one request, and nothing about you is stored on it.

- The endpoint answers on both the bare host and `/mcp` — either URL works.
- It accepts `POST` only; a `GET` on the endpoint returns `405`, and any other path is a
  deliberate `404` so a mistyped URL fails loudly instead of half-working.
- Authentication is a normal API key from **Settings → Developer** in the dashboard, sent as
  `Authorization: Bearer …`. No OAuth — browser and mobile connector UIs that expect an
  OAuth flow cannot connect; use a client that reads a config file.

## Connect a client

Create an API key in the dashboard under **Settings → Developer** — ideally one dedicated to
the agent (see [Security](#security)) — then add the server to your client:

```bash [Claude Code]
claude mcp add --transport http sendai https://mcp.sendai.co.zw \
  --header "Authorization: Bearer sk_your_key_here"
```

```json [Claude Desktop]
// claude_desktop_config.json
{
  "mcpServers": {
    "sendai": {
      "type": "http",
      "url": "https://mcp.sendai.co.zw",
      "headers": { "Authorization": "Bearer sk_your_key_here" }
    }
  }
}
```

```json [Cursor]
// ~/.cursor/mcp.json
{
  "mcpServers": {
    "sendai": {
      "type": "http",
      "url": "https://mcp.sendai.co.zw",
      "headers": { "Authorization": "Bearer sk_your_key_here" }
    }
  }
}
```

The same JSON shape works in any client that reads an `mcpServers` config file — VS Code
included. The dashboard's **Settings → Developer** tab shows this exact configuration with a
copy button.

## The six tools

The tools are the same operations as the [REST reference](/api), so everything an agent does
is visible in your dashboard's messages log and reports.

- **`send_sms`** (`write · charges the account`)
  Sends one SMS. **Real message, real charge.** Args: `to` (recipient MSISDN in international
  format, e.g. `263771000000`), `message` (billed per GSM-7/UCS-2 segment), `from` (an
  approved sender ID — an unapproved value is rejected with `400`). Returns `202` — accepted,
  priced and queued, **not delivered**; poll `get_sms` to confirm delivery.

- **`send_bulk_sms`** (`write · charges the account`)
  Sends the same body to many recipients, priced as one send. **Real messages, real
  charges.** Args: `to` (array of MSISDNs), `message`, `from` (approved sender ID), optional
  `webhook_url` (a delivery callback applied to every message in the send). Rejected outright
  with *insufficient balance* when the prepaid balance does not cover it — never partially
  delivered.

- **`get_sms`** (`read-only`)
  Reads a message back by id. Args: `id` (from the `send_sms` response). `status: success`
  means the carrier accepted or confirmed it — `delivered_at` non-null is the confirmed
  handset delivery. There is no separate delivery-status field.

- **`get_balance`** (`read-only`)
  Prepaid wallet balance, one row per currency. No args. Worth calling before a bulk send —
  an underfunded send is rejected, not partially delivered.

- **`list_sender_ids`** (`read-only`)
  Sender identities on the account. No args. Rows with `usable_as_from: true` are valid `from`
  values — an SMS identity that is both active and verified; the rest are listed so a rejected
  sender can be explained rather than guessed at.

- **`list_api_keys`** (`read-only`)
  API keys on the account — `ID`, `Name`, `Active`, `CreatedAt`, `LastUsedAt`. **Metadata
  only, never key material.** No args.

One convenience the tools add over raw REST: every charge is decorated with a
`charge_display` sibling (`"0.045 USD"`), so an agent reads four cents as four cents rather
than as the API's raw ten-thousandths integer `450`.

Deliberately **not** exposed: creating or revoking API keys. Minting a key returns the
secret once, in plaintext — over MCP it would land in the model's context and the client
transcript, and it would let an agent create credentials that outlive the session. Listing
is safe (only metadata comes back), so only listing is here.

## A worked example

A typical first session — you ask the agent to check the balance, then send a verification
code and confirm it arrived:

```text [Transcript]
// sample — tool calls an agent makes for:
// "Check my Sendai balance, then text the code 4821 to 263771000000."

→ get_balance
← [{ "currency": "USD", "amount": 184500, "amount_display": "18.45 USD" }]

→ list_sender_ids
← [{ "from": "Sendai SMS", "channel": "sms", "usable_as_from": true }]

→ send_sms { "to": "263771000000", "from": "Sendai SMS",
             "message": "Your code is 4821" }
← { "id": "0f9c1b3a-…", "status": "enqueued", "charge": "450",
    "charge_display": "0.045 USD", "delivered_at": null }   // 202 — queued

→ get_sms { "id": "0f9c1b3a-…" }                            // a little later
← { "status": "success", "delivered_at": "2026-07-06T09:12:44Z" }
```

That sequence exercises the whole delivery story, and teaches the agent the same lesson the
[Quickstart](/guides/quickstart) teaches you: `202` is queued; `delivered_at` is delivered.

## Security

- **Give the agent its own key.** A key is the only credential the server sees, and it scopes
  every call to one account — a dedicated key means you can cut the agent off without
  touching your services. Create and revoke keys under **Settings → Developer** or with the
  [API tokens](/api/api-tokens) endpoints; revocation takes effect on the next request.
- **Your key is never stored.** Each request's bearer key is forwarded to the REST API for
  that one request and then gone — the MCP server keeps no key material and no per-tenant
  state.
- **The send tools spend money.** `send_sms` and `send_bulk_sms` are flagged destructive so
  clients prompt first, but treat any agent holding a key as able to send: fund the account
  accordingly and watch the messages log.
- **Agents cannot mint keys.** Key creation and revocation are excluded from the tool
  surface by design (see above).

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| `401` Missing or malformed Authorization header | The key is absent, not in `Authorization: Bearer sk_…` form, or revoked. Check the header in your client config, and that the key is still active under **Settings → Developer**. |
| `400` unknown or unapproved sender id | The `from` value is not an approved sender ID on your account. Run `list_sender_ids` and use a row with `usable_as_from: true` — see [Sender IDs](/api/sender-ids). |
| `404` No MCP endpoint at that path | The URL has a typo. The server answers on the bare host and `/mcp` only. |
| `405` This endpoint accepts POST | The client is probing with `GET`. Configure it as an HTTP (streamable) MCP server, not SSE. |
| *insufficient balance* on a bulk send | The prepaid balance does not cover the whole send. Check `get_balance`, top up in the dashboard, retry — nothing was partially sent. |

## Health

`GET /health` on the MCP host answers unauthenticated and says only that the server is up —
nothing about your account.

## Next steps

- The endpoints behind the tools → [API overview](/api)
- Delivery, confirmed → [Retrieve an SMS](/api/sms#retrieve-an-sms)
- Push instead of poll → [Webhooks](/guides/webhooks)
- Key hygiene in full → [Authentication](/guides/authentication)
