Skip to main content
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:
  1. Type Safety - Enforce action/observation schemas via Pydantic models
  2. Schema Generation - Auto-generate LLM-compatible tool descriptions from Pydantic schemas
  3. Execution Lifecycle - Validate inputs, execute logic, wrap outputs
  4. Tool Registry - Discover and resolve tools by name or pattern

Tool System

Architecture Overview

Key Components

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

Tool Definition

Tools are defined using two patterns depending on complexity:

Pattern 1: Direct Instantiation (Simple Tools)

For stateless tools that don’t need runtime configuration (e.g., finish, think): Components:
  1. Action - Pydantic model with visualize property for display
  2. Observation - Pydantic model with to_llm_content property for LLM
  3. ToolExecutor - Stateless executor with __call__(action) → observation
  4. ToolDefinition - Direct instantiation with executor instance

Pattern 2: Subclass with Factory (Stateful Tools)

For tools requiring runtime configuration or persistent state (e.g., execute_bash, file_editor, glob): Components:
  1. Action/Observation - Same as Pattern 1
  2. ToolExecutor - Stateful executor with __init__() for configuration and optional close() for cleanup
  3. MyTool(ToolDefinition) - Subclass with @classmethod create(conv_state, ...) factory method
  4. Factory Method - Returns sequence of configured tool instances
Key Design Elements: When to Use Each Pattern:

Tool Annotations

Tools include optional ToolAnnotations based on the Model Context Protocol (MCP) spec that provide behavioral hints to LLMs: Key Behaviors:

Tool Registry

The registry enables dynamic tool discovery and instantiation from tool specifications: Resolution Workflow:
  1. Tool (Spec) - Configuration object with name (e.g., “BashTool”) and params (e.g., {"working_dir": "/workspace"})
  2. Resolver Lookup - Registry finds the registered resolver for the tool name
  3. Factory Invocation - Resolver calls the tool’s .create() method with params and conversation state
  4. Instance Creation - Tool instance(s) are created with configured executors
  5. Agent Usage - Instances are added to the agent’s tools_map for execution
Registration Types:

File Organization

Tools follow a consistent file structure for maintainability:
File Responsibilities: 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

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

Tool Discovery Flow

Source: create_mcp_tools() | agent._initialize() Discovery Steps:
  1. Spawn Server - Launch MCP server via stdio protocol (using MCPClient)
  2. List Tools - Call MCP tools/list endpoint to retrieve available tools
  3. Parse Schemas - Extract tool names, descriptions, and inputSchema from MCP response
  4. 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)
  5. Add to Agent - All MCPToolDefinition instances are added to agent’s tools_map during initialize() (bypasses ToolRegistry)
  6. 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 Server Configuration

MCP servers are configured via the mcp_config field on the Agent class. Configuration follows FastMCP config format:

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