fix: stop reporting MCP client errors to Sentry - #34
Merged
Conversation
aradng
force-pushed
the
fix/mute-mcp-client-error-noise
branch
2 times, most recently
from
August 20, 2026 15:47
b581082 to
5e31d84
Compare
FastMCP's OpenAPI provider calls the mounted app over HTTP and does
`response.raise_for_status()`, so a 4xx the route legitimately returns
becomes an `HTTPStatusError`, is re-raised as `ValueError(...) from e`, and
reaches `logger.exception` on `fastmcp.server.server`. Sentry's logging
integration turns that record into an issue even though the MCP request
itself answered 200:
mechanism : logging
logger : fastmcp.server.server
handled : yes
trace : status "ok", http.response.status_code 200
So every tool call that hits a 404, 403 or 422 pages the team. Asking for a
row that does not exist is an answer, not a fault.
Dropped in `before_send`, not with a logging filter, because the two are not
interchangeable. A filter on the logger stops the record reaching any
handler, so the line disappears from the console and from logfire as well —
and `_AttrFilter` in `fastloom/logging/utils.py` already encodes the house
rule that ERROR records are never filtered out of logs. `before_send` is
scoped to the Sentry client, so the log still happens and only the issue
does not. `ignore_logger` is Sentry-scoped too but all-or-nothing per logger
name, and a 500 travels the identical path, so it would bury real crashes.
The status is read off the exception rather than the message text:
`raise ValueError(...) from e` leaves the `HTTPStatusError` on `__cause__`.
Two links are checked, the logged exception and its direct `__cause__`,
because that is the shape FastMCP produces. Deliberately not a walk up the
chain:
- `__context__` must not be followed at all. A genuine failure raised
while a 4xx was in flight carries that 4xx as its context, so
suppressing on context would hide the real error.
- a `__cause__` walk needs a termination guard, because a cycle is
reachable from ordinary `raise a from b` / `raise b from a` over reused
instances — no manual `__cause__` assignment required. An unbounded loop
inside `before_send` would hang a worker on the error path, which is a
worse failure than the noise this removes.
Fixing the depth at the shape we actually receive avoids both, with no
sentinel constant to justify. Deeper nesting fails open — the event is
reported, so the outcome is noise rather than a hang.
Only wired when `FASTMCP_INSTALLED`, on the branch that already registers
`MCPIntegration`, and scoped to that one logger so a service's own httpx
4xx handling is untouched.
Verified end to end against a live client: 4xx suppressed with the log line
still emitted, 5xx and non-HTTP failures captured, a context-only 4xx kept,
other loggers untouched, and a cyclic cause chain returns instead of
spinning.
aradng
force-pushed
the
fix/mute-mcp-client-error-noise
branch
from
August 20, 2026 15:52
5e31d84 to
f55cfa2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
FastMCP's OpenAPI provider calls the mounted app over HTTP and does
response.raise_for_status(), so a 4xx the route legitimately returns becomes anHTTPStatusError, is re-raised asValueError(...) from e, and reacheslogger.exceptiononfastmcp.server.server. Sentry's logging integration turns that record into an issue — while the MCP request itself answered 200:So every tool call hitting a 404, 403 or 422 pages the team. Asking for a row that does not exist is an answer, not a fault. Seen in the wild as one issue with 14 occurrences whose entire content is
'Dashboard does not exist'.Why
before_sendand not a logging filterThey are not interchangeable, which is the crux of this change:
_AttrFilterinfastloom/logging/utils.pyalready encodes the house rule against that:if record.levelno >= logging.ERROR: return True.before_sendis scoped to the Sentry client. The log still happens; only the issue does not.Why not
ignore_loggerignore_loggeris also Sentry-scoped, but it is all-or-nothing per logger name. A 500 from a tool travels the identicalraise_for_status → ValueError → logger.exceptionpath, so it would bury genuine crashes.The status comes from the exception, not the message
raise ValueError(...) from eleaves theHTTPStatusErroron__cause__, so it is read from there — no regex over log text.Two links are checked, the logged exception and its direct
__cause__, because that is the shape FastMCP produces. Deliberately not a walk up the chain:__context__must not be followed at all. A genuine failure raised while a 4xx was in flight carries that 4xx as its__context__, so suppressing on context would hide the real error. Tested.__cause__walk needs a termination guard. A cycle is reachable from plainraise a from b/raise b from aover reused instances — no manual__cause__assignment required, verified in 2 hops. An unbounded loop insidebefore_sendwould hang a worker on the error path, a worse failure than the noise being removed.Fixing the depth at the shape we actually receive avoids both, with no sentinel constant to justify. Deeper nesting fails open — the event is reported, so the outcome is noise, never a hang.
Scoped to that one logger, and only wired when
FASTMCP_INSTALLED(the branch that already registersMCPIntegration), so a service's own httpx 4xx handling is untouched.Verified
End to end against a live Sentry client, checking the log line survives:
HTTP error 404HTTP error 403HTTP error 422HTTP error 500HTTP error 502__context__only__cause__chain14 new tests cover each of those plus the no-exception and wrong-logger paths.
125 passedon the full suite, ruff and format clean. (mypycannot run in this checkout —returns.contrib.mypyplugin is missing — and fails identically on unmodifiedmain.)