# 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

```sh
hs-x init my-breeze-server --type mcp-server
```

The 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.

```ts
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](https://developers.hubspot.com/docs/apps/developer-platform/add-features/mcp-server) 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:

```text
https://oauth-redirect.hubspot.com/callback/mcp_server
```

Copy 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.read` scope 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](https://developers.cloudflare.com/agents/model-context-protocol/apis/handler-api/), [remote server guide](https://developers.cloudflare.com/agents/model-context-protocol/guides/remote-mcp-server/), and [`workers-oauth-provider`](https://github.com/cloudflare/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:

```sh
cloudflared tunnel --url http://localhost:8787
```

Call 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:

1. Open the HubSpot project, click the app, and copy its install link from **Distribution**.
2. Install the app in the target account.
3. In **Development → Projects**, open the MCP server component and click **Test connection**, then **Connect**.
4. Open **Agent Hub → Agent Builder**, configure an agent, click **Add tool → MCP Servers**, then click **Connect and add** for your server.
5. 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:

```sh
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:

```sh
hs-x api hubspot \
  "feature-flags/2026-03/${HUBSPOT_APP_ID}/flags/hs-release-mcp-server" \
  -X PUT defaultState=ON
```

The portal must already have the app installed, and the preapproval limit is five ON overrides. Recheck HubSpot’s [feature-flags guide](https://developers.hubspot.com/docs/api-reference/latest/app-management/feature-flags/guide) before operating the beta; the API and component remain subject to change.

**Keep the concepts separate**

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.

**Where next**

- [Agent tools: expose a worker handler to Breeze without hosting a separate MCP service](/docs/guides/agent-tools)
- [Marketplace: the listing review a marketplace-distributed MCP server goes through](/docs/guides/marketplace-listing)
- [Secrets: where the admin secret and developer API key live](/docs/guides/secrets)

