tmux-python / tmux-python/libtmux-mcp
Error middleware masks the real tool error when the logging path itself fails
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 13
- Forks
- 0
- Avg merge
- 13h 52m
- Merged PRs (30d)
- 4
Description
Type: bug
Tier: resilience / diagnostics
Tool: all (middleware-wide)
Version: v0.1.0a11
Problem
When the server process outlives a rebuild of its own virtualenv (e.g. a uv sync / dependency bump under a long-lived stdio server), every tool call starts returning the same misleading error:
Internal error: No module named 'rich.traceback'
This message masks both the real failure and the remedy. An agent reading it cannot tell whether its arguments were wrong, the tool is broken, or a retry would help — and the actual remedy (reconnect the server so it re-resolves its environment) is nowhere in the message. In practice this took a whole session of a tool being silently unusable before the cause was understood.
Mechanism
The trigger is environmental, but the masking is a structural defect in the error middleware. In src/libtmux_mcp/middleware.py, ToolErrorResultMiddleware.on_call_tool (v0.1.0a11, lines 333-343) logs the error before converting it to a result:
async def on_call_tool(self, context, call_next):
try:
return await call_next(context)
except Exception as error:
self._log_error(error, context) # ← can itself raise
return _error_tool_result(error, context)
_log_error (lines 292-331) ends with an unguarded log call (lines 318-325):
self.logger.log(
level,
"Error in %s: %s: %s",
method,
error_type,
error,
exc_info=self.include_traceback,
)
When the active logging handler formats that record it lazily imports rich.traceback — fastmcp's RichHandler is built with rich_tracebacks=True and only pulls in rich.traceback the first time it renders an exc_info record (pinned fastmcp>=3.4.0,<4.0.0). If the package moved underneath the running process, that import raises ModuleNotFoundError, _log_error escapes the except, and _error_tool_result never runs. The stock fastmcp transform_errors path then re-wraps the logging exception as the -32603 "Internal error: …", discarding the original tool error entirely.
The asymmetry that pinpoints the gap: the error_callback invocation immediately below (lines 327-331) is already wrapped so a failing callback can't escape — the self.logger.log call on the success path of the same method is not.
Note this is independent of the environmental trigger: any failure inside the logging handler (not just rich.traceback) can replace a real tool error today.
Fix
Never let the error-logging path mask the result — guarantee on_call_tool always reaches _error_tool_result:
except Exception as error:
with contextlib.suppress(Exception):
self._log_error(error, context)
return _error_tool_result(error, context)
Logging inside the suppress would be self-defeating when the handler itself is what's broken, so the suppress stays silent. This mirrors the existing defensive treatment of error_callback.
Secondary (separate follow-up, not required for the fix): fail fast instead of degrading silently — eagerly import the traceback dependency (or run a one-shot self-check) at server startup so a broken environment surfaces loudly at launch rather than on the first tool call. The handshake currently serves tool schemas and instructions fine, which masks the breakage until first execution.
References
src/libtmux_mcp/middleware.py@ v0.1.0a11 —ToolErrorResultMiddleware.on_call_tool(333-343),_log_error(292-331; unguardedlogger.logat 318-325; guardederror_callbackat 327-331).- fastmcp
RichHandlerconfiguration withrich_tracebacksand the lazyrich.tracebackimport on firstexc_inforender (fastmcp>=3.4.0,<4.0.0). - Scope: not covered by the recent error-handling work (
is_errorpreservation, argument-schema classification, unrecognized-argument suggestions) — those refine classification, none guard the logging call — nor by #49, #61, #17, or #50-#55.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/libtmux_mcp/middleware.py at ToolErrorResultMiddleware.on_call_tool and compare the unguarded _log_error call with the guarded error_callback path. Exercise the middleware with a tool failure and a logging-handler failure; done means the original tool error still reaches _error_tool_result rather than being replaced by the logging exception.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 85/100