Skip to content

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.

from fastmcp import FastMCP
from 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:

Terminal window
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"))
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.

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_feedback
RetrievalMixin(db).register_tools(admin_app, prefix="reports") # reports_list_feedback, ...
ManagementMixin(db).register_tools(admin_app, prefix="admin") # admin_update_feedback_status, ...
MixinTools
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.

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.

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.

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 Context
from 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.tool
async 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 to n calls in chronological order: the current session’s first, then the same user’s within window. It never raises.
  • link_feedback(ref, links) stores them in ffb_feedback_call_links, replacing earlier links for the same id, and returns False instead of raising when it cannot.
  • await mw.feedback_context(ref) reads them back in order with their details.
  • The feedback.submitted event is optional; it lets an embedding sink find similar reports.