Skip to main content
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 nameconnect-crm-mcp (version 1.0.0)
TransportStreamable HTTP, stateless (no session id; JSON responses)
MethodsPOST for tool calls; the endpoint is a single stateless route
AuthOAuth 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:
DocumentPath
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:
ScopePurpose
user:org:readRequired. Forces the Clerk consent screen to show an Organization (Workspace) selector, so the issued token carries an org_id.
read:dataRequired to reach the /mcp transport and call any read tool.
write:dataRequired by the write tools (create_record, update_record, delete_record, and the draft tools create_draft / update_draft / delete_draft).
send:messagesRequired 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.

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)

ToolArgumentsReturns
list_objects(none)Every visible Object in the Workspace: id, slug, singular_noun, plural_noun, type.
list_attributesobjectThe Object’s Attributes with slug, type, and required/unique flags.
list_optionsobject, attributeThe Options of a select / multi-select Attribute: each id, title, sort_order, and is_archived.
list_stagesobject, attributeThe ordered Stages of a pipeline Attribute: each id, title, sort_order, and closed/won/lost config.
get_recordobject, record_idA single Record with its current attribute values.
search_recordsobject, 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.
ToolArgumentsReturns
list_conversationsunread, 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_conversationconversation_idOne conversation’s full message thread in chronological order. Attachments are metadata only (filename, content_type, size) — never the file or a URL.
list_record_conversationsobject, 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.

Write tools (write:data)

ToolArgumentsReturns
create_recordobject, values (map keyed by attribute slug or id)The created Record.
update_recordobject, record_id, values (only supplied attributes change)The updated Record.
delete_recordobject, record_idA { 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.

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.
ToolArgumentsReturns
create_draftchannel, 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_draftdraft_id + the same compose fieldsThe updated draft’s id, with the same recipient annotation.
delete_draftdraft_idThe 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.

Send tool (send:messages)

ToolArgumentsReturns
send_messagedraft_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.