---
name: form2wf-mcp
description: Use when building, inspecting or managing Form2Workflow (form2wf) forms through its MCP server — creating forms that post to n8n webhooks, editing form config/fields, data pages with onload webhooks and tables, embed snippets, submissions, themes, form users and form access. Triggers on form2wf, Form2Workflow, f2wf, "form that posts to my n8n webhook", f2w_ API keys, or the mcp__form2wf__* tools.
---

# form2wf MCP

form2wf renders forms and data pages on any website and sends submissions to
n8n webhooks. Its MCP server lets you manage one organization's forms.

## Connect

- Endpoint: `https://<form2wf-host>/mcp` — stateless Streamable HTTP, POST only.
- Auth: `Authorization: Bearer f2w_...` (or `X-API-Key: f2w_...`). Keys are created
  by an org admin in the dashboard under **API Keys**; the raw key is shown once.
- Claude Code:
  ```bash
  claude mcp add --transport http form2wf https://<form2wf-host>/mcp \
    --header "Authorization: Bearer f2w_..."
  ```
- Call `whoami` first: it returns the org, the key's access level and what it can do.

## Access levels

| Level | Can |
|---|---|
| viewer | Read everything: forms, submissions, usage, folders, themes, roles, form users, access, embed snippets |
| editor | viewer + create/update forms, manage folders |
| admin | editor + delete forms, set form access, create/update themes, create form users, assign them to forms |

A key never exceeds the current role of the member who created it. A 403 in a
tool result means the key's level is too low — say so; don't retry.

## Tools

- Read: `whoami`, `get_config_reference`, `list_forms`, `get_form`, `get_embed_snippet`,
  `list_submissions`, `get_usage`, `list_folders`, `list_themes`, `list_roles`,
  `list_form_users`, `get_form_access`
- Editor: `create_form`, `update_form`, `create_folder`, `rename_folder`, `delete_folder`
- Admin: `delete_form`, `set_form_access`, `create_theme`, `update_theme`,
  `create_form_user`, `assign_form_user`, `unassign_form_user`

## Building a form — the reliable sequence

1. `get_config_reference` once per session. Don't guess config keys; there are
   exactly five top-level keys (`showTitle`, `columns`, `theme_id`, `buttonColor`, `fields`).
2. Ask the user for the n8n **production** webhook URL (`/webhook/...`, not
   `/webhook-test/...`). Without `n8n_webhook_url` the form has no submit button.
3. `create_form` with `name`, `n8n_webhook_url`, and `config.fields`. Give every
   field a unique `id`.
4. `get_embed_snippet` and hand the user the HTML and the hosted URL.
5. Suggest a secret header: `webhook_headers: [{"key":"X-Form2wf-Secret","value":"<random>"}]`
   plus Header Auth on the n8n Webhook node.

## Editing — avoid destroying data

- `update_form` replaces `config` **as a whole**. Always `get_form`, modify the
  returned `config`, send the complete object. Sending only `fields` drops
  `columns`, `showTitle` and `theme_id`.
- Header values come back as `"[redacted]"`. Omit `webhook_headers` to keep them;
  sending the placeholders is rejected.
- Changing `slug` breaks every existing embed and link — confirm with the user first.
- Prefer `update_form` with `is_active: false` over `delete_form` (deletion is permanent
  and removes submission history). Confirm before deleting anything.

## Gotchas

- Forms have a UUID `id` (tools, submit API) and a `slug` (embeds, hosted URL,
  `targetForm`, navigation). Don't mix them up.
- Submitted values reach n8n under `body.data` (JSON). With a `file` field the
  request is multipart with flat fields and no `data` wrapper — don't combine
  `file` with `object`/`array` fields.
- Table and chart `dataSource` is a plain dot path (`customers`), not `{{data.customers}}`.
- Chart fields (`type: "chart"`, settings in `chartConfig`) draw onload data and submit
  nothing. `format: "percent"` expects fractions (0.997 → 99.7%); for 0–100 values use
  `suffix: "%"`. An indicator's `value` IS a template (`{{data.total}}`).
- Templates (`{{params.x}}`, `{{data.x}}`, `{{row.x}}`) work in `defaultValue`,
  `displayValue`, link/button params and button/row-action webhook URLs — NOT in
  the form's `n8n_webhook_url` or `onload_webhook_url`.
- Navigation params (row actions, navigate buttons/links, `nav` menu items)
  reach the target form's onload webhook in `params`, so an edit form can load
  its record by id.
- Workflow replies (submit, button, row action) can steer the page with
  top-level keys: `redirectUrl` (e.g. Stripe Checkout), `navigate: { form, params }`,
  `refresh: true`, `message`. Without them the widget shows its own success text.
- `markdown` fields show formatted text (`markdown`, or `dataSource` = path to a
  markdown string in onload data); HTML in it is shown as text. `nav` fields
  (`navItems: [{ label, form | url, params }]`) give a set of forms one menu.
- Tables take `tableSearch: true` and `pageSize: n` (client-side).
- `set_form_access` takes `access_mode`: `public`, `verified` (sign in with
  Microsoft, no accounts; `verified_config.emailDomains` limits who) or
  `protected` (form users). Verified needs a sign-in provider set up by an admin
  in Settings, and embedding sites listed in the form's `allowed_domains`.
- Failed deliveries (5xx/timeout) are retried up to twice — tell users to make
  workflows idempotent.
- Unchecked checkboxes are omitted from submitted data.
- Rate limit: 60 MCP requests per minute per key owner. Form quota depends on
  plan (Free: 5 forms, 1,000 submissions/month) — `create_form` returns
  `FORM_QUOTA_EXCEEDED` when full.

## Debugging a form that "doesn't work"

1. `get_form` — is `is_active` true? Is `n8n_webhook_url` set and a production URL?
2. `list_submissions` — look at `status` and the stored n8n response/error.
   A 503 means n8n failed, timed out, rejected the call (check its header auth),
   or the webhook is on a private network address (refused unless the org
   allows LAN webhooks).
3. `get_form_access` — a protected form needs the visitor to be an assigned form
   user, or a member of the form's organization. A verified form needs a sign-in
   provider the org has set up; sign-in only returns to the hosted page or an
   allowed domain.
