Skip to main content
FirstFlow ships an MCP (Model Context Protocol) server so any MCP-compatible coding agent Claude Code, Cursor, Claude Desktop can read your product context and build experiences by calling tools. Instead of clicking through the dashboard, you tell the agent what you want (“survey users who hit the pricing page twice”) and it lists agents, inspects existing experiences and audience, then creates tours, surveys, announcements, and guides and publishes them. The server runs on the backend at <BACKEND_URL>/mcp for the managed cloud that is https://api.firstflow.app/mcp. Two things make it more than a remote function list: the connection is authenticated with OAuth 2.0 + PKCE (no copy-pasted secret), and the exact set of tools the agent sees is computed server-side from your role, so an agent can never call something you couldn’t do yourself in the dashboard.
Static API keys from Settings → API keys are intentionally rejected on /mcp MCP clients must use the OAuth flow. Those same keys still work on the REST API. See API keys for why.

How it works

The endpoint is stateless: the controller builds a fresh McpServer for every HTTP request, binds it to your resolved identity and scopes, answers the call, and tears the transport down. Nothing about your session is cached between calls. Transport is streamable HTTP (@modelcontextprotocol/sdk), so a single agent turn can stream a long tool result back without holding a socket open. Three things happen, in order, on every request to /mcp: 1. Authenticate. The controller reads the Authorization: Bearer … header and tries a session JWT first (in-product callers like the dashboard), then an API key. A key is only accepted if it was minted by the OAuth flow (metadata.issuedVia === 'mcp-oauth'); a key you created in Settings is bounced with a 401 whose WWW-Authenticate header tells the client to start OAuth. With no token at all, the response is a plain 401 advertising the OAuth resource metadata. 2. Resolve scopes. For a JWT, the server reads your OrganizationMember role and maps it to a scope set. For an OAuth key, the scopes ride on the key itself and any legacy coarse scopes are expanded to their granular equivalents. The result is a small context organization id plus a list of granted scopes that governs everything downstream. No organization context means a 403. 3. Build the tool surface. The service constructs an McpServer and wraps its registerTool method with your organization’s tool policy. Tools your org disabled, or that you lack the scope for, are never registered they don’t appear in tools/list, and a tools/call for them returns the SDK’s native “method not found”. This is why the catalog is the source of truth: eligibility is decided on the server, so a client cannot conjure a tool that your role forbids.

Why OAuth instead of a token

A pasted API key has to be created, scoped, and stored by hand, and it never expires on its own. The OAuth + PKCE flow (RFC 9728, “OAuth protected resource”) removes all of that: the client discovers the endpoints, registers itself, sends you to a consent screen in the dashboard, and receives a scoped token — with no secret ever living in a config file. You approve exactly what the agent may do, per organization, and can revoke it later. The end-to-end handshake is walked step by step in the quickstart.

What an agent can do

The tool surface splits into two jobs. Reading context is always partly available the Discovery group is forced on for every caller so an agent can bootstrap (list_agents, get_agent, browse the integrations catalog) before it knows anything about your workspace. Building and managing covers creating tours, surveys, announcements, and guides, editing flow steps and logic nodes, targeting audiences, and publishing all gated by scope. The full, grouped list with one-line descriptions lives in the tool catalog. The exact slice you get is decided by your scopes, which come from your role.

Roles map straight to scopes

There are two role outcomes. An admin (org:admin) gets every scope and the complete catalog. A member (basic_member) gets read and list only — agents:list, agents:read, experiences:list, experiences:read, audience:list, audience:read so an agent acting as a member can survey your workspace but cannot change it. Granular scopes (experiences:create, experiences:publish, agents:delete, …) exist for fine-grained API keys; the two roles above are just preset bundles of them.

Connecting

Point your client at the /mcp URL and let it run the OAuth handshake; you only intervene to approve the consent screen. For Claude Code:
For a generic mcp.json (Cursor / Claude Desktop style):
To confirm the connection, ask the agent to call list_agents:
A returned agent list means you are authenticated. A 401 means the OAuth flow has not completed open the dashboard and approve the consent screen. The full discovery → register → authorize → token sequence, including self-host URL overrides, is in the quickstart.

Specific cases

The OAuth issuer and consent redirect are derived from your environment: set BACKEND_PUBLIC_URL to the backend’s public origin and FRONTEND_URL to the dashboard’s, so the /oauth/consent redirect resolves. Set OAUTH_ISSUER only if you need the issuer to differ from the backend origin. Then use your own <BACKEND_URL>/mcp in the client config above. See self-hosting configuration.
A tool that never appears in tools/list is either disabled for your organization or outside your scopes it was skipped at registration time. A 403 with MCP_FORBIDDEN_SCOPE means a call reached a tool your role can’t use. Neither means the feature is broken; check your role and the org’s enabled tools. Members are read-only by design.
Keys minted before the granular permission system carry coarse scopes (experiences:write, agents:write, analytics:read). The server expands these to their granular equivalents on every request, so old keys keep working experiences:write, for example, becomes experiences:create + experiences:update + experiences:publish. A key with no valid MCP scope is rejected with MCP_KEY_NO_SCOPES.
/mcp is rate-limited per caller and metered per organization. In the self-hosted community edition, quotas are unlimited. There is no per-request session to keep alive each call is independent so retries are safe but still count toward the rate limit.

Next

Walk the OAuth handshake and connect your first client in the quickstart, then browse the full tool catalog and the scopes and roles that gate it.