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.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/holmjames/hubspot-mcp-operator-demo'

If you have feedback or need assistance with the MCP directory API, please join our Discord server