Endpoint
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:
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:
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 singleorg_id. - The server reads
org_idas theworkspaceId, validates the(user, workspace)pair, and resolves the actor for the request. - A token with no
org_idis rejected (401) — the server never guesses a default Workspace.
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)
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.
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)
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.
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.
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)
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.
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 aforbidden 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.