Skip to content

Commit 80fa25c

Browse files
committed
mcp(fix[_utils]): Add tmux_bin to cache key and is_alive eviction
why: Cache key only included (socket_name, socket_path), so changing LIBTMUX_TMUX_BIN between calls returned a stale Server. Dead servers were never evicted from the cache. what: - Change cache key to 3-tuple (socket_name, socket_path, tmux_bin) - Add is_alive() check on cache hit to evict dead servers - Add _invalidate_server() for explicit cache eviction - Call _invalidate_server() in kill_server tool after server.kill() - Update test fixtures for 3-tuple cache keys - Add tests for is_alive eviction and _invalidate_server
1 parent 6d748cf commit 80fa25c

File tree

4 files changed

+56
-5
lines changed

4 files changed

+56
-5
lines changed

src/libtmux/mcp/_utils.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
logger = logging.getLogger(__name__)
2323

24-
_server_cache: dict[tuple[str | None, str | None], Server] = {}
24+
_server_cache: dict[tuple[str | None, str | None, str | None], Server] = {}
2525

2626

2727
def _get_server(
@@ -49,7 +49,12 @@ def _get_server(
4949

5050
tmux_bin = os.environ.get("LIBTMUX_TMUX_BIN")
5151

52-
cache_key = (socket_name, socket_path)
52+
cache_key = (socket_name, socket_path, tmux_bin)
53+
if cache_key in _server_cache:
54+
cached = _server_cache[cache_key]
55+
if not cached.is_alive():
56+
del _server_cache[cache_key]
57+
5358
if cache_key not in _server_cache:
5459
kwargs: dict[str, t.Any] = {}
5560
if socket_name is not None:
@@ -63,6 +68,26 @@ def _get_server(
6368
return _server_cache[cache_key]
6469

6570

71+
def _invalidate_server(
72+
socket_name: str | None = None,
73+
socket_path: str | None = None,
74+
) -> None:
75+
"""Evict a server from the cache.
76+
77+
Parameters
78+
----------
79+
socket_name : str, optional
80+
tmux socket name used in the cache key.
81+
socket_path : str, optional
82+
tmux socket path used in the cache key.
83+
"""
84+
keys_to_remove = [
85+
key for key in _server_cache if key[0] == socket_name and key[1] == socket_path
86+
]
87+
for key in keys_to_remove:
88+
del _server_cache[key]
89+
90+
6691
def _resolve_session(
6792
server: Server,
6893
session_name: str | None = None,

src/libtmux/mcp/tools/server_tools.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from libtmux.mcp._utils import (
99
_get_server,
10+
_invalidate_server,
1011
_serialize_session,
1112
handle_tool_errors,
1213
)
@@ -102,6 +103,7 @@ def kill_server(socket_name: str | None = None) -> str:
102103
"""
103104
server = _get_server(socket_name=socket_name)
104105
server.kill()
106+
_invalidate_server(socket_name=socket_name)
105107
return "Server killed successfully"
106108

107109

tests/mcp/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ def mcp_server(server: Server) -> Server:
3030
This fixture sets up the server cache so MCP tools can find the
3131
test server without environment variables.
3232
"""
33-
cache_key = (server.socket_name, None)
33+
cache_key = (server.socket_name, None, None)
3434
_server_cache[cache_key] = server
35-
# Also register as default (None, None) for tools that don't specify a socket
36-
_server_cache[(None, None)] = server
35+
# Also register as default for tools that don't specify a socket
36+
_server_cache[(None, None, None)] = server
3737
return server
3838

3939

tests/mcp/test_utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from libtmux import exc
1010
from libtmux.mcp._utils import (
1111
_get_server,
12+
_invalidate_server,
1213
_resolve_pane,
1314
_resolve_session,
1415
_resolve_window,
@@ -36,8 +37,12 @@ def test_get_server_caches(monkeypatch: pytest.MonkeyPatch) -> None:
3637
"""_get_server returns the same instance for the same socket."""
3738
_server_cache.clear()
3839
s1 = _get_server(socket_name="test_cache")
40+
# Simulate a live server so the cache is not evicted
41+
monkeypatch.setattr(s1, "is_alive", lambda: True)
3942
s2 = _get_server(socket_name="test_cache")
4043
assert s1 is s2
44+
# Verify 3-tuple cache key includes tmux_bin
45+
assert (s1.socket_name, None, None) in _server_cache
4146

4247

4348
def test_get_server_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -120,3 +125,22 @@ def test_serialize_pane(mcp_pane: Pane) -> None:
120125
assert "pane_id" in data
121126
assert "window_id" in data
122127
assert "session_id" in data
128+
129+
130+
def test_get_server_evicts_dead(monkeypatch: pytest.MonkeyPatch) -> None:
131+
"""_get_server evicts cached server when is_alive returns False."""
132+
_server_cache.clear()
133+
s1 = _get_server(socket_name="test_evict")
134+
# Patch is_alive to return False to simulate a dead server
135+
monkeypatch.setattr(s1, "is_alive", lambda: False)
136+
s2 = _get_server(socket_name="test_evict")
137+
assert s1 is not s2
138+
139+
140+
def test_invalidate_server() -> None:
141+
"""_invalidate_server removes matching entries from cache."""
142+
_server_cache.clear()
143+
_get_server(socket_name="test_inv")
144+
assert len(_server_cache) == 1
145+
_invalidate_server(socket_name="test_inv")
146+
assert len(_server_cache) == 0

0 commit comments

Comments
 (0)