<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 freshMcpServer 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:
mcp.json (Cursor / Claude Desktop style):
list_agents:
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
Self-hosting the MCP server
Self-hosting the MCP server
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 is missing or returns 403
A tool is missing or returns 403
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.Legacy and fine-grained API keys
Legacy and fine-grained API keys
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.Rate limits and metering
Rate limits and metering
/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.