Tools — Records
Core MCP tools for searching, creating, and updating CRM records, moving deals through pipelines, and managing duplicates.
The Records tools let your AI assistant read and write any CRM object — contacts, companies, opportunities, or any custom object you have defined — plus move deals through their pipeline and detect and merge duplicates.
Records can be deleted over MCP via crm_delete_records (soft-delete, max 50 per call, RBAC-guarded). Deletion is irreversible, so the assistant should re-read the records, show them to you, and delete only after explicit confirmation. To consolidate records instead of deleting, use crm_manage_duplicates.
crm_find_records
Search, list, or get CRM records on any object — full-text search, field filtering, sorting, grouping, and cursor pagination.
This tool has an attached MCP App: flat list results render as an interactive table / cards / kanban view in clients that support MCP Apps.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
object_name | string | Yes | Object name: contacts, companies, opportunities, or any custom object. Use kasar://schema to discover available objects and fields. |
record_id | string | No | Get a single record by UUID. Returns the augmented record (see below). |
search | string | No | Full-text search across all text fields (min 2 chars). Not compatible with group_by. |
filter_field | string | No | Field name for a single simple condition. For anything richer use filters. |
filter_operator | enum | No | One of equals, not_equals, contains, starts_with, ends_with, greater_than, less_than, greater_equal, less_equal, is_null, is_not_null, in. |
filter_value | string | No | Value to filter against. For in: comma-separated values. |
filters | string | No | JSON-encoded FilterGroup for nested AND/OR logic. Takes precedence over filter_field/filter_operator/filter_value. |
sort_by | string | No | Single sort field (default: created_at). For multi-column sort use sort. |
sort_dir | asc | desc | No | Sort direction (default desc). |
sort | string | No | Multi-column sort as a JSON array: [{"field":"amount_amount","direction":"desc"},{"field":"name","direction":"asc"}]. Takes precedence over sort_by/sort_dir. |
group_by | string | No | Group results into buckets by a SELECT/relation field, or a PIPELINE step column. Returns {groups, counts} instead of {data, total} — use it for kanban-style views. Not compatible with search. |
limit_per_group | number | No | Max rows per group when group_by is set (default 20, max 100). |
cursor | string | No | Pagination cursor from a previous response. Ignored in grouped mode. |
limit | number | No | Results per page (default 20, max 100). |
The augmented record
Fetching by record_id returns the augmented record: the enriched record plus its list memberships.
- Display values for foreign keys (e.g.
entreprise_id__display→ the company name). - Expanded compound fields (EMAILS, PHONES, ADDRESS, CURRENCY, …).
- Many-to-many relations auto-enriched as
related_to[](and any other M2M field on the object). - A
listsarray of list memberships. Each entry carries the values of the list's per-entry fields (custom fields defined on the list itself, such as a review status or score):
{
"record": { "id": "550e8400-…", "full_name": "Alice Martin" },
"lists": [
{
"list_id": "a529e4c5-…",
"list_name": "Salon 2026",
"entry_id": "f3dcfe67-…",
"entry_fields": { "added_at": "2026-06-11T16:57:09Z", "statut_revue": "valide" }
}
]
}lists is omitted when the record belongs to no list.
Examples
Get a contact by ID:
{
"object_name": "contacts",
"record_id": "550e8400-e29b-41d4-a716-446655440000"
}Search contacts by name:
{
"object_name": "contacts",
"search": "Alice Martin",
"limit": 10
}Complex filter with sorting:
Find contacts with status "Lead" or "customer", sorted by name:
{
"object_name": "contacts",
"filters": "{\"type\":\"OR\",\"conditions\":[{\"field\":\"contact_status\",\"operator\":\"equals\",\"value\":\"Lead\"},{\"field\":\"contact_status\",\"operator\":\"equals\",\"value\":\"customer\"}]}",
"sort": "[{\"field\":\"full_name\",\"direction\":\"asc\"}]"
}Group opportunities by pipeline stage (kanban):
{
"object_name": "opportunities",
"group_by": "pipeline_step_id",
"limit_per_group": 25
}FilterGroup format
The filters parameter accepts a JSON-encoded FilterGroup that supports nested AND/OR logic.
{
"type": "AND",
"conditions": [
{ "field": "full_name", "operator": "contains", "value": "marc" },
{
"type": "OR",
"conditions": [
{ "field": "contact_status", "operator": "equals", "value": "Lead" },
{ "field": "contact_status", "operator": "equals", "value": "customer" }
]
}
]
}This matches records where the name contains "marc" and the status is either "Lead" or "customer". Groups can be nested to any depth. In filters, operators also include not_in and between in addition to the simple ones above.
Response
A flat list returns { data, total, nextCursor }:
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"full_name": "Alice Martin",
"email": "alice@example.com",
"contact_status": "customer",
"created_at": "2025-01-15T10:30:00Z"
}
],
"total": 42,
"nextCursor": "eyJpZCI6IjU1MGU4NDAw..."
}When nextCursor is absent or null, you have reached the last page. Grouped mode returns { object_name, group_by, limit_per_group, groups, counts } instead.
To narrow the returned columns or export a slice as CSV/JSON, use crm_export_records instead — crm_find_records always returns the full enriched record.
crm_save_records
The upsert tool — create or update one or many records in a single call, including their relations and list memberships.
A record is "augmented" by three layers, and this one tool edits all three: its own fields (data), its many-to-many links and junction fields (relations), and its list memberships and per-entry fields (the list params).
Replaces the old crm_save_record (singular). Pass a single object for one record, or an array to upsert many. To set the same value across many records, or to bulk add/remove an M2M link, use crm_bulk_update_records instead.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
object_name | string | Yes | Target object. Check kasar://schema/{object_name} for fields and relation write shapes. |
data | object | object[] | Yes | A single record object, or an array of record objects for batch upsert (max 200 per call — a larger array is rejected with TOO_MANY_RECORDS; split into smaller batches). In array mode, put id (or record_id) in an element to update it; omit it to create. |
record_id | string | No | Single mode only: UUID to update. Omit to create. Ignored when data is an array (use each element's id). |
relations | object | No | Incremental M2M edits keyed by field name: { fieldName: { add, remove, update } }. See below. |
add_to_list_ids | string[] | No | List UUIDs — adds the record(s) to these lists in the same call. Per-list outcome returned in lists. |
create_list_name | string | No | Creates a new list with this name (allowing this object) and adds the record(s) to it (admin only). |
remove_from_list_ids | string[] | No | List UUIDs — removes the record(s) from these lists. Outcome in removed_from_lists. |
update_list_entries | object[] | No | Edit the per-entry fields of existing list memberships: [{ list_id, fields: { …entryFields } }]. Mirror of the read-side lists[].entry_fields. Outcome in updated_list_entries. |
The relations delta object per field accepts:
| Key | Type | Description |
|---|---|---|
add | array | Links to create: [{ targetId, junctionData?, _objectType? }]. junctionData sets junction fields; _objectType is required for multi-target relations. |
remove | string[] | Target UUIDs to unlink. |
update | array | Patch the junction fields of an existing link in place: [{ targetId, junctionData, _objectType? }] — without replacing the whole set. |
Two ways to write many-to-many
- Replace the whole set — set the field directly in
dataas an array. This drops any link you omit:{ "data": { "companies": [{ "targetId": "uuid-company", "junctionData": { "position": "CEO" } }] } } - Edit incrementally — use
relations[field] = { add, remove, update }.updatepatches thejunctionDataof an existing link in place without touching the others — use this to edit junction fields:{ "relations": { "companies": { "update": [{ "targetId": "uuid-company", "junctionData": { "position": "CEO & Chairman" } }] } } }
Relation payloads use camelCase keys. Single-target M2M = [{ targetId, junctionData }]; multi-target M2M (e.g. tasks.related_to, notes.related_to) = [{ targetId, _objectType, junctionData }] where _objectType is required; BELONGS_TO_ONE = pass the FK UUID string directly in data; MORPH_RELATION = { id, _objectType }. Check each field's writeShape / junctionFields / multiTarget in kasar://schema/{object_name}.
Automatic behaviors on create
When creating a new record (no id/record_id), the server applies defaults automatically:
- Pipeline defaults — if the object has a pipeline, the default pipeline and its first step are assigned.
- USER fields —
owner_idandcreated_by_idare set to the authenticated user. - Field defaults — default values defined in field metadata are applied for any omitted fields.
- Tasks and notes route through their canonical create actions, so a created
tasksrecord fires the assignment notification + title-based entity auto-link, and a creatednotesrecord derives its title from the content.
Changing the pipeline step
Set the step column directly in data (e.g. pipeline_step_id, or the PIPELINE field name) to run the same canonical transition as crm_move_deal:
- Required-field validation. If the source step has
required_for_next_stagequestions (forward transition) orrequired_for_closurequestions (closure to a final step), the call is rejected withMISSING_REQUIRED_FIELDSwhen those linked fields are empty on the record. - Forced and default values configured on the target step are applied server-side.
- Witness rows are recorded in
pipeline_step_historyso the step is tagged as visited.
Read kasar://pipelines first to discover which fields are required per step, then fill them in the same crm_save_records call as the step change — one payload that sets the required field AND moves the step passes validation.
{
"object_name": "opportunities",
"record_id": "uuid-opp",
"data": {
"budget": 50000,
"decision_maker": "Alice Martin",
"pipeline_step_id": "uuid-proposal-step"
}
}Examples
Create a contact linked to a company:
{
"object_name": "contacts",
"data": {
"first_name": "Alice",
"last_name": "Martin",
"email": "alice@example.com",
"entreprise_id": "7a2b3c4d-e5f6-7890-abcd-ef1234567890",
"contact_status": "Lead"
}
}Create an opportunity (auto pipeline):
{
"object_name": "opportunities",
"data": {
"name": "Acme Corp renewal",
"amount": 50000,
"close_date": "2025-06-30"
}
}The response includes the auto-assigned pipeline_id and pipeline_step_id.
Batch upsert (create two, update one):
{
"object_name": "contacts",
"data": [
{ "first_name": "Paul", "last_name": "Durand" },
{ "first_name": "Sophie", "last_name": "Bernard" },
{ "id": "550e8400-e29b-41d4-a716-446655440000", "contact_status": "customer" }
]
}Create a contact and add it to lists in the same call:
{
"object_name": "contacts",
"data": { "first_name": "Paul", "last_name": "Durand" },
"add_to_list_ids": ["a529e4c5-…"],
"create_list_name": "Salon 2026"
}Response
Single mode returns { id, action, record }:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"action": "created",
"record": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"first_name": "Alice",
"last_name": "Martin",
"entreprise_id": "7a2b3c4d-e5f6-7890-abcd-ef1234567890",
"entreprise_id__display": "Acme Corp",
"contact_status": "Lead",
"owner_id": "user-uuid",
"pipeline_step_id": "first-step-uuid"
}
}action is "created" for new records and "updated" for updates. The record always contains the full enriched record with display values.
Array mode returns per-record results with counts:
{
"object_name": "contacts",
"total": 3,
"created": 2,
"updated": 1,
"failed": 0,
"results": [
{ "id": "d244…", "action": "created" },
{ "id": "fe69…", "action": "created" },
{ "id": "550e…", "action": "updated" }
]
}Augmentation outcomes (lists, created_list, removed_from_lists, relations, updated_list_entries) are appended to the response when those params are used. A failed list addition never rolls back the record write — the failure is reported per list.
Error handling
Validation error (field-level details):
{
"error": true,
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"field_errors": [
{ "field": "email", "message": "Must be a valid email address" }
]
}Duplicate record (unique constraint violation):
{
"error": true,
"code": "DUPLICATE_RECORD",
"message": "A contact with this email already exists"
}crm_bulk_update_records
Apply one change across many records at once: set a single field to the same value, or add/remove a many-to-many link in bulk. For different values per record, use crm_save_records with a data array instead.
Destructive — it overwrites in bulk. Confirm the target record set with the user first. Max 200 records per call.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
action | update_field | add_relation | remove_relation | Yes | The bulk operation. |
object_name | string | Yes | Target object. |
record_ids | string[] | Yes | UUIDs of the records to update (max 200). |
field_name | string | No | Field to set (update_field only). |
field_value | any | No | New value for every record (update_field only). |
relation_field | string | No | M2M relation field name (add_relation / remove_relation). |
target_ids | string[] | No | Target record UUIDs to link/unlink (add_relation / remove_relation). |
Example
Tag 3 contacts as customers:
{
"action": "update_field",
"object_name": "contacts",
"record_ids": ["550e…", "661f…", "772a…"],
"field_name": "contact_status",
"field_value": "customer"
}crm_delete_records
Delete one or more records — soft-delete via the canonical path (RBAC + dashboard-owner guard + cascade of the records' list entries). This is the only way to remove records; crm_save_records cannot delete.
Irreversible and easy to get wrong. The assistant should re-read the records, show them to you, and delete only after explicit confirmation. Max 50 per call.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
object_name | string | Yes | CRM object name. |
record_ids | string[] | Yes | UUIDs to delete (max 50). |
Example
{
"object_name": "contacts",
"record_ids": ["550e8400-e29b-41d4-a716-446655440000"]
}crm_move_deal
Move a deal to a different pipeline step.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
record_id | string | Yes | UUID of the deal to move. |
step_id | string | Yes | UUID of the target pipeline step (from kasar://pipelines). |
object_name | string | No | Object with a PIPELINE field. Default opportunities. |
pipeline_id | string | No | UUID of the target pipeline — only needed to move the deal into a step in a different pipeline than its current one. |
Behavior
Always read kasar://pipelines first — it lists every step with its questions, required_for_next_stage, required_for_closure, and forced_value flags.
- Before moving, set the required fields of the source step (and target, for closures) on the record via
crm_save_records. Otherwise the server returnsMISSING_REQUIRED_FIELDSand the move is rejected. - The server applies the forced/default values of the target step automatically and records a witness row in
pipeline_step_history. - The caller must have
editpermission on the record.
Example
{
"object_name": "opportunities",
"record_id": "uuid-opp",
"step_id": "uuid-proposal-step"
}Response
{
"deal_id": "uuid-opp",
"pipeline_step_id": "uuid-proposal-step",
"action": "moved"
}If required fields are missing, the response is a MISSING_REQUIRED_FIELDS error listing the missing fields in field_errors.
crm_manage_duplicates
Detect duplicate records on an object and optionally merge them.
Both actions are admin-only. Renamed from crm_find_duplicates.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
action | detect | merge | Yes | detect finds duplicate groups; merge consolidates a group into a master. |
object_name | string | No | Object to check: contacts or companies (default contacts). |
limit | number | No | Max duplicate groups to return (detect only, default 20). |
fields | string[] | No | Fields to exact-match on (detect only). Default: email, phone, linkedin, name. |
master_id | string | No | UUID of the record to keep (required for merge). |
duplicate_ids | string[] | No | UUIDs of records to merge into the master (required for merge). |
merge_strategy | keep_master | fill_blanks | No | keep_master: only delete the duplicates. fill_blanks: copy non-empty fields from duplicates into the master's empty fields before deleting (default fill_blanks). |
detect groups records that exactly match (case-insensitive, trimmed) on one of the compared fields and returns each group with the matched-field reason. merge runs the same engine as the app's Duplicates tab — relations, junctions and compound values are transferred to the master, then the duplicates are deleted.
Examples
Detect duplicate contacts by email and phone:
{
"action": "detect",
"object_name": "contacts",
"fields": ["email", "phone"],
"limit": 50
}Response:
{
"object_name": "contacts",
"duplicate_groups": [
{
"records": [
{ "id": "550e…", "full_name": "Alice Martin", "email": "alice@example.com" },
{ "id": "661f…", "full_name": "Alice M.", "email": "alice@example.com" }
],
"match_reason": "email: \"alice@example.com\""
}
],
"total_groups": 1,
"records_scanned": 1240
}Merge two contacts into a master:
{
"action": "merge",
"object_name": "contacts",
"master_id": "550e8400-e29b-41d4-a716-446655440000",
"duplicate_ids": ["661f9511-f30c-52e5-b827-557766551111"],
"merge_strategy": "fill_blanks"
}Response:
{
"master_id": "550e8400-e29b-41d4-a716-446655440000",
"duplicates_deleted": 1,
"updated_relations": 7,
"strategy": "fill_blanks"
}crm_manage_emails_phones
Manage a record's emails or phone numbers. These are child records in a separate table — not a scalar field — so they are edited here, never through crm_save_records.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
object_name | string | Yes | Parent object (e.g. contacts). |
record_id | string | Yes | Parent record UUID. |
field_type | emails | phones | Yes | Which compound field to manage. |
action | list | add | remove | set_primary | Yes | The operation. |
value | string | Cond. | The email address or phone number (add). The first entry added becomes primary automatically. |
child_id | string | Cond. | The entry id (from list) — required for remove and set_primary. |
company_id | string | No | Optional company to link the entry to (add). |
Example
"Add a second email to this contact"
{
"object_name": "contacts",
"record_id": "550e8400-e29b-41d4-a716-446655440000",
"field_type": "emails",
"action": "add",
"value": "alice.pro@example.com"
}Tips
crm_find_recordsandcrm_save_recordsalso work on thetasksandnotesobjects, since they support any CRM object.- For tasks-specific features (completion toggling, linked entities) use the dedicated
crm_manage_taskstool. For notes-specific features (visibility controls, @mention notifications) usecrm_manage_notes. - To narrow columns or export a slice, use
crm_export_records. To bulk-create from an analyzed dataset, usecrm_import_records.