Utilities¶
Shared utilities for libtmux MCP server.
Provides server caching, object resolution, serialization, and error handling for all MCP tool functions.
-
class libtmux_mcp._utils.CallerIdentity¶class libtmux_mcp._utils.CallerIdentity¶
Bases:
objectIdentity of the tmux pane hosting this MCP server process.
Parsed from the
TMUXandTMUX_PANEenvironment variables that tmux injects into every child of a pane.TMUXhas the formatsocket_path,server_pid,session_id(see tmuxenviron.c:281).Used to scope self-protection checks to the caller’s own tmux server — a pane ID like
%1is only unique within a single server, so comparisons must also verify the socket path matches.
-
libtmux_mcp._utils._get_caller_identity()¶libtmux_mcp._utils._get_caller_identity()¶
Return the caller’s tmux identity, or None if not inside tmux.
Reads
TMUXfor socket_path/server_pid/session_id andTMUX_PANEfor the pane id. Tolerant of missing/malformedTMUXvalues — callers should check individual fields rather than relying on all being populated.- Return type:
-
libtmux_mcp._utils._compute_is_caller(pane)¶libtmux_mcp._utils._compute_is_caller(pane)¶
Decide whether
paneis the MCP caller’s own tmux pane.The returned value is used as the
is_callerannotation onPaneInfo,PaneSnapshot, andPaneContentMatch.Tri-state semantics match the original bare-equality check:
None— process is not inside tmux at all (neitherTMUXnorTMUX_PANEare set). No caller exists, so the annotation carries no signal.True— the caller’sTMUX_PANEmatchespane.pane_idand_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 returnedTruehere, 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 destructive actions, 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 toFalse.
-
libtmux_mcp._utils._effective_socket_path(server)¶libtmux_mcp._utils._effective_socket_path(server)¶
Return the filesystem socket path a Server will actually use.
libtmux leaves
Server.socket_pathasNonewhen onlysocket_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’sTMUXsocket path.Resolution order:
Server.socket_pathif libtmux already has it.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_TMPDIRunder launchd diverges from the interactive shell (seedocs/topics/safety.mdfor the self-kill guard gap this closes).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.
-
libtmux_mcp._utils._caller_is_on_server(server, caller)¶libtmux_mcp._utils._caller_is_on_server(server, caller)¶
Return True if
callerlooks 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$TMUXname and the target’s declaredsocket_nameare both unaffected by$TMUX_TMPDIRdivergence (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_PANEset withoutTMUX) →True. We can’t rule out that the caller is on the target server, so err on the side of blocking a destructive action.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 safety property.Otherwise →
False.
When a conservative block is a false positive, the caller’s error message directs the user to run tmux manually.
- Parameters:
server (
Server)caller (
CallerIdentity|None)
- Return type:
-
libtmux_mcp._utils._caller_is_strictly_on_server(server, caller)¶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 informationalis_callerannotation. The destructive-action guard is biased toward True-when-uncertain so a macOS$TMUX_TMPDIRdivergence 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 confirmedrealpathmatch.Decision table:
caller is None→False. No caller identity.caller.socket_pathunset (TMUX_PANEset withoutTMUX) →False. We cannot verify the caller is on this server.target server’s effective socket path unresolvable →
False.realpathof caller’s socket path equals target’s effective path →True. Primary and only positive signal.Fallback on
OSErrorfromrealpath: 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:
server (
Server)caller (
CallerIdentity|None)
- Return type:
-
Annotations for tools that move user-supplied payloads into a shell context. Five consumers today:
send_keys,paste_text,pipe_pane— the canonical shell-driving tools; caller’s keys/text/stream reaches the shell prompt or pipes into an external command respectively.load_buffer,paste_buffer—load_bufferstages content into a tmux paste buffer;paste_bufferpushes that content into a target pane where the shell receives it as input. The two are split into a stage/fire pair so callers can validate before paste, but both participate in the same open-world transfer.
Distinguished from
ANNOTATIONS_CREATEbyopenWorldHint=True: the effects of these tools extend into whatever command or content the caller supplies, which is the canonical open-world MCP interaction.
Per-tool MCP
metapayload that hints clients to keep this tool always visible (not deferred). FastMCP passesmetaopaquely (verified vs~/study/python/fastmcp/src— no special handling); honoring is delegated to Claude Code, wherealwaysLoadis 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 read-tier 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.
-
Annotations for tools that stay in the
mutatingtier (so they remain visible to default-profile agents) but whose default behaviour can terminate processes or otherwise lose state.respawn_paneis the canonical user: tier=mutating because shell recovery is part of the normal agent workflow;destructiveHint=Truebecausekill=True(the default) sendsSPAWN_KILLto the existing process (cmd-respawn-pane.c:78-79);idempotentHint=Falsebecause repeated calls kill repeated processes — the MCP spec defines idempotent as “calling repeatedly with the same arguments will have no additional effect” (mcp/types.py:1276-1282).Distinct from
ANNOTATIONS_DESTRUCTIVE(same hint values) because the tier tag differs:ANNOTATIONS_DESTRUCTIVEis paired withTAG_DESTRUCTIVEeverywhere it is used; this preset is paired withTAG_MUTATING. The distinct name documents intent at the call site.
-
libtmux_mcp._utils._tmux_argv(server, *tmux_args)¶libtmux_mcp._utils._tmux_argv(server, *tmux_args)¶
Build a full tmux argv list honouring
socket_nameandsocket_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 ownServer.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:
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)¶libtmux_mcp._utils._get_server(socket_name=None, socket_path=None)¶
Get or create a cached Server instance.
-
libtmux_mcp._utils._invalidate_server(socket_name=None, socket_path=None)¶libtmux_mcp._utils._invalidate_server(socket_name=None, socket_path=None)¶
Evict a server from the cache.
-
libtmux_mcp._utils._resolve_session(server, session_name=None, session_id=None)¶libtmux_mcp._utils._resolve_session(server, session_name=None, session_id=None)¶
Resolve a session by name or ID.
-
libtmux_mcp._utils._resolve_window(server, session=None, window_id=None, window_index=None, session_name=None, session_id=None)¶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)¶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)¶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
nameso the error messages identify the offending parameter.- 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, orNone.
- Returns:
The decoded dict, or
Noneif the input wasNoneor an empty string.- Return type:
dict or None- Raises:
ToolError– Ifvalueis 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)¶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:
- Raises:
ToolError– If a filter key uses an invalid lookup operator.
-
libtmux_mcp._utils._serialize_session(session)¶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:
-
libtmux_mcp._utils._serialize_window(window)¶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:
-
libtmux_mcp._utils._coerce_int(value)¶libtmux_mcp._utils._coerce_int(value)¶
Parse a tmux format-string number into
intorNone.tmux format variables come back as strings; an empty string means “tmux returned nothing” (e.g. older tmux that doesn’t know the var).
-
libtmux_mcp._utils._coerce_bool(value)¶libtmux_mcp._utils._coerce_bool(value)¶
Parse a tmux
"1"/"0"flag intoboolorNone.Mirrors libtmux’s own
Pane.at_top/at_bottomtyping, which folds"1"to True and everything else to False — except we keepNonedistinct so callers can tell “tmux didn’t tell us” from “tmux said no”.
-
libtmux_mcp._utils._serialize_pane(pane)¶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:
-
libtmux_mcp._utils._map_exception_to_tool_error(fn_name, e)¶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.- Parameters:
fn_name (
str)e (
BaseException)
- Return type:
ToolError
-
libtmux_mcp._utils.handle_tool_errors(fn)¶libtmux_mcp._utils.handle_tool_errors(fn)¶
Decorate synchronous MCP tool functions with standardized error handling.
Catches libtmux exceptions and re-raises as
ToolErrorso that MCP responses haveisError=Truewith a descriptive message. Usehandle_tool_errors_async()forasync deftools — this wrapper only supports plain sync callables.
-
libtmux_mcp._utils.handle_tool_errors_async(fn)¶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 afastmcp.Contextparameter because Context’sreport_progress/elicit/read_resourcemethods are coroutines that only run insideasync deftools.Maps the same libtmux exception set to the same
ToolErrormessages as the sync decorator by delegating to a shared helper.