Skip to content

ant_ai.tools.tool

Tool pydantic-model

Bases: BaseModel

Single public abstraction for tools.

Usage patterns:

1) Subclass for namespaces (methods → tools "ClassName.method"):

class Math(Tool):
    def add(self, x: int, y: int) -> int: ...
    def mul(self, x: int, y: int) -> int: ...

2) Decorate functions with @tool:

@tool
def ping(host: str) -> str: ...

3) MCP tools loaded via mcp_tools_from_url(...):

tools = await mcp_tools_from_url("http://localhost:8000/mcp")
# returns list[Tool] that proxy to MCP tools

Internally, a Tool is either:

  • a namespace (class-based, many methods)
  • a single-callable Python tool (function-based)
  • or a proxy for an MCP tool (created via mcp_tools_from_url)
Notes

When defining a namespace tool, so as a subclass of Tool, the documentation of the class itself is not used by the agent. Instead, it's the documentation of each method that is used.

Show JSON schema:
{
  "description": "Single public abstraction for tools.\n\nUsage patterns:\n\n1) Subclass for *namespaces* (methods \u2192 tools \"ClassName.method\"):\n\n```python\nclass Math(Tool):\n    def add(self, x: int, y: int) -> int: ...\n    def mul(self, x: int, y: int) -> int: ...\n```\n\n2) Decorate functions with @tool:\n\n```python\n@tool\ndef ping(host: str) -> str: ...\n```\n\n3) MCP tools loaded via `mcp_tools_from_url(...)`:\n\n```python\ntools = await mcp_tools_from_url(\"http://localhost:8000/mcp\")\n# returns list[Tool] that proxy to MCP tools\n```\n\nInternally, a Tool is either:\n\n- a *namespace* (class-based, many methods)\n- a *single-callable* Python tool (function-based)\n- or a proxy for an MCP tool (created via `mcp_tools_from_url`)\n\nNotes:\n    When defining a namespace tool, so as a subclass of Tool, the documentation of the class itself is not used by the agent. Instead, it's the documentation of each method that is used.",
  "properties": {
    "name": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Tool name.",
      "title": "Name"
    },
    "description": {
      "anyOf": [
        {
          "type": "string"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "Tool description. Is used by the LLM to decide whether to call or not the specific tool.",
      "title": "Description"
    },
    "parameters": {
      "anyOf": [
        {
          "additionalProperties": true,
          "type": "object"
        },
        {
          "type": "null"
        }
      ],
      "default": null,
      "description": "The parameters needed by the tool. This is a self-constructed field.",
      "title": "Parameters"
    }
  },
  "title": "Tool",
  "type": "object"
}

Config:

  • arbitrary_types_allowed: True

Fields:

Validators:

  • _set_defaults
Source code in src/ant_ai/tools/tool.py
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
class Tool(BaseModel):
    """
    Single public abstraction for tools.

    Usage patterns:

    1) Subclass for *namespaces* (methods → tools "ClassName.method"):

    ```python
    class Math(Tool):
        def add(self, x: int, y: int) -> int: ...
        def mul(self, x: int, y: int) -> int: ...
    ```

    2) Decorate functions with @tool:

    ```python
    @tool
    def ping(host: str) -> str: ...
    ```

    3) MCP tools loaded via `mcp_tools_from_url(...)`:

    ```python
    tools = await mcp_tools_from_url("http://localhost:8000/mcp")
    # returns list[Tool] that proxy to MCP tools
    ```

    Internally, a Tool is either:

    - a *namespace* (class-based, many methods)
    - a *single-callable* Python tool (function-based)
    - or a proxy for an MCP tool (created via `mcp_tools_from_url`)

    Notes:
        When defining a namespace tool, so as a subclass of Tool, the documentation of the class itself is not used by the agent. Instead, it's the documentation of each method that is used.
    """

    model_config = ConfigDict(arbitrary_types_allowed=True)

    name: str | None = Field(default=None, description="Tool name.")
    description: str | None = Field(
        default=None,
        description="Tool description. Is used by the LLM to decide whether to call or not the specific tool.",
    )
    parameters: dict[str, Any] | None = Field(
        default=None,
        description="The parameters needed by the tool. This is a self-constructed field.",
    )

    _func: Callable[..., Any] | None = PrivateAttr(default=None)
    __namespace_methods__: list[str] = []
    _func_signature: inspect.Signature | None = PrivateAttr(default=None)

    @model_validator(mode="after")
    def _set_defaults(self) -> Tool:
        if self.name is None:
            self.name: str = self.__class__.__name__

        if self.description is None:
            doc: str = inspect.getdoc(self.__class__) or ""
            self.description = doc.strip() or ""

        if self.parameters is None:
            self.parameters: dict[str, Any] = {"type": "object", "properties": {}}

        return self

    def __init_subclass__(cls, **kwargs: Any) -> None:
        """
        When you subclass Tool, this inspects the class body and collects
        all public callables as "namespace methods".
        """
        super().__init_subclass__(**kwargs)
        inherited_names: set[str] = {
            name for klass in cls.__mro__[1:] for name in klass.__dict__
        }
        cls.__namespace_methods__: list[str] = [
            name
            for name, value in cls.__dict__.items()
            if _is_public_callable(name, value) and name not in inherited_names
        ]

    def _call_func(self, *args: Any, **kwargs: Any) -> Any:
        """
        Internal: call self._func with positional + keyword args,
        normalized via the underlying function's signature.
        """
        if self._func is None:
            raise RuntimeError("Cannot invoke a namespace Tool directly.")

        # Lazily cache the signature
        if self._func_signature is None:
            self._func_signature = inspect.signature(self._func)

        bound = self._func_signature.bind_partial(*args, **kwargs)
        return self._func(*bound.args, **bound.kwargs)

    @property
    def is_namespace(self) -> bool:
        """
        True if this Tool is a namespace (class with methods),
        False if it's a single-callable tool created by @tool or MCP.
        """
        return (
            bool(getattr(self.__class__, "__namespace_methods__", []))
            and self._func is None
        )

    @classmethod
    def _from_function(
        cls,
        func: Callable[..., Any],
        *,
        name: str | None = None,
        description: str | None = None,
        args_model: type[BaseModel] | None = None,
    ) -> Tool:
        tool_name: str = name or getattr(func, "__name__", None) or ""

        if args_model is None:
            args_model = _build_args_model_from_signature(func, f"{tool_name}_Args")

        schema = args_model.model_json_schema()
        schema.setdefault("type", "object")

        inst = cls(
            name=tool_name,
            description=(description or inspect.getdoc(func) or "").strip(),
            parameters=schema,
        )
        inst._func = func
        return inst

    @classmethod
    def _from_mcp_descriptor(
        cls,
        *,
        url: str,
        mcp_tool: Any,
        namespace: str | None = None,
        unwrap_result: bool = True,
    ) -> Tool:
        full_name: str = f"{namespace}.{mcp_tool.name}" if namespace else mcp_tool.name
        params: dict = dict(mcp_tool.inputSchema or {})
        params.setdefault("type", "object")

        async def _call_mcp(**kwargs: Any) -> Any:
            # Open a short-lived MCP HTTP connection per call.
            # This keeps the Tool self-contained and agent-friendly.
            async with streamable_http_client(url) as client_context:  # pyright: ignore[reportOptionalCall]
                read, write, _aclose = client_context
                async with MCPClientSession(read, write) as session:  # pyright: ignore[reportOptionalCall]
                    await session.initialize()
                    result: mcp.types.CallToolResult = await session.call_tool(
                        mcp_tool.name, arguments=kwargs
                    )

            if not unwrap_result:
                return result

            structured = getattr(result, "structuredContent", None)
            if structured not in (None, {}):
                return structured

            content = getattr(result, "content", None)
            if content and hasattr(content[0], "text"):
                return content[0].text

            return result

        inst = cls(
            name=full_name, description=mcp_tool.description or "", parameters=params
        )
        inst._func = _call_mcp
        return inst

    def _expand_namespace(self) -> list[Tool]:
        """
        For a namespace Tool (class-based), build a list of
        single-callable Tools named "ClassName.method".
        """
        tools: list[Tool] = []
        ns: str = self.name or self.__class__.__name__

        for method_name in self.__class__.__namespace_methods__:
            bound = getattr(self, method_name)
            doc = (
                inspect.getdoc(bound)
                or inspect.getdoc(getattr(self.__class__, method_name))
                or ""
            )

            method_tool: Tool = Tool._from_function(
                func=bound,
                name=f"{ns}_{method_name}",
                description=doc.strip() or f"Method {method_name} on {ns}",
            )
            tools.append(method_tool)

        return tools

    def invoke(self, *args, **kwargs: Any) -> Any:
        """
        Synchronous invocation.

        For plain Python tools, this returns the result directly.
        For MCP-backed tools, this returns a coroutine; you should use
        `await tool.ainvoke(...)` in async code.
        """
        if self._func is None:
            raise RuntimeError("Cannot invoke a namespace Tool directly.")
        return self._call_func(*args, **kwargs)

    async def ainvoke(self, *args, **kwargs: Any) -> Any:
        """
        Asynchronous invocation.

        - If the underlying tool is async → await it directly.
        - If it's sync → run it in a thread executor so we don't block the loop.
        """
        if self._func is None:
            raise RuntimeError("Cannot invoke a namespace Tool directly.")

        if inspect.iscoroutinefunction(self._func):
            # Call it directly; don't go through invoke() to avoid double checks
            result: Any = self._call_func(*args, **kwargs)
            # result should already be a coroutine
            return await cast(Awaitable[Any], result)

        loop: AbstractEventLoop = asyncio.get_running_loop()
        bound: partial[Any] = functools.partial(self._call_func, *args, **kwargs)
        return await loop.run_in_executor(None, bound)

    @model_serializer
    def _serialize(self) -> dict[str, Any]:
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description or "",
                "parameters": self.parameters or {"type": "object", "properties": {}},
            },
        }

    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        if self._func is None:
            raise RuntimeError("Cannot call a namespace Tool directly.")
        return self._call_func(*args, **kwargs)

name pydantic-field

name: str | None = None

Tool name.

description pydantic-field

description: str | None = None

Tool description. Is used by the LLM to decide whether to call or not the specific tool.

parameters pydantic-field

parameters: dict[str, Any] | None = None

The parameters needed by the tool. This is a self-constructed field.

is_namespace property

is_namespace: bool

True if this Tool is a namespace (class with methods), False if it's a single-callable tool created by @tool or MCP.

__init_subclass__

__init_subclass__(**kwargs: Any) -> None

When you subclass Tool, this inspects the class body and collects all public callables as "namespace methods".

Source code in src/ant_ai/tools/tool.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def __init_subclass__(cls, **kwargs: Any) -> None:
    """
    When you subclass Tool, this inspects the class body and collects
    all public callables as "namespace methods".
    """
    super().__init_subclass__(**kwargs)
    inherited_names: set[str] = {
        name for klass in cls.__mro__[1:] for name in klass.__dict__
    }
    cls.__namespace_methods__: list[str] = [
        name
        for name, value in cls.__dict__.items()
        if _is_public_callable(name, value) and name not in inherited_names
    ]

invoke

invoke(*args, **kwargs: Any) -> Any

Synchronous invocation.

For plain Python tools, this returns the result directly. For MCP-backed tools, this returns a coroutine; you should use await tool.ainvoke(...) in async code.

Source code in src/ant_ai/tools/tool.py
329
330
331
332
333
334
335
336
337
338
339
def invoke(self, *args, **kwargs: Any) -> Any:
    """
    Synchronous invocation.

    For plain Python tools, this returns the result directly.
    For MCP-backed tools, this returns a coroutine; you should use
    `await tool.ainvoke(...)` in async code.
    """
    if self._func is None:
        raise RuntimeError("Cannot invoke a namespace Tool directly.")
    return self._call_func(*args, **kwargs)

ainvoke async

ainvoke(*args, **kwargs: Any) -> Any

Asynchronous invocation.

  • If the underlying tool is async → await it directly.
  • If it's sync → run it in a thread executor so we don't block the loop.
Source code in src/ant_ai/tools/tool.py
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
async def ainvoke(self, *args, **kwargs: Any) -> Any:
    """
    Asynchronous invocation.

    - If the underlying tool is async → await it directly.
    - If it's sync → run it in a thread executor so we don't block the loop.
    """
    if self._func is None:
        raise RuntimeError("Cannot invoke a namespace Tool directly.")

    if inspect.iscoroutinefunction(self._func):
        # Call it directly; don't go through invoke() to avoid double checks
        result: Any = self._call_func(*args, **kwargs)
        # result should already be a coroutine
        return await cast(Awaitable[Any], result)

    loop: AbstractEventLoop = asyncio.get_running_loop()
    bound: partial[Any] = functools.partial(self._call_func, *args, **kwargs)
    return await loop.run_in_executor(None, bound)

tool

tool(_func: Callable[..., Any]) -> Tool
tool(
    _func: None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    args_model: type[BaseModel] | None = None,
) -> Callable[[Callable[..., Any]], Tool]
tool(
    _func: Callable[..., Any] | None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    args_model: type[BaseModel] | None = None,
) -> Callable[[Callable[..., Any]], Tool] | Tool

Decorator for functions that turns them into Tool instances.

From the user's POV:

@tool
def ping(host: str) -> str: ...
Source code in src/ant_ai/tools/tool.py
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
def tool(
    _func: Callable[..., Any] | None = None,
    *,
    name: str | None = None,
    description: str | None = None,
    args_model: type[BaseModel] | None = None,
) -> Callable[[Callable[..., Any]], Tool] | Tool:
    """
    Decorator for *functions* that turns them into Tool instances.

    From the user's POV:

        @tool
        def ping(host: str) -> str: ...
    """

    def decorator(func: Callable[..., Any]) -> Tool:
        return Tool._from_function(
            func,
            name=name,
            description=description,
            args_model=args_model,
        )

    if _func is not None:
        return decorator(_func)

    return decorator

mcp_tools_from_url async

mcp_tools_from_url(
    url: str,
    *,
    namespace: str | None = None,
    unwrap_result: bool = True,
) -> list[Tool]

Connect to an MCP server over HTTP(S) and adapt its tools into Tool objects.

This is the only MCP-specific entry point you need from the outside.

  • url: MCP HTTP endpoint (streamable), e.g. "http://localhost:8000/mcp"
  • namespace: optional prefix for tool names, e.g. "weather.get_forecast"
  • unwrap_result:
    • if True: return structuredContent or first text fragment
    • if False: return the full CallToolResult

Example:

tools = await mcp_tools_from_url("http://localhost:8000/mcp", namespace="remote")
agent = Agent(tools=tools, ...)  # your agent just sees Tool objects
Source code in src/ant_ai/tools/tool.py
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
async def mcp_tools_from_url(
    url: str,
    *,
    namespace: str | None = None,
    unwrap_result: bool = True,
) -> list[Tool]:
    """
    Connect to an MCP server over HTTP(S) and adapt its tools into `Tool` objects.

    This is the *only* MCP-specific entry point you need from the outside.

    - `url`: MCP HTTP endpoint (streamable), e.g. "http://localhost:8000/mcp"
    - `namespace`: optional prefix for tool names, e.g. "weather.get_forecast"
    - `unwrap_result`:
        * if True: return structuredContent or first text fragment
        * if False: return the full `CallToolResult`

    Example:

        tools = await mcp_tools_from_url("http://localhost:8000/mcp", namespace="remote")
        agent = Agent(tools=tools, ...)  # your agent just sees Tool objects
    """

    async with streamable_http_client(url) as client_context:  # pyright: ignore[reportOptionalCall]
        read, write, _aclose = client_context
        async with MCPClientSession(read, write) as session:  # pyright: ignore[reportOptionalCall]
            await session.initialize()
            tools_resp: mcp.types.ListToolsResult = await session.list_tools()
            mcp_tools: list[mcp.types.Tool] = tools_resp.tools

    tools: list[Tool] = [
        Tool._from_mcp_descriptor(
            url=url,
            mcp_tool=mcp_tool,
            namespace=namespace,
            unwrap_result=unwrap_result,
        )
        for mcp_tool in mcp_tools
    ]

    return tools