Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 74 additions & 15 deletions camel/utils/mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -593,21 +593,13 @@ def generate_function_from_mcp_tool(
parameters_schema = mcp_tool.inputSchema.get("properties", {})
required_params = mcp_tool.inputSchema.get("required", [])

type_map = {
"string": str,
"integer": int,
"number": float,
"boolean": bool,
"array": list,
"object": dict,
}
annotations = {} # used to type hints
defaults: Dict[str, Any] = {} # store default values

func_params = []
for param_name, param_schema in parameters_schema.items():
param_type = param_schema.get("type", "Any")
param_type = type_map.get(param_type, Any)
param_type = self._build_function_param_type(param_type)

annotations[param_name] = param_type
if param_name not in required_params:
Expand Down Expand Up @@ -752,11 +744,12 @@ def run_in_thread():
raise

# Add an async_call method to the function for explicit async usage
adaptive_dynamic_function.async_call = async_mcp_call # type: ignore[attr-defined]
dynamic_fn = adaptive_dynamic_function
dynamic_fn.async_call = async_mcp_call # type: ignore[attr-defined]

adaptive_dynamic_function.__name__ = func_name
adaptive_dynamic_function.__doc__ = func_desc
adaptive_dynamic_function.__annotations__ = annotations
dynamic_fn.__name__ = func_name
dynamic_fn.__doc__ = func_desc
dynamic_fn.__annotations__ = annotations

sig = inspect.Signature(
parameters=[
Expand All @@ -769,9 +762,75 @@ def run_in_thread():
for param in func_params
]
)
adaptive_dynamic_function.__signature__ = sig # type: ignore[attr-defined]
dynamic_fn.__signature__ = sig # type: ignore[attr-defined]

return dynamic_fn

def _build_function_param_type(self, param_type) -> Any:
"""
Dynamically generates a Python type hint corresponding to a given MCP
tool parameter type.

This method maps JSON Schema types (used in MCP) to Python's typing
system.

Examples:
- "string" -> str
- ["string", "null"] -> Optional[str]
- ["string", "integer"] -> Union[str, int]

:param param_type: The 'type' field from the JSON Schema (can be a
string or a list of strings).
:return: A Python type object (e.g., str, int, Optional[str],
Union[...]).
"""

# Map JSON Schema types to Python built-in types
type_map = {
"string": str,
"integer": int,
"number": float,
"boolean": bool,
"array": list,
"object": dict,
}
# Single string type (e.g., "string")
if isinstance(param_type, str):
return type_map.get(param_type, Any)
# Input validation: If it's not a string or a list, fallback to Any.
if not isinstance(param_type, list):
return Any

# List of types (Union Type in JSON Schema)

# Pre-processing: Filter out "null".
# In JSON Schema, the presence of "null" implies the field is
# Nullable/Optional.
tool_types = [t for t in param_type if t != "null"]

# If the list is empty (or contained only "null"), we cannot determine
# a specific type.
if len(tool_types) == 0:
return Any
exist_optional = 'null' in param_type

# Construct the base Python type
type_value: Any
python_types: List[Any] = [type_map.get(t, Any) for t in tool_types]
unique_python_types: List[Any] = []
for python_type in python_types:
if python_type not in unique_python_types:
unique_python_types.append(python_type)

if exist_optional:
unique_python_types.append(type(None))

if len(unique_python_types) == 1:
type_value = unique_python_types[0]
else:
type_value = Union[tuple(unique_python_types)]

return adaptive_dynamic_function
return type_value

def _build_tool_schema(self, mcp_tool: types.Tool) -> Dict[str, Any]:
r"""Build tool schema for OpenAI function calling format."""
Expand Down
51 changes: 51 additions & 0 deletions test/utils/test_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import json
import tempfile
from pathlib import Path
from typing import Any, Optional, Union
from unittest.mock import MagicMock

import mcp.types as types
Expand Down Expand Up @@ -206,6 +207,56 @@ def test_session_properties(self):
assert not client.is_connected()


class TestBuildFunctionParamType:
@pytest.fixture
def client(self):
return MCPClient({"url": "https://example.com"})

@pytest.mark.parametrize(
("input_type", "expected"),
[
("string", str),
("integer", int),
("number", float),
("boolean", bool),
("array", list),
("object", dict),
],
)
def test_basic_primitive_types(self, client, input_type, expected):
assert client._build_function_param_type(input_type) == expected

def test_unknown_type_fallback(self, client):
assert client._build_function_param_type("unknown_alien_type") == Any

def test_optional_types(self, client):
assert (
client._build_function_param_type(["string", "null"])
== Optional[str]
)

def test_union_types(self, client):
assert (
client._build_function_param_type(["string", "integer"])
== Union[str, int]
)

def test_complex_union_optional(self, client):
assert (
client._build_function_param_type(["string", "integer", "null"])
== Optional[Union[str, int]]
)

def test_edge_cases(self, client):
assert client._build_function_param_type([]) == Any
assert client._build_function_param_type(["null"]) == Any
assert (
client._build_function_param_type(["string", "unknown"])
== Union[str, Any]
)
assert client._build_function_param_type(123) == Any


class TestCreateMCPClient:
"""Test create_mcp_client factory function."""

Expand Down
Loading