Middleware¶
Middleware for libtmux MCP server.
Provides the project’s middleware infrastructure, in definition order:
SafetyMiddlewaregates tools by safety tier based on theLIBTMUX_SAFETYenvironment variable. Tools tagged above the configured tier are hidden from listing and blocked from execution.ToolErrorResultMiddlewareconverts tool-call failures intoToolResult(is_error=True)results that carry the clean error message plus a structuredmetapayload, instead of fastmcp’s stock-32603catch-all that prefixed every expected failure with"Internal error: ".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).ReadonlyRetryMiddlewareretries transient libtmux failures, but only for readonly tools — re-running a mutating tool would silently double side effects.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.
-
libtmux_mcp.middleware._schema_validation_error(error)¶libtmux_mcp.middleware._schema_validation_error(error)¶
Return the Pydantic validation error behind a schema failure.
- Parameters:
error (
BaseException)- Return type:
ValidationError|None
-
libtmux_mcp.middleware._is_schema_validation_error(error)¶libtmux_mcp.middleware._is_schema_validation_error(error)¶
Return True for fastmcp argument-schema validation failures.
fastmcp validates tool arguments against the input schema before tool code runs, raising a bare
pydantic.ValidationError— too early for thehandle_tool_errorsdecorators to classify. Bad arguments are agent-correctable (fix the call and retry), so they get the same expected/WARNING treatment asExpectedToolError.Output validation cannot be mistaken for this case: fastmcp’s tool layer converts output-shape failures into error results itself, so they never reach the middleware as exceptions.
- Parameters:
error (
BaseException)- Return type:
-
libtmux_mcp.middleware._validation_errors_without_inputs(error)¶libtmux_mcp.middleware._validation_errors_without_inputs(error)¶
Return validation errors without rejected input values.
-
libtmux_mcp.middleware._format_schema_validation_error(error)¶libtmux_mcp.middleware._format_schema_validation_error(error)¶
Format a Pydantic validation error without raw input values.
- Parameters:
error (
BaseException)- Return type:
-
libtmux_mcp.middleware._strip_validation_error_inputs(value)¶libtmux_mcp.middleware._strip_validation_error_inputs(value)¶
Remove raw input payloads from structured validation errors.
-
class libtmux_mcp.middleware._FastMCPValidationLogFilter¶class libtmux_mcp.middleware._FastMCPValidationLogFilter¶
Bases:
FilterRedact FastMCP invalid-argument warning payloads.
-
libtmux_mcp.middleware.install_fastmcp_validation_log_filter()¶libtmux_mcp.middleware.install_fastmcp_validation_log_filter()¶
Install the FastMCP validation log redaction filter once.
- Return type:
-
libtmux_mcp.middleware._CLIENT_SCHEDULING_FLAG = 'wait_for_previous'¶libtmux_mcp.middleware._CLIENT_SCHEDULING_FLAG = 'wait_for_previous'¶
Scheduling flag some MCP clients (notably Gemini CLI when batching several tool calls in one turn) merge into the tool’s arguments. Recognized only to word the rejection helpfully — the argument is still rejected, never silently stripped, so genuine argument typos from other clients stay loud. Contrast MemPalace/mempalace#322, which strips the key, and #647, which whitelists arguments against the schema — silent dropping would let a mis-named flag on a mutating tool (e.g.
enteron send_keys) run with defaults.
-
libtmux_mcp.middleware._unexpected_kwargs(error)¶libtmux_mcp.middleware._unexpected_kwargs(error)¶
Argument names rejected as unexpected by schema validation.
Reads pydantic’s structured
errors()fromerroror its__cause__(same posture as_is_schema_validation_error()) and returns the names flaggedunexpected_keyword_argument. Empty list for every other failure shape.- Parameters:
error (
BaseException)- Return type:
-
libtmux_mcp.middleware._client_label(context)¶libtmux_mcp.middleware._client_label(context)¶
"name version"of the connected client, when the handshake exposed it.Walks
fastmcp_context.session.client_params.clientInfo— the MCPinitializehandshake’s client identity. Every hop can be absent (unit-test contexts, background tasks, clients that omitclientInfo), so any failure resolves toNone. Used only to word error suggestions; never gates behavior.
-
libtmux_mcp.middleware._error_tool_result(error, context=None)¶libtmux_mcp.middleware._error_tool_result(error, context=None)¶
Build a rich
ToolResult(is_error=True)from a tool failure.The text block carries the error message exactly as raised — no transform-layer prefix — with the recovery
suggestionappended when the error provides one.metamirrors the details in machine-readable form:error_type— class name of the originating exception (__cause__when the raise site chained one, so agents seePaneNotFoundrather than theToolErrorwrapper).expected— True for agent-correctable failures (ExpectedToolErrorand argument-schema validation errors), False for operator faults and potential server bugs.suggestion— recovery hint. Carried by the error when the raise site provided one; synthesized for schema-validation failures that rejected unexpected arguments, so the agent knows to drop or fix exactly those names (with a client-flag note for_CLIENT_SCHEDULING_FLAGleaks, naming the client viacontextwhen the handshake exposed it).
structured_contentis deliberately left unset: tools declare output schemas for their success payloads, and clients validatestructuredContentagainst them — an error-shaped payload there would fail validation on strict clients.
-
class libtmux_mcp.middleware.ToolErrorResultMiddleware¶class libtmux_mcp.middleware.ToolErrorResultMiddleware¶
Bases:
ErrorHandlingMiddlewareConvert tool-call failures into rich
ToolResulterrors.Replaces the stock
ErrorHandlingMiddlewarebehavior fortools/callonly. The stocktransform_errors=Truepath funnels every non-MCP exception through a-32603catch-all, so agents received"Internal error: Pane not found: %5"— the transform mangled every expected failure message. This subclass intercepts tool-call exceptions first (on_call_toolis the innermost hook of a middleware’s chain) and returns_error_tool_result()instead; non-tool messages fall through to the inheritedon_message, preserving the MCP-32002resource-not-found transform this middleware was originally adopted for.Logging honors
FastMCPError.log_level(fastmcp >= 3.3): the expected failures demoted to WARNING byExpectedToolErrorno longer get re-shouted at ERROR by the stock_log_error. Argument-schema validation failures — raised by fastmcp before tool code can classify them — are treated as expected too (see_is_schema_validation_error()), and when the rejected arguments include unexpected names the error result carries a synthesized suggestion telling the agent which names to drop or fix (see_error_tool_result()).Ordering invariant: must sit outside
AuditMiddleware,ReadonlyRetryMiddleware, andSafetyMiddleware. All three depend on exception semantics — audit detects failures by catching, retry matchesLibTmuxExceptionvia__cause__, and safety’s tier denials must propagate as exceptions for audit to record them — so converting the exception to a result any deeper in the stack would silently break all three.
-
Argument names that carry user-supplied payloads we never want in logs.
keys(send_keys),text(paste_text),command(run_command),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.
Nested argument containers that may contain sensitive argument names.
operationsis used bysend_keys_batchand the generic tool-batch wrappers. Preserving routing metadata is useful for audit trails, but nested payloads must be digested the same way top-level tool calls are.
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._redacted_value_shape(value)¶libtmux_mcp.middleware._redacted_value_shape(value)¶
Return non-payload metadata for a value that cannot be logged.
-
libtmux_mcp.middleware._summarize_send_keys_operation_args(args)¶libtmux_mcp.middleware._summarize_send_keys_operation_args(args)¶
Summarize one
send_keys_batchoperation for audit logging.
-
libtmux_mcp.middleware._summarize_tool_batch_operation_args(args)¶libtmux_mcp.middleware._summarize_tool_batch_operation_args(args)¶
Summarize one generic tool-batch operation for audit logging.
-
libtmux_mcp.middleware._summarize_nested_operation_args(args)¶libtmux_mcp.middleware._summarize_nested_operation_args(args)¶
Summarize a known nested operation shape.
-
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. Known nested operation lists are summarized recursively so batched tool calls keep target metadata while redacting inner payloads.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 = 1000000¶libtmux_mcp.middleware.DEFAULT_RESPONSE_LIMIT_BYTES = 1000000¶
Default byte ceiling for
TailPreservingResponseLimitingMiddleware. Matches FastMCP’s stock 1 MB default so normal schema-bearing tool responses stay below this global backstop. Tool-level caps remain responsible for terminal-specific truncation metadata.
-
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.capture_since.capture_since(),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.Error results keep their
is_errorflag through truncation. The stock truncation path rebuilds the result without it, which turns an oversized error (e.g. a validation message echoing a huge argument) into an apparent success — MCP clients then validate the truncated text against the tool’s output schema and fail with a transport-level error instead of delivering the tool error.meta(error_type/expected/suggestion) already survives via the base class, and tail-preservation keeps the suggestion line, which sits at the end of the text.