https://api.firstflow.app/mcp (or <BACKEND_PUBLIC_URL>/mcp when self-hosting). Transport is streamable HTTP via @modelcontextprotocol/sdk, and access is gated by OAuth 2.0 + PKCE following RFC 9728. There is no token to paste the first unauthenticated request triggers the whole flow.
The complete connection
Add the server, then run any FirstFlow tool. The client handles discovery, registration, authorization, and token exchange on its own.How the OAuth handshake works
FirstFlow’s/mcp controller authenticates every request manually rather than behind a generic guard, so it can return the right WWW-Authenticate header on a 401. When a request arrives with no Authorization: Bearer … header, the server responds 401 with a header pointing at its OAuth metadata. A compliant MCP client reads that header and runs the rest of the flow without any manual configuration.
The mechanism is the standard RFC 9728 “OAuth protected resource” dance: discover the metadata, register a client dynamically, authorize with PKCE, exchange the code for a token, then call /mcp with that token as a bearer credential.
1
Discover the metadata
The client fetches
GET /.well-known/oauth-protected-resource and
GET /.well-known/oauth-authorization-server. Both URLs are derived from the
server’s issuer (OAUTH_ISSUER, falling back to BACKEND_PUBLIC_URL), which is
why self-hosters must set those values correctly.2
Register dynamically
The client calls
POST /oauth/register with a client_name and
redirect_uris. No pre-shared client secret is required registration is
dynamic, so each tool registers itself on first use.3
Authorize with PKCE and consent
The client opens
GET /oauth/authorize with a PKCE S256 challenge, which
redirects to the dashboard consent screen. You sign in, pick the
organization, and approve the requested scopes. The organization you pick
determines which agents and experiences the connection can touch.4
Exchange the code for a token
The client calls
POST /oauth/token with grant_type=authorization_code and
the code_verifier that matches its PKCE challenge. The response is an access
token the client sends as Authorization: Bearer … on every /mcp request.
To disconnect, the client can POST /oauth/revoke.Why static API keys are rejected here
Keys you create under Settings → API Keys do not work on/mcp. The controller verifies the bearer token, and if it resolves to an ordinary API key one not issued through the OAuth flow it returns a 401 whose error_description tells the client to use OAuth instead. Only keys minted by the MCP OAuth flow (tagged issuedVia: "mcp-oauth", or named with an oauth: prefix) are accepted.
This is deliberate: OAuth ties the connection to a specific user, organization, and an explicitly approved scope set, whereas a pasted static key carries whatever scopes it was created with and no consent step. Those static keys still work everywhere else they are only refused on /mcp. See API keys for the REST surface.
Dashboard and internal callers can also reach
/mcp with a session JWT. In
that case scopes are resolved from your stored organization-member role, not
from a token’s scope list. For an external coding agent you will always use the
OAuth flow above.What the agent can see and do
Once connected, the agent’s available tools are not fixed they are computed per request from your scopes, which derive from your organization role. The server builds a fresh, stateless MCP server instance for each HTTP request and only registers the tools you are allowed to call. Tools you lack scope for never appear intools/list, and calling one returns the SDK’s native “method not found” rather than leaking its existence.
Admins (org:admin or admin) get the full catalog. Members (basic_member or member) get read-and-list access only:
The Discovery tool group is always registered regardless of policy, so any connected agent can bootstrap context but the individual Discovery tools still enforce their own scopes (for example
get_organization_settings requires org:admin, and list_agents requires agents:list). A missing tool or a 403 with code MCP_FORBIDDEN_SCOPE means your role lacks the scope, not that the feature is gone. The full tool list lives at Tool catalog, and the role-to-scope mapping at Scopes & roles.
Verify
Ask your agent to run a Discovery tool:401 means the handshake never finished open the dashboard and approve the consent screen, then retry. A 403 means you are connected but your role lacks the scope for that tool; reconnect as an admin or have an admin run it.
Self-hosting
Swaphttps://api.firstflow.app/mcp for your backend’s public origin. The OAuth issuer and consent redirect are derived from two environment variables, so both must be set correctly or the handshake breaks midway.
Required environment for the OAuth flow
Required environment for the OAuth flow
Set
BACKEND_PUBLIC_URL to the backend’s public origin the discovery
metadata URLs and the issuer come from it. Set FRONTEND_URL to the
dashboard’s origin so the /oauth/authorize redirect resolves to a reachable
consent screen. If you need a different issuer than BACKEND_PUBLIC_URL, set
OAUTH_ISSUER explicitly. See
Self-hosting configuration.Rate limits and quotas
Rate limits and quotas
The
/mcp endpoint is rate-limited and metered per organization. In the
community edition quotas are unlimited, so a self-hosted deployment will not
hit a usage ceiling. See Self-hosting overview.