mirror of
https://github.com/langgenius/dify.git
synced 2026-10-04 13:47:55 +08:00
Co-authored-by: Taranum01 <taranum01@users.noreply.github.com>
This commit is contained in:
1 parent
43f64090e1
commit
60744157d8
4 files changed
+141
-2
No files matched your search
@@ -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"
|
||||
Reference in new issue
Block a user