Always emit a tool title, derived from name when unset (#4694)

* Always emit a tool title, derived from name when unset

Some MCP clients (e.g. ChatGPT) drop tools with no `title` instead of
falling back to `name` for display as the spec allows. Deriving a
default title in Tool.to_mcp_tool() fixes this for every tool built on
top of it, including the search-transform, code-mode, and session
proxy tools that never set one explicitly.

Fixes #4414

* Derive fallback title from the overridden name, document it

Addresses Codex review on #4694.

* Resolve title precedence from effective overrides

* Normalize mapping annotations before deriving the title
This commit is contained in:
Jeremiah Lowin 2026-07-28 17:30:20 -04:00 committed by GitHub
commit baced6281c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 84 additions and 11 deletions

View file

@ -137,6 +137,28 @@ class TestBaseTransformBehavior:
assert await mcp.get_tool("find_tools") is not None
assert await mcp.get_tool("run_tool") is not None
@pytest.mark.parametrize(
"transform_cls", [RegexSearchTransform, BM25SearchTransform]
)
async def test_synthetic_tools_have_titles(self, transform_cls):
"""Synthetic search/call tools must carry a title.
Some MCP clients (e.g. ChatGPT) drop tools with no `title` field,
which breaks tool-search discovery entirely. See #4414.
"""
mcp = _make_server_with_tools()
mcp.add_transform(
transform_cls(
search_tool_name="find_tools", call_tool_name="call_read_tool"
)
)
tools = await mcp.list_tools()
titles = {t.name: t.to_mcp_tool().title for t in tools}
assert titles == {
"find_tools": "Find Tools",
"call_read_tool": "Call Read Tool",
}
async def test_search_respects_visibility_filtering(self):
"""Tools disabled via Visibility transform should not appear in search."""
mcp = _make_server_with_tools()