10 KiB
10 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Calendar Versioning (YYYY.MM.DD).
[2026.09.28] - 2026-09-28
Keep the tool-call table small: leave out or sample busy tools, and prune old rows.
Added
InstrumentationMiddleware(..., exclude_tools=...): tools that are never recorded and never kept for feedback links. The calls themselves are untouched.InstrumentationMiddleware(..., sample_rates={"tool": 0.01}): record that fraction of a tool'sokcalls. Failures are always recorded: calls with outcomeerrororsoft_errorare kept whatever the rate. Rates outside (0, 1] raiseValueError. Calls sampled out skip the hooks and redaction and are not kept for feedback links.random=takes a() -> floatfor tests.- Records have a
sample_rate: the configured rate on sampled-inokrows, None on everything recorded unconditionally (including failures of sampled tools).SUM(1.0 / COALESCE(sample_rate, 1))estimates the true call count. DatabaseSink(..., retention=timedelta(days=30), prune_interval=..., prune_batch=5000). Withretentionset, the background writer prunes after an insert at most once perprune_interval(one hour by default). A failing prune is logged and never stops the insert.DatabaseSink.prune(older_than=None) -> intdeletes older rows in batches ofprune_batch, one transaction each, on SQLite and Postgres. Calls linked to feedback are never pruned, and links are never deleted.
Migration
- New nullable
sample_ratecolumn (Float) on{prefix}tool_calls. Hosts that manage the schema with Alembic need a migration adding it.create_tables=Truedoes not alter a table that already exists, so add it by hand there (ALTER TABLE ffb_tool_calls ADD COLUMN sample_rate FLOAT). Until the column exists,DatabaseSinkwrites fail and those records are dropped (logged). Hosts still on 2026.09.27.3 or earlier also need theoutcomecolumn from 2026.09.27.4.
[2026.09.27.4] - 2026-09-27
Count errors that tools return as data instead of raising.
Added
- Records have an
outcome:ok,soft_error(the tool returned a result flagged as a failure) orerror(the tool raised). Soft errors geterror_type="SoftError"and anerror_messagebuilt from the result, such asstatus=failed: disk full, redacted and truncated tomax_error_chars. soft_error_classifier(statuses=..., status_keys=..., flag_keys=..., extra_statuses=...)builds a classifier that flags dict results whereokorsuccessisFalse, wherestatusorstateiserror,failedorfailure, or that the tool markedisErrorwithout raising.DEFAULT_RESULT_CLASSIFIERissoft_error_classifier().InstrumentationMiddleware(..., result_classifier=...)takes any(tool, result) -> str | None;Noneturns classification off. A classifier that raises is logged and the call is recorded asok.feedback_context()rows includeoutcome.
Changed
okis nowoutcome == "ok", so it is also false for soft errors. ExistingWHERE NOT okqueries and error rates will rise; filter onoutcome = 'error'for exceptions only.CallLink.okfollows.- The package's own feedback tools answer
{"success": False, ...}on failure, so those calls are now recorded as soft errors.
Migration
- New nullable, indexed
outcomecolumn (String(16)) on{prefix}tool_calls. Hosts that manage the schema with Alembic need a migration adding it.create_tables=Truedoes not alter a table that already exists, so add the column by hand there (ALTER TABLE ffb_tool_calls ADD COLUMN outcome VARCHAR(16)plus an index). Until the column exists,DatabaseSinkwrites fail and those records are dropped (logged). Rows from earlier versions keep NULL.
[2026.09.27.3] - 2026-09-27
Link a feedback item to the tool calls that preceded it.
Added
InstrumentationMiddleware.capture_recent_calls(ctx, n=20)returns the calls that led up to now. It matches the same client session first (sessionrule), then fills remaining slots with calls by the sameuser_subinsidewindow(user_windowrule, 15 minutes by default), and each link records which rule matched.- Calls are kept in an in-memory buffer as they happen, so calls still
waiting in the sink queue count. With a
DatabaseSinkthe lookup falls back to the database, which covers calls from other workers and from before a restart. link_feedback(ref, links)stores links against a feedback id of any format (bug-7Q2X,42) in a new{prefix}feedback_call_linkstable.build_metadata()includes it; hosts that manage the schema with Alembic need a new migration.feedback_context(ref)returns the linked calls in order with tool, outcome, error type and matching rule, filling in calls not yet written from memory.add_feedback_tools(..., instrumentation=mw):submit_feedbacklinks the preceding calls automatically and returnslinked_callsin its response.session_key(ctx)gives a per-client key that stays stable across calls: the HTTPmcp-session-idheader when present, then one key per process for stdio, thenctx.session_id.
Fixed
- With
create_tables=True, the background writer andlink_feedbackcould both runcreate_allat once and fail with "table already exists". Table creation is now serialized by a lock.
Note
- Clients on MCP protocol 2026-07-28 (Claude Code, FastMCP 4's
Client) open no HTTP sessions, so their calls link by user only. HTTP servers should configure anidentity_resolver. Stdio servers link by session with every client.
[2026.09.27.2] - 2026-09-27
Fixed
DatabaseSinkstored missingargs/result(everymetamode row) as the JSON literalnull, soWHERE args IS NULLmatched nothing. They are now SQLNULL. No schema change; rows written by 2026.09.27.1 keep JSONnulland can be backfilled withUPDATE ffb_tool_calls SET args = NULL WHERE args = 'null'::jsonb(same forresult). Reported from production by blender-mcp.
[2026.09.27.1] - 2026-09-27
Per-call instrumentation for any FastMCP server.
Added
fastmcp_feedback.instrumentation:InstrumentationMiddlewareandinstrument(app)record every tool call on the server (tool, timing, outcome, original error type, session and request ids, argument and result sizes). Modesoff,meta(default) andfull, set by argument orFEEDBACK_INSTRUMENTATION_MODE;meta_only_toolskeeps chosen tools atmeta.- Recording never slows or breaks a call: records reach sinks from a
background task through a bounded queue and are dropped (and counted) when it
is full. Sink and hook errors are logged, never raised; async hooks are cut
off after
hook_timeout. Redactor: secrets removed from arguments, results, error text and hook output, by key at any depth and by value pattern (JWT, bearer,sk-, GitHub, Slack, AWS, PyPI andbmcp_tokens,password=...in text).- Sinks:
JsonLinesSink(stderr),CallbackSink,MemorySink, andDatabaseSinkon async SQLAlchemy.DatabaseSinkaccepts the host'sAsyncEngine, writes to a prefixedffb_tool_callstable, never creates tables unless asked, andbuild_metadata()plugs the table into a host's Alembicenv.py. identity_resolverandenricherhooks filluser_sub,caller_kind,client_id,server_versionand free-formidentity/extrafields.instrumentationextra:sqlalchemy[asyncio]andaiosqlite.- CI covers FastMCP 3.x as well as 2.12, 2.14 and 4.x.
[2026.09.27] - 2026-09-27
FastMCP 4 support, and a composition bug that affected every earlier release.
Fixed
compose_feedback_services()composed nothing on FastMCP 2.14. It calledimport_server(), which became async in 2.14, without awaiting it, then logged success anyway. On FastMCP 4, whereimport_server()no longer exists, it raisedAttributeError. It now usesmount(), which is synchronous and available on every supported version.- The test suite could not start (a pytest-html hook with pytest-html not installed), so it had not run since the package was split out. It runs again: 407 tests.
Changed
- Dependency is now
fastmcp>=2.12.2,<5. Earlier releases had no ceiling, so a fresh install picked up FastMCP 4 untested. examples/moved into this repository and ships in the sdist.create_demo_server()accepts an optionaldatabase_url. Examples list and call tools throughfastmcp.Client, which works on 2.x and 4.x.
Added
- CI (Gitea Actions) runs the suite against FastMCP 2.12.2, 2.14.3 and 4.x on every push and weekly, plus a non-blocking job against the newest FastMCP with no ceiling.
- A composition test using real servers instead of mocks.
Documentation
- README rewritten against the code and every Python example executed.
Previous docs described things that do not exist: four tools (there are
five, including
delete_feedback),get_feedback_stats(it isget_feedback_statistics),InsightsMixin, anenable_insightsparameter, feedback typesbug_report/feature_request/general(they arebug/feature/improvement/question), and anew -> reviewing -> plannedworkflow (it isopen -> in_progress -> resolved -> closed). - The default database is in-memory SQLite (
sqlite:///:memory:), notsqlite:///feedback.dbas previously documented. Passdatabase_urlto keep feedback across restarts.
[2026.01.12.1] - 2026-01-12
Fixed
ImportErroron install:mixins/base.pydid not re-export the names the other mixin modules import from it.
[2026.01.12] - 2026-01-12
Added
- Initial public release
- Core feedback tools:
submit_feedback,list_feedback,get_feedback_stats,update_feedback_status add_feedback_tools()one-line integration function- Modular mixin architecture:
SubmissionMixin- feedback collectionRetrievalMixin- listing and statisticsManagementMixin- status workflow managementInsightsMixin- privacy-compliant analytics
- Multi-database support: SQLite, PostgreSQL, MySQL
create_feedback_server()for server composition- Comprehensive test suite (97%+ coverage)
- Full type hints and Pydantic validation
- Documentation site at https://fastmcp-feedback.l.supported.systems
Technical Details
- Built on FastMCP 2.12.2+
- SQLAlchemy 2.0+ ORM
- Pydantic 2.11+ validation
- Python 3.11+ required