Import Contacts
Tool names below are expected names for the HubSpot MCP connector. If a named tool isn't available, check for updates to the skill and/or alternates in the HubSpot toolset as available tools may change
Phase 1 — Identify the data source
Use this priority order:
- Infer from context — if the user attached a file, said "from my CSV", "from Salesforce", or similar, proceed directly to Phase 2 with that source.
- Check connected services — silently test which connectors are available (attempt a lightweight tool call for each). Build a dynamic list of sources to offer.
- Ask if the source isn't clear.
Ask as plain text — no widget. Build the question based on what connectors are available:
If connected services are detected, list them under a heading, then offer the manual options:
If no connectors are detected:
For connected email provider: Use the connector's search/thread tools to find recent contacts. Extract unique senders and recipients from recent threads. Record the timestamp of the most recent thread per contact as the "last contact" date.
For connected CRM: Use the CRM's export or search tools to pull contact records. Map the source CRM's fields to HubSpot properties.
Fetch portal context early using get_organization_details and get_user_details. Store portalId, userId (for hubspot_owner_id), and the user's email domain (for filtering internal colleagues in Phase 2).
Phase 2 — Parse, deduplicate, and confirm
Field mapping
Map detected column headers (or inferred positions) to HubSpot contact properties:
Use get_properties with objectType: contacts to confirm target properties exist. Use search_properties for non-standard or custom column names.
Filter internal contacts
Remove any contact whose email domain matches the user's own domain from Phase 1. Contacts with no email cannot be filtered on domain — leave them in; a warning will surface in the widget. Note how many were removed — surface this in the widget header.
Duplicate check
For contacts with a valid email, batch into groups of 50 and call search_crm_objects:
- objectType: contacts
- filter: email IN [...batch of emails...]
- properties: email, firstname, lastname, hs_object_id
Classify each contact as New (not in HubSpot) or Existing (already in HubSpot).
Data quality flags
Before showing the widget, flag:
- Rows with no email address
- Malformed email addresses
- Duplicate emails within the source file itself
- Unmapped columns
Contact selection widget
Render an interactive widget using show_widget. Do not show a text summary first — the widget is the confirmation step.
The widget must include:
- Header: source label (e.g. "Found in your CSV"), subtitle with source metadata, and a note if contacts were skipped (internal domain, invalid emails)
- Scrollable table with columns: checkbox, NAME, EMAIL, COMPANY, LAST CONTACT (for email/CRM source; omit for CSV/paste), STATUS badge ("Already in HubSpot" for existing contacts)
- Select-all checkbox in the header row — toggles all, shows indeterminate state if partial
- Footer: "Cancel" text button and a primary "Import N contacts" button (N updates live as checkboxes are toggled; disabled if 0 selected)
On "Import N contacts" click:
On "Cancel":
Include all contacts in the widget regardless of duplicate status. Contacts with no email or invalid email: include but unchecked by default with a muted "No email" label. Existing contacts: include, checked by default, with the status badge.
Handling responses:
IMPORT_CONFIRMED:[...]— parse the JSON array and proceed to Phase 3IMPORT_CANCELLED— acknowledge and stop- Free-text message (e.g. "skip the ones from Acme") — interpret, adjust selection, re-render widget
Do not proceed to Phase 3 until a confirmed selection is received.
Phase 3 — Import
Prepare records
For each confirmed contact:
- Build HubSpot properties object from the confirmed field mapping
- Split full names: everything before the first space =
firstname, everything after =lastname. Single-word names →firstnameonly,lastnameblank. Iffirstname/lastnamecolumns exist separately, use them directly instead of splitting. - Assign
hubspot_owner_idto the current user's ID from Phase 1 - Note any company name for association
Batch import
Split into batches of 50. For each batch, call manage_crm_objects:
- New contacts:
createRequest, objectType: contacts - Updates:
updateRequest, objectType: contacts, usinghs_object_idfrom Phase 2
Show a single updating progress line:
Capture any per-record errors and store for the Phase 4 summary.
Company association
For contacts with a company name:
search_crm_objectscompanies, filter: name = [company name]- If found, associate via
manage_crm_objectswith association typecontact_to_company - If not found, create via
manage_crm_objectscreateRequest, objectType: companies, then associate
Batch company lookups where possible to reduce API calls.
Phase 4 — Summary
Name individual skipped/errored contacts up to 5; after that "[first 5 names] + N more." If any rows errored, offer to export a list so the user can fix and re-import.
Phase 5 — Post-import: log history and surface deal signals
Run after Phase 4. Only apply the steps relevant to the import source.
5a — Log existing communication history (email/CRM sources only)
If contacts were sourced from a connected email provider or CRM:
- For each newly imported contact, search the source connector for recent threads or interactions involving their email address (last 90 days, up to 5 per contact).
- For each thread found, create a note in HubSpot:
manage_crm_objectsnotes, createRequest: hs_note_body: "[Date] — [Subject/topic] via [source]", hs_timestamp: thread date, association: contact
- Skip contacts where no threads are found — no note needed.
- Cap total notes written to 100 (prioritise most recent threads first if over the limit).
5b — Detect deal signals
Review the threads or records from 5a (or the source data itself if CSV/paste) for buying signals: pricing discussions, demo requests, proposal mentions, evaluation timelines, or decision language.
For contacts with a clear signal:
- Suggest creating a deal: "I noticed [Name] mentioned [signal]. Want me to create a deal for them?"
- If the user confirms, create via
manage_crm_objectsdeals, createRequest: dealname: "[Contact Name] — [company]", dealstage: appointmentscheduled, associations: contact → contactId. - If there are multiple signal contacts, show a brief table (Name, Signal, Company) and ask which to convert.
Skip contacts with no signals — don't create speculative deals.
5c — Recommend ongoing activity logging
After a successful import, prompt:
"To keep [N] new contacts up to date automatically, connect your email to HubSpot. That way future emails are logged as activity without any manual steps. Say 'connect my email to HubSpot' when you're ready to set it up."
Skip this prompt if an email connector is already logging to HubSpot (infer from whether 5a returned activity data).
Edge cases
- No email column: warn strongly — HubSpot deduplicates by email. Offer to continue with a caveat or ask the user to add emails first.
- Large files (>500 rows): warn that import may take a moment. Continue without extra confirmation.
- All contacts are duplicates: confirm with the user before exiting — they may have intended to update records.
- Source returns no contacts: let the user know and suggest they try a different source or add contacts manually.
- Company association fails: log the failure in the summary but do not block the contact import.
