Documentation Index
Fetch the complete documentation index at: https://docs.openhands.dev/llms.txt
Use this file to discover all available pages before exploring further.
The Tool System provides a type-safe, extensible framework for defining agent capabilities. It standardizes how agents interact with external systems through a structured Action-Observation pattern with automatic validation and schema generation.
Source: openhands-sdk/openhands/sdk/tool/
Core Responsibilities
The Tool System has four primary responsibilities:
- Type Safety - Enforce action/observation schemas via Pydantic models
- Schema Generation - Auto-generate LLM-compatible tool descriptions from Pydantic schemas
- Execution Lifecycle - Validate inputs, execute logic, wrap outputs
- Tool Registry - Discover and resolve tools by name or pattern
Architecture Overview
Key Components
| Component | Purpose | Design |
|---|
ToolBase | Abstract base class | Generic over Action and Observation types, defines abstract create() |
ToolDefinition | Concrete tool class | Can be instantiated directly or subclassed for factory pattern |
Action | Input model | Pydantic model with visualize property |
Observation | Output model | Pydantic model with to_llm_content property |
ToolExecutor | Execution interface | ABC with __call__() method, optional close() |
ToolAnnotations | Behavioral hints | MCP-spec hints (readOnly, destructive, idempotent, openWorld) |
Tool (spec) | Tool specification | Configuration object with name and params |
ToolRegistry | Tool discovery | Resolves Tool specs to ToolDefinition instances |
Action-Observation Pattern
The tool system follows a strict input-output contract: Action → Observation. The Agent layer wraps these in events for conversation management.
Tool System Boundary:
- Input:
dict[str, Any] (JSON arguments) → validated Action instance
- Output:
Observation instance with structured result
- No knowledge of: Events, LLM messages, conversation state
Tools are defined using two patterns depending on complexity:
For stateless tools that don’t need runtime configuration (e.g., finish, think):
Components:
- Action - Pydantic model with
visualize property for display
- Observation - Pydantic model with
to_llm_content property for LLM
- ToolExecutor - Stateless executor with
__call__(action) → observation
- ToolDefinition - Direct instantiation with executor instance
For tools requiring runtime configuration or persistent state (e.g., execute_bash, file_editor, glob):
Components:
- Action/Observation - Same as Pattern 1
- ToolExecutor - Stateful executor with
__init__() for configuration and optional close() for cleanup
- MyTool(ToolDefinition) - Subclass with
@classmethod create(conv_state, ...) factory method
- Factory Method - Returns sequence of configured tool instances
Key Design Elements:
| Component | Purpose | Requirements |
|---|
| Action | Defines LLM-provided parameters | Extends Action, includes visualize property returning Rich Text |
| Observation | Defines structured output | Extends Observation, includes to_llm_content property returning content list |
| ToolExecutor | Implements business logic | Extends ToolExecutor[ActionT, ObservationT], implements __call__() method |
| ToolDefinition | Ties everything together | Either instantiate directly (Pattern 1) or subclass with create() method (Pattern 2) |
When to Use Each Pattern:
| Pattern | Use Case | Examples |
|---|
| Direct Instantiation | Stateless tools with no configuration needs | finish, think, simple utilities |
| Subclass with Factory | Tools requiring runtime state or configuration | execute_bash, file_editor, glob, grep |
Tools include optional ToolAnnotations based on the Model Context Protocol (MCP) spec that provide behavioral hints to LLMs:
| Field | Meaning | Examples |
|---|
readOnlyHint | Tool doesn’t modify state | glob (True), execute_bash (False) |
destructiveHint | May delete/overwrite data | file_editor (True), task_tracker (False) |
idempotentHint | Repeated calls are safe | glob (True), execute_bash (False) |
openWorldHint | Interacts beyond closed domain | execute_bash (True), task_tracker (False) |
Key Behaviors:
The registry enables dynamic tool discovery and instantiation from tool specifications:
Resolution Workflow:
- Tool (Spec) - Configuration object with
name (e.g., “BashTool”) and params (e.g., {"working_dir": "/workspace"})
- Resolver Lookup - Registry finds the registered resolver for the tool name
- Factory Invocation - Resolver calls the tool’s
.create() method with params and conversation state
- Instance Creation - Tool instance(s) are created with configured executors
- Agent Usage - Instances are added to the agent’s tools_map for execution
Registration Types:
| Type | Registration | Resolver Behavior |
|---|
| Tool Instance | register_tool(name, instance) | Returns the fixed instance (params not allowed) |
| Tool Subclass | register_tool(name, ToolClass) | Calls ToolClass.create(**params, conv_state=state) |
| Factory Function | register_tool(name, factory) | Calls factory(**params, conv_state=state) |
File Organization
Tools follow a consistent file structure for maintainability:
openhands-tools/openhands/tools/my_tool/
├── __init__.py # Export MyTool
├── definition.py # Action, Observation, MyTool(ToolDefinition)
├── impl.py # MyExecutor(ToolExecutor)
└── [other modules] # Tool-specific utilities
File Responsibilities:
| File | Contains | Purpose |
|---|
definition.py | Action, Observation, ToolDefinition subclass | Public API, schema definitions, factory method |
impl.py | ToolExecutor implementation | Business logic, state management, execution |
__init__.py | Tool exports | Package interface |
Benefits:
- Separation of Concerns - Public API separate from implementation
- Avoid Circular Imports - Import
impl only inside create() method
- Consistency - All tools follow same structure for discoverability
Example Reference: See terminal/ for complete implementation
MCP Integration
The tool system supports external tools via the Model Context Protocol (MCP). MCP tools are configured separately from the tool registry via the mcp_config field in Agent class and are automatically discovered from MCP servers during agent initialization.
Source: openhands-sdk/openhands/sdk/mcp/
Architecture Overview
Key Components
| Component | Purpose | Design |
|---|
MCPClient | MCP server connection | Extends FastMCP with sync/async bridge |
MCPToolDefinition | Tool wrapper | Wraps MCP tools as SDK ToolDefinition with dynamic validation |
MCPToolExecutor | Execution handler | Bridges agent actions to MCP tool calls via MCPClient |
MCPToolAction | Generic action wrapper | Simple dict[str, Any] wrapper for MCP tool arguments |
MCPToolObservation | Result wrapper | Wraps MCP tool results as observations with content blocks |
_create_mcp_action_type() | Dynamic schema | Runtime Pydantic model generated from MCP inputSchema for validation |
Sync/Async Bridge
MCP protocol is asynchronous, but SDK tools execute synchronously. The bridge pattern in client.py solves this:
Bridge Features:
- Background Event Loop - Executes async code from sync contexts
- Timeout Support - Configurable timeouts for MCP operations
- Error Handling - Wraps MCP errors in observations
- Connection Pooling - Reuses connections across tool calls
Source: create_mcp_tools() | agent._initialize()
Discovery Steps:
- Spawn Server - Launch MCP server via stdio protocol (using
MCPClient)
- List Tools - Call MCP
tools/list endpoint to retrieve available tools
- Parse Schemas - Extract tool names, descriptions, and
inputSchema from MCP response
- Create Definitions - For each tool, call
MCPToolDefinition.create() which:
- Creates an
MCPToolExecutor instance bound to the tool name and client
- Wraps the MCP tool metadata in
MCPToolDefinition
- Uses generic
MCPToolAction as the action type (NOT dynamic models yet)
- Add to Agent - All
MCPToolDefinition instances are added to agent’s tools_map during initialize() (bypasses ToolRegistry)
- Lazy Validation - Dynamic Pydantic models are generated lazily when:
action_from_arguments() is called (argument validation)
to_openai_tool() is called (schema export to LLM)
Schema Handling:
| MCP Schema | SDK Integration | When Used |
|---|
name | Tool name (stored in MCPToolDefinition) | Discovery, execution |
description | Tool description for LLM | Discovery, LLM prompt |
inputSchema | Stored in mcp_tool.inputSchema | Lazy model generation |
inputSchema fields | Converted to Pydantic fields via Schema.from_mcp_schema() | Validation, schema export |
annotations | Mapped to ToolAnnotations | Security analysis, LLM hints |
MCP Server Configuration
MCP servers are configured via the mcp_config field on the Agent class. Configuration follows FastMCP config format:
from openhands.sdk import Agent
agent = Agent(
mcp_config={
"mcpServers": {
"fetch": {
"command": "uvx",
"args": ["mcp-server-fetch"]
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/path"]
}
}
}
)
Component Relationships
Relationship Characteristics:
- Native → Registry → tools_map: Native tools resolved via
ToolRegistry
- MCP → tools_map: MCP tools bypass registry, added directly during
initialize()
- tools_map → LLM: Generate schemas describing all available capabilities
- Agent → tools_map: Execute actions, receive observations
- tools_map → Conversation: Read state for context-aware execution
- tools_map → Security: Tool annotations inform risk assessment
See Also