Middleware¶
Middleware for libtmux MCP server.
Provides three pieces of infrastructure:
SafetyMiddlewaregates tools by safety tier based on theLIBTMUX_SAFETYenvironment variable. Tools tagged above the configured tier are hidden from listing and blocked from execution.AuditMiddlewareemits a structured log record for each tool invocation (name, duration, outcome, client/request ids, and a summary of arguments with payload-bearing fields redacted to a length + SHA-256 prefix).TailPreservingResponseLimitingMiddlewareis a backstop cap for oversized tool output. Unlike FastMCP’s stockResponseLimitingMiddlewareit preserves the tail of the response — terminal scrollback has its active prompt and the output agents actually need at the bottom, so dropping the head is always the correct direction.
-
class libtmux_mcp.middleware.SafetyMiddleware¶class libtmux_mcp.middleware.SafetyMiddleware¶
Bases:
MiddlewareGate tools by safety tier.
- Parameters:
max_tier (
str) – Maximum allowed tier. One ofTAG_READONLY,TAG_MUTATING, orTAG_DESTRUCTIVE.
-
Argument names that carry user-supplied payloads we never want in logs.
keys(send_keys),text(paste_text),value(set_environment),content(load_buffer),shell(respawn_pane), andenvironment(respawn_pane) can contain commands, secrets, or arbitrary large strings. Matched by exact name, case-sensitive, to mirror the tool signatures.environmentis dict-shaped (dict[str, str]); the redaction logic in_summarize_args()recognises this and digests each value while leaving the keys (env var names likeDATABASE_URL) visible — env var names are operator-debug-useful, but their values are the secret. All other entries are scalar strings; mixing the two is intentional.Note on
shellandenvironmentredaction: this redacts the MCP audit log only.respawn_pane(shell="env SECRET=... bash")andenvironment={"AWS_SECRET_KEY": "..."}may briefly expose the values via the OS process table and tmux’spane_current_commandmetadata until the spawned shell takes over — seedocs/topics/safety.md.
String arguments longer than this get truncated in the log summary to keep records bounded. Non-sensitive strings only — sensitive ones are replaced entirely by their digest.
-
libtmux_mcp.middleware._redact_digest(value)¶libtmux_mcp.middleware._redact_digest(value)¶
Return a length + SHA-256 prefix summary of
value.The digest is stable and deterministic, which lets operators correlate the same payload across log lines without ever recording the payload itself.
Examples
>>> _redact_digest("hello") {'len': 5, 'sha256_prefix': '2cf24dba5fb0'} >>> _redact_digest("") {'len': 0, 'sha256_prefix': 'e3b0c44298fc'}
-
libtmux_mcp.middleware._summarize_args(args)¶libtmux_mcp.middleware._summarize_args(args)¶
Summarize tool arguments for audit logging.
Sensitive keys get replaced by a digest; over-long strings get truncated with a marker; everything else passes through as-is. Sensitive values that are dict-shaped (e.g.
environmentonrespawn_pane) have each value digested while keys remain visible — env-var-name-like keys are operator-debug-useful and rarely sensitive, while their values usually are.Examples
Non-sensitive scalars pass through unchanged:
>>> _summarize_args({"pane_id": "%1", "bracket": True}) {'pane_id': '%1', 'bracket': True}
Sensitive payload names are replaced by a digest dict:
>>> _summarize_args({"keys": "rm -rf /"})["keys"]["len"] 8
Sensitive dict-shaped payloads keep their keys but digest values:
>>> redacted = _summarize_args({"environment": {"FOO": "bar"}}) >>> redacted["environment"]["FOO"]["len"] 3 >>> "bar" in str(redacted) False
-
class libtmux_mcp.middleware.AuditMiddleware¶class libtmux_mcp.middleware.AuditMiddleware¶
Bases:
MiddlewareEmit a structured log record per tool invocation.
Records carry: tool name, outcome (ok/error), duration in ms, error type on failure, the fastmcp client_id / request_id when available, and a redacted argument summary. The logger name is
libtmux_mcp.auditby default so operators can route it independently (e.g. to a JSON file) without touching the modulelibtmux_mcplogger.
-
libtmux_mcp.middleware.DEFAULT_RESPONSE_LIMIT_BYTES = 50000¶libtmux_mcp.middleware.DEFAULT_RESPONSE_LIMIT_BYTES = 50000¶
Default byte ceiling for
TailPreservingResponseLimitingMiddleware. Chosen strictly above the per-toolmax_linescaps (500 lines x ~100 bytes/line) so normal operation does not trip the middleware — it only fires when a tool forgot to declare its own cap or the user opted out viamax_lines=None.
-
class libtmux_mcp.middleware.ReadonlyRetryMiddleware¶class libtmux_mcp.middleware.ReadonlyRetryMiddleware¶
Bases:
MiddlewareRetry transient libtmux failures, but only for readonly tools.
Wraps fastmcp’s
fastmcp.server.middleware.error_handling.RetryMiddlewareso retries are bounded by the safety tier the tool is registered under. Mutating and destructive tools (send_keys,create_session,kill_server, …) pass straight through — re-running them on a transient socket error would silently double side effects, which is unacceptable. Readonly tools (list_sessions,capture_pane,snapshot_pane, …) are safe to retry because they observe state without mutating it.Default retry trigger is
libtmux.exc.LibTmuxException— libtmux wraps the subprocess failures we actually want to retry (socket EAGAIN, transient connect errors). The fastmcp default(ConnectionError, TimeoutError)does NOT match these, so the upstream defaults would be a silent no-op.Place this in the middleware stack inside
AuditMiddleware(so retried calls are audited once each) and outsideSafetyMiddleware(so tier-denied tools never reach retry).
-
libtmux_mcp.middleware._TRUNCATION_HEADER_TEMPLATE = '[... truncated {dropped} bytes ...]\n'¶libtmux_mcp.middleware._TRUNCATION_HEADER_TEMPLATE = '[... truncated {dropped} bytes ...]\n'¶
Header prefixed to a truncated response. Intentionally matches the format used by the per-tool
capture_panetruncation so clients see a consistent marker regardless of which layer fired.
-
class libtmux_mcp.middleware.TailPreservingResponseLimitingMiddleware¶class libtmux_mcp.middleware.TailPreservingResponseLimitingMiddleware¶
Bases:
ResponseLimitingMiddlewareResponse-limiter that keeps the tail of oversized output.
FastMCP’s stock
ResponseLimitingMiddlewaretruncates the tail of the response (keeps the start, appends a suffix). That’s exactly wrong for terminal scrollback, where the active shell prompt and most recent command output live at the bottom of the buffer. This subclass overrides_truncate_to_resultto drop the head instead, prefixing a single truncation-header line so callers can detect the cap fired.Used as a global backstop for
libtmux_mcp.tools.pane_tools.io.capture_pane(),libtmux_mcp.tools.pane_tools.meta.snapshot_pane(), andlibtmux_mcp.tools.pane_tools.search.search_panes(). Per-tool caps at the tool layer fire first under normal operation; this middleware catches pathological output from future tools that forget to declare their own bounds.