Middleware¶
Middleware for libtmux MCP server.
Provides the project’s middleware infrastructure, in definition order:
ToolsetMiddlewarerechecks the configured surface for any tool that reaches dispatch. FastMCP visibility makes tools outside that surface unavailable on the wire first.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).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.
-
libtmux_mcp.middleware._allow_nested_read_tool(name)¶libtmux_mcp.middleware._allow_nested_read_tool(name)¶
Scope one nested read-batch operation to
name.
-
class libtmux_mcp.middleware._NestedReadToolVisibility¶class libtmux_mcp.middleware._NestedReadToolVisibility¶
Bases:
TransformExpose the current read-batch operation to FastMCP lookup.
-
class libtmux_mcp.middleware.ToolsetMiddleware¶class libtmux_mcp.middleware.ToolsetMiddleware¶
Bases:
MiddlewareFilter tools to the enabled toolsets.
Filtering controls which MCP tool calls this server advertises and accepts. It does not confine tmux: an enabled
executetool can type the equivalent of any hidden tool.
-
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 writing 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 the
initializehandshake’s client identity onfastmcp_context.session.client_params. Every hop can be absent (unit-test contexts, background tasks, clients that omit the field), so any failure resolves toNone. Used only to word error suggestions; never gates behavior.The field is read under BOTH its Python names. The SDK renamed it
clientInfo->client_infobetweenmcp1.x and 2.x while keeping the camelCase wire alias, so the attribute is what moved, not the protocol. Since a miss here is swallowed by design, pinning one spelling turns that rename into a silent feature loss — measured on mcp 2.0.0b2, where the suggestion quietly fell back to generic wording with nothing logged. Reading both keeps this working across the migration and costs onegetattr.
-
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.That inherited transform matches the not-found exception types by exact class –
error_type in (FileNotFoundError, KeyError, NotFoundError)– so only an unknown resource URI, which fastmcp raises asNotFoundErroritself, reaches a client as-32002. Thetmux://handlers raiseResourceErrorfor a missing session, and path screening raisesResourceSecurityError, aNotFoundErrorsubclass; neither is an exact match, so both arrive as-32603“Internal error” – the code that tells a client the server is broken, for what is really an absent or disallowed target.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
AuditMiddlewareandToolsetMiddleware. Both depend on exception semantics: audit detects failures by catching them, and toolset denials must propagate as exceptions for audit to record them. Converting the exception to a result any deeper in the stack would silently break both.
-
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.environmentaccepts a mapping or a JSON object string. The redaction logic in_summarize_args()digests each mapping value while leaving its keys (env var names likeDATABASE_URL) visible. A JSON string is instead redacted as one scalar digest, so its keys are not retained.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/trust.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.
-
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 backstop for
libtmux_mcp.tools.pane_tools.capture_pane(),libtmux_mcp.tools.pane_tools.capture_since(),libtmux_mcp.tools.pane_tools.snapshot_pane(), andlibtmux_mcp.tools.pane_tools.search_panes(), pluslibtmux_mcp.tools.buffer_tools.show_buffer(). Per-tool caps at the tool layer fire first under normal operation; this middleware catches a missed bound within that configured high-volume set.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.