Production-grade, read-only MCP server for HubSpot CRM
Project description
hubspot-mcp-server
A production-grade, read-only MCP server for HubSpot CRM: list and fetch contacts, companies, deals, and tickets (or any custom object), full-text search, walk the CRM's association graph, and discover an object's property schema — from Claude Desktop, Claude Code, or any MCP client — with HubSpot's rate limits handled properly and no live portal required to run the test suite.
Read-only by construction: the HTTP layer this server is built on
(hubspot_mcp.client.HubSpotClient) exposes exactly two verbs — get, and post. The
only post calls this server ever makes are to HubSpot's CRM Search API
(POST /crm/v3/objects/{object}/search), which HubSpot itself models as a POST purely
because the search filter body is too large for a query string — it is a read, not a
write. The client hard-allowlists this: post() rejects any path that doesn't end in
/search with a ValueError before a request is ever sent. There is no put,
patch, or delete method anywhere in the codebase.
The server can read whatever the token's scopes allow. A HubSpot Private App
access token's granted scopes are the actual security boundary — this server enforces
nothing beyond what the token itself is permitted to see. Grant only the read scopes
you need (e.g. crm.objects.contacts.read, not every crm.objects.*.read scope) —
see the setup walkthrough below.
hubspot-mcp-server is not affiliated with, endorsed by, or sponsored by HubSpot, Inc.
Quickstart
Requires uv and a HubSpot portal to point at. Don't have one? developers.hubspot.com lets you create a free developer account and a test portal in a couple of minutes — no cost, no credit card.
Install and run
This project will be published to PyPI as the distribution hubspot-mcp; its console
script is named hubspot-mcp-server (a different, unrelated package already holds the
hubspot-mcp-server PyPI name). Because of that mismatch, once published you must tell
uvx which distribution to pull the script from — uvx hubspot-mcp-server alone would
fetch the wrong package:
uvx --from hubspot-mcp hubspot-mcp-server
That starts the server over stdio. In practice you'll point an MCP client at it instead of running it directly — for Claude Code:
Note: hubspot-mcp is not yet published to PyPI either, so --from hubspot-mcp
below won't resolve. Until it's published, replace it with the from-source form
uvx --from git+https://github.com/inogen-ai/hubspot-mcp-server hubspot-mcp-server.
claude mcp add hubspot \
-e HUBSPOT_MCP_ACCESS_TOKEN=<your-private-app-token> \
-- uvx --from hubspot-mcp hubspot-mcp-server
For Claude Desktop, add to claude_desktop_config.json:
{
"mcpServers": {
"hubspot": {
"command": "uvx",
"args": ["--from", "hubspot-mcp", "hubspot-mcp-server"],
"env": {
"HUBSPOT_MCP_ACCESS_TOKEN": "<your-private-app-token>"
}
}
}
}
Create a Private App and access token
- In your HubSpot portal, click the Settings gear icon (top nav).
- In the left sidebar, go to Integrations > Private Apps.
- Click Create a private app, give it a name (e.g. "hubspot-mcp-server").
- Open the Scopes tab and select the read scopes for what you want this server
to access — for example
crm.objects.contacts.read,crm.objects.companies.read,crm.objects.deals.read,crm.objects.tickets.read, andcrm.schemas.contacts.read(and the matchingcrm.schemas.<object>.readscopes for whichever objects you uselist_propertieson). Grant only what you actually need — see the scope warning above. - Click Create app, confirm, then copy the access token shown — HubSpot only displays it once. Store it somewhere safe; it's a secret.
Tools
| Tool | Parameters | Returns |
|---|---|---|
list_records |
object_type: str, properties: str = "", limit: int | None = None |
Recent records of object_type. Sensible default properties per standard object (see below); properties (comma-separated) overrides them. id is always first. Says plainly when more records are available than limit returned. limit defaults to HUBSPOT_MCP_ITEM_LIMIT when omitted. |
get_record |
object_type: str, record_id: str, properties: str = "" |
One full record by numeric id — typically an id from a prior list_records/search_records call. |
search_records |
object_type: str, query: str, limit: int | None = None |
Full-text search of object_type for query (plain text — a name, email, domain, etc. — never filter syntax) via the CRM Search API. Appends "(showing N of total matches)" when more matches exist than limit returned. limit defaults to HUBSPOT_MCP_ITEM_LIMIT when omitted. |
get_associations |
object_type: str, record_id: str, to_object_type: str |
The to_object_type records associated with one object_type record (e.g. the contacts on a company) — the CRM relationship graph. Each hit's numeric id composes directly into get_record. |
list_properties |
object_type: str |
The property schema (name, type, label) defined on object_type — discover valid property names before building a properties list. Capped at a multiple of HUBSPOT_MCP_ITEM_LIMIT, with a note when more exist. |
Object model cheat sheet
object_type accepts a standard HubSpot object name — contacts, companies,
deals, tickets — or a custom object's name, everywhere it appears above. Record
ids are always plain numeric strings (e.g. "12345"), never HubSpot's hs_object_id
formatted any other way; get_record rejects anything that isn't purely digits before
making a request. search_records' query is plain search text — it goes straight
into the search request's JSON query field, never string-interpolated into a filter
expression.
Environment variables
All settings are prefixed HUBSPOT_MCP_ and can be set in the environment or a .env
file (see .env.example).
| Variable | Default | Purpose |
|---|---|---|
HUBSPOT_MCP_ACCESS_TOKEN |
(unset, required) | Private App access token, sent as Authorization: Bearer. See the setup walkthrough above. |
HUBSPOT_MCP_BASE_URL |
https://api.hubapi.com |
HubSpot API origin. Override for EU data residency or to point the client at a local test double; must be https (a plain-http localhost override is allowed for testing). |
HUBSPOT_MCP_ITEM_LIMIT |
25 |
Default page size for list_records/search_records when their limit parameter is omitted, and the base for list_properties's output cap (item_limit × 10, see the Tools table). Pass a per-call limit to list_records/search_records to override it for that call. |
HUBSPOT_MCP_TIMEOUT_SECONDS |
30.0 |
HTTP timeout (seconds) per HubSpot request. |
HUBSPOT_MCP_ACCESS_TOKEN unset fails startup immediately with a message naming the
env var to fix, rather than failing obscurely on the first tool call.
Rate limits
HubSpot Private Apps are subject to daily and per-second API call limits. This server
reads HubSpot's X-HubSpot-RateLimit-* headers off every response and appends a
warning to a tool's result once daily usage crosses 90% used (10% or less of the
daily quota remaining), so a client sees it coming rather than hitting a hard stop
mid-session. A 429 is retried honoring
Retry-After (clamped to at most 60s, up to 3 retries, exponential 1→2→4s backoff
when no header is sent) — except when HubSpot's error body names the DAILY policy,
which is not retried (retrying can't fix a quota that only resets at midnight portal
time) and instead returns an actionable message immediately.
Security notes
- Read-only by construction, not by policy — see the warning at the top of this README. The Private App token's granted scopes, not this server, decide what's actually readable.
- Rate-limit aware. See "Rate limits" above.
- Credentials never appear in error messages, logs, or exceptions raised to an MCP client — failures are reduced to plain, credential-free sentences before they leave the client layer.
- Not affiliated with, endorsed by, or sponsored by HubSpot, Inc.
Development
Requires Python 3.11+ and uv.
uv sync
uv run pytest -q
uv run ruff check .
No live HubSpot portal is needed for the test suite — the CRM API is faked at the
httpx.MockTransport boundary, and a committed stdio integration gate drives the real
server process against an in-process fake CRM API. See
docs/manual-verification.md for the live-portal check a
maintainer runs before releases.
Contributing
Contributions welcome — see CONTRIBUTING.md for dev setup and PR expectations, and SECURITY.md for reporting vulnerabilities privately.
Not affiliated with, endorsed by, or sponsored by HubSpot, Inc.
Part of InoGen's open-source portfolio: kilnworks (self-hostable RAG assistant) and the read-only MCP connectors m365, servicenow, salesforce, and hubspot.
Built and maintained by InoGen.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hubspot_mcp-0.1.0.tar.gz.
File metadata
- Download URL: hubspot_mcp-0.1.0.tar.gz
- Upload date:
- Size: 1.3 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.29 {"installer":{"name":"uv","version":"0.9.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c1a85dcc71b6fc1fdc0307ef05f037c8882a6743e170cba02822ebd5975e0741
|
|
| MD5 |
502e5e2c174cbdb947eb5e62062d65bf
|
|
| BLAKE2b-256 |
6ed76d7fff45fe10e2f9d7b6d9e42384bd1f33a86fc706dceb4ac4c27209bba8
|
File details
Details for the file hubspot_mcp-0.1.0-py3-none-any.whl.
File metadata
- Download URL: hubspot_mcp-0.1.0-py3-none-any.whl
- Upload date:
- Size: 23.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.29 {"installer":{"name":"uv","version":"0.9.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26d0780c15685586f46f5cb909af69d2b2be637e08623ed4f39604f454b63c99
|
|
| MD5 |
721986f58a90ae41b89fbfa1f3a2d413
|
|
| BLAKE2b-256 |
3a2129fe18028b30c8c5eb9b98f70bc7cee53a48e49aab54c50ef4aba1d1ec6e
|