Repository: huggingface/smolagents

Issue:
[BUG] DocstringParsingException when creating a tool with types in docstring
**Describe the bug**
When creating a new tool, I get an error "DocstringParsingException: Cannot generate JSON schema for ... because the docstring has no description for the argument '...'", if the type of arguments is given.

**Code to reproduce the error**

Example 1:
```python
@tool
def get_text(book_title:str, ref:str = None, commentary:str=None) -> str:
    """
    Récupère le texte d'un livre ou d'un commentaire.

    Args:
        book_title (str) : Titre du livre (ex. "Genesis", "Berachot")
        ref (str) : Reference interne. Exemple : "3:5" pour Genesis 3:5, "4a" pour Berachot 4a, "5" pour la page Berachot 5 entière.
        commentary (str) : Si spécifié, récupère le commentaire (ex. "Rashi")
    Returns:
        str : Texte du livre ou du commentaire.
    """
    pass
```

Or 

Example 2:
```python
@tool
def get_text(book_title:str) -> str:
    """
    Récupère le texte d'un livre ou d'un commentaire.

    Args:
        book_title (str) : Titre du livre (ex. "Genesis", "Berachot")
    Returns:
        str : Texte du livre ou du commentaire.
    """
    pass
```

**Expected behavior**
No error

**Packages version:**
smolagents==1.12.0


**Additional context**
I think that the bug is due in smolagents/_function_type_hints_utils.py.
I identified two problems:

1- The first is in 

```python
args_split_re = re.compile(
    r"""
(?:^|\n)  # Match the start of the args block, or a newline
\s*(\w+)\s*(?:\([^)]*\))?[<HERE WE SHOULD ADD \s*>]:\s*  # Capture the argument name (ignore the type) and strip spacing
(.*?)\s*  # Capture the argument description, which can span multiple lines, and strip trailing spacing
(?=\n\s*\w+:|\Z)  # Stop when you hit the next argument or the end of the block
""",
    re.DOTALL | re.VERBOSE,
)
```

This explain why example 2 works if I replace 
`        book_title (str) : Titre du livre (ex. "Genesis", "Berachot")` 
by

`        book_title (str): Titre du livre (ex. "Genesis", "Berachot")`


The second is in:

```python
args_split_re = re.compile(
    r"""
(?:^|\n)  # Match the start of the args block, or a newline
\s*(\w+)\s*(?:\([^)]*\))?:\s*  # Capture the argument name (ignore the type) and strip spacing
(.*?)\s*  # Capture the argument description, which can span multiple lines, and strip trailing spacing
(?=\n\s*\w+[<WE SHOULD ADD \s*\([^)]+\)\s*>]:|\Z)  # Stop when you hit the next argument or the end of the block
""",
    re.DOTALL | re.VERBOSE,
)
```

Because for now we can't detect the next line as the next argument if it contains a type. This explain why the Example 1 cannot work even without the space between the type and the ":".

Discussion:
@sysradium: Does this fix the problem https://github.com/huggingface/smolagents/pull/1030?@daavoo: > Does this fix the problem [#1030](https://github.com/huggingface/smolagents/pull/1030)?

Yes, it does. This was one of the bugs I was hitting

Solution patch:
diff --git a/docs/source/en/tutorials/tools.mdx b/docs/source/en/tutorials/tools.mdx
index a075fd7..0786a2f 100644
--- a/docs/source/en/tutorials/tools.mdx
+++ b/docs/source/en/tutorials/tools.mdx
@@ -229,6 +229,14 @@ To speed up the start, tools are loaded only if called by the agent.
 
 Leverage tools from the hundreds of MCP servers available on [glama.ai](https://glama.ai/mcp/servers) or [smithery.ai](https://smithery.ai/).
 
+> [!WARNING]
+> **Security Warning:** Using MCP servers comes with security risks:
+> - **Trust is essential:** Only use MCP servers from trusted sources. Malicious servers can execute harmful code on your machine.
+> - **Stdio-based MCP servers** will always execute code on your machine (that's their intended functionality).
+> - **SSE-based MCP servers** in the current implementation may be vulnerable to remote code execution attacks. A fix is being developed, but caution is still advised.
+>
+> Always verify the source and integrity of any MCP server before connecting to it, especially for production environments.
+
 The MCP servers tools can be loaded with [`ToolCollection.from_mcp`].
 
 For stdio-based MCP servers, pass the server parameters as an instance of `mcp.StdioServerParameters`:
diff --git a/src/smolagents/_function_type_hints_utils.py b/src/smolagents/_function_type_hints_utils.py
index f3b5d09..db9c66a 100644
--- a/src/smolagents/_function_type_hints_utils.py
+++ b/src/smolagents/_function_type_hints_utils.py
@@ -224,16 +224,20 @@ description_re = re.compile(r"^(.*?)[\n\s]*(Args:|Returns:|Raises:|\Z)", re.DOTA
 args_re = re.compile(r"\n\s*Args:\n\s*(.*?)[\n\s]*(Returns:|Raises:|\Z)", re.DOTALL)
 # Splits the Args: block into individual arguments
 args_split_re = re.compile(
-    r"""
-(?:^|\n)  # Match the start of the args block, or a newline
-\s*(\w+)\s*(?:\([^)]*\))?:\s*  # Capture the argument name (ignore the type) and strip spacing
-(.*?)\s*  # Capture the argument description, which can span multiple lines, and strip trailing spacing
-(?=\n\s*\w+:|\Z)  # Stop when you hit the next argument or the end of the block
-""",
+    r"(?:^|\n)"  # Match the start of the args block, or a newline
+    r"\s*(\w+)\s*(?:\([^)]*?\))?:\s*"  # Capture the argument name (ignore the type) and strip spacing
+    r"(.*?)\s*"  # Capture the argument description, which can span multiple lines, and strip trailing spacing
+    r"(?=\n\s*\w+\s*(?:\([^)]*?\))?:|\Z)",  # Stop when you hit the next argument (with or without type) or the end of the block
     re.DOTALL | re.VERBOSE,
 )
 # Extracts the Returns: block from the docstring, if present. Note that most chat templates ignore the return type/doc!
-returns_re = re.compile(r"\n\s*Returns:\n\s*(.*?)[\n\s]*(Raises:|\Z)", re.DOTALL)
+returns_re = re.compile(
+    r"\n\s*Returns:\n\s*"
+    r"(?:[^)]*?:\s*)?"  # Ignore the return type if present
+    r"(.*?)"  # Capture the return description
+    r"[\n\s]*(Raises:|\Z)",
+    re.DOTALL,
+)
 
 
 def _parse_google_format_docstring(


Test patch:
diff --git a/tests/test_function_type_hints_utils.py b/tests/test_function_type_hints_utils.py
index 3379237..c352cf4 100644
--- a/tests/test_function_type_hints_utils.py
+++ b/tests/test_function_type_hints_utils.py
@@ -12,16 +12,216 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-import unittest
-from typing import List, Optional, Tuple
+from typing import Any, Dict, List, Optional, Tuple, Union
 
 import pytest
 
-from smolagents._function_type_hints_utils import get_imports, get_json_schema
+from smolagents._function_type_hints_utils import DocstringParsingException, get_imports, get_json_schema
 
 
-class TestJsonSchema(unittest.TestCase):
-    def test_get_json_schema(self):
+@pytest.fixture
+def valid_func():
+    """A well-formed function with docstring, type hints, and return block."""
+
+    def multiply(x: int, y: float) -> float:
+        """
+        Multiplies two numbers.
+
+        Args:
+            x: The first number.
+            y: The second number.
+        Returns:
+            Product of x and y.
+        """
+        return x * y
+
+    return multiply
+
+
+@pytest.fixture
+def no_docstring_func():
+    """Function with no docstring."""
+
+    def sample(x: int):
+        return x
+
+    return sample
+
+
+@pytest.fixture
+def missing_arg_doc_func():
+    """Function with docstring but missing an argument description."""
+
+    def add(x: int, y: int):
+        """
+        Adds two numbers.
+
+        Args:
+            x: The first number.
+        """
+        return x + y
+
+    return add
+
+
+@pytest.fixture
+def bad_return_func():
+    """Function docstring with missing return description (allowed)."""
+
+    def do_nothing(x: Optional[str] = None):
+        """
+        Does nothing.
+
+        Args:
+            x: Some optional string.
+        """
+        pass
+
+    return do_nothing
+
+
+@pytest.fixture
+def complex_types_func():
+    def process_data(items: List[str], config: Dict[str, float], point: Tuple[int, int]) -> Dict:
+        """
+        Process some data.
+
+        Args:
+            items: List of items to process.
+            config: Configuration parameters.
+            point: A position as (x,y).
+
+        Returns:
+            Processed data result.
+        """
+        return {"result": True}
+
+    return process_data
+
+
+@pytest.fixture
+def optional_types_func():
+    def process_with_optional(required_arg: str, optional_arg: Optional[int] = None) -> str:
+        """
+        Process with optional argument.
+
+        Args:
+            required_arg: A required string argument.
+            optional_arg: An optional integer argument.
+
+        Returns:
+            Processing result.
+        """
+        return "processed"
+
+    return process_with_optional
+
+
+@pytest.fixture
+def enum_choices_func():
+    def select_color(color: str) -> str:
+        """
+        Select a color.
+
+        Args:
+            color: The color to select (choices: ["red", "green", "blue"])
+
+        Returns:
+            Selected color.
+        """
+        return color
+
+    return select_color
+
+
+@pytest.fixture
+def union_types_func():
+    def process_union(value: Union[int, str]) -> Union[bool, str]:
+        """
+        Process a value that can be either int or string.
+
+        Args:
+            value: An integer or string value.
+
+        Returns:
+            Processing result.
+        """
+        return True if isinstance(value, int) else "string result"
+
+    return process_union
+
+
+@pytest.fixture
+def nested_types_func():
+    def process_nested_data(data: List[Dict[str, Any]]) -> List[str]:
+        """
+        Process nested data structure.
+
+        Args:
+            data: List of dictionaries to process.
+
+        Returns:
+            List of processed results.
+        """
+        return ["result"]
+
+    return process_nested_data
+
+
+@pytest.fixture
+def typed_docstring_func():
+    def calculate(x: int, y: float) -> float:
+        """
+        Calculate something.
+
+        Args:
+            x (int): An integer parameter with type in docstring.
+            y (float): A float parameter with type in docstring.
+
+        Returns:
+            float: The calculated result.
+        """
+        return x * y
+
+    return calculate
+
+
+@pytest.fixture
+def mismatched_types_func():
+    def convert(value: int) -> str:
+        """
+        Convert a value.
+
+        Args:
+            value (str): A string value (type mismatch with hint).
+
+        Returns:
+            int: Converted value (type mismatch with hint).
+        """
+        return str(value)
+
+    return convert
+
+
+@pytest.fixture
+def complex_docstring_types_func():
+    def process(data: Dict[str, List[int]]) -> List[Dict[str, Any]]:
+        """
+        Process complex data.
+
+        Args:
+            data (Dict[str, List[int]]): Nested structure with types.
+
+        Returns:
+            List[Dict[str, Any]]: Processed results with types.
+        """
+        return [{"result": sum(v) for k, v in data.items()}]
+
+    return process
+
+
+class TestGetJsonSchema:
+    def test_get_json_schema_example(self):
         def fn(x: int, y: Optional[Tuple[str, str, float]] = None) -> None:
             """
             Test function
@@ -50,10 +250,185 @@ class TestJsonSchema(unittest.TestCase):
             },
             "return": {"type": "null"},
         }
-        self.assertEqual(
-            schema["function"]["parameters"]["properties"]["y"], expected_schema["parameters"]["properties"]["y"]
+        assert schema["function"]["parameters"]["properties"]["y"] == expected_schema["parameters"]["properties"]["y"]
+        assert schema["function"] == expected_schema
+
+    @pytest.mark.parametrize(
+        "fixture_name,should_fail",
+        [
+            ("valid_func", False),
+            # ('no_docstring_func', True),
+            # ('missing_arg_doc_func', True),
+            ("bad_return_func", False),
+        ],
+    )
+    def test_get_json_schema(self, request, fixture_name, should_fail):
+        func = request.getfixturevalue(fixture_name)
+        schema = get_json_schema(func)
+        assert schema["type"] == "function"
+        assert "function" in schema
+        assert "parameters" in schema["function"]
+
+    @pytest.mark.parametrize(
+        "fixture_name,should_fail",
+        [
+            # ('valid_func', False),
+            ("no_docstring_func", True),
+            ("missing_arg_doc_func", True),
+            # ('bad_return_func', False),
+        ],
+    )
+    def test_get_json_schema_raises(self, request, fixture_name, should_fail):
+        func = request.getfixturevalue(fixture_name)
+        with pytest.raises(DocstringParsingException):
+            get_json_schema(func)
+
+    @pytest.mark.parametrize(
+        "fixture_name,expected_properties",
+        [
+            ("valid_func", {"x": "integer", "y": "number"}),
+            ("bad_return_func", {"x": "string"}),
+        ],
+    )
+    def test_property_types(self, request, fixture_name, expected_properties):
+        """Test that property types are correctly mapped."""
+        func = request.getfixturevalue(fixture_name)
+        schema = get_json_schema(func)
+
+        properties = schema["function"]["parameters"]["properties"]
+        for prop_name, expected_type in expected_properties.items():
+            assert properties[prop_name]["type"] == expected_type
+
+    def test_schema_basic_structure(self, valid_func):
+        """Test that basic schema structure is correct."""
+        schema = get_json_schema(valid_func)
+        # Check schema type
+        assert schema["type"] == "function"
+        assert "function" in schema
+        # Check function schema
+        function_schema = schema["function"]
+        assert function_schema["name"] == "multiply"
+        assert "description" in function_schema
+        assert function_schema["description"] == "Multiplies two numbers."
+        # Check parameters schema
+        assert "parameters" in function_schema
+        params = function_schema["parameters"]
+        assert params["type"] == "object"
+        assert "properties" in params
+        assert "required" in params
+        assert set(params["required"]) == {"x", "y"}
+        properties = params["properties"]
+        assert properties["x"]["type"] == "integer"
+        assert properties["y"]["type"] == "number"
+        # Check return schema
+        assert "return" in function_schema
+        return_schema = function_schema["return"]
+        assert return_schema["type"] == "number"
+        assert return_schema["description"] == "Product of x and y."
+
+    def test_complex_types(self, complex_types_func):
+        """Test schema generation for complex types."""
+        schema = get_json_schema(complex_types_func)
+        properties = schema["function"]["parameters"]["properties"]
+        # Check list type
+        assert properties["items"]["type"] == "array"
+        # Check dict type
+        assert properties["config"]["type"] == "object"
+        # Check tuple type
+        assert properties["point"]["type"] == "array"
+        assert len(properties["point"]["prefixItems"]) == 2
+        assert properties["point"]["prefixItems"][0]["type"] == "integer"
+        assert properties["point"]["prefixItems"][1]["type"] == "integer"
+
+    def test_optional_types(self, optional_types_func):
+        """Test schema generation for optional arguments."""
+        schema = get_json_schema(optional_types_func)
+        params = schema["function"]["parameters"]
+        # Required argument should be in required list
+        assert "required_arg" in params["required"]
+        # Optional argument should not be in required list
+        assert "optional_arg" not in params["required"]
+        # Optional argument should be nullable
+        assert params["properties"]["optional_arg"]["nullable"] is True
+        assert params["properties"]["optional_arg"]["type"] == "integer"
+
+    def test_enum_choices(self, enum_choices_func):
+        """Test schema generation for enum choices in docstring."""
+        schema = get_json_schema(enum_choices_func)
+        color_prop = schema["function"]["parameters"]["properties"]["color"]
+        assert "enum" in color_prop
+        assert color_prop["enum"] == ["red", "green", "blue"]
+
+    def test_union_types(self, union_types_func):
+        """Test schema generation for union types."""
+        schema = get_json_schema(union_types_func)
+        value_prop = schema["function"]["parameters"]["properties"]["value"]
+        return_prop = schema["function"]["return"]
+        # Check union in parameter
+        assert len(value_prop["type"]) == 2
+        # Check union in return type
+        assert len(return_prop["type"]) == 2
+
+    def test_nested_types(self, nested_types_func):
+        """Test schema generation for nested complex types."""
+        schema = get_json_schema(nested_types_func)
+        data_prop = schema["function"]["parameters"]["properties"]["data"]
+        assert data_prop["type"] == "array"
+
+    def test_typed_docstring_parsing(self, typed_docstring_func):
+        """Test parsing of docstrings with type annotations."""
+        schema = get_json_schema(typed_docstring_func)
+        # Type hints should take precedence over docstring types
+        assert schema["function"]["parameters"]["properties"]["x"]["type"] == "integer"
+        assert schema["function"]["parameters"]["properties"]["y"]["type"] == "number"
+        # Description should be extracted correctly
+        assert (
+            schema["function"]["parameters"]["properties"]["x"]["description"]
+            == "An integer parameter with type in docstring."
         )
-        self.assertEqual(schema["function"], expected_schema)
+        assert (
+            schema["function"]["parameters"]["properties"]["y"]["description"]
+            == "A float parameter with type in docstring."
+        )
+        # Return type and description should be correct
+        assert schema["function"]["return"]["type"] == "number"
+        assert schema["function"]["return"]["description"] == "The calculated result."
+
+    def test_mismatched_docstring_types(self, mismatched_types_func):
+        """Test that type hints take precedence over docstring types when they conflict."""
+        schema = get_json_schema(mismatched_types_func)
+        # Type hints should take precedence over docstring types
+        assert schema["function"]["parameters"]["properties"]["value"]["type"] == "integer"
+        # Return type from type hint should be used, not docstring
+        assert schema["function"]["return"]["type"] == "string"
+
+    def test_complex_docstring_types(self, complex_docstring_types_func):
+        """Test parsing of complex type annotations in docstrings."""
+        schema = get_json_schema(complex_docstring_types_func)
+        # Check that complex nested type is parsed correctly from type hints
+        data_prop = schema["function"]["parameters"]["properties"]["data"]
+        assert data_prop["type"] == "object"
+        # Check return type
+        return_prop = schema["function"]["return"]
+        assert return_prop["type"] == "array"
+        # Description should include the type information from docstring
+        assert data_prop["description"] == "Nested structure with types."
+        assert return_prop["description"] == "Processed results with types."
+
+    @pytest.mark.parametrize(
+        "fixture_name,expected_description",
+        [
+            ("typed_docstring_func", "An integer parameter with type in docstring."),
+            ("complex_docstring_types_func", "Nested structure with types."),
+        ],
+    )
+    def test_type_in_description_handling(self, request, fixture_name, expected_description):
+        """Test that type information in docstrings is preserved in description."""
+        func = request.getfixturevalue(fixture_name)
+        schema = get_json_schema(func)
+        # First parameter description should contain the expected text
+        first_param_name = list(schema["function"]["parameters"]["properties"].keys())[0]
+        assert schema["function"]["parameters"]["properties"][first_param_name]["description"] == expected_description
 
 
 class TestGetCode: