Repository: stanfordnlp/dspy

Issue:
[Bug] convert_mcp_tool() returns incorrect args when input schema has no parameters
### What happened?

Take this took, which accept not input parameters:

```python
@mcp.tool()
def current_datetime() -> str:
    """Get the current datetime as an iso formatted string with timezone"""
    return "2025-07-23T09:10:10.0+00:00"
```

Calling `convert_mcp_tool()` on that MCP tool results in this `dspy.adapters.types.tool.Tool`:

```python
def current_datetime() -> str:
    """Get the current datetime"""
    return "2025-07-23T09:10:10.0+00:00"

from dspy.adapters.types.tool import Tool
tool = Tool(
    name="current_datetime",
    desc="Get the current datetime as an iso formatted string with timezone",
    args={"args": {}, "kwargs": {}},
    func=current_datetime,
)
```

This becomes a problem when something like ReAct tries to use the tool:

```python
import dspy
dspy.configure(lm=dspy.LM('openai/gpt-4o-mini'))
react = dspy.ReAct(signature="question->answer", tools=[tool])
pred = react(question="What time is it?")
print(dspy.inspect_history(100))
```

Here's a snippet from the history where DSPy attempts to call the tool and fails because it passes the arg arguments that are not accepted:

```
...
User message:

[[ ## question ## ]]
What time is it?

[[ ## trajectory ## ]]
[[ ## thought_0 ## ]]
I need to get the current time to answer the question. I will use the current_datetime tool to retrieve it.

[[ ## tool_name_0 ## ]]
current_datetime

[[ ## tool_args_0 ## ]]
{"args": {}, "kwargs": {}}

[[ ## observation_0 ## ]]
Execution error in current_datetime: 
Traceback (most recent call last):
  File "/Users/tvd/src/others/dspy/dspy/predict/react.py", line 111, in forward 
    trajectory[f"observation_{idx}"] = self.tools[pred.next_tool_name](**pred.next_tool_args)
                                       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/tvd/src/others/dspy/dspy/utils/callback.py", line 326, in sync_wrapper
    return fn(instance, *args, **kwargs)
  File "/Users/tvd/src/others/dspy/dspy/adapters/types/tool.py", line 175, in __call__
    result = self.func(**parsed_kwargs) 
TypeError: current_datetime() got an unexpected keyword argument 'args'
...
```

Expected: we can convert MCP tools with no input parameters to DSPy tools and use them.

Actual: DSPy tools that were created by converting MCP tools with no input parameters cannot be invoked.

### Steps to reproduce

I wrote a test that exercises this behavior here: https://github.com/tvdaptible/dspy/blob/35b7b2e74ae9e9fa8366f42d1cd48192a706bfb3/tests/utils/test_mcp.py#L91-L99

This python file repros the step outlined in the description:
```python

def current_datetime() -> str:
    """Get the current datetime"""
    return "2025-07-23T09:10:10.0+00:00"

from dspy.adapters.types.tool import Tool
tool = Tool(
    name="current_datetime",
    desc="Get the current datetime as an iso formatted string with timezone",
    args={"args": {}, "kwargs": {}},
    func=current_datetime,
)

import dspy
dspy.configure(lm=dspy.LM('openai/gpt-4o-mini'))
react = dspy.ReAct(signature="question->answer", tools=[tool])
pred = react(question="What time is it?")
print(pred)
print("############")
print(dspy.inspect_history(100))
```

### DSPy version

3.0.0b3 (main)


Discussion:
Thanks for the issue and PR, let me take a look



Patch:
diff --git a/dspy/adapters/types/tool.py b/dspy/adapters/types/tool.py
index 843eceed17..0eb8c90fd8 100644
--- a/dspy/adapters/types/tool.py
+++ b/dspy/adapters/types/tool.py
@@ -111,8 +111,8 @@ def _parse_function(self, func: Callable, arg_desc: dict[str, str] | None = None
 
         self.name = self.name or name
         self.desc = self.desc or desc
-        self.args = self.args or args
-        self.arg_types = self.arg_types or arg_types
+        self.args = self.args if self.args is not None else args
+        self.arg_types = self.arg_types if self.arg_types is not None else arg_types
         self.has_kwargs = any(param.kind == param.VAR_KEYWORD for param in sig.parameters.values())
 
     def _validate_and_parse_args(self, **kwargs):


Test patch:
diff --git a/tests/adapters/test_tool.py b/tests/adapters/test_tool.py
index 185714539e..90e23b3312 100644
--- a/tests/adapters/test_tool.py
+++ b/tests/adapters/test_tool.py
@@ -5,7 +5,7 @@
 from pydantic import BaseModel
 
 import dspy
-from dspy.adapters.types.tool import Tool, ToolCalls
+from dspy.adapters.types.tool import Tool, ToolCalls, convert_input_schema_to_tool_args
 
 
 # Test fixtures
@@ -457,3 +457,46 @@ def test_tool_calls_format_from_dict_list():
     assert len(result[0]["tool_calls"]) == 2
     assert result[0]["tool_calls"][0]["function"]["name"] == "search"
     assert result[0]["tool_calls"][1]["function"]["name"] == "translate"
+
+
+def test_tool_convert_input_schema_to_tool_args__no_input_params():
+    args, arg_types, arg_desc = convert_input_schema_to_tool_args(schema={"properties": {}})
+    assert args == {}
+    assert arg_types == {}
+    assert arg_desc == {}
+
+
+def test_tool_convert_input_schema_to_tool_args__lang_chain():
+    # Example from langchain docs:
+    # https://web.archive.org/web/20250723101359/https://api.python.langchain.com/en/latest/tools/langchain_core.tools.tool.html
+    args, arg_types, arg_desc = convert_input_schema_to_tool_args(schema={
+        "title": "fooSchema",
+        "description": "The foo.",
+        "type": "object",
+        "properties": {
+            "bar": {
+                "title": "Bar",
+                "description": "The bar.",
+                "type": "string",
+            },
+            "baz": {
+                "title": "Baz",
+                "type": "integer",
+            }
+        },
+        "required": [
+            "baz",
+        ],
+    })
+    assert args == {
+        "bar": {"title": "Bar", "description": "The bar.", "type": "string"},
+        "baz": {"title": "Baz", "type": "integer"},
+    }
+    assert arg_types == {
+        "bar": str,
+        "baz": int,
+    }
+    assert arg_desc == {
+        "bar": "The bar.",
+        "baz": "No description provided. (Required)",
+    }
diff --git a/tests/utils/resources/mcp_server.py b/tests/utils/resources/mcp_server.py
index 6208ac720e..0361262112 100644
--- a/tests/utils/resources/mcp_server.py
+++ b/tests/utils/resources/mcp_server.py
@@ -38,5 +38,11 @@ def get_account_name(account: Account):
     return account.profile.name
 
 
+@mcp.tool()
+def current_datetime() -> str:
+    """Get the current datetime"""
+    return "2025-07-23T09:10:10.0+00:00"
+
+
 if __name__ == "__main__":
     mcp.run()
diff --git a/tests/utils/test_mcp.py b/tests/utils/test_mcp.py
index 296db0e2c1..9bcdc1e486 100644
--- a/tests/utils/test_mcp.py
+++ b/tests/utils/test_mcp.py
@@ -87,3 +87,12 @@ async def test_convert_mcp_tool():
             }
             result = await nested_pydantic_tool.acall(account=account_in_json)
             assert result == "Bob"
+
+            # Check no input parameter current_datetime tool
+            current_datetime_tool = convert_mcp_tool(session, response.tools[4])
+            assert current_datetime_tool.name == "current_datetime"
+            assert current_datetime_tool.desc == "Get the current datetime"
+            assert current_datetime_tool.args == {}
+            assert current_datetime_tool.arg_types == {}
+            assert current_datetime_tool.arg_desc == {}
+            assert await current_datetime_tool.acall() == "2025-07-23T09:10:10.0+00:00"