Skip to content

Record arguments and results safely

By default the middleware records sizes, not contents. When you need the arguments and results themselves, for debugging or for replaying a failure, switch to full mode. Secrets are removed before anything reaches a sink.

ModeRecords
offNothing; calls pass straight through
meta (default)Tool, timing, outcome, identity, argument and result sizes
fullAlso the arguments and result, redacted

Set it in code:

from fastmcp import FastMCP
from fastmcp_feedback.instrumentation import MemorySink, instrument
app = FastMCP("My Server")
sink = MemorySink()
mw = instrument(app, [sink], mode="full")

or with the environment, which is read when the middleware is created and applies when mode is not passed:

Terminal window
FEEDBACK_INSTRUMENTATION_MODE=full uv run server.py

Tools whose arguments are sensitive by nature (code to run, credentials to create) can stay at meta while the rest of the server records in full:

mw = instrument(app, [sink], mode="full", meta_only_tools={"run_code", "create_token"})

Redaction applies to arguments, results, error messages, identity and enricher output, and event attributes. It works two ways:

  • By key, at any depth: a dict value is replaced with [REDACTED] when its key, lowercased with - turned into _, is one of token, password, authorization, cookie, secret, client_secret, api_key, private_key and similar, or ends in _token, _secret, _password, _api_key, _apikey or _private_key.
  • By value, anywhere in text: JWTs, Bearer ..., sk-... API keys, GitHub, Slack, AWS, PyPI and bmcp_ tokens, and password=..., token: ... style pairs in error text.

The full lists are in the redaction reference.

import asyncio
from fastmcp import Client
@app.tool
def deploy(service: str, github_token: str) -> dict:
return {"service": service, "log": "pushed with Bearer abcdef0123456789"}
async def main():
async with Client(app) as client:
await client.call_tool("deploy", {"service": "api", "github_token": "ghp_x"})
await mw.flush()
call = sink.calls[-1]
print(call.args) # {'service': 'api', 'github_token': '[REDACTED]'}
print(call.result) # {'service': 'api', 'log': 'pushed with [REDACTED]'}
asyncio.run(main())

Sizes (args_size, result_size) are measured before redaction.

Pass a Redactor with extra keys and regular expressions. They are added to the defaults, not substituted for them:

from fastmcp_feedback.instrumentation import Redactor, instrument
app = FastMCP("Licensed Server")
mw = instrument(
app,
mode="full",
redactor=Redactor(extra_keys=["license"], extra_patterns=[r"acme_[A-Za-z0-9]{32}"]),
)

To replace a default list, pass keys= or patterns= instead. The same redactor is used for embedded text when you add an embedding sink.