HubSpot CRM MCP server: contacts, deals, pipelines. Idempotent writes and a full audit trail.
Strong naming conventions (verb_noun pattern consistently applied). Descriptions are detailed and contextual, averaging ~180 chars, well above the 34-char baseline minimum. All 15 tools have explicit descriptions. Parameters are well-annotated with Annotated types and Field descriptions. Input schemas are complete with type constraints (min/max for limits, enums implicit via descriptions). Output schemas documented via return type hints (Page, CrmObject, Pipeline, BatchResult). Tool annotations present (readOnlyHint, destructiveHint, idempotentHint). Key strength: descriptions include WHEN to use each tool and prerequisites (scope requirements, caching behavior, pagination guidance). Weakness: some parameter descriptions could be more concise; batch tool descriptions are minimal (tools 7, 14); error handling guidance is implicit rather than explicit in descriptions.
Create multiple HubSpot contacts in one call and return succeeded and failed rows separately.
Create multiple HubSpot deals in one call and return succeeded and failed rows separately.
Create one HubSpot contact from the properties given and return the new record. Use this for single creates, especially ones a retry might repeat. Use crm_batch_create_contacts to load many rows at once, and crm_update_contact to change a record that already exists. Writes, additively. Needs the crm.objects.contacts.write scope. The call is keyed by 'idempotency_key', or by a hash of the properties when none is given, and a repeat of the same key replays the stored record instead of creating a second one. That key store lives in this process, so it covers retries within a session and not restarts. Cached contact reads are invalidated. At least one property must be supplied or the call fails validation.
Create one HubSpot deal from the properties given and return the new record.
Archive (soft-delete) a HubSpot contact so it drops out of active views. Use this i
crm_delete_contact and crm_delete_deal descriptions are truncated/incomplete ('Archive (soft-delete)...' cuts off mid-sentence). Agents cannot determine full behavior or recovery options.
Batch tool descriptions (crm_batch_create_contacts, crm_batch_create_deals) are minimal (~50 chars). Missing guidance on when to use batch vs single-create, error handling per-item, and retry semantics.
Deal and pipeline tool descriptions (crm_search_deals, crm_list_deals, crm_get_deal, crm_delete_deal, crm_list_pipelines) lack context on when to use each vs alternatives, caching behavior, and scope requirements. Descriptions are 60 chars or less.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 61 | 2026-07-28+ | v2 |
Archive (soft-delete) a HubSpot deal so it drops out of active views.
Fetch one HubSpot contact by record id with its email, name, phone, and company. Use this once an id is known. Use crm_search_contacts to find the id first. Read-only. Needs the crm.objects.contacts.read scope. Served from an in-process cache with a 300 second default TTL; writes made through this server invalidate the entry immediately, and a cache hit is recorded in the audit log as a cache hit rather than a live read. Raises a not-found error for an unknown or archived id.
Fetch one HubSpot deal by record id with its name, amount, stage, and close date.
List HubSpot contacts in record-id order, one page at a time. Use this to walk the whole contact set. Prefer crm_search_contacts when there is a term to match on, since listing everything is slower and burns rate limit. Read-only. Needs the crm.objects.contacts.read scope; a missing scope is denied locally and recorded in the audit log. Not cached. Pass the returned 'after' cursor back for the next page and stop when it comes back null. Archived contacts are excluded.
List HubSpot deals in record-id order, one page at a time.
List all deal pipelines in the HubSpot account with their stages.
Search HubSpot contacts by free-text term and return one page of matches. Use this to find contacts by name, email, company, or any other indexed property. Prefer crm_list_contacts when there is no term to match on, and crm_get_contact when the record id is already known. Read-only. Needs the crm.objects.contacts.read scope; without it the call is denied before any CRM data request is made and the denial is written to the audit log. Search results are never cached, so every call reaches HubSpot. Pass the returned 'after' cursor back to walk further pages; a null 'after' means this was the last page. Archived contacts are excluded.
Search HubSpot deals by free-text term and return one page of matches.
Update properties on an existing HubSpot contact and return the updated record. Use this to change a known record. Use crm_create_contact for records that do not exist yet. Writes. Only the properties passed are sent, and each one overwrites the value HubSpot held before, so the previous value is lost; properties left out are untouched. Applying the same values again produces the same record, which makes a retry safe. Needs the crm.objects.contacts.write scope. Invalidates this contact's cache entry. Raises a not-found error for an unknown or archived id, and a validation error when no property is supplied.
Update properties on an existing HubSpot deal and return the updated record.
No explicit error handling guidance in tool descriptions. Agents cannot determine if errors are retryable, user-fixable, or fatal. E.g., 'Raises a not-found error' is stated but no recovery path offered.
Batch tool input schemas accept generic dict arrays ('flat property map') without documenting which custom properties are valid or how they map to HubSpot fields. LLMs may pass invalid property names.