Metadata-Version: 2.5
Name: fastmcp-feedback
Version: 2026.09.27.3
Summary: Production-ready feedback collection system for FastMCP servers
Project-URL: Homepage, https://git.supported.systems/fastmcp-feedback/fastmcp-feedback
Project-URL: Documentation, https://fastmcp-feedback.l.supported.systems
Project-URL: Repository, https://git.supported.systems/fastmcp-feedback/fastmcp-feedback.git
Project-URL: Issues, https://git.supported.systems/fastmcp-feedback/fastmcp-feedback/issues
Project-URL: Changelog, https://git.supported.systems/fastmcp-feedback/fastmcp-feedback/blob/main/CHANGELOG.md
Author-email: Ryan Malloy <ryan@supported.systems>
Maintainer-email: Ryan Malloy <ryan@supported.systems>
License-Expression: MIT
License-File: LICENSE
Keywords: analytics,database,enterprise,fastmcp,feedback,mcp,model-context-protocol,privacy-compliant,server
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Database :: Database Engines/Servers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Logging
Classifier: Topic :: System :: Monitoring
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: fastmcp<5,>=2.12.2
Requires-Dist: pydantic>=2.11.7
Requires-Dist: sqlalchemy>=2.0.43
Provides-Extra: all
Requires-Dist: aiosqlite>=0.22.1; extra == 'all'
Requires-Dist: mypy>=1.7.0; extra == 'all'
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'all'
Requires-Dist: pymysql>=1.1.0; extra == 'all'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'all'
Requires-Dist: pytest-cov>=5.0.0; extra == 'all'
Requires-Dist: pytest-html>=4.2.0; extra == 'all'
Requires-Dist: pytest-mock>=3.12.0; extra == 'all'
Requires-Dist: pytest>=8.2.0; extra == 'all'
Requires-Dist: ruff>=0.1.0; extra == 'all'
Requires-Dist: sqlalchemy[asyncio]>=2.0.43; extra == 'all'
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.22.1; extra == 'dev'
Requires-Dist: mypy>=1.7.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
Requires-Dist: pytest-html>=4.2.0; extra == 'dev'
Requires-Dist: pytest-mock>=3.12.0; extra == 'dev'
Requires-Dist: pytest>=8.2.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Requires-Dist: sqlalchemy[asyncio]>=2.0.43; extra == 'dev'
Provides-Extra: instrumentation
Requires-Dist: aiosqlite>=0.22.1; extra == 'instrumentation'
Requires-Dist: sqlalchemy[asyncio]>=2.0.43; extra == 'instrumentation'
Provides-Extra: mysql
Requires-Dist: pymysql>=1.1.0; extra == 'mysql'
Provides-Extra: postgresql
Requires-Dist: psycopg2-binary>=2.9.0; extra == 'postgresql'
Provides-Extra: test
Requires-Dist: aiosqlite>=0.22.1; extra == 'test'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
Requires-Dist: pytest-html>=4.2.0; extra == 'test'
Requires-Dist: pytest-mock>=3.12.0; extra == 'test'
Requires-Dist: pytest>=8.2.0; extra == 'test'
Requires-Dist: sqlalchemy[asyncio]>=2.0.43; extra == 'test'
Description-Content-Type: text/markdown

# FastMCP Feedback

Instrumentation and QA feedback for [FastMCP](https://gofastmcp.com) servers.

Record every tool call your server handles, and let people and models report problems with it, each with a line or two of code.

## Features

- **Per-Call Instrumentation** - Middleware records every tool call (timing, outcome, identity, payload sizes) without slowing or breaking it
- **Secret Redaction** - Tokens and passwords removed from recorded arguments, results and errors by default
- **One-Line Integration** - Add feedback tools to any FastMCP server instantly
- **Modular Mixins** - Selective tool registration for fine-grained control
- **Multi-Database Support** - SQLite (dev), PostgreSQL, MySQL (production)
- **Privacy-Compliant Analytics** - Optional usage insights without sensitive data
- **Type-Safe** - Full Pydantic validation and type hints
- **Well-Tested** - 98% coverage, run against FastMCP 2.12, 2.14, 3.x and 4.x

## Installation

### From PyPI (Recommended)

```bash
# Using uv (recommended)
uv add fastmcp-feedback

# Using pip
pip install fastmcp-feedback

# With PostgreSQL support
uv add "fastmcp-feedback[postgresql]"
```

### From Git (Latest Development)

```bash
# Latest from main branch
uv add git+https://git.supported.systems/fastmcp-feedback/fastmcp-feedback.git

# Specific release tag
uv add git+https://git.supported.systems/fastmcp-feedback/fastmcp-feedback.git@2026.01.12

# Specific branch (for testing PRs)
uv add git+https://git.supported.systems/fastmcp-feedback/fastmcp-feedback.git@feature-branch
```

### Local Development Install

For contributors or local modifications:

```bash
# Clone the repository
git clone git@git.supported.systems:fastmcp-feedback/fastmcp-feedback.git
cd fastmcp-feedback

# Create virtual environment and install in editable mode
uv venv
uv pip install -e ".[dev]"

# Run tests to verify
uv run pytest
```

## Quick Start

### Basic Integration (One Line)

```python
from fastmcp import FastMCP
from fastmcp_feedback import add_feedback_tools

app = FastMCP("My Server")
add_feedback_tools(app, database_url="sqlite:///feedback.db")

# Your server now has five tools:
# - submit_feedback: bug reports, feature requests, improvements, questions
# - list_feedback: page through feedback, filtered by type or status
# - get_feedback_statistics: counts by type and status, plus the last 7 days
# - update_feedback_status: move an item through open -> in_progress -> resolved -> closed
# - delete_feedback: remove an item
```

Pass a `database_url`. Without one the tools use an in-memory SQLite
database, which is handy for tests but loses everything when the process exits.

### PostgreSQL

```python
add_feedback_tools(app, database_url="postgresql://user:pass@db.example.com/feedback")
```

Install the driver with `uv add "fastmcp-feedback[postgresql]"`.

### Prefixed Tool Names

```python
add_feedback_tools(app, database_url="sqlite:///feedback.db", prefix="support")
# support_submit_feedback, support_list_feedback, ...
```

The prefix and tool name are joined with `separator` (default `_`), so leave
the trailing underscore off the prefix.

### Usage Analytics

```python
from fastmcp_feedback import FeedbackInsights

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 enabled they record tool usage and
submission metadata (type, lengths, timing), never feedback text or contact details.

## Instrumentation

Record every tool call on your server:

```python
from fastmcp import FastMCP
from fastmcp_feedback.instrumentation import instrument

app = FastMCP("My Server")
instrument(app)  # one JSON line per tool call on stderr
```

Each record has the tool name, start time, duration, success or error type,
session and request ids, argument and result sizes, and whatever your hooks
add. The recording happens in middleware, so it covers every tool on the server,
not just the feedback ones.

It never slows or breaks a tool call. Records go to the sinks from a background
task through a bounded queue, and when the queue is full they are dropped and
counted instead of waited on. Errors in a sink or a hook are logged and swallowed.

### Modes

| Mode | Records |
|------|---------|
| `off` | Nothing |
| `meta` (default) | Tool, timing, outcome, identity, payload sizes |
| `full` | Also the arguments and result, redacted |

Set it with `mode=` or the `FEEDBACK_INSTRUMENTATION_MODE` environment variable.
`meta_only_tools={"run_code", "create_token"}` keeps chosen tools at `meta`
even in `full` mode.

### Redaction

Secrets are removed from arguments, results, error messages and hook output
before anything reaches a sink. It matches keys at any depth (`password`,
`token`, `authorization`, `client_secret`, `*_api_key`, ...) and secret-shaped
values anywhere in text (JWTs, `Bearer ...`, `sk-...`, GitHub, Slack, AWS and
PyPI tokens, `password=...` in error text). Add your own:

```python
from fastmcp_feedback.instrumentation import Redactor, instrument

instrument(
    app,
    mode="full",
    redactor=Redactor(extra_keys=["license"], extra_patterns=[r"acme_[A-Za-z0-9]{32}"]),
)
```

### Sinks

```python
from fastmcp_feedback.instrumentation import CallbackSink, DatabaseSink, JsonLinesSink, instrument

instrument(app, [
    JsonLinesSink(),                                   # stderr
    DatabaseSink("sqlite+aiosqlite:///calls.db", create_tables=True),
    CallbackSink(lambda records: print(len(records))),  # sync or async
])
```

`DatabaseSink` needs the `instrumentation` extra
(`uv add "fastmcp-feedback[instrumentation]"`), plus `asyncpg` for Postgres.

### Using your own database and migrations

Pass the `AsyncEngine` you already have. Records go to a `ffb_tool_calls`
table (the prefix is configurable), and nothing is created at startup unless you
ask. To manage the table with Alembic, add it to your `env.py`:

```python
from fastmcp_feedback.instrumentation import build_metadata

target_metadata = [Base.metadata, build_metadata(prefix="ffb_")]
```

```python
from fastmcp_feedback.instrumentation import DatabaseSink, instrument

sink = DatabaseSink(engine, prefix="ffb_")  # your engine is never disposed
instrument(app, [sink])
```

### Identity and enrichment hooks

```python
from fastmcp_feedback.instrumentation import instrument

def who(context):
    """Who made this call. May be async."""
    user = context.fastmcp_context  # read your auth state from here
    return {"user_sub": "user-123", "caller_kind": "pat:7", "client_id": "cli"}

async def enrich(tool, args, result, context):
    """Extra indexed fields. May be async."""
    return {"job_id": (result or {}).get("job_id"), "server_version": "1.4.0"}

instrument(app, identity_resolver=who, enricher=enrich)
```

`user_sub`, `caller_kind` and `client_id` become columns; other identity keys
go in `identity`. Enricher output goes in `extra`, except `server_version`,
which has its own column. Async hooks that take longer than `hook_timeout`
(0.25 s by default) are skipped for that call.

### Linking feedback to the calls behind it

When a model reports "the render tool hung", the report is far more useful with
the calls that led up to it attached. Pass the middleware to
`add_feedback_tools` and every `submit_feedback` does this for you:

```python
from fastmcp_feedback import add_feedback_tools
from fastmcp_feedback.instrumentation import DatabaseSink, instrument

mw = instrument(app, [DatabaseSink("sqlite+aiosqlite:///calls.db", create_tables=True)])
add_feedback_tools(app, database_url="sqlite:///feedback.db", instrumentation=mw)
# submit_feedback now answers {"feedback_id": ..., "linked_calls": 3, ...}
```

Read the linked calls back, in order, with their outcomes:

```python
for call in await mw.feedback_context(feedback_id):
    print(call["position"], call["tool"], call["ok"], call["error_type"], call["rule"])
```

If you have your own feedback tools, use the same pieces directly. Feedback ids
can be anything (`bug-7Q2X`, `42`):

```python
from fastmcp import Context

@app.tool
async def report_problem(summary: str, ctx: Context) -> dict:
    ref = save_my_feedback(summary)                 # your storage, your id
    links = await mw.capture_recent_calls(ctx, n=20)
    await mw.link_feedback(ref, links)
    return {"id": ref, "linked_calls": len(links)}
```

Calls are matched in two ways, and each link records which one (`rule`):

- `session`: the same MCP client session. Over HTTP this is the
  `mcp-session-id`; over stdio it is the server process, which serves one client.
- `user_window`: the same `user_sub` within the last 15 minutes (`window=`),
  for clients that open a new session per call (personal access tokens, CI).
  This needs an `identity_resolver`.

Session matches come first, and user matches fill any remaining slots. Calls
still waiting in the queue are included, and with a `DatabaseSink`, so are
calls from other workers or from before a restart.

Links live in a `ffb_feedback_call_links` table next to `ffb_tool_calls`.
`build_metadata()` includes it, so hosts that manage the schema with Alembic
need one new migration before calling `link_feedback`.

**Stateless clients:** clients on MCP protocol 2026-07-28, including Claude
Code and FastMCP 4's own `Client`, do not open sessions over HTTP. Their calls
link by user only, so configure an `identity_resolver` for HTTP servers. Stdio
servers link by session with every client.

### Middleware order and shutdown

FastMCP runs the first middleware added as the outermost. Call `instrument()`
before adding other middleware to include their time and record calls they
reject; call it last to time only the tool. On shutdown, `await mw.aclose()`
flushes queued records and closes the sinks.

## Advanced Usage

### Mixin Architecture

Pick only the tool groups you want to expose:

```python
from fastmcp_feedback import (
    ManagementMixin,
    RetrievalMixin,
    SubmissionMixin,
    get_database_session,
)

db = get_database_session("sqlite:///feedback.db")

# Public server: submission only
SubmissionMixin(db).register_tools(app)

# Reporting tools under a prefix: analytics_list_feedback, ...
RetrievalMixin(db).register_tools(app, prefix="analytics")

# Admin server gets the workflow tools
ManagementMixin(db).register_tools(admin_app, prefix="admin")
```

### Available Mixins

| Mixin | Tools | Use Case |
|-------|-------|----------|
| `SubmissionMixin` | `submit_feedback` | Public feedback collection |
| `RetrievalMixin` | `list_feedback`, `get_feedback_statistics` | Dashboards, reporting |
| `ManagementMixin` | `update_feedback_status`, `delete_feedback` | Admin workflow |

### Multi-Tenant Pattern

```python
# Each tenant gets its own database
def add_tenant_tools(tenant_id: str):
    db = get_database_session(f"sqlite:///data/{tenant_id}.db")
    SubmissionMixin(db).register_tools(app, prefix=f"tenant_{tenant_id}")
```

### Server Composition

```python
from fastmcp_feedback import create_feedback_server

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.

## Compatibility

Tested against FastMCP 2.12, 2.14, 3.x and 4.x on every commit. The dependency is
declared as `fastmcp>=2.12.2,<5`, and the ceiling moves up once a new major
version passes the suite.

## API Reference

### `add_feedback_tools(mcp, database_url=None, insights=None, prefix="", separator="_", instrumentation=None)`

Add all five feedback tools to a FastMCP server.

**Parameters:**
- `mcp`: FastMCP server instance
- `database_url`: SQLAlchemy URL. Defaults to in-memory SQLite (`sqlite:///:memory:`)
- `insights`: a `FeedbackInsights` instance. Defaults to one with analytics disabled
- `prefix`: prepended to every tool name
- `separator`: placed between `prefix` and the tool name
- `instrumentation`: the middleware returned by `instrument()`. When given,
  `submit_feedback` links the calls that preceded it to the new item and
  returns `linked_calls` (see [Linking feedback to the calls behind it](#linking-feedback-to-the-calls-behind-it))

### Feedback Types

- `bug` - something is broken
- `feature` - a new capability
- `improvement` - make an existing capability better
- `question` - unclear behavior or usage

### Status Workflow

`open` → `in_progress` → `resolved` → `closed`

## Documentation

Full documentation: https://fastmcp-feedback.l.supported.systems

- [Quick Start Tutorial](https://fastmcp-feedback.l.supported.systems/tutorials/quickstart/)
- [Architecture](https://fastmcp-feedback.l.supported.systems/explanation/architecture/)
- [API Reference](https://fastmcp-feedback.l.supported.systems/reference/api/)
- [Integration Patterns](https://fastmcp-feedback.l.supported.systems/how-to/integration-patterns/)

## Examples

See the [fastmcp-feedback-showcase](https://git.supported.systems/fastmcp-feedback/fastmcp-example) repository for examples of every feature:

```bash
uvx fastmcp-feedback-showcase  # Run full demo
```

## Contributing

We welcome contributions! FastMCP Feedback is designed to be contributor-friendly.

### Quick Contribution Setup

```bash
# Fork and clone
git clone git@git.supported.systems:YOUR_USERNAME/fastmcp-feedback.git
cd fastmcp-feedback

# Install with dev dependencies
uv venv
uv pip install -e ".[dev]"

# Create feature branch
git checkout -b feature/your-feature

# Make changes, run tests
uv run pytest
uv run ruff check .

# Submit PR
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.

## Versioning

This project uses **calendar versioning** (CalVer): `YYYY.MM.DD`

- `2026.01.12` = Release on January 12, 2026
- Multiple releases on same day: `2026.01.12.1`, `2026.01.12.2`

This makes it clear when each release was made and simplifies dependency management.

## License

MIT License - see [LICENSE](LICENSE) for details.

## Links

- **Documentation**: https://fastmcp-feedback.l.supported.systems
- **Repository**: https://git.supported.systems/fastmcp-feedback/fastmcp-feedback
- **Issues**: https://git.supported.systems/fastmcp-feedback/fastmcp-feedback/issues
- **Showcase**: https://git.supported.systems/fastmcp-feedback/fastmcp-example
- **FastMCP**: https://gofastmcp.com
