Add the feedback tools
The feedback tools let people and models file bug reports, feature requests, improvements and questions through MCP. They work with or without the instrumentation; with it, each report is linked to the calls behind it.
All five tools
Section titled “All five tools”from fastmcp import FastMCPfrom fastmcp_feedback import add_feedback_tools
app = FastMCP("My Server")add_feedback_tools(app, database_url="sqlite:///feedback.db")This registers submit_feedback, list_feedback, get_feedback_statistics,
update_feedback_status and delete_feedback. Their arguments and responses
are in the feedback tools reference.
Feedback is stored with synchronous SQLAlchemy in a table named feedback,
created on first use. Pass a database_url: without one the tools use an
in-memory SQLite database, which loses everything when the process exits.
For PostgreSQL, install the sync driver and use a plain postgresql:// URL:
uv add "fastmcp-feedback[postgresql]"import os
app = FastMCP("My Server")add_feedback_tools(app, database_url=os.environ.get("FEEDBACK_DATABASE_URL", "sqlite:///feedback.db"))Prefix the tool names
Section titled “Prefix the tool names”app = FastMCP("My Server")add_feedback_tools(app, database_url="sqlite:///feedback.db", prefix="support")# support_submit_feedback, support_list_feedback, ...The prefix and the tool name are joined with separator (default _), so
leave the trailing underscore off. The prefix changes tool names only; the
table is still feedback.
Pick tool groups with mixins
Section titled “Pick tool groups with mixins”Expose submission on a public server and keep the workflow tools for an admin server:
from fastmcp_feedback import ( ManagementMixin, RetrievalMixin, SubmissionMixin, get_database_session,)
public_app = FastMCP("Public")admin_app = FastMCP("Admin")db = get_database_session("sqlite:///feedback.db")
SubmissionMixin(db).register_tools(public_app) # submit_feedbackRetrievalMixin(db).register_tools(admin_app, prefix="reports") # reports_list_feedback, ...ManagementMixin(db).register_tools(admin_app, prefix="admin") # admin_update_feedback_status, ...| Mixin | Tools |
|---|---|
SubmissionMixin(database, insights=None, instrumentation=None) | submit_feedback |
RetrievalMixin(database, insights=None) | list_feedback, get_feedback_statistics |
ManagementMixin(database, insights=None) | update_feedback_status, delete_feedback |
Pass instrumentation=mw to SubmissionMixin to link submissions to calls, as
add_feedback_tools does.
Mount a dedicated feedback server
Section titled “Mount a dedicated feedback server”from fastmcp_feedback import create_feedback_server
main_app = FastMCP("Main")feedback_server = create_feedback_server("Feedback API", database_url="sqlite:///feedback.db")
# Tools appear on main_app as feedback_submit_feedback, feedback_list_feedback, ...main_app.mount(feedback_server, "feedback")mount() works on every supported FastMCP version. import_server() was
removed in FastMCP 4.
Usage analytics
Section titled “Usage analytics”from fastmcp_feedback import FeedbackInsights
app = FastMCP("My Server")add_feedback_tools(app, database_url="sqlite:///feedback.db", insights=FeedbackInsights(enabled=True))Analytics are off unless you pass FeedbackInsights(enabled=True) or set
FEEDBACK_INSIGHTS_ENABLED=true. When on, they record how the feedback tools
are used (type, lengths, timing) in memory, never feedback text or contact
details. They are separate from the per-call instrumentation, which covers every
tool on the server.
Link calls from your own feedback tool
Section titled “Link calls from your own feedback tool”If you already have a feedback tool, keep it and attach the calls yourself.
Feedback ids can be any string (bug-7Q2X, 42):
import secrets
from fastmcp import Contextfrom fastmcp_feedback.instrumentation import DatabaseSink, instrument
app = FastMCP("My Server")mw = instrument( app, [DatabaseSink("sqlite+aiosqlite:///calls.db", create_tables=True)], identity_resolver=lambda context: {"user_sub": "demo-user"}, # see "Add identity")reports: dict[str, str] = {} # stand-in for your storage
@app.toolasync def report_problem(summary: str, ctx: Context) -> dict: ref = f"bug-{secrets.token_hex(3)}" reports[ref] = summary links = await mw.capture_recent_calls(ctx, n=20) await mw.link_feedback(ref, links) mw.record_event("feedback.submitted", key=ref, attrs={"title": summary}) return {"id": ref, "linked_calls": len(links)}capture_recent_calls(ctx=None, n=20, *, user_sub=None, window=timedelta(minutes=15))returns up toncalls in chronological order: the current session’s first, then the same user’s withinwindow. It never raises.link_feedback(ref, links)stores them inffb_feedback_call_links, replacing earlier links for the same id, and returnsFalseinstead of raising when it cannot.await mw.feedback_context(ref)reads them back in order with their details.- The
feedback.submittedevent is optional; it lets an embedding sink find similar reports.