Utilities

Shared utilities for libtmux MCP server.

Provides server caching, object resolution, serialization, and error handling for all MCP tool functions.

exception libtmux_mcp._utils.ExpectedToolError
exception[source]
exception[source]
exception libtmux_mcp._utils.ExpectedToolError

Bases: ToolError

ToolError for expected, agent-correctable failures.

Defaults the error’s log_level to WARNING (honored by fastmcp >= 3.3 when logging tool/resource failures) so routine validation errors, missing objects, and toolset denials do not surface as ERROR records. Unexpected failures keep stock ToolError and its ERROR default — those are the ones operators must see.

Parameters:
  • *args (object) – Positional arguments forwarded to ToolError (typically the error message).

  • log_level (int) – Level fastmcp’s server layer logs this failure at. Defaults to logging.WARNING.

  • suggestion (str, optional) – Agent-facing recovery hint. ToolErrorResultMiddleware appends it to the error result’s text and mirrors it into the result’s meta.

Examples

>>> import logging
>>> ExpectedToolError("Pane not found: %5").log_level == logging.WARNING
True

An explicit level still wins:

>>> err = ExpectedToolError("noisy", log_level=logging.INFO)
>>> err.log_level == logging.INFO
True

Catch sites that handle ToolError keep working — this is a plain subclass:

>>> isinstance(ExpectedToolError("x"), ToolError)
True

An optional suggestion carries an agent-facing recovery hint; libtmux_mcp.middleware.ToolErrorResultMiddleware surfaces it in the error result’s text and meta:

>>> err = ExpectedToolError("Pane not found: %5",
...     suggestion="Call list_panes to discover valid pane ids.")
>>> err.suggestion
'Call list_panes to discover valid pane ids.'
>>> ExpectedToolError("no hint").suggestion is None
True
class libtmux_mcp._utils.CallerIdentity
class libtmux_mcp._utils.CallerIdentity

Bases: object

Identity of the tmux pane hosting this MCP server process.

Parsed from the TMUX and TMUX_PANE environment variables that tmux injects into every child of a pane. TMUX has the format socket_path,server_pid,session_id (see tmux environ.c:281).

Used to scope self-protection checks to the caller’s own tmux server — a pane ID like %1 is only unique within a single server, so comparisons must also verify the socket path matches.

libtmux_mcp._utils._get_caller_identity()
function[source]
function[source]
libtmux_mcp._utils._get_caller_identity()

Return the caller’s tmux identity, or None if not inside tmux.

Reads TMUX for socket_path/server_pid/session_id and TMUX_PANE for the pane id. Tolerant of missing/malformed TMUX values — callers should check individual fields rather than relying on all being populated.

Return type:

CallerIdentity | None

libtmux_mcp._utils._compute_is_caller(pane)
function[source]
function[source]
libtmux_mcp._utils._compute_is_caller(pane)

Decide whether pane is the MCP caller’s own tmux pane.

The returned value is used as the is_caller annotation on PaneInfo, PaneSnapshot, and PaneContentMatch.

Tri-state semantics match the original bare-equality check:

  • None — process is not inside tmux at all (neither TMUX nor TMUX_PANE are set). No caller exists, so the annotation carries no signal.

  • True — the caller’s TMUX_PANE matches pane.pane_id and _caller_is_strictly_on_server() confirms the caller’s socket realpath equals the target’s.

  • False — the pane ids differ, or they match but the socket does not (or cannot be proven to). A bare pane-id equality check would have returned True here, which is the cross-socket false-positive fixed by tmux-python/libtmux-mcp#19.

Uses _caller_is_strictly_on_server() rather than _caller_is_on_server(): the kill-guard comparator is conservative-True-when-uncertain (right for blocking a kill, wrong for an informational annotation that should demand a positive match). The strict variant declines the basename fallback, the unresolvable-target branch, and the socket-path-unset branch so ambiguous cases resolve to False.

Parameters:

pane (Pane)

Return type:

bool | None

libtmux_mcp._utils._effective_socket_path(server)
function[source]
function[source]
libtmux_mcp._utils._effective_socket_path(server)

Return the filesystem socket path a Server will actually use.

libtmux leaves libtmux.Server.socket_path as None when only socket_name (or neither) was supplied, but tmux still resolves to a real path under ${TMUX_TMPDIR:-/tmp}/tmux-<uid>/<name>. This helper reproduces that resolution so _caller_is_on_server() can compare against the caller’s TMUX socket path.

Resolution order:

  1. libtmux.Server.socket_path if libtmux already has it.

  2. tmux display-message -p '#{socket_path}' against the target server — authoritative because tmux itself reports the path it is actually using, regardless of our process environment. Necessary on macOS where $TMUX_TMPDIR under launchd diverges from the interactive shell (see docs/topics/trust.md for the self-kill guard gap this closes).

  3. Fallback: reconstruct from $TMUX_TMPDIR + euid + socket name. This path is reached only when the target server is unreachable (e.g. not running), in which case no self-kill is possible and the conservative caller check still blocks via _caller_is_on_server’s None-socket branch.

Parameters:

server (Server)

Return type:

str | None

libtmux_mcp._utils._caller_is_on_server(server, caller)
function[source]
function[source]
libtmux_mcp._utils._caller_is_on_server(server, caller)

Return True if caller looks like it is on the same tmux server.

Compares socket paths via os.path.realpath() so symlinked temp dirs still match, then falls back to basename comparison when realpath disagrees — the authoritative caller-side $TMUX name and the target’s declared socket_name are both unaffected by $TMUX_TMPDIR divergence (the macOS launchd case), so a last-chance name match still blocks a self-kill when the path comparison was fooled by env mismatch.

Decision table:

  • caller is None → False. The process isn’t inside tmux at all, so there is no caller-side pane to protect and no self-kill is possible.

  • caller has a pane id but no socket path (e.g. TMUX_PANE set without TMUX) → True. We can’t rule out that the caller is on the target server, so err on the side of blocking the kill.

  • target server has no resolvable socket path → True. Same conservative reasoning.

  • realpath of caller’s socket path matches target’s effective path → True (primary positive signal).

  • basename of caller’s socket path equals target’s socket_name (or "default") → True. Conservative last-chance block for env-mismatch scenarios where reconstruction produced a wrong path but the name was authoritative on both sides. Trades off one exotic false positive (two daemons with identical socket_name under different tmpdirs) for a real correctness property.

  • Otherwise → False.

When a conservative block is a false positive, the caller’s error message directs the user to run tmux manually.

Parameters:
Return type:

bool

libtmux_mcp._utils._caller_is_strictly_on_server(server, caller)
function[source]
function[source]
libtmux_mcp._utils._caller_is_strictly_on_server(server, caller)

Return True only on a confirmed socket-path match.

Counterpart to _caller_is_on_server() for the informational is_caller annotation. The kill guard is biased toward True-when-uncertain so a macOS $TMUX_TMPDIR divergence cannot fool it into permitting self-kill; the annotation cannot absorb that bias — ambiguous cases are exactly the cross-socket false positives documented by tmux-python/libtmux-mcp#19. This function therefore declines every branch other than a confirmed realpath match.

Decision table:

  • caller is None → False. No caller identity.

  • caller.socket_path unset (TMUX_PANE set without TMUX) → False. We cannot verify the caller is on this server.

  • target server’s effective socket path unresolvable → False.

  • realpath of caller’s socket path equals target’s effective path → True. Primary and only positive signal.

  • Fallback on OSError from realpath: exact string match → True. Still a positive signal, just without the resolve step.

  • Otherwise → False (including the basename-only match that _caller_is_on_server() permits as a conservative block).

Parameters:
Return type:

bool

libtmux_mcp._utils.TOOLSET_INSPECT = 'inspect'
data
data
libtmux_mcp._utils.TOOLSET_INSPECT = 'inspect'

Request tmux state or terminal output, or render server-local prompt text. The built-in operation does not pass caller input as a tmux or shell command.

Reading is not “safe” — a capture returns whatever a pane holds, including credentials and text written by a remote process — so this names what the tools do, not how much they are trusted.

libtmux_mcp._utils.TOOLSET_MANAGE = 'manage'
data
data
libtmux_mcp._utils.TOOLSET_MANAGE = 'manage'

Change tmux-managed structure, presentation, staging, or coordination state. The built-in operation does not supply a shell command, pane input, or a value tmux treats as executable configuration.

libtmux_mcp._utils.TOOLSET_EXECUTE = 'execute'
data
data
libtmux_mcp._utils.TOOLSET_EXECUTE = 'execute'

Start a pane process, deliver input to one, or store state that can control later execution. The product lives here.

libtmux_mcp._utils.TOOLSET_TEARDOWN = 'teardown'
data
data
libtmux_mcp._utils.TOOLSET_TEARDOWN = 'teardown'

Delete tmux objects or retained scrollback. Irreversible at the tmux level.

libtmux_mcp._utils.VALID_TOOLSETS: tuple[str, ...] = ('inspect', 'manage', 'execute', 'teardown')
data
data
libtmux_mcp._utils.VALID_TOOLSETS: tuple[str, ...] = ('inspect', 'manage', 'execute', 'teardown')

The four toolsets, in the order startup reports them.

An unordered set, deliberately. The ordered model this replaced accumulated upward, so the kill tools could not be enabled without also enabling the typing tools; LIBTMUX_TOOLSETS=inspect,teardown is a legal surface. They group tools by what they do, for inventory configuration, context reduction, and client routing. They control MCP tool calls, not tmux authority or containment: an enabled execute tool can type the equivalent of anything hidden.

libtmux_mcp._utils.ANNOTATIONS_AMBIENT_UNKNOWN: dict[str, bool] = {'destructiveHint': True, 'idempotentHint': False, 'openWorldHint': True, 'readOnlyHint': False}
data
data
libtmux_mcp._utils.ANNOTATIONS_AMBIENT_UNKNOWN: dict[str, bool] = {'destructiveHint': True, 'idempotentHint': False, 'openWorldHint': True, 'readOnlyHint': False}

Conservative MCP defaults for calls into programmable tmux servers. Aliases and hooks can replace or extend the requested operation.

libtmux_mcp._utils.DISCOVERY_META: dict[str, Any] = {'anthropic/alwaysLoad': True}
data
data
libtmux_mcp._utils.DISCOVERY_META: dict[str, Any] = {'anthropic/alwaysLoad': True}

Per-tool MCP meta payload that hints clients to keep this tool always visible (not deferred). FastMCP passes meta opaquely (verified vs ~/study/python/fastmcp/src — no special handling); honoring is delegated to Claude Code, where alwaysLoad is documented at https://code.claude.com/docs/en/mcp (v2.1.121+).

Best-effort by design — safe no-op for clients that don’t index the anthropic/* namespace. Apply only to inspect discovery anchors (list_panes, list_windows, snapshot_pane); each always-loaded tool consumes a fixed schema budget in clients that honour the hint, so widening the set has a real cost.

libtmux_mcp._utils._TMUX_HASH_RUN = re.compile('(#+)(\\[?)')
data
data
libtmux_mcp._utils._TMUX_HASH_RUN = re.compile('(#+)(\\[?)')

A maximal run of #, plus the [ that may follow it. tmux reads a #-run by what comes next, so the run is the unit to escape, not the individual #.

libtmux_mcp._utils._escape_tmux_format(value)
function[source]
function[source]
libtmux_mcp._utils._escape_tmux_format(value)

Escape value so tmux’s format expander reproduces it literally.

Doubling every # is the obvious escape and it is wrong. A #-run followed by [ is a style sequence reserved for format_draw, and the expander copies the run through verbatim rather than collapsing it, so doubling there corrupts the value. Leave those runs alone and double the rest.

This covers the # expander only. A caller reaching an argument tmux expands with format_expand_time must escape % for strftime as well — see pipe_pane().

Parameters:

value (str) – Text to pass through a tmux format argument.

Returns:

value escaped for one pass of tmux format expansion.

Return type:

str

Examples

>>> _escape_tmux_format("/srv/#(id)")
'/srv/##(id)'
>>> _escape_tmux_format("/srv/#[x]")
'/srv/#[x]'
>>> _escape_tmux_format("issue #42")
'issue ##42'
libtmux_mcp._utils._prepare_start_directory(start_directory)
function[source]
function[source]
libtmux_mcp._utils._prepare_start_directory(start_directory)

Resolve a caller path to the directory tmux will actually use.

tmux expands -c as a format before using it as a working directory, then silently falls back to $HOME, or /, when the result cannot be entered — so an unresolvable path starts the pane somewhere else instead of failing. Resolving here leaves no format for tmux to run and turns a bad path into an error the caller can correct.

Parameters:

start_directory (str or None) – Caller-supplied working directory, or None.

Returns:

Absolute path escaped for tmux, or None when none was given.

Return type:

str or None

Raises:

ExpectedToolError – If the path does not name an existing directory.

Examples

>>> _prepare_start_directory(None) is None
True
>>> _prepare_start_directory("/")
'/'
libtmux_mcp._utils._tmux_argv(server, *tmux_args)
function[source]
function[source]
libtmux_mcp._utils._tmux_argv(server, *tmux_args)

Build a full tmux argv list honouring socket_name and socket_path.

Internal helper shared by every module that has to invoke the tmux binary directly via subprocess.run() (the buffer, wait-for, and paste_text tools). libtmux’s own libtmux.Server.cmd() wraps the same logic but does not expose a timeout, so tools that need bounded blocking have to shell out themselves — and when they do they must honour the caller’s socket.

Parameters:
  • server (libtmux.server.Server) – The resolved server whose socket to target.

  • *tmux_args (str) – tmux subcommand and its flags, e.g. "load-buffer", "-b", name.

Returns:

Complete argv ready for subprocess.run().

Return type:

list[str]

Examples

>>> class _S:
...     tmux_bin = "tmux"
...     socket_name = "s"
...     socket_path = None
>>> _tmux_argv(t.cast("Server", _S()), "list-sessions")
['tmux', '-L', 's', 'list-sessions']
>>> class _P:
...     tmux_bin = "tmux"
...     socket_name = None
...     socket_path = "/tmp/tmux-1000/default"
>>> _tmux_argv(t.cast("Server", _P()), "ls")
['tmux', '-S', '/tmp/tmux-1000/default', 'ls']
libtmux_mcp._utils._get_server(socket_name=None, socket_path=None)
function[source]
function[source]
libtmux_mcp._utils._get_server(socket_name=None, socket_path=None)

Get or create a cached Server instance.

Parameters:
  • socket_name (str, optional) – tmux socket name (-L). Falls back to LIBTMUX_SOCKET env var.

  • socket_path (str, optional) – tmux socket path (-S). Falls back to LIBTMUX_SOCKET_PATH env var.

Returns:

A cached libtmux Server instance.

Return type:

Server

libtmux_mcp._utils._invalidate_server(socket_name=None, socket_path=None)
function[source]
function[source]
libtmux_mcp._utils._invalidate_server(socket_name=None, socket_path=None)

Evict a server from the cache.

Parameters:
  • socket_name (str, optional) – tmux socket name used in the cache key.

  • socket_path (str, optional) – tmux socket path used in the cache key.

Return type:

None

libtmux_mcp._utils._resolve_session(server, session_name=None, session_id=None)
function[source]
function[source]
libtmux_mcp._utils._resolve_session(server, session_name=None, session_id=None)

Resolve a session by name or ID.

Parameters:
  • server (Server) – The tmux server.

  • session_name (str, optional) – Session name to look up.

  • session_id (str, optional) – Session ID (e.g. ‘$1’) to look up.

Return type:

Session

Raises:

exc.TmuxObjectDoesNotExist – If no matching session is found.

libtmux_mcp._utils._resolve_window(server, session=None, window_id=None, window_index=None, session_name=None, session_id=None)
function[source]
function[source]
libtmux_mcp._utils._resolve_window(server, session=None, window_id=None, window_index=None, session_name=None, session_id=None)

Resolve a window by ID, index, or default.

Parameters:
  • server (Server) – The tmux server.

  • session (Session, optional) – Session to search within.

  • window_id (str, optional) – Window ID (e.g. ‘@1’).

  • window_index (str, optional) – Window index within the session.

  • session_name (str, optional) – Session name for resolution.

  • session_id (str, optional) – Session ID for resolution.

Return type:

Window

Raises:

exc.TmuxObjectDoesNotExist – If no matching window is found.

libtmux_mcp._utils._resolve_pane(server, pane_id=None, session_name=None, session_id=None, window_id=None, window_index=None, pane_index=None)
function[source]
function[source]
libtmux_mcp._utils._resolve_pane(server, pane_id=None, session_name=None, session_id=None, window_id=None, window_index=None, pane_index=None)

Resolve a pane by ID or hierarchical targeting.

Parameters:
  • server (Server) – The tmux server.

  • pane_id (str, optional) – Pane ID (e.g. ‘%1’). Globally unique within a server.

  • session_name (str, optional) – Session name for hierarchical resolution.

  • session_id (str, optional) – Session ID for hierarchical resolution.

  • window_id (str, optional) – Window ID for hierarchical resolution.

  • window_index (str, optional) – Window index for hierarchical resolution.

  • pane_index (str, optional) – Pane index within the window.

Return type:

Pane

Raises:

exc.TmuxObjectDoesNotExist – If no matching pane is found.

libtmux_mcp._utils._coerce_dict_arg(name, value)
function[source]
function[source]
libtmux_mcp._utils._coerce_dict_arg(name, value)

Coerce a tool parameter to a dict, accepting JSON-string form.

Workaround: Cursor’s composer-1/composer-1.5 models and some other MCP clients serialize dict params as JSON strings instead of objects. Claude and GPT models through Cursor work fine; the bug is model-specific. This helper is the canonical place to absorb the string form so each tool can stay dict-typed on the Python side. Callers pass name so the error messages identify the offending parameter.

See:

https://forum.cursor.com/t/145807 https://github.com/anthropics/claude-code/issues/5504

Parameters:
  • name (str) – Parameter name, used in error messages.

  • value (dict, str, or None) – Either an already-decoded dict, a JSON string of a dict, or None.

Returns:

The decoded dict, or None if the input was None or an empty string.

Return type:

dict or None

Raises:

ExpectedToolError – If value is a string that is not valid JSON, or decodes to a JSON value that is not an object.

libtmux_mcp._utils._apply_filters(items, filters, serializer)
function[source]
function[source]
libtmux_mcp._utils._apply_filters(items, filters, serializer)

Apply QueryList filters and serialize results.

Parameters:
  • items (QueryList) – The QueryList of tmux objects to filter.

  • filters (dict or str, optional) – Django-style filters as a dict (e.g. {"session_name__contains": "dev"}) or as a JSON string. Some MCP clients require the string form. If None or empty, all items are returned.

  • serializer (callable) – Serializer function to convert each item to a model.

Returns:

Serialized list of matching items.

Return type:

list

Raises:

ExpectedToolError – If a filter key uses an invalid lookup operator.

libtmux_mcp._utils._serialize_session(session)
function[source]
function[source]
libtmux_mcp._utils._serialize_session(session)

Serialize a Session to a Pydantic model.

Parameters:

session (Session) – The session to serialize.

Returns:

Session data including id, name, window count.

Return type:

SessionInfo

libtmux_mcp._utils._serialize_window(window)
function[source]
function[source]
libtmux_mcp._utils._serialize_window(window)

Serialize a Window to a Pydantic model.

Parameters:

window (Window) – The window to serialize.

Returns:

Window data including id, name, index, pane count, layout.

Return type:

WindowInfo

libtmux_mcp._utils._coerce_int(value)
function[source]
function[source]
libtmux_mcp._utils._coerce_int(value)

Parse a tmux format-string number into int or None.

tmux format variables come back as strings; an empty string means “tmux returned nothing” (e.g. older tmux that doesn’t know the var).

Parameters:

value (str | None)

Return type:

int | None

libtmux_mcp._utils._coerce_bool(value)
function[source]
function[source]
libtmux_mcp._utils._coerce_bool(value)

Parse a tmux "1"/"0" flag into bool or None.

Mirrors libtmux’s own Pane.at_top / at_bottom typing, which folds "1" to True and everything else to False — except we keep None distinct so callers can tell “tmux didn’t tell us” from “tmux said no”.

Parameters:

value (str | None)

Return type:

bool | None

libtmux_mcp._utils._serialize_pane(pane)
function[source]
function[source]
libtmux_mcp._utils._serialize_pane(pane)

Serialize a Pane to a Pydantic model.

Parameters:

pane (Pane) – The pane to serialize.

Returns:

Pane data including id, dimensions, geometry, current command, title.

Return type:

PaneInfo

libtmux_mcp._utils._map_exception_to_tool_error(fn_name, e)
function[source]
function[source]
libtmux_mcp._utils._map_exception_to_tool_error(fn_name, e)

Translate a libtmux / unexpected exception into a ToolError.

Shared between the sync and async handle_tool_errors* decorators so the two paths stay byte-for-byte identical in what agents see.

Expected, agent-correctable failures map to ExpectedToolError (logged at WARNING). Two cases stay at ERROR: a missing tmux binary (operator-environment fault that must be loud) and the unexpected catch-all (potential bug in this server).

Parameters:
Return type:

ToolError

libtmux_mcp._utils.handle_tool_errors(fn)
function[source]
function[source]
libtmux_mcp._utils.handle_tool_errors(fn)

Decorate synchronous MCP tool functions with standardized error handling.

Catches libtmux exceptions and re-raises them through _map_exception_to_tool_error() so MCP responses have isError=True with a descriptive message — expected, agent-correctable failures as ExpectedToolError (logged at WARNING), the unexpected catch-all as stock ToolError (logged at ERROR).

The re-raise chains the original exception via from e so logs and debuggers retain the libtmux cause behind the caller-facing error.

Use handle_tool_errors_async() for async def tools — this wrapper only supports plain sync callables.

Parameters:

fn (Callable[P, R])

Return type:

Callable[P, R]

libtmux_mcp._utils.handle_tool_errors_async(fn)
function[source]
function[source]
libtmux_mcp._utils.handle_tool_errors_async(fn)

Decorate asynchronous MCP tool functions with standardized error handling.

Async counterpart to handle_tool_errors(). Required for tools that accept a fastmcp.Context parameter because Context’s report_progress/elicit/read_resource methods are coroutines that only run inside async def tools.

Maps the same libtmux exception set to the same messages and error classes as the sync decorator (expected failures as ExpectedToolError at WARNING, the unexpected catch-all as stock ToolError at ERROR) by delegating to a shared helper, and chains the original exception via from e for diagnostics.

Parameters:

fn (Callable[P, Coroutine[Any, Any, R]])

Return type:

Callable[P, Coroutine[Any, Any, R]]