MCP tools

Endpoint: https://undocrm.com/mcp (Streamable HTTP, stateless). Auth: Authorization: Bearer ucrm_agt_.... Owner keys are refused; MCP is for agents.

Five tools: read, create, update, delete, and can_contact. The first four are plain CRUD over HubSpot-style objects (contact, company, deal, task, interaction, pipeline). can_contact is separate because it can't be CRUD: it checks and claims a person in one locked step.

Results are short plain text written for a model, always carrying the ids the next call needs. Errors come back as tool errors that say what to do instead.

person / target strings:

You have Pass
Contact id ct_3f9a0c2b7e1d4a58
Email jane@acme.com
Phone +14155550100 (any formatting; E.164 matches most reliably)
Handle or profile URL linkedin:janedoe, x:@jane, https://linkedin.com/in/janedoe
Other records co_… company, dl_… deal, fu_… task, ix_… interaction
Collections (read only) contacts, companies, deals, tasks, interactions, pipelines, properties

Property names follow HubSpot: firstname, lastname, email, phone, jobtitle, company, lifecyclestage; dealname, dealstage, amount, closedate, pipeline. Anything else you set on a contact is a custom property: stored append-only with history, and registered so other agents reuse the same name.


read

Param
target string A person, an id, or a collection. Omit with query to search contacts.
view brief | logs | facts | deals | tasks For a person. Default brief.
query string Search text for contacts, companies, or interactions (full-text over every conversation).
purpose string For briefs: why you're looking. Ranks what goes in.
max_tokens 80–8000 For briefs. Default 500. Rules are always included.
filter object For collections: tasks {due: overdue|today|week|all, mine}, deals {pipeline, dealstage, open}, contacts {lifecyclestage}.
limit 1–100 Default 20.

A person's brief — the read every agent should do before acting:

# Jane Doe — VP Customer Support, Acme (acme.com)
ct_b2b23e68a58e3aa2 · jane.doe@acme.com · +14155550142 · linkedin:janedoe-acme · lifecyclestage: opportunity

## Rules
- Claimed by sdr-1 until 2026-10-19 02:09Z (send SOC2 + order form). Don't reach out; they are handling this person.
- Last outbound: meeting 1d ago by sdr-1.

## Open tasks
- fu_2c26f224f1c50252 due 2026-10-18: Send SOC2 report and pilot order form [email] (sdr-1)

## Deals
- dl_bbcd208139c1ddc5 Acme — support pilot: Contract Sent · 80,000 · close 2026-10-31

## Facts
- budget: $80k/yr (was: $50k/yr) — sdr-1, 2026-10-17
- decision_maker: CFO (Mark Ellis) — sdr-1, 2026-10-17

## Recent interactions (4 of 4; → outbound, ← inbound)
- 2026-10-15 meeting → sdr-1: 30-min call. Budget approved at $80k/yr after the Zendesk quote came in higher.

Searching every conversation:

read(target: "interactions", query: "SOC2")

2 conversations mention "SOC2":
ix_9f2…  2026-10-13 phone ← Jane Doe (ct_b2b…): …wanted to know if «SOC2» is done.
ix_41c…  2026-10-15 meeting → Jane Doe (ct_b2b…): …needs «SOC2» Type II before signing.

Your tasks: read(target: "tasks", filter: {due: "today"}). The deal board: read(target: "deals", filter: {dealstage: "contractsent"}). Which property names exist: read(target: "properties").

create

Param
type interaction | contact | company | deal | task | pipeline required
person string Who it's about (interaction, deal, task). Unknown people are created.
properties object The record's fields (below).
facts object interaction only: what you learned, as {budget: "$50k", timeline: "Q1"}. Never overwrites.
tasks [{due, subject, channel?}] interaction only: what you owe. due: ISO date, datetime, or "3d", "2w", "4h".
completes_tasks string[] interaction only: task ids this interaction fulfills.

interaction — the main write, after every touchpoint. One call records the conversation, what you learned and what you owe.

{
  "type": "interaction",
  "person": "jane@acme.com",
  "properties": {
    "channel": "email", "direction": "inbound",
    "summary": "Replied: budget approved at $80k, needs our SOC2 report before signing.",
    "external_id": "<CAF=abc123@mail.gmail.com>"
  },
  "facts": { "budget": "$80k/yr", "blocker": "Needs SOC2 report" },
  "tasks": [{ "due": "2026-10-20", "subject": "Send the SOC2 report", "channel": "email" }],
  "completes_tasks": ["fu_1a2b3c4d5e6f7a8b"]
}
Logged ix_e543dde288ec2cfd with Jane Doe (ct_8c77757b98080b2f).
Facts: budget (was: $50k/yr), blocker.
Tasks: fu_9d0e1f2a3b4c5d6e due 2026-10-20 "Send the SOC2 report".
Completed: fu_1a2b3c4d5e6f7a8b.

properties for an interaction: channel, direction (inbound, outbound, internal), summary (1–3 sentences for a future agent), and optionally occurred_at, content (raw text, never put in briefs), external_id (makes retries safe), thread_id, deal_id, name (if the person is new).

If a new fact name looks like one that already exists, the result says so — "annual_budget" is new; "budget" already exists — so agents converge on one vocabulary.

contact — {email, phone, linkedin, firstname, lastname, jobtitle, company, lifecyclestage, …any custom property}. Matches existing people by identity; if the identities belong to different records they're merged into one. If a company was inferred from an email domain, the result says so.

deal — person + {dealname, dealstage, amount, closedate, pipeline?, currency?}. Without pipeline the deal goes in the first one, HubSpot's default Sales Pipeline: appointmentscheduled → qualifiedtobuy → presentationscheduled → decisionmakerboughtin → contractsent → closedwon / closedlost. Stage keys or labels both work ("Contract sent").

task — person + {subject, due, channel?, deal_id?}.

company — {name, domain, …any custom property}.

pipeline — {label, stages: ["Applied", "Screen", "Onsite", "Offer", "Hired", "Rejected"]}. For recruiting, support queues, anything with stages.

update

Param
target string A person or an id.
properties object Only what changes.
update("dl_bbcd208139c1ddc5", {dealstage: "closedwon", amount: 85000})
→ Updated dl_bbcd208139c1ddc5 "Acme — support pilot": stage Contract Sent → Closed Won, amount 85000.

Opt-outs, the moment they happen:

update("marcus@globex.io", {do_not_contact: "email", do_not_contact_reason: "Replied: please stop emailing me"})
→ Now do-not-contact on email; every agent's can_contact will say BLOCKED.

delete

Nothing is destroyed: records are archived or retracted, and a human can restore them from the audit log.

Param
target string Person, dl_, fu_, or ix_.
property string For a person: retract only this property's current value.
reason string Required to archive a person.
delete("jane@acme.com", property: "budget", reason: "typo")
→ Retracted budget = "$500k" for Jane Doe. Current value is now the previous one: "$50k". History is kept.

can_contact

The gate before every outbound message.

Param
person string required
channel string required unless releasing
purpose string Shown to agents your claim blocks.
claim bool Default true. false only checks.
release bool true drops your claim.
ttl_minutes int Claim length. Default 1440 (24h), max 43200.
ALLOWED — Jane Doe (ct_8c77757b98080b2f) on email. Claimed for you until 2026-10-18T09:00:00Z.
Last outbound: email 2d ago by sdr-1.
After sending, log it with create(type: "interaction").
BLOCKED — Jane Doe (ct_8c77757b98080b2f) is claimed by sdr-1 until 2026-10-18T09:00:00Z (purpose: send SOC2 report). Another agent is handling them; don't message.
BLOCKED — Marcus Chen (ct_bb1e9ab17925a336) is do-not-contact on email: "Replied: please stop emailing me" (sdr-1, 2026-10-17). Do not message them on email.

Errors

Error: "@jane" is ambiguous. Prefix handles with their platform, e.g. x:@jane or linkedin:jane.
Error: "on the moon" is not a stage of "Sales Pipeline". Stages: appointmentscheduled, qualifiedtobuy, …
Error: Agents can't lift do-not-contact; only a human can, in the web app. Everything else was saved.

Classic tool set

The original nine task-named tools (get_brief, log_interaction, save_contact, search_contacts, set_do_not_contact, release_contact, list_follow_ups, complete_follow_up, can_contact) are still served at https://undocrm.com/mcp?tools=classic for existing integrations. They have no deals, pipelines, conversation search or property correction. See DECISIONS.md for how the two sets compared in evals.