Skip to content
indraft
Start free

Documentation

MCP

Indraft speaks Model Context Protocol over Streamable HTTP at /mcp. Authorization is delegated to WorkOS AuthKit, so connecting a client is an authorization flow rather than a pasted secret, and a workspace administrator can revoke a connection at any time. Revocation takes effect on that connection's very next request, not when its token expires.

The tool catalogue

46 tools. Nothing is removed or renamed without notice, and the set grows only when there is a new thing to do: adding one is a specification change and a test fails on any drift. Each expresses a customer operation rather than a table operation, because a generic row tool pushes every business rule out to the caller.

This page said "frozen" until 2026-08-25, and /mcp-crm retracted the word the same day for a reason worth repeating here: it promised something stronger than it sounds, which is that the set would never grow, and that was not a promise this product could keep or should have made. Three watch tools were added the week before it was retired.

get_crm_schema Describe the CRM: object types and whether they can be extended, pipelines and stages, custom fields, tags, your own capabilities, the workspace timezone and default currency, ID prefixes, every stated limit, every error code, and the derived values the database computes on read and nothing can write. Call this first; it removes the need to probe.
search_crm Search companies, contacts, opportunities, tasks, interaction summaries, and this workspace's own records by keyword. Matches whole words, prefixes, and substrings, so three characters from the middle of a name finds the record. Supports OR, a leading minus to exclude a term, a phrase in double quotes, and field qualifiers such as name:, domain: and email:. It finds records; it never decides that two records are the same one.
find_duplicates Propose pairs of records that look like the same record, ranked by how alike their names are and whether they share a relationship. Works on companies, contacts, and a type this workspace defined. It PROPOSES only: nothing here merges anything, and the merge tools still require you to name both ids. Use it on a type with a declared identity too: the key cannot collide, so what is left to find is one person entered twice under two different keys. Walks the workspace a page at a time; pass the returned cursor to continue.
list_companies List companies, newest first, with a cursor for the next page. Use this to walk the customer base; search_crm answers a keyword question and cannot enumerate. Narrow with filter, for example filter: ['owner_actor_id:eq:act_...', 'expected_close_date:isNotNull'] and sort: '-expected_close_date'; get_crm_schema lists the fields each type accepts.
list_contacts List contacts, newest first, with a cursor for the next page. Use this to walk the people you know; search_crm answers a keyword question and cannot enumerate. Narrow with filter, for example filter: ['owner_actor_id:eq:act_...', 'expected_close_date:isNotNull'] and sort: '-expected_close_date'; get_crm_schema lists the fields each type accepts.
list_tasks List tasks by status or assignee, newest first, with a cursor for the next page. Narrow with filter, for example filter: ['owner_actor_id:eq:act_...', 'expected_close_date:isNotNull'] and sort: '-expected_close_date'; get_crm_schema lists the fields each type accepts.
list_opportunities List opportunities by stage or status, newest first, with a cursor for the next page. Narrow with filter, for example filter: ['owner_actor_id:eq:act_...', 'expected_close_date:isNotNull'] and sort: '-expected_close_date'; get_crm_schema lists the fields each type accepts.
get_company_context One bounded view of a company: the record, its contacts, open opportunities, open tasks, and recent interactions. Each collection is capped and the response says whether the cap was reached.
get_contact_context One bounded view of a contact: the record, its companies, open opportunities, open tasks, and recent interactions.
upsert_company Create a company, or match and update an existing one. Matching uses the normalized domain, never name similarity. If the identity is ambiguous the call returns candidates and writes NOTHING; choose one and pass its id, or set createDistinct to create a separate record.
upsert_contact Create a contact, or match and update an existing one. Matching uses the normalized email, never name similarity. An ambiguous identity returns candidates and writes nothing.
archive_record Archive a company or contact, or restore one with restore=true. For a record of a type this workspace defined for itself, use archive_object_record instead. An archived record keeps its history, stays readable, and stops counting toward the workspace's record limit, so this is how a workspace gets back under a limit without deleting anything.
merge_companies Merge a duplicate company into a target. References move to the target and the source is archived. Not undoable by revert_change; use unmerge.
merge_contacts Merge a duplicate contact into a target. References move to the target and the source is archived. Not undoable by revert_change; use unmerge.
unmerge Invert a merge, restoring exactly the references it moved. Refused if the merge has already been undone.
create_opportunity Create an opportunity, always as a new one. If another system already knows this deal and you are syncing it, use upsert_opportunity, which matches on that system's identity instead of creating a second copy. Omit stageId to use the first open stage of the default pipeline. Amount is an integer in minor units and requires a currency. Set ownerActorId and expectedCloseDate here rather than in a second call: both are filterable and sortable, and recording one fact as two assertions makes the history harder to read than the API.
update_opportunity Change an opportunity's name, amount, probability, expected close date, or owner. Pass expectedVersion to refuse the write if someone else changed it first.
upsert_opportunity Create a deal, or find the one another system already knows about and update it. Match is on the identity that system issued (externalProvider plus externalId) or on an explicit id, and never on the name: two deals with the same name are routinely two deals. Use create_opportunity when you mean to create one regardless.
advance_opportunity Move an opportunity to a stage. Status (open, won, lost) is derived from the stage, so this is how a deal is won or lost.
record_interaction Record what happened: an email, call, meeting, message, or note, as a SUMMARY you write. Indraft never stores a raw message body, transcript, or attachment. Link it to the companies, contacts, and opportunities it concerns, and put the structured conclusion in customFields: disposition, meeting type and next step belong in fields you can group by rather than in prose.
create_task Create a follow-up, optionally due at a time and linked to CRM records.
link_contact_company Attach a contact to a company, detach them, or change which company is their primary one. A person may be at several companies or none, with at most one primary. Set unlink to true to detach: an affiliation added by mistake was otherwise permanent, and somebody who changed jobs kept the old employer forever.
update_task Correct a task that already exists: its title, when it is due, who owns it, or how urgent it is. Only the fields you send move, and an explicit null clears a due date or unassigns it. Use this when you got a detail wrong; use complete_task when the work is done and cancel_task when it is not going to be. A task that is already done or cancelled is refused rather than edited, because it records what happened.
cancel_task Call a task off. Different from completing it: cancelled means the work was not done and is not going to be, and the ledger and every report built on it record which. Use this for a task you created in error or that no longer applies.
complete_task Mark a task done. Completing a completed task is a no-op, not a second completion.
archive_custom_field Retire a custom field definition. Values already written stay readable and exportable, because a field somebody stopped using is history rather than a mistake. The field stops appearing in the schema, stops accepting new values, and frees its quota slot.
configure_pipeline Create or reconfigure a pipeline and its ordered stages. Every pipeline needs at least one open stage and both a won and a lost stage, or a record in it could never close. Set objectType to one of this workspace's own types to give that type a board of its own: a type with two relationships and a pipeline is a junction, so a Submission can carry the stage of a candidate against a requisition rather than either one carrying it alone.
configure_custom_field Define or update a typed custom field on a company, contact, opportunity, interaction, or task. A field is addressed by its object type and key. The value type cannot change once values exist. Use interaction for call disposition, meeting type and next step: those are what activity reporting is made of, and a summary is prose rather than a field you can group by. Pass `computed` to make the field DERIVED instead of stored: it is then recalculated from stored fields on every read, nothing can write it, and it can never go stale behind its inputs. Two forms only. `{form:'binary', op:'multiply', left:{field:'salary_minor'}, right:{field:'placement_pct_bps'}, divideBy:10000}` computes a fee from two fields and a constant. `{form:'rollup', relationship:'opportunities', function:'sum', field:'amount_minor'}` totals over a declared relationship. A computed field must be a number, cannot be required or searchable, and cannot read another computed field. get_crm_schema lists the relationships and the inputs each type offers.
list_recent_changes Inspect what changed and who changed it: the mutation, the actor, the access path, and the field-level diff. Use this to check your own work.
aggregate_crm Count, sum, average, min or max over one object type, grouped by up to TWO dimensions. Answers 'how is the quarter going', 'pipeline by owner by quarter', 'pipeline by customer segment' and 'how many have no owner' in ONE call, with the database doing the arithmetic. Use this instead of paging records and adding them up yourself: it is one call rather than hundreds, and the number is computed rather than estimated. Filters use the same field:op:value language as the list tools. groupBy takes one dimension or an array of two, and a dimension written as hop.field crosses one declared relationship, so opportunity can group by company.industry. Dates must be bucketed to day, week, month, quarter or year, and quarter and year follow the workspace's own fiscal calendar, which the answer states. Too many groups is refused rather than truncated, and the cap applies to the PRODUCT of two dimensions, because a truncated total reads exactly like a real one. convertTo totals across currencies at a rate this workspace STORED and names the rate it used; a currency with no stored rate is refused rather than converted, because Indraft never invents one. For weighted pipeline, reduce the DERIVED field weighted_amount_minor: 'sum weighted_amount_minor where status is open' is your weighted pipeline in one call, computed by the database as amount times probability, and it obeys the currency rule exactly as the unweighted amount does. Derived fields can be reduced and not grouped by, because they are measures rather than dimensions. get_crm_schema lists the dimensions, the hops, the derived fields, the fiscal calendar and the stored rates.
set_exchange_rate State an exchange rate this workspace stands behind, with the day it takes effect and where it came from. Indraft never fetches, infers, or interpolates a rate, so this is the ONLY way a total can cross currencies: without a stored rate an aggregate refuses rather than converting. The rate is recorded as an assertion like any other, with the actor who made it, and every converted total names it. Give the rate in millionths as an integer, so 1.0842 is 1084200: a decimal rate would be a float, and a float is how a rounding error reaches a pipeline total. Asserting the same pair for the same day again corrects the standing rate rather than adding a second one.
list_unconfirmed Values an agent asserted that no person has checked. Use it before you present a fact as settled, and to tell a person what is worth reviewing: every entry names the value, who asserted it, when, and how they came by it. You cannot confirm one. A confirmation means a person checked it, so only a person can make one, and that is the whole point of the queue. Narrow with objectType, or with assertionKinds to look at the weakest claims first.
list_stale_records Records nobody has touched for a while, quietest first, with how many days each has been quiet. Use this to find what has stopped being maintained: the change feed tells you what DID happen, and this tells you what did not. Returns a list and dates, never a score.
configure_object_type Define a NEW object type this workspace needs and Indraft does not have: a Candidate, a Campaign, a Unit. To change a type that already exists, use alter_object_type. Typed fields and named relationships to existing records. The canonical types cannot be redefined this way, and a type you define carries the same history, attribution, and undo as everything else.
alter_object_type Change a type this workspace has ALREADY defined; use configure_object_type to create one. Add a field, declare which field is its identity so duplicates are refused rather than created, or archive and restore it. Declaring an identity over records that already share a value is refused and names them, so nothing is merged by surprise.
list_object_types The custom object types this workspace has defined, with their fields and relationships. Canonical types (company, contact, opportunity, interaction, task) are not here: they are in get_crm_schema and mean the same thing in every workspace.
list_object_records Records of one custom object type, newest first, with a cursor for the next page. The type must be one this workspace defined; ask get_crm_schema or list_object_types for the names. Filters and sorts exactly like the built-in lists, on the fields the type declared indexed.
upsert_object_record Create or update one record of a custom object type. Supply only fields the type declares; an undeclared field is refused rather than ignored, so nothing you send goes missing silently.
archive_object_record Archive one record of a type this workspace defined for itself, or restore it. For a company or a contact use archive_record instead. Archiving keeps the record readable and exportable and takes it out of lists; it is not a delete.
advance_object_record Move one record of a custom type to a stage of that type's pipeline. Status (open, won, lost) is derived from the stage, exactly as it is for an opportunity, so this is how a record of your own type is won or lost. The type needs a pipeline first: give it one with configure_pipeline naming the type. Filter list_object_records by _stage_id or _status to read a board.
merge_object_records Merge a duplicate record of a custom type into a target. References move to the target and the source is archived. You must name both ids: nothing here works out which records are the same, and a custom type has no natural key. Not undoable by revert_change; use unmerge.
revert_change Compensate a previous field change. Refused if the mutation created a record, if it was a merge, or if any field it covered has changed since. Ask list_recent_changes for the mutation id.
bulk_upsert Apply up to 100 operations in one call, so a first population session is not one call per record. Each item is independently idempotent; one failure does not discard the rest, and the result reports each item's outcome.
list_watches Every question this workspace asks itself on a clock, with what each last counted and when it last had something to say. A watch reports only when its count is above its threshold AND has changed, so one that has not fired recently is usually one whose answer has not moved.
create_watch Save a question to be asked on a clock, and told to whoever subscribes when the answer moves. Use this for anything the operator says to check nightly, weekly or monthly: it is how something gets done when nobody is in the conversation. The filter is the same grammar every list tool takes, so anything you can ask for you can watch. Best for what did NOT happen, which no event can report: a deposit that never advanced a stage, a service date that passed, an account nobody touched.
stop_watch Stop asking a saved question. The watch keeps its history and simply goes quiet; nothing it already reported is removed.

Read the schema first

get_crm_schema describes the canonical object types and the ones your workspace defined for itself, with every field and relationship on each, the pipelines and their stages, the custom fields, your own capabilities, the workspace timezone and currency, the identifier prefixes for both tiers, every limit, and every error code. An agent that has read it needs nothing else. An agent that discovers a limit by being refused has already wasted a call.

It also says what each object type can be filtered and sorted on, and which actor id the caller is. Both matter for the same reason: a list narrows with filter and sort, in the same field:op:value language the HTTP API uses, and only fields an index can serve are accepted. Anything else is refused by name rather than answered slowly, because a workspace is one database and a scan would hold up every other read and write in it. So "which accounts are mine and going quiet" is one call, and an agent that read the schema never has to guess at a field name to find out.

When the question is how many or how much, aggregate_crm answers it in one call with the database doing the arithmetic. That matters more here than on the HTTP API: the alternative is your agent reading thousands of records into its context and adding them up, which costs tokens, takes as long as the workspace is large, and produces a number nobody can check. Too many groups is refused rather than shortened, because a truncated total reads exactly like a real one.

Stated limits

Bulk batch size100 operations
Page size100
Context expansion 200 per collection
Interaction summary 4000 characters
Idempotency keys Honored for 30 days

Nothing is unbounded. Every list, search, context payload, and tool result has a stated bound and a typed refusal above it, and nothing is ever truncated silently: an agent handed a shortened list reasons from it as though it were complete.