Skip to main content
Glama
holmjames

HubSpot MCP Operator

by holmjames

HubSpot MCP Operator (demo)

A Model Context Protocol server that lets an AI agent author HubSpot automation end-to-end — build a workflow, set who gets enrolled, wire up the steps, define the conversion goal, and segment contacts into lists — entirely through tool calls, with every mutation verified by reading state back.

About this repo. This is a sanitized, standalone reconstruction of an internal tool I built and ran in production against a live HubSpot portal. All organization-specific data — real object IDs, list/workflow IDs, customer segments, credentials — has been removed. It targets only HubSpot's standard objects and ships with an .env.example you point at your own test portal. It builds, boots, and registers all 24 tools (verified); running the write tools requires your own HubSpot private-app token.


Why this exists

HubSpot's UI is the normal way to build a marketing workflow. But "build a workflow" is exactly the kind of multi-step, schema-heavy task an agent should be able to do — if it has tools that are safe to hand to a model.

The hard part isn't calling the API. It's making the tools trustworthy enough to act autonomously:

  • A 200 OK from HubSpot does not mean the thing you intended actually happened. The flows API will accept a malformed filter and silently store it as "always false." A list filter can be saved and quietly match zero records.

  • A model that fuzzy-matches "the onboarding workflow" and mutates the wrong flow is a disaster.

  • The v4 flows API is read-modify-write: to change one field you must PUT the entire flow back, minus the fields the API rejects on write.

This server is built around those realities.

Related MCP server: hubspot-mcp-server

Design principles

1. Every tool returns the same envelope — and writes verify themselves.

{
  "ok": true,
  "operation": "workflows.set_enrollment_criteria",
  "data": { /* request echo + the re-fetched object */ },
  "audit": {
    "attempted": true,
    "verified": true,        // ← we re-read the object and confirmed the change
    "targetType": "workflow",
    "targetId": "1234567890",
    "targetName": "Trial → Activation Nurture"
  }
}

audit.verified is the contract. A write tool performs the mutation, then re-fetches the object and checks the change is actually present before claiming success. create reads the new record back; delete re-lists to confirm the object is gone; lists.members.add re-reads each record's memberships. An agent can branch on audit.verified instead of trusting a status code.

2. Mutations require an unambiguous target. Workflow write tools resolve their target by ID or by exact (case-insensitive) name only. No partial match ever selects a flow to mutate — if the name isn't unique, the tool returns the candidate list and refuses to act.

3. Read-modify-write is encapsulated. sanitizeWorkflowForUpdate / sanitizeWorkflowForCreate strip the server-managed fields (id, revisionId, timestamps) that HubSpot rejects on write, so a single field change round-trips the whole flow safely. Callers just say "set these actions" or "set this enrollment criteria."

4. Failure modes are distinguished. "You sent a bad request" and "this portal can't do this via the API" are different signals to an agent. workflows.add_go_to_workflow_step detects the unsupported-step failure and flags unsupported_via_api: true rather than returning a generic error.

5. Raw filter branches pass straight through. Workflow enrollment, workflow goals, and list filters all share HubSpot's nested AND/OR filterBranch shape. The tools don't invent a DSL on top — they pass the raw branch through, so the full expressiveness of HubSpot filters is available. (See the gotcha below for how to validate one.)

Tool surface (24 tools)

Workflows (automation/v4/flows)

Tool

What it does

workflows.search

List / find by ID, exact name, or partial query

workflows.get

Full flow detail

workflows.create_manual

Create an empty, disabled flow shell

workflows.set_enrollment_criteria

Set who is enrolled (raw enrollmentCriteria branch)

workflows.set_actions

Replace the action graph; auto-derives startActionId

workflows.set_goal_criteria

Set the conversion goal (goalFilterBranch)

workflows.rename / set_enabled / delete

Lifecycle, by ID or exact name

workflows.clone_basic

Copy criteria + steps into a new disabled flow

workflows.add_go_to_workflow_step

Append a cross-workflow jump (flags unsupported_via_api)

Lists (crm/v3/lists)

Tool

What it does

lists.search / lists.get

Find lists; get can read the filterBranch back

lists.create

Create a DYNAMIC (or MANUAL/SNAPSHOT) list with a raw filterBranch

lists.update_filters

Replace an existing dynamic list's filters in place (full replace)

lists.rename / lists.delete

Lifecycle (delete is a verified soft-delete)

lists.members.list / add / remove

Manual membership, verified per record

CRM (crm/v3/objects)

Tool

What it does

crm.search

Query / by-ID / structured filterGroups; count:true returns only the total

crm.get

One record with optional properties + associations

crm.update_properties

Patch a record, verified by readback

crm.associations.get

Associated records of another object type

A worked example: an agent builds a nurture from one prompt

"Enroll trial contacts who haven't activated into a 3-step nurture, and mark them converted when they activate."

crm.search { objectType: "contacts", count: true,           → total: 4,812  (sizing the audience)
             filterGroups: [{ filters: [
               { propertyName: "lifecyclestage", operator: "EQ", value: "trial" },
               { propertyName: "activated", operator: "NEQ", value: "true" } ] }] }

workflows.create_manual { name: "Trial → Activation Nurture" }          → id 1234567890, verified
workflows.set_enrollment_criteria { workflowId: "1234567890", ... }     → verified
workflows.set_actions { workflowId: "1234567890", actions: [email, delay, email, delay, email] }  → verified
workflows.set_goal_criteria { workflowId: "1234567890",                 → verified
             goalFilterBranch: { activated EQ "true" } }
workflows.set_enabled { workflowId: "1234567890", isEnabled: true }     → verified

Each step returns audit.verified: true only after the change is re-read from HubSpot. If set_enrollment_criteria had stored an "always false" branch, the agent could catch it before turning the flow on.

The filter-validation gotcha (the most useful thing I learned)

HubSpot will accept and store an invalid filterBranch with a 200, then silently treat it as always false — the workflow enrolls no one and gives no error. You cannot trust the write echo.

The reliable validation is to create a throwaway dynamic list with the same branch: a list returns precise per-filter validation errors and a real member count. Confirm the count is sane, then apply the branch to the workflow and delete the probe list. lists.create + lists.get + lists.delete exist partly to make this loop cheap.

Setup

npm install
cp .env.example .env        # then paste your HubSpot private-app token
npm run build               # compiles src/ -> dist/
npm start                   # runs the MCP server over stdio

Register it with any MCP client (e.g. Claude Desktop / Claude Code):

{
  "mcpServers": {
    "hubspot-operator": {
      "command": "node",
      "args": ["/absolute/path/to/hubspot-mcp-operator-demo/dist/server.js"]
    }
  }
}

⚠️ The write tools mutate real HubSpot objects. Point this at a test/sandbox portal, and scope the private-app token to only what you need.

Layout

src/
  server.ts     MCP bootstrap — registers all 24 tools, one envelope renderer
  hubspot.ts    the single fetch choke point + typed HubSpotApiError
  config.ts     .env loading + requireEnv
  types.ts      ToolEnvelope / ToolAudit — the shared contract
  utils.ts      envelopes, exact-match targeting, read-modify-write sanitizers
  workflows.ts  the 11 flow tools (the centerpiece)
  lists.ts      the 9 list tools, incl. per-record membership verification
  crm.ts        the 4 CRM tools, incl. count-mode search

Tech

TypeScript (strict, NodeNext ESM) · @modelcontextprotocol/server · zod for tool input schemas · HubSpot REST v3/v4. No build step beyond tsc.

License

MIT — see LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables AI clients to seamlessly take HubSpot actions and interact with HubSpot data, allowing users to create/update CRM records, manage associations, and gain insights through natural language.
    22
    10 npm
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    Enables 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.
    11
    MIT