mcp-crm
Enables AI agents to safely operate HubSpot CRM contacts, deals, and pipelines via MCP, with caching, idempotency, audit trails, and robust error handling.
README
mcp-crm
A HubSpot MCP server (Model Context Protocol) that behaves like production software instead of a thin REST wrapper. It works with a HubSpot free-tier private-app token or a full OAuth app refresh flow, prompts for the exact missing scope instead of leaking a raw 403, retries rate limits with backoff, serves reads from a local cache (TTL plus write-invalidation, with a signature-verified webhook handler you can wire to your own HTTP ingress for out-of-band changes), walks cursor pagination, returns per-item results on batch partial failures, makes writes idempotent, and writes a PII-redacted audit trail of every tool call. Point an MCP client (Claude Desktop, or any MCP host) at it and let an agent operate contacts, deals, and pipelines safely.
See docs/COMPARISON.md for side-by-side transcripts of a naive API-wrapper MCP versus this one — every transcript is generated by running both against the mocked test suite.
Architecture
flowchart TD
Agent["MCP client / agent"] -->|"stdio (JSON-RPC)"| Server["FastMCP server<br/>server.py"]
Server --> Service["CrmService<br/>scope checks · audit · orchestration"]
Service --> Cache["LocalCache<br/>TTL + write invalidation"]
Service --> Idem["Idempotency store"]
Service --> Audit["Audit log<br/>PII redaction"]
Service --> Client["HubSpotClient<br/>retries · pagination · error mapping"]
Client --> Auth["Token provider<br/>private-app · OAuth refresh"]
Client -->|HTTPS| HubSpot["HubSpot CRM API"]
Ingress["Your HTTP ingress<br/>(optional, host-provided)"] -->|"signed v3 payload"| Processor["WebhookProcessor<br/>verify_signature"]
Processor -->|"invalidate(object)"| Cache
The stdio server speaks JSON-RPC only; it does not listen for webhooks. WebhookProcessor and verify_signature are shipped as a tested component you mount on your own HTTP ingress (see Webhook cache invalidation).
Tools
| Tool | Purpose | Scope |
|---|---|---|
crm_search_contacts / crm_list_contacts |
search or page contacts | crm.objects.contacts.read |
crm_get_contact |
fetch one contact (cached) | crm.objects.contacts.read |
crm_create_contact / crm_update_contact / crm_delete_contact |
idempotent write / update / archive | crm.objects.contacts.write |
crm_batch_create_contacts |
batch create with partial-failure reporting | crm.objects.contacts.write |
crm_search_deals / crm_get_deal |
search / fetch deals | crm.objects.deals.read |
crm_create_deal / crm_update_deal / crm_delete_deal |
deal writes | crm.objects.deals.write |
crm_list_pipelines / crm_get_pipeline |
pipelines and ordered stages (cached) | crm.schemas.deals.read |
crm_export_audit_log |
export the session audit trail as JSONL | — |
Quickstart
Requires Python 3.12+ and uv.
uv venv
uv pip install -e ".[dev]"
cp .env.example .env # then fill in your HubSpot credentials
Authenticate with either a private-app token (HUBSPOT_PRIVATE_APP_TOKEN) or an OAuth app (HUBSPOT_CLIENT_ID + HUBSPOT_CLIENT_SECRET + HUBSPOT_REFRESH_TOKEN). The server auto-detects which is present.
Run it over stdio:
uv run mcp-crm
Wiring into an MCP client
Add to your MCP host config (for example Claude Desktop's claude_desktop_config.json):
{
"mcpServers": {
"hubspot": {
"command": "uv",
"args": ["run", "mcp-crm"],
"env": { "HUBSPOT_PRIVATE_APP_TOKEN": "pat-na1-..." }
}
}
}
OAuth authorization URL
For the OAuth flow, mcp_crm.auth.build_authorization_url(...) builds the consent URL with the scopes the tools need; exchange the returned code for a refresh token and set HUBSPOT_REFRESH_TOKEN.
Webhook cache invalidation
The stdio server does not receive webhooks. To invalidate the cache on changes made outside this process (edits in the HubSpot UI, other integrations), mount WebhookProcessor on your own HTTP endpoint and verify HubSpot's v3 signature with verify_signature (using the HUBSPOT_WEBHOOK_SECRET you configure). Point it at the same LocalCache your CrmService uses:
from mcp_crm.webhooks import WebhookProcessor, verify_signature
processor = WebhookProcessor(cache)
def handle_hubspot_webhook(request):
ok = verify_signature(
secret=webhook_signing_secret,
method="POST",
uri=request.url,
body=request.raw_body,
signature=request.headers["X-HubSpot-Signature-v3"],
timestamp=request.headers["X-HubSpot-Request-Timestamp"],
)
if not ok:
return 401
processor.process(request.json())
return 200
verify_signature rejects tampered bodies, wrong secrets, and stale timestamps; WebhookProcessor.process maps each subscription to the object it invalidates and reports what it touched.
Design decisions
- Cache scope. Only object-detail reads (
crm_get_contact,crm_get_deal) and the pipeline list are cached; list/search results are query-dependent and left uncached to avoid serving stale result sets. Writes invalidate the relevant object immediately. For out-of-band changes, a signature-verifiedWebhookProcessorships as a component you mount on your own HTTP ingress (see Webhook cache invalidation) — the stdio server itself does not listen for webhooks. - Idempotency is client-side. HubSpot's create endpoints are not natively idempotent, so a key (supplied or derived from the payload) is stored and replayed. This makes at-least-once tool retries safe without duplicating records.
- Scope prompting happens twice. The service pre-checks granted scopes (via token introspection) for a fast, actionable error, and the HTTP client also maps a server-side
MISSING_SCOPES403to the same typed error — belt and suspenders. - Backoff and clocks are injectable. Retry sleep, RNG jitter, and time sources are constructor parameters, which is why the whole suite runs offline in well under a second.
Testing
Every external HubSpot call is served by an in-memory fake (tests/fake_hubspot.py) backed by JSON fixtures (tests/fixtures/), wired in through httpx.MockTransport. No network, no credentials, deterministic.
uv run pytest -q
Regenerate the comparison document (CI also checks it stays in sync):
uv run python scripts/generate_comparison.py
Scope and safety
This is a client for HubSpot data you own or are authorized to access. Point it only at HubSpot accounts you control or have written permission to operate. The audit log redacts emails and phone numbers before writing records; treat exported audit logs as sensitive regardless. Behaviour documented here is point-in-time against the included fixtures, not a guarantee about any live HubSpot account.
Hire me
I build MCP servers and API integrations that survive a senior-dev code review — auth, retries, idempotency, and audit trails included, not bolted on later. Portfolio and contact: https://amin-ale.github.io/portfolio-site · amin.ale.business@gmail.com.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.