formbasedocs
Go to appApp

Developers

MCP server

The Model Context Protocol (MCP) server lets AI agents read and edit your formbase forms with rich, schema-driven tools.


What it is

MCP is an open standard for AI tools to connect to external services. formbase exposes a hosted MCP endpoint that any MCP-compatible client can connect to, including Claude Code, Claude desktop, and Cursor.

Connection

text
URL:  https://api.formbase.so/api/mcp   (POST, streamable HTTP)
Auth: Bearer <token>

Two kinds of bearer token work:

  • API token (fb_…) — created from API tokens. Best for personal use and quick setup.

  • OAuth access token (fbo_…) — issued by the OAuth flow. Best for third-party apps connecting on behalf of a user.

Both are bound to exactly one workspace and reach the same tools. A tool call that names another workspace, or a form in one, fails with FORBIDDEN. OAuth tokens also carry scopes (mcp:read, mcp:write, offline_access), but no tool is gated on them today — treat any token as full access inside its workspace.

Tool calls are limited to 120 per minute per token, shared with the API: only tools/call spends the budget, while initialize, tools/list, prompts/ and resources/ are free. Over budget, the call still returns HTTP 200 with a failed tool result carrying RATE_LIMITED and a retryAfterMs — poll on a timer, never in a loop.

Where to get a token

Open OAuth and API Keys in your workspace sidebar to create API tokens and view connected OAuth apps. See API tokens for a step-by-step guide.

Core tools

Every tool is advertised on tools/list when a client connects. Clients that load schemas on demand, like Claude Code, fetch a tool’s full schema when a task needs it. The table below covers the core tools most tasks start with; load_tools (catalogs) and load_skill (domain guides) document the rest.

ToolWhat it does
form_listList forms in a workspace. Supports folder filter, fuzzy name search, and cursor pagination.
form_getGet full details for a form: questions, cover, logo, and a live preview URL.
form_createCreate a new empty form in a workspace. Returns a preview URL for live editing.
form_updateUpdate form metadata: name, folder, emoji, cover, or logo.
form_deleteSoft-delete a form (moves to trash, revokes share links).
form_publishPublish a form so it can accept responses. Idempotent.
workspace_listList all workspaces accessible by your token.
workspaceFolder_listList folders in a workspace.
formSubmission_listList submissions for a form with pagination. Includes drafts on Pro and Business; Free lists completed responses only.
fields_listList the field keys a request can address on a published form, each with its value type, option keys, and a usage line. Call it before request_create.
request_createAssign a published form to one named recipient: prefill, locked fields, context, delivery, expiry, and an optional callback.
request_getRead one request: status, timeline, and — once completed — answers keyed by field key, plus display.
request_listList requests for a form or a whole workspace, filtered by status, outcome, or your own external id.
editor_getDocumentGet the full document structure of a form: all elements, their types, and properties.
editor_updateElementEdit an existing form element: replace its text, change properties (title, required, etc.), or reposition it.
editor_deleteElementRemove an element from the form.
editor_insertTextQuestionInsert a short or long text question. Each question type has its own insert tool with a precise schema.
editor_insertContactQuestionInsert an email, phone number, or website URL question.
editor_insertNumberQuestion · editor_insertDateQuestionInsert a number or date question.
editor_insertRadioQuestion · editor_insertCheckboxQuestion · editor_insertSelectQuestionInsert single-choice (radio), multi-choice (checkbox), or dropdown questions.
editor_insertDecisionQuestionInsert the decision question: the approve / decline / changes choice whose answer becomes a request outcome. A radio built by hand never produces one.
editor_insertRatingQuestion · editor_insertLinearScaleQuestionInsert a star rating or a linear scale question.
editor_insertHeader · editor_insertParagraph · editor_insertImage · editor_insertPageDividerInsert non-question content: headings, paragraphs, images, and page breaks.
load_toolsLoad documentation for a tool catalog. Returns schemas and usage patterns for grouped tools.
load_skillLoad a domain knowledge guide (themes, question types, logic rules, etc.).

More insert variants — time, file upload, signature, payment, matrix/grid, ranking, picture choice, toggle switch, table, list, row, calculated field, hidden field, inline variable, embedded content (editor_insertEmbedded for YouTube, Google Maps, or iframe embeds), and a conditional-logic block (editor_insertLogic) — are on tools/list too. Load load_skill(“question-types”) for the full set, each with its tool name and fields. Conditional logic is authored with editor_setLogic in the editor-actions catalog.

Tool catalogs

These tools are on tools/list as well. Run load_tools with a catalog name to get enriched documentation (intro, full schemas, usage patterns, edge cases) for the grouped tools, then call them directly.

CatalogTools included
form-dataformAnalytics_get — aggregate metrics (views, submissions, completion rate, breakdowns by device, country, browser, source). Needs the Pro or Business plan; without it the call fails with a message naming the plan.
form-appearanceformTheme_get, formTheme_set, form_update — per-mode themes (light and dark), plus the cover and logo on form_update.
form-behaviorformSettings_get, formSettings_update — notification emails, completion redirect, password, retention, language, payment.
form-sharingformShareLink_list, formShareLink_create, formShareLink_update — share link CRUD with custom domain support.
form-translationstranslationLanguage_list, translationDraft_get, translationDraft_update, translationDraft_publish, translationLanguage_delete — multilingual draft-then-publish workflow. Publishing an empty draft is the only way to unpublish a language.
form-lifecycleform_unpublish, form_restore — lifecycle ops beyond the core publish and delete.
workspace-managementworkspaceFolder_create, workspaceFolder_update, workspaceFolder_delete — folder CRUD beyond the core list verbs.
editor-actionseditor_formatText, editor_setLogic, editor_testLogic — text formatting, conditional-logic authoring, and logic simulation.
request-lifecyclefields_list, request_create, request_get, request_list, request_cancel, request_remind, request_replayCallback, document_create — the whole request surface: withdraw a pending request, chase the recipient, replay a callback that never landed, reserve a per-request document upload.
editor-insertsThe long tail of editor_insert* tools: time, switch, file, signature, documents block, matrix, ranking, payment, appointment, picture choice, embedded content, table, list, row, calculated field, hidden field, repeating group, logic, and variable.

The request-lifecycle catalog lists all eight request tools because the in-app chat advertises a smaller core set. Over this endpoint all eight are already on tools/list, so what the catalog adds is documentation.

Skills (domain knowledge)

Skills are built-in guides the agent can load via load_skill. They provide domain knowledge that helps the agent make better decisions — not tool schemas, but design advice and field semantics.

SkillWhat it covers
question-typesEvery question type, its fields, and when to use each one.
logic-rulesConditional logic: operators, actions, combinators, and edge cases.
editing-flowsPatterns for building forms: ordering, page breaks, piping.
form-best-practicesUX guidelines for effective form design.
form-themesTheme structure, token reference, and styling guidelines.
form-settingsSettings reference: notifications, email templates, variables.
analyticsMetrics definitions and how to interpret form analytics.
toon-formatCompact output format for structured data display.
requestsRequests end to end: field keys, prefill value shapes, delivery, per-request documents, callbacks, polling, and error recovery.

Requests

A request assigns one published form to one named recipient, with its own link, its own prefilled answers, and its own outcome. It is how an agent asks a real person for something and finds out what they said.

Creating a request

Always start with fields_list(formId). It returns the addressable keys of the form’s current published version, each with a usage line saying which argument the key belongs in — visible questions go in prefill, hidden fields in context. Never derive a key from a question title, and re-read after form_publish.

request_create
json
{
  "formId": "j57...",
  "recipient": { "email": "ada@acme.com", "name": "Ada" },
  "prefill": { "company_name": "Acme", "plan": "pro" },
  "readonly": ["company_name"],
  "context": { "crm_id": "A-42" },
  "metadata": { "run_id": "exec_918" },
  "delivery": "email",
  "expiresAt": 1780000000000,
  "callbackUrl": "https://hooks.acme.com/formbase",
  "idempotencyKey": "po-42"
}

The result carries the link and the clock:

result
json
{
  "id": "kd7...",
  "status": "pending",
  "url": "https://form.formbase.so/r/rq_...",
  "deliveryStatus": "queued",
  "expiresAt": 1780000000000,
  "createdAt": 1747000000000,
  "deduplicated": false,
  "next": "..."
}

delivery defaults to “none”, which hands you url to deliver yourself; “email” sends the invitation and needs recipient.email on a Pro or Business plan, or one of a Free account’s 10 free invitations. readonly locks fields the recipient may not edit, and every locked key must also be prefilled. context only takes hidden-field keys, while metadata is opaque bookkeeping echoed back on request_get and in the callback. expiresAt is epoch milliseconds, defaulting to 30 days out and capped at 365 days. idempotencyKey is workspace-scoped for 30 days: the same key with the same body returns the original request with deduplicated: true, and a different body is a conflict. Every response also carries a next line telling the agent what to do from here.

deliveryStatus is not_requested until an invitation is queued, then queued → sent or failed, and bounced once the mail provider reports a hard bounce or a complaint. Creating a request spends one unit of the workspace's monthly allowance whether or not the recipient ever answers; once it is gone, request_create fails with MONTHLY_ALLOWANCE_REACHED.

Callbacks or polling

With a callbackUrl, formbase POSTs once per terminal event — completion, expiry, cancellation — signed with the workspace request signing secret. See Callbacks and signing for the payload and the verification recipe.

Autonomous agents should poll

The request signing secret only ever appears on the Credentials page in your workspace — it is never returned over MCP or the API. An agent running on its own, with no human to stand up and configure a receiver, therefore cannot verify a callback. Leave callbackUrl off and poll request_get(requestId) instead, on the order of minutes rather than seconds, until status leaves “pending”. expiresAt bounds how long that is worth doing.

Test mode

Pass test: true to rehearse the whole shape before a real run. The link still opens and can be completed, and the callback fires with “test”: true — but nothing is emailed whatever delivery says, the request stays hidden from the Requests page and the analytics funnel, and its submission counts nowhere: no quota, no exports, no integrations. Test requests only appear in request_list when you pass includeTest: true. The link closes within 24 hours, and on Free a workspace may create 10 test requests a day.

Per-request documents

To hand one recipient a file — a contract draft, their own quote — the form needs a Documents block, which an author or an agent inserts with editor_insertDocumentsBlock. fields_list reports it as type: “documents”. The bytes never travel through a tool:

  1. Call document_create with formId, name, contentType, and the exact size in bytes. You get back { id, name, contentType, size, uploadUrl, expiresAt }. PDF and images only (no Office documents), 25 MB per file, and 100 MB of documents per request.

  2. PUT the raw bytes to uploadUrl within the hour, with Content-Type set to the type you declared.

  3. Reference it from request_create: documents: [{ documentId, field?, name? }]. field is the Documents block’s field key, optional only when the form has exactly one such block. name overrides the display name for this request.

The block’s authored documents stay put and yours appear below them, for this recipient only. request_create verifies the upload before the request exists, so DOCUMENT_NOT_UPLOADED means step 2 was skipped. One upload can be referenced by any number of requests, and its bytes count toward your workspace storage.

Custom domains

Pass domainId to request_create to mint the link on one of the workspace’s custom domains. The ids come from formShareLink_list, which returns them as availableCustomDomains. Omit it and the link takes whatever domain the form is already published under.

Reading results

request_get returns the whole request. Once it is completed, answers holds the recipient’s values keyed by field key, display the same keys as readable text, and outcome — approve, decline, or changes — is their verdict when the form has a decision question. A callbackFailedAt timestamp means delivery ran out of retries and nothing reached your endpoint; fix the receiver, then call request_replayCallback, which re-sends the original event id so your receiver dedupes instead of re-running. After the form’s retention policy strips a request, dataPurgedAt is set and the answers are gone for good.

request_list sweeps many at once, filtered by status, outcome, externalId, and includeTest. Page with nextCursor: a page can rarely come back with an empty items and hasMore: true, which is not the end of the list — pass the cursor back and keep going.

Resources and prompts

Every skill and tool catalog is also an MCP resource at skill://<name> — skill://requests, skill://editor-inserts. A client that supports resources/list can browse and read them without calling load_skill or load_tools. The server also serves four prompts on prompts/list: identity, capabilities, data_tools, and editor_tools.

Tools that ask first

Every tool carries the MCP hints readOnlyHint and destructiveHint, derived from its verb. These tools are marked destructive, because undoing them takes another call or is not possible: form_delete, form_unpublish, workspaceFolder_delete, editor_deleteElement, translationLanguage_delete, and request_cancel. Most clients ask the user before running them, but the prompt is the client’s decision, so check its approval settings if you need a hard stop.

Limitations

  • No binary uploads through a tool call. Images are set by URL: covers, logos, and image blocks accept http(s):// or data:image URIs. A per-request document is the exception: document_create returns an upload URL that a client with HTTP access can PUT the file to, as Per-request documents explains. To turn a PDF or screenshot into a form, use the built-in AI chat.

  • No workspace AI skills. Skills written in formbase are only available in the built-in AI chat. The server’s own skills (load_skill) are available over MCP.

Connecting with an API token

Most clients sign in with OAuth: add the URL with no header and follow Connect an AI agent. A client that cannot open a browser, such as a script, CI job, or headless agent, sends an API token as a header instead.

Claude Code, from the command line:

bash
claude mcp add --transport http formbase https://api.formbase.so/api/mcp \
  --header "Authorization: Bearer fb_YOUR_TOKEN"

Or in a project’s .mcp.json:

.mcp.json
json
{
  "mcpServers": {
    "formbase": {
      "type": "http",
      "url": "https://api.formbase.so/api/mcp",
      "headers": {
        "Authorization": "Bearer fb_YOUR_TOKEN"
      }
    }
  }
}

Cursor, in .cursor/mcp.json or ~/.cursor/mcp.json:

mcp.json
json
{
  "mcpServers": {
    "formbase": {
      "url": "https://api.formbase.so/api/mcp",
      "headers": {
        "Authorization": "Bearer fb_YOUR_TOKEN"
      }
    }
  }
}

To sign in with OAuth instead of a token, use Add to Cursor. It adds the server URL with no header, and Cursor asks you to sign in to formbase.

Other clients take the same URL and header; see their documentation for where.

Using OAuth instead of API tokens

A third-party app connecting on behalf of a user should use OAuth rather than ask for a pasted token. formbase is an OAuth 2.1 authorization server with mandatory PKCE (S256) and opaque tokens — no JWTs, no implicit grant. Claude desktop, Claude Code, and the Claude.ai web connector discover all of it from the MCP endpoint, so pasting the URL with no header is enough: the 401 points at /.well-known/oauth-protected-resource, and the client takes it from there.

The flow, for a client you are writing yourself:

  1. GET /.well-known/oauth-protected-resource, then GET /.well-known/oauth-authorization-server for the endpoint URLs, scopes, and supported auth methods.

  2. POST /oauth/register with your redirect_uris (dynamic client registration, no credentials needed). You get a client_id, plus a client_secret if you asked for anything other than token_endpoint_auth_method: “none”. Redirect URIs must be HTTPS, or HTTP on localhost. Registration is capped at 20 per hour per IP.

  3. Send the user to /oauth/authorize with response_type=code, your client_id, the registered redirect_uri, scope=mcp:read mcp:write offline_access, state, and a code_challenge with code_challenge_method=S256. They sign in, pick one workspace, and authorize.

  4. Exchange the code at POST /oauth/token with grant_type=authorization_code and your code_verifier, within 60 seconds. Codes are single use.

  5. Call the MCP endpoint with Authorization: Bearer fbo_…. Access tokens last 1 hour; refresh tokens last 30 days and rotate on every use. Reusing a spent refresh token burns the whole chain, so store the newest one.

POST /oauth/revoke (RFC 7009) revokes an access or refresh token. A user can also disconnect the whole app from Connected apps on the OAuth and API Keys page, which kills every token it holds for that workspace.

If you do have a token in hand, it goes in the same place as an API key:

MCP config with OAuth token
json
{
  "mcpServers": {
    "formbase": {
      "type": "http",
      "url": "https://api.formbase.so/api/mcp",
      "headers": {
        "Authorization": "Bearer fbo_USER_OAUTH_TOKEN"
      }
    }
  }
}

Connected apps

Every OAuth connection is listed under Connected apps on the OAuth and API Keys page, with when it was connected and last used. Connections are personal: only the user who authorized one sees it, and workspace admins cannot view or revoke another member’s. Disconnecting takes effect at once. A user who leaves or is removed from the workspace loses all their connections to it.

Next steps