MCP server
Tools: create_form, get_snippet, send_test, list_submissions, get_submission. Setup for Claude Code, Cursor, Codex and VS Code is below.
[ For AI agents and builders ]
No signup step for the agent. It gets a key, wires the form, sends one test lead to your own inbox and hands you a claim link. Anything that emails a customer, changes a rule or deletes data waits for your click.
The prompt says <your form key>; a key looks like f_8Kq2xLm9. An agent with only your email address can make its own: the key starts weak and waits for you to claim it (how).
› claude “add a contact form that emails me”
● contactforms.create_form(email: “mani@northwind.studio”)
✓ form f_8Kq2xLm9 created · key returned once · unclaimed · bounded real intake, held until claimed
● contactforms.get_snippet(framework: “nextjs”)
✓ wrote app/contact/page.tsx
● contactforms.send_test(form: “f_8Kq2xLm9”) · to your inbox only
✓ test lead stored · delivery pending (email sending is not live yet)
→ owner: open the claim link while signed in with that address
Illustrative example, not a live session.
[ 01 · Three ways in ]
Tools: create_form, get_snippet, send_test, list_submissions, get_submission. Setup for Claude Code, Cursor, Codex and VS Code is below.
One plain-text page with every endpoint, tool and error code, generated from the code and written for models.
POST /api/forms with an email returns a working key and a claim link. No account needed. Then read forms and submissions with the key.
[ 02 · Ready prompts ]
Each prompt tells the builder to show success only when the answer says success: true and to show the error and its fix otherwise. A prompt is a draft until it has been run in its own tool; the label above each one says whether that has happened.
Prompt for Lovable
Not yet run in Lovable. This prompt is a draft until it has been run there.
Connect the contact form on this site to ContactForms, a form backend.
Form address: https://contactforms.dev/f/<your form key>
Replace <your form key> with the form key you were given. It looks like f_8Kq2xLm9. If you were given none, stop and ask me for it; do not invent one.
Do this:
1. Send the form with fetch: POST to that address with the headers "Content-Type: application/json" and "Accept: application/json", and a JSON body with the fields name, email, phone and message (these exact names).
2. Read the JSON answer. Show the success message ONLY when the answer has "success": true. Never show success because the request merely finished.
3. When "success" is false, do not show success. Show the answer's "message" to the visitor, and log its "fix" text so the developer can repair it.
4. The form key in the address is public and is meant to be in the page. Do not put any API key (cf_live_ or cf_agent_) in the page, a file or a commit.
5. After building it, send one real test submission from the page and tell me exactly what the answer said.
[ 03 · Connect over MCP ]
The formats follow each tool's documentation (checked 20 Oct 2026). Use an agent key with the scope you want it to have; see the MCP docs for the tools and limits.
claude mcp add --transport http contactforms https://contactforms.dev/api/mcp \
--header "Authorization: Bearer ${CF_AGENT_KEY}"
[ 04 · Errors agents can fix ]
So the agent repairs its own code instead of guessing — and the form your customer sees is the one that works.
{
"success": false,
"error": "origin_not_allowed",
"fix": "Add brightsmile.in to allowed domains, or call from that site.",
"docs_url": "https://contactforms.dev/docs/domains"
}
[ 05 · Safe by default ]
Bounded real intake, held and never delivered. After 72 hours the key sleeps: leads are still stored but held until you claim it, for up to 90 days.
Any send, change or delete needs your click in the dashboard.
Form content can never give instructions to an AI step.