> ## Agent Instructions
>
> Base URL: https://api.anysite.io
> Authentication: send the `access-token` header. Do NOT use `Authorization: Bearer`.
> Full endpoint catalog: https://app.anysite.io/docs
# CRM tools

Let your AI client read and safely enrich HubSpot or Pipedrive records through the MCP server, with previews and undo.

Goal: connect your CRM and have your AI client enrich contacts and companies without overwriting data you care about.

## Set up

1. Open your profile and turn on **CRM Integration**. Until it is on, the `crm_*` tools are hidden from your
   MCP client, and CRM calls fail with `CRM integration is not enabled. Activate it in account settings first.`
2. Click **Connect HubSpot** or **Connect Pipedrive**. A new tab opens in the CRM: approve access there and
   finish within 10 minutes. The connection shows as `active` in your profile.
3. Optional: under **Protected fields**, list extra property names (comma-separated) that agents must never write.
4. Refresh the tool list in your MCP client (or start a new chat). The change reaches the MCP server within a minute.

> ACTION FOR THE HUMAN: open https://app.anysite.io/profile (Open profile).

You can also connect from the chat: the `crm_connect` tool returns a link to open in the browser, and
`crm_connect_status` reports when the connection is active. Only one CRM can be connected at a time —
connecting a second one fails with `Another CRM is already connected. Disconnect it before connecting a new one.`
Disconnect the current one in your profile to switch.

## How writes work

`crm_upsert_contacts` and `crm_upsert_companies` apply the same rules to every field of every record:

- **Fill blanks only.** A field that already has a value is left alone, unless its name is in
  `overwrite_properties`.
- **Protected fields are never written**, not even with `overwrite_properties`. Built-in protected fields:
  HubSpot `hubspot_owner_id`, `lifecyclestage`, `hs_lead_status`; Pipedrive `owner_id`, `label`, `label_ids`,
  `visible_to`. Your own protected fields from the profile are added to these.
- **Read-only CRM properties**, empty values, and values that are not one of a dropdown property's options
  are skipped.
- **No new records by default.** A record that does not exist is created only with `allow_create: true`.
- **Preview first.** `dry_run: true` returns, for every record, the fields that would be written with their
  old and new values, and writes nothing. Your client should show you this preview before a bulk write.

Every skipped field or record comes back as a warning with a reason, so nothing is dropped silently.

### Matching records

| Object | Matched by | Creating a new record requires |
|---|---|---|
| Contacts | `email`, `record_id` or `linkedin_url` | `email` |
| Companies | `domain` or `record_id` | `domain` |

- `linkedin_url` matching always works in HubSpot. In Pipedrive it works only if the account has a custom
  person field for LinkedIn that Pipedrive search can reach; otherwise such records never match.
- When one key matches several records, the oldest one is updated and a `duplicate_contacts` /
  `duplicate_companies` warning reports how many candidates there were.
- A contact can be linked to a company with `associate_company_domain` or `associate_company_id`. An existing
  company link is kept unless `overwrite_associations: true`.

### Warning reasons

| Reason | Meaning |
|---|---|
| `fill_blank_skip` | The field already had a value and was not in `overwrite_properties`. |
| `protected_field` | The field is protected. |
| `read_only_property` | The CRM does not allow writing this property. |
| `empty_value` | The value to write was empty. |
| `enum_option_missing` | The value is not one of the property's options. |
| `not_found` + `create_not_allowed` | No record matched and `allow_create` was off. |
| `record_not_found` | The given `record_id` does not exist. |
| `create_requires_email` | A new contact could not be created without an email. |
| `duplicate_contacts` / `duplicate_companies` | Several records matched; the oldest was updated. |
| `scan_incomplete` | The CRM was too large to search completely, so an existing company may have been missed. |

## Read records

`crm_get_schema` returns contact and company properties (including custom ones and dropdown options), lists
and the protected fields; ask for specific properties to keep the answer short. `crm_query_records` reads
records by list, record ids, contact emails or free-text search, with the properties you name and a cursor
for the next page.

## Undo

Every real write returns a `run_id`. `crm_undo(run_id)` restores the previous value of every field that run
wrote and removes the company links it added:

- A field changed again after the run is reported under `conflicts` and left as it is.
- Fields that are now protected or read-only are skipped with a warning; records deleted since are reported as
  `record_missing`.
- Records the run created are not deleted; the fields the run wrote on them are cleared.
- The undo is a run of its own: its `undo_run_id` can be undone the same way.

## Verify

Ask your client to run a small enrichment as a dry run, for example
`Enrich my 5 newest HubSpot contacts with their current job title — dry run first`. You should see a preview
with old and new values and no change in the CRM. Run it for real, then `crm_undo` with the returned `run_id`
to see the values restored.

## Troubleshooting

| Message | Fix |
|---|---|
| No `crm_*` tools in the client | Turn on **CRM Integration** in your profile, then refresh the client's tool list. |
| `No active CRM connection. Run crm_connect first.` | Connect a CRM in your profile or with `crm_connect`. |
| `Multiple active CRM connections, pass connection_id explicitly.` | Ask the client to use `crm_list_connections` and pass the `connection_id`. |
| `CRM connection was revoked. Reconnect your CRM.` / `CRM connection is not active` | Access was removed on the CRM side. Disconnect and connect again in your profile. |
| `Run not found` | The `run_id` is wrong or belongs to another connection. |
