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
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 are free.
Over budget, the call still returns HTTP 200 with a failed tool result carrying resources/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.
| Tool | What it does |
|---|---|
| form_list | List forms in a workspace. Supports folder filter, fuzzy name search, and cursor pagination. |
| form_get | Get full details for a form: questions, cover, logo, and a live preview URL. |
| form_create | Create a new empty form in a workspace. Returns a preview URL for live editing. |
| form_update | Update form metadata: name, folder, emoji, cover, or logo. |
| form_delete | Soft-delete a form (moves to trash, revokes share links). |
| form_publish | Publish a form so it can accept responses. Idempotent. |
| workspace_list | List all workspaces accessible by your token. |
| workspaceFolder_list | List folders in a workspace. |
| formSubmission_list | List submissions for a form with pagination. Includes drafts on Pro and Business; Free lists completed responses only. |
| fields_list | List 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_create | Assign a published form to one named recipient: prefill, locked fields, context, delivery, expiry, and an optional callback. |
| request_get | Read one request: status, timeline, and — once completed — answers keyed by field key, plus display. |
| request_list | List requests for a form or a whole workspace, filtered by status, outcome, or your own external id. |
| editor_getDocument | Get the full document structure of a form: all elements, their types, and properties. |
| editor_updateElement | Edit an existing form element: replace its text, change properties (title, required, etc.), or reposition it. |
| editor_deleteElement | Remove an element from the form. |
| editor_insertTextQuestion | Insert a short or long text question. Each question type has its own insert tool with a precise schema. |
| editor_insertContactQuestion | Insert an email, phone number, or website URL question. |
| editor_insertNumberQuestion · editor_insertDateQuestion | Insert a number or date question. |
| editor_insertRadioQuestion · editor_insertCheckboxQuestion · editor_insertSelectQuestion | Insert single-choice (radio), multi-choice (checkbox), or dropdown questions. |
| editor_insertDecisionQuestion | Insert 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_insertLinearScaleQuestion | Insert a star rating or a linear scale question. |
| editor_insertHeader · editor_insertParagraph · editor_insertImage · editor_insertPageDivider | Insert non-question content: headings, paragraphs, images, and page breaks. |
| load_tools | Load documentation for a tool catalog. Returns schemas and usage patterns for grouped tools. |
| load_skill | Load 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.
| Catalog | Tools included |
|---|---|
| form-data | formAnalytics_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-appearance | formTheme_get, formTheme_set, form_update — per-mode themes (light and dark), plus the cover and logo on form_update. |
| form-behavior | formSettings_get, formSettings_update — notification emails, completion redirect, password, retention, language, payment. |
| form-sharing | formShareLink_list, formShareLink_create, formShareLink_update — share link CRUD with custom domain support. |
| form-translations | translationLanguage_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-lifecycle | form_unpublish, form_restore — lifecycle ops beyond the core publish and delete. |
| workspace-management | workspaceFolder_create, workspaceFolder_update, workspaceFolder_delete — folder CRUD beyond the core list verbs. |
| editor-actions | editor_formatText, editor_setLogic, editor_testLogic — text formatting, conditional-logic authoring, and logic simulation. |
| request-lifecycle | fields_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-inserts | The 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.
| Skill | What it covers |
|---|---|
| question-types | Every question type, its fields, and when to use each one. |
| logic-rules | Conditional logic: operators, actions, combinators, and edge cases. |
| editing-flows | Patterns for building forms: ordering, page breaks, piping. |
| form-best-practices | UX guidelines for effective form design. |
| form-themes | Theme structure, token reference, and styling guidelines. |
| form-settings | Settings reference: notifications, email templates, variables. |
| analytics | Metrics definitions and how to interpret form analytics. |
| toon-format | Compact output format for structured data display. |
| requests | Requests 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.
{
"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:
{
"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:
Call
document_createwithformId,name,contentType, and the exactsizein 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.PUTthe raw bytes touploadUrlwithin the hour, withContent-Typeset to the type you declared.Reference it from
request_create:documents: [{ documentId, field?, name? }].fieldis the Documents block’s field key, optional only when the form has exactly one such block.nameoverrides 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)://ordata:imageURIs. A per-request document is the exception:document_createreturns an upload URL that a client with HTTP access canPUTthe 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:
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:
{
"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:
{
"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:
GET /.well-known/oauth-protected-resource, thenGET /.well-known/oauth-authorization-serverfor the endpoint URLs, scopes, and supported auth methods.POST /oauth/registerwith yourredirect_uris(dynamic client registration, no credentials needed). You get aclient_id, plus aclient_secretif you asked for anything other thantoken_endpoint_auth_method: “none”. Redirect URIs must be HTTPS, or HTTP onlocalhost. Registration is capped at 20 per hour per IP.Send the user to
/oauth/authorizewithresponse_type=code, yourclient_id, the registeredredirect_uri,scope=mcp:read mcp:write offline_access,state, and acode_challengewithcode_challenge_method=S256. They sign in, pick one workspace, and authorize.Exchange the code at
POST /oauth/tokenwithgrant_type=authorization_codeand yourcode_verifier, within 60 seconds. Codes are single use.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:
{
"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.