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.
Metadata
Release files for hubspot-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| hubspot_mcp-0.1.0.tar.gz | 1.3 MB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hubspot_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.3 MB
Release files / hubspot_mcp-0.1.0.tar.gz
| Download URL | hubspot_mcp-0.1.0.tar.gz |
|---|---|
| Size | 1.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c1a85dcc71b6fc1fdc0307ef05f037c8882a6743e170cba02822ebd5975e0741
|
|
BLAKE2b-256 checksum How to use checksums |
6ed76d7fff45fe10e2f9d7b6d9e42384bd1f33a86fc706dceb4ac4c27209bba8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / hubspot_mcp-0.1.0-py3-none-any.whl
| Download URL | hubspot_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 23.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
26d0780c15685586f46f5cb909af69d2b2be637e08623ed4f39604f454b63c99
|
|
BLAKE2b-256 checksum How to use checksums |
3a2129fe18028b30c8c5eb9b98f70bc7cee53a48e49aab54c50ef4aba1d1ec6e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Security for hubspot-mcp 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.