Skip to content

Commit de39b7a

Browse files
committed
mcp(test[server]): guard _HANDLE_HINTS against tool-name drift
``_format_handles_section`` filters card hints by ``tool in visible_tool_names`` — a tool name in ``_HANDLE_HINTS`` that no longer matches a registered tool produces the worst of both worlds: agents see a phantom tool name during the brief import-time placeholder window (where ``visible_tool_names is None`` and every hint is emitted unconditionally), then lose the guidance entirely once ``run_server`` filters by real visibility. Phase 1's ``test_tool_description_includes`` already catches drift in tool descriptions. This test catches drift in the parallel ``_HANDLE_HINTS`` table — the two together close the rename-without- update loophole. Pure addition; no source changes.
1 parent 883cd80 commit de39b7a

1 file changed

Lines changed: 32 additions & 0 deletions

File tree

tests/test_server.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,38 @@ def test_build_instructions_default_visible_tool_names_emits_full_card() -> None
279279
)
280280

281281

282+
def test_handle_hints_keys_are_registered_tools() -> None:
283+
"""Every key in ``_HANDLE_HINTS`` must be an actual registered tool name.
284+
285+
``_format_handles_section`` filters hints by ``tool in visible_tool_names``,
286+
so a stale key (e.g. left behind after a tool rename) silently disappears
287+
from the filtered card while still being emitted in the unfiltered
288+
placeholder — the worst of both worlds: agents would see a phantom tool
289+
name during the brief import-time window, then lose the guidance entirely
290+
once ``run_server`` filters by real visibility.
291+
292+
Phase 1's ``test_tool_description_includes`` catches drift in tool
293+
*descriptions*; this test catches drift in the parallel ``_HANDLE_HINTS``
294+
table. The two together close the rename-without-update loophole.
295+
"""
296+
import asyncio
297+
298+
from fastmcp import FastMCP
299+
300+
from libtmux_mcp.server import _HANDLE_HINTS
301+
from libtmux_mcp.tools import register_tools
302+
303+
mcp = FastMCP(name="hint-key-contract")
304+
register_tools(mcp)
305+
306+
registered = {tool.name for tool in asyncio.run(mcp.list_tools())}
307+
for tool_name, _hint in _HANDLE_HINTS:
308+
assert tool_name in registered, (
309+
f"_HANDLE_HINTS key {tool_name!r} is not a registered tool name; "
310+
f"update _HANDLE_HINTS in server.py when renaming or removing tools"
311+
)
312+
313+
282314
def test_registered_tools_accept_socket_name() -> None:
283315
"""All registered tools (except list_servers) accept ``socket_name``.
284316

0 commit comments

Comments
 (0)