# MCP server

CommSync speaks the [Model Context Protocol](https://modelcontextprotocol.io).
Point an MCP-capable agent (Claude Desktop, a custom client, or your own
integration) at the endpoint with an [API key](/docs/api-keys). It can then
read threads, send messages, manage contacts and labels, and more. The key
owner's user and org role scope every operation it performs.

  Agents can fetch <a href="/docs/mcp.txt">/docs/mcp.txt</a> for the same catalog
  as plain text with full parameter details — one request, no scraping.

## Connect

The server is a single stateless HTTP endpoint. Authenticate with a
`csk_…` key as a bearer token.

  
    In CommSync, open **Settings → API Keys** and create a key. CommSync shows
    it once — store it securely. See <a href="/docs/api-keys">API keys</a>.
  
  
    POST JSON-RPC to the <code>/api/mcp</code> route on your CommSync API origin.

    ```bash
    curl -X POST "$COMMSYNC_API/api/mcp" \
      -H "Authorization: Bearer csk_your_api_key" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
    ```
  
  
    Use <code>tools/call</code> with the tool name and its arguments.

    ```json
    {
      "jsonrpc": "2.0",
      "id": 2,
      "method": "tools/call",
      "params": { "name": "list_threads", "arguments": { "limit": 20 } }
    }
    ```
  

| | |
| --- | --- |
| **Endpoint** | `POST /api/mcp` (on your CommSync API origin) |
| **Transport** | HTTP, stateless — one request per call |
| **Auth** | `Authorization: Bearer csk_…` |

## Authorization model

Every request resolves your key to `(userId, orgId, role, accessibleChannels)`.
Each tool sits behind one of four gates. The server rejects a call that
exceeds your access before anything happens.

any member any user in the org &nbsp;
channel access needs access to the relevant channel &nbsp;
owner / admin &nbsp;
owner only

- **any member** — any user with org membership.
- **channel access** — you must have access to the channel involved.
- **owner / admin** — reserved for org owners and admins.
- **owner only** — the single workspace owner (for example, billing).

Owners and admins have channel access implicitly; members get it per
channel — see [Roles & permissions](/docs/roles-and-permissions).

CommSync also tags tools by effect: `read` (no change), `write` (mutates), or
`destructive` (removes data — use with care).

### Channel-scoped keys

An API key can be fine-tuned to **specific lines** instead of your full
channel access. When you create (or edit) a key in **Settings**, then **API
Keys**, choose *Specific lines*. Pick the email addresses and phone numbers
it can touch.

The server then intersects your live channel access with the key's
allowlist on every request. A scoped key can only **read, send, and act on
the chosen lines**. Threads on other channels are invisible to it, and
CommSync refuses sends from other channels. Channel listings only show what
is in scope.

- The default is **All channels**: the key follows your live access.
- This includes lines you connect later; CommSync migrated pre-existing
  keys this way.
- A scoped key never out-privileges you.
- If you lose access, or someone deletes the line, it drops from the
  key's scope too.
- Per-user surfaces (contacts, labels, AI, webhooks, profile) do not belong
  to a channel, so scope does not affect them.

## Tool catalog

### Threads

Org-scoped reads (filtered by channel access); mutations write only your own
per-user view state.

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `list_threads` | channel access | read | List threads visible to you |
| `get_thread_summary` | channel access | read | Single-thread summary in list-row shape |
| `get_thread_messages` | channel access | read | Paginated messages for a thread |
| `mark_thread_read` | channel access | write | Clear your unread count |
| `mark_thread_unread` | channel access | write | Force a thread unread for you |
| `archive_thread` | channel access | write | Archive (per-user) |
| `unarchive_thread` | channel access | write | Unarchive (per-user) |
| `mark_thread_spam` | channel access | write | Move to Spam (per-user) |
| `mark_thread_promotions` | channel access | write | Move to Promotions (per-user) |
| `mark_thread_automated` | channel access | write | Move to Automated Messages — non-human mail (per-user) |
| `move_thread_to_inbox` | channel access | write | Clear bucket flags (per-user) |
| `snooze_thread` | channel access | write | Snooze until a timestamp (per-user) |
| `unsnooze_thread` | channel access | write | Clear a snooze (per-user) |
| `delete_thread` | channel access | destructive | Hide the thread from your view; the org keeps its copy |
| `restore_thread` | channel access | write | Restore a soft-deleted thread |
| `delete_message` | channel access | destructive | Hide a message from you |
| `hard_delete_thread` | owner / admin | destructive | Permanently delete the shared thread and messages |

### Outbound sends

CommSync enforces channel access before any send. Compose and forward also
need at least one accessible channel of the right kind.

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `send_sms` | channel access | write | Reply on a current SMS thread |
| `send_email` | channel access | write | Reply on a current email thread |
| `compose_sms` | channel access | write | Start a new SMS conversation |
| `compose_email` | channel access | write | Start a new email conversation |
| `forward_message` | channel access | write | Forward a message to a new recipient |
| `resend_failed_message` | channel access | write | Retry a failed outbound message |
| `get_send_capacity` | channel access | read | Sustainable send rate per line, plus any active rate-limit pause |

#### Rate limits never reach you

CommSync accepts every authorised send and owns delivery from that point
on — **this covers carrier and mail-host rate limits too**. Send tools do
not return `429` and never ask you to retry. When a provider throttles us
the message stays queued, backs off, and goes out on its own.

Every send tool returns a receipt that describes where the message
actually is:

```json
{
  "accepted": true,
  "messageId": "cm9x…",
  "threadId": "cm7a…",
  "state": "waiting_on_line",
  "estimatedSendAt": "2026-07-29T18:41:12.000Z",
  "pacing": {
    "provider": "JustCall",
    "sustainedPerMinute": 54,
    "rateLimited": true,
    "reason": "JustCall rate limit — waiting 45s"
  }
}
```

`state` is one of `dispatching`, `waiting_on_line`, `scheduled`, `sent`, or
`failed`. **Never call a send tool twice for the same message** — CommSync
has already queued a `waiting_on_line` message and will deliver it.

Before a bulk run, call `get_send_capacity` for the sustainable rate on each
line you can send from. If you exceed it, that is safe — messages queue
rather than fail; they simply take longer to go out.

### Channel management

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `list_email_accounts` | channel access | read | List email-account channels you can see |
| `add_email_account` | any member | write | Link an `IMAP`/`SMTP` mailbox to the org |
| `delete_email_account` | owner / admin | destructive | Remove an email account |
| `test_email_account` | channel access | read | Test stored `IMAP`/`SMTP` credentials |
| `list_phone_numbers` | channel access | read | List phone-number channels you can see |
| `add_phone_number` | any member | write | Link a phone number to the org |
| `delete_phone_number` | owner / admin | destructive | Remove a phone number |

### Billing

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `get_billing_state` | any member | read | Tier, status, seats, caps, usage |
| `change_tier` | owner only | write | Swap subscription tier (prorates) |
| `set_seats` | owner only | write | Set the paid seat quantity (prorates) |
| `seats_preview` | owner only | read | Dry-run a prorated seat change |

### Contacts

The contact graph is private per user — these never leak across users.

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `list_contacts` | any member | read | List your contacts |
| `get_contact` | any member | read | A contact, with identities and labels |
| `create_contact` | any member | write | Create a new contact |
| `update_contact` | any member | write | Update the display name or notes |
| `delete_contact` | any member | destructive | Delete a contact; identities orphan |
| `merge_contacts` | any member | destructive | Merge whole contacts into one survivor; CommSync deletes the sources |
| `merge_identities` | any member | write | Merge two identities under one contact |
| `split_identity` | any member | write | Detach an identity into an orphan |
| `attach_identity_to_contact` | any member | write | Attach an orphan to a contact |
| `promote_identity_to_contact` | any member | write | Promote an orphan into a new contact |

### Identities

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `get_identity` | any member | read | An identity and its contact |
| `update_identity_notes` | any member | write | Edit per-channel notes |
| `list_orphaned_identities` | any member | read | Identities not yet attached to a contact |
| `list_all_identities` | any member | read | Every identity you own |

### Labels

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `list_labels` | any member | read | List labels with usage counts |
| `create_label` | any member | write | Create a label (name and hex color) |
| `update_label` | any member | write | Update the name, color, or AI prompt |
| `delete_label` | any member | destructive | Delete a label everywhere |
| `assign_label` | any member | write | Apply a label to a contact or identity |
| `unassign_label` | any member | write | Remove a label |

### AI

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `get_ai_settings` | any member | read | Read [AI](/docs/ai) configuration |
| `update_ai_settings` | any member | write | Update AI configuration |
| `get_todays_digest` | any member | read | Today's daily digest (or a date) |
| `list_digests` | any member | read | Recent daily digests |
| `dismiss_digest` | any member | write | Mark a digest dismissed |
| `trigger_digest_run` | any member | write | Run a digest now |
| `list_ai_runs` | any member | read | Recent AI activity entries |
| `test_ai_connectivity` | any member | read | Check that CommSync has AI configured |
| `trigger_inbox_backfill` | any member | write | Classify historical senders into Promotions or Spam |

### Search, account & webhooks

| Tool | Access | Kind | Description |
| --- | --- | --- | --- |
| `search` | any member | read | Search contacts, identities, messages |
| `search_threads` | any member | read | Full thread-centric search: every conversation that matches a query, ranked and paginated, with match snippets |
| `get_profile` | any member | read | Your profile (id, email, name) |
| `update_profile` | any member | write | Update your display name |
| `list_webhooks` | any member | read | List your [webhook](/docs/webhooks) endpoints |
| `get_webhook` | any member | read | A single endpoint |
| `register_webhook` | any member | write | Create an endpoint (returns secret once) |
| `update_webhook` | any member | write | Patch url / events / mode / status |
| `rotate_webhook_secret` | any member | write | Rotate the signing secret (old valid 24h) |
| `delete_webhook` | any member | destructive | Delete an endpoint + its history |
| `list_webhook_deliveries` | any member | read | Paginated delivery log |
| `resend_webhook_delivery` | any member | write | Retry a delivery |

  CommSync Agents are the AI teammates that answer inbound texts and
  email. You configure and manage them through the app or the REST admin
  API, not through this MCP tool catalog. See <a href="/docs/agents">Agents</a>
  for details.

  There is deliberately no MCP tool that lets an external agent create,
  reconfigure, or approve turns for a CommSync Agent. Use
  <code>send_sms</code> or <code>send_email</code> above to have your own
  integration reply directly instead.

## Resources

Beyond tools, the server exposes MCP resources for direct reads:

```text
commsync://threads/{threadId}/messages   — messages in a thread
commsync://contacts/{personId}            — a contact's detail
commsync://digests/{localDate}            — the AI digest for a date
```

## Common workflows

  
    `list_threads` → `get_thread_messages(threadId)` to read the latest.
  
  
    `get_thread_messages(threadId)` to find the identity a message came in on →
    <code>send_sms</code> or <code>send_email</code> with that `identityId`.
  
  
    `list_orphaned_identities` (or `search`) to find the stray identity →
    `merge_identities(identityAId, identityBId)` or
    `attach_identity_to_contact(personId, identityId)`. For two whole
    contact records of the same human, confirm both with `get_contact` and
    call `merge_contacts(survivorPersonId, sourcePersonIds)` instead.
  
  
    You do not have to call `list_threads` again and again. Register a
    <a href="/docs/webhooks">webhook</a> and call back into MCP only when an
    event fires.
  

  Create a key in <a href="/docs/api-keys">API keys</a>, then wire up
  <a href="/docs/webhooks">webhooks</a> so your agent reacts to messages
  and does not have to ask again and again.
  Full parameter reference: <a href="/docs/mcp.txt">/docs/mcp.txt</a>.
