> ## Documentation Index
> Fetch the complete documentation index at: https://docs.connect-crm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Server

> Technical reference for connecting a custom client to the Connect-CRM Model Context Protocol (MCP) server.

Connect-CRM exposes a remote **Model Context Protocol** server so AI clients can read and write Records through a small set of tools. Most users should just [connect a supported client](/integrations/ai-clients/connecting) — this page is for developers wiring a custom MCP client or who want the protocol details.

## Endpoint

<CodeBlock>[https://mcp.connect-crm.com/mcp](https://mcp.connect-crm.com/mcp)</CodeBlock>

|             |                                                                 |
| ----------- | --------------------------------------------------------------- |
| Server name | `connect-crm-mcp` (version `1.0.0`)                             |
| Transport   | Streamable HTTP, **stateless** (no session id; JSON responses)  |
| Methods     | `POST` for tool calls; the endpoint is a single stateless route |
| Auth        | OAuth 2.1 Bearer token (`Authorization: Bearer <token>`)        |

Because the server is stateless, a fresh MCP server instance is built per request — there is no long-lived SSE session to maintain. Tool work runs within the API gateway's \~29-second timeout, so list/search operations are paginated.

## Authentication

Authentication is **Clerk-native MCP OAuth**: Clerk is the Authorization Server and Connect-CRM is only the protected resource. Clients do PKCE and dynamic client registration directly against Clerk — Connect-CRM never sees credentials.

### Discovery

An unauthenticated request to `/mcp` is challenged with:

<CodeBlock>
  WWW-Authenticate: Bearer
  resource\_metadata="[https://mcp.connect-crm.com/.well-known/oauth-protected-resource](https://mcp.connect-crm.com/.well-known/oauth-protected-resource)"
</CodeBlock>

The server publishes the two discovery documents a spec-compliant client expects:

| Document                                 | Path                                      |
| ---------------------------------------- | ----------------------------------------- |
| Protected resource metadata (RFC 9728)   | `/.well-known/oauth-protected-resource`   |
| Authorization server metadata (RFC 8414) | `/.well-known/oauth-authorization-server` |

The authorization-server metadata mirrors Clerk's endpoints, advertising the
`authorization_code` and `refresh_token` grants, the `S256` PKCE challenge
method, and dynamic client registration. A well-behaved MCP client follows the
discovery chain automatically — you should not need to hard-code any of it.

### Scopes

The capability tier is a trust gradient — read → write → send — and each level
is a separately-consented step:

| Scope           | Purpose                                                                                                                                                |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `user:org:read` | **Required.** Forces the Clerk consent screen to show an Organization (Workspace) selector, so the issued token carries an `org_id`.                   |
| `read:data`     | Required to reach the `/mcp` transport and call any read tool.                                                                                         |
| `write:data`    | Required by the write tools (`create_record`, `update_record`, `delete_record`, and the draft tools `create_draft` / `update_draft` / `delete_draft`). |
| `send:messages` | Required by `send_message` — a dedicated tier **above** `write:data`. Creating a draft never implies the right to send it.                             |

A token granted only `read:data` may call the read tools; the write tools refuse
it with a `forbidden` error, and `send_message` refuses any actor without
`send:messages` even when `write:data` is present.

<Note>
  Clerk's OAuth flow carries identity only (`openid profile email
      user:org:read`) — it cannot issue custom scopes. `read:data`, `write:data`,
  and `send:messages` are **grant-backed capabilities** resolved per request
  from the connection's grant, not claims in the Clerk token. A new connection
  defaults to read-only; the owning user elevates it to write or send from the
  in-app [Connected AI clients](/integrations/ai-clients/managing) screen. A
  tool that needs a capability the grant lacks returns a `forbidden` error whose
  message points the user there.
</Note>

### Workspace resolution

A Connect-CRM Workspace **is** a Clerk Organization (`workspace_id == org_id`). A Clerk user may belong to several Workspaces, so the token must say which one:

* The consent screen (driven by `user:org:read`) makes the user pick a Workspace, and the issued token carries that single `org_id`.
* The server reads `org_id` as the `workspaceId`, validates the `(user, workspace)` pair, and resolves the actor for the request.
* A token with **no** `org_id` is rejected (`401`) — the server never guesses a default Workspace.

**One token = one Workspace.** Accessing another Workspace means a new connection and a fresh consent.

### Entitlement gate

After the Workspace is resolved, the server checks the Workspace's entitlements and requires paid **API access** (`apiAccess`). A Free, lapsed, or downgraded Workspace fails closed and is refused with `403` — regardless of scopes.

## Tools

The v1 surface. Identifiers (`object`) accept either an Object **slug** (e.g. `people`) or its id. Results are returned as JSON text content.

### Read tools (`read:data`)

| Tool              | Arguments                                                                | Returns                                                                                                       |
| ----------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| `list_objects`    | *(none)*                                                                 | Every visible Object in the Workspace: `id`, `slug`, `singular_noun`, `plural_noun`, `type`.                  |
| `list_attributes` | `object`                                                                 | The Object's Attributes with `slug`, type, and required/unique flags.                                         |
| `list_options`    | `object`, `attribute`                                                    | The Options of a `select` / `multi-select` Attribute: each `id`, `title`, `sort_order`, and `is_archived`.    |
| `list_stages`     | `object`, `attribute`                                                    | The ordered Stages of a `pipeline` Attribute: each `id`, `title`, `sort_order`, and closed/won/lost `config`. |
| `get_record`      | `object`, `record_id`                                                    | A single Record with its current attribute values.                                                            |
| `search_records`  | `object`, `limit` (default 25, **clamped to 100**), `offset` (default 0) | A page of Records: `records`, `total`, `has_more`, and `next_offset` (present only while more pages remain).  |

`search_records` is cursor-paginated: pass the previous response's `next_offset`
back as `offset` to fetch the next page. The absence of `next_offset` means
there are no more pages.

`list_options` and `list_stages` enumerate the closed value sets that
`list_attributes` only names by type. When `list_attributes` reports an
Attribute as `select` or `multi-select`, call `list_options` to learn its valid
values; when it reports `pipeline`, call `list_stages` to learn its Stages.
These are the values a `create_record` / `update_record` must use — the Writer
rejects an option or stage value outside the Attribute's set. `list_stages`
returns only active Stages, in pipeline order; `list_options` includes archived
Options (flagged `is_archived: true`) so existing values still resolve, but a new
write should prefer an active Option. Both are read-only Workspace-configuration
reads and never change anything.

### Conversation read tools (`read:data`)

These read the token user's inbox, applying the same Blocked-Conversation
Filter and visibility rules as the app.

| Tool                        | Arguments                                                                                                                 | Returns                                                                                                                                                               |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list_conversations`        | `unread`, `starred`, `archived`, `channel` (all optional), `limit` (default 25, **clamped to 100**), `offset` (default 0) | A page of the user's conversations: `conversations`, `total`, `has_more`, `next_offset`. Each row carries `id`, `channel`, `external_id`, and inbox-state timestamps. |
| `get_conversation`          | `conversation_id`                                                                                                         | One conversation's full message thread in chronological order. Attachments are **metadata only** (`filename`, `content_type`, `size`) — never the file or a URL.      |
| `list_record_conversations` | `object`, `record_id`, `limit` (default 25, **clamped to 100**), `offset` (default 0)                                     | A page of the conversations a Record participates in, **workspace-wide** (including archived), newest first. Each row adds `in_my_inbox`.                             |

`list_conversations` and `list_record_conversations` are cursor-paginated like
`search_records`. Filters on `list_conversations` mirror the app inbox:
`archived` conversations are excluded unless you pass `archived: true`.

`get_conversation` is the natural drill-in for any thread these lists return. A
conversation the user has fully blocked, or that is not in the user's inbox, is
reported as **not found** — a blocked conversation is indistinguishable from a
missing one.

<Note>
  `list_record_conversations` reaches across the whole Workspace (parity with
  the app's per-Record message panel), so it can return a teammate's
  conversation with the Record. Such a row carries `in_my_inbox: false`, its
  owner-private inbox state (`archived_at` / `last_read_at` / `starred_at`) is
  nulled, and `get_conversation` returns **not found** for it. Only rows with
  `in_my_inbox: true` can be opened with `get_conversation`.
</Note>

### Write tools (`write:data`)

| Tool            | Arguments                                                         | Returns                             |
| --------------- | ----------------------------------------------------------------- | ----------------------------------- |
| `create_record` | `object`, `values` (map keyed by attribute slug or id)            | The created Record.                 |
| `update_record` | `object`, `record_id`, `values` (only supplied attributes change) | The updated Record.                 |
| `delete_record` | `object`, `record_id`                                             | A `{ deleted: true }` confirmation. |

Writes go through the same use-cases as the [REST API](/developers/overview):
validation, the Record timeline, and downstream automations all apply
identically. The system fields `id`, `created_at`, and `created_by` are managed
automatically and ignored if supplied.

<Warning>
  `delete_record` is a **hard delete** — the Record is removed permanently and
  cannot be restored, exactly like `DELETE /v1/records/{object}/{recordId}` on
  the REST API. References to the Record from other Records are cleared and a
  `RecordDeletedEvent` fires.
</Warning>

### Draft tools (`write:data`)

Messages follow a **draft-then-send** model. The draft tools compose and edit
drafts; nothing is sent until `send_message` (below) flips a draft. A draft is a
reviewable artifact visible in the inbox UI, and the compose schema is reused
verbatim from the app's compose box, so the same validation applies.

| Tool           | Arguments                                                                                     | Returns                                                                                                                          |
| -------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `create_draft` | `channel`, `body`, `subject` (email only), `reply_to` (optional), `to[]`, `cc[]` (email only) | The created draft's `id`, plus each recipient annotated with its CRM resolution (a matched `record_id` or an explicit no-match). |
| `update_draft` | `draft_id` + the same compose fields                                                          | The updated draft's `id`, with the same recipient annotation.                                                                    |
| `delete_draft` | `draft_id`                                                                                    | The deleted draft's `id`.                                                                                                        |

`channel` is one of `gmail`, `outlook` (email — `subject` plus multiple
`to`/`cc`), or `whatsapp`, `facebook`, `instagram` (text — a single recipient,
no subject). Pass `reply_to` (a parent Message id) to thread into an existing
conversation; omit it to start a new one.

Recipient annotation is **advisory** — raw, unmatched addresses are still
accepted (the new-lead flow is preserved). The draft tools operate on the token
user's own drafts, including drafts started in the app, but only on Messages
still in `draft` status: editing or deleting a message that has already been
sent (or is pending/failed) is a `conflict`.

<Note>
  The compose schema omits `files[]` — attachment content is never accepted over
  MCP. A human attaches files in the app UI before approving a draft. Forwarding
  is also intentionally **not** offered.
</Note>

### Send tool (`send:messages`)

| Tool           | Arguments  | Returns                                                  |
| -------------- | ---------- | -------------------------------------------------------- |
| `send_message` | `draft_id` | `{ id, status: "accepted" }` once the message is queued. |

`send_message` is the **only** send path, and it takes nothing but a `draft_id`
— the body, recipients, and channel come from the draft. A client must therefore
`create_draft` first, so every send leaves a reviewable pre-send artifact and
presents as a distinct, separately-scoped approval.

It is gated by the dedicated `send:messages` scope, **separate from**
`write:data`: a grant that can create drafts cannot send unless it has also been
elevated to send. Re-sending an already-sent (or pending/failed) draft is a
`conflict`, so a retrying client cannot double-send.

<Warning>
  `send_message` returns `status: "accepted"` (queued), **not** delivered.
  Delivery is asynchronous: confirm the outcome with `get_conversation`, where a
  message status of `sent` means the provider accepted it and `failed` means it
  was rejected. There is no polling and no status tool.
</Warning>

## Errors

Tool failures are returned as MCP tool errors mapped from the same typed
domain errors as the REST API — for example a missing Object or Record, a
validation failure, or a `forbidden` error when a read-only token calls a write
tool. HTTP-level `401` (missing/invalid token or absent `org_id`) and `403`
(no API-access entitlement) are returned before any tool runs.

## Revocation

Each connection is an auditable grant the user can revoke from
[Connected AI clients](/integrations/ai-clients/managing). A revoked grant is
rejected (`401`) on its next request, independently of the Clerk token's own
lifetime.
