# MCP server (/mcp)



`@sendkuy/mcp` is a [Model Context Protocol](https://modelcontextprotocol.io) server. It lets an AI assistant or agent reach people through your SendKuy account. It can send alerts and summaries, notify customers per order, send and check OTP codes, and run campaigns you prepared in the dashboard.

It runs on your machine through `npx`, talks to `https://app.sendkuy.com/api/v1` with your API key, and lists only the tools that key may use.

## Create a key for the agent [#create-a-key-for-the-agent]

Open [**Developers → Keys**](https://app.sendkuy.com/user/api?utm_source=docs\&utm_medium=mcp) and create a key just for the agent. Every key needs `account:read`, because the server reads the key's scopes when it starts. Then add only what the agent should do:

| The agent should…                                    | Scopes                                                                            |
| ---------------------------------------------------- | --------------------------------------------------------------------------------- |
| Read messages and webhook deliveries while you debug | `account:read`, `messages:read`, `webhooks:read`                                  |
| Send notifications and alerts                        | `account:read`, `messages:write`, `messages:read`, `groups:read`, `contacts:read` |
| Run OTP                                              | `account:read`, `verifications:write`                                             |
| Run campaigns from the dashboard                     | `account:read`, `campaigns:read`, `campaigns:write`                               |

A key with read scopes only never lists a tool that sends. When you change a key's scopes, restart the MCP client so the server reads them again.

## Install [#install]

Cursor reads `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for every project. VS Code reads `.vscode/mcp.json` and asks for the key, so it stays out of the file. Claude Desktop reads `claude_desktop_config.json`, and Windsurf reads `~/.config/devin/mcp_config.json`.

<CodeBlockTabs defaultValue="Claude Code">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="Claude Code">
      Claude Code
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Cursor">
      Cursor
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="VS Code">
      VS Code
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Claude Desktop">
      Claude Desktop
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Windsurf">
      Windsurf
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Claude Agent SDK">
      Claude Agent SDK
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="LangChain">
      LangChain
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="Claude Code">
    ```bash
    claude mcp add --transport stdio --env SENDKUY_API_KEY=sk_live_YOUR_API_KEY sendkuy -- npx -y @sendkuy/mcp
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Cursor">
    ```json
    {
      "mcpServers": {
        "sendkuy": {
          "command": "npx",
          "args": ["-y", "@sendkuy/mcp"],
          "env": { "SENDKUY_API_KEY": "sk_live_YOUR_API_KEY" }
        }
      }
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="VS Code">
    ```json
    {
      "inputs": [
        { "type": "promptString", "id": "sendkuy-key", "description": "SendKuy API key", "password": true }
      ],
      "servers": {
        "sendkuy": {
          "type": "stdio",
          "command": "npx",
          "args": ["-y", "@sendkuy/mcp"],
          "env": { "SENDKUY_API_KEY": "${input:sendkuy-key}" }
        }
      }
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Claude Desktop">
    ```json
    {
      "mcpServers": {
        "sendkuy": {
          "command": "npx",
          "args": ["-y", "@sendkuy/mcp"],
          "env": { "SENDKUY_API_KEY": "sk_live_YOUR_API_KEY" }
        }
      }
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Windsurf">
    ```json
    {
      "mcpServers": {
        "sendkuy": {
          "command": "npx",
          "args": ["-y", "@sendkuy/mcp"],
          "env": { "SENDKUY_API_KEY": "${env:SENDKUY_API_KEY}" }
        }
      }
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Claude Agent SDK">
    ```ts
    import { query } from '@anthropic-ai/claude-agent-sdk';

    for await (const message of query({
      prompt: 'Send the on-call group a WhatsApp alert: checkout API is down.',
      options: {
        mcpServers: {
          sendkuy: {
            command: 'npx',
            args: ['-y', '@sendkuy/mcp'],
            env: { SENDKUY_API_KEY: process.env.SENDKUY_API_KEY! },
          },
        },
      },
    })) {
      console.log(message);
    }
    ```
  </CodeBlockTab>

  <CodeBlockTab value="LangChain">
    ```python
    import os

    from langchain_mcp_adapters.client import MultiServerMCPClient

    client = MultiServerMCPClient({
        "sendkuy": {
            "transport": "stdio",
            "command": "npx",
            "args": ["-y", "@sendkuy/mcp"],
            "env": {"SENDKUY_API_KEY": os.environ["SENDKUY_API_KEY"]},
        }
    })
    tools = await client.get_tools()
    ```
  </CodeBlockTab>
</CodeBlockTabs>

One click: [Install in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=sendkuy\&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBzZW5ka3V5L21jcCJdLCJlbnYiOnsiU0VOREtVWV9BUElfS0VZIjoic2tfbGl2ZV9ZT1VSX0FQSV9LRVkifX0=) · [Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22sendkuy%22%2C%22type%22%3A%22stdio%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40sendkuy%2Fmcp%22%5D%2C%22env%22%3A%7B%22SENDKUY_API_KEY%22%3A%22%24%7Binput%3Asendkuy-key%7D%22%7D%2C%22inputs%22%3A%5B%7B%22type%22%3A%22promptString%22%2C%22id%22%3A%22sendkuy-key%22%2C%22description%22%3A%22SendKuy%20API%20key%22%2C%22password%22%3Atrue%7D%5D%7D) · Claude Desktop: open `sendkuy-mcp.mcpb` from the [latest release](https://github.com/sendkuy/sendkuy-mcp/releases/latest). The Cursor link fills in `sk_live_YOUR_API_KEY`, so replace it with your key afterwards.

## Scenarios [#scenarios]

### On-call alert [#on-call-alert]

**Who builds it:** a team running its own monitoring agent.
**What the agent does:** when a health check fails, it messages the on-call group on WhatsApp with the service and the error, once per incident.

> When the checkout health check fails, send the on-call group a WhatsApp message with the service name and the error. Use the incident id as dedupe_key.

**Tools:** `sendkuy_list_groups` to find the on-call group, then `sendkuy_send_message` with its `grp_` id in `to` and `dedupe_key: "incident-4821"`. A monitor that fires three times for the same incident still sends one message, as long as the text stays the same: the same key with a different body is refused with `idempotency_key_reused`.
**Scopes:** `account:read`, `groups:read`, `messages:write`.

### Daily revenue summary [#daily-revenue-summary]

**Who builds it:** a store owner, or the developer who runs their back office.
**What the agent does:** every evening, it reads the day's sales from the store's database and sends the owner a short summary.

> Every day at 21:00, total today's paid orders, compare them with yesterday, and WhatsApp the summary to +6281234567890. Use daily-revenue-YYYY-MM-DD as dedupe_key.

**Tools:** `sendkuy_send_message`. Before a long run, `sendkuy_get_account` shows how much WhatsApp credit is left.
**Scopes:** `account:read`, `messages:write`.

### Store operations [#store-operations]

**Who builds it:** a shop with stock and orders in its own system.
**What the agent does:** it watches for low stock and orders that have waited too long, and tells the right person.

> Every hour, find products with fewer than 5 units and orders paid more than 2 hours ago that are not packed. Message the warehouse lead about stock and the shop admin about orders.

**Tools:** `sendkuy_find_contacts` to look up staff by `external_id`, then `sendkuy_send_message` to their `con_` ids.
**Scopes:** `account:read`, `contacts:read`, `messages:write`.

### A notification per order [#a-notification-per-order]

**Who builds it:** a developer adding customer updates to a store.
**What the agent does:** it collects every order shipped in the last hour and sends each customer their own tracking number.

> For every order shipped since the last run, send the customer a WhatsApp message with their name, order number and tracking link. Email customers who have no phone number.

**Tools:** one `sendkuy_send_batch` call with a different body per row, mixing WhatsApp and email. A row without credit fails on its own; the rest still go out. `sendkuy_get_messages` confirms delivery afterwards.
**Scopes:** `account:read`, `messages:write`, `messages:read`.

### OTP [#otp]

**Who builds it:** a developer adding phone verification to sign-up.
**What the agent does:** it sends a code and checks what the user typed.

> Send a verification code to +6281234567890 by WhatsApp, falling back to SMS. Then check the code 482913.

**Tools:** `sendkuy_send_verification`, then `sendkuy_check_verification`.
**Scopes:** `account:read`, `verifications:write`.

### A promotion to a customer group [#a-promotion-to-a-customer-group]

**Who builds it:** a shop that writes its campaign in the SendKuy dashboard and lets an agent decide when it goes out.
**What the agent does:** it finds the campaign and reports its audience. Then it starts the campaign, watches delivery, and pauses if something looks wrong.

> Find the draft campaign "Promo Payday", tell me how many contacts it reaches, start it, and pause it if more than 10% of the first 200 fail.

**Tools:** `sendkuy_list_campaigns`, `sendkuy_start_campaign`, `sendkuy_get_campaign`, `sendkuy_control_campaign`. For a quick one-off with the same text to everyone, `sendkuy_send_message` with the group's `grp_` id is enough.
**Scopes:** `account:read`, `campaigns:read`, `campaigns:write`.

### Debugging an integration [#debugging-an-integration]

**Who builds it:** you, while wiring SendKuy into your app.
**What the agent does:** it answers questions about what happened, without you opening the dashboard.

> Which WhatsApp messages failed in the last hour, and why? What is the status of msg_9cBe3A1H…? Why is my webhook failing?

**Tools:** `sendkuy_list_messages`, `sendkuy_get_messages`, `sendkuy_list_webhook_deliveries`.
**Scopes:** `account:read`, `messages:read`, `webhooks:read`.

## Tools [#tools]

| Tool                              | What it does                                                      | Scopes                |
| --------------------------------- | ----------------------------------------------------------------- | --------------------- |
| `sendkuy_get_account`             | Plan, credits per channel, usage this period, the key's scopes    | `account:read`        |
| `sendkuy_send_message`            | One body to 1–100 recipients, contacts or a group, on one channel | `messages:write`      |
| `sendkuy_send_batch`              | Up to 500 messages with different content, answered row by row    | `messages:write`      |
| `sendkuy_get_messages`            | Status of up to 20 messages by id                                 | `messages:read`       |
| `sendkuy_list_messages`           | Recent messages by channel, status and time                       | `messages:read`       |
| `sendkuy_cancel_message`          | Cancel a message not sent yet                                     | `messages:write`      |
| `sendkuy_send_verification`       | Send an OTP code                                                  | `verifications:write` |
| `sendkuy_check_verification`      | Check the code a user typed                                       | `verifications:write` |
| `sendkuy_check_whatsapp_numbers`  | Which numbers are on WhatsApp                                     | `numbers:read`        |
| `sendkuy_find_contacts`           | A contact by phone or `external_id`                               | `contacts:read`       |
| `sendkuy_list_templates`          | Saved message templates                                           | `templates:read`      |
| `sendkuy_list_groups`             | Contact groups and their size                                     | `groups:read`         |
| `sendkuy_list_campaigns`          | Campaigns prepared in the dashboard                               | `campaigns:read`      |
| `sendkuy_start_campaign`          | Start a campaign                                                  | `campaigns:write`     |
| `sendkuy_get_campaign`            | Progress and delivery stats                                       | `campaigns:read`      |
| `sendkuy_control_campaign`        | Pause, resume or cancel                                           | `campaigns:write`     |
| `sendkuy_list_webhook_deliveries` | Webhooks and their latest delivery attempts                       | `webhooks:read`       |

## Good to know [#good-to-know]

* **Tools that send really send, and spend credit.** Give the agent a key with only the scopes it needs.
* **`dedupe_key` stops a repeated step from sending twice.** Derive it from the event, such as `order-1234-shipped`, and keep changing values such as the time out of the message. The same key with different content is refused with [`idempotency_key_reused`](/errors#idempotency_key_reused). A refusal is remembered under the key as well: after fixing a bad number or topping up, retry with a new key. Keys last about 24 hours. See [Idempotency](/idempotency).
* **A suppressed recipient still gets a message id.** Someone who unsubscribed or is on your suppression list is checked when the message goes out, so the message fails later. Read the final status with `sendkuy_get_messages`.
* **Without a key the server still starts.** It lists every tool and answers each call with an error until `SENDKUY_API_KEY` is set.
* **Errors carry the API's own code and a link** to [Errors](/errors), so the agent can fix the call itself, for example a phone number without the leading `+`.
* **A send to a large group answers with counts.** `sendkuy_send_message` returns the number of messages per status and the first 20; read the rest with `sendkuy_list_messages`.
