hubspot-mcp-server
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| ALLOW_CRM | No | Controls CRM tool registration. Values: none (default), read, write, all. With none, CRM tools are not registered and CRM paths are blocked. | none |
| LOG_LEVEL | No | Logging level (DEBUG, INFO, WARNING, ERROR). Defaults to INFO. | |
| ALLOW_PUBLISH | No | Controls which publishing tools are registered. Values: all (default), none, blog, pages, pages,blog. | all |
| DEFAULT_TIMEZONE | Yes | Default timezone used for scheduling and publishing (e.g., Europe/Berlin). | |
| HUBSPOT_API_BASE | No | HubSpot API base URL. Defaults to https://api.hubapi.com. The host must be allow-listed because the bearer token follows it. | |
| HUBSPOT_PORTAL_ID | Yes | Your HubSpot portal ID (e.g., 12345678). Used for portal-specific operations and shown in the connection test. | |
| HUBSPOT_ACCESS_TOKEN | Yes | Your HubSpot private app or service key token. Required to authenticate all API requests. | |
| HUBSPOT_MCP_ENV_FILE | No | Path 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
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| 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 |
| 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
( 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 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 |
| 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 |
| 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 |
| 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
|
| list_templatesA | List CMS templates available in the portal. Use this to find the 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 Args: limit: maximum results, 1-100. Default 100. |
| list_blog_authorsA | List blog authors, for setting 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 |
| 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
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 45 tools
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.
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.
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.
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.