Skip to main content
Glama
README.md
# Orbit CPaaS — MCP Server

Stdio-based [Model Context Protocol](https://modelcontextprotocol.io) server
that exposes the Orbit CPaaS API as first-class tools for any MCP client
(Claude Desktop, Claude Code, Cursor, VS Code extensions, custom agent
harnesses, …).

Docs: <https://docs.orbit.devotel.io/guides/mcp-claude-cursor>

## Install

Most MCP clients launch the server on demand with `npx`, so no separate
install step is needed — the per-client config blocks below do it for you.
To install it explicitly:

```bash
npm install -g @devotel-orbit/mcp-server
```

The package installs a single `orbit-mcp-server` binary.

## Quick start

```bash
ORBIT_API_KEY=dv_live_sk_xxxxxxxxxxxxxxxx npx @devotel-orbit/mcp-server
```

The process speaks JSON-RPC framing on stdio per the MCP spec. All logs go
to stderr — never write to stdout from this server (it would corrupt MCP
framing).

---

## Claude Desktop setup

Add an entry to `~/Library/Application Support/Claude/claude_desktop_config.json`
(macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "orbit": {
      "command": "npx",
      "args": ["-y", "@devotel-orbit/mcp-server"],
      "env": {
        "ORBIT_API_KEY": "dv_live_sk_xxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

Restart Claude Desktop. The Orbit tools appear in the toolbox on the next
conversation. Verify with: "List my Orbit campaigns."

### Sandbox mode (no charges)

Use a test key (`dv_test_sk_*`) to exercise every tool against the sandbox
without incurring real charges. Sandbox calls are logged in the Orbit
dashboard under **Settings → API Logs → sandbox**.

---

## Cursor setup

[![Add orbit MCP server to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](cursor://anysphere.cursor-deeplink/mcp/install?name=orbit&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBkZXZvdGVsLW9yYml0L21jcC1zZXJ2ZXIiXSwiZW52Ijp7Ik9SQklUX0FQSV9LRVkiOiJkdl90ZXN0X3NrX3h4eHh4eHh4eHh4eHh4eHgifX0=)

Click the badge above to install with one click (Cursor prompts you to
confirm, then fill in your real `ORBIT_API_KEY`), or set it up manually.
Open **Cursor Settings → Features → MCP → Add new MCP server** and set:

| Field   | Value                                         |
| ------- | --------------------------------------------- |
| Name    | `orbit`                                       |
| Type    | `command`                                     |
| Command | `npx -y @devotel-orbit/mcp-server`                  |
| Env     | `ORBIT_API_KEY=dv_live_sk_xxxxxxxxxxxxxxxx`   |

Alternatively, add to `.cursor/mcp.json` in your project root:

```json
{
  "mcpServers": {
    "orbit": {
      "command": "npx",
      "args": ["-y", "@devotel-orbit/mcp-server"],
      "env": {
        "ORBIT_API_KEY": "dv_live_sk_xxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

Cursor automatically reloads MCP config on file save. Test with `@orbit` in
any Cursor chat: "Using orbit, send an SMS to +14155551234 saying hello."

---

## Claude Code (CLI)

```bash
export ORBIT_API_KEY=dv_live_sk_xxxxxxxxxxxxxxxx
claude mcp add orbit -- npx -y @devotel-orbit/mcp-server
```

Or add the block below to your project's `.claude/settings.json` /
`.mcp.json`:

```json
{
  "mcpServers": {
    "orbit": {
      "command": "npx",
      "args": ["-y", "@devotel-orbit/mcp-server"],
      "env": {
        "ORBIT_API_KEY": "dv_live_sk_xxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

---

## VS Code (GitHub Copilot Chat / MCP extension)

Add to `.vscode/mcp.json` in your workspace (or User Settings → `mcp.servers`
for a global install):

```json
{
  "servers": {
    "orbit": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@devotel-orbit/mcp-server"],
      "env": {
        "ORBIT_API_KEY": "dv_live_sk_xxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

Run **MCP: List Servers → orbit → Start Server** from the command palette,
then invoke Orbit tools from Copilot Chat's agent mode.

---

## Windsurf

Open **Windsurf Settings → Cascade → MCP Servers → View raw config** (or edit
`~/.codeium/windsurf/mcp_config.json` directly) and add:

```json
{
  "mcpServers": {
    "orbit": {
      "command": "npx",
      "args": ["-y", "@devotel-orbit/mcp-server"],
      "env": {
        "ORBIT_API_KEY": "dv_live_sk_xxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

Click **Refresh** in the MCP panel, then call Orbit tools from Cascade.

---

## Cline (VS Code extension)

Open **Cline → MCP Servers → Configure MCP Servers**, which opens
`cline_mcp_settings.json`, and add:

```json
{
  "mcpServers": {
    "orbit": {
      "command": "npx",
      "args": ["-y", "@devotel-orbit/mcp-server"],
      "env": {
        "ORBIT_API_KEY": "dv_live_sk_xxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

Cline reloads MCP servers automatically after the file is saved.

---

## Zed

Add to your Zed `settings.json` (**Zed → Settings → Open Settings**):

```json
{
  "context_servers": {
    "orbit": {
      "source": "custom",
      "command": {
        "path": "npx",
        "args": ["-y", "@devotel-orbit/mcp-server"],
        "env": {
          "ORBIT_API_KEY": "dv_live_sk_xxxxxxxxxxxxxxxx"
        }
      }
    }
  }
}
```

Orbit tools then appear in the Zed Assistant panel's context-server picker.

---

## Continue

Add a block to `~/.continue/config.json` (or the workspace `.continue/config.json`):

```json
{
  "mcpServers": {
    "orbit": {
      "command": "npx",
      "args": ["-y", "@devotel-orbit/mcp-server"],
      "env": {
        "ORBIT_API_KEY": "dv_live_sk_xxxxxxxxxxxxxxxx"
      }
    }
  }
}
```

Reload the Continue window (Cmd/Ctrl+Shift+P → **Continue: Reload**) to pick
up the new server.

---

## Configuration

| Env var                      | Default                              | Notes                                               |
| ---------------------------- | ------------------------------------ | --------------------------------------------------- |
| `ORBIT_API_KEY`              | _(required)_                         | `dv_live_sk_*` (live) or `dv_test_sk_*` (sandbox). |
| `ORBIT_API_URL`              | `https://api.orbit.devotel.io`       | Override for self-hosted / staging clusters.        |
| `MCP_TOOLS_ALLOW_LIST`       | _(empty — all enabled)_              | Comma-separated list of tools to expose. Case-insensitive. |
| `MCP_TOOLS_DENY_LIST`        | _(empty)_                            | Comma-separated list of tools to hide. Wins over allow list. |
| `MCP_TOOLS_BUDGET_LIMIT_USD` | `0` (disabled)                       | Per-process advisory cap on chargeable tool spend.  |
| `MCP_SESSION_ID`             | _(auto-generated)_                   | Override the per-process session id for idempotency-key salt. |

### Auth header

The Orbit REST API authenticates MCP traffic via the `X-API-Key` header.
You can also pass a Devotel API key as `Authorization: Bearer …`: since
DC-UNCOVERED_SURFACES-01 (2026-06-01) Bearer tokens that carry a Devotel key
prefix (`dv_live_sk_`, `dv_test_sk_`, `dv_live_pk_`, `dv_test_pk_`) are accepted
as API keys. Only a non-Devotel Bearer token falls through to Clerk session-JWT
verification and is rejected with 401 when no Clerk cookie is present (verified
at `packages/auth/src/middleware.ts` ~880-994).

The MCP server attaches `X-API-Key` automatically. A boot-time ping validates
the key and surfaces a clear error to stderr:

```
[MCP] ORBIT_API_KEY rejected by API; check key format
      (should start with dv_live_sk_ or dv_test_sk_).
```

Generate a key at **Orbit Dashboard → Settings → API Keys**.

---

## Idempotency (LLM retry semantics)

Every chargeable POST tool attaches a deterministic `Idempotency-Key` header
derived from `(toolName, args, sessionId)`. `callId` is intentionally
**excluded** from the key: it advances on every tools/call request (including
network retries), so folding it in would produce a fresh key on each retry,
defeat the 24h idempotency cache, and double-charge. `callId` is retained only
as an audit-trace breadcrumb. When an LLM retries a tool call after a network
blip with the same arguments the Orbit API replays its 24h Redis-cached
response — operators see one message / purchase / campaign-start, never a
duplicate.

Key shape: `mcp_<toolName>_<32 hex chars>` (well under the 255-char API cap).

---

## Per-tool budget guard

Set `MCP_TOOLS_BUDGET_LIMIT_USD=10` to refuse chargeable calls whose
pessimistic estimated cost would exceed $10 in a single MCP session. Advisory
bonus on top of the API's own per-tenant spend caps.

---

## Per-tool allow / deny lists

```bash
# Read-only mode — no chargeable tools.
MCP_TOOLS_ALLOW_LIST=lookup_contact,list_campaigns,kb_query,list_segments,list_templates,get_message_status,search_numbers,check_balance,list_agents,analytics_summary

# Block high-impact tools.
MCP_TOOLS_DENY_LIST=purchase_number,start_campaign,voice_call
```

Disabled tools are NOT advertised in the `tools/list` response — the LLM
never sees them as available primitives.

---

## Docs-MCP tools (no Orbit account required)

Three tools are pure documentation lookups (plus one live, unauthenticated
fetch) — they work even with no `ORBIT_API_KEY` set, so an agent can look up
the real endpoint for a channel, get a copy-pasteable quickstart, or check
current pricing before a developer has signed up for Orbit at all:

| Tool                | What it does                                                                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `get_api_reference` | Real endpoint/method/body shape for a resource (sms, whatsapp, email, voice, templates, numbers, verify, contacts, pricing) — grounded in the same endpoints the execute tools below actually call. |
| `get_quickstart`    | Copy-pasteable "add `<channel>` to my app" curl example + the 5-step sandbox-to-live path.                                                             |
| `get_pricing`       | Live fetch of `GET /api/v1/public/pricing` (no auth) — never a stale hard-coded rate.                                                                  |

See `src/docs-tools.ts` for the full design note (grounding + SSRF-safety
rationale for `get_pricing`'s fetch).

---

## Tools (47 total)

44 API / documentation tools are listed below; the remaining 3 are the
host-model reasoning tools in the next section. Listed in registration order
(see `src/index.ts`). The `Chargeable` column
reflects membership of the `CHARGEABLE_TOOLS` set — those calls flow through the
Idempotency-Key + audit + budget-guard path.

| Tool                         | Method | Endpoint                                       | Chargeable |
| ---------------------------- | ------ | ---------------------------------------------- | :--------: |
| `get_api_reference`          | —      | static reference (no API call)                 | no         |
| `get_quickstart`             | —      | static reference (no API call)                 | no         |
| `get_pricing`                | GET    | `/api/v1/public/pricing` (unauthenticated)     | no         |
| `send_sms`                   | POST   | `/api/v1/messages`                             | yes        |
| `send_whatsapp`              | POST   | `/api/v1/messages`                             | yes        |
| `send_email`                 | POST   | `/api/v1/messages`                             | yes        |
| `lookup_contact`             | GET    | `/api/v1/contacts`                             | no         |
| `create_contact`             | POST   | `/api/v1/contacts`                             | no         |
| `list_campaigns`             | GET    | `/api/v1/campaigns`                            | no         |
| `start_campaign`             | POST   | `/api/v1/campaigns/{id}/send`                  | yes\*      |
| `search_numbers`             | GET    | `/api/v1/numbers/available`                    | no         |
| `purchase_number`            | POST   | `/api/v1/numbers/purchase`                     | yes        |
| `number_lookup`              | POST   | `/api/v1/numbers/lookup`                       | no         |
| `list_segments`              | GET    | `/api/v1/contacts/segments`                    | no         |
| `create_segment_from_prompt` | POST   | `/api/v1/contacts/segments/from-prompt`        | no         |
| `list_journeys`              | GET    | `/api/v1/flows`                                | no         |
| `start_journey`              | POST   | `/api/v1/flows/{id}/start`                     | yes\*      |
| `agent_handoff_to_human`     | POST   | `/api/v1/conversations/{id}/handoff`           | yes\*      |
| `kb_query`                   | POST   | `/api/v1/agents/kb/query`                      | no         |
| `list_templates`             | GET    | `/api/v1/messages/templates`                   | no         |
| `send_template`              | POST   | `/api/v1/messages`                             | yes        |
| `get_message_status`         | GET    | `/api/v1/messages/{id}`                        | no         |
| `verify_start`               | POST   | `/api/v1/verify/send`                          | yes        |
| `verify_check`               | POST   | `/api/v1/verify/check`                         | no         |
| `voice_call`                 | POST   | `/api/v1/voice/calls`                          | yes        |
| `list_agents`                | GET    | `/api/v1/agents`                               | no         |
| `analytics_summary`          | GET    | `/api/v1/analytics/messages`                   | no         |
| `check_balance`              | GET    | `/api/v1/billing/balance`                      | no         |
| `analyze_text`               | POST   | `/api/v1/ai/analyze`                           | no         |
| `create_support_ticket`      | POST   | `/api/v1/inbox/tickets/internal`               | no         |
| `track_event`                | POST   | `/api/v1/events/track`                         | no         |
| `identify_contact`           | POST   | `/api/v1/contacts`                             | no         |
| `create_calendar_event`      | POST   | `/api/v1/agents/calendar-events`               | no         |
| `add_memory`                 | POST   | `/api/v1/agents/memory`                        | no         |
| `search_memory`              | GET    | `/api/v1/agents/memory/search`                 | no         |
| `list_memories`              | GET    | `/api/v1/agents/memory`                        | no         |
| `delete_memory`              | DELETE | `/api/v1/agents/memory/{id}`                   | no         |
| `delete_contact_memories`    | DELETE | `/api/v1/agents/memory`                        | no         |
| `video_create_room`          | POST   | `/api/v1/video/rooms-scheduled`                | no         |
| `video_send_invite`          | POST   | `/api/v1/video/rooms-scheduled/{id}/invites`   | no         |
| `video_end_session`          | DELETE | `/api/v1/video/rooms/{name}`                   | no         |
| `cdp_track`                  | POST   | `/api/v1/events/track`                         | no         |
| `cdp_identify`               | POST   | `/api/v1/events/track`                         | no         |
| `update_contact`             | PUT    | `/api/v1/contacts/{id}`                        | no         |

\* Charges accrue per-message at fan-out time, not at invocation. The budget
guard treats them as zero-cost; the audit log still records every invocation.

### Host-model reasoning (MCP sampling)

Three tools don't call the Orbit API at all — they reason over free text by
asking the connected host to run an LLM completion on the server's behalf
(`sampling/createMessage`). The completion runs on the **host's** model, so no
separate Orbit inference key or spend is involved, and the host stays in
control of model choice and human approval. They return generated text only —
never send or spend — so they are neither chargeable nor high-risk.

| Tool                    | Uses            | What it returns                                   |
| ----------------------- | --------------- | ------------------------------------------------- |
| `summarize_transcript`  | host completion | A concise summary of a call / chat transcript     |
| `draft_message`         | host completion | Draft SMS/WhatsApp/email/campaign copy (not sent) |
| `classify_conversation` | host completion | The best-matching label + a short rationale       |

Requires a host that advertises the `sampling` capability (Claude Desktop,
Claude Code, Cursor, or your own agent). When it's absent, each tool returns a
clear message saying so rather than failing silently.

---

## Interactive app cards (Apps SDK)

In an Apps-SDK host (ChatGPT, Claude), three tools render as interactive
cards instead of plain text. The server advertises the `resources`
capability and serves each card as a `ui://widget/*.html` template that
the host mounts and hydrates with the tool's `structuredContent`:

| Tool                 | Card                | Action button → tool         |
| -------------------- | ------------------- | ---------------------------- |
| `search_numbers`     | Buy a number        | Buy → `purchase_number`      |
| `list_campaigns`     | Launch a campaign   | Launch → `start_campaign`    |
| `get_message_status` | Delivery status     | Refresh → `get_message_status` |

Each button is confirm-to-act and routes back through the existing tool,
so the idempotency, audit, and budget guarantees are unchanged. The card
templates are self-contained (no outbound network) and hidden when the
producing tool is denied via the allow/deny lists. Clients that don't
support generative UI receive the same JSON text content and ignore the
extra metadata.

## Audit log

Every chargeable tool call emits a JSON-Lines audit entry to stderr:

```json
{
  "ts": "2026-05-20T14:33:21.402Z",
  "level": "audit",
  "component": "orbit-mcp",
  "event": "tool.chargeable.invoked",
  "tool": "voice_call",
  "session_id": "mcp_session_94821_1715353101204",
  "call_id": "call_3",
  "args": { "to": "+14155551234", "record": true }
}
```

Pipe stderr to your structured-log sink to retain a full trace. The API key
is never logged.

---

## Local development

```bash
pnpm --filter @devotel-orbit/mcp-server dev        # tsx watch src/index.ts
pnpm --filter @devotel-orbit/mcp-server test       # vitest run
pnpm --filter @devotel-orbit/mcp-server type-check # tsc --noEmit
```

---

## License

MIT. See `LICENSE`.

Maintenance

ActivitySlowing
ResponsivenessNo issues