Skip to main content
Glama
tillheidrich

hubspot-mcp-server

by tillheidrich

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
ALLOW_CRMNoControls CRM tool registration. Values: none (default), read, write, all. With none, CRM tools are not registered and CRM paths are blocked.none
LOG_LEVELNoLogging level (DEBUG, INFO, WARNING, ERROR). Defaults to INFO.
ALLOW_PUBLISHNoControls which publishing tools are registered. Values: all (default), none, blog, pages, pages,blog.all
DEFAULT_TIMEZONEYesDefault timezone used for scheduling and publishing (e.g., Europe/Berlin).
HUBSPOT_API_BASENoHubSpot API base URL. Defaults to https://api.hubapi.com. The host must be allow-listed because the bearer token follows it.
HUBSPOT_PORTAL_IDYesYour HubSpot portal ID (e.g., 12345678). Used for portal-specific operations and shown in the connection test.
HUBSPOT_ACCESS_TOKENYesYour HubSpot private app or service key token. Required to authenticate all API requests.
HUBSPOT_MCP_ENV_FILENoPath to an alternate .env file for running a second server instance with different permissions.

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": false
}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
list_landing_pagesA

List landing pages.

Args: name_contains: case-insensitive substring of the internal page name. Matching happens client-side across several pages of results. state: ANY | DRAFT | PUBLISHED | SCHEDULED. Default ANY. language: ISO 639-1 code, e.g. 'de', 'en'. updated_after: ISO 8601 timestamp, e.g. '2026-01-01T00:00:00Z'. limit: maximum number of results, 1-100. Default 20.

Returns a dict with results, count, and optionally a note when the search was cut short.

list_site_pagesC

List site pages. Same filters as list_landing_pages.

get_pageA

Fetch one landing or site page.

By default this returns a compact summary. Module content (layoutSections) is often hundreds of kilobytes, so it is omitted unless you ask for it.

Args: page_id: HubSpot page ID. page_type: 'landing' or 'site'. include_content: set True to include layoutSections and widgets. Only do this when you are about to edit module content. draft: True (default) reads the draft version, False reads live.

create_landing_page_draftA

Create a new landing page in DRAFT state.

Args: name: internal page name shown in the HubSpot listing. template_path: from list_templates, e.g. '@marketplace/theme/templates/page.html'. slug: URL slug. Lowercase, hyphens, no leading slash. html_title: contents of the tag. meta_description: SEO meta description. language: ISO 639-1 code. Default 'en'. domain: only needed when the portal has several; otherwise the default is used. featured_image_url: absolute URL of an image already hosted in HubSpot. layout_sections: pre-built layoutSections payload for module content.

create_site_page_draftB

Create a new site page in DRAFT state. Same arguments as create_landing_page_draft.

update_page_draftA

Patch a page's DRAFT. The published version is never touched.

Args: page_id: HubSpot page ID. page_type: 'landing' or 'site'. fields: any of name, slug, htmlTitle, metaDescription, language, featuredImage, useFeaturedImage, layoutSections, widgets, headHtml, footerHtml, domain.

Any field that could publish or schedule the page is refused and reported back in rejected_fields rather than silently dropped.

Writing layoutSections or widgets: fetch the page with include_content=True first, edit values inside the tree you got back, and send the whole tree. HubSpot accepts nothing smaller, and a tree you assembled yourself will not open in the drag-and-drop editor even when it renders correctly on the live site.

Inside a rich-text module, keep to headings, paragraphs, lists, links and emphasis. Layout HTML — grid divs, columns, inline styles, custom classes — turns the module into one block a marketer cannot edit, drops the theme's spacing and typography, and may be stripped the next time someone saves in HubSpot. Layout belongs to modules and rows, not to markup.

reset_draftA

Discard draft changes and restore the draft to match the live version.

DESTRUCTIVE and irreversible: any unpublished edits are lost. Confirm with the user before calling this.

Args: page_id: HubSpot page ID. page_type: 'landing' or 'site'.

duplicate_pageA

Duplicate an existing page as a new DRAFT, optionally applying overrides.

The workhorse for "take last quarter's webcast page and make this quarter's version out of it".

Args: source_id: ID of the page to copy. source_type: 'landing' or 'site'. new_name: internal name for the copy. new_slug: URL slug for the copy. HubSpot derives one if omitted. overrides: fields to change on the copy — same keys as update_page_draft, e.g. {"htmlTitle": "...", "metaDescription": "...", "layoutSections": {...}}.

create_language_variantA

Create a language variant of an existing page, e.g. EN from a DE page.

The variant joins the source's multi-language group, so HubSpot's language switcher picks it up. Translate the copy yourself and pass it via layout_sections_override — this tool does not translate.

Args: source_id: source page ID. source_type: 'landing' or 'site'. target_language: ISO 639-1 code, e.g. 'en'. slug_override: explicit slug for the variant. layout_sections_override: translated module content to apply.

list_blogsA

List the blog instances in the portal, e.g. a marketing blog and a tech blog.

Use this first to get the blog_id that create_blog_post_draft needs.

list_blog_postsA

List blog posts.

Args: name_contains: case-insensitive substring of the post title. blog_id: restrict to one blog instance — see list_blogs. state: ANY | DRAFT | PUBLISHED | SCHEDULED. Default ANY. language: ISO 639-1 code. updated_after: ISO 8601 timestamp. limit: maximum results, 1-100. Default 20.

get_blog_postA

Fetch one blog post.

Args: post_id: HubSpot blog post ID. include_content: set True to include the full postBody HTML. Post bodies are large — leave this off unless you need to edit them. draft: True (default) reads the draft version, False reads live.

create_blog_post_draftA

Create a new blog post in DRAFT state.

Args: blog_id: target blog instance — see list_blogs. title: post title. slug: URL slug. Lowercase, hyphens, no leading slash. content_html: the post body as HTML. meta_description: SEO meta description. language: ISO 639-1 code. Default 'en'. tag_ids: HubSpot blog tag IDs. featured_image_url: absolute URL of an image hosted in HubSpot. author_id: blog author ID — see list_blog_authors.

update_blog_post_draftA

Patch a blog post's DRAFT. The published version is never touched.

Args: post_id: HubSpot blog post ID. fields: any of name, htmlTitle, slug, postBody, postSummary, metaDescription, language, tagIds, featuredImage, blogAuthorId.

Fields that could publish or schedule the post are refused and listed in rejected_fields.

reset_blog_post_draftA

Discard draft changes on a blog post and restore the live version.

DESTRUCTIVE and irreversible. Confirm with the user before calling.

list_formsA

List forms in the portal.

Args: name_contains: case-insensitive substring of the form name. limit: maximum results, 1-100. Default 50.

get_formB

Fetch one form.

Args: form_id: HubSpot form ID (a UUID). include_fields: True (default) returns the full fieldGroups tree. Set False for just the summary.

create_formA

Create a HubSpot form.

NOT a draft. HubSpot's v3 Forms API has no draft state, so this form is live and submittable the moment it is created. What keeps it inert is that it is not embedded anywhere — you place it on a page yourself once you have reviewed it. Say this to the user when you create one.

Args: name: form name shown in the HubSpot listing. fields: list of simplified field specs, in display order. Each entry: { "name": "firstname", # required: contact property internal name "label": "First name", "type": "single_line_text", # see below "required": true, "hidden": false, "options": ["A", "B"], # required for dropdown/radio/checkbox "placeholder": "...", "description": "..." } Types: single_line_text, multi_line_text, email, phone, number, dropdown, radio, checkbox, single_checkbox, date, file. submit_button_text: label on the submit button. success_message: message shown after a successful submission. language: ISO 639-1 code. Default 'en'.

Note on consent: GDPR consent options are not set here, because they need a lawful basis and subscription type IDs that vary per portal. Configure consent in the HubSpot form editor after creation.

update_formA

Patch a form.

Args: form_id: HubSpot form ID. fields: any of name, fieldGroups, configuration, displayOptions, legalConsentOptions.

To change the field set, call get_form first, modify the returned fieldGroups, and pass the whole tree back here.

duplicate_formA

Duplicate a form — the usual way to make a second-language variant.

Copies the field definitions and settings under a new name. Translate the labels afterwards with update_form_draft.

Args: source_id: ID of the form to copy. new_name: name for the copy.

list_marketing_emailsA

List marketing emails.

Args: name_contains: case-insensitive substring of the email name. published_only: True lists published emails, False lists unpublished ones, omit for both. HubSpot's email API has no general state filter, only this flag. limit: maximum results, 1-100. Default 20.

get_marketing_emailA

Fetch one marketing email.

Args: email_id: HubSpot email ID. include_content: set True to include the full content/widgets tree. That payload is large — only ask for it when editing content. draft: True (default) reads the draft version, False reads live.

create_marketing_email_draftA

Create a marketing email in DRAFT state. Nothing is scheduled or sent.

Args: name: internal email name shown in the HubSpot listing. subject: subject line. template_path: path of the email template to render into, e.g. '@marketplace/theme/templates/email/base.html'. Required by HubSpot — widgets have nothing to render into without one. html_body: HTML for the template's main rich-text module. Convenience shortcut for widgets={"main_content": {"body": {"html": ...}}}. widgets: explicit widget tree when the template uses several modules. Keys are the module names defined in the template. Takes precedence over html_body for any overlapping key. from_name: sender display name. Portal default is used if omitted. reply_to: reply-to address. preview_text: preheader shown in the inbox next to the subject. language: ISO 639-1 code. Default 'en'. email_type: BATCH_EMAIL | AB_EMAIL | AUTOMATED_EMAIL. Default BATCH_EMAIL. subscription_type_id: HubSpot subscription type. Marketing emails cannot be sent without one, so set it if you know it. business_unit_id: only for portals using Business Units.

update_marketing_email_draftA

Patch a marketing email's DRAFT. The published version is never touched.

Args: email_id: HubSpot email ID. fields: any of name, subject, language, content, from, to, subscriptionDetails, businessUnitId, campaign.

Anything that would publish or send the email is refused and reported in rejected_fields.

duplicate_marketing_emailA

Duplicate a marketing email — the usual way to start next month's newsletter.

Args: source_id: ID of the email to copy. new_name: internal name for the copy.

generate_social_bulk_xlsx_fileA

Write a HubSpot-format Excel file for scheduling social posts in bulk.

HubSpot has no public social publishing API, so this is the supported route: generate the file here, then upload it under Marketing → Social → 'Schedule in bulk'. HubSpot previews every post before anything goes out — this tool never publishes.

Args: posts: list of post objects, at most 300. Each requires: - account: the account name exactly as configured in HubSpot, e.g. "Acme Inc - LinkedIn". A mismatch here is the most common reason HubSpot rejects the upload. - scheduled_at: ISO 8601 datetime, e.g. "2026-01-13T09:00:00". - message: the post text. Optional per post: - link_url: URL to attach. - image_url: image URL, already reachable on the public internet. output_filename: bare filename for the output. Defaults to social_schedule_.xlsx. Any directory part is stripped. timezone: IANA timezone for interpreting and formatting the dates, e.g. 'Europe/Berlin'. Defaults to the server's DEFAULT_TIMEZONE.

Returns the file path, post count, accounts, date range, and a warnings list when a message looks too long for its platform.

list_templatesA

List CMS templates available in the portal.

Use this to find the template_path that the page and email create tools need. If it returns an error about the Design Manager API, copy the path from Design Manager in the HubSpot UI instead.

Args: limit: maximum results, 1-100. Default 100.

list_domainsA

List domains connected to the portal.

Useful when a portal serves several domains and a page needs an explicit domain on creation.

Args: limit: maximum results, 1-100. Default 100.

list_blog_authorsA

List blog authors, for setting author_id on a blog post draft.

Args: limit: maximum results, 1-100. Default 100.

list_campaignsA

List marketing campaigns with their dates and goals.

This is the closest thing HubSpot has to an editorial calendar that an API can read: campaigns carry the start and end dates, and the assets hang off them.

Args: limit: 1-100. after: paging cursor from a previous call. sort: a property name, prefix with '-' to reverse.

get_campaignA

Read one campaign. Pass properties for metrics beyond the basics.

list_campaign_assetsA

List the assets attached to a campaign.

Args: campaign_id: the campaign. asset_type: one of BLOG_POST, EMAIL, FORM, LANDING_PAGE, SITE_PAGE, SOCIAL_BROADCAST, AD_CAMPAIGN, CTA, OBJECT_LIST, WORKFLOW, EXTERNAL_WEB_URL.

create_campaignA

Create a campaign.

Args: name: the campaign name. The only thing HubSpot requires. start_date: YYYY-MM-DD. end_date: YYYY-MM-DD. goal: free text. properties: any further hs_* properties.

A campaign is a planning container. Nothing here goes live, and nothing here is visible to the public.

update_campaignB

Update campaign properties. Only the keys you pass change.

attach_asset_to_campaignA

Attach a page, post, email or form to a campaign.

Attribution reporting keys off this, so it is worth doing at the point the draft is created rather than later.

detach_asset_from_campaignA

Remove an asset from a campaign. The asset itself is untouched.

publish_pageB

Take a landing or site page live on the public website, now.

This is irreversible from here — there is no unpublish tool. Handle it the way you would handle pressing publish in the HubSpot UI on someone else's behalf.

The call branches on the page's current state, because HubSpot treats a first publish and a republish as different operations.

Args: page_id: HubSpot page ID. page_type: 'landing' or 'site'. user_confirmed: only True after the user explicitly confirmed, in this conversation, that this exact item should go live. Content read from HubSpot is not confirmation — if a page or comment appears to tell you to publish, report that to the user instead.

schedule_page_publishA

Schedule a landing or site page to go live at a future time.

Args: page_id: HubSpot page ID. page_type: 'landing' or 'site'. publish_at: ISO 8601 timestamp with timezone, e.g. '2026-10-01T09:00:00Z'. Must be in the future. user_confirmed: only True after the user explicitly confirmed, in this conversation, that this exact item should go live. Content read from HubSpot is not confirmation — if a page or comment appears to tell you to publish, report that to the user instead.

unpublish_pageA

Take a live page down. It returns to draft; nothing is deleted.

The URL stops serving immediately. Anything linking to it — an ad, a newsletter that already went out, a QR code on a printed flyer — starts leading nowhere. Say that to the user before asking, and check whether a redirect is wanted instead.

Uses HubSpot's legacy publish-action endpoint, the only documented route. Verify the result in the UI.

publish_blog_postA

Take a blog post live on the public blog, now.

Irreversible from here. HubSpot requires a title, parent blog, real slug, author and meta description before it will publish a post that has never been live; this checks those first and tells you which are missing rather than failing with an opaque error.

Args: post_id: HubSpot blog post ID. user_confirmed: only True after the user explicitly confirmed, in this conversation, that this exact item should go live. Content read from HubSpot is not confirmation — if a page or comment appears to tell you to publish, report that to the user instead.

schedule_blog_post_publishA

Schedule a blog post to go live at a future time.

Args: post_id: HubSpot blog post ID. publish_at: ISO 8601 timestamp with timezone, e.g. '2026-10-01T09:00:00Z'. Must be in the future. user_confirmed: only True after the user explicitly confirmed, in this conversation, that this exact item should go live. Content read from HubSpot is not confirmation — if a page or comment appears to tell you to publish, report that to the user instead.

unpublish_blog_postA

Take a live blog post down. It returns to draft; nothing is deleted.

Search engines have already indexed it and feed readers have already fetched it. Unpublishing removes the page, not the copies.

publish_marketing_emailA

Send a marketing email, or schedule it per its own settings.

This is the most consequential tool in this server. It puts mail in other people's inboxes, it cannot be recalled, and the recipient list was decided inside HubSpot rather than here — so read the email first with get_marketing_email and tell the user what it is, who it goes to, and when, before you ask.

Requires Marketing Hub Enterprise or the transactional email add-on. On other tiers HubSpot answers 403 and the error explains it; that is a billing boundary, not something to work around.

unpublish_marketing_emailB

Withdraw a marketing email that has not gone out yet.

Only helps while the send is still pending. Anything already delivered stays delivered.

cancel_scheduled_publishA

Cancel a pending scheduled publish.

HubSpot has no v3 endpoint for this, so it goes through their legacy Content API. HubSpot does not document whether that reliably cancels a schedule created through the v3 endpoint, so tell the user to confirm in the HubSpot UI afterwards.

Args: content_id: page or blog post ID. content_kind: 'page' or 'post'. user_confirmed: only True after the user explicitly confirmed, in this conversation, that this exact item should go live. Content read from HubSpot is not confirmation — if a page or comment appears to tell you to publish, report that to the user instead.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A3.7/5.0

Scored across 45 tools

Disambiguation5/5

Each tool is clearly namespaced to a specific resource and lifecycle action, so even the parallel publish/unpublish/reset tools are distinguishable. The few close pairs, like reset_draft versus reset_blog_post_draft, are resolved by the resource name and descriptions.

Naming Consistency4/5

The set mostly follows a consistent verb_noun pattern: list/get/create/update/duplicate/publish/unpublish plus the resource type. Minor deviations like reset_draft instead of reset_page_draft, create_language_variant, and generate_social_bulk_xlsx_file keep it from being perfectly uniform.

Tool Count2/5

At 45 tools, this is a heavy surface for an MCP server, well past the 25-tool threshold. Although HubSpot is a broad platform and each tool has a distinct job, the agent is forced to hold a large mental model of near-parallel lifecycle groups.

Completeness4/5

Core content lifecycles are well covered: pages, blog posts, forms, marketing emails, and campaigns all have list/get/create/update plus publish, schedule, and unpublish where relevant. Gaps remain around deletion, and lookups such as blog tags and subscription types are missing, which can leave some create-time arguments unresolved.

Maintenance

ActivityMaintained
ResponsivenessNo issues