mirror of
https://github.com/langgenius/dify.git
synced 2026-03-14 05:29:45 +08:00
fix: fix mcp tool parameter extract (#33258)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
6625828246
commit
0f938d453c
@ -33,6 +33,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolTransformService:
|
||||
_MCP_SCHEMA_TYPE_RESOLUTION_MAX_DEPTH = 10
|
||||
|
||||
@classmethod
|
||||
def get_tool_provider_icon_url(
|
||||
cls, provider_type: str, provider_name: str, icon: str | Mapping[str, str]
|
||||
@ -435,6 +437,46 @@ class ToolTransformService:
|
||||
:return: list of ToolParameter instances
|
||||
"""
|
||||
|
||||
def resolve_property_type(prop: dict[str, Any], depth: int = 0) -> str:
|
||||
"""
|
||||
Resolve a JSON schema property type while guarding against cyclic or deeply nested unions.
|
||||
"""
|
||||
if depth >= ToolTransformService._MCP_SCHEMA_TYPE_RESOLUTION_MAX_DEPTH:
|
||||
return "string"
|
||||
prop_type = prop.get("type")
|
||||
if isinstance(prop_type, list):
|
||||
non_null_types = [type_name for type_name in prop_type if type_name != "null"]
|
||||
if non_null_types:
|
||||
return non_null_types[0]
|
||||
if prop_type:
|
||||
return "string"
|
||||
elif isinstance(prop_type, str):
|
||||
if prop_type == "null":
|
||||
return "string"
|
||||
return prop_type
|
||||
|
||||
for union_key in ("anyOf", "oneOf"):
|
||||
union_schemas = prop.get(union_key)
|
||||
if not isinstance(union_schemas, list):
|
||||
continue
|
||||
|
||||
for union_schema in union_schemas:
|
||||
if not isinstance(union_schema, dict):
|
||||
continue
|
||||
union_type = resolve_property_type(union_schema, depth + 1)
|
||||
if union_type != "null":
|
||||
return union_type
|
||||
|
||||
all_of_schemas = prop.get("allOf")
|
||||
if isinstance(all_of_schemas, list):
|
||||
for all_of_schema in all_of_schemas:
|
||||
if not isinstance(all_of_schema, dict):
|
||||
continue
|
||||
all_of_type = resolve_property_type(all_of_schema, depth + 1)
|
||||
if all_of_type != "null":
|
||||
return all_of_type
|
||||
return "string"
|
||||
|
||||
def create_parameter(
|
||||
name: str, description: str, param_type: str, required: bool, input_schema: dict[str, Any] | None = None
|
||||
) -> ToolParameter:
|
||||
@ -461,10 +503,7 @@ class ToolTransformService:
|
||||
parameters = []
|
||||
for name, prop in props.items():
|
||||
current_description = prop.get("description", "")
|
||||
prop_type = prop.get("type", "string")
|
||||
|
||||
if isinstance(prop_type, list):
|
||||
prop_type = prop_type[0]
|
||||
prop_type = resolve_property_type(prop)
|
||||
if prop_type in TYPE_MAPPING:
|
||||
prop_type = TYPE_MAPPING[prop_type]
|
||||
input_schema = prop if prop_type in COMPLEX_TYPES else None
|
||||
|
||||
@ -7,7 +7,7 @@ import pytest
|
||||
from core.mcp.types import Tool as MCPTool
|
||||
from core.tools.entities.api_entities import ToolApiEntity, ToolProviderApiEntity
|
||||
from core.tools.entities.common_entities import I18nObject
|
||||
from core.tools.entities.tool_entities import ToolProviderType
|
||||
from core.tools.entities.tool_entities import ToolParameter, ToolProviderType
|
||||
from models.tools import MCPToolProvider
|
||||
from services.tools.tools_transform_service import ToolTransformService
|
||||
|
||||
@ -175,6 +175,137 @@ class TestMCPToolTransform:
|
||||
# The actual parameter conversion is handled by convert_mcp_schema_to_parameter
|
||||
# which should be tested separately
|
||||
|
||||
def test_convert_mcp_schema_to_parameter_preserves_anyof_object_type(self):
|
||||
"""Nullable object schemas should keep the object parameter type."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"retrieval_model": {
|
||||
"anyOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "检索模型配置",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
result = ToolTransformService.convert_mcp_schema_to_parameter(schema)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "retrieval_model"
|
||||
assert result[0].type == ToolParameter.ToolParameterType.OBJECT
|
||||
assert result[0].input_schema == schema["properties"]["retrieval_model"]
|
||||
|
||||
def test_convert_mcp_schema_to_parameter_preserves_oneof_object_type(self):
|
||||
"""Nullable oneOf object schemas should keep the object parameter type."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"retrieval_model": {
|
||||
"oneOf": [{"type": "object"}, {"type": "null"}],
|
||||
"description": "检索模型配置",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
result = ToolTransformService.convert_mcp_schema_to_parameter(schema)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "retrieval_model"
|
||||
assert result[0].type == ToolParameter.ToolParameterType.OBJECT
|
||||
assert result[0].input_schema == schema["properties"]["retrieval_model"]
|
||||
|
||||
def test_convert_mcp_schema_to_parameter_handles_null_type(self):
|
||||
"""Schemas with only a null type should fall back to string."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"null_prop_str": {"type": "null"},
|
||||
"null_prop_list": {"type": ["null"]},
|
||||
},
|
||||
}
|
||||
|
||||
result = ToolTransformService.convert_mcp_schema_to_parameter(schema)
|
||||
|
||||
assert len(result) == 2
|
||||
param_map = {parameter.name: parameter for parameter in result}
|
||||
assert "null_prop_str" in param_map
|
||||
assert param_map["null_prop_str"].type == ToolParameter.ToolParameterType.STRING
|
||||
assert "null_prop_list" in param_map
|
||||
assert param_map["null_prop_list"].type == ToolParameter.ToolParameterType.STRING
|
||||
|
||||
def test_convert_mcp_schema_to_parameter_preserves_allof_object_type_with_multiple_object_items(self):
|
||||
"""Property-level allOf with multiple object items should still resolve to object."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"config": {
|
||||
"allOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {"type": "boolean"},
|
||||
},
|
||||
"required": ["enabled"],
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"priority": {"type": "integer", "minimum": 1, "maximum": 10},
|
||||
},
|
||||
"required": ["priority"],
|
||||
},
|
||||
],
|
||||
"description": "Config must match all schemas (allOf)",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
result = ToolTransformService.convert_mcp_schema_to_parameter(schema)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "config"
|
||||
assert result[0].type == ToolParameter.ToolParameterType.OBJECT
|
||||
assert result[0].input_schema == schema["properties"]["config"]
|
||||
|
||||
def test_convert_mcp_schema_to_parameter_preserves_allof_object_type(self):
|
||||
"""Composed property schemas should keep the object parameter type."""
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"retrieval_model": {
|
||||
"allOf": [
|
||||
{"type": "object"},
|
||||
{"properties": {"top_k": {"type": "integer"}}},
|
||||
],
|
||||
"description": "检索模型配置",
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
result = ToolTransformService.convert_mcp_schema_to_parameter(schema)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "retrieval_model"
|
||||
assert result[0].type == ToolParameter.ToolParameterType.OBJECT
|
||||
assert result[0].input_schema == schema["properties"]["retrieval_model"]
|
||||
|
||||
def test_convert_mcp_schema_to_parameter_limits_recursive_schema_depth(self):
|
||||
"""Self-referential composed schemas should stop resolving after the configured max depth."""
|
||||
recursive_property: dict[str, object] = {"description": "Recursive schema"}
|
||||
recursive_property["anyOf"] = [recursive_property]
|
||||
schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"recursive_config": recursive_property,
|
||||
},
|
||||
}
|
||||
|
||||
result = ToolTransformService.convert_mcp_schema_to_parameter(schema)
|
||||
|
||||
assert len(result) == 1
|
||||
assert result[0].name == "recursive_config"
|
||||
assert result[0].type == ToolParameter.ToolParameterType.STRING
|
||||
assert result[0].input_schema is None
|
||||
|
||||
def test_mcp_provider_to_user_provider_for_list(self, mock_provider_full):
|
||||
"""Test mcp_provider_to_user_provider with for_list=True."""
|
||||
# Set tools data with null description
|
||||
|
||||
Loading…
Reference in New Issue
Block a user