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 — this page is for developers wiring a custom MCP client or who want the protocol details.
Endpoint
https://mcp.connect-crm.com/mcp
| |
|---|
| 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:
WWW-Authenticate: Bearer
resource_metadata=“https://mcp.connect-crm.com/.well-known/oauth-protected-resource”
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.
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 screen. A
tool that needs a capability the grant lacks returns a forbidden error whose
message points the user there.
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.
The v1 surface. Identifiers (object) accept either an Object slug (e.g. people) or its id. Results are returned as JSON text content.
| 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.
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.
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.
| 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:
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.
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.
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.
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.
| 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.
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.
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. A revoked grant is
rejected (401) on its next request, independently of the Clerk token’s own
lifetime.