tmux-python / tmux-python/libtmux-mcp
Surface invocation context (in tmux? server running? which socket?) and make tmux-server auto-start explicit
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 13
- Forks
- 0
- Avg merge
- 13h 52m
- Merged PRs (30d)
- 4
Description
Summary
libtmux-mcp can be invoked in four materially different situations, and it currently distinguishes none of them to the agent:
| # | Scenario | What the agent is told today |
|---|---|---|
| a | Launched inside a tmux pane | A prose line in instructions — which can be silently dropped under the byte cap, and names a socket the tools don't actually query |
| b | Launched outside tmux, a tmux server is running | Nothing. The Agent context line is simply absent, with no replacement |
| c | Launched outside tmux, no server running | Nothing. Discovery returns [], indistinguishable from "empty server" |
| d | Outside tmux, no server, and the agent must not start one | No such mode exists |
The only in-tmux detection happens once, at import, in _build_instructions — and it feeds nothing but a sentence. Nothing else in the process consumes it.
Separately: create_session silently spawns a long-lived tmux daemon (sourcing the user's tmux.conf) with no gate, no opt-in, and no signal in its response that it did so.
Evidence
Detection exists but is inert
server.py#L195—_build_instructionsis the sole startup reader ofTMUX_PANEserver.py#L279— the lifespan probe checks only that the tmux binary exists; it never probes whether a server doesserver.py#L221— no negative branch: withTMUX/TMUX_PANEunset theAgent contextblock is simply omitted. Verified. Agents in scenarios (b)/(d) must infer their context from an absence.resources/hierarchy.py#L38— every registered resource is a hierarchy read. Nothing exposesinside_tmux, effective socket, or server-running state.
"No server" is indistinguishable from "empty server"
Verified empirically against a dead socket: list_sessions/list_windows/list_panes → []; list_servers → []. libtmux swallows the daemon-not-up error and returns an empty QueryList:
libtmux/server.py#L2372—Server.sessionsswallows everyLibTmuxExceptionlibtmux/server.py#L79—_fetch_or_empty(used by.panes/.windows) swallows only daemon-not-up and re-raises the rest
get_server_info is the only discriminator (is_alive=False) — and nothing in the instructions tells the agent to call it first:
Worse, on the default socket it reports socket_name=None and socket_path=None even when alive — so it cannot even tell the agent which socket it just queried:
The error path is a dead-end loop
With no server running, capture_pane(pane_id="%0") fails with "Pane not found: %0" and the suggestion "Call list_panes to discover valid pane ids" — and list_panes returns [], because no daemon exists. The agent is sent in a circle.
And "no tmux server" is logged as an unexpected ERROR for set_environment, because libtmux raises a bare ValueError (not a LibTmuxException), missing the expected-error branch and landing in the catch-all with logger.exception:
_utils.py#L1018libtmux/common.py#L115— the upstream bareValueError
A routine "no server running" thereby pollutes operator error budgets as a non-bug.
Auto-start is silent, ungated, and unannounced
I probed every plausible candidate against a dead socket (show_option, set_option, show_environment, set_environment, signal_channel, load_buffer, create_window) — none spawn a daemon. create_session is the only tool that does (is_alive False → True):
server_tools.py#L141libtmux/server.py#L2327—new_session→tmux new-session, which auto-starts the server
It is registered with ANNOTATIONS_CREATE (destructiveHint=False) in the plain mutating tier — so a default-profile agent will spawn a long-lived tmux daemon, sourcing the user's ~/.tmux.conf, as a routine "create":
SessionInfo carries no signal that a whole tmux server came into existence as a side effect — the agent and the audit log see an ordinary session create:
And there is no "do not start a server" mode: no start_server tool is registered, libtmux.Server.start_server() is never called anywhere in the package, and the only way to block an auto-start is LIBTMUX_SAFETY=readonly — which hides every mutating tool.
Reproduction
Scenario (c) — no server, dead-end loop:
tmux -L novel_socket kill-server 2>/dev/null; LIBTMUX_SOCKET=novel_socket <launch mcp>
list_panes()→[]. 2.capture_pane(pane_id="%0")→ "Pane not found: %0 … Calllist_panesto discover valid pane ids." 3.list_panes()→[]. Observed: a loop with no way out and no statement that the server is simply not running. Expected: "No tmux server is running on socketnovel_socket."
Scenario (c) — silent daemon spawn: on that same dead socket, call create_session(session_name="x"). Observed: a tmux daemon is started (sourcing the user's config) and SessionInfo looks like any other create. Expected: either an explicit opt-in, or at minimum server_started: true in the response.
Suggested fix
1. Expose the context surface. This is the same tool proposed in the self-location issue — where_am_i (readonly) + a tmux://self resource — whose payload deliberately covers server/safety state as well as caller identity:
{
"inside_tmux": bool,
"pane_id": str | None, "window_id": str | None, "session_id": str | None,
"caller_socket_path": str | None,
"effective_socket_name": str | None, # what the tools will ACTUALLY query
"effective_socket_path": str | None, # (can differ from caller_* — see the socket bug)
"server_running": bool,
"live_server_count": int,
"safety_level": str, # readonly | mutating | destructive
"suppress_history": bool,
}
One call answers "where am I / is anything running / what will my tools target / what am I allowed to do". It replaces a prose suffix that vanishes silently under the 2048-byte instructions cap. The two issues should ship together — one tool, not two. (Naming rationale, including why get_context was rejected — "context" is hopelessly overloaded in agent-land — lives in that issue.)
Liveness must come from Server.is_alive() — one list-sessions, provably no daemon spawn (libtmux/server.py#L241). Do not infer liveness from socket-file existence: start-server leaves a stale socket file behind after the sessionless daemon exits under exit-empty on (libtmux/server.py#L1448). And do not infer it from emptiness of .sessions vs .panes — they swallow different exception sets.
2. Make server auto-start opt-in. Add allow_server_start: bool = False to create_session (default from LIBTMUX_ALLOW_SERVER_START), and register a dedicated start_server tool wrapping libtmux's existing Server.start_server(). Starting a tmux daemon sources the user's config and leaves a long-lived background process — that should be an explicit, auditable action, not a side effect of a create tagged destructiveHint=False.
(Caveat for whoever implements this: start_server() alone is insufficient — under exit-empty on the sessionless daemon exits immediately and is_alive() still returns False. A real "ensure server" must create a session.)
3. Announce the spawn. Add server_started: bool to SessionInfo so both the agent and the AuditMiddleware record can see that a create_session brought a whole daemon into existence rather than joining an existing one.
4. Give the not-found errors a real recovery hint. Have the targeted resolvers (_resolve_pane/_resolve_window/_resolve_session) probe server.is_alive() before raising, so the error names the actual cause:
"No tmux server is running on socket
<name>. Calllist_serversto find live sockets, orstart_server/create_session(allow_server_start=true)to start one."
Also fix show_environment, which returns {"variables": {}} on a dead server — indistinguishable from a genuinely empty environment.
5. Map libtmux's bare ValueError from set_environment/unset_environment into the expected-error path in _map_exception_to_tool_error (or fix it upstream — see the libtmux issue), so "no tmux server running" stops being logged as an unexpected ERROR.
6. State the negative case in the instructions. When TMUX_PANE is absent, append an explicit segment rather than silently omitting the context block:
"This MCP is NOT running inside tmux: there is no 'self'.
is_calleris null on every pane. Callwhere_am_i/list_serversfirst; ask the user which session to target rather than guessing."
Tests
where_am_i()outside tmux →inside_tmux=False, andserver_runningcorrectly reflects a live vs dead socket.where_am_i()on a dead socket →server_running=False,live_server_count=0; on a live one →True.create_session(allow_server_start=False)on a dead socket raises rather than spawning; assert no socket file appears.create_session(allow_server_start=True)on a dead socket returnsSessionInfo(server_started=True).capture_paneon a dead server produces an error naming "no tmux server", not "Pane not found → call list_panes".set_environmenton a dead server raises an expectedToolError, and is not logged vialogger.exception.- Instructions assertion: with
TMUX_PANEunset, the not-in-tmux segment is present.
Related
- Companion feature: the
where_am_iself-location primitive + self-first routing. That issue defines the tool; this one defines the semantics it must report. Ship together. - Companion bug: caller socket advertised but never targeted — which is why
effective_socket_*must be a distinct field fromcaller_socket_*in the context payload. - Upstream libtmux: bare
ValueErrorfromset_environment;start_server()leaves a stale socket.
Environment
- libtmux-mcp: 0.1.0a17
- libtmux: 0.61.0
- fastmcp: 3.4.3 / mcp: 1.28.1
- tmux: 3.7b
- Python: 3.14.0
Cross-references — filed together from a single audit of v0.1.0a17. The self-location gap is the root cause; the rest are what it exposed.
- tmux-python/libtmux-mcp#92 — [bug] documented self-location recipe is unexecutable (
filters={"is_caller": true}→[]) - tmux-python/libtmux-mcp#93 — [feat]
where_am_iself-location primitive + self-first relational routing (root fix) - tmux-python/libtmux-mcp#94 — [bug] caller's socket advertised but never targeted;
TMUXenv race vs. the threadpool - tmux-python/libtmux-mcp#95 — [perf]
list_*unbounded, unscoped, unprojected, excluded from the response limiter - tmux-python/libtmux-mcp#96 — [feat] invocation context (in tmux? server running?) + explicit tmux-server auto-start
- tmux-python/libtmux#704 — upstream: no self-location API (
TMUX_PANE); unreachablesocket_pathderivation
(← this issue: #96)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the context and instruction paths in src/libtmux_mcp/server.py, then inspect server_tools.py, models.py, and _utils.py for server probing, session creation, response fields, and error mapping. Use the issue's seven tests as the acceptance checklist, including dead and live sockets, explicit server-start control, recovery errors, expected ToolError handling, and the negative instruction segment. Coordinate with the companion where_am_i issue because the proposed context surface is shared.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, tooling
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 28/100