# MCP-server

Gebruik Formstep vanuit Claude, Cursor en andere MCP-compatibele tools.

## MCP-server

De Model Context Protocol (MCP)-server laat AI-agents je Formstep-formulieren lezen en bewerken via uitgebreide, schema-gestuurde tools.

<h2 id="what">Wat het is</h2>
<p>
  MCP is een open standaard waarmee AI-tools verbinding kunnen maken met externe diensten. Formstep biedt een gehoste MCP-endpoint aan
  waarmee elke MCP-compatibele client verbinding kan maken, waaronder Claude Code, Claude desktop en Cursor.
</p>

<h2 id="connection">Verbinding</h2>

```
URL:  https://api.formstep.io/api/mcp   (POST, streamable HTTP)
Auth: Bearer <token>
```

<p>Twee soorten bearer token werken:</p>
<ul>
  <li>
    <strong>API-token</strong> (<code>fb_...</code>) — aangemaakt via <a href="/nl/developers/api-tokens">API-tokens</a>. Het beste voor
    persoonlijk gebruik en snelle configuratie.
  </li>
  <li>
    <strong>OAuth-toegangstoken</strong> (<code>fbo_...</code>) — uitgegeven door de <a href="#oauth">OAuth-flow</a>. Het beste voor apps
    van derden die namens een gebruiker verbinding maken.
  </li>
</ul>
<p>
  Beide zijn gebonden aan precies één werkruimte en bereiken dezelfde tools. Een tool-aanroep die een andere werkruimte noemt, of een
  formulier daarin, faalt met <code>FORBIDDEN</code>. OAuth-tokens dragen ook scopes (<code>mcp:read</code>, <code>mcp:write</code>,{' '}
  <code>offline_access</code>), maar vandaag wordt geen enkele tool erdoor beperkt — behandel elk token als volledige toegang binnen zijn
  werkruimte.
</p>
<p>
  Tool-aanroepen zijn beperkt tot 120 per minuut per token, gedeeld met de <a href="/nl/developers/rest-api">API</a>: alleen{' '}
  <code>tools/call</code> verbruikt het budget, terwijl <code>initialize</code>, <code>tools/list</code>, <code>prompts/*</code> en{' '}
  <code>resources/*</code> gratis zijn. Bij overschrijding geeft de aanroep nog steeds HTTP 200 terug met een mislukt toolresultaat dat{' '}
  <code>RATE_LIMITED</code> en een <code>retryAfterMs</code> draagt — poll op een timer, nooit in een lus.
</p>

> 💡 **Waar je een token krijgt**
> <p>
>     Open <strong>OAuth en API-sleutels</strong> in de zijbalk van je workspace om API-tokens aan te maken en verbonden OAuth-apps te
>     bekijken. Zie <a href="/nl/developers/api-tokens">API-tokens</a> voor een stapsgewijze handleiding.
>   </p>

<h2 id="core-tools">Kerntools</h2>
<p>
  Elke tool wordt geadverteerd via <code>tools/list</code> wanneer een client verbinding maakt. Clients die schema's on demand laden, zoals
  Claude Code, halen het volledige schema van een tool op zodra een taak hem nodig heeft. De onderstaande tabel toont de kerntools waarmee
  de meeste taken beginnen; <code>load_tools</code> (catalogi) en <code>load_skill</code> (domeinhandleidingen) documenteren de rest.
</p>

<p>
  Meer invoegvarianten — tijd, bestandsupload, handtekening, betaling, matrix/raster, rangschikking, afbeeldingskeuze, schakelaar, tabel,
  lijst, rij, berekend veld, verborgen veld, inline variabele, ingesloten inhoud (<code>editor_insertEmbedded</code> voor YouTube, Google
  Maps of iframe-insluitingen) en een conditioneel-logicablok (<code>editor_insertLogic</code>) — staan ook op <code>tools/list</code>. Laad{' '}
  <code>load_skill("question-types")</code> voor de volledige set, elk met zijn toolnaam en velden. Conditionele logica wordt geschreven met{' '}
  <code>editor_setLogic</code> in de <a href="#tool-catalogs">editor-actions</a> catalogus.
</p>

<h2 id="tool-catalogs">Toolcatalogi</h2>
<p>
  Deze tools staan ook op <code>tools/list</code>. Voer <code>load_tools</code> uit met een catalogusnaam voor verrijkte documentatie
  (intro, volledige schema's, gebruikspatronen, randgevallen) voor de gegroepeerde tools, en roep ze daarna direct aan.
</p>

<p>
  De catalogus <code>request-lifecycle</code> vermeldt alle acht aanvraagtools omdat de in-app chat een kleinere kernset adverteert. Via dit
  endpoint staan alle acht al op <code>tools/list</code>, dus wat de catalogus toevoegt is documentatie.
</p>

<h2 id="skills">Skills (domeinkennis)</h2>
<p>
  Skills zijn ingebouwde gidsen die de agent kan laden via <code>load_skill</code>. Ze bieden domeinkennis die de agent helpt betere
  beslissingen te nemen — geen toolschema’s, maar ontwerpadvies en veldsemantiek.
</p>

<h2 id="requests">Aanvragen</h2>

<p>
  Een <a href="/nl/requests/overview">aanvraag</a> wijst één gepubliceerd formulier toe aan één met naam genoemde ontvanger, met zijn eigen
  link, zijn eigen vooraf ingevulde antwoorden, en zijn eigen uitkomst. Het is hoe een agent een echt persoon om iets vraagt en te weten
  komt wat die persoon heeft gezegd.
</p>

<h3 id="requests-create">Een aanvraag maken</h3>

<p>
  Begin altijd met <code>fields_list(formId)</code>. Het geeft de aanspreekbare sleutels van de huidige gepubliceerde versie van het
  formulier terug, elk met een <code>usage</code>-regel die aangeeft in welk argument de sleutel thuishoort — zichtbare vragen gaan in{' '}
  <code>prefill</code>, verborgen velden in <code>context</code>. Leid nooit een sleutel af van een vraagtitel, en lees opnieuw na{' '}
  <code>form_publish</code>.
</p>

```
{
  "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/formstep",
  "idempotencyKey": "po-42"
}
```

<p>Het resultaat bevat de link en de klok:</p>

```
{
  "id": "kd7...",
  "status": "pending",
  "url": "https://form.formstep.io/r/rq_...",
  "deliveryStatus": "queued",
  "expiresAt": 1780000000000,
  "createdAt": 1747000000000,
  "deduplicated": false,
  "next": "..."
}
```

<p>
  <code>delivery</code> staat standaard op <code>"none"</code>, wat je <code>url</code> geeft om zelf af te leveren; <code>"email"</code>{' '}
  verstuurt de uitnodiging en vereist <code>recipient.email</code> op een Pro- of Business-abonnement, of een van de 10 gratis uitnodigingen
  van een Free-account. <code>readonly</code> vergrendelt velden die de ontvanger niet mag bewerken, en elke vergrendelde sleutel moet ook
  vooraf zijn ingevuld. <code>context</code> accepteert alleen sleutels van verborgen velden, terwijl <code>metadata</code> ondoorzichtige
  administratie is die wordt teruggegeven op <code>request_get</code> en in de callback. <code>expiresAt</code> is in epoch-milliseconden,
  standaard 30 dagen vooruit en met een maximum van 365 dagen. <code>idempotencyKey</code> is 30 dagen geldig binnen de werkruimte: dezelfde
  sleutel met dezelfde body geeft de oorspronkelijke aanvraag terug met <code>deduplicated: true</code>, en een andere body is een conflict.
  Elke response bevat ook een <code>next</code>-regel die de agent vertelt wat hij hierna moet doen.
</p>
<p>
  <code>deliveryStatus</code> is <code>not_requested</code> tot een uitnodiging in de wachtrij staat, dan <code>queued</code> →{' '}
  <code>sent</code> of <code>failed</code>, en <code>bounced</code> zodra de mailprovider een harde bounce of een klacht meldt. Een aanvraag
  aanmaken verbruikt één eenheid van de maandelijkse toewijzing van de werkruimte, ongeacht of de ontvanger ooit antwoordt; zodra die op is,
  faalt <code>request_create</code> met <code>MONTHLY_ALLOWANCE_REACHED</code>.
</p>

<h3 id="requests-callbacks">Callbacks of pollen</h3>

<p>
  Met een <code>callbackUrl</code> post Formstep één keer per eindgebeurtenis — voltooiing, verlopen, annulering — ondertekend met het{' '}
  <strong>request signing secret</strong> van de werkruimte. Zie <a href="/nl/requests/callbacks">Callbacks & ondertekening</a> voor de
  payload en het verificatierecept.
</p>

> ℹ️ **Autonome agents moeten pollen**
> <p>
>     Het request signing secret verschijnt alleen ooit op de pagina met inloggegevens van je werkruimte — het wordt nooit teruggegeven via
>     MCP of de API. Een agent die op zichzelf draait, zonder mens om een ontvanger op te zetten en te configureren, kan daarom geen callback
>     verifiëren. Laat <code>callbackUrl</code> weg en poll in plaats daarvan <code>request_get(requestId)</code>, in de orde van minuten in
>     plaats van seconden, tot <code>status</code> niet meer <code>"pending"</code> is. <code>expiresAt</code> begrenst hoelang dat de moeite
>     waard is.
>   </p>

<h3 id="requests-test-mode">Testmodus</h3>

<p>
  Geef <code>test: true</code> op om de hele opzet te repeteren vóór een echte run. De link opent nog steeds en kan worden afgerond, en de
  callback vuurt met <code>"test": true</code> — maar er wordt niets gemaild wat <code>delivery</code> ook zegt, de aanvraag blijft
  verborgen voor de pagina Aanvragen en de analytics-funnel, en de inzending telt nergens mee: geen quotum, geen exports, geen integraties.
  Testaanvragen verschijnen alleen in <code>request_list</code> als je <code>includeTest: true</code> opgeeft. De link sluit binnen 24 uur,
  en op Free mag een workspace 10 testaanvragen per dag aanmaken.
</p>

<h3 id="requests-documents">Documenten per aanvraag</h3>

<p>
  Om één ontvanger een bestand te geven — een conceptcontract, hun eigen offerte — heeft het formulier een <strong>Documentenblok</strong>{' '}
  nodig, dat een auteur of een agent invoegt met <code>editor_insertDocumentsBlock</code>. <code>fields_list</code> rapporteert het als{' '}
  <code>type: "documents"</code>. De bytes reizen nooit via een tool:
</p>

<ol>
  <li>
    Roep <code>document_create</code> aan met <code>formId</code>, <code>name</code>, <code>contentType</code>, en de exacte{' '}
    <code>size</code> in bytes. Je krijgt <code>{'{ id, name, contentType, size, uploadUrl, expiresAt }'}</code> terug. Alleen PDF en
    afbeeldingen (geen Office-documenten), 25 MB per bestand, en 100 MB aan documenten per aanvraag.
  </li>
  <li>
    <code>PUT</code> de ruwe bytes naar <code>uploadUrl</code> binnen het uur, met <code>Content-Type</code> ingesteld op het type dat je
    hebt opgegeven.
  </li>
  <li>
    Verwijs ernaar vanuit <code>request_create</code>: <code>documents: [{'{ documentId, field?, name? }'}]</code>. <code>field</code> is de
    veldsleutel van het Documentenblok, alleen optioneel wanneer het formulier precies één zo'n blok heeft. <code>name</code> overschrijft
    de weergavenaam voor deze aanvraag.
  </li>
</ol>

<p>
  De aangeleverde documenten van het blok blijven staan en die van jou verschijnen eronder, alleen voor deze ontvanger.{' '}
  <code>request_create</code> verifieert de upload voordat de aanvraag bestaat, dus <code>DOCUMENT_NOT_UPLOADED</code> betekent dat stap 2
  is overgeslagen. Eén upload kan door elk aantal aanvragen worden gerefereerd, en de bytes tellen mee voor je werkruimteopslag.
</p>

<h3 id="requests-domains">Aangepaste domeinen</h3>

<p>
  Geef <code>domainId</code> mee aan <code>request_create</code> om de link te genereren op een van de{' '}
  <a href="/nl/branding-domains/custom-domains">aangepaste domeinen</a> van de werkruimte. De ids komen van <code>formShareLink_list</code>,
  dat ze teruggeeft als <code>availableCustomDomains</code>. Laat je het weg, dan neemt de link het domein waaronder het formulier al is
  gepubliceerd.
</p>

<h3 id="requests-reading">Resultaten lezen</h3>

<p>
  <code>request_get</code> geeft de hele aanvraag terug. Zodra deze is voltooid, bevat <code>answers</code> de waarden van de ontvanger,
  geordend per veldsleutel, bevat <code>display</code> dezelfde sleutels als leesbare tekst, en is <code>outcome</code> — goedkeuren,
  afwijzen of wijzigen — hun oordeel wanneer het formulier een <a href="/nl/requests/overview">beslissingsvraag</a> heeft. Een{' '}
  <code>callbackFailedAt</code>-tijdstempel betekent dat de bezorging geen pogingen meer over had en niets je endpoint bereikte; herstel de
  ontvanger en roep dan <code>request_replayCallback</code> aan, die het oorspronkelijke event-id opnieuw verstuurt zodat jouw ontvanger
  dedupliceert in plaats van opnieuw uit te voeren. Nadat het bewaarbeleid van het formulier een aanvraag verwijdert, wordt{' '}
  <code>dataPurgedAt</code> ingesteld en zijn de antwoorden definitief verdwenen.
</p>

<p>
  <code>request_list</code> haalt er in één keer veel op, gefilterd op <code>status</code>, <code>outcome</code>, <code>externalId</code>,
  en <code>includeTest</code>. Pagineer met <code>nextCursor</code>: een pagina kan zelden terugkomen met een lege <code>items</code> en{' '}
  <code>hasMore: true</code>, wat niet het einde van de lijst is — geef de cursor terug en ga door.
</p>

<h2 id="resources">Resources en prompts</h2>
<p>
  Elke skill en toolcatalogus is ook een MCP-resource op <code>skill://&lt;name&gt;</code> — <code>skill://requests</code>,{' '}
  <code>skill://editor-inserts</code>. Een client die <code>resources/list</code> ondersteunt, kan ze doorbladeren en lezen zonder{' '}
  <code>load_skill</code> of <code>load_tools</code> aan te roepen. De server serveert ook vier prompts op <code>prompts/list</code>:{' '}
  <code>identity</code>, <code>capabilities</code>, <code>data_tools</code>, en <code>editor_tools</code>.
</p>

<h2 id="confirmation">Tools die eerst bevestiging vragen</h2>
<p>
  Elke tool draagt de MCP-hints <code>readOnlyHint</code> en <code>destructiveHint</code>, afgeleid van het werkwoord. Deze tools zijn
  gemarkeerd als destructief, omdat ze ongedaan maken een extra aanroep vergt of niet mogelijk is: <code>form_delete</code>,{' '}
  <code>form_unpublish</code>, <code>workspaceFolder_delete</code>, <code>editor_deleteElement</code>,{' '}
  <code>translationLanguage_delete</code>, en <code>request_cancel</code>. De meeste clients vragen de gebruiker om bevestiging voordat ze
  deze uitvoeren, maar die prompt is de keuze van de client, dus controleer de goedkeuringsinstellingen ervan als je een harde stop nodig
  hebt.
</p>

<h2 id="limitations">Beperkingen</h2>
<ul>
  <li>
    <strong>Geen binaire uploads via een tool-aanroep.</strong> Afbeeldingen worden ingesteld via URL: omslagen, logo's en
    afbeeldingsblokken accepteren <code>http(s)://</code>- of <code>data:image</code>-URI's. Een document per aanvraag is de uitzondering:{' '}
    <code>document_create</code> geeft een upload-URL terug waar een client met HTTP-toegang het bestand naartoe kan <code>PUT</code>en (zie{' '}
    <a href="#requests-documents">Documenten per aanvraag</a>). Om een PDF of screenshot om te zetten in een formulier, gebruik je de{' '}
    <a href="/nl/ai/ai-form-generation#files">ingebouwde AI-chat</a>.
  </li>
  <li>
    <strong>Geen werkruimte-AI-vaardigheden.</strong> <a href="/nl/ai/ai-skills">Vaardigheden geschreven in Formstep</a> zijn alleen
    beschikbaar in de ingebouwde AI-chat. De eigen skills van de server (<code>load_skill</code>) zijn wel beschikbaar via MCP.
  </li>
</ul>

<h2 id="api-token-clients">Verbinden met een API-token</h2>
<p>
  De meeste clients loggen in via OAuth: voeg de URL toe zonder header en volg{' '}
  <a href="/nl/guides/ai-agents/connect">Een AI-agent met Formstep verbinden</a>. Een client die geen browser kan openen, zoals een script,
  CI-taak of headless agent, stuurt in plaats daarvan een <a href="/nl/developers/api-tokens">API-token</a> mee als header.
</p>
<p>Claude Code, vanaf de command line:</p>

```
claude mcp add --transport http formstep https://api.formstep.io/api/mcp \
  --header "Authorization: Bearer fb_YOUR_TOKEN"
```

<p>
  Of in het <code>.mcp.json</code>-bestand van een project:
</p>

```
{
  "mcpServers": {
    "formstep": {
      "type": "http",
      "url": "https://api.formstep.io/api/mcp",
      "headers": {
        "Authorization": "Bearer fb_YOUR_TOKEN"
      }
    }
  }
}
```

<p>
  Cursor, in <code>.cursor/mcp.json</code> of <code>~/.cursor/mcp.json</code>:
</p>

```
{
  "mcpServers": {
    "formstep": {
      "url": "https://api.formstep.io/api/mcp",
      "headers": {
        "Authorization": "Bearer fb_YOUR_TOKEN"
      }
    }
  }
}
```

<p>
  Om in te loggen met OAuth in plaats van met een token, gebruik je{' '}
  <a href="https://cursor.com/install-mcp?name=formstep&config=eyJ1cmwiOiJodHRwczovL2FwaS5mb3Jtc3RlcC5pby9hcGkvbWNwIn0=">
    Toevoegen aan Cursor
  </a>
  . Dit voegt de server-URL toe zonder header, en Cursor vraagt je om in te loggen bij Formstep.
</p>
<p>Andere clients gebruiken dezelfde URL en header; raadpleeg hun documentatie voor waar.</p>

<h2 id="oauth">OAuth gebruiken in plaats van API-tokens</h2>
<p>
  Een app van een derde partij die namens een gebruiker verbinding maakt, moet OAuth gebruiken in plaats van om een geplakt token te vragen.
  Formstep is een OAuth 2.1-autorisatieserver met verplichte PKCE (S256) en ondoorzichtige tokens — geen JWT's, geen implicit grant. Claude
  desktop, Claude Code en de Claude.ai-webconnector ontdekken dit allemaal via het MCP-endpoint, dus alleen de URL plakken zonder header
  volstaat: de 401 wijst naar <code>/.well-known/oauth-protected-resource</code>, en de client neemt het vanaf daar over.
</p>
<p>De flow, voor een client die je zelf schrijft:</p>
<ol>
  <li>
    <code>GET /.well-known/oauth-protected-resource</code>, dan <code>GET /.well-known/oauth-authorization-server</code> voor de
    endpoint-URL's, scopes en ondersteunde authenticatiemethoden.
  </li>
  <li>
    <code>POST /oauth/register</code> met jouw <code>redirect_uris</code> (dynamische clientregistratie, geen inloggegevens nodig). Je
    krijgt een <code>client_id</code>, plus een <code>client_secret</code> als je iets anders opgaf dan{' '}
    <code>token_endpoint_auth_method: "none"</code>. Redirect-URI's moeten HTTPS zijn, of HTTP op <code>localhost</code>. Registratie is
    beperkt tot 20 per uur per IP.
  </li>
  <li>
    Stuur de gebruiker naar <code>/oauth/authorize</code> met <code>response_type=code</code>, jouw <code>client_id</code>, de
    geregistreerde <code>redirect_uri</code>, <code>scope=mcp:read mcp:write offline_access</code>, <code>state</code>, en een{' '}
    <code>code_challenge</code> met <code>code_challenge_method=S256</code>. Ze loggen in, kiezen één werkruimte, en autoriseren.
  </li>
  <li>
    Wissel de code in bij <code>POST /oauth/token</code> met <code>grant_type=authorization_code</code> en jouw <code>code_verifier</code>,
    binnen 60 seconden. Codes zijn eenmalig te gebruiken.
  </li>
  <li>
    Roep het MCP-endpoint aan met <code>Authorization: Bearer fbo_...</code>. Toegangstokens gelden 1 uur; refreshtokens gelden 30 dagen en
    roteren bij elk gebruik. Een gebruikt refreshtoken opnieuw gebruiken verbrandt de hele keten, dus bewaar het nieuwste.
  </li>
</ol>
<p>
  <code>POST /oauth/revoke</code> (RFC 7009) trekt een toegangs- of refreshtoken in. Een gebruiker kan ook de hele app verbreken via{' '}
  <strong>Verbonden apps</strong> op de pagina OAuth en API-sleutels, wat elk token van die app voor die werkruimte tenietdoet.
</p>

<p>Heb je wel al een token in handen, dan gaat het op dezelfde plek als een API-sleutel:</p>

```
{
  "mcpServers": {
    "formstep": {
      "type": "http",
      "url": "https://api.formstep.io/api/mcp",
      "headers": {
        "Authorization": "Bearer fbo_USER_OAUTH_TOKEN"
      }
    }
  }
}
```

<h3 id="connected-apps">Verbonden apps</h3>
<p>
  Elke OAuth-verbinding staat vermeld onder <strong>Verbonden apps</strong> op de pagina <strong>OAuth en API-sleutels</strong>, met wanneer
  die tot stand kwam en voor het laatst is gebruikt. Verbindingen zijn persoonlijk: alleen de gebruiker die er een heeft geautoriseerd ziet
  hem, en werkruimtebeheerders kunnen die van een ander lid niet bekijken of intrekken. Verbreken werkt onmiddellijk. Een gebruiker die de
  werkruimte verlaat of eruit wordt verwijderd, verliest al zijn verbindingen ermee.
</p>

<h2 id="next-steps">Volgende stappen</h2>
<div class="not-prose grid gap-3 sm:grid-cols-2">
  - [Een AI-agent met Formstep verbinden](/nl/guides/ai-agents/connect) — Stapsgewijze installatie voor elke MCP-client
  - [Webhooks-referentie](/nl/developers/webhooks-reference) — Payload-schema en ondertekening
  - [REST API](/nl/developers/rest-api) — API-methoden voor programmatische toegang
  - [Abonnementen & prijzen](/nl/subscription-billing/plans-pricing) — Vergelijk API-toegang en limieten per abonnement
</div>
