Skip to content

feat: opt-in engine-ops chained plan tools and watchers#87

Draft
tony wants to merge 9 commits into
mainfrom
engine-ops
Draft

feat: opt-in engine-ops chained plan tools and watchers#87
tony wants to merge 9 commits into
mainfrom
engine-ops

Conversation

@tony

@tony tony commented Jul 4, 2026

Copy link
Copy Markdown
Member

Adds an opt-in tier of MCP tools backed by libtmux's experimental engine-ops stack. Off by default; enabled with LIBTMUX_MCP_ENGINE_OPS=1.

What's here

  • Chained plan tools (engine_plan.py): registers libtmux's plan tools over an AsyncSubprocessEngine, so a client can build and run a folded multi-step plan (record → resolve forward refs → execute) in a few ;-chained dispatches instead of one tool call per step. Uses the operation registry + LazyPlan.from_list, so no op_* tools leak into the schema.

  • Watchers (engine_watch.py): registers libtmux's control-mode event/monitor tools over an AsyncControlModeEngine:

    • wait_for_output — a needle-free settle monitor; folds a pane's live %output and returns when the pane goes quiet, replacing sleep-then-capture_pane polling.
    • watch_events (push) and/or poll_events (pull) over the control-mode notification stream, selected by LIBTMUX_MCP_EVENTS.

    The engine is opened/closed from the server lifespan; streaming is non-blocking (a single reader fans out to bounded, drop-oldest per-subscriber queues).

Configuration

  • LIBTMUX_MCP_ENGINE_OPS=1 — enable the tier (default: off)
  • LIBTMUX_MCP_EVENTSpush | pull | both (default: push)
  • LIBTMUX_MCP_EVENT_SOURCE — event source (default: subscription)

Run it via uvx / uv tool

This branch pins libtmux to its unreleased engine-ops branch via [tool.uv.sources], and a recent uv (tested with 0.11.25) honors that pin when installing from git — so no extra flags are needed. Do not add --with git+…libtmux…: the pin already supplies it, and a second URL fails with a "conflicting URLs" error. The engine-ops tools stay off until LIBTMUX_MCP_ENGINE_OPS=1.

Run the server ephemerally over stdio:

LIBTMUX_MCP_ENGINE_OPS=1 uvx --from git+https://github.com/tmux-python/libtmux-mcp.git@engine-ops libtmux-mcp

Install it persistently as libtmux-mcp:

uv tool install --from git+https://github.com/tmux-python/libtmux-mcp.git@engine-ops libtmux-mcp

Add to your coding agent (uvx)

Each snippet below registers the branch as an MCP server named tmux with the engine-ops tier enabled. Every one was verified to launch the server and expose this PR's 7 engine-ops tools (build_workspace, execute_plan, explain_plan, preview_plan, result_schema, wait_for_output, watch_events) on top of the 52 base tools — 59 total.

Claude Code (claude mcp add):

claude mcp add tmux -s user -e LIBTMUX_MCP_ENGINE_OPS=1 -- uvx --from git+https://github.com/tmux-python/libtmux-mcp.git@engine-ops libtmux-mcp

Codex CLI (codex mcp add):

codex mcp add tmux --env LIBTMUX_MCP_ENGINE_OPS=1 -- uvx --from git+https://github.com/tmux-python/libtmux-mcp.git@engine-ops libtmux-mcp

Gemini CLI (gemini mcp add):

gemini mcp add tmux -s user -e LIBTMUX_MCP_ENGINE_OPS=1 uvx -- --from git+https://github.com/tmux-python/libtmux-mcp.git@engine-ops libtmux-mcp

Grok (grok mcp add):

grok mcp add tmux -e LIBTMUX_MCP_ENGINE_OPS=1 -- uvx --from git+https://github.com/tmux-python/libtmux-mcp.git@engine-ops libtmux-mcp

Cursor CLI — Cursor has no mcp add; add the server to ~/.cursor/mcp.json (global) or .cursor/mcp.json (project):

{
  "mcpServers": {
    "tmux": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/tmux-python/libtmux-mcp.git@engine-ops", "libtmux-mcp"],
      "env": { "LIBTMUX_MCP_ENGINE_OPS": "1" }
    }
  }
}

then approve it:

cursor-agent mcp enable tmux

Antigravity (agy) — add the server to ~/.gemini/config/mcp_config.json (same mcpServers shape as Cursor):

{
  "mcpServers": {
    "tmux": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/tmux-python/libtmux-mcp.git@engine-ops", "libtmux-mcp"],
      "env": { "LIBTMUX_MCP_ENGINE_OPS": "1" }
    }
  }
}

Drop LIBTMUX_MCP_ENGINE_OPS (or set it to 0) to register only the 52 base tools. The engine-ops tools drive tmux, so a reachable tmux server is required; add LIBTMUX_SOCKET=<name> to the env block to isolate them on a dedicated socket instead of your default server.

Dependency

Pins libtmux to the engine-ops branch via [tool.uv.sources] — the engine-ops API is unreleased (libtmux.experimental). Draft until that lands.

@codecov-commenter

codecov-commenter commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.44444% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.85%. Comparing base (f3aa48b) to head (277bd91).

Files with missing lines Patch % Lines
src/libtmux_mcp/tools/engine_watch.py 74.07% 6 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #87      +/-   ##
==========================================
- Coverage   84.86%   84.85%   -0.01%     
==========================================
  Files          43       45       +2     
  Lines        3198     3243      +45     
  Branches      438      442       +4     
==========================================
+ Hits         2714     2752      +38     
- Misses        352      358       +6     
- Partials      132      133       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

tony added 8 commits July 5, 2026 18:06
why: the chained plan tools and watchers consume libtmux's unreleased
experimental MCP adapter (libtmux.experimental.mcp / .engines). This
branch-only [tool.uv.sources] pin makes them importable; not for release.

what:
- Add [tool.uv.sources] libtmux -> engine-ops git branch
- uv lock
why: the hand-written tools are single-op. libtmux's engine-ops plan tier
lets an agent chain multiple tmux operations that fold into a few
`tmux a ; b` dispatches, previewing and explaining a plan and resolving
forward-reference ids via a bindings map before executing.

what:
- Add tools/engine_plan.py: opt-in (LIBTMUX_MCP_ENGINE_OPS=1) registration
  of preview_plan/explain_plan/result_schema/execute_plan over an
  AsyncSubprocessEngine, using the operation registry (no op_* tool clutter)
- Wire it into register_tools; default off so the shipped surface is intact
- Cover enabled()/opt-in, tool presence, no op_* leak, and a folded
  forward-ref preview+explain round-trip via the in-process client
why: the shipped waiters poll capture_pane. libtmux's engine-ops event
stream offers a needle-free wait_for_output settle monitor (returns when a
pane goes quiet after a command, no sentinel) plus watch_events/poll_events
over the control-mode notification stream -- non-blocking by construction.

what:
- Add tools/engine_watch.py: opt-in (same LIBTMUX_MCP_ENGINE_OPS flag) that
  builds one AsyncControlModeEngine and registers the event/monitor tools;
  astartup/ashutdown open and close it
- Start/close that engine in the server _lifespan; wire register into
  register_tools before the visibility gate
- Cover per-mode registration, the opt-in/no-op-leak gate, the off-path
  lifecycle no-op, and a live wait_for_output settle against a real pane
why: The libtmux engine-ops branch advanced (bug fixes + doc hygiene);
re-resolve the pin so this branch builds against the current tip.
why: The libtmux engine-ops branch advanced (supervised reconnect,
drain-restart error fix, reconnect backoff + proc-cleanup hardening,
and the experimental facade->objects package rename), so re-resolve
the git pin to pick it up.

what:
- uv.lock: libtmux engine-ops 4393b02a -> f52b2d3f.
why: Track the latest engine-ops branch tip so the pinned libtmux
matches the branch HEAD used downstream.

what:
- Bump locked libtmux rev f52b2d3f -> be88f08d (scripts-only delta:
  hermetic engine benchmark grid + results)
why: Track the latest engine-ops branch tip so the pinned libtmux
matches the branch HEAD used downstream.

what:
- Bump locked libtmux rev be88f08d -> aa6d822d (scripts-only delta:
  bench_engines mypy fix)
why: Track the latest engine-ops branch tip so the pinned libtmux
matches the branch HEAD used downstream.

what:
- Bump locked libtmux rev aa6d822d -> 1fe8fc67 (ConcreteEngine renamed
  to MockEngine upstream; no downstream code references it)
why: libtmux's engine-ops branch was rebased onto master (absorbing the
gp-sphinx docs-dep bump); the prior pinned commit is now unreachable and
GC-eligible on the remote.

what:
- Bump locked libtmux rev 1fe8fc67 -> e88e87b6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants