fix(api): fall back to tool name when MCP tool title is null (#42453) (#43010)

Co-authored-by: Taranum01 <taranum01@users.noreply.github.com>
This commit is contained in:
Taranum WasuandTaranum01 authored and GitHub committed 2026-09-28 01:35:22 +00:00
1 parent 43f64090e1
commit 60744157d8
4 files changed
+141 -2

No files matched your search

+7 -1
View File
@@ -71,7 +71,13 @@ class MCPToolProviderController(ToolProviderController[ToolProviderEntityWithPlu
identity=ToolIdentity(
author="Anonymous", # Tool level author is not stored
name=remote_mcp_tool.name,
label=I18nObject(en_US=remote_mcp_tool.name, zh_Hans=remote_mcp_tool.name),
# Prefer the server-provided human-readable title and fall
# back to the programmatic name when the server omitted the
# title or returned ``title: null`` (e.g. Exa's MCP server).
label=I18nObject(
en_US=remote_mcp_tool.title or remote_mcp_tool.name,
zh_Hans=remote_mcp_tool.title or remote_mcp_tool.name,
),
provider=entity.server_identifier,
icon=entity.icon if isinstance(entity.icon, str) else "",
),
@@ -291,7 +291,10 @@ class ToolTransformService:
ToolApiEntity(
author=user_name or "Anonymous",
name=tool.name,
label=I18nObject(en_US=tool.name, zh_Hans=tool.name),
# Prefer the server-provided human-readable title and fall back
# to the programmatic name when the server omitted the title
# or returned ``title: null`` (e.g. Exa's MCP server).
label=I18nObject(en_US=tool.title or tool.name, zh_Hans=tool.title or tool.name),
description=I18nObject(en_US=tool.description or "", zh_Hans=tool.description or ""),
parameters=ToolTransformService.convert_mcp_schema_to_parameter(tool.inputSchema),
labels=[],
@@ -73,3 +73,82 @@ def test_mcp_tool_provider_controller_from_db_delegates_to_entity():
with patch("core.tools.mcp_tool.provider.ToolTransformService.convert_mcp_schema_to_parameter", return_value=[]):
controller = MCPToolProviderController.from_db(db_provider)
assert isinstance(controller, MCPToolProviderController)
def _build_mcp_entity_with_tools(tools: list[dict]) -> MCPProviderEntity:
now = datetime.now()
return MCPProviderEntity(
id="db-id",
server_identifier="mcp_server",
name="MCP Provider",
tenant_id="tenant-1",
user_id="user-1",
server_url="https://mcp.example.com",
headers={},
timeout=30,
sse_read_timeout=300,
authed=False,
credentials={},
tools=tools,
icon="icon.svg",
created_at=now,
updated_at=now,
)
def test_mcp_tool_provider_controller_falls_back_to_name_when_title_is_null():
"""Tools where the server returned ``title: null`` should fall back to the tool name.
Regression test for langgenius/dify#42453.
"""
entity = _build_mcp_entity_with_tools(
tools=[
{
"name": "search_web",
"title": None,
"description": "Search the web",
"inputSchema": {},
"outputSchema": {"type": "object"},
},
{
# ``title`` key missing entirely
"name": "get_content",
"description": "Fetch page content",
"inputSchema": {},
},
]
)
with patch("core.tools.mcp_tool.provider.ToolTransformService.convert_mcp_schema_to_parameter", return_value=[]):
controller = MCPToolProviderController.from_entity(entity)
assert len(controller.entity.tools) == 2
# ``title: null`` -> fallback to ``name``
assert controller.entity.tools[0].identity.name == "search_web"
assert controller.entity.tools[0].identity.label is not None
assert controller.entity.tools[0].identity.label.en_US == "search_web"
assert controller.entity.tools[0].identity.label.zh_Hans == "search_web"
# ``title`` missing entirely -> also fallback to ``name``
assert controller.entity.tools[1].identity.name == "get_content"
assert controller.entity.tools[1].identity.label.en_US == "get_content"
assert controller.entity.tools[1].identity.label.zh_Hans == "get_content"
def test_mcp_tool_provider_controller_uses_title_when_provided():
"""When the server provides a non-null title, it should drive the label."""
entity = _build_mcp_entity_with_tools(
tools=[
{
"name": "search_web",
"title": "Search the Web",
"description": "Search the web",
"inputSchema": {},
}
]
)
with patch("core.tools.mcp_tool.provider.ToolTransformService.convert_mcp_schema_to_parameter", return_value=[]):
controller = MCPToolProviderController.from_entity(entity)
assert len(controller.entity.tools) == 1
assert controller.entity.tools[0].identity.name == "search_web"
assert controller.entity.tools[0].identity.label.en_US == "Search the Web"
assert controller.entity.tools[0].identity.label.zh_Hans == "Search the Web"
@@ -402,3 +402,54 @@ class TestMCPToolTransform:
assert result.original_headers == {"Authorization": "Bearer secret-token"}
assert len(result.tools) == 1
assert result.tools[0].description.en_US == "Tool description"
def test_mcp_tool_to_user_tool_falls_back_to_name_when_title_is_null(self, mock_provider):
"""Tools where the server returned ``title: null`` should fall back to the tool name.
Regression test for langgenius/dify#42453: Exa's MCP server returns
``title: null`` for some tools, breaking tool conversion when the
server-provided title is required.
"""
tools = [
MCPTool(
name="search_web",
title=None,
description="Search the web",
inputSchema={"type": "object", "properties": {}},
),
MCPTool(
name="get_content",
description="Fetch page content",
inputSchema={"type": "object", "properties": {}},
),
]
result = ToolTransformService.mcp_tool_to_user_tool(mock_provider, tools)
assert len(result) == 2
# title is explicitly null -> must fall back to ``name``
assert result[0].name == "search_web"
assert result[0].label.en_US == "search_web"
assert result[0].label.zh_Hans == "search_web"
# title key is absent entirely -> must also fall back to ``name``
assert result[1].name == "get_content"
assert result[1].label.en_US == "get_content"
assert result[1].label.zh_Hans == "get_content"
def test_mcp_tool_to_user_tool_uses_title_when_provided(self, mock_provider):
"""When the server provides a non-null title, it should be used for the label."""
tools = [
MCPTool(
name="search_web",
title="Search the Web",
description="Search the web",
inputSchema={"type": "object", "properties": {}},
)
]
result = ToolTransformService.mcp_tool_to_user_tool(mock_provider, tools)
assert len(result) == 1
assert result[0].name == "search_web"
assert result[0].label.en_US == "Search the Web"
assert result[0].label.zh_Hans == "Search the Web"