Feedback MCP
Collect and analyze user feedback from any app via a single API endpoint, with MCP tools for listing, searching, and stats.
README
<p align="center"> <img src="assets/exports/banner/banner.png" alt="Feedback MCP: collect user feedback from any app, analyze it with Claude" width="100%" /> </p>
<p align="center"> <a href="https://github.com/Parra-Inc/feedback-mcp/actions/workflows/ci.yml"><img src="https://github.com/Parra-Inc/feedback-mcp/actions/workflows/ci.yml/badge.svg" alt="CI" /></a> <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-34d399" alt="MIT License" /></a> <img src="https://img.shields.io/badge/PRs-welcome-34d399" alt="PRs welcome" /> <img src="https://img.shields.io/badge/Next.js-16-black" alt="Next.js 16" /> <img src="https://img.shields.io/badge/Prisma-7-2D3748" alt="Prisma 7" /> <img src="https://img.shields.io/badge/MCP-streamable_http-7dd3fc" alt="MCP" /> </p>
Feedback MCP is a free, open-source, self-hosted service for collecting and analyzing user feedback. Your apps submit feedback to one API endpoint. You (and your AI assistant) read it back through a built-in MCP server.
There is intentionally no dashboard. Projects and forms are declarative JSON config, feedback lives in your own database, and analysis happens in Claude (or any MCP client): "summarize this week's bug reports", "what are users asking for most on iOS?".
- One endpoint in.
POST /api/v1/feedbackfrom iOS, Android, web, or any backend. - MCP out.
list_feedback,search_feedback,feedback_stats, and more at/api/mcp. - Forms as config. Each form declares a field schema; submissions are validated with Zod.
- Your database. PostgreSQL or SQLite, chosen with one env var.
- Slack cross-posting. Optional webhook posts every submission to your team channel.
Quickstart (Docker)
git clone https://github.com/Parra-Inc/feedback-mcp.git
cd feedback-mcp
cp apps/server/.env.example .env
# edit .env: set MCP_SECRET and EXAMPLE_APP_INGEST_KEY (openssl rand -hex 32)
docker compose up -d # SQLite, zero external dependencies
Prefer PostgreSQL?
docker compose -f docker-compose.postgres.yml up -d
The server listens on http://localhost:3000. Check it:
curl http://localhost:3000/api/health
# {"status":"ok","database":"ok","config":"ok","projects":1}
Submit your first feedback:
curl -X POST http://localhost:3000/api/v1/feedback \
-H "Content-Type: application/json" \
-H "X-Feedback-Key: $EXAMPLE_APP_INGEST_KEY" \
-d '{
"project": "example-app",
"form": "bug-report",
"platform": "ios",
"data": {
"title": "Crash on launch",
"description": "The app closes immediately after opening.",
"severity": "high"
},
"metadata": { "appVersion": "1.2.0" }
}'
# {"feedback":{"id":"fb_...","createdAt":"..."}}
One-click deploys
Connect Claude
The MCP server is served over streamable HTTP at /api/mcp. Two ways to authenticate:
claude.ai and Claude Desktop (OAuth)
Add a custom connector with the URL https://feedback.your-domain.com/api/mcp. The server implements the MCP OAuth flow (discovery, dynamic client registration, PKCE): claude.ai opens an approval page where you enter your MCP_SECRET once, and tokens are issued from there. Rotating MCP_SECRET revokes every issued token.
Claude Code
claude mcp add --transport http feedback https://feedback.your-domain.com/api/mcp \
--header "Authorization: Bearer <MCP_SECRET>"
Any MCP client (.mcp.json)
{
"mcpServers": {
"feedback": {
"command": "npx",
"args": [
"-y", "mcp-remote",
"https://feedback.your-domain.com/api/mcp",
"--header", "Authorization: Bearer ${MCP_SECRET}"
]
}
}
}
MCP tools
| Tool | What it does |
|---|---|
list_projects |
All configured projects with their platforms and forms |
get_project |
One project by slug |
list_forms / get_form |
Form definitions, including field schemas |
list_feedback |
Feedback for a project, newest first, with form / platform / date filters and cursor pagination |
get_feedback |
A single submission by id |
search_feedback |
Full-text search across submission data and metadata |
feedback_stats |
Counts grouped by platform, form, or day |
Configuration
Projects and forms are files, not database rows. The server loads and validates everything under config/ at boot (and on every request in dev), so adding a project is a pull request, and LLMs can edit your forms as easily as you can.
apps/server/config/
projects/
example-app/
project.json
forms/
bug-report.json
feature-request.json
project.json
{
"slug": "example-app",
"name": "Example App",
"platforms": ["ios", "android", "web"],
"ingestKeys": [{ "id": "default", "secretEnv": "EXAMPLE_APP_INGEST_KEY" }],
"auth": {
"jwt": {
"issuer": "https://auth.your-domain.com",
"audience": "example-app",
"algorithms": ["RS256"],
"jwksUrl": "https://auth.your-domain.com/.well-known/jwks.json",
"required": false
}
},
"slackWebhookEnv": "EXAMPLE_APP_SLACK_WEBHOOK"
}
| Field | Required | Description |
|---|---|---|
slug |
yes | Must match the directory name |
name |
yes | Display name |
description |
no | Shown in the read API and MCP |
platforms |
no | Allowed platform values for submissions. Omit to allow any. |
ingestKeys |
yes | Keys that authorize submissions. secretEnv names the env var holding the secret, so no secrets live in git. |
auth.jwt |
no | Verify end-user tokens on submission (see below) |
slackWebhookEnv |
no | Env var naming a per-project Slack webhook (overrides SLACK_WEBHOOK_URL) |
Form files (forms/<slug>.json)
{
"slug": "bug-report",
"name": "Bug Report",
"fields": [
{ "name": "title", "type": "string", "required": true, "max": 120 },
{ "name": "description", "type": "string", "required": true },
{ "name": "severity", "type": "enum", "values": ["low", "medium", "high", "critical"] },
{ "name": "email", "type": "email" }
]
}
Submissions are validated against the form's fields with Zod. Unknown keys are rejected.
| Field type | Options | Validates as |
|---|---|---|
string |
min, max, pattern |
string with length / regex constraints |
number |
min, max |
number in range |
boolean |
boolean | |
enum |
values (required) |
one of the listed strings |
email |
email address | |
url |
URL | |
date |
ISO 8601 date or datetime |
Every field also accepts label, description, and required (default false).
Authentication
Three separate credentials, three separate jobs:
| Credential | Sent as | Grants |
|---|---|---|
| Ingest key | X-Feedback-Key: <key> |
Submitting feedback to one project. Safe to embed in clients as a spam deterrent; treat it as public. |
| End-user JWT (optional) | Authorization: Bearer <jwt> on submission |
Attaches a verified user identity (sub) to the feedback. You configure how your tokens are verified per project: jwksUrl, publicKeyEnv (PEM), or secretEnv (HMAC), plus optional issuer, audience, algorithms, and required. |
| MCP secret | Authorization: Bearer <MCP_SECRET> |
Reading everything: the admin REST API and the MCP server. Keep it secret. |
REST API
Ingest (CORS-open, ingest key):
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/feedback |
Submit feedback: { project, form, platform?, data, metadata? } |
Read and manage (requires Authorization: Bearer <MCP_SECRET>):
| Method | Path | Description |
|---|---|---|
GET |
/api/v1/projects |
List projects |
GET |
/api/v1/projects/:slug |
One project |
GET |
/api/v1/projects/:slug/forms |
Forms for a project |
GET |
/api/v1/projects/:slug/feedback |
Feedback with form, platform, since, until, limit, cursor query params |
DELETE |
/api/v1/projects/:slug/feedback |
Bulk delete with user, form, platform, before filters (or all=true). Covers GDPR erasure requests. |
GET |
/api/v1/projects/:slug/export |
Stream every submission as NDJSON (backups, portability) |
GET |
/api/v1/feedback/:id |
One submission |
DELETE |
/api/v1/feedback/:id |
Delete one submission |
GET |
/api/health |
Health check (no auth) |
Rate limiting
The ingest endpoint is rate limited out of the box: per client IP (default 60/min) and per project (default 600/min), returning 429 with a Retry-After header. Tune or disable with RATE_LIMIT_IP_PER_MINUTE and RATE_LIMIT_PROJECT_PER_MINUTE (0 disables). The limiter is in-process; if you run multiple replicas or serverless, add a shared limit at your reverse proxy.
Data lifecycle
- Delete: remove a single submission by id, or bulk delete by user, form, platform, or age (see the table above). Deleting by
userhandles GDPR/CCPA erasure requests. - Export: stream a project's entire history as NDJSON for backups or offline analysis.
- Retention: set
FEEDBACK_RETENTION_DAYSand feedback older than the window is deleted automatically (swept at most hourly, piggybacking on ingest traffic; no cron needed).
Environment variables
| Variable | Required | Description |
|---|---|---|
MCP_SECRET |
yes | Bearer secret for the MCP server, admin API, and OAuth flow. openssl rand -hex 32 |
DATABASE_PROVIDER |
no | postgresql (default) or sqlite |
DATABASE_URL |
no | Connection string. Defaults: local Postgres on :5457, or file:./data/feedback.db for SQLite |
SLACK_WEBHOOK_URL |
no | Slack incoming webhook; every submission is cross-posted after the database write |
RATE_LIMIT_IP_PER_MINUTE |
no | Ingest requests per minute per client IP (default 60, 0 disables) |
RATE_LIMIT_PROJECT_PER_MINUTE |
no | Ingest requests per minute per project (default 600, 0 disables) |
FEEDBACK_RETENTION_DAYS |
no | Auto-delete feedback older than this many days (unset keeps everything) |
PUBLIC_URL |
no | Public origin used in OAuth discovery metadata when behind a proxy, e.g. https://feedback.your-domain.com |
CONFIG_DIR |
no | Override the config directory (default config/ in the app root) |
| per-project vars | Whatever your project.json files reference via secretEnv, slackWebhookEnv, publicKeyEnv |
See apps/server/.env.example for a documented template.
Databases
The Prisma schema is a single portable Feedback table, so switching providers is one env var:
- PostgreSQL (default): production-ready, uses the
@prisma/adapter-pgdriver adapter, with real migration history (prisma migrate deployruns on container start). - SQLite: perfect for a single container with a volume. Zero external services. Schema is applied with
prisma db push, which refuses destructive changes. - MongoDB: on the roadmap, currently blocked on a Prisma 7 driver adapter.
The provider is baked into the Prisma schema at generate time; pnpm db:sync (or the Docker entrypoint) rewrites it from DATABASE_PROVIDER automatically.
Slack cross-posting
Set SLACK_WEBHOOK_URL (or a per-project slackWebhookEnv) and every accepted submission is posted to Slack as a Block Kit message with the project, form, platform, user, and submitted fields. Posting happens after the database write and never fails a request: if Slack is down, you just miss the ping, not the feedback.
Development
pnpm install
pnpm up # local Postgres on :5457 (or use sqlite below)
pnpm db:sync # generate client + push schema
MCP_SECRET=dev EXAMPLE_APP_INGEST_KEY=dev pnpm dev # server on :3060
- SQLite instead:
DATABASE_PROVIDER=sqlite pnpm db:sync && DATABASE_PROVIDER=sqlite ... pnpm dev - Unit tests:
pnpm --filter @feedback-mcp/server test - End-to-end smoke test:
pnpm --filter @feedback-mcp/server smoke - Prisma Studio:
pnpm --filter @feedback-mcp/server db:studio(:5560) - Marketing site:
pnpm dev:site(:3061), deployed to GitHub Pages fromapps/site
Repo layout:
apps/server the self-hosted app: ingest API + read API + MCP server + OAuth
apps/site the marketing one-pager (static export, GitHub Pages)
assets open-assets project for the banner and social images
examples copy-paste client snippets (Swift, TypeScript)
See CONTRIBUTING.md for the full guide and SECURITY.md for reporting vulnerabilities. Client integration snippets live in examples/.
Roadmap and non-goals
Planned:
- MongoDB support (blocked on a Prisma 7 driver adapter)
- A
delete_feedbackMCP tool (destructive operations are REST-only for now) - Drop-in feedback form widgets
Non-goals, by design:
- A web dashboard. The read API, MCP tools, and Slack are the interface. Your AI assistant is the dashboard.
- A hosted SaaS. Feedback MCP is self-hosted; your feedback lives in your database.
License
MIT © Parra, Inc.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
E2B
Using MCP to run code via e2b.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.