Breeze MCP servers
Host an OAuth-protected MCP service on a public Cloudflare Worker, register its URL and public OAuth client ID in HS-X, then upload the generated HubSpot mcp-server component.
TL;DR — hs-x init --type mcp-server scaffolds an app that registers an external OAuth-protected MCP service with HubSpot Breeze. You host the service itself on a public Cloudflare Worker, bootstrap a public OAuth client whose redirect is HubSpot’s fixed MCP callback, paste its client id into mcpServer(...), and hs-x deploy uploads the generated mcp-server component. Private apps roll out directly; marketplace apps are gated behind a HubSpot feature flag until Ecosystem approval.
Scaffold the HubSpot registration
hs-x init my-breeze-server --type mcp-serverThe template adds mcpServer(...), forces auth: "oauth", and pins platformVersion: "2026.09-beta", the minimum version on which HubSpot’s mcp-server component exists — which is why this guide’s config differs from the 2026.03 every other guide uses. Replace the clearly marked URL and public client-ID placeholders before upload. hs-x init --type mcp-server --auth static is rejected because HubSpot Breeze MCP components require OAuth.
The declaration registers an external remote MCP service with HubSpot. It is not @hs-x/mcp: that separate package is a local stdio coding-agent server for validating and inspecting an HS-X project.
import { defineApp, mcpServer } from '@hs-x/sdk';
export default defineApp({
name: 'Customer data MCP',
distribution: 'private',
auth: 'oauth',
platformVersion: '2026.09-beta',
scopes: [],
mcpServers: [mcpServer('customer-data', {
uid: 'customer-data',
name: 'Customer data',
description: 'Read-only customer lookup.',
mcpUrl: 'https://mcp.example.com/mcp',
mcpClientId: 'public-client-id-from-bootstrap',
requiredScopes: ['demo.read'],
version: '1.0.0',
})],
});See the HubSpot component documentation for the platform contract.
Use the modern Cloudflare handler
The reference service HS-X was built against uses stateless createMcpHandler from agents/mcp/server with @modelcontextprotocol/server@2. Do not start new work with the deprecated McpAgent API. The reference app also declares a non-public HS-X card-backend capability solely so hs-x deploy provisions the app runtime that owns HubSpot’s separate install OAuth callback; no card or workflow component is emitted for it.
@cloudflare/workers-oauth-provider wraps /mcp, serves OAuth authorization-server and protected-resource discovery, advertises the example scopes, and requires PKCE S256 with allowPlainPKCE: false. It does not configure a dynamic client-registration endpoint. Instead, an admin-secret-protected, one-time bootstrap route calls OAuthHelpers.createClient() with HubSpot’s fixed redirect URI:
https://oauth-redirect.hubspot.com/callback/mcp_serverCopy the generated public client ID into mcpClientId. Never commit the admin secret or a developer API key.
One requirement HubSpot's connection flow enforces that is easy to miss: your authorization server must issue refresh tokens. HubSpot requires the refresh_token grant, and if your provider gates refresh tokens behind an offline_access scope, that scope has to appear in the component's requiredScopes. An authorization server that cannot issue refresh tokens is not supported by HubSpot's MCP connection flow, and it is the most common reason a Test connection fails after the authorization redirect succeeds.
The security properties the reference service carries, and yours should too:
- PKCE S256 required on every authorization request.
- Verified OAuth props carried into MCP tool execution, with the declared
demo.readscope enforced. - A harmless read-only tool as the first thing Breeze can call.
- Bounded request parsing and constant-time secret comparison.
- No global request state.
- CSP and anti-clickjacking headers, plus CSRF protection on the consent form.
- A demo identity/consent shortcut that fails closed unless explicitly enabled, and is development-only; production needs a real identity provider.
Current implementation references: handler API, remote server guide, and workers-oauth-provider.
Localhost is not an MCP URL HubSpot can call
HubSpot cannot reach localhost. For development, put ADMIN_SECRET and the explicitly development-only ALLOW_DEMO_CONSENT=true in an uncommitted .dev.vars, run wrangler dev, and expose it with a Cloudflare Quick Tunnel:
cloudflared tunnel --url http://localhost:8787Call the client-bootstrap route through the tunnel. Local Wrangler KV/DO state is separate from deployed Worker storage, so copy both the public client ID returned from that local environment and the tunnel HTTPS origin into hsx.config.ts (keeping /mcp). Quick Tunnel URLs are ephemeral and development-only; rerun hs-x deploy whenever the hostname or local OAuth state changes. Production uses a stable workers.dev or custom domain and a real identity provider.
Install the app and add the tools to Breeze
The HubSpot app install and the external MCP authorization are two different OAuth flows. hs-x deploy provisions the managed app runtime and its HubSpot install callback; the external service recognizes the public mcpClientId and redirects MCP authorization to HubSpot’s fixed MCP callback.
Projects on 2026.09-beta use HubSpot's release-management flow. If the project does not auto-deploy, hs-x deploy stops after the build succeeds and prints Next: hs project release create --build=<id>; run that command to make the build live before you install.
After both Workers are deployed and the placeholders are replaced:
- Open the HubSpot project, click the app, and copy its install link from Distribution.
- Install the app in the target account.
- In Development → Projects, open the MCP server component and click Test connection, then Connect.
- Open Agent Hub → Agent Builder, configure an agent, click Add tool → MCP Servers, then click Connect and add for your server.
- Invoke the harmless example tool before adding tools that read or mutate production data.
Private apps and Marketplace apps roll out differently
Private apps do not use the Marketplace approval gate. Deploy the valid MCP component to the private app and test it on the intended portal.
For a Marketplace-distributed mcp-server, HubSpot automatically creates hs-release-mcp-server with app-level defaultState OFF. Before Ecosystem approval, only up to five INSTALLED portals may receive ON overrides. Approval and flag state are separate facts: an approval does not itself prove a portal override is ON, and an ON override does not mean the component is approved. After Ecosystem approval, the app-level defaultState may be changed to ON.
Use HS-X’s existing HubSpot developer API surface with the developer API key in the HSX_HUBSPOT_DEVELOPER_API_KEY environment variable — not source code or a command argument. To enable an installed test portal before approval:
export HUBSPOT_APP_ID='1234567890'
hs-x api hubspot \
"feature-flags/2026-03/${HUBSPOT_APP_ID}/flags/hs-release-mcp-server/portals/batch/upsert" \
-X POST \
'portalStates:=[{"portalId":46993937,"flagState":"ON"}]'After Ecosystem approval, broad release changes the app-level default:
hs-x api hubspot \
"feature-flags/2026-03/${HUBSPOT_APP_ID}/flags/hs-release-mcp-server" \
-X PUT defaultState=ONThe portal must already have the app installed, and the preapproval limit is five ON overrides. Recheck HubSpot’s feature-flags guide before operating the beta; the API and component remain subject to change.
Ecosystem approval controls whether a Marketplace app may move beyond five installed test portals. hs-release-mcp-server controls actual component availability. Private apps are outside that Marketplace approval gate.
