Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,59 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Breaking changes

**List tools return page envelopes**

{tooliconl}`list-servers`, {tooliconl}`list-sessions`,
{tooliconl}`list-windows`, and {tooliconl}`list-panes` now return typed page
objects instead of bare lists. Read rows from `.items`; for example,
`list_sessions(...)[0]` becomes `list_sessions(...).items[0]`. The envelope
also reports `total`, `offset`, `limit`, and `truncated` for bounded discovery.
(#95)

**Starting a tmux server now requires permission**

{tooliconl}`create-session` no longer starts a tmux server silently when its
target is not running. MCP calls must pass `allow_server_start=true` or enable
{envvar}`LIBTMUX_ALLOW_SERVER_START`; direct Python calls must pass
`allow_server_start=True`. Starting a server launches a long-lived daemon and
loads the user's tmux configuration, so the side effect is denied by default.

The returned {class}`~libtmux_mcp.models.SessionInfo` sets
{attr}`~libtmux_mcp.models.SessionInfo.server_started` to `true` when the call
used a startup-enabled path after a no-start attempt proved the target absent;
it is `false` when the no-start path used an existing server. Dead-server target
errors now say that no server is running, including when
{tooliconl}`set-environment` targets a stopped server. See {ref}`configuration`
for the startup policy. (#96)

### What's new

**Typed caller location**

The zero-input {tooliconl}`where-am-i` tool returns a
{class}`~libtmux_mcp.models.CallerContext` for relational requests such as
"this pane", "current window", and "this session". The result distinguishes
caller identity from availability, so an outside-tmux invocation, stopped
server, stale pane, or configured-target mismatch is structured state rather
than a failed discovery call. (#93)

**Bounded list discovery**

All four hierarchy list tools return deterministic pages with honest totals.
{tooliconl}`list-windows` and {tooliconl}`list-panes` default to compact
discovery summaries and accept `detail="full"` for layout, geometry, process,
and working-directory metadata. Filters still run against full metadata before
projection and paging.

Window and pane lists also accept the explicit `scope="caller_session"` option
to list the session containing the frozen caller pane. Their default remains
`scope="server"`; caller-session scope rejects explicit hierarchy selectors
and fails rather than widening the search when the caller is unavailable on
the effective tmux target. See {ref}`pagination-overview` for the page contract.
(#95)

## libtmux-mcp 0.1.0a18 (2026-07-12)

libtmux-mcp 0.1.0a18 raises the libtmux floor to 0.62.0 and tightens how the server handles lookups that cannot succeed. A readonly call carrying a stale or ambiguous id now fails on the first attempt instead of paying a second tmux round-trip and a backoff window to fail identically, and an ambiguous target — a window shared between sessions, matched by name or index — now comes back with a recovery hint that says to target it by id instead of an unexplained error. The floor bump is what makes both fixes possible: libtmux 0.62.0 folds its query errors into the {exc}`libtmux.exc.LibTmuxException` hierarchy the server keys on.
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Give your AI agent hands inside the terminal — create sessions, run commands,

| Module | Tools |
|--------|-------|
| **Server** | `list_servers`, `list_sessions`, `create_session`, `kill_server`, `get_server_info` |
| **Server** | `where_am_i`, `list_servers`, `list_sessions`, `create_session`, `kill_server`, `get_server_info` |
| **Batch** | `call_readonly_tools_batch`, `call_mutating_tools_batch`, `call_destructive_tools_batch` |
| **Session** | `list_windows`, `get_session_info`, `create_window`, `rename_session`, `select_window`, `kill_session` |
| **Window** | `list_panes`, `get_window_info`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
Expand All @@ -25,6 +25,11 @@ Give your AI agent hands inside the terminal — create sessions, run commands,
| **Buffers** | `load_buffer`, `paste_buffer`, `show_buffer`, `delete_buffer` |
| **Hooks** | `show_hooks`, `show_hook` |

Call `where_am_i` with no arguments when a request says "this pane",
"current window", or "this session". Its typed result distinguishes a live
caller from an outside-tmux invocation, a stopped server, a stale pane, or a
configured server that differs from the caller's server.

## Quickstart

**Requirements:** Python 3.10+, tmux on `$PATH`.
Expand Down
7 changes: 7 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,20 @@ def _patched_tool_collector_tool(self: ToolCollector, **kwargs: t.Any) -> t.Any:
conf["fastmcp_server_module"] = "libtmux_mcp.server:mcp"
conf["fastmcp_model_module"] = "libtmux_mcp.models"
conf["fastmcp_model_classes"] = (
"ListPage",
"SessionInfo",
"SessionPage",
"WindowInfo",
"WindowSummary",
"WindowPage",
"PaneInfo",
"PaneSummary",
"PanePage",
"PaneContentMatch",
"SearchPanesResult",
"PaneSnapshot",
"ServerInfo",
"ServerPage",
"OptionResult",
"OptionSetResult",
"EnvironmentResult",
Expand Down
57 changes: 52 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ Runtime configuration for the libtmux-mcp server. For MCP client setup, see {ref
tmux socket name (`-L`). Isolates the MCP server to a specific tmux socket.

- **Type:** string
- **Default:** (none — uses the default tmux socket)
- **Default:** (none — follows the frozen caller socket inside tmux, then the
tmux default outside tmux)

```{envvar} LIBTMUX_SOCKET_PATH
```

tmux socket path (`-S`). Alternative to socket name for custom socket locations.

- **Type:** string
- **Default:** (none)
- **Default:** (none — falls through to the configured name, frozen caller
socket, then tmux default)

```{envvar} LIBTMUX_TMUX_BIN
```
Expand All @@ -39,6 +41,37 @@ Safety tier controlling which tools are available. See {ref}`safety`.
- **Default:** `mutating`
- **Values:** `readonly`, `mutating`, `destructive`

```{envvar} LIBTMUX_ALLOW_SERVER_START
```

Controls the MCP default for allowing {tooliconl}`create-session` to start a
tmux server when its target is not running. Starting a server launches a
long-lived background daemon and sources the user's tmux configuration.

- **Type:** string flag
- **Default:** `0` (disabled)
- **Values:** `0`, `1`

Unset and `0` deny the implicit daemon start; `1` allows it. Any other value
fails server startup with
`LIBTMUX_ALLOW_SERVER_START must be unset, '0', or '1'`, without echoing the
rejected value. An explicit `allow_server_start` value wins for each MCP call.
Direct Python calls always default to `False`.

When the effective value is `false` and no server is running,
{toolref}`create-session` fails without starting one. Pass
`allow_server_start=true` for an intentional per-call start. The returned
{class}`~libtmux_mcp.models.SessionInfo` sets
{attr}`~libtmux_mcp.models.SessionInfo.server_started` to `true` when a
no-start attempt proved the target absent and the call then succeeded on the
startup-enabled path. A no-start creation accepted by an existing server
returns `false`; the field does not claim stronger attribution across other
processes.

The server resolves {envvar}`LIBTMUX_ALLOW_SERVER_START` once during startup.
Restart the MCP server after changing it, usually by reconnecting or restarting
the MCP client. Per-call arguments take effect without a restart.

```{envvar} LIBTMUX_SUPPRESS_HISTORY
```

Expand Down Expand Up @@ -71,6 +104,7 @@ Set environment variables in your MCP client config:
"env": {
"LIBTMUX_SOCKET": "ai_workspace",
"LIBTMUX_SAFETY": "readonly",
"LIBTMUX_ALLOW_SERVER_START": "0",
"LIBTMUX_SUPPRESS_HISTORY": "1"
}
}
Expand All @@ -80,14 +114,27 @@ Set environment variables in your MCP client config:

## Socket isolation

By default, the MCP server connects to the default tmux socket. Set {envvar}`LIBTMUX_SOCKET` to isolate AI agent activity from your personal tmux sessions:
With no configured selector, a server launched inside tmux follows the frozen
caller socket; outside tmux it connects to the tmux default. Set
{envvar}`LIBTMUX_SOCKET` to isolate AI agent activity from your personal tmux
sessions:

```json
"env": { "LIBTMUX_SOCKET": "ai_workspace" }
```

The agent will only see sessions on the `ai_workspace` socket, not your personal sessions.

## All tools accept `socket_name`
## Target selection

Target precedence is: explicit per-call selector, configured path, configured
name, frozen caller socket, tmux default. The configured path comes from
{envvar}`LIBTMUX_SOCKET_PATH`; the configured name comes from
{envvar}`LIBTMUX_SOCKET`.

Every tool accepts an optional `socket_name` parameter that overrides {envvar}`LIBTMUX_SOCKET` for that call. This allows agents to work across multiple tmux servers in a single session.
When both `LIBTMUX_SOCKET*` settings are unset, inside tmux, the frozen caller
socket is selected. When both are unset outside tmux, the tmux default is
selected. A targeted tool's optional `socket_name` overrides the configured and
caller-derived selectors for that call, which lets agents work across multiple
servers. {tooliconl}`list-servers` is the discovery exception: pass
`extra_socket_paths` to include sockets outside the canonical scan.
14 changes: 8 additions & 6 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ Run the Playwright tests against my dev server in the myapp session.
```{admonition} Agent reasoning
:class: agent-thought

{toolref}`list-panes` will not help here -- it shows metadata like current
command and working directory, not terminal content. The dev server printed
its URL to the terminal minutes ago, so I need to search terminal content.
{toolref}`list-panes` will not help here -- it shows metadata like the current
command (and, with `detail="full"`, the working directory), not terminal
content. The dev server printed its URL to the terminal minutes ago, so I need
to search terminal content.
```

The agent calls {tooliconl}`search-panes` with `pattern: "Local:"` and
Expand Down Expand Up @@ -79,9 +80,10 @@ like a blank shell.

{toolref}`search-panes` searches terminal *content* -- what you would see on
screen. {toolref}`list-panes` searches *metadata* like current command and
working directory. If the agent had used {toolref}`list-panes` to find a pane
running `node`, it would know a process exists but not whether it is ready or
what URL it chose.
working directory; request `detail="full"` when the compact default does not
carry the property you need. If the agent had used {toolref}`list-panes` to
find a pane running `node`, it would know a process exists but not whether it
is ready or what URL it chose.

---

Expand Down
23 changes: 20 additions & 3 deletions docs/tools/server/create-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,28 @@ container — create one before creating windows or panes.
**Avoid when** a session with the target name already exists — check with
{tooliconl}`list-sessions` first, or the command will fail.

**Side effects:** Creates a new tmux session with one window and one pane.
**Side effects:** Creates a new tmux session with one window and one pane. If
no server is running, `allow_server_start=true` also launches a long-lived tmux
daemon and sources the user's tmux configuration. The call fails without
starting a daemon when that permission is false.

**Do not pass credentials directly in `environment`.** Values persist in the
new session, can be inspected with {tooliconl}`show-environment`, and reach
its initial pane and future panes. Pass credential references instead; see
{ref}`safety` for details.

`allow_server_start` defaults to `false` for direct Python calls. Omitted MCP
input inherits {envvar}`LIBTMUX_ALLOW_SERVER_START`, which is disabled by
default; an explicit value wins. Set it to `true` only when starting the target
tmux server is intended. The returned
{class}`~libtmux_mcp.models.SessionInfo` reports that side effect through
{attr}`~libtmux_mcp.models.SessionInfo.server_started`: `true` means a no-start
attempt proved the target absent and the call then succeeded on the
startup-enabled path. It does not claim stronger attribution across other
processes. Change the environment default only after restarting the MCP
server; per-call values apply immediately. See {ref}`configuration` for the
startup setting.

`suppress_persistent_history` defaults to `false` for MCP and direct Python calls. It does not inherit {envvar}`LIBTMUX_SUPPRESS_HISTORY`. Leave it `false` to add no history controls for this call. That choice cannot remove inherited, session, or startup-file controls.

Set it to `true` and {tooliconl}`create-session` copies and merges best-effort no-disk history controls into the tmux session environment. They reach the initial pane and future panes in that session. The shell can retain in-memory history, and a startup file can override these controls after the process starts.
Expand All @@ -28,7 +43,8 @@ When you enable it, tmux environment arguments are added, but the spawned proces
{
"tool": "create_session",
"arguments": {
"session_name": "dev"
"session_name": "dev",
"allow_server_start": false
}
}
```
Expand All @@ -42,7 +58,8 @@ Response ({class}`~libtmux_mcp.models.SessionInfo`):
"window_count": 1,
"session_attached": "0",
"session_created": "1774521872",
"active_pane_id": "%0"
"active_pane_id": "%0",
"server_started": false
}
```

Expand Down
5 changes: 5 additions & 0 deletions docs/tools/server/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Server & process-level tools — discover sessions, control the tmux daemon, rea
::::{grid} 1 2 2 3
:gutter: 2 2 3 3

:::{grid-item-card} {tooliconl}`where-am-i`
Resolve the frozen caller pane and effective tmux target.
:::

:::{grid-item-card} {tooliconl}`list-sessions`
List all sessions on the tmux server.
:::
Expand Down Expand Up @@ -47,6 +51,7 @@ Set an environment variable on the server.
:hidden:
:maxdepth: 1

where-am-i
list-sessions
list-servers
get-server-info
Expand Down
49 changes: 29 additions & 20 deletions docs/tools/server/list-servers.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ side project.
target — pass it directly to the tool that needs it via `socket_name`.

**Side effects:** None. Readonly. Stale socket files are filtered
via a kernel-fast UNIX `connect()` probe so the call stays under one
second even on machines with thousands of orphaned `tmux-<uid>/`
inodes.
via a kernel-level UNIX `connect()` probe, keeping discovery responsive
when `tmux-<uid>/` contains orphaned socket files.

**Scope:** Only servers under `${TMUX_TMPDIR:-/tmp}/tmux-<uid>/` are
discovered by the canonical scan. Custom `tmux -S /some/path/...`
daemons that live outside that directory must be supplied via
`extra_socket_paths`.

Results arrive as a {class}`~libtmux_mcp.models.ServerPage`. The default
page uses `limit=100` and `offset=0`; `total` describes every discovered live
server before paging, and `truncated` tells you whether another offset remains.

**Example:**

```json
Expand All @@ -33,22 +36,28 @@ daemons that live outside that directory must be supplied via
Response:

```json
[
{
"is_alive": true,
"socket_name": "default",
"socket_path": null,
"session_count": 3,
"version": "3.6a"
},
{
"is_alive": true,
"socket_name": "ci-runner",
"socket_path": null,
"session_count": 1,
"version": "3.6a"
}
]
{
"items": [
{
"is_alive": true,
"socket_name": "ci-runner",
"socket_path": null,
"session_count": 1,
"version": "3.6a"
},
{
"is_alive": true,
"socket_name": "default",
"socket_path": null,
"session_count": 3,
"version": "3.6a"
}
],
"total": 2,
"offset": 0,
"limit": 100,
"truncated": false
}
```

To include a custom-path daemon:
Expand All @@ -57,7 +66,7 @@ To include a custom-path daemon:
{
"tool": "list_servers",
"arguments": {
"extra_socket_paths": ["/home/user/.cache/tmux/socket"]
"extra_socket_paths": ["/path/to/tmux.sock"]
}
}
```
Expand Down
Loading