Skip to content

Commit cafb64f

Browse files
committed
mcp(test[server]): verify socket_name instruction contract
_BASE_INSTRUCTIONS promised "All tools accept an optional socket_name parameter" — but list_servers signature is list_servers(extra_socket_paths: list[str] | None = None) — no socket_name. The instruction lied to agents, and any contract test written against the original wording would fail. Tighten the wording to acknowledge list_servers as the explicit exception: Targeted tmux tools accept an optional socket_name parameter (defaults to LIBTMUX_SOCKET env var); list_servers discovers sockets via TMUX_TMPDIR plus optional extra_socket_paths instead. Add two tests in tests/test_server.py: - test_base_instructions_document_socket_name_contract: locks the three new keywords in the wording. - test_registered_tools_accept_socket_name: enumerates every registered tool via FastMCP.list_tools(), introspects each FunctionTool's signature, and asserts `socket_name in sig.parameters` with `list_servers` as the documented exception. Catches future tool drift before it silently makes the agent-facing instructions a lie again.
1 parent 21ad23e commit cafb64f

3 files changed

Lines changed: 76 additions & 2 deletions

File tree

CHANGES

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@
66
_Notes on upcoming releases will be added here_
77
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->
88

9+
### Tests
10+
11+
- New ``test_registered_tools_accept_socket_name`` introspection test
12+
in ``tests/test_server.py`` enumerates every registered tool via
13+
``FastMCP.list_tools()`` and asserts each accepts a ``socket_name``
14+
parameter, with ``list_servers`` as the documented exception (it
15+
takes ``extra_socket_paths`` instead). Catches future tool drift
16+
before it silently makes ``_BASE_INSTRUCTIONS`` lie to agents. The
17+
``_BASE_INSTRUCTIONS`` text itself is tightened from "All tools
18+
accept an optional socket_name parameter" to "Targeted tmux tools
19+
accept an optional socket_name parameter (defaults to LIBTMUX_SOCKET
20+
env var); list_servers discovers sockets via TMUX_TMPDIR plus
21+
optional extra_socket_paths instead." A companion content test
22+
(``test_base_instructions_document_socket_name_contract``) locks the
23+
wording.
24+
925
### Scripts
1026

1127
- ``scripts/mcp_swap.py``: ``_claude_project_node`` validates that

src/libtmux_mcp/server.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@
4848
"Use pane_id (e.g. '%1') as the preferred targeting method - "
4949
"it is globally unique within a tmux server. "
5050
"Use send_keys to execute commands and capture_pane to read output. "
51-
"All tools accept an optional socket_name parameter for multi-server "
52-
"support (defaults to LIBTMUX_SOCKET env var).\n\n"
51+
"Targeted tmux tools accept an optional socket_name parameter "
52+
"(defaults to LIBTMUX_SOCKET env var); list_servers discovers "
53+
"sockets via TMUX_TMPDIR plus optional extra_socket_paths instead.\n\n"
5354
"IMPORTANT — metadata vs content: list_windows, list_panes, and "
5455
"list_sessions only search metadata (names, IDs, current command). "
5556
"To find text that is actually visible in terminals — when users ask "

tests/test_server.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,63 @@ def test_base_instructions_document_hook_boundary() -> None:
173173
assert "tmux config file" in _BASE_INSTRUCTIONS
174174

175175

176+
def test_base_instructions_document_socket_name_contract() -> None:
177+
"""_BASE_INSTRUCTIONS frames the socket_name promise precisely.
178+
179+
list_servers does NOT accept socket_name (it's the discovery tool —
180+
see server_tools.py:263-264 where the signature is
181+
``list_servers(extra_socket_paths=...)``), so the previous "All
182+
tools accept socket_name" wording was a lie. The instruction now
183+
qualifies "Targeted tmux tools" and explicitly names list_servers
184+
as the documented exception, matching what
185+
test_registered_tools_accept_socket_name asserts at the schema
186+
level.
187+
"""
188+
assert "Targeted tmux tools accept" in _BASE_INSTRUCTIONS
189+
assert "list_servers" in _BASE_INSTRUCTIONS
190+
assert "extra_socket_paths" in _BASE_INSTRUCTIONS
191+
192+
193+
def test_registered_tools_accept_socket_name() -> None:
194+
"""All registered tools (except list_servers) accept ``socket_name``.
195+
196+
``_BASE_INSTRUCTIONS`` promises this with ``list_servers`` as the
197+
documented exception (it discovers sockets via
198+
``extra_socket_paths`` instead, see ``server_tools.py:263-264``).
199+
If a future tool registration drops ``socket_name``, this test
200+
catches the regression instead of silently making the agent-facing
201+
instructions a lie.
202+
"""
203+
import asyncio
204+
import inspect
205+
206+
from fastmcp import FastMCP
207+
from fastmcp.tools.function_tool import FunctionTool
208+
209+
from libtmux_mcp.tools import register_tools
210+
211+
socket_name_exempt = {"list_servers"}
212+
213+
mcp = FastMCP(name="socket-name-contract")
214+
register_tools(mcp)
215+
216+
tools = asyncio.run(mcp.list_tools())
217+
assert tools, "register_tools should have registered at least one tool"
218+
for tool in tools:
219+
if tool.name in socket_name_exempt:
220+
continue
221+
assert isinstance(tool, FunctionTool), (
222+
f"Tool {tool.name!r} is not a FunctionTool; the registry "
223+
f"introspection assumes FastMCP wraps each registered "
224+
f"function with FunctionTool"
225+
)
226+
sig = inspect.signature(tool.fn)
227+
assert "socket_name" in sig.parameters, (
228+
f"Tool {tool.name!r} omits socket_name; either add it, "
229+
f"add to socket_name_exempt, or update _BASE_INSTRUCTIONS"
230+
)
231+
232+
176233
def test_base_instructions_document_buffer_lifecycle() -> None:
177234
"""_BASE_INSTRUCTIONS explains the buffer lifecycle + no list_buffers.
178235

0 commit comments

Comments
 (0)