MCP server
@formfeed/mcp exposes Formfeed to agents that speak the Model Context Protocol
(MCP): Claude Desktop, Claude Code, Cursor, VS Code, Zapier agents and your own. It runs in one of two ways:
- Locally (stdio): your client starts
npx -y @formfeed/mcpwith an API key inFORMFEED_API_KEY. See Run it locally. - Hosted (Streamable HTTP): your client connects to
https://mcp.formfeed.dev/mcp, with an API key or by signing in with your Formfeed account. See Hosted endpoint.
A server acts in one workspace: the one its API key was made for, or the one you choose when you sign
in. The server is listed in the official MCP registry as io.github.formfeed-dev/formfeed.
Tools
| Tool | What it does | Cost |
|---|---|---|
list_templates | The workspace's templates, most recently changed first and without archived ones: id (tpl_…), slug, name, kind (pdf, image, docx, pptx), engine, description, tags and published_version (null means never published, so render cannot use it yet). Filters by kind, engine, tag and search | free |
get_template_schema | The JSON Schema of the data a template expects and its sample data, from the published version, else the latest draft. When the template stores no schema, one is inferred from the sample data: a guide, not a contract | free |
validate_template | Checks a template with data without rendering; nothing is stored. With a slug the API checks the published version (else the latest draft) as a render would: syntax, header and footer, errors that only this data triggers, and the data against the stored data schema (a wrong type or a missing required field is an error). With html plus engine it compiles offline and reports syntax errors, unknown filters and missing variables. Returns ok and diagnostics; for a Word or PowerPoint template a diagnostic names part and paragraph. Warnings leave ok true | free |
check_invoice | Checks the _invoice block of an e-invoice offline, as the API does before it renders one: the fields EN 16931 asks for, the totals to the cent, VAT categories and code lists, and France's rules for an invoice between two French businesses. Nothing is sent to the API. Returns ok and problems, each with the field (path), the rule (BR-…) where one applies, and a message | free |
render | Renders a stored template or raw HTML with data and returns the render with status and download_url: PDF, PNG, JPG or WebP, and DOCX, PPTX or PDF for Word and PowerPoint templates; without output, the template decides. A stored template renders its published version. With einvoice the PDF becomes a ZUGFeRD / Factur-X e-invoice built from data._invoice, and the render carries the validator's report. An image up to 2 MB also comes back as a picture the agent can look at; a PDF comes as its link | units; free with a test key |
convert_to_pdf | Converts an existing Word, Excel, PowerPoint, OpenDocument, RTF or HTML document (up to 20 MB, not kept), or the render of a Word or PowerPoint template, to PDF as it is, without data, and returns the render. Pass render_id, file_base64 with file_name, or, with the local server only, path | units like a PDF of that length; Starter plan and above |
get_render | Status, download_url with its expires_at, page count, units and error of a render. It issues a fresh link, so call it when the old one has expired | free |
get_workspace | The workspace (with its region) and organisation the server works in, the plan, the environment (live or test) and the units of the current period: included, used and prepaid units left | free |
A typical agent flow: list_templates → get_template_schema → build the data → validate_template → render → hand the download_url to the user.
For an e-invoice the agent builds the _invoice block, corrects it with check_invoice until it is ok, and calls render with einvoice and the block as data._invoice. The template prints the same block, as the E-invoice example of the gallery does. A refused invoice comes back as a tool error that names each field and rule, so the agent can correct the block and try again. Live e-invoices need the Starter plan; test keys make a limited number a day, each with the test mark (see Plans and units).
Every tool answers with structured content beside its text and declares the shape of it as an output schema (MCP's outputSchema), so a client can check the fields it reads: the status, download_url and units of a render, the diagnostics of a check, the plan and units of the workspace. A tool error carries text only.
Arguments
The agent reads these from the server itself; they are here so you know what it can reach for. Every argument is optional unless it says otherwise, and a tool that takes none needs none.
list_templates — narrows the list; without arguments it returns every template of the workspace.
| Argument | Type | Meaning |
|---|---|---|
kind | pdf | image | docx | pptx | Only templates of that kind |
engine | jinja2 | liquid | handlebars | Only templates written in that engine |
tag | string | Exact tag, as the app writes them (lower case, dashes) |
q | string | Search in name and slug, case-insensitive |
get_template_schema
| Argument | Type | Meaning |
|---|---|---|
template | string, required | Slug or tpl_… id. The published version, or the latest one when nothing is published |
validate_template — pass template, or html with engine.
| Argument | Type | Meaning |
|---|---|---|
template | string | Slug or tpl_… id |
html | string | Template source, when no slug is given |
engine | jinja2 | liquid | handlebars | Required with html |
data | object | Checked against the template; without it the template's sample data is used |
check_invoice
| Argument | Type | Meaning |
|---|---|---|
invoice | object, required | The _invoice block, with the fields of the field reference |
profile | en16931 | basic | The profile the render will ask for, en16931 when left out. basic cannot carry a rounding amount and is refused between two French businesses |
render — pass template or html.
| Argument | Type | Meaning |
|---|---|---|
template | string | Slug or tpl_… id. Renders the published version; a template that was never published fails with template_not_found |
html | string | A complete HTML document instead of a template, up to 5 MB |
engine | jinja2 | liquid | handlebars | For html that contains template syntax to fill from data |
data | object | Bound to the template, shaped as get_template_schema describes |
output | pdf | png | jpg | webp | docx | pptx | Left out, the template decides: PDF for PDF templates and html, its image format for image templates, its default for Word and PowerPoint templates. Word templates render docx or pdf, PowerPoint templates pptx or pdf |
filename | string | Name of the file behind download_url, for example invoice-2026-0042.pdf |
locale | string | BCP 47 tag for helpers and translations, for example de-DE |
wait | boolean, default true | The tool asks for the document synchronously, so most renders have finished when it answers. A render that outlasts the API's synchronous limit (a minute) continues in the background: with true the tool waits for it, up to two more minutes; with false it returns the render as it is (queued or rendering), to follow with get_render |
einvoice | boolean | object | Makes the PDF an e-invoice from data._invoice: true for the defaults, or the options profile, flavour, xml and display_check; false switches off what the template declares. PDF only. Live e-invoices from the Starter plan; test renders make a limited number a day, each with the test mark |
convert_to_pdf — pass exactly one source: render_id, file_base64 (with file_name) or path.
| Argument | Type | Meaning |
|---|---|---|
render_id | string | rnd_… of a render whose output is docx or pptx |
file_base64 | string | The document itself, base64 encoded, up to 20 MB |
file_name | string | Required with file_base64. Its name, for example report.xlsx; the type is read from the content, not the extension |
path | string | Absolute path of a file on this machine. Local server only: the hosted endpoint does not offer this argument and has no access to your disk |
page_ranges | string | Pages to convert, for example 1-3,5; all pages when left out |
landscape | boolean | Landscape, for spreadsheets and documents without their own page setup |
single_page_sheets | boolean | Each sheet of a spreadsheet on one page |
filename | string | Name of the PDF; the document's own name with .pdf when left out |
get_render
| Argument | Type | Meaning |
|---|---|---|
id | string, required | rnd_… id of the render |
get_workspace takes no arguments. It answers workspace, organization, plan, environment,
period and units as GET /account does.
Run it locally (stdio)
The local server needs Node.js 22 or newer; npx downloads and starts it. Claude Desktop
(claude_desktop_config.json) and Cursor (~/.cursor/mcp.json for every project, .cursor/mcp.json
for one) take the same entry:
{
"mcpServers": {
"formfeed": {
"command": "npx",
"args": ["-y", "@formfeed/mcp"],
"env": { "FORMFEED_API_KEY": "ff_test_…" }
}
}
}
Claude Code adds it with one command. The server's name comes before --env, which takes several
values and would otherwise read the name as one of them; everything after -- is the command Claude
Code starts:
claude mcp add --transport stdio formfeed --env FORMFEED_API_KEY=ff_test_… -- npx -y @formfeed/mcp
VS Code reads .vscode/mcp.json, where the key is servers and an input variable keeps the API key
out of the file:
{
"inputs": [
{
"type": "promptString",
"id": "formfeed-key",
"description": "Formfeed API key",
"password": true
}
],
"servers": {
"formfeed": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@formfeed/mcp"],
"env": { "FORMFEED_API_KEY": "${input:formfeed-key}" }
}
}
}
The local server reads these environment variables:
| Variable | Meaning |
|---|---|
FORMFEED_API_KEY | An API key of the workspace. Without it the server starts with the offline checks only, validate_template with html and check_invoice; every other tool answers with how to get a key |
FORMFEED_REGION | eu (the default) or us: the API host of that region. Every workspace is in the EU today |
FORMFEED_BASE_URL | Another API address, with /v1, for example https://api-eu.formfeed.dev/v1; it wins over the region |
Use a test key (ff_test_…) while building: its renders are free (with the test mark on the Free
plan). Switch to a live key (ff_live_…) when the agent goes to production. The key needs the
scopes of the tools the agent uses; the defaults of a new key
(render:create, render:read, template:read, account:read) cover all eight.
Step-by-step setup for 18 clients and agent frameworks (Claude Desktop and Code, Cursor, VS Code, Windsurf (now Devin Desktop), Zed, Codex, Gemini CLI, the OpenAI Agents SDK, LangChain, Mastra and more) is on formfeed.dev/agents.
For coding agents, Formfeed's plugin adds the hosted server together with two skills that write and check templates and the code that renders them: see Coding agents.
Hosted endpoint
mcp.formfeed.dev serves the same tools over Streamable HTTP, for clients that connect remotely
(Claude.ai connectors, Claude Code, Cursor, ChatGPT, n8n and the agent frameworks):
| URL | Sign in with | For |
|---|---|---|
https://mcp.formfeed.dev/mcp | your Formfeed account (OAuth), or Authorization: Bearer ff_… | everyone: a chat client opens the sign-in itself and you choose the workspace; a server or automation sends its API key |
https://mcp.formfeed.dev/mcp/<workspace id> | your Formfeed account (OAuth) | a second connection that should work in another workspace than the one you chose |
With an API key, the client sends it as a header. In Claude Code:
claude mcp add --transport http formfeed https://mcp.formfeed.dev/mcp --header "Authorization: Bearer ff_test_…"
A client that looks servers up before it connects finds this one through
https://formfeed.dev/.well-known/ai-catalog.json, which points at the endpoint's server card,
https://mcp.formfeed.dev/mcp/server-card: its name, version and both addresses, read without a key.
Signing in with your account
Add https://mcp.formfeed.dev/mcp to the client without a key. It discovers the Formfeed OAuth
server, registers itself and opens the browser: you sign in to Formfeed if you are not already, see
which client asks and what it may do, choose the workspace the agent works in, and allow it. The
client keeps a token and refreshes it on its own; signing out of the client ends the access.
# Claude Code
claude mcp add --transport http formfeed https://mcp.formfeed.dev/mcp
The workspace stays with the client: the agent works there until you change it under
Connected agents in the account menu of the app, where every agent you allowed is listed, each with its
workspace and a way to revoke it. A change reaches the agent within an hour, with its next token. To
work in two workspaces at once, connect the client a second time to the workspace's own address,
https://mcp.formfeed.dev/mcp/<workspace id> (the id is under Settings → Details as
Workspace ID); a client connected that way works there whatever is chosen for it.
An agent that signed in acts as you in that workspace, but with at most four
scopes, template:read, render:read, render:create and
account:read, and never more than your role allows (a viewer's agent cannot render). So it reads
templates and their data schemas, validates data, renders documents and converts office files (units
count as for any render), reads the workspace's renders, and reads which workspace it works in and
what the period has left (get_workspace). It cannot publish or change templates, upload or delete
files, or manage webhooks, keys, members or billing, whatever your role. A signed-in agent always
renders live; to render for free while you build, connect with a test key instead.
Nothing is stored on the endpoint: every request carries its token, and the API checks on each call that you are still a member of the agent's workspace.
With two-factor authentication on your account, an
agent works only after you allowed it on the consent page from a session that passed your code. An
agent you allowed before you turned two-factor authentication on is refused from its next token on
(within the hour): connect it again and allow it once more. Until then, and for any agent that was
never allowed this way, the API answers 403 forbidden with the reason.
Self-hosting the endpoint
The same server runs anywhere Node.js 22 does:
npx -y @formfeed/mcp --http --port 8790 --host 0.0.0.0
--port defaults to 8790 and --host to 127.0.0.1. The server takes API keys on /mcp, sent as
Authorization: Bearer ff_… with each request, and calls the API host that FORMFEED_REGION or
FORMFEED_BASE_URL selects. --auth-server <issuer> (or FORMFEED_AUTH_SERVER) adds the OAuth
sign-in on /mcp and /mcp/<workspace id>, which is how the hosted endpoint runs: the server passes
the access tokens on to the Formfeed API, which accepts only tokens of Formfeed's own sign-in, so a
self-hosted endpoint is for API keys. --public-origin <url> (or FORMFEED_PUBLIC_ORIGIN) names the
origin clients see behind a proxy. Its server card is at /mcp/server-card of that origin.
Notes
- Validation runs the same engine as the editor and the render servers, so an agent can fix data or template problems before spending units.
- Every
rendercall is a new render that counts its units; nothing is deduplicated between calls. - Errors come back as tool errors whose text starts with the API problem code (
quota_exceeded: …,template_not_found: …), never as exceptions. A refused e-invoice (einvoice_data_invalid,einvoice_display_mismatch,pdfa_conversion_failed) adds the problem'serrorsandeinvoicereport, and a strict PDF/UA failure (pdfua_validation_failed) itsaccessibilityverdict. Errors lists every code.