Custom Tools
Capability tools in v1 come from Python files exported by a capability manifest. They are not
declared inline in capability.yaml, and there is no separate shell-tool runtime in the current
contract.
Tools are structured functions that an LLM can call. The SDK uses Python type annotations and Pydantic models to validate inputs and serialize parameters for model providers.
Export tools from a capability
Section titled “Export tools from a capability”Add Python files to the manifest:
schema: 1name: threat-huntingversion: '0.1.0'description: Threat hunting tools.
tools: - tools/intel.pyIf tools is omitted, the runtime auto-discovers Python files under tools/.
Function tools
Section titled “Function tools”Use @dreadnode.tool for simple stateless tools:
import typing as t
import dreadnode
@dreadnode.tooldef lookup_indicator( indicator: t.Annotated[str, "IP, domain, or hash to investigate"],) -> dict[str, str]: """Look up an indicator in an intel source.""" return { "indicator": indicator, "verdict": "unknown", }If you want full control without the decorator, use Tool.from_callable() directly.
from dreadnode.agents.tools import Tool
def add(x: int, y: int) -> int: return x + y
add_tool = Tool.from_callable(add, name="add", description="Add two numbers.")Type hints drive the parameter schema. Use typing.Annotated to add short parameter descriptions.
Toolsets
Section titled “Toolsets”Use dreadnode.Toolset when you want a grouped or stateful set of tools:
import typing as t
import dreadnode
class IntelTools(dreadnode.Toolset): @dreadnode.tool_method def lookup( self, indicator: t.Annotated[str, "Indicator to investigate"], ) -> dict[str, str]: """Look up an indicator.""" return {"indicator": indicator, "verdict": "unknown"}The loader collects:
- module-level
Toolinstances - module-level
Toolsetinstances Toolsetsubclasses that can be constructed without arguments
Wrapping capability tools
Section titled “Wrapping capability tools”Capabilities ship tool definitions in capability.yaml. Use wrap_tool and wrap_capability to
convert capability tool defs into SDK Tool instances that can be merged into your tool map.
import asyncio
from dreadnode.capabilities import load_capability, wrap_capability, wrap_tool
async def main() -> None: loaded = await load_capability("./capabilities/threat-hunting") wrapped = wrap_capability(loaded)
first_tool = loaded.manifest.tools[0] if loaded.manifest.tools else None if first_tool: single = wrap_tool(first_tool, loaded) print("Wrapped tool:", single.name)
print(f"Wrapped {len(wrapped.tools)} tools from {wrapped.name}.")
asyncio.run(main())When not to use Python tools
Section titled “When not to use Python tools”Use MCP instead when the implementation is:
- a shell command
- a Node or Go service
- a remote API integration
- a third-party tool you want to run out of process
That keeps capability tooling split into two paths:
- Python-native logic via
@dreadnode.toolanddreadnode.Toolset - everything else via MCP