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
19 changes: 19 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### Breaking changes

**{tooliconl}`wait-for-text` warnings move from MCP log notifications to stderr**

MCP deprecated the protocol-level Logging capability in
[SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577),
effective with the `2026-07-28` revision, naming stderr as the migration path
for stdio servers. {tooliconl}`wait-for-text` was the only tool that used it —
an invalid regex, a timeout, and the history-limit trim-risk band each emitted a
`notifications/message` warning. All three are now ordinary `libtmux_mcp.*` log
records on stderr, where every other libtmux-mcp record already goes.

A client that subscribed to MCP log notifications no longer receives these three
over the protocol. Nothing an agent can act on is lost: the invalid regex is
still raised as a tool error, and the timeout is still reported as
{attr}`~libtmux_mcp.models.WaitForTextResult.outcome`. This supersedes the
0.1.0a19 note that the trim-risk signal is "delivered only as an MCP warning
notification". (#110)

### Documentation

#### Caller identity fields are described (#105)
Expand Down
31 changes: 31 additions & 0 deletions MIGRATION
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,37 @@ for the full release log.
[tracker]: https://github.com/tmux-python/libtmux-mcp/discussions
```

## libtmux-mcp 0.1.x (unreleased)

### `wait_for_text` warnings arrive on stderr, not as MCP log notifications

MCP deprecated the protocol-level Logging capability in [SEP-2577], effective
with the `2026-07-28` revision, and names stderr as the migration path for
stdio servers. `wait_for_text` was the only tool using it.

Three warnings changed channel — an invalid regex, a wait that timed out, and
polling inside tmux's history-limit trim-risk band. Wording and timing are
unchanged; only the transport differs.

#### Before

An MCP `notifications/message` at level `warning`.

#### After

An ordinary `libtmux_mcp.tools.pane_tools.wait` log record on stderr. Under the
stdio transport the client already captures that stream, so most log panels
show them as before.

If you read these through an MCP client's log handler, switch to the server
process's stderr. If you drive the tools from Python, `caplog` or any handler
on the `libtmux_mcp` logger sees them.

This supersedes the 0.1.0a19 note below stating that the trim-risk signal is
delivered only as an MCP warning notification.

[SEP-2577]: https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577

## libtmux-mcp 0.1.0a19 (2026-07-25)

### `wait_for_text` takes `patterns`, and `wait_for_content_change` is gone
Expand Down
4 changes: 2 additions & 2 deletions docs/tools/pane/wait-for-text.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ rather than rejected, so this can be lower than what you asked for.

Matching is best-effort once polling enters tmux's history-limit trim-risk band,
because older scrollback can be discarded while the wait is active. The server
reports that as an MCP warning notification; use {tooliconl}`wait-for-channel`
for deterministic command completion.
logs that to stderr; use {tooliconl}`wait-for-channel` for deterministic
command completion.

```{fastmcp-tool-input} pane_tools.wait_for_text
```
67 changes: 17 additions & 50 deletions src/libtmux_mcp/tools/pane_tools/wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,31 +118,6 @@ async def _maybe_report_progress(
return


_LogLevel = t.Literal["debug", "info", "warning", "error"]


async def _maybe_log(
ctx: Context | None,
*,
level: _LogLevel,
message: str,
) -> None:
"""Call the matching ``ctx.{level}`` if a Context is available.

Sibling to :func:`_maybe_report_progress` for client-visible log
notifications (``notifications/message`` in MCP). Same suppression
contract: silent only when the transport is gone, propagating
everything else so programming errors stay loud.
"""
if ctx is None:
return
method = getattr(ctx, level)
try:
await method(message)
except _TRANSPORT_CLOSED_EXCEPTIONS:
return


# ---------------------------------------------------------------------------
# Timeout-bounded tmux reads
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -412,13 +387,12 @@ async def _first_pane_of_window(
# ---------------------------------------------------------------------------


async def _compile_patterns(
def _compile_patterns(
values: list[str],
*,
label: str,
regex: bool,
match_case: bool,
ctx: Context | None,
) -> list[re.Pattern[str]]:
"""Compile one pattern list, raising ``ExpectedToolError`` on bad input."""
flags = 0 if match_case else re.IGNORECASE
Expand All @@ -431,7 +405,7 @@ async def _compile_patterns(
compiled.append(re.compile(value if regex else re.escape(value), flags))
except re.error as e:
msg = f"Invalid regex pattern: {e}"
await _maybe_log(ctx, level="warning", message=msg)
logger.warning(msg)
raise ExpectedToolError(msg) from e
return compiled

Expand Down Expand Up @@ -556,8 +530,8 @@ async def wait_for_text(
``hsize`` shrinks below the entry value (``clear-history``, and any
rollover whose dip is observable between polls). It does not
reliably detect ``grid_collect_history`` trim during continuous
output; a runtime ``ctx.warning`` fires when sampled state enters
the trim-risk band. Use ``wait_for_channel`` when correctness
output; a runtime warning is logged to stderr when sampled state
enters the trim-risk band. Use ``wait_for_channel`` when correctness
matters more than convenience.
"""
ceiling = _wait_ceiling_seconds()
Expand All @@ -578,19 +552,17 @@ async def wait_for_text(
# compute, and a field the agent never branches on is permanent
# weight in ``outputSchema``.

compiled_patterns = await _compile_patterns(
compiled_patterns = _compile_patterns(
patterns or [],
label="patterns",
regex=regex,
match_case=match_case,
ctx=ctx,
)
compiled_stop = await _compile_patterns(
compiled_stop = _compile_patterns(
stop or [],
label="stop",
regex=regex,
match_case=match_case,
ctx=ctx,
)

server = _get_server(socket_name=socket_name)
Expand Down Expand Up @@ -727,18 +699,15 @@ async def wait_for_text(
trim_batch = max(baseline_hlimit // 10, 1)
risk_floor = baseline_hlimit - trim_batch
if state.history_size >= risk_floor:
await _maybe_log(
ctx,
level="warning",
message=(
f"pane {target} is polling in the "
"history-limit trim-risk band "
f"(history_size {state.history_size} / "
f"history_limit {baseline_hlimit}); "
"wait_for_text correctness is best-effort "
"here. For deterministic synchronization "
"use wait_for_channel."
),
logger.warning(
"pane %s is polling in the history-limit "
"trim-risk band (history_size %s / "
"history_limit %s); wait_for_text correctness "
"is best-effort here. For deterministic "
"synchronization use wait_for_channel.",
target,
state.history_size,
baseline_hlimit,
)
warned_risk_band = True
# Anchored ON the entry cursor row, not below it. That row is
Expand Down Expand Up @@ -835,10 +804,8 @@ async def wait_for_text(
# is a different fix — read the screen, don't retry the wait.
outcome = "alternate_screen"
if not found:
await _maybe_log(
ctx,
level="warning",
message=f"No match in pane {target} before {effective_timeout}s timeout",
logger.warning(
"No match in pane %s before %ss timeout", target, effective_timeout
)

limited_tail = _limit_lines(
Expand Down
Loading