tmux-python / tmux-python/libtmux-mcp

The documented self-location recipe is unexecutable: filters={"is_caller": true} silently returns []

Open
#92 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Python
Stars
13
Forks
0
Avg merge
13h 52m
Merged PRs (30d)
4

Description

Summary

The server instructions tell every agent, verbatim, how to answer "which pane am I in?":

Tool results mark is_caller=true; filter list_panes for it to answer 'which pane am I in?' (no whoami tool).

That instruction cannot work. _apply_filters applies filters to the raw libtmux QueryList of Pane objects before serialization, but is_caller is a serialization-time computation — it does not exist as an attribute on libtmux.Pane. libtmux's keygetter returns None for the unknown attribute and filter_lookup then returns False for every row, so the call returns an empty list.

It fails silently: no error, no warning, just []. That is the worst possible failure shape for an LLM — the agent concludes "I'm not in any pane" or, more commonly, falls back to pulling the full unbounded listing and post-filtering in its own context. Which is precisely the expensive path the instruction appears to be steering it away from.

Evidence

The instruction, in _BASE_INSTRUCTIONS:

". Tool results mark is_caller=true; filter list_panes for it to answer "
"'which pane am I in?' (no whoami tool)."

Filters are applied to un-serialized libtmux objects:

But is_caller is only produced during serialization:

And the lookup silently degrades to False rather than raising:

Reproduction

Environment: libtmux-mcp==0.1.0a17, MCP server launched from inside a tmux pane on the default socket.

  1. Call list_panes(filters={"is_caller": true}).
  2. Observed: [] — an empty list, no error.
  3. Expected: exactly one PaneInfo (the caller's own pane); or, failing that, an explicit error stating that is_caller is not a filterable field.
  4. Control: call list_panes() with no filters and observe that exactly one row does carry is_caller: true. The field is real; it just cannot be filtered on.

Suggested fix

Two independent changes; both are worth making.

1. Make the advertised filter actually work. is_caller is a synthetic, serialization-time field, so it must be filtered after serialization. Split the filter set in _apply_filters:

_SYNTHETIC_PANE_FIELDS = {"is_caller"}

def _apply_filters(items, filters, serializer):
    coerced = _coerce_dict_arg("filters", filters)
    if not coerced:
        return [serializer(item) for item in items]
    synthetic = {k: v for k, v in coerced.items() if k in _SYNTHETIC_PANE_FIELDS}
    native = {k: v for k, v in coerced.items() if k not in _SYNTHETIC_PANE_FIELDS}
    rows = [serializer(item) for item in (items.filter(**native) if native else items)]
    for key, want in synthetic.items():
        rows = [r for r in rows if getattr(r, key, None) == want]
    return rows

Alternatively, reject unknown filter keys with an ExpectedToolError rather than returning [] — a silent empty result is strictly worse than a loud failure.

2. Stop routing self-location through a full listing at all. Even a working filters={"is_caller": true} still pays a full list-panes -a server-side; it only saves tokens, not tmux work (_apply_filters materializes the whole QueryList and filters in Python). The right primitive is a dedicated self-location tool (where_am_i) — see the companion issue. Once it exists, delete the filter list_panes for it sentence from _BASE_INSTRUCTIONS entirely.

Tests

In tests/test_pane_tools.py (or tests/test_utils.py):

  1. Monkeypatch _get_caller_identity to return a deterministic CallerIdentity pointing at a known pane on the test server.
  2. Assert list_panes(filters={"is_caller": True}) returns exactly that one pane — this fails today, returning [].
  3. Assert list_panes() (unfiltered) contains exactly one row with is_caller is True, proving the field is populated and the bug is in the filter path only.
  4. Add a regression test that an unknown/synthetic filter key does not silently yield [].

Related

  • #19 — is_caller annotation gives false positives across tmux sockets (same annotation, adjacent defect)
  • Companion issue: add a whoami primitive so the instructions no longer have to advertise this recipe at all.

Environment

  • libtmux-mcp: 0.1.0a17
  • libtmux: 0.61.0
  • fastmcp: 3.4.3 / mcp: 1.28.1
  • tmux: 3.7b
  • Python: 3.14.0

Cross-references — filed together from a single audit of v0.1.0a17. The self-location gap is the root cause; the rest are what it exposed.

  • tmux-python/libtmux-mcp#92 — [bug] documented self-location recipe is unexecutable (filters={"is_caller": true}[])
  • tmux-python/libtmux-mcp#93 — [feat] where_am_i self-location primitive + self-first relational routing (root fix)
  • tmux-python/libtmux-mcp#94 — [bug] caller's socket advertised but never targeted; TMUX env race vs. the threadpool
  • tmux-python/libtmux-mcp#95 — [perf] list_* unbounded, unscoped, unprojected, excluded from the response limiter
  • tmux-python/libtmux-mcp#96 — [feat] invocation context (in tmux? server running?) + explicit tmux-server auto-start
  • tmux-python/libtmux#704 — upstream: no self-location API (TMUX_PANE); unreachable socket_path derivation

(← this issue: #92)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with _apply_filters in src/libtmux_mcp/_utils.py, then read _serialize_pane and _BASE_INSTRUCTIONS in src/libtmux_mcp/server.py. Reproduce the empty result with list_panes(filters={"is_caller": true}), then run the relevant tests in tests/test_pane_tools.py or tests/test_utils.py. Done means synthetic filtering works and unknown filter keys no longer silently return an empty list.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
backend-api-design, testing-qa
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.