Repository navigation
Compose / bulk operations: run multiple actions in one MCP tool call (start with send_keys) #49
Description
Problem
Every MCP tool call costs at least one round-trip between the agent and the server. When an agent needs to send several keystroke sequences to one pane, or coordinate send_keys across multiple panes, it currently needs one MCP call per operation. That burns agent turns, MCP transport round-trips, and libtmux/tmux subprocess work for sequences that can be represented as a batch.
A composed surface should let an agent request "run these N send-key operations in this declared order, with this error policy" as one MCP call.
tmux and FastMCP support the shape
tmux command queues already provide useful semantics for a future native optimization:
- Chained commands share a group via
cmd_get_group()assignment. - A command error removes subsequent commands in that group via
cmdq_remove_group()and the error path atcmd-queue.c#L780. send-keysaccepts a single target pane through its-ttarget field, so cross-pane batching requires either multiple libtmux calls or a tmux command chain; seecmd-send-keys.c.- tmux command grammar uses semicolon-separated commands; see
cmd-parse.y.
FastMCP/Pydantic can expose the MCP input safely:
list[...]parameters are normal in FastMCP examples, including nested Pydantic models inexamples/complex_inputs.py.Annotated[list[Op], Field(min_length=1, max_length=N)]renders as ordinary JSON Schema bounds.- Pydantic gives per-item validation locations, so malformed op docs: DX improvements — docstrings, recipes, gotchas, prompting guide #3 can be reported precisely.
- Discriminated unions are possible server-side, but FastMCP strips OpenAPI
discriminatorkeys from client-facing schema injson_schema.py, so heterogeneous batches are less ergonomic for clients.
Recommendation: start with send_keys_batch
Add a homogeneous batch tool first. Keep send_keys unchanged for the common single-operation case.
class SendKeysOp(BaseModel):
keys: str
pane_id: str | None = None
session_name: str | None = None
session_id: str | None = None
window_id: str | None = None
enter: bool = True
literal: bool = False
suppress_history: bool = False
class SendKeysOpResult(BaseModel):
pane_id: str | None
success: bool
elapsed_seconds: float
error: str | None = None
async def send_keys_batch(
operations: Annotated[list[SendKeysOp], Field(min_length=1, max_length=50)],
on_error: Literal["stop", "continue"] = "stop",
socket_name: str | None = None,
ctx: Context | None = None,
) -> list[SendKeysOpResult]: ...Default on_error="stop" should short-circuit after the first failed operation. on_error="continue" should record the failure for that item and proceed with later operations in declared order.
Deferred avenues
- Heterogeneous pane-op batches such as
send_keys -> wait_for_text -> capture_paneare intentionally deferred. They have discovery/schema costs and overlap with the observation-first work in feat:capture_since— non-blocking, cursor-based delta capture for agentic flows #60/Observation-first architecture: eliminate agent lockout from blocking wait tools #61. - Native tmux command chaining is a future optimization. Start with libtmux calls in declared order because per-item error attribution is clearer.
- Progress notifications can be emitted for longer batches, but small batches should not depend on notification delivery for correctness.
Acceptance criteria
- New
send_keys_batchtool with Pydantic-validatedoperationsand expliciton_errorpolicy. - Per-operation result list preserves declared order and includes success/error/timing metadata.
- Tests cover input bounds, declared ordering,
on_error="stop",on_error="continue", cross-pane operations, and validation error reporting. - Docs explain when to use
send_keys_batchversus singlesend_keys, and call out that command completion should still usewait_for_channelwhen the agent authors the command. - CHANGES describes the user-facing capability as one batch send-keys surface, not as internal tmux mechanics.
Activity
tony commentedon May 24, 2026
Triage pass (2026-05-24): keep, but action after #55/#54/#53 and after the first #60 design pass.
Current-code/source check: the proposed homogeneous
send_keys_batchshape is compatible with the existing singlesend_keyssurface and with FastMCP/Pydantic list inputs. Pin future source links to tmux command grouping/error behavior atcmd-queue.c#L470-L516andcmd-queue.c#L772-L781, the target-pane field insend-keysatcmd-send-keys.c#L32-L44, tmux semicolon command parsing atcmd-parse.y#L407-L418, and FastMCPv3.3.1complex-list examples / schema behavior (complex inputs,_strip_discriminator).Overlap/value: this is valuable for round-trip reduction, but it should stay intentionally narrower than #60/#61. Do not expand this into heterogeneous
send_keys -> wait -> capturecomposition; that would duplicate the observation-first work. Recommended action when picked up: implement only homogeneoussend_keys_batch, preserve declared order, and keep per-item error reporting clearer than native tmux command-chain optimization.mcp(docs[CHANGES]): Note send_keys_batch workflow split
mcp(feat[pane_tools]): Add send_keys_batch, clarify workflows (#78)