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

# MCP Server

> Configure and use the Formbricks v3 Surveys MCP server

## Overview

The Formbricks MCP server exposes a small v3 Surveys tool surface for AI agents. It runs inside the
Formbricks web app at `/api/mcp` and reuses the same v3 Surveys API authentication, authorization,
rate limiting, response handling, and audit logging paths as the REST routes.

<Note>
  OAuth 2.1 is the preferred authentication path for MCP clients. Formbricks API keys remain supported as a
  backwards-compatible fallback for local development, self-hosters, and clients that do not support MCP OAuth yet.
</Note>

<Note>
  Looking for step-by-step setup guides for Claude Code, the Claude apps, and Codex? See
  [Connect AI agents (MCP)](/platform/mcp/overview). This handbook covers the technical internals.
</Note>

## Endpoint

Use the streamable HTTP MCP endpoint on the Formbricks app:

```text theme={null}
https://app.formbricks.com/api/mcp
```

For local development:

```text theme={null}
http://localhost:3000/api/mcp
```

The route supports `POST` requests, runs with the Next.js Node.js runtime, and sets private no-store
response headers. Browser-origin MCP requests must come from the configured Formbricks app origin.

OAuth clients discover the protected resource metadata here:

```text theme={null}
https://app.formbricks.com/.well-known/oauth-protected-resource/api/mcp
```

The metadata advertises `/api/mcp` as the protected resource and points clients to the Better Auth
issuer under `/api/auth`.

## Authentication

### OAuth 2.1

OAuth clients use Authorization Code + PKCE with Dynamic Client Registration. The MCP protected
resource is:

```text theme={null}
https://app.formbricks.com/api/mcp
```

The authorization server issuer is:

```text theme={null}
https://app.formbricks.com/api/auth
```

Discovery endpoints:

| Endpoint                                           | Purpose                              |
| -------------------------------------------------- | ------------------------------------ |
| `/api/auth/.well-known/oauth-authorization-server` | Better Auth authorization metadata   |
| `/api/auth/.well-known/openid-configuration`       | OpenID Connect metadata              |
| `/.well-known/oauth-authorization-server/api/auth` | RFC 8414 path-insertion alias        |
| `/.well-known/openid-configuration/api/auth`       | RFC 8414 OpenID path-insertion alias |
| `/.well-known/oauth-protected-resource`            | MCP protected resource metadata      |
| `/.well-known/oauth-protected-resource/api/mcp`    | MCP resource-specific metadata       |

Supported scopes:

| Scope            | Use                                                                     |
| ---------------- | ----------------------------------------------------------------------- |
| `surveys:read`   | `list_surveys`, `get_survey`, and read-only validation paths            |
| `surveys:write`  | `create_survey`, `patch_survey`, `delete_survey`, and write validations |
| `openid`         | OpenID Connect subject interoperability                                 |
| `profile`        | User profile claims                                                     |
| `email`          | User email claim                                                        |
| `offline_access` | Refresh tokens for MCP clients                                          |

The authorization server supports all of the above, but the MCP protected-resource metadata
(`/.well-known/oauth-protected-resource/api/mcp`) advertises only `surveys:read`, `surveys:write`,
and `offline_access` — the scopes an MCP client needs. Clients derive their Dynamic Client
Registration scopes from that list, so `offline_access` must be advertised there for clients to be
issued refresh tokens (`openid`/`profile`/`email` are OIDC scopes the MCP resource does not require).

OAuth scopes only gate MCP tool categories. Actual workspace access is still evaluated at tool
execution through the existing v3 authorization checks for the signed-in Formbricks user.

### API-key fallback

API-key MCP access remains supported. Authenticate with a Formbricks API key in a request header:

```http theme={null}
Authorization: Bearer fbk_...
```

or:

```http theme={null}
x-api-key: fbk_...
```

Do not pass credentials in the query string. The MCP route rejects query credential names such as
`api_key`, `x-api-key`, `access_token`, `token`, and `authorization` case-insensitively.

API key permissions are enforced through the same workspace access checks as the v3 REST API:

| Tool              | Minimum Workspace Permission                                        |
| ----------------- | ------------------------------------------------------------------- |
| `list_surveys`    | `read`                                                              |
| `get_survey`      | `read`                                                              |
| `create_survey`   | `write` or `manage`                                                 |
| `validate_survey` | `write` or `manage` when the validation request checks write access |
| `patch_survey`    | `write` or `manage`                                                 |
| `delete_survey`   | `write` or `manage`                                                 |

<Warning>
  Store fallback MCP API keys as environment variables or client secrets. Do not commit API keys into MCP client
  config files.
</Warning>

## Local Setup

Start Formbricks and prepare the database:

```bash theme={null}
pnpm db:up
pnpm db:migrate:dev
pnpm --filter @formbricks/web dev
```

Verify OAuth discovery:

```bash theme={null}
curl -sS http://localhost:3000/.well-known/oauth-protected-resource/api/mcp
curl -sS http://localhost:3000/.well-known/oauth-authorization-server/api/auth
```

OAuth-capable MCP clients should dynamically register a public client and request `resource` equal to
the MCP URL:

```text theme={null}
http://localhost:3000/api/mcp
```

Manual Dynamic Client Registration smoke test:

```bash theme={null}
curl -sS -X POST http://localhost:3000/api/auth/oauth2/register \
  -H "Content-Type: application/json" \
  --data '{
    "client_name": "MCP Inspector",
    "redirect_uris": ["http://127.0.0.1:6274/oauth/callback"],
    "token_endpoint_auth_method": "none",
    "grant_types": ["authorization_code", "refresh_token"],
    "response_types": ["code"],
    "scope": "openid profile email offline_access surveys:read surveys:write"
  }'
```

<Note>
  This passes `scope` explicitly, so it does **not** reproduce a real client's behavior — real MCP
  clients register with the scopes from the protected-resource metadata's `scopes_supported`
  (`surveys:read surveys:write offline_access`). Because the authorization server validates the
  `/authorize` request against the client's **registered** scopes, a client that registers with a
  narrower set than it later requests (e.g. it registers surveys-only but requests `offline_access`)
  is rejected with `invalid_scope`. To smoke-test the real path, omit `scope` and let the client
  adopt the advertised set, or pass exactly what the metadata advertises.
</Note>

The consent screen is served at `/account/authorize`. Users can revoke approved MCP clients from
`/account/settings/authorized-apps`.

For API-key fallback, create an API key in the Formbricks app with access to the target workspace.
Use the least privileged permission needed by the agent workflow.

Verify that the MCP endpoint can list tools:

```bash theme={null}
export FORMBRICKS_MCP_API_KEY="fbk_..."

curl -sS -X POST http://localhost:3000/api/mcp \
  -H "Accept: application/json, text/event-stream" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $FORMBRICKS_MCP_API_KEY" \
  --data '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```

## Codex Configuration

### OAuth

Add the HTTP MCP server. Include `--oauth-resource` so the OAuth access token is audience-bound to
the MCP resource URL:

```bash theme={null}
codex mcp add formbricks-local \
  --url http://localhost:3000/api/mcp \
  --oauth-resource http://localhost:3000/api/mcp
```

Authenticate and request the scopes the agent needs:

```bash theme={null}
codex mcp login formbricks-local \
  --scopes openid,profile,email,offline_access,surveys:read,surveys:write
```

For read-only usage, omit `surveys:write`:

```bash theme={null}
codex mcp login formbricks-local \
  --scopes openid,profile,email,offline_access,surveys:read
```

Codex opens the browser to Formbricks, completes Dynamic Client Registration, and stores the OAuth
client and tokens in Codex's credential store. The user approves the requested scopes on the
Formbricks consent screen.

Equivalent Codex config for the server entry:

```toml theme={null}
[mcp_servers.formbricks-local]
url = "http://localhost:3000/api/mcp"
oauth_resource = "http://localhost:3000/api/mcp"
```

Verify the registration:

```bash theme={null}
codex mcp list
codex mcp get formbricks-local
```

Then ask Codex to use the configured server:

```text theme={null}
Use the formbricks-local MCP server to list surveys for workspace clxx1234567890123456789012.
```

### API-key fallback

Store the API key in the shell environment:

```bash theme={null}
export FORMBRICKS_MCP_API_KEY="fbk_..."
```

For Codex Desktop on macOS, persist the value in the launch environment and restart Codex Desktop:

```bash theme={null}
launchctl setenv FORMBRICKS_MCP_API_KEY "fbk_..."
```

Register the local MCP server with a bearer-token environment variable:

```bash theme={null}
codex mcp add formbricks-local-api-key \
  --url http://localhost:3000/api/mcp \
  --bearer-token-env-var FORMBRICKS_MCP_API_KEY
```

## Claude Configuration

### Claude Code

#### OAuth

Add the remote HTTP MCP server:

```bash theme={null}
claude mcp add --transport http formbricks-local http://localhost:3000/api/mcp
```

Run `/mcp` and authenticate `formbricks-local`. Claude Code discovers the MCP protected-resource
metadata, registers a public client whose scopes are taken from that metadata's `scopes_supported`
(`surveys:read surveys:write offline_access`), and launches the browser-based OAuth flow — there is
no scope-entry step. You approve the requested scopes on the Formbricks consent screen. Workspace
role determines whether `surveys:write` tools actually succeed at execution time (see
[Authentication](#authentication)), regardless of the granted scope.

Verify it:

```bash theme={null}
claude mcp list
claude mcp get formbricks-local
```

For project-shared Claude Code configuration, use `.mcp.json` without static credentials:

```json theme={null}
{
  "mcpServers": {
    "formbricks-local": {
      "type": "http",
      "url": "http://localhost:3000/api/mcp"
    }
  }
}
```

If the installed Claude Code version does not start the OAuth flow for remote HTTP servers, use the
API-key fallback below until that client is updated.

#### API-key fallback

Set the API key:

```bash theme={null}
export FORMBRICKS_MCP_API_KEY="fbk_..."
```

Add the HTTP MCP server:

```bash theme={null}
claude mcp add --transport http formbricks-local-api-key http://localhost:3000/api/mcp \
  --header "Authorization: Bearer $FORMBRICKS_MCP_API_KEY"
```

For a project-shared fallback configuration, keep the API key outside git:

```json theme={null}
{
  "mcpServers": {
    "formbricks-local-api-key": {
      "headers": {
        "Authorization": "Bearer ${FORMBRICKS_MCP_API_KEY}"
      },
      "type": "http",
      "url": "http://localhost:3000/api/mcp"
    }
  }
}
```

Claude Code will fail to parse this fallback config if `FORMBRICKS_MCP_API_KEY` is not set.

### Claude Desktop

If the installed Claude Desktop build supports remote HTTP MCP server entries, add the same server
definition to the Claude Desktop config file and restart Claude Desktop. Prefer the OAuth entry
without headers when supported; use the API-key header entry only as a fallback.

macOS config path:

```text theme={null}
~/Library/Application Support/Claude/claude_desktop_config.json
```

Example config:

```json theme={null}
{
  "mcpServers": {
    "formbricks-local": {
      "type": "http",
      "url": "http://localhost:3000/api/mcp"
    }
  }
}
```

Fallback config with an API key:

```json theme={null}
{
  "mcpServers": {
    "formbricks-local-api-key": {
      "headers": {
        "Authorization": "Bearer ${FORMBRICKS_MCP_API_KEY}"
      },
      "type": "http",
      "url": "http://localhost:3000/api/mcp"
    }
  }
}
```

Set the API key in the Desktop launch environment before opening Claude Desktop when using the
fallback config:

```bash theme={null}
launchctl setenv FORMBRICKS_MCP_API_KEY "fbk_..."
```

If a Claude Desktop version only supports stdio MCP servers, use Claude Code for the HTTP endpoint or
add a local stdio-to-HTTP bridge.

## Tool Responses

Tool results include both `structuredContent` and a text content item containing the same JSON string.
Successful results mirror the v3 REST response body and add `requestId` for correlation.

Errors are returned as MCP error tool results with a structured v3 problem payload:

```json theme={null}
{
  "error": {
    "code": "bad_request",
    "detail": "Invalid survey document",
    "invalid_params": [
      {
        "name": "blocks.0.elements.0.headline",
        "reason": "Required"
      }
    ],
    "requestId": "req_...",
    "status": 400,
    "title": "Bad Request"
  }
}
```

## Available Tools

### list\_surveys

Lists surveys in one workspace. The tool is read-only and idempotent.

Input:

```json theme={null}
{
  "cursor": "opaque-cursor-from-previous-response",
  "filter": {
    "name": {
      "contains": "feedback"
    },
    "status": {
      "in": ["draft", "inProgress"]
    },
    "type": {
      "in": ["link"]
    }
  },
  "includeTotalCount": true,
  "limit": 20,
  "sortBy": "updatedAt",
  "workspaceId": "clxx1234567890123456789012"
}
```

Output:

```json theme={null}
{
  "data": [
    {
      "createdAt": "2026-04-21T10:00:00.000Z",
      "creator": {
        "name": "Ada Lovelace"
      },
      "id": "clsv1234567890123456789012",
      "name": "Product Feedback",
      "responseCount": 0,
      "status": "draft",
      "type": "link",
      "updatedAt": "2026-04-21T11:00:00.000Z",
      "workspaceId": "clxx1234567890123456789012"
    }
  ],
  "meta": {
    "limit": 20,
    "nextCursor": null,
    "totalCount": 1
  },
  "requestId": "req_..."
}
```

### get\_survey

Gets one survey by ID. The tool is read-only and idempotent.

Input:

```json theme={null}
{
  "lang": ["en-US"],
  "surveyId": "clsv1234567890123456789012"
}
```

`lang` is optional. When supplied, it filters translatable survey fields to the requested language
codes or configured aliases.

### create\_survey

Creates a block-based link survey using the v3 survey document contract. The tool writes data and is
not idempotent.

Input:

```json theme={null}
{
  "blocks": [
    {
      "elements": [
        {
          "headline": {
            "de-DE": "Was sollen wir verbessern?",
            "en-US": "What should we improve?"
          },
          "id": "satisfaction",
          "required": true,
          "type": "openText"
        }
      ],
      "name": "Main Block"
    }
  ],
  "defaultLanguage": "en-US",
  "endings": [],
  "hiddenFields": {
    "enabled": false
  },
  "languages": [
    {
      "code": "de-DE",
      "enabled": true
    }
  ],
  "metadata": {
    "cx_operation": "enterprise_onboarding",
    "title": {
      "de-DE": "Produktfeedback",
      "en-US": "Product Feedback"
    }
  },
  "name": "Product Feedback Survey",
  "status": "draft",
  "variables": [],
  "welcomeCard": {
    "enabled": true,
    "headline": {
      "de-DE": "Willkommen",
      "en-US": "Welcome"
    }
  },
  "workspaceId": "clxx1234567890123456789012"
}
```

Output uses the same survey resource shape as `GET /api/v3/surveys/{surveyId}` and includes the MCP
`requestId`.

### validate\_survey

Validates a create or patch payload without writing survey changes. The tool is read-only and
idempotent, but create validation still checks workspace write access when `workspaceId` is present.

Create validation input:

```json theme={null}
{
  "data": {
    "blocks": [
      {
        "elements": [
          {
            "headline": {
              "en-US": "What should we improve?"
            },
            "id": "satisfaction",
            "required": true,
            "type": "openText"
          }
        ],
        "name": "Main Block"
      }
    ],
    "defaultLanguage": "en-US",
    "name": "Product Feedback Survey",
    "workspaceId": "clxx1234567890123456789012"
  },
  "operation": "create"
}
```

Patch validation input:

```json theme={null}
{
  "data": {
    "name": "Updated Product Feedback Survey"
  },
  "operation": "patch",
  "surveyId": "clsv1234567890123456789012"
}
```

Validation failures return `200` with `valid: false` and structured `invalid_params`, matching
`POST /api/v3/surveys/validate`.

### patch\_survey

Updates a survey by ID using the v3 survey patch contract. The tool writes data, can be
destructive, and is not idempotent. Omitted top-level fields are preserved. Provided top-level
objects and arrays replace that whole subtree, so omitted nested entries inside a provided subtree
can be removed. The patch tool does not deep-merge nested objects and does not implement JSON Patch.

<Note>
  For agent workflows, fetch the current survey first, modify only the intended top-level fields, run
  `validate_survey` with `operation: "patch"`, and then submit the same patch with `patch_survey`.
</Note>

Input:

```json theme={null}
{
  "data": {
    "metadata": {
      "cx_operation": "enterprise_onboarding",
      "title": {
        "en-US": "Updated Product Feedback"
      }
    },
    "name": "Updated Product Feedback"
  },
  "surveyId": "clsv1234567890123456789012"
}
```

Output uses the same survey resource shape as `GET /api/v3/surveys/{surveyId}` and includes the MCP
`requestId`.

### delete\_survey

Deletes one survey by ID. The tool is destructive, writes data, and is not idempotent.

Input:

```json theme={null}
{
  "surveyId": "clsv1234567890123456789012"
}
```

The v3 REST delete operation returns `204 No Content`. The MCP tool result contains the `requestId`
so callers can correlate the audit trail:

```json theme={null}
{
  "requestId": "req_..."
}
```

## Relationship To V3 Surveys API

The MCP server does not run custom survey database queries. Each tool calls the shared server-only v3
survey operations used by the REST routes:

| MCP Tool          | REST Contract                       |
| ----------------- | ----------------------------------- |
| `list_surveys`    | `GET /api/v3/surveys`               |
| `get_survey`      | `GET /api/v3/surveys/{surveyId}`    |
| `create_survey`   | `POST /api/v3/surveys`              |
| `validate_survey` | `POST /api/v3/surveys/validate`     |
| `patch_survey`    | `PATCH /api/v3/surveys/{surveyId}`  |
| `delete_survey`   | `DELETE /api/v3/surveys/{surveyId}` |

When the v3 OpenAPI contract changes, update the MCP schemas and this page together. The hand-maintained
v3 OpenAPI spec lives at `docs/api-v3-reference/openapi.yml`.

## Limitations

* OAuth is user-delegated only. Machine-to-machine client credentials for MCP are not supported.
* OAuth access tokens must be JWTs audience-bound to `/api/mcp`; opaque MCP access token introspection is not
  accepted by the MCP route.
* API-key authentication remains supported for compatibility and fallback use.
* The MCP server exposes only the v3 survey operations listed on this page.
* Tool coverage depends on the current v3 Surveys REST endpoint coverage.
* `create_survey` creates link surveys only. In-app survey creation and distribution settings are not
  part of the current v3 create operation.
* `patch_survey` follows the v3 PATCH contract: top-level partial document updates only, no JSON Patch,
  and no nested deep merge. Provided top-level objects and arrays replace their whole subtree and can
  remove omitted nested entries.
* `validate_survey` validates payload shape and references; it does not create languages, survey
  versions, or surveys.
* Query-string credentials are rejected; use headers for API keys.
* Large request bodies are rejected before the MCP handler using the same body-size policy as v3 APIs.

## Implementation Files

* `apps/web/app/api/mcp/route.ts`
* `apps/web/modules/mcp/auth.ts`
* `apps/web/modules/mcp/server.ts`
* `apps/web/modules/mcp/tools/surveys.ts`
* `apps/web/modules/mcp/tools/schemas.ts`
* `apps/web/app/api/v3/surveys/lib/operations.ts`
