Connect an AI assistant (MCP)
Point Claude, ChatGPT or your own agent at the Simple Feedback MCP server to build surveys and read results, signing in from the assistant or with an API key.
Die Entwickler-Dokumentation gibt es nur auf Englisch, wie die API selbst.
Simple Feedback speaks the Model Context Protocol (MCP). Connect an AI assistant to it and the assistant can list your venues, build and publish surveys, create QR codes and read responses, statistics and insights. It runs on your assistant and your model; the only call that uses your plan's AI quota is generating insights.
The MCP server uses the same scopes and plan check as the REST API. An assistant connects either by signing in (OAuth, nothing to copy) or with an API key; see Authentication and API keys for keys.
Endpoint
| Setting | Value |
|---|---|
| URL | https://simplefeedback.de/api/mcp |
| Transport | Streamable HTTP, stateless |
| Method | POST only; GET and DELETE answer 405 |
| Auth | Authorization: Bearer <token>: an OAuth access token or an API key |
| Headers | Content-Type: application/json and Accept: application/json, text/event-stream |
There is no MCP session: every request is authenticated on its own.
Sign in from the assistant (OAuth)
Clients that support MCP authorization, such as Claude and Claude Code, need only the URL. On the first connection the client opens a Simple Feedback page in your browser. Sign in, pick the organization, untick any permission you don't want to give, and allow access. An owner or admin of the organization has to do this, and the organization's plan must include API access.
The client receives a token that lasts an hour and refreshes it on its own. The access shows up under Settings → API keys → Connected apps with the app's name and who approved it. There you can change what the app may do, which applies from its next request, or disconnect it, which signs the client out.
For your own client, the server follows the
MCP authorization spec: a request
without a token answers 401 with a WWW-Authenticate header pointing at
https://simplefeedback.de/api/.well-known/oauth-protected-resource/mcp. Registration is dynamic (RFC 7591), the code flow
requires PKCE with S256, and refresh tokens rotate on every use. A refresh token used twice ends the grant.
Scopes are the API scopes, space-separated; a client that asks for none is offered all of them.
Connect a client
Claude
In Claude, add a custom connector with the URL https://simplefeedback.de/api/mcp and connect it. Claude opens the sign-in page.
Claude Code
claude mcp add --transport http simplefeedback "https://simplefeedback.de/api/mcp"Then run /mcp in Claude Code and choose to authenticate. With an API key instead, add
--header "Authorization: Bearer $SF_KEY" to the command.
Any client that supports remote MCP servers
Enter the URL https://simplefeedback.de/api/mcp. If the client can't sign in, add the header
Authorization: Bearer <your API key>.
Clients that only start local servers
Some desktop clients can only launch local (stdio) servers from a config file. Bridge them with the community
package mcp-remote:
{
"mcpServers": {
"simplefeedback": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://simplefeedback.de/api/mcp", "--header", "Authorization: Bearer sfk_live_..."]
}
}
}If your client can add a remote server directly, prefer that. It is one moving part fewer.
Your own agent in TypeScript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const transport = new StreamableHTTPClientTransport(new URL('https://simplefeedback.de/api/mcp'), {
requestInit: { headers: { Authorization: `Bearer ${process.env.SF_KEY}` } },
});
const client = new Client({ name: 'my-integration', version: '1.0.0' });
await client.connect(transport);
const { tools } = await client.listTools();
const me = await client.callTool({ name: 'whoami', arguments: {} });Check the connection with curl
curl -X POST "$SF_API/mcp" \
-H "Authorization: Bearer $SF_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'The answer lists the tools your key may use.
Tools
| Tool | Scope | What it does |
|---|---|---|
about_simplefeedback |
none | What Simple Feedback is and what your plan allows |
whoami |
none | The organization, plan, scopes and venue of the key |
list_venues |
surveys:read |
All venues of the organization |
list_surveys |
surveys:read |
Survey summaries, newest first, paginated |
get_survey |
surveys:read |
One survey with its pages, questions and settings |
create_survey |
surveys:write |
Creates a survey, from a full body or from a short draft |
update_survey |
surveys:write |
Changes a survey; each field sent replaces the old value |
delete_survey |
surveys:write |
Deletes a survey for good |
undo_last_change |
surveys:write |
Puts a survey back the way it was before the last change made through the API |
get_survey_qr |
surveys:read |
The public link and a QR code image of it |
get_plan_fit |
surveys:read |
Surveys beyond the plan, the locked ones and when the grace period ends |
preview_plan_copy |
surveys:read |
What a copy that fits the plan would contain and lose |
create_plan_copy |
surveys:write |
Replaces a survey with a copy that fits; moving QR codes also needs qr:write |
create_qr_code |
qr:write |
A saved QR code with a short link and scan statistics |
list_qr_codes |
qr:read |
Saved QR codes, newest first, paginated |
get_qr_code |
qr:read |
One saved QR code, optionally with its image |
get_qr_code_stats |
qr:read |
Scans per day and platform, and how many ended in a response |
get_qr_codes_stats |
qr:read |
All codes compared: scans, responses, busy hours and hints |
update_qr_code |
qr:write |
Renames a code, switches it off or points it at another survey |
delete_qr_code |
qr:write |
Deletes a saved QR code and its scan statistics for good |
create_access_codes |
access-codes:write |
Creates single-use codes; they are shown only in this result |
list_access_codes |
access-codes:read |
Single-use codes with their status, newest first, paginated |
revoke_access_codes |
access-codes:write |
Revokes the listed codes or every unused one |
get_access_code_qr |
access-codes:read |
The link and a QR code image of one code you hold |
list_responses |
responses:read |
Submitted responses, newest first, paginated |
get_survey_statistics |
responses:read |
Counts, distributions and averages per question |
get_insights |
insights:read |
The stored AI analysis, without generating anything |
generate_insights |
insights:write |
Runs a new AI analysis; the only tool that uses your AI quota |
upload_image |
files:write |
Uploads a survey logo or background; returns the id a survey refers to |
The tool list follows the key's scopes. A read-only key never sees create_survey, so the assistant only plans
with tools that will work. The scope is still checked on every call.
Hints for the client. Every tool carries MCP annotations. Tools that only read are marked readOnlyHint,
delete_survey, delete_qr_code and revoke_access_codes are marked destructiveHint, and the other writing tools are
neither. The tools behind a POST route (create_survey, undo_last_change, create_qr_code, create_plan_copy,
create_access_codes, revoke_access_codes, get_access_code_qr, generate_insights) are also marked
idempotentHint, because they take an idempotencyKey. Clients such as Claude use the hints to decide which calls to confirm with you
first. They are hints only: the key's scopes decide what a call may do.
Retries. The same tools take an optional idempotencyKey, which works like the REST Idempotency-Key header: repeat a call with the same key
after a timeout, and you get the first result instead of a second survey, a second QR code, a second batch of codes or a
second AI run.
Drafts. For a new survey, create_survey takes a short draft: a name, a language, a welcome line and a flat
list of questions. The server builds the pages, ids and presentation defaults from it, exactly as for
POST /v1/surveys.
Undo. Every change made through the API or MCP can be undone one step at a time with undo_last_change, or
with POST /v1/surveys/{id}/undo over REST. It answers NOTHING_TO_UNDO when there is no such change, which
includes edits made in the dashboard, and SURVEY_CHANGED_SINCE when someone edited the survey afterwards. It
doesn't bring back a deleted survey.
QR codes. get_survey_qr returns a throwaway image that is recorded nowhere. Pass includeImage: false when
the link is all you need; the PNG is tens of kilobytes of context. create_qr_code creates a code
that shows up in the dashboard, has a short link and counts scans; it counts against your plan's QR code limit.
Single-use codes. For a survey that each person may answer only once with a personal code, the assistant sets
settings.participation.accessMode to code with update_survey and creates the codes with
create_access_codes. The codes are in that result only, so it hands them to you right away. See
Single-use codes.
After a downgrade. Surveys that use more than the new plan allows (more pages or languages, custom branding, a
Google review link) keep working until the grace period ends; then they are locked and respondents see a pause page.
get_plan_fit lists them with what each one goes beyond. preview_plan_copy shows the copy that would fit, and
create_plan_copy creates it, moves the QR codes you list in qrCodeIds to it and locks the original for good. The
copy starts without responses; the original's stay readable. List the survey's codes with list_qr_codes first:
a code you leave out keeps opening the locked original. Over REST the same is GET /v1/surveys/plan-fit and
GET / POST /v1/surveys/{id}/plan-copy.
Resources
The server publishes four resources. The schemas describe the full survey body that get_survey returns and
update_survey accepts; a body that matches them is a body the server accepts.
| URI | Type | Content |
|---|---|---|
simplefeedback://schema/survey-modules |
application/schema+json |
JSON Schema of a survey's pages and every question type |
simplefeedback://schema/survey-settings |
application/schema+json |
JSON Schema of the survey settings |
simplefeedback://schema/survey-branding |
application/schema+json |
JSON Schema of the custom header and footer |
simplefeedback://guide/getting-started |
text/markdown |
A short walkthrough written for the model |
Errors
A failed tool call is a normal result marked isError. Its text starts with the same CODE: message the REST API
uses, followed by details as JSON when there are any:
{
"isError": true,
"content": [{ "type": "text", "text": "SURVEY_NOT_FOUND: Survey not found" }]
}The codes are listed in Errors. What an assistant should do with the common ones:
| Code | What to do |
|---|---|
INSUFFICIENT_SCOPE |
Don't retry; tell the user which scope the key needs |
VALIDATION_FAILED |
Read the schema resource again and fix the input |
SURVEY_NOT_FOUND |
Wrong id, or another organization's survey |
EMAIL_NOT_VERIFIED |
Don't retry; the user has to verify their email |
TIER_LIMIT_REACHED, FEATURE_NOT_IN_TIER, AI_QUOTA_EXCEEDED |
Don't retry; a person has to upgrade or wait |
SURVEY_CHANGED_SINCE |
Read the survey and make the change the user wants |
RATE_LIMITED |
Wait and retry |
UPSTREAM_ERROR, INTERNAL_ERROR |
Retry later |
When create_survey with active: true creates the survey but can't publish it, details.createdSurveyId names
the draft. Publish it with update_survey once the problem is fixed instead of creating it again.
A rejected key fails before any tool runs. It answers with HTTP 401 or 403 and a JSON-RPC error with code
-32001:
{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32001,
"message": "API_KEY_MISSING: Authorization header with a Bearer API key is required",
"data": { "code": "API_KEY_MISSING" }
}
}Rate limits
MCP budgets are separate from the REST API's. Each JSON-RPC method has its own, and tools/call counts each tool
separately:
| Applies to | Limit |
|---|---|
| Each key, per method or tool | 300 requests per minute |
Each key, get_survey_statistics |
60 requests per minute |
Each key, generate_insights |
2 requests per minute |
| Each organization or IP address | 300 requests per minute |
Over the limit, the request answers HTTP 429 with a Retry-After header and a JSON-RPC error whose data carries
retryAfterSeconds.
Batches. A JSON-RPC batch counts every message in it, and is refused as a whole before anything runs if it would
go over a limit. A batch of more than 20 messages is rejected with HTTP 400 and data.maxBatchSize: 20; send
fewer per request. Current MCP clients send one message per request and never meet this.