Model Context Protocol server exposing Coevera CRM data and operations as AI-ready tools.
Model Context Protocol (MCP) is an open standard that lets AI assistants (such as Claude, ChatGPT, or any MCP-compatible agent) call structured tools hosted on a remote server over HTTP. Instead of copy-pasting data into a chat window, the AI connects to this server and reads, searches, or updates CRM records directly.
This server implements the Streamable HTTP MCP transport.
Requests are POSTed to the /mcp endpoint with a Bearer token.
The AI model discovers what tools are available, then calls them with structured JSON arguments.
Long-running tool calls stream incremental progress notifications back to the client
via Server-Sent Events (SSE). The client receives status messages such as
"Fetching records…" and "Normalizing records…" before the final result,
so the user sees live feedback instead of a blank wait. SSE is activated automatically when the client
sends Accept: text/event-stream in the request (all MCP-compliant clients do this);
clients that omit the header receive a plain JSON response.
Each MCP session is stateless — authentication is re-validated on every request via OAuth token introspection.
This server bridges AI assistants (such as Claude, ChatGPT, or any MCP-compatible agent) with the Coevera CRM. Instead of manually exporting data and pasting it into a chat window, the AI connects here and reads, searches, or updates CRM records directly using structured tool calls.
search_recordssearch_records + get_recordscreate_recordsupload_cloud_object_document, then POSTs the file to the returned upload_urlmeta_describe_entitymeta_list_entity_types →
meta_describe_entity →
search_records / get_records
Every MCP tool call follows this path from the AI assistant to the CRM and back:
| # | Step | Component |
|---|---|---|
| 1 | The AI assistant posts a tool-call JSON to POST /mcp with an Authorization: Bearer <token> header |
HTTP / Streamable MCP transport |
| 2 | Token is introspected against the Coevera OAuth server; required scopes are verified and the tenant space_id is extracted |
BearerAuthMiddleware |
| 3 | Request is checked against burst and sustained rate limits (when enabled) | RateLimitMiddleware |
| 4 | FastMCP routes the call to the named tool handler | core/tools/simple.py |
| 5 | Tool handler validates inputs and sends an initial notifications/progress SSE event to the client (e.g. "Fetching records…"), then delegates to the business logic layer |
core/tools/simple.py |
| 6 | PipelinerService queries the Coevera CRM REST / GraphQL API (with automatic retry on transient failures) |
core/pipeliner_client.py |
| 7 | Response is normalized to an LLM-friendly JSON structure; a final progress event is sent, then the result is returned to the AI assistant | core/llm_view.py |
Each request is fully stateless — no session is maintained between calls.
The space_id tenant identifier is re-extracted from the token on every request.
All /mcp requests must carry an Authorization: Bearer <token> header.
Tokens are obtained from the Coevera OAuth2 authorization server using the Authorization Code flow.
OAuth discovery documents are served at
/.well-known/oauth-authorization-server and
/.well-known/openid-configuration.
Accept: text/event-stream, plain JSON otherwise)file — no Bearer header; the single-use token issued by upload_cloud_object_document is the credential. 410 if expired/used, 503 if upload store not configured)The server exposes 16 tools. Read-only tools are safe to call at any time. Write tools (write) modify CRM data.
Returns identity information for the authenticated user: connected Pipeliner space (space_id), user ID, OAuth client ID, granted scopes, token expiry (ISO 8601 UTC), and resource audience. No CRM API call is made — all data is derived from the validated Bearer token. Invoke when user identity, connected space, or OAuth permissions need to be determined.
| Name | Type | Required | Description |
|---|---|---|---|
| No parameters | |||
Returns all entity types available in the CRM for the authenticated user. Each entry includes the entity name, display label, kind (primary or relation), api_name, custom_name, description, and the user's CRUD permissions.
| Name | Type | Required | Description |
|---|---|---|---|
| No parameters | |||
Returns the field schema for a named CRM entity. Each field entry contains: api_name, label, data type, required, read_only, filterable, default_value, and — for dropdown fields — an options list. When the entity has expandable relation fields, the response also includes an expandable_fields array. Each entry has api_name, entity_name (the related entity), and type (object for single-record relations, list for multi-record relations). This includes both built-in relations and custom lookup fields created in the CRM UI.
| Name | Type | Required | Description |
|---|---|---|---|
| entity_name* | string | Required | The entity name (e.g. Account). |
fields array, an optional expandable_fields array listing relation fields that can be passed to the expand parameter of search_records or get_records, and an optional ownership object describing how ownership can be transferred for this entity: supported (bool), requires_unit (bool), options (list of valid keys for the change_ownership tool's options parameter), and an optional warning string for entities where the service account loses access after transfer. The ownership key is omitted entirely when ownership transfer is not supported.
Searches a CRM entity and returns matching records for the current page. Supports boolean filter trees (AND/OR/NOT), typed field comparators, sorting, and cursor-based pagination.
| Name | Type | Required | Description |
|---|---|---|---|
| entity_name* | string | Required | Target entity. |
| fields* | string[] | Required | Field api_names to include in each record. |
| filter | object|null | Optional | Boolean filter tree. Logical nodes: AND / OR (array of child nodes), NOT (single child node). Leaf node: {"<field>": {"<op>": value}}. Omit to match all records. See Filter DSL reference below. |
| order_by | array|null | Optional | List of sort objects, each mapping one field api_name to "Asc" or "Desc". Example: [{"modified": "Desc"}, {"name": "Asc"}]. |
| limit | int | Optional | Page size (default 100, max 100). |
| after | string|null | Optional | Pagination cursor from a previous response's next_cursor. |
| expand | object|null | Optional | Load related entities inline. Values must be non-empty sub-field lists. Flat key: {"product_relations": ["id", "product_id"]}. Dotted key: {"product_relations.product": ["name", "id"]} — expands a nested relation inside the parent with explicit sub-fields. Both forms can be combined. See Expand Fields reference. |
Fetches specific records by ID from a CRM entity. Returns all matching records as a plain list — no sample cap, no schema inference.
| Name | Type | Required | Description |
|---|---|---|---|
| entity_name* | string | Required | Target entity. |
| record_ids* | string[] | Required | List of record UUIDs to fetch. |
| fields* | string[] | Required | Field api_names to include. |
| expand | object|null | Optional | Load related entities inline. Same format as search_records. See Expand Fields reference. |
Creates one or more records in a CRM entity. Each record is a dict of field api_name → value pairs.
| Name | Type | Required | Description |
|---|---|---|---|
| entity_name* | string | Required | Target entity. |
| records* | object[] | Required | List of record dicts (field api_name → value). |
{"results": [...]} — one entry per input record, in the same order.| Outcome | Shape |
|---|---|
| Success | {"id": "...", "url": "..."} |
| Read-only field violation | {"error": "read_only_field_violation", "message": "...", "violations": ["field", ...], "id": "..."} |
| CRM API error | {"code": "...", ...} |
| Tool error | {"error": "invalid_input" | "permission_denied" | "api_error" | "server_error", "message": "..."} |
Partially updates existing records in a CRM entity. Only the fields provided in each record dict are changed. Each record dict must include the record's id field.
| Name | Type | Required | Description |
|---|---|---|---|
| entity_name* | string | Required | Target entity. |
| records* | object[] | Required | List of record dicts, each must contain "id". |
{"results": [...]} — one entry per input record, in the same order. Records with errors are skipped; valid records are still submitted.| Outcome | Shape |
|---|---|
| Success | {"id": "...", "url": "..."} |
| Missing id | {"error": "missing_id", "message": "..."} |
| Read-only field violation | {"error": "read_only_field_violation", "message": "...", "violations": ["field", ...], "id": "..."} |
| CRM API error | {"code": "...", ...} |
| Tool error | {"error": "invalid_input" | "permission_denied" | "api_error" | "server_error", "message": "..."} |
Deletes records by ID from a CRM entity. This is a soft delete: records are flagged as deleted, not physically removed. Related records can be affected — deleting a recurring Appointment also deletes its occurrences, deleting an Email also deletes its draft replies, and deleting an Account re-parents its child accounts. Only entities whose delete permission is true in meta_list_entity_types can be deleted; at most 100 IDs per call.
| Name | Type | Required | Description |
|---|---|---|---|
| entity_name* | string | Required | Target entity. |
| record_ids* | string[] | Required | List of record UUIDs to delete. |
{"results": [...]} — one entry per input ID, in the same order. Failing IDs are skipped; the remaining records are still deleted.| Outcome | Shape |
|---|---|
| Success | {"id": "..."} |
| Invalid id (not a UUID) | {"error": "invalid_id", "message": "...", "id": "..."} |
| CRM API error (e.g. not found, no permission) | {"code": "...", ...} |
| Tool error | {"error": "invalid_input" | "permission_denied" | "api_error" | "server_error", "message": "..."} |
Initiates a two-phase file upload into a CRM CloudObjectFolder. This tool is phase 1: it registers the upload (filename, MIME type, target folder, visibility) and returns a short-lived, single-use upload_url. Phase 2 is performed by the client outside the MCP session — it POSTs the file binary as multipart/form-data (field name file) to that URL, e.g. curl -X POST {upload_url} -F file=@/path/to/file. The upload endpoint creates the CloudObject in the CRM and returns it. See the File Upload reference for the full flow, and use search_records on CloudObjectFolder to find a valid folder_id.
| Name | Type | Required | Description | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| document* | object | Required | Upload descriptor with the fields below.
|
{"cloud_object_id": "...", "upload_token": "...", "upload_url": "...", "expires_in_seconds": 300, "max_file_bytes": ..., "max_file_mib": ...} — cloud_object_id is a tracking id for the upload (not yet a CRM record id). The client must POST the file to upload_url before expires_in_seconds elapses; the token is consumed on first use. max_file_bytes / max_file_mib is the server's current size limit (upload_max_bytes, see the File Upload reference for this server's value); check the file against it before POSTing — a larger file gets HTTP 413 and the single-use token is spent. The limit is returned at runtime rather than written into the tool description because clients snapshot tool contracts and the value differs per environment. The POST returns the created CloudObject: {"createCloudObject": {"cloudObject": {"id", "filename", "mimeType", "size", "url", "publicUrl", "isPublic", "created"}}}. Returns {"error": "Upload store is not configured"} when the server has no Redis upload store.
Moves a set of opportunities to a new pipeline step. This is a business operation, not a plain field update — it triggers server-side logic: timeframe-in-step tracking, approval removal, optional memo creation, activity completion, and quote status transitions. Cross-pipeline moves are not allowed. Batches of 20 or more are processed asynchronously; the response will include an asyncOperationId to track progress.
| Name | Type | Required | Description |
|---|---|---|---|
| opportunity_ids* | string[] | Required | UUIDs of the opportunities to move. |
| step_id* | string | Required | UUID of the destination pipeline step (must belong to the same pipeline). |
| memo_text | string|null | Optional | Optional memo text linked to each opportunity after the move. |
| complete_activity_ids | string[]|null | Optional | UUIDs of activities to mark as completed on each opportunity. |
| accept_quote_ids | string[]|null | Optional | UUIDs of quotes to transition to Accepted status. |
| archive_quote_ids | string[]|null | Optional | UUIDs of quotes to transition to Lost status. |
| rollback_method | string | Optional | ROLLBACK_ALL (default) — abort the entire batch on any failure. ROLLBACK_FAILED — commit successful records, roll back only failures. |
{asyncOperationId: "..."}.
Archives a set of opportunities. This is a business operation, not a plain flag update — it triggers server-side logic: AI fitness indicator snapshot, approval removal, optional memo creation, activity completion, and quote archiving (linked quotes transition to Lost). If an opportunity has a recurrence configured with a Lost condition, archiving may spawn a new copy at the recurrence step. Batches of 20 or more are processed asynchronously.
| Name | Type | Required | Description |
|---|---|---|---|
| opportunity_ids* | string[] | Required | UUIDs of the opportunities to archive. |
| entity_fields | object|null | Optional | Optional field values to apply before archiving (e.g. {"reason_of_close_id": "...", "closing_date": "2024-12-31"}). |
| memo_text | string|null | Optional | Optional memo text linked to each opportunity after archiving. |
| complete_activity_ids | string[]|null | Optional | UUIDs of activities to mark as completed. |
| archive_quote_ids | string[]|null | Optional | UUIDs of quotes to transition to Lost status. |
| rollback_method | string | Optional | ROLLBACK_ALL (default) or ROLLBACK_FAILED. |
{asyncOperationId: "..."}.
Archives a set of leads. This is a business operation, not a plain flag update — it triggers server-side logic: AI fitness indicator snapshot, timeframe-in-step tracking, approval removal, optional memo creation, and task completion. When entity_fields is omitted, all field validation is skipped entirely. Activity completion is scoped to Tasks only; appointments are not affected. Batches of 20 or more are processed asynchronously.
| Name | Type | Required | Description |
|---|---|---|---|
| lead_ids* | string[] | Required | UUIDs of the leads to archive. |
| entity_fields | object|null | Optional | Optional field values to apply before archiving (e.g. {"reason_of_close_id": "..."}). When omitted, all field validation is skipped. |
| memo_text | string|null | Optional | Optional memo text linked to each lead after archiving. |
| complete_activity_ids | string[]|null | Optional | UUIDs of Tasks (only) to mark as completed. |
| rollback_method | string | Optional | ROLLBACK_ALL (default) or ROLLBACK_FAILED. |
{asyncOperationId: "..."}.
Converts a set of leads into opportunities. This is a destructive entity-type promotion — each Lead is physically deleted and replaced by a new Opportunity sharing the same primary key. Common fields are copied; the target step must belong to an Opportunity pipeline (steps with lead_process_id are invalid targets). The final (Won) step is also rejected. Both the Lead's step checklist and the target Opportunity step's checklist are enforced. All leads are converted synchronously regardless of batch size.
| Name | Type | Required | Description |
|---|---|---|---|
| lead_ids* | string[] | Required | UUIDs of the leads to qualify (convert to opportunities). |
| step_id* | string | Required | UUID of the target Opportunity pipeline step (must have pipeline_id, not lead_process_id; must not be the Won step). |
| opportunity_fields | object|null | Optional | Additional camelCase field name→value pairs for the resulting opportunities (e.g. {"closingDate": "2026-12-31"}). Keys must use camelCase GraphQL names, not snake_case. Do not include stepId here. |
| rollback_method | string|null | Optional | ROLLBACK_ALL or ROLLBACK_FAILED. The mutation has no server-side default — omit to let the server decide. |
Unified ownership reassignment across supported CRM entity types. Dispatches to a dedicated GraphQL mutation for Task and Opportunity (processed sequentially, one call per record), and to a plain owner_id field update for Contact, Account, Appointment, Lead, and Memo. Lead and Memo: after reassignment the service account loses write and ownership permissions on those records and cannot self-revert — each successful result includes a warning field. All other entity types return an unsupported error without hitting the CRM.
| Name | Type | Required | Description |
|---|---|---|---|
| entity_name* | string | Required | CRM entity type to reassign. Supported: Task, Opportunity, Contact, Account, Appointment, Lead, Memo. |
| record_ids* | string[] | Required | UUIDs of the records to reassign. Must not be empty. |
| owner_id* | string | Required | UUID of the new owner (Client). Must be a member of the target unit. |
| unit_id | string|null | Optional | UUID of the new sales unit. Required for Task and Opportunity; optional for other entity types. |
| options | object|null | Optional | Entity-specific flags forwarded to the mutation. Unknown keys are silently ignored; missing keys default to false. Supported keys: keep_owner_as_editor (Task, Opportunity) — keep the displaced owner as a sharing Editor; transfer_linked_records (Opportunity) — cascade-reassign linked records owned by the displaced owner. |
| Outcome | Shape |
|---|---|
| Success | {"id": "...", "url": "..."} |
| Success with warning (Lead / Memo) | {"id": "...", "url": "...", "warning": "..."} |
| Per-record mutation error (Task / Opportunity) | {"id": "...", "error": "..."} |
| Tool error | {"error": "invalid_input" | "permission_denied" | "api_error" | "server_error", "message": "..."} |
Returns the checklist items configured on a pipeline step — the requirements that must be satisfied before an opportunity can advance to that step. When opportunity_id is provided, each item is annotated with is_complete (true/false) showing whether the requirement is already satisfied for that specific opportunity. Results are ordered by sort_order ascending.
| Name | Type | Required | Description |
|---|---|---|---|
| step_id* | string | Required | UUID of the pipeline step to inspect. |
| opportunity_id | string|null | Optional | UUID of a specific opportunity. When provided, each item is annotated with its completion status for that opportunity. |
id, item_type (FieldType / ActivityType / CreateNote / AttachDocument / UseDocumentTemplate / SendEmail / EnrollToEmailSequence / RunProcess / Default), item_validation (Required / RelatedActivityLinked / RelatedActivityCompleted), sort_order, and — when opportunity_id was supplied — is_complete.
Records that a step checklist requirement has been satisfied for a specific opportunity, lead, or quote by creating a LeadOpptyStepCheckListRelation record. Each checklist item type requires different evidence — supply the matching ID parameter. FieldType items do not need a relation record; set the field value directly via update_records instead. Either lead_oppty_id or quote_id must be provided, but not both.
| Name | Type | Required | Description |
|---|---|---|---|
| step_checklist_id* | string | Required | UUID of the StepChecklist item being satisfied. |
| lead_oppty_id | string|null | Optional | UUID of the Opportunity or Lead. Mutually exclusive with quote_id. |
| quote_id | string|null | Optional | UUID of the Quote. Mutually exclusive with lead_oppty_id. |
| item_checked | bool|null | Optional | Set to true for Default-type items that have no linked evidence. |
| activity_id | string|null | Optional | UUID of the Task or Appointment that satisfies an ActivityType item. |
| note_id | string|null | Optional | UUID of the Note that satisfies a CreateNote item. |
| cloud_object_id | string|null | Optional | UUID of the uploaded document for AttachDocument / UseDocumentTemplate items. |
| email_id | string|null | Optional | UUID of the sent Email for SendEmail items. |
LeadOpptyStepCheckListRelation record.
Every MCP tool declares four behavior hints so that AI clients (and their safety layers) can describe a tool's impact before it is called. Each hint is set explicitly — never null — and justified against the tool's actual behavior. The values below are the single source of truth: they are served verbatim in each tool's MCP definition.
| Hint | Meaning |
|---|---|
| Read Only | readOnlyHint — the tool only reads or computes; it never creates, updates, deletes, or sends data. |
| Destructive | destructiveHint — the tool can delete, overwrite, or cause irreversible side effects. Only meaningful for writes. |
| Idempotent | idempotentHint — calling again with the same arguments yields the same end state and no additional side effects. |
| Open World | openWorldHint — true only if the tool can reach arbitrary external URLs, files, or resources; false when it affects a bounded target (e.g. a record by id in this CRM tenant). |
| Hint | Value | Justification |
|---|---|---|
| Read Only | true | Only reads the current session's identity claims from the validated Bearer token; makes no CRM API call and writes nothing. |
| Destructive | false | Writes nothing, so nothing can be overwritten or deleted. |
| Idempotent | true | Returns the same identity for the same session on every call, with no side effects. |
| Open World | false | Returns identity claims for the caller's own Coevera CRM session only; no external resources are contacted. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | true | Fetches entity metadata and permissions from the CRM; writes or modifies nothing. |
| Destructive | false | Read-only call; nothing is overwritten or deleted. |
| Idempotent | true | Repeated calls return the same metadata and cause no state change. |
| Open World | false | Reads only from the caller's own Coevera CRM tenant; it never touches arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | true | Fetches an entity's field schema from the CRM; writes or modifies nothing. |
| Destructive | false | Read-only call; nothing is overwritten or deleted. |
| Idempotent | true | The same entity returns the same schema on every call, with no side effects. |
| Open World | false | Reads only from the caller's own Coevera CRM tenant; it never touches arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | true | Queries records from the CRM; writes or modifies nothing. |
| Destructive | false | Query only; nothing is overwritten or deleted. |
| Idempotent | true | Re-running the same query causes no state change. |
| Open World | false | Reads only from the caller's own Coevera CRM tenant; it never touches arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | true | Fetches records by id from the CRM; writes or modifies nothing. |
| Destructive | false | Read-only call; nothing is overwritten or deleted. |
| Idempotent | true | Repeated fetches of the same ids cause no state change. |
| Open World | false | Reads only from the caller's own Coevera CRM tenant; it never touches arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Writes new records to the CRM. |
| Destructive | false | Purely additive — creates new records without overwriting or deleting existing data. |
| Idempotent | false | Each call inserts new records; calling again with the same input creates duplicates. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Modifies existing field values on records in the CRM. |
| Destructive | true | Overwrites existing field values in place; the previous values are permanently replaced. |
| Idempotent | true | Applying the same field values again yields the same end state, with no further change. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Deletes existing records in the CRM. |
| Destructive | true | Removes records from the CRM (soft delete — flagged as deleted) and cascades to related records, e.g. recurring appointment occurrences and draft email replies are deleted too. |
| Idempotent | true | Deleting the same ids again leaves the same end state; already-deleted ids are simply reported as not found. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Phase 1 of a two-phase upload: writes a one-time upload token to Redis and, once the client POSTs the file to the returned upload_url, a new CloudObject document is created in the CRM. |
| Destructive | false | Purely additive — a new CloudObject is created inside the target folder; no existing document, record, or field is overwritten or deleted. |
| Idempotent | false | Each call mints a fresh single-use token; completing the upload twice creates two separate CloudObject documents. |
| Open World | true | Unlike the other tools, the upload is completed by an out-of-band HTTP POST from the client's own environment (e.g. curl in a sandbox), so the tool reaches beyond the bounded MCP session into the client's local files and network. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Moves opportunities to a new pipeline step. |
| Destructive | true | Overwrites step state and cascades irreversible side effects: activities completed and quote statuses transitioned (Accepted/Lost). A pending approval blocks the move rather than being removed. |
| Idempotent | false | Side effects such as memo creation and activity completion re-run on each call, so it is not a no-op. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Archives opportunities. |
| Destructive | true | Removes opportunities from active state, transitions linked quotes to Lost, and may spawn a new recurrence copy — irreversible side effects. |
| Idempotent | false | Repeating can create additional memos/recurrence copies and re-run side effects, so it is not a no-op. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Archives leads. |
| Destructive | true | Removes leads from active state and completes linked tasks, overwriting prior state. |
| Idempotent | false | Side effects (memo creation, task completion) re-run on each call, so it is not a no-op. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Converts leads into opportunities. |
| Destructive | true | Each lead is physically deleted and replaced by a new opportunity sharing its primary key; the source lead is permanently removed. |
| Idempotent | false | After the first call the source lead no longer exists, so a repeat call cannot reproduce the same effect. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Reassigns the owner (and optionally sales unit) on existing records. |
| Destructive | true | Overwrites the existing owner in place; the change can be irreversible — for Lead/Memo the service account loses write/ownership access after transfer and cannot self-revert. |
| Idempotent | false | Triggers per-record side effects, and for Lead/Memo the caller can lose access after the first call, so repeating is not guaranteed to be a no-op. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | true | Fetches step-checklist definitions and per-record completion status from the CRM; writes nothing. |
| Destructive | false | Read-only call; nothing is overwritten or deleted. |
| Idempotent | true | Repeated calls return the same status with no side effects. |
| Open World | false | Reads only from the caller's own Coevera CRM tenant; it never touches arbitrary external URLs, files, or resources — a closed, bounded domain. |
| Hint | Value | Justification |
|---|---|---|
| Read Only | false | Writes a new checklist-relation record marking a step requirement satisfied. |
| Destructive | false | Purely additive — creates a new LeadOpptyStepCheckListRelation; no existing record or field is overwritten or deleted (same class as create_records). |
| Idempotent | false | Each call can create an additional relation record; repeating is not a no-op. |
| Open World | false | Affects only records within the caller's own Coevera CRM tenant, addressed by id; it never writes to arbitrary external URLs, files, or resources — a closed, bounded domain. |
The expand parameter on search_records and get_records
loads related entities inline with each record instead of returning them as URL references.
Use meta_describe_entity to find which relation fields support expansion
(look for the expandable_fields array in the response).
| Form | Key | Value | Result |
|---|---|---|---|
| Flat | "relation_field" |
["sub", "nested.sub", ...] |
Expands the relation; only the listed sub-fields are returned. Dot-notation in a sub-field also expands that nested object. |
| Dotted | "relation_field.nested" |
["sub", ...] |
Explicitly expands a nested relation inside the parent and returns only the listed sub-fields from it. |
Values must be non-empty lists — an empty list is rejected.
Both forms can be combined in the same expand object.
Returns only account_id and the nested account object (with id and name)
for each item in account_relations.
{
"account_relations": ["account_id", "account.id", "account.name"]
}
Expand product_relations with explicit sub-fields, and also expand the
nested product object returning only name and id.
{
"product_relations": ["id", "opportunity_id", "product_id"],
"product_relations.product": ["name", "id"]
}
meta_describe_entity and check the
expandable_fields array to see which relation fields support expansion.
This includes both built-in relations (e.g. account_relations,
primary_contact) and custom lookup fields created in the CRM UI
(e.g. cf_lookup_leadoppty_contact).
When a record has no related records for an expanded field, the response still includes that
field with an empty list — the absence is explicit, not silently omitted.
The filter parameter for search_records is a nested boolean tree.
Leaf nodes map a field api_name to a comparator object.
| Operator | Description |
|---|---|
| AND | All conditions must be true |
| OR | At least one condition must be true |
| NOT | Negates a single condition |
| Comparator | Value type | Description |
|---|---|---|
| String operators — value is a list of strings (multiple values are evaluated as OR) | ||
| eq | ["val", ...] | Exact match, case-sensitive |
| ieq | ["val", ...] | Exact match, case-insensitive |
| contains | ["val", ...] | Substring match, case-sensitive |
| icontains | ["val", ...] | Substring match, case-insensitive |
| starts | ["val", ...] | Prefix match, case-sensitive |
| istarts | ["val", ...] | Prefix match, case-insensitive |
| ends | ["val", ...] | Suffix match, case-sensitive |
| iends | ["val", ...] | Suffix match, case-insensitive |
| Numeric / date operators — value is a single string | ||
| lt | "value" | Less than |
| lte | "value" | Less than or equal |
| gt | "value" | Greater than |
| gte | "value" | Greater than or equal |
Existence operators — value is a boolean (true or false, not a list) | ||
| null | true / false | true — field value is NULL; false — field value is not NULL |
| empty | true / false | true — field value is an empty string; false — field value is not empty |
{
"AND": [
{"name": {"contains": ["Acme"]}},
{"value": {"gte": "10000"}},
{"OR": [
{"status": {"eq": ["active"]}},
{"close_date": {"null": true}}
]}
]
}
Only fields with filterable: true (returned by meta_describe_entity)
can be used in filter expressions. Filterable types: string, int, date, datetime, uuid, enum.
expandable_fields (from meta_describe_entity) hold
related records, not scalar values, and cannot be used as filter keys — neither the field itself
(e.g. account_relations) nor a dot-path into it
(e.g. account_relations.account_id).
The API silently ignores such conditions; an error is returned if they are detected.
Use scalar fields of the main entity instead.
MCP tool calls carry JSON, which makes them a poor fit for large binaries (base64 inflates the payload by a third and the whole file would have to pass through the LLM context). Uploads are therefore split into two phases: the MCP tool only registers the upload, and the file itself is sent by the client directly to a dedicated HTTP endpoint.
| Phase | Who | What happens |
|---|---|---|
| 1 — register | AI client via MCP | Calls upload_cloud_object_document with filename, mime_type,
folder_id, is_public. The server stores this metadata together with the
caller's bearer token (Fernet-encrypted) in Redis under a random single-use token with a
300-second TTL, and returns upload_url + upload_token
together with the current size limit (max_file_bytes, currently
52,428,800 bytes = 50 MiB on this server). |
| 2 — upload | Client, plain HTTP | POST {upload_url} as multipart/form-data with the binary in field
file. No Authorization header is needed or read — the token in the path
is the credential. The server atomically consumes the token (Redis GETDEL), re-creates a
CRM session from the stored bearer token and space, and calls the createCloudObject
GraphQL mutation. The created CloudObject is returned as JSON. |
Client-side network allow-list. Phase 2 is an outbound HTTP request made from the
AI client's own environment (its code-execution sandbox or connector runtime), not through MCP.
Claude and ChatGPT restrict which hosts that environment may reach, so the domain
*.coevera.com must be allowed in the client application's security / network
settings before uploads work — see the app's preferences. If phase 1 succeeds but phase 2 never
reaches the server (no POST /upload/… in the logs), this is the first thing to check.
// Phase 1 — MCP tool call
upload_cloud_object_document({
"document": {
"filename": "contract.pdf",
"mime_type": "application/pdf",
"folder_id": "0195d175-7d96-e9c2-40fb-d0c793886f92",
"is_public": false
}
})
// → {"cloud_object_id": "…", "upload_token": "…", "upload_url": "https://mcp-server.coevera.com/upload/…",
// "expires_in_seconds": 300, "max_file_bytes": 52428800, "max_file_mib": 50}
// Phase 2 — plain HTTP, from the client's environment
curl -X POST "https://mcp-server.coevera.com/upload/<upload_token>" -F file=@contract.pdf
// → {"createCloudObject": {"cloudObject": {"id": "…", "filename": "contract.pdf", "size": 48213, "url": "…", "publicUrl": null, …}}}
| Status | Meaning |
|---|---|
| 200 | File stored; body is the createCloudObject payload. |
| 400 | The multipart body could not be read (client aborted, malformed request). Token is consumed. |
| 410 | Token expired or already used. Call the tool again to get a fresh one. |
| 413 | File larger than upload_max_bytes (50 MiB on this server). Token is consumed — compare the file against max_file_bytes from phase 1 before POSTing. |
| 500 | CRM rejected the upload (e.g. invalid folder_id, no write permission on documents). Body contains the CRM error. |
| 503 | Either the upload store is not configured on this server (see below), or all upload slots are busy — then a Retry-After header is set and the token is not consumed, so the same upload_url can simply be retried. |
secrets.token_hex randomness — unguessable — and is deleted on first use, so a leaked URL cannot be replayed.upload_token_encryption_key); a Redis dump does not yield live CRM credentials./upload/* is exempt from BearerAuthMiddleware by design — the one-time token replaces the bearer header for that single request.The tool is only registered as usable when a Redis upload store is configured. All keys live in config/defaults.py (override via environment / config/local.yml):
| Key | Purpose |
|---|---|
upload_redis_url | Redis URL for the token store. Unset = feature disabled (tool returns an error, endpoint returns 503). |
upload_redis_username / upload_redis_password | Optional Redis ACL credentials. |
upload_token_encryption_key | Fernet key used to encrypt the stored bearer token. Required when upload_redis_url is set — startup fails otherwise. Generate with python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())". |
upload_token_ttl_seconds | How long the client has to complete phase 2. Default 300. |
mcp_server_host | Public base URL of this server, used to build upload_url. Must be reachable from the client that performs phase 2. |
upload_max_bytes | Hard limit on a single uploaded file; larger bodies get HTTP 413. Returned to the client as max_file_bytes. Currently 52,428,800 (50 MiB). |
upload_read_chunk_bytes | Chunk size used while reading the uploaded body. Currently 1,048,576. |
upload_max_concurrent | Uploads processed at the same time per uvicorn worker. Currently 2. |
upload_queue_timeout_seconds | How long a request waits for a free upload slot before getting HTTP 503 + Retry-After. Currently 60. |
meta_describe_entity tool (or call meta_list_entity_types first
to discover custom entities prefixed with CE_).
The server uses a two-stage rate limiter: a short-window burst limit
per token and a longer-window sustained limit per space (tenant).
When either limit is hit, the server returns 429 Too Many Requests.
Retry-After response header for the recommended back-off duration.503.get_records rather than making one call per record.meta_list_entity_types — if that fails, the problem is
auth or connectivity, not filter syntax.
meta_describe_entity to confirm exact api_name values
before passing fields to search_records or get_records.
filterable: true (returned by meta_describe_entity)
can appear in filter expressions.
/lbready endpoint is public and returns 200 OK when the server
is running — use it to confirm reachability before debugging authentication.
/.well-known/oauth-authorization-server and
/.well-known/openid-configuration are also public and can confirm the correct
token / introspection endpoints.
delete_records — deletes records by ID. This is a soft delete: records are flagged as deleted in the CRM, not physically removed. Results are returned per record in input order, and a failing ID does not block the rest. At most 100 IDs per call; IDs must be UUIDs.upload_max_bytes, default 50 MiB), chunked reads, and a per-worker concurrency gate (upload_max_concurrent, upload_queue_timeout_seconds). upload_cloud_object_document now returns max_file_bytes / max_file_mib so clients can check the size before uploading.issuer (RFC 8414), and advertises PKCE code_challenge_methods_supported: ["S256"], which clients such as ChatGPT require to register a connector.WWW-Authenticate challenge with resource_metadata (RFC 9728), so clients can start OAuth discovery from a bare 401.upload_cloud_object_document — two-phase file upload into a CRM CloudObjectFolder. The tool registers the upload and returns a single-use, time-limited upload_url; the client then POSTs the file as multipart/form-data to the new /upload/{token} endpoint, which creates the CloudObject under the original caller's identity. See the File Upload reference.upload_redis_url, upload_token_encryption_key, upload_token_ttl_seconds, mcp_server_host. The feature is disabled unless Redis is configured.change_ownership — unified ownership reassignment tool covering Task, Opportunity, Contact, Account, Appointment, Lead, and Memo in a single call. Dispatches to a dedicated changeOwnership* GraphQL mutation for Task and Opportunity, and to a plain REST owner update for the remaining entity types. Lead and Memo transfers include a warning in the response: the service account loses write/ownership access on those records after reassignment and cannot self-revert.bulk_move_opportunities, bulk_archive_opportunities, bulk_archive_leads, and bulk_qualify_leads — execute business operations that trigger server-side logic (timeframe tracking, approval removal, AI fitness snapshots, quote transitions, recurrence spawning). Each tool accepts an optional rollback_method (ROLLBACK_ALL or ROLLBACK_FAILED). Batches of 20 or more are processed asynchronously; the response includes an asyncOperationId. All operations are also instrumented with integration metrics.get_step_checklist_status and complete_step_checklist_item — expose the per-step entry requirements configured by pipeline admins. get_step_checklist_status lists all checklist items for a step and, when an opportunity_id is provided, annotates each item with is_complete. complete_step_checklist_item records that a requirement has been satisfied by creating a LeadOpptyStepCheckListRelation record; the correct evidence ID parameter depends on the item type (activity, note, document, email, or a simple item_checked: true for Default items). FieldType items do not use this tool — set the field value directly via update_records instead.meta_describe_entity. CRM fields with primitive_type: list that carry a lookup.entity_type are surfaced in the expandable_fields array alongside built-in relation fields (e.g. account_relations). These fields — typically named cf_lookup_<entity>_<target> — can be passed to the expand parameter of search_records and get_records to load related records inline.CoeveraEntity now carries an OwnershipConfig that encodes per-entity dispatch method, required fields, and any access-loss warning — making ownership routing data-driven rather than hard-coded.readOnlyHint, destructiveHint, idempotentHint, openWorldHint) are now explicitly set to true or false on every tool — no implicit nulls.GET /.well-known/openai-apps-challenge) — enables ChatGPT plugin verification.token_cache_timeout) to control how long introspection results are reused.503 response instead of an unhandled exception, keeping the server operational when Redis is degraded.notifications/progress events to the client, giving users live feedback during long-running CRM queries.units_and_roles.read to the required OAuth scope set.get_records no longer applies a sample cap — all matched records are returned regardless of count.create_records, update_records).