> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstflow.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect an agent

> Point an MCP coding agent at FirstFlow's /mcp endpoint and complete the OAuth + PKCE consent flow so it can build and manage experiences as tools.

Connect a coding agent Claude Code, Cursor, or Claude Desktop to FirstFlow's MCP server so it can list agents, build experiences, and edit flows directly from your editor. You add one HTTP server URL; the client and FirstFlow negotiate access over OAuth automatically, and you approve a scoped consent screen in the dashboard.

The endpoint is `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.

<CodeGroup>
  ```bash Claude Code theme={null}
  claude mcp add --transport http firstflow https://api.firstflow.app/mcp
  ```

  ```json mcp.json (Cursor / Claude Desktop) theme={null}
  {
    "mcpServers": {
      "firstflow": {
        "type": "http",
        "url": "https://api.firstflow.app/mcp"
      }
    }
  }
  ```
</CodeGroup>

The first time the agent calls a tool, your browser opens the dashboard consent screen. Sign in, pick the organization, approve the scopes, and the agent is connected for that workspace.

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

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant C as Coding agent
    participant M as /mcp
    participant D as Dashboard consent
    participant T as /oauth/token
    rect rgb(245, 238, 255)
    C->>M: POST /mcp (no token)
    M-->>C: 401 + WWW-Authenticate (resource_metadata, as_uri)
    end
    rect rgb(232, 245, 233)
    C->>M: GET /.well-known/oauth-protected-resource
    C->>M: GET /.well-known/oauth-authorization-server
    C->>M: POST /oauth/register (client_name, redirect_uris)
    end
    rect rgb(255, 243, 224)
    C->>D: GET /oauth/authorize (PKCE S256)
    D-->>C: redirect with authorization code
    C->>T: POST /oauth/token (code + code_verifier)
    T-->>C: access token
    end
    rect rgb(232, 240, 254)
    C->>M: POST /mcp (Authorization: Bearer …)
    M-->>C: tool result
    end
```

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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`.
  </Step>
</Steps>

## 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](/mcp/api-keys) for the REST surface.

<Note>
  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.
</Note>

## 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 in `tools/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:

| Role   | Scopes                                                                                                 | Effect                                       |
| ------ | ------------------------------------------------------------------------------------------------------ | -------------------------------------------- |
| Admin  | All scopes                                                                                             | Full catalog create, update, publish, delete |
| Member | `agents:list`, `agents:read`, `experiences:list`, `experiences:read`, `audience:list`, `audience:read` | Browse and inspect; no mutations             |

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](/mcp/tools), and the role-to-scope mapping at [Scopes & roles](/mcp/scopes).

## Verify

Ask your agent to run a Discovery tool:

```text theme={null}
Call the FirstFlow MCP's list_agents tool and tell me which agents exist.
```

A returned agent list means the OAuth flow completed and your token is valid. A `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

Swap `https://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.

<AccordionGroup>
  <Accordion title="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](/self-hosting/configuration).
  </Accordion>

  <Accordion title="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](/self-hosting/overview).
  </Accordion>
</AccordionGroup>

## Next

With the agent connected, browse the [tool catalog](/mcp/tools) to see what it can build, review [scopes & roles](/mcp/scopes) to control who can do what, or manage credentials under [API keys](/mcp/api-keys).
