hubspot-pipeline-mcp
Read-only integration with HubSpot CRM that exposes pipeline analysis tools: finding stalled deals by stage and idle days, detecting duplicate contacts (exact email or company plus fuzzy name), summarizing pipelines by stage with conversion and win rates, finding deals missing required fields, comparing rep stage-to-won conversion against the team, and fetching a single deal's details and tripped checks, each with USD value at risk.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@hubspot-pipeline-mcp Which proposal-stage deals have gone quiet, and how much is at risk?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Ask Claude which deals are stuck: an MCP server over HubSpot, read-only
hubspot-pipeline-mcp lets Claude answer pipeline questions straight from a HubSpot CRM.
Ask "which proposals have gone quiet?" and Claude calls a tool, gets compact JSON with a USD value
at risk, and answers in plain English.
It reads. It never writes. Every tool is read-only by design (see Why read-only).
It runs in two modes:
Fixture mode (default): a synthetic HubSpot snapshot ships in the repo. No account, no token.
Live mode: your HubSpot portal, through a private-app token with read scopes only.
The checks and the dollar logic come from my Revenue Leak Engine, which found $881,884 leaking from a synthetic $1.0M pipeline. This repo puts three of those checks, plus three new pipeline views, behind an MCP server, so you can ask instead of open a dashboard.
Synthetic data only. The fixture snapshot is built from the Revenue Leak Engine's generated sample (seed 42, as of 2026-10-31). Every company, person and deal is made up. No real CRM data.
Tools
Tool | What it answers | USD at risk |
| Which open deals in this stage have had no activity for N days? | Sum of their amounts (measured) |
| Which contacts are the same person twice? Email exact, plus company + fuzzy name. | Open deals on the duplicate: pipeline counted twice (measured) |
| By stage: count, value, how many reached it, rate to the next stage, win rate from it | Stalled deals per stage (measured) |
| Which deals have empty required fields? | Open amount if the contact has no email (can't be worked); other gaps $0 |
| Stage-to-won rate by owner, against the rest of the team | Gap x closed deals x average won amount (estimated) |
| One deal: stage, owner, history, contacts, and every check it trips | Largest single flag, so nothing is counted twice |
Each result says whether its dollar figure is measured (real deal amounts) or estimated
(a win-rate gap), in a usd_basis field. Lists are capped by limit; totals always cover every match.
Same numbers as the Revenue Leak Engine
In fixture mode the tools reproduce the leak engine's results on the same data. The tests check this.
Leak | Revenue Leak Engine | This server |
Deals stalled at proposal | 40 deals, $454,200 |
|
Duplicate leads | 90 duplicates, $84,850 |
|
Weak rep (proposal to won) | Rep R02, $198,933 |
|
The duplicate pairs also match the leak engine's answer key one for one.
Related MCP server: BackCrew HubSpot MCP Server
Demo
Claude Desktop config (claude_desktop_config.json). Fixture mode, no token needed:
{
"mcpServers": {
"hubspot-pipeline": {
"command": "uv",
"args": ["--directory", "C:/path/to/hubspot-pipeline-mcp", "run", "hubspot-pipeline-mcp"],
"env": { "HUBSPOT_MODE": "fixtures" }
}
}
}Three questions, with the real tool output from fixture mode. The server sends compact JSON; it's pretty-printed and trimmed here so you can read it.
1. "Which proposals have gone quiet, and how much is at stake?"
Claude calls find_stalled_deals(stage="Proposal", days_idle=30, limit=3):
{
"check": "stalled_deals",
"as_of": "2026-10-31",
"rule": "open deals in stage 'Proposal' with no activity for 30+ days",
"count": 40,
"usd_at_risk": 454200.0,
"usd_basis": "measured: sum of the stalled deals' amounts (pipeline at risk)",
"deals": [
{"id": "500533", "name": "Clearpoint Goods", "owner": "Danielle Johnson", "stage": "Proposal",
"amount_usd": 42550.0, "last_activity": "2026-07-20", "days_idle": 103},
{"id": "500476", "name": "Riverpoint Supply", "owner": "Jeffery Wagner", "stage": "Proposal",
"amount_usd": 34150.0, "last_activity": "2026-05-14", "days_idle": 170},
{"id": "500071", "name": "Aspenspring Market", "owner": "Patricia Miller", "stage": "Proposal",
"amount_usd": 29100.0, "last_activity": "2026-04-12", "days_idle": 202}
],
"truncated": true,
"mode": "fixtures"
}What it means: 40 proposals worth $454,200 have had no activity for a month or more. Start with Clearpoint Goods ($42,550, quiet for 103 days).
2. "Is any rep converting proposals well below the team?"
Claude calls rep_conversion(from_stage="Proposal") (four middle reps trimmed):
{
"check": "rep_conversion",
"as_of": "2026-10-31",
"rule": "closed deals that entered 'Proposal', won / closed per owner, vs the rest of the team; flag at z <= -3.0",
"average_won_amount_usd": 9414.22,
"reps": [
{"owner": "Joshua Walker", "closed": 60, "won": 11, "win_rate": 0.1833,
"rest_of_team_rate": 0.5355, "z": -4.76, "flagged": true, "usd_at_risk": 198933.28},
{"owner": "Robert Johnson", "closed": 35, "won": 25, "win_rate": 0.7143,
"rest_of_team_rate": 0.4038, "z": 3.42, "flagged": false, "usd_at_risk": 0.0}
],
"flagged": ["Joshua Walker"],
"usd_at_risk": 198933.28,
"usd_basis": "estimated: closed deals x (team rate - rep rate) x average won amount. A correlation, not proof the rep caused the gap.",
"mode": "fixtures"
}What it means: Joshua Walker wins 18% of proposals; the rest of the team wins 54%. If he matched the team, that's about $199K more won. It's an estimate, so it's a reason to review his deals, not a verdict.
3. "Are duplicate contacts inflating the pipeline?"
Claude calls find_duplicate_contacts(limit=2):
{
"check": "duplicate_contacts",
"as_of": "2026-10-31",
"rule": "same mailbox (case and dots ignored), or same company with a matching name",
"count": 90,
"by_match_type": {"email_exact": 73, "company_name_fuzzy": 17},
"duplicates_with_open_deals": 12,
"usd_at_risk": 84850.0,
"usd_basis": "measured: open deals opened on a duplicate contact (pipeline counted twice)",
"duplicates": [
{"duplicate_id": "101975", "original_id": "101358", "match_type": "email_exact",
"reason": "same email address", "name": "mary wiley", "company": "Embermore Coffee, Inc.",
"email": "marywiley@embermorecoffee.com", "open_deal_ids": ["500599"], "usd_at_risk": 17750.0},
{"duplicate_id": "101961", "original_id": "100956", "match_type": "email_exact",
"reason": "same email address", "name": "B. Mcknight", "company": "Iron Stone Yoga",
"email": "brycemcknight@ironstoneyoga.com", "open_deal_ids": ["500597"], "usd_at_risk": 13100.0}
],
"truncated": true,
"mode": "fixtures"
}What it means: 90 contacts are duplicates. 12 of them carry an open deal, so $84,850 of pipeline is counted twice. Merge those first.
Setup
You need Python 3.10+ and uv (or plain pip).
git clone https://github.com/RynoJor/hubspot-pipeline-mcp.gitcd hubspot-pipeline-mcp && uv sync --extra devuv run pytestClaude Desktop
Add the JSON block from Demo to claude_desktop_config.json and restart Claude Desktop.
Use the full path to your clone. For live mode, change HUBSPOT_MODE to live. Keep the token
in .env, not in this file.
Claude Code
claude mcp add hubspot-pipeline -e HUBSPOT_MODE=fixtures -- uv --directory /path/to/hubspot-pipeline-mcp run hubspot-pipeline-mcpThen ask: "Use hubspot-pipeline. Which deals are stuck?"
Fixture mode vs live mode
Fixture mode | Live mode | |
Set with |
|
|
Data |
| Your HubSpot portal |
"Today" | 2026-10-31, the snapshot date | Today (UTC), or |
Needs | Nothing | A private app with read scopes |
Both modes go through the same code. The snapshot is shaped like HubSpot's v3 API responses (pipelines, owners, contacts, deals with contact associations), so fixture mode runs the same parsing and checks as live mode. A test also serves the snapshot through a fake HubSpot API, with paging, and gets the same numbers.
Live mode details. The client reads /crm/v3/pipelines/deals, /crm/v3/owners,
/crm/v3/objects/contacts and /crm/v3/objects/deals (with contact associations). It follows
paging.next.after cursors 100 records at a time. On HTTP 429 it waits for Retry-After (or backs
off 1s, 2s, 4s...) and retries, up to five times; 5xx errors are retried the same way. A 401 or 403
returns a message that says what to fix and never shows the token. One portal read is cached for
five minutes (HUBSPOT_CACHE_SECONDS).
Private app scopes: crm.objects.contacts.read, crm.objects.deals.read, crm.objects.owners.read.
Seeding a test account. scripts/seed_hubspot.py can push the same synthetic data into a
HubSpot developer test account, so live mode has something to read. It's a dry run unless you pass
--apply, it uses its own write token (HUBSPOT_SEED_TOKEN), and it refuses any portal that
HubSpot doesn't report as a developer test account or sandbox. After seeding, run the server with
HUBSPOT_PROFILE=seeded (see the script's docstring for why).
scripts/smoke_live.py runs pipeline_summary against the portal in .env and prints the result.
Why read-only
An AI assistant with write access to a CRM can do real damage: one bad tool call can close the wrong deal or merge the wrong contacts. Pipeline questions don't need write access, so this server doesn't have it. The limit is enforced three ways:
No write tools. All six tools only read. Each one is marked
readOnlyHint: trueanddestructiveHint: false, so MCP clients can tell.No write code. The HubSpot client has five public methods, all GET. Its request helper only sends GET. A test fails if anyone adds a public method.
No write scopes. The private app token gets read scopes only, so even a code change couldn't write.
The tools find the problem and put a dollar figure on it. A person decides what to do. Writing back (merging duplicates, nudging owners) would be a separate tool with a human approval step.
Limits
Synthetic data flatters the numbers. The leaks were planted by rules the checks encode. On a real portal, expect messier results and some false matches.
It reads the whole portal. Each refresh pulls every contact and deal. Fine for thousands of records; for a large portal you'd move to HubSpot's Search API with filters.
Rep estimates are correlation.
rep_conversiondollars assume the rep could match the team. Small samples swing. Use it to pick deals to review.Stage history comes from HubSpot. Conversion reads
hs_date_entered_<stage>, which HubSpot sets when a deal moves. Deals imported straight into a late stage have gaps.Last activity is
notes_last_updated, withcreatedateas a fallback. Portals that log activity elsewhere will look more idle than they are.Exact-email duplicates are rare in HubSpot. HubSpot blocks a second contact with the same email, so on a live portal most hits will be dot or case variants and name matches. The fixture keeps the leak engine's data as it is.
Missing fields are checked on deals. The leak engine's missing-field leak ($16,776) sits on leads that never became deals, so this tool doesn't see it. Also, the synthetic data has no expected close dates, so all 108 open deals report a missing
closedate. That's a real gap in the data, not a planted leak.USD only. Amounts use the
amountproperty. Multi-currency portals would wantamount_in_home_currency.Owners in seeded data. HubSpot's API can't create users, so seeded deals keep the rep's name in a custom property.
Duplicate labels differ from the leak engine. Here any same-mailbox match is
email_exact(73), even with a different name format; the leak engine called 40 "exact" (all three fields identical). The 90 pairs are the same.
Project layout
src/hubspot_pipeline_mcp/
server.py MCP server: six tools, read-only annotations, compact JSON
checks.py the checks and USD valuations (pure functions)
crm.py settings, fixture/live loading, parsing HubSpot JSON into records
client.py GET-only HubSpot v3 client: paging, 429/5xx retries, safe errors
data/snapshot.json synthetic fixture
scripts/
build_fixture.py rebuilds the snapshot from revenue-leak-engine/data/sample
seed_hubspot.py seeds a developer test account (dry run by default)
smoke_live.py live pipeline_summary check
tests/ 33 pytest tests, all in fixture mode or against a fake HubSpotBuilt with the official MCP Python SDK (MCPServer, the v2 name for FastMCP) and httpx.
Safety
No credentials in the repo.
.envis git-ignored;.env.examplehas empty values.Tests force fixture mode, even if
.envholds a token.Synthetic data only.
About
Built by Ryno Jordaan, GTM engineer, HubSpot Sales Hub certified. Related: Revenue Leak Engine · Portfolio · LinkedIn
MIT licence. See LICENSE.
Available Tools
6 toolsdeals_missing_fieldsDeals missing fieldsARead-onlyIdempotent
Deals with empty required fields, and which fields. An open deal whose contact has no email cannot be worked, so its amount is at risk; other gaps are routing/reporting costs, valued at $0.
Inputs: limit, fields, include_closed. Returns: result
find_duplicate_contactsFind duplicate contactsARead-onlyIdempotent
Duplicate contacts: the same mailbox (case and dots ignored), or the same company with a matching name ('Steve' ~ 'Steven', 'N. Galloway' ~ 'Nina Galloway').
usd_at_risk is the open deal value sitting on the duplicates (pipeline counted twice). Rows list duplicate_id, original_id, match_type, reason and open_deal_ids.
Inputs: limit. Returns: result
find_stalled_dealsFind stalled dealsARead-onlyIdempotent
Open deals sitting in a stage with no activity for days_idle+ days.
Returns count, usd_at_risk (sum of their amounts) and the largest deals first: id, name, owner, amount_usd, last_activity, days_idle.
Inputs: limit, stage, days_idle. Returns: result
get_dealGet dealARead-onlyIdempotent
One deal: stage, owner, amount, days idle, stage history, contacts, and every check it trips (stalled, duplicate contact, missing fields, weak rep), with usd_at_risk.
Inputs: deal_id (required). Returns: result
pipeline_summaryPipeline summaryBRead-onlyIdempotent
By stage: deal count, value, how many reached it, the rate to the next stage, the win rate from that stage, and stalled deals (idle 30+ days) as usd_at_risk. Plus pipeline totals: open value, won value, win rate.
Inputs: no named parameters. Returns: result
rep_conversionRep conversionARead-onlyIdempotent
Stage-to-won rate by owner: of each owner's closed deals that entered from_stage, the share
won, against the rest of the team. Flags owners at z <= -3 and estimates usd_at_risk as
closed deals x (team rate - rep rate) x average won amount.
Inputs: from_stage. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
Each tool targets a distinct diagnostic question (stalled deals, duplicates, stage funnel, missing fields, rep conversion, single-deal detail), so an agent can pick correctly in most cases. There is mild overlap: stalled deals appear both in find_stalled_deals and inside pipeline_summary, and get_deal re-surfaces every check, but the descriptions frame one as aggregate and the other as drill-down.
Names mix verb-first (find_stalled_deals, find_duplicate_contacts, get_deal) with noun-first (pipeline_summary, deals_missing_fields, rep_conversion). All are snake_case and readable, but there is no single predictable verb_noun convention an agent can rely on.
Six tools is well-scoped for a pipeline health/diagnostics server — one per distinct audit dimension plus a per-deal drill-down. No tool looks redundant or filler.
The read/diagnostic surface is strong and covers most health checks, but every workflow dead-ends: there are tools to find duplicate contacts or stalled deals yet none to merge, update, reassign, or log activity. An agent can report at-risk value but cannot remediate it.
Related MCP Connectors
Sales-rep jobs on your HubSpot CRM: daily brief, triage, call prep, forecast. Writes need approval.
The HubSpot MCP Server acts as a bridge that enables AI assistants and Large Language Models to securely interact with HubSpot CRM data through natural conversation, without requiring users to understand complex API structures. It provides read-only access to standard CRM objects (contacts, companies, deals, tickets, products, invoices, and more) and their associations, secured via OAuth 2.0, allowing AI agents to perform tasks like summarizing deals, fetching company updates, and looking up record changes.
Data Parrot brings AI revenue analysis of your HubSpot data into your AI tools.
Read-only sales CRM for AI clients: leads, pipeline, AI-agent chats, calls, ad attribution.
Related MCP Servers
- AlicenseBqualityCmaintenanceEnables Claude to interact with HubSpot CRM for sales analysis and insights, providing tools for managing contacts, deals, companies, and retrieving sales analytics data.179 npm11MIT
- AlicenseBqualityDmaintenanceEnables AI assistants to interact with a HubSpot CRM account via natural language, starting with read-only lookups and optionally enabling write operations like creating contacts, deals, and notes.11MIT
- AlicenseNot gradedqualityDmaintenanceExposes CRM tools from HubSpot to Claude and other agents, enabling pipeline analysis, deal risk detection, and automated follow-up drafting.704 npmMIT
- AlicenseAqualityCmaintenanceEnables Claude to search, create, and update contacts, deals, and support tickets in HubSpot, and to generate cross-object account summaries.10MIT