Skip to content

Collect feedback linked to the calls behind it

A report that says “the forecast tool is broken” is more useful with the calls that came before it attached. In this tutorial you add the five feedback tools to an instrumented server, file a report the way a model would, and read back the calls it was linked to.

It builds on Instrument a FastMCP server in five minutes; the code below is complete on its own, so you can start here too.

  1. Install the package with the instrumentation extra, if you have not already:

    Terminal window
    uv add "fastmcp-feedback[instrumentation]"
  2. Create support.py. It is the weather server again, plus an identity resolver and the feedback tools:

    from fastmcp import FastMCP
    from fastmcp_feedback import add_feedback_tools
    from fastmcp_feedback.instrumentation import DatabaseSink, instrument
    app = FastMCP("Weather")
    @app.tool
    def forecast(city: str) -> dict:
    """Tomorrow's high for a city."""
    if city == "Atlantis":
    return {"ok": False, "error": "no such city"}
    return {"city": city, "high_c": 21}
    def who(context):
    """Who is calling. A real server reads its auth state here."""
    return {"user_sub": "demo-user"}
    mw = instrument(
    app,
    [DatabaseSink("sqlite+aiosqlite:///calls.db", create_tables=True)],
    identity_resolver=who,
    )
    add_feedback_tools(app, database_url="sqlite:///feedback.db", instrumentation=mw)

    add_feedback_tools registers submit_feedback, list_feedback, get_feedback_statistics, update_feedback_status and delete_feedback. Passing instrumentation=mw makes submit_feedback link each new report to the calls before it.

    The identity resolver matters here. Clients on MCP protocol 2026-07-28, including FastMCP 4’s own Client, do not keep a session, so calls are matched to a report by user instead. How correlation works explains why.

  3. Add a session that uses the tool, hits the failure, and reports it. Note that submit_feedback takes a single request object:

    import asyncio
    from fastmcp import Client
    async def main():
    async with Client(app) as client:
    await client.call_tool("forecast", {"city": "Lisbon"})
    await client.call_tool("forecast", {"city": "Atlantis"})
    result = await client.call_tool("submit_feedback", {"request": {
    "type": "bug",
    "title": "forecast fails for some cities",
    "description": "Atlantis returns ok=False with 'no such city'.",
    "submitter": "tutorial-model",
    }})
    report = result.structured_content
    print(report)
    for call in await mw.feedback_context(report["feedback_id"]):
    print(call["position"], call["tool"], call["outcome"], call["rule"],
    call["error_message"] or "")
    await mw.aclose()
    if __name__ == "__main__":
    asyncio.run(main())
  4. Run it:

    Terminal window
    uv run support.py
    {'success': True, 'feedback_id': '1', 'message': 'Feedback submitted successfully', 'linked_calls': 2}
    0 forecast ok user_window
    1 forecast soft_error user_window ok=False: no such city

    The report was stored in feedback.db and linked to the two forecast calls, in the order they ran, each with its outcome. rule says why each call matched: user_window means the same user_sub within the last 15 minutes.

  5. List the feedback the way a maintainer would, with the same client:

    async def review():
    async with Client(app) as client:
    page = await client.call_tool("list_feedback", {"status_filter": "open"})
    for item in page.structured_content["feedback"]:
    print(item["id"], item["type"], item["status"], item["title"])
    await client.call_tool(
    "update_feedback_status",
    {"feedback_id": "1", "new_status": "in_progress"},
    )
    await mw.aclose()
    asyncio.run(review())
    1 bug open forecast fails for some cities

Your server now accepts bug reports, feature requests, improvements and questions through MCP, stores them in feedback.db, and keeps a link from each report to the calls behind it in calls.db (table ffb_feedback_call_links). Each report is also recorded as a feedback.submitted event, which is what an embedding sink uses to find similar reports later.