LibTmux.Mcp 0.0.0-alpha.7

Prefix Reserved
This is a prerelease version of LibTmux.Mcp.
dotnet tool install --global LibTmux.Mcp --version 0.0.0-alpha.7
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local LibTmux.Mcp --version 0.0.0-alpha.7
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=LibTmux.Mcp&version=0.0.0-alpha.7&prerelease
                    
nuke :add-package LibTmux.Mcp --version 0.0.0-alpha.7
                    

LibTmux.Mcp

A Model Context Protocol server that gives an assistant hands on tmux, built on LibTmux.

Alpha. The tool surface is not settled and can change between prereleases without notice, so pin an exact version.

This is a .NET tool, not a library reference:

$ dotnet tool install --global LibTmux.Mcp --prerelease

Point a client at it

It speaks the protocol over standard input and output, which is how an MCP client starts it:

{
  "mcpServers": {
    "tmux": {
      "command": "libtmux-mcp"
    }
  }
}

To drive a server other than the ambient one — a sandbox, a test rig, a long-lived project socket — pass a socket name:

{
  "mcpServers": {
    "tmux": {
      "command": "libtmux-mcp",
      "args": ["my-socket"]
    }
  }
}

What it is for

Anything where the answer lives in a terminal rather than in a file. Run a build and learn whether it passed. Watch a dev server come up. Find which of eleven panes is showing the stack trace. Lay out a workspace and drive it.

The design goal is that an assistant never gets stuck and never wastes context: no tool polls, no tool returns unbounded output, and no failure comes back as "an error occurred".

Waiting, not polling

The tool an assistant reaches for first is usually the wrong one. These four cover the cases, and the server's instructions steer between them:

You want Use Why
Run a command, know if it worked tmux_run Waits, returns the shell's real exit status
The same, but it takes minutes tmux_start_jobtmux_job Returns a handle at once; collect later
Output you did not start tmux_wait_for_text Wakes on the pane printing, not on a timer
Watch a pane across turns tmux_tail_pane Answers only what is new since its cursor

A client that speaks the Tasks extension can start tmux_run, tmux_wait_for_text, tmux_wait_for_channel or tmux_job as a task and collect the result later — the protocol's own version of what tmux_start_job does by hand. It is offered, never required, so a client without it keeps the blocking call it had. A listing stays a plain call: making it a task would cost a round trip to collect an answer that was already there.

Nothing here sleeps in a loop. A wait subscribes to tmux's own control mode, so tmux reports pane output as it happens and the wait is released the moment there is something to look at.

Two details make that safe. The control client attaches with ignore-size (tmux 3.2+), so it never drags the window down to its own size; and it is reference counted per session, so it exists only while a wait is running. What arrives on that stream is the pane's raw terminal bytes, so it is used as a signal and never as content — the text you get always comes from a capture, which is what tmux has already rendered.

If control mode cannot start, waits fall back to polling. Cost changes; answers do not.

The tools are ordinary classes, so an application that already hosts an assistant can run one directly instead of launching a second process:

using LibTmux;
using LibTmux.Mcp;

WriteTools tools = McpTools.Writing(server);

RunResult result = await tools.RunAsync(
    "test -f /etc/hostname && echo present",
    pane.Id.ToString(),
    timeoutSeconds: 20,
    cancellationToken: ct);

// The status comes from the shell, not from reading the screen, so a
// command that prints nothing still says what it did.
Console.WriteLine($"exit {result.ExitStatus}, timed out: {result.TimedOut}");

Nothing returns unbounded output

Every content-bearing result is cut to a budget, keeps the newest lines, and says what it dropped:

{
  "lines": ["...", "make: *** [build] Error 1"],
  "truncated": true,
  "droppedLines": 407,
  "droppedBytes": 2034
}

A reader that cannot see lines are missing concludes the pane never printed them:

using LibTmux;
using LibTmux.Mcp;

ReadTools reading = McpTools.Reading(server);

CaptureResult captured = await reading.CapturePaneAsync(
    pane.Id.ToString(),
    includeHistory: true,
    maxLines: 5,
    cancellationToken: ct);

// A terminal's newest line is the one that says what happened, so the
// budget keeps the end — and says what it cost, because a reader who
// cannot see the loss concludes the pane never printed it.
Console.WriteLine(captured.Content.ToDisplayString());
Console.WriteLine($"dropped {captured.Content.DroppedLines} earlier lines");

tmux_tail_pane avoids the problem instead of managing it. Pass its cursor back and the tenth read of a busy pane costs what the first did:

using LibTmux;
using LibTmux.Mcp;

ReadTools reading = McpTools.Reading(server);
string paneId = pane.Id.ToString();

// A first call establishes a position and returns nothing, so watching
// a pane never starts by paying for a screenful nobody asked for.
TailResult first = await reading.TailPaneAsync(paneId, cancellationToken: ct);

await reading.WaitForTextAsync(
    paneId,
    patterns: null,
    timeoutSeconds: 5,
    cancellationToken: ct);

TailResult next = await reading.TailPaneAsync(paneId, first.Cursor, cancellationToken: ct);
Console.WriteLine($"{next.Content.Lines.Count} new lines");

To offer these beside your own tools rather than as a separate process:

using LibTmux;
using LibTmux.Mcp;
using Microsoft.Extensions.DependencyInjection;

ServiceCollection services = new();
services.AddLogging();

// Registers the tools, resources and prompts, and gates them on the
// tier. Choose the transport yourself — this returns the builder.
McpServerComposition.Add(
    services,
    new ServerPolicy { Tier = SafetyTier.ReadOnly },
    server.ConnectionOptions,
    callerPaneId: null);

Three tiers, and a tool you do not have cannot be called

LIBTMUX_SAFETY picks how much of tmux is exposed. Tools above the tier are not registered, so they never reach the model's list:

LIBTMUX_SAFETY Offers Example
readonly Reading only tmux_capture_pane, tmux_search_panes
mutating (default) Reading, creating, changing tmux_run, tmux_split_pane
destructive Everything, including removal tmux_kill_session

A tier bounds the tools, not the intent: an assistant denied tmux_kill_session can still type exit into a pane with tmux_send_keys. Use readonly when that distinction matters.

Configuration

Variable Default Does
LIBTMUX_SAFETY mutating Which tier to register
LIBTMUX_SOCKET (ambient) Socket used when a call names none
LIBTMUX_TMUX tmux Which tmux binary to drive
LIBTMUX_MCP_WAIT_MAX_SECONDS 30 Ceiling on any one wait
LIBTMUX_MCP_MAX_LINES 500 Default line budget
LIBTMUX_MCP_MAX_BYTES 128000 Byte budget per result

An unreadable value is clamped and logged rather than refused — except LIBTMUX_SAFETY, where anything unrecognised falls to readonly, because a typo must never widen what the server offers.

Resources and prompts

Six resources expose the hierarchy without a tool call — tmux://hierarchy, tmux://sessions, tmux://sessions/{id}/panes, tmux://panes/{id}/content, tmux://self, tmux://servers. A client can pin or refresh one on its own initiative; one nobody reads costs nothing.

A client that subscribes to tmux://hierarchy, tmux://sessions or tmux://servers is told when they change, from tmux's own notifications rather than from a timer — so a view goes stale only when something actually moved. That watcher holds a second control client, started on the first subscription and stopped with the last, attached with no-output because it wants the hierarchy and not every byte a pane prints.

Both subscription shapes are served: resources/subscribe, and the subscriptions/listen stream that replaced it in the 2026-07-28 revision. The newer one is answered by this server rather than by the SDK's built-in handling, because that grants the subscription without telling the application — which would leave a client subscribed to a watcher nobody started, waiting for events that never come.

Long calls report progress while they run, so a wait shows as running rather than hung. It costs nothing when the client asks for none.

Four prompts package workflows that are easy to get wrong: tmux_run_and_report, tmux_diagnose_pane, tmux_build_workspace, tmux_interrupt_pane.

Which pane am I in?

When the client that launched this server was itself inside tmux, the server knows its own pane from TMUX_PANE and says so in its instructions. Every pane listing marks it isCaller, and tmux_whoami answers it directly — so an assistant can avoid typing into the terminal it is talking through.

Confirm a build without a client

$ { printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'; sleep 1; } | libtmux-mcp

The pause matters. Closing standard input immediately is a different test: the session is torn down while the reply is still being written and you get no bytes back. A real client holds the stream open for the session, which is what the pause imitates.

Standard output belongs to the protocol

Every log line goes to standard error, and the default level is Warning so a working server is quiet. A message written to the wrong stream does not produce a stray log line — it corrupts the protocol and the client disconnects. That is worth knowing if you wrap this in something of your own.

Which tmux it drives

Whatever tmux resolves to on the path, or the binary LIBTMUX_TMUX names. The supported range is 3.2a to 3.7b, proven from source on every commit.

If you install the SDK through a version manager rather than system-wide, an agent that spawns this server will not inherit your shell and the launcher will not find the runtime. Set DOTNET_ROOT in the client's config for that server; eng/mcp/mcp_swap.py does it for you.

Package Adds
LibTmux The client this is built on
LibTmux.Query.Json JSON for query documents
LibTmux.Workspace Sessions from tmuxp YAML

Source, docs and issues: https://github.com/libtmux/libtmux-dotnet

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.0.0-alpha.7 59 8/16/2026
0.0.0-alpha.6 52 8/16/2026
0.0.0-alpha.5 48 8/16/2026
0.0.0-alpha.4 57 8/16/2026
0.0.0-alpha.3 54 8/16/2026
0.0.0-alpha.2 51 8/15/2026
0.0.0-alpha.1 45 8/15/2026

Alpha. LibTmux.Mcp is a different server: 42 tools across three safety tiers, six tmux:// resources and four workflow prompts, where it had five tools. Every tool answers a typed record with a JSON output schema rather than prose, and the tool names all changed. Nothing polls — tmux_run reports the shell's real exit status, tmux_wait_for_text sleeps on tmux's control-mode stream, and tmux_start_job carries work that outlives one call. Every capture keeps the newest lines and reports what it dropped. Subscriptions and the Tasks extension are served on the current protocol revision. No change to LibTmux itself. The public API is not settled and may change without notice between prereleases; pin an exact version. Full history: https://github.com/libtmux/libtmux-dotnet/blob/master/CHANGELOG.md