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

ComponentPurposeDesign
ToolBaseAbstract base classGeneric over Action and Observation types, defines abstract create()
ToolDefinitionConcrete tool classCan be instantiated directly or subclassed for factory pattern
ActionInput modelPydantic model with visualize property
ObservationOutput modelPydantic model with to_llm_content property
ToolExecutorExecution interfaceABC with __call__() method, optional close()
ToolAnnotationsBehavioral hintsMCP-spec hints (readOnly, destructive, idempotent, openWorld)
Tool (spec)Tool specificationConfiguration object with name and params
ToolRegistryTool discoveryResolves 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

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:
ComponentPurposeRequirements
ActionDefines LLM-provided parametersExtends Action, includes visualize property returning Rich Text
ObservationDefines structured outputExtends Observation, includes to_llm_content property returning content list
ToolExecutorImplements business logicExtends ToolExecutor[ActionT, ObservationT], implements __call__() method
ToolDefinitionTies everything togetherEither instantiate directly (Pattern 1) or subclass with create() method (Pattern 2)
When to Use Each Pattern:
PatternUse CaseExamples
Direct InstantiationStateless tools with no configuration needsfinish, think, simple utilities
Subclass with FactoryTools requiring runtime state or configurationexecute_bash, file_editor, glob, grep

Tool Annotations

Tools include optional ToolAnnotations based on the Model Context Protocol (MCP) spec that provide behavioral hints to LLMs:
FieldMeaningExamples
readOnlyHintTool doesn’t modify stateglob (True), execute_bash (False)
destructiveHintMay delete/overwrite datafile_editor (True), task_tracker (False)
idempotentHintRepeated calls are safeglob (True), execute_bash (False)
openWorldHintInteracts beyond closed domainexecute_bash (True), task_tracker (False)
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:
TypeRegistrationResolver Behavior
Tool Instanceregister_tool(name, instance)Returns the fixed instance (params not allowed)
Tool Subclassregister_tool(name, ToolClass)Calls ToolClass.create(**params, conv_state=state)
Factory Functionregister_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:
FileContainsPurpose
definition.pyAction, Observation, ToolDefinition subclassPublic API, schema definitions, factory method
impl.pyToolExecutor implementationBusiness logic, state management, execution
__init__.pyTool exportsPackage 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 execute_bash/ 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

ComponentPurposeDesign
MCPClientMCP server connectionExtends FastMCP with sync/async bridge
MCPToolDefinitionTool wrapperWraps MCP tools as SDK ToolDefinition with dynamic validation
MCPToolExecutorExecution handlerBridges agent actions to MCP tool calls via MCPClient
MCPToolActionGeneric action wrapperSimple dict[str, Any] wrapper for MCP tool arguments
MCPToolObservationResult wrapperWraps MCP tool results as observations with content blocks
_create_mcp_action_type()Dynamic schemaRuntime 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

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 SchemaSDK IntegrationWhen Used
nameTool name (stored in MCPToolDefinition)Discovery, execution
descriptionTool description for LLMDiscovery, LLM prompt
inputSchemaStored in mcp_tool.inputSchemaLazy model generation
inputSchema fieldsConverted to Pydantic fields via Schema.from_mcp_schema()Validation, schema export
annotationsMapped to ToolAnnotationsSecurity 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