HubSpot MCP
by ohneben
README.md
# ohneben's HubSpot MCP
[](https://buymeacoffee.com/ohneben)
---
#### License & checks
[](https://github.com/ohneben/Hubspot-MCP/actions/workflows/ci.yml)
[](./LICENSE.md)
#### MCP registries
[](https://registry.modelcontextprotocol.io/v0.1/servers/io.github.ohneben%2Fhubspot-mcp/versions/latest)
[](https://mcpservers.org/servers/ohneben/hubspot-mcp)
[](https://glama.ai/mcp/servers/ohneben/Hubspot-MCP)
**The most complete HubSpot MCP server there is.** Run your entire
[HubSpot](https://www.hubspot.com/) account in plain language from **Claude**,
**Cursor**, or any other [MCP](https://modelcontextprotocol.io) client.
This [Model Context Protocol](https://modelcontextprotocol.io) server exposes the
**whole public HubSpot API β all 1,076 endpoints across 102 APIs, in 687
tools** β CRM, CMS, Marketing, Automation, Conversations, Commerce, Files,
Settings, Webhooks and more, generated straight from HubSpot's own OpenAPI
definitions. Endpoints HubSpot repeats per object type are one tool each
(`crm_objects_search` with `objectType: "deals"` instead of 32 separate search
tools), and every tool description says when to use it. Every tool is
**safety-categorized** (π’ read-only / π‘ write / π΄ destructive) and β unique to
this server β **hub & plan aware**: HubSpot publishes which hub and tier every
API needs (Free / Starter / Professional / Enterprise), and this server carries
that straight into each tool plus a live **capability report** for *your*
portal. It runs over **stdio** (Claude Desktop and other local launchers) or
**Streamable HTTP** (hosted in Docker), and ships with retries, client-side rate
limiting tuned to HubSpot's burst caps, and request timeouts so it holds up
against a live account.
## Why you'll want this
Some MCP servers just forward a slice of the API. This one is built to be **safe
to hand to an LLM**, **complete**, and **easy to run for real**:
| What you get | Why it matters |
| --- | --- |
| **All 1,076 endpoints β the whole public API** | Contacts, companies, deals, tickets, every engagement type, associations v4, properties, pipelines, lists, imports/exports, marketing emails & events, campaigns, forms, transactional email, sequences, workflows/actions, conversations & custom channels, CMS pages/posts/HubDB/source code, files, commerce (invoices, orders, carts, payments, subscriptions), settings, webhooks β nothing hand-picked or left behind. Most servers stop at ~30 CRM tools. |
| **687 tools, not 1,076 look-alikes** | HubSpot repeats the same endpoints for 32 CRM object types, for landing and site pages, and for blog posts, authors and tags. Those are one tool each with a selector argument (`objectType`, `pageType`, `blogResource`), so `crm_objects_search` covers contacts, deals, tickets and custom objects. Every call still goes to exactly the endpoint it went to before, with that endpoint's scopes and plan hints. |
| **Descriptions that say when to use a tool** | Each description opens with what the tool does, then when to prefer a sibling (`crm_objects_list` points to `crm_objects_search` for filtering and `crm_objects_batch_read` for known IDs), what it changes, plan and scopes, and the endpoint it calls. Undocumented HubSpot parameters get a description. |
| **Hub & plan awareness** *(nobody else has this)* | HubSpot gates APIs by hub and tier β HubDB needs Content/Marketing Hub **Professional**, custom-object schemas need **Enterprise**, sequences need Sales/Service **Professional**. Every tool states its requirement, straight from HubSpot's own API index. |
| **Access check at startup** | When the server starts it reads the token's scopes and probes every paid-tier or beta API group with one cheap read. The model gets the result in the server instructions, search results say per tool whether this token can use it, and `hubspot_get_capabilities` explains each status with the scopes to add. No more walls of mystery 403s. |
| **Curated safety categories** π’ / π‘ / π΄ | Not naive "GET = safe": a `POST β¦/search` is a **read-only query**, `merge` is flagged **irreversible**, `gdpr-delete` is a **permanent purge** (vs. archive β recycle bin), list-membership calls are reversible **links**, `POST /crm/v3/imports` is a **bulk import**, and transactional email is **sends messages**. |
| **Machine-readable MCP annotations** (`readOnlyHint`, `destructiveHint`) | Hosts that honor annotations (Claude included) can auto-trust reads and demand confirmation before anything destructive. |
| **Actionable error hints** | 403 with `MISSING_SCOPES` β the exact scopes to add and where; plain 403 on a gated API β the plan tier it needs; 401 β token type & expiry guidance; 429 β your limits. The model gets *how to fix it*, not just *what broke*. |
| **Read-only mode & group filtering** | Expose only the 302 π’ read-only tools (`HUBSPOT_READ_ONLY=true`), or narrow to the groups you use (`HUBSPOT_INCLUDE_GROUPS=contacts,deals,cms:*`). Area wildcards included. |
| **Discovery mode by default** | Out of the box the model sees 6 tools (~2k tokens): search, inspect and invoke over all 687 endpoint tools, plus the capability, GraphQL and raw-request tools. `HUBSPOT_TOOL_MODE=all` exposes every endpoint tool directly. Read-only mode applies either way. |
| **Real file uploads** | The multipart endpoints (Files, CRM imports, HubDB import, CMS source code) actually work β pass file content inline or as base64. Most generated servers can't do multipart at all. |
| **Automatic retries with backoff** | Transient `429` / `5xx` responses are retried with jittered exponential backoff, honoring HubSpot's `Retry-After` header. |
| **Built-in rate limiting** | Self-throttles under HubSpot's burst caps (default 100 req / 10 s) with a **separate limiter for the `/search` endpoints** (~5 req/s cap). A burst of tool calls won't trip a `429`. |
| **Response-size guard** | Optionally cap huge list responses (`HUBSPOT_MAX_RESPONSE_CHARS`) so one call can't blow the model's context window. |
| **CRM GraphQL passthrough** | HubSpot's GraphQL endpoint is query-only, so it's a π’ tool here β fetch a contact, its company and that company's deals in one round-trip. |
| **Raw-request escape hatch** | `hubspot_api_request` reaches brand-new or beta endpoints the moment HubSpot ships them β auth, throttling and retries still handled server-side. |
| **Two transports: stdio *and* Streamable HTTP** | Use it locally in Claude Desktop, or run one always-on server that any number of MCP clients reach over HTTP. |
| **Docker + docker-compose, health check, auto-restart** | Production-style deployment out of the box: `docker compose up` and it stays up. |
| **Optional bearer-token auth** on the HTTP endpoint | Put the server behind a shared secret the moment it's reachable beyond localhost. |
| **Your token never reaches the model** | The access token lives in the server's environment and is injected on every request β the assistant only ever sees tool inputs and API responses. |
| **Drop-in spec updates** | `npm run fetch-specs` pulls HubSpot's latest OpenAPI definitions (and their hub/tier metadata) from HubSpot's public index β new endpoints become new tools on rebuild, no code changes. |
## How it compares
There are a few ways to reach HubSpot from an AI assistant today. Here's how this
server stacks up against the alternatives:
| | **This server** | Official HubSpot MCP | shinzo-labs `hubspot-mcp` | `mcp-hubspot` (buryhuang) | CData MCP |
|---|:---:|:---:|:---:|:---:|:---:|
| Approx. tools | **~690 (all 1,076 endpoints)** | ~7 curated (remote) | 100+ | ~7 | 3 (generic SQL) |
| Whole public API (CRM **and** CMS Β· Marketing Β· Automation Β· Commerce Β· Files Β· Settings Β· Webhooks) | β
| β CRM + some content reads | β CRM-centric | β | β |
| Hub & plan-tier awareness per tool | β
| β | β | β | β |
| Account capability report (scopes Β· usage Β· unlocks) | β
| β | β | β | β |
| Reads **and** writes | β
| β
| β
| β partial | β read-only |
| Curated π’ / π‘ / π΄ safety categories | β
| β | β | β | n/a |
| `readOnlyHint` / `destructiveHint` annotations | β
| β | β | β | β |
| Read-only mode + group filtering | β
| β | β | β | always read-only |
| File uploads (multipart) | β
| β | β | β | β |
| GraphQL passthrough | β
| β | β | β | β |
| Rate limiting + auto-retry (`429`/`5xx`) | β
| β | β | β | β |
| `stdio` transport | β
| β
local package | β
| β
| β
|
| Streamable-HTTP transport | β
| β
hosted remote | β
| β | β |
| Docker + compose + health check | β
| β | β Dockerfile | β image only | β |
| Self-hosted on your own infra | β
| β vendor cloud | β
| β
| β
|
| Language | TypeScript | TypeScript | TypeScript | Python | Java |
| License | MIT | β | MIT | MIT | MIT |
<sub>β
= yes Β· β = partial / not documented Β· β = no. Compiled from each
project's public documentation; this is an unofficial project, not affiliated
with HubSpot or the projects listed. Tool counts are approximate and move as
APIs evolve.</sub>
**The short version:** the official remote server is a great managed on-ramp
but covers a handful of curated tools. The community servers are solid but
CRM-centric, without plan awareness or safety guardrails. This one gives you
**the entire public API**, **hub/plan awareness no one else has**, **both
transports**, a **production Docker deployment**, and **curated safety
categories** β self-hosted, on your token, MIT.
## What you can do
Once it's connected, ask your assistant things like:
- "Find jane@example.com, show her company, open deals and the last emails we exchanged."
- "Create a $12,000 deal 'Acme expansion' in the Sales pipeline, stage Qualified, associated with Acme Corp."
- "Search contacts created this month with lifecycle stage MQL and add them to the 'Q3 nurture' list."
- "Which HubDB tables exist? Add a row to 'pricing' and publish the table."
- "Clone last month's newsletter as a draft β don't publish."
- "Upload this CSV and start a contact import mapped to email + first name."
- "What can my token do? Check my API usage and which scopes are missing for HubDB." *(β `hubspot_get_capabilities`)*
- "Merge these two duplicate contacts β after showing me both records first."
Tools are generated automatically from HubSpot's specs and grouped into
π’ read-only, π‘ write and π΄ destructive β so a well-behaved host can treat each
group differently.
## How it works
```
Claude / Cursor / any MCP client ββMCPβββΊ this server ββHTTPSβββΊ api.hubapi.com (your portal)
```
The server parses 104 bundled OpenAPI definitions (fetched from HubSpot's public
API index, which also publishes per-API **hub/tier requirements**) into MCP
tools β resolving `$ref`s, guarding against recursive schemas, merging
endpoints that only differ by object type into one tool, deriving clean names
like `crm_objects_search` and `hubdb_tables_create_table`, and tagging each
tool with a curated safety category, its plan requirement and its OAuth scopes.
Your access token is injected server-side on every request; the model never
sees or handles it.
## Hub & plan awareness
HubSpot isn't one API β what you can call depends on **which hubs** (Marketing,
Sales, Service, Content, Commerce, Operations) and **which tier** (Free,
Starter, Professional, Enterprise) the portal has, plus the **scopes** granted
to your token. This server is built around that reality:
1. **Every tool description carries the requirement**, from HubSpot's own index:
```
π’ READ-ONLY Β· Get all published tables.
Use to page through tables: pass limit, then the after cursor from paging.next.after for the next page.
β¦
Plan: Professional tier of Marketing Hub / Content Hub. Scopes: hubdb.
Endpoint: GET /cms/v3/hubdb/tables (Hubdb API, CMS)
```
Consolidated tools state it per value: `crm_objects_list` lists which
object types need a paid tier in its `objectType` parameter.
2. **An access check runs when the server starts.** HubSpot has no API that
returns a portal's subscription, so the server combines what it can
observe: the token's granted scopes, and one cheap read per paid-tier or
beta group (up to about 30 API calls; plan gates only surface as 403s).
Every group gets a status: `available`, `missing_scopes`, `blocked` (plan
tier, user permission, or a 401 from an API that does not accept this kind
of token) or `unverified`. HubSpot's error details count: a 403 that names
scopes the token lacks is a missing scope, while `MISSING_SCOPES` for an
object whose scopes the token already holds is how HubSpot answers a plan
gate, so that is reported as blocked. A read
that succeeds proves read access only; a paid-tier API can still refuse
writes, and the report says so. The model receives a summary in
the server instructions, `hubspot_search_endpoints` marks each result (and
takes `usable_only: true`), and `hubspot_get_capabilities` returns the full
report with reasons, `"unlockedByScopes": "13/16"` counts and the scopes to
add; `refresh: true` runs it again. Turn it off with
`HUBSPOT_CAPABILITY_CHECK=false`.
3. **403s come back with the fix**: missing scope β the exact scope name and
where to grant it; plan-gated API β the tier HubSpot requires.
## Requirements
- A **HubSpot account** and a **service key** (or an existing legacy private
app token); see [Get your API credentials](#get-your-api-credentials).
- **Docker** (Docker Desktop on macOS/Windows) for the quick start below β or
**Node.js β₯ 18** to [run from source](#run-from-source-stdio-no-docker).
## Quick start (Docker)
**1. Add your credentials.** Copy the example config and fill it in:
```bash
cp .env.example .env
# edit .env β set HUBSPOT_ACCESS_TOKEN
# β set MCP_AUTH_TOKEN. REQUIRED unless HOST is a loopback address,
# otherwise the server refuses to start: MCP_AUTH_TOKEN=$(openssl rand -hex 32)
```
**2. Start the server:**
```bash
docker compose up -d --build
```
The bundled `docker-compose.yml` binds to `127.0.0.1:8765` only, so the server is
reachable from your machine but not the network.
**3. Confirm it's running:**
```bash
curl -s http://localhost:8765/health
# β {"status":"ok","server":"hubspot-mcp"}
```
**4. Connect your MCP client.** The MCP endpoint is `http://localhost:8765/mcp`.
- **Claude Desktop** β add a **custom connector** (Settings β Connectors) pointing
at the URL, or bridge it locally with
[`mcp-remote`](https://www.npmjs.com/package/mcp-remote). Add this under
`mcpServers` in your config, then fully quit and reopen the app:
```json
{
"mcpServers": {
"hubspot": {
"command": "npx",
"args": [
"mcp-remote",
"http://localhost:8765/mcp",
"--header", "Authorization: Bearer YOUR_MCP_AUTH_TOKEN"
]
}
}
}
```
(Drop the `--header` line only if the server runs without a token, which it
allows on a loopback bind alone.)
- **Claude Code** β one command:
```bash
claude mcp add --transport http hubspot http://localhost:8765/mcp
```
- **Claude Cowork** β shares Claude Code's MCP config, so the command above makes
the tools available there too.
> **Tip:** by default the model sees 6 tools and reaches every endpoint through
> search β inspect β invoke. For one tool per endpoint set
> `HUBSPOT_TOOL_MODE=all` (690 tools) and trim it with `HUBSPOT_INCLUDE_GROUPS`
> (e.g. `contacts,companies,deals,tickets,lists`) β see
> [Context footprint](#context-footprint) for measured numbers per
> configuration.
### Prefer a prebuilt image?
Every push to `main` publishes a ready-to-run image to the GitHub Container
Registry, so you can skip the local build entirely:
```bash
docker run -d --name hubspot-mcp -p 127.0.0.1:8765:8765 --env-file .env \
ghcr.io/ohneben/hubspot-mcp:latest
```
## Get your API credentials
The recommended way is a **service key**, HubSpot's replacement for legacy
private apps:
1. In HubSpot, open **Settings β Integrations β Service Keys** (or
**Development β Keys β Service Keys**) and create a key. You need super
admin rights or the *Developer tools access* permission.
2. Select the scopes you want the assistant to reach. Scopes map 1:1 to tool
groups: grant read scopes (`crm.objects.contacts.read`, β¦) for a reporting
setup, and add write scopes only where you want changes.
3. Copy the key (`pat-β¦`) β `HUBSPOT_ACCESS_TOKEN` in `.env`. It is sent as a
bearer token exactly like a private app token, so nothing else changes.
Notes:
- **Service keys are in public beta** and differ from private apps in three
ways that matter here: they do **not** support GraphQL (set
`HUBSPOT_ENABLE_GRAPHQL=false`), rotation keeps the old key valid for a
7-day grace period, and the scope and usage figures in the capability report
may be incomplete for them.
- **Existing private app tokens keep working.** HubSpot stops the creation of
new legacy private apps (existing portals from 26 October 2026, new portals
from 28 September 2026); apps already created are not affected. A private
app is still the only option here if you rely on the GraphQL tool.
- **OAuth access tokens work too** (for apps you've built) β but they expire
after ~30 minutes and this server does not refresh them; a service key or
private app token is the right fit for a long-running server.
- The token determines the portal β no portal ID needed.
- **EU data residency**: if your portal lives in HubSpot's EU data center, set
`HUBSPOT_BASE_URL=https://api-eu1.hubapi.com`.
## Configuration
Everything is set in `.env` (copied from `.env.example`):
| Variable | Required | Default | Description |
|---|---|---|---|
| `HUBSPOT_ACCESS_TOKEN` | β
| β | Service key or legacy private app token (both `pat-β¦`), or an OAuth access token. Without it the server still starts and lists its tools (for registries and inspectors), but every call returns setup instructions |
| `HUBSPOT_BASE_URL` | β | `https://api.hubapi.com` | Use `https://api-eu1.hubapi.com` for EU data residency |
| `MCP_TRANSPORT` | β | `stdio` | `stdio` or `http` (the Docker image defaults to `http`) |
| `PORT` | β | `8765` | HTTP listen port |
| `HOST` | β | `0.0.0.0` | HTTP bind address |
| `MCP_HTTP_PATH` | β | `/mcp` | HTTP MCP route |
| `MCP_AUTH_TOKEN` | β οΈ | _(off)_ | Require `Authorization: Bearer <token>` on `/mcp`. **Required** when `HOST` is not a loopback address, otherwise the server refuses to start. Renamed from `MCP_SHARED_TOKEN` in 1.1.0 |
| `MCP_ALLOWED_HOSTS` | β | _(derived)_ | Comma-separated hostnames the `Host` header may carry. Defaults to the loopback names on a loopback bind, and to no check behind a reverse proxy |
| `MCP_ALLOW_INSECURE` | β | `0` | Start without a token on a non-loopback bind. Only for a port that genuinely is not reachable by anyone else |
| `MCP_BODY_LIMIT` | β | `25mb` | Largest accepted request body |
| `MCP_SESSION_TTL` | β | `1800` | Seconds an idle session is kept before it is swept |
| `MCP_MAX_SESSIONS` | β | `256` | Concurrent sessions before the least recently used one is evicted |
| `HUBSPOT_MAX_REQUESTS` | β | `100` | Client-side requests per window (`0` disables throttling) |
| `HUBSPOT_RATE_WINDOW_MS` | β | `10000` | Rate-limit window in ms (default: 100 req / 10 s) |
| `HUBSPOT_SEARCH_MAX_REQUESTS` | β | `4` | Extra throttle for `/search` endpoints (`0` disables) |
| `HUBSPOT_SEARCH_RATE_WINDOW_MS` | β | `1000` | Search throttle window (default: 4 req / 1 s) |
| `HUBSPOT_MAX_RETRIES` | β | `3` | Retries on `429` / `5xx` / network errors |
| `HUBSPOT_TIMEOUT_MS` | β | `30000` | Per-attempt request timeout |
| `HUBSPOT_READ_ONLY` | β | `false` | Expose only π’ read-only tools |
| `HUBSPOT_INCLUDE_GROUPS` | β | _(all)_ | Only expose these groups β keys like `contacts,deals` and area wildcards like `cms:*` |
| `HUBSPOT_EXCLUDE_GROUPS` | β | _(none)_ | Hide these groups (same syntax) |
| `HUBSPOT_INCLUDE_BETA` | β | `true` | Include beta / developer-preview APIs |
| `HUBSPOT_TOOL_MODE` | β | `discovery` | `discovery` (3 meta-tools over the whole catalog) or `all` (one tool per endpoint or endpoint family) |
| `HUBSPOT_CAPABILITY_CHECK` | β | `true` | Read the token's scopes and probe paid-tier or beta groups at startup (up to about 30 API calls) |
| `HUBSPOT_ENABLE_GRAPHQL` | β | `true` | Expose the CRM GraphQL query tool |
| `HUBSPOT_GRAPHQL_URL` | β | _(derived)_ | Override the GraphQL endpoint |
| `HUBSPOT_ENABLE_RAW_REQUEST` | β | `true` | Expose the raw-request escape hatch (auto-hidden in read-only mode) |
| `HUBSPOT_MAX_RESPONSE_CHARS` | β | `0` | Truncate responses longer than N chars (`0` = never) |
| `HUBSPOT_SPEC_DIR` | β | _(bundled)_ | Load OpenAPI files + catalog.json from a different directory |
After changing `.env`, reload with `docker compose up -d --force-recreate`.
Run `npm run list-tools` (no credentials needed) to print the full catalog, the
per-category counts, and every group key you can filter on β add `--names` to
list all 687 tool names with the endpoint each one calls.
## Tool safety categories
Each tool's description starts with one of these banners and carries the matching
[MCP annotations](https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations):
| Banner | Tools | Endpoints | `readOnlyHint` | `destructiveHint` | Meaning |
|---|:---:|:---:|:---:|:---:|---|
| π’ **READ-ONLY** | 274 | 348 | `true` | `false` | `GET` β fetches data only. Safe. |
| π’ **READ-ONLY Β· query** | 28 | 95 | `true` | `false` | A `POST` that *searches/reads* (object search, batch read, export start, token introspection) β changes no records. |
| π‘ **WRITE Β· creates data** | 133 | 198 | `false` | `false` | Creates records (not idempotent; may duplicate). |
| π‘ **WRITE Β· creates or updates** | 7 | 36 | `false` | `false` | Idempotent upserts (batch upsert, marketing-event upsert). |
| π‘ **WRITE Β· updates data** | 131 | 215 | `false` | `false` | Modifies records/settings in place, including publishing, scheduling and restoring existing content. |
| π‘ **WRITE Β· links records** | 18 | 19 | `false` | `false` | Associates records (list memberships, v4 associations). Reversible. |
| π‘ **WRITE Β· unlinks records** | 11 | 12 | `false` | `false` | Removes associations. Reversible β records survive. |
| π‘ **WRITE Β· sends messages** | 4 | 4 | `false` | `false` | Marketing/transactional email, sequence enrollment, conversation replies. |
| π‘ **WRITE Β· bulk import** | 1 | 1 | `false` | `false` | `POST /crm/v3/imports` β can create/update thousands of records. |
| π΄ **DESTRUCTIVE Β· deletes data** | 64 | 96 | `false` | `true` | Deletes/archives a record (CRM archives are restorable ~90 days). |
| π΄ **DESTRUCTIVE Β· bulk delete** | 10 | 42 | `false` | `true` | Batch archive β many records in one call. |
| π΄ **DESTRUCTIVE Β· merges records** | 3 | 7 | `false` | `true` | HubSpot **cannot un-merge**. Confirm both IDs first. |
| π΄ **DESTRUCTIVE Β· permanent GDPR purge** | 3 | 3 | `false` | `true` | Skips the recycle bin; gone forever. |
That's **302 read-only Β· 305 write Β· 80 destructive = 687 endpoint tools**
(443 Β· 485 Β· 148 = 1,076 endpoints), plus the three power tools below. Hosts
that respect annotations (Claude included) can require confirmation for
`destructiveHint` tools and trust `readOnlyHint` tools automatically. Prefer to
lock it down further? Set `HUBSPOT_READ_ONLY=true` to expose *only* the 302
read-only tools (plus the π’ capability and GraphQL tools).
<details>
<summary><strong>Coverage by area (APIs / endpoints by π’ read / π‘ write / π΄ destructive / tools)</strong></summary>
| Area | APIs | π’ Read | π‘ Write | π΄ Delete | Endpoints | Tools |
|---|:---:|:---:|:---:|:---:|:---:|:---:|
| CRM | 58 | 216 | 245 | 92 | 553 | **211** |
| CMS | 13 | 74 | 123 | 22 | 219 | **172** |
| Marketing | 7 | 42 | 46 | 11 | 99 | **99** |
| Conversations | 3 | 18 | 11 | 3 | 32 | **32** |
| Automation | 3 | 17 | 10 | 4 | 31 | **31** |
| Webhooks Journal | 1 | 20 | 3 | 3 | 26 | **26** |
| Settings | 3 | 14 | 9 | 1 | 24 | **24** |
| Files | 1 | 10 | 7 | 4 | 21 | **21** |
| Communication Preferences | 1 | 6 | 8 | 0 | 14 | **14** |
| Events | 3 | 4 | 7 | 2 | 13 | **13** |
| Webhooks | 1 | 3 | 4 | 2 | 9 | **9** |
| Data Studio | 1 | 1 | 6 | 1 | 8 | **8** |
| Auth (OAuth) | 1 | 3 | 2 | 2 | 7 | **7** |
| Account | 2 | 5 | 0 | 0 | 5 | **5** |
| Commerce | 1 | 2 | 2 | 1 | 5 | **5** |
| Scheduler | 1 | 3 | 2 | 0 | 5 | **5** |
| Meta | 1 | 4 | 0 | 0 | 4 | **4** |
| Business Units | 1 | 1 | 0 | 0 | 1 | **1** |
| **Total** | **102** | **443** | **485** | **148** | **1,076** | **687** |
</details>
## Context footprint
What does the tool list cost in model context? Measured on the bundled specs
(`tools/list` JSON payload; tokens β chars Γ· 3.6):
| Configuration | Tools | Payload | β Tokens |
|---|---:|---:|---:|
| **Discovery mode (default)** | **6** | **0.01 MB** | **~2k** |
| `HUBSPOT_TOOL_MODE=all` | 690 | 1.33 MB | ~370k |
| `all` + read-only | 304 | 0.38 MB | ~104k |
| `all` + CRM core presetΒΉ | 85 | 0.13 MB | ~36k |
| `all` + CRM core presetΒΉ + read-only | 33 | 0.04 MB | ~12k |
In discovery mode each `hubspot_get_endpoint` lookup adds only the schema the
model asked for: about 350 tokens for a typical endpoint, up to ~12k for the
largest CMS page bodies.
<sub>ΒΉ `HUBSPOT_INCLUDE_GROUPS=contacts,companies,deals,tickets,lists,properties,associations,pipelines,crm-owners,notes,tasks,calls,emails,meetings`</sub>
How to read that:
- **Discovery mode loses no coverage**: all 1,076 endpoints stay callable
through search β inspect β invoke, at the cost of one or two extra calls the
first time the model uses an endpoint.
- **Clients with tool search / deferred loading** (Claude Code, claude.ai
connectors) load tool definitions on demand, so `HUBSPOT_TOOL_MODE=all` works
well there; narrow it with a `HUBSPOT_INCLUDE_GROUPS` preset and/or
`HUBSPOT_READ_ONLY=true`.
- **Responses consume context too.** Cap outliers with
`HUBSPOT_MAX_RESPONSE_CHARS` (e.g. `40000`) and request only the
`properties` you need on CRM reads.
- Oversized inline schemas are already handled: the four pathological
recursive schemas (list filters, workflow definitions β ~1.7 MB *each*
fully inlined) are budget-pruned to β€24 KB with their top levels intact.
Input schemas also drop examples and any prose below the body's own fields;
every field, type, enum and required list stays.
## The power tools
Besides the generated endpoint tools, the server ships four hand-built ones:
- **`hubspot_get_capabilities`** π’ β the access report described
[above](#hub--plan-awareness). Returns the startup check without new API
calls; `refresh: true` runs it again.
- **`hubspot_graphql_query`** π’ β HubSpot's CRM GraphQL API
(`POST /collector/graphql`). Query-only by design (HubSpot exposes no
mutations), so it stays available even in read-only mode. Requires the
`collector.graphql_query.execute` scope and Marketing/Content Hub Pro+.
- **`hubspot_api_request`** π΄ β raw escape hatch for any path on the HubSpot
host (new betas, undocumented corners). Same auth injection, throttling and
retries. Hidden in read-only mode.
- **Discovery mode** (the default) β replaces the 687
endpoint tools with `hubspot_search_endpoints` β
`hubspot_get_endpoint` β `hubspot_invoke_endpoint` over the same registry.
All filters (groups, beta, read-only) still apply; in read-only mode the
invoke tool physically cannot reach a write because writes aren't in the
registry.
## Upgrading from 1.x
2.0 merges endpoints that HubSpot repeats per object type into one tool with a
selector argument. Every endpoint is still reachable and sends the same
request; only tool names and a few argument names change. Update saved
prompts, client allowlists and permission rules that name tools. Group keys
for `HUBSPOT_INCLUDE_GROUPS` / `HUBSPOT_EXCLUDE_GROUPS` (`contacts`, `deals`,
`pages`, β¦) are unchanged.
| 1.x tools | 2.0 tool | Arguments |
|---|---|---|
| `contacts_list`, `deals_list`, `tickets_list`, β¦ `custom_objects_list` | `crm_objects_list` | `objectType: "contacts"` (or `"deals"`, `"0-3"`, a custom `"2-12345"`, β¦) |
| `<type>_get`, `<type>_update`, `<type>_archive` (some were `<type>_delete`) | `crm_objects_get`, `crm_objects_update`, `crm_objects_archive` | `objectType`; the record ID is `objectId` instead of `contactId`, `dealId`, β¦ |
| `<type>_create`, `<type>_search`, `<type>_merge`, `<type>_batch_read` / `_create` / `_update` / `_upsert` / `_archive` | `crm_objects_create`, `crm_objects_search`, `crm_objects_merge`, `crm_objects_batch_*` | `objectType` |
| `partner_clients_associations_*`, `partner_services_associations_*` | `crm_objects_associations_list` / `_create` / `_delete` | `objectType: "partner_clients"` or `"partner_services"`, `objectId` |
| `pages_landing_*`, `pages_site_*` | `cms_pages_*`, e.g. `cms_pages_draft_push_live` | `pageType: "landing"` or `"site"` |
| `posts_blogs_*`, `authors_blogs_*`, `tags_blogs_*` for list, get, archive, batch read/update/archive and multi-language | `cms_blog_*`, e.g. `cms_blog_list` | `blogResource: "posts"`, `"authors"` or `"tags"` |
Unchanged: endpoints only one type has (`contacts_gdpr_delete`, landing page
folders, blog post drafts and revisions, blog create/update, whose bodies differ
per resource). `npm run list-tools -- --names` prints every tool with the endpoint
it calls. POST endpoints that publish, schedule or restore existing content are
now labelled π‘ *updates data* instead of *creates data*.
Two defaults change as well: `HUBSPOT_TOOL_MODE` is now `discovery` (set it to
`all` to expose the endpoint tools themselves), and the server runs the access
check at startup, which costs up to about 30 API calls per start
(`HUBSPOT_CAPABILITY_CHECK=false` turns it off).
## Run from source (stdio, no Docker)
Prefer the classic stdio mode for Claude Desktop? Build it locally:
```bash
npm install
npm run build
```
Then point Claude Desktop at the compiled entrypoint in
`claude_desktop_config.json`:
```json
{
"mcpServers": {
"hubspot": {
"command": "node",
"args": ["/ABSOLUTE/PATH/Hubspot-MCP/dist/index.js"],
"env": {
"MCP_TRANSPORT": "stdio",
"HUBSPOT_ACCESS_TOKEN": "pat-na1-β¦"
}
}
}
}
```
## Keeping the specs current
The bundled files under `spec/` are the source of truth for the tools. They are
fetched from **HubSpot's own public API index**
(`https://api.hubspot.com/public/api/spec/v1/specs`), which lists every public
API with per-version OpenAPI documents **and the hub/tier requirements** this
server bakes into tool descriptions. `spec/catalog.json` records what was
fetched (API, version, stage, beta flag, requirements, docs links).
To refresh against HubSpot's latest:
```bash
npm run fetch-specs # rewrites spec/*.json + spec/catalog.json
npm run build && npm run list-tools
```
New endpoints become new tools automatically β no code changes. The fetch
script prefers each API's **stable classic version** (v1/v3/v4) and falls back
to dated or beta versions when that's all HubSpot offers; a couple of APIs are
pinned to bundle multiple genuinely-different versions (OAuth v1 + v3,
Communication Preferences v3 + v4).
## Development
```bash
npm install
npm run build # compile TypeScript β dist/
npm test # run the Vitest suite (160 tests)
npm run list-tools # print the categorized tool catalog (no credentials needed)
npm run fetch-specs # refresh spec/ from HubSpot's public API index
```
CI builds and tests every push across Node 20 and 22; pushes to `main` also
publish a Docker image to the GitHub Container Registry.
## Notes & conventions
- **Transports**: `MCP_TRANSPORT=stdio` (default) for local launchers;
`MCP_TRANSPORT=http` for the always-on Streamable-HTTP server the Docker image
runs.
- **Paging**: CRM list tools use cursor paging β pass `limit` and the `after`
cursor from `paging.next.after`. Ask for the properties you need via the
`properties` parameter (arrays become repeated query params).
- **Selectors**: consolidated tools take `objectType` (`crm_objects_*`),
`pageType` (`cms_pages_*`) or `blogResource` (`cms_blog_*`). `objectType`
accepts names (`contacts`, `line_items`), objectTypeIds (`0-3`) and custom
object types (`2-12345`).
- **Search**: `crm_objects_search` and the other `*_search` tools take a JSON
`body` with `filterGroups`, `sorts`, `query`, `properties`, `limit` and
`after`. HubSpot caps search at ~5 req/s per token β the built-in search
throttle keeps you under it.
- **Batch tools** (`crm_objects_batch_read`, `crm_objects_batch_create`, β¦) are
the efficient way to touch many records β prefer them over loops of single
calls.
- **Associations**: use the v4 association tools (`associations_*`) to link
records, with optional labels via the association-schema tools.
- **File uploads**: multipart tools accept file fields as
`{"fileName": "report.pdf", "contentBase64": "β¦"}` (or `content` for plain
text); other fields are sent as regular form fields, objects as JSON strings.
- **Deletes are archives** for CRM objects (recycle bin, ~90 days) β the truly
permanent ones are the π΄ `gdpr_delete` purge tools and record merges.
- **Rate limits**: HubSpot enforces burst caps per 10 s (private apps:
100β200/10 s depending on plan; OAuth apps ~110/10 s) plus daily caps. The
server self-throttles at `HUBSPOT_MAX_REQUESTS` per window and retries any
`429` it still receives, honoring `Retry-After`. Check real usage anytime via
`hubspot_get_capabilities`.
- **Request bodies**: write tools take a `body` argument; its schema is resolved
from the spec and shown to the model (e.g. `crm_objects_create` expects
`{"properties": {β¦}}`).
- **Beta APIs** (developer preview / public beta) are included by default and
labelled β οΈ in descriptions; hide them with `HUBSPOT_INCLUDE_BETA=false`.
## Security
- Your access token lives only in `.env`, which is git-ignored. **Never commit
real secrets.** The token grants whatever its scopes allow β if it leaks,
expire it in **Settings β Integrations β Service Keys** (or **Private Apps**
for a legacy token).
- The HTTP endpoint refuses to start unauthenticated once it is bound beyond
this machine. Set `MCP_AUTH_TOKEN` (`openssl rand -hex 32`) and send it as an
`Authorization: Bearer <token>` header, ideally behind TLS. A loopback bind
still needs no token, and is additionally protected against DNS rebinding by
a `Host` header check.
- Destructive tools (delete / **merge** / **GDPR purge**) and **send** tools
(marketing & transactional email, sequences) carry the right annotations so a
well-behaved host prompts before acting β keep that confirmation on, or run
with `HUBSPOT_READ_ONLY=true`.
- Scope the blast radius at the source: grant the service key only the scopes
you actually need β the startup access check and `hubspot_get_capabilities`
tell you what's missing when you want more. The check itself only reads.
See [SECURITY.md](./SECURITY.md) for the full policy and how to report a
vulnerability.
## Credits & license
An unofficial community integration for [HubSpot](https://www.hubspot.com/);
not affiliated with or endorsed by HubSpot. Built on the
[Model Context Protocol](https://modelcontextprotocol.io). Tools are generated
from HubSpot's public OpenAPI definitions. Licensed under the
[MIT License](./LICENSE.md).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues