Skip to content

Tool Providers

Tool Providers manage resources and return tools via async context manager. Use them when your tools need lifecycle management.

When to Use ToolProviders

Use a ToolProvider when your tool requires:

  • Connections - HTTP clients, database connections, websockets
  • Temporary resources - Temp directories, sandboxes, processes
  • Cleanup logic - Releasing resources, closing connections
  • Shared state - State shared across multiple tool calls

ToolProvider Protocol

All providers must implement the ToolProvider protocol:

Member Description
has_lifecycle = True Required marker to identify as a provider
__aenter__() Setup resources and return Tool or list[Tool]
__aexit__() Cleanup resources (close connections, delete temp files)

Basic Example

An HTTP client provider that shares a connection pool:

import httpx
from pydantic import BaseModel, Field
from stirrup import Tool, ToolResult, ToolUseCountMetadata


class FetchParams(BaseModel):
    url: str = Field(description="URL to fetch")


class HTTPToolProvider:
    """Provides an HTTP fetch tool with shared client."""

    has_lifecycle = True  # Required marker

    def __init__(self, timeout: float = 30.0):
        self.timeout = timeout
        self._client: httpx.AsyncClient | None = None

    async def __aenter__(self) -> Tool:
        # Setup: create HTTP client
        self._client = httpx.AsyncClient(timeout=self.timeout)

        async def fetch(params: FetchParams) -> ToolResult[ToolUseCountMetadata]:
            response = await self._client.get(params.url)
            return ToolResult(
                content=response.text[:5000],  # Truncate for context
                metadata=ToolUseCountMetadata(),
            )

        return Tool(
            name="http_fetch",
            description="Fetch content from a URL",
            parameters=FetchParams,
            executor=fetch,
        )

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        # Cleanup: close HTTP client
        if self._client:
            await self._client.aclose()
            self._client = None

Returning Multiple Tools

A provider can return multiple related tools:

class DatabaseToolProvider:
    """Provides query and insert tools for a database."""

    has_lifecycle = True

    def __init__(self, connection_string: str):
        self.connection_string = connection_string
        self._conn = None

    async def __aenter__(self) -> list[Tool]:
        self._conn = await connect(self.connection_string)

        return [
            self._create_query_tool(),
            self._create_insert_tool(),
        ]

    def _create_query_tool(self) -> Tool:
        async def query(params: QueryParams) -> ToolResult[ToolUseCountMetadata]:
            results = await self._conn.execute(params.sql)
            return ToolResult(content=str(results), metadata=ToolUseCountMetadata())

        return Tool(
            name="db_query",
            description="Execute a SQL query",
            parameters=QueryParams,
            executor=query,
        )

    def _create_insert_tool(self) -> Tool:
        # Similar implementation...
        pass

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        if self._conn:
            await self._conn.close()

Using with Agents

The agent's session() automatically manages ToolProvider lifecycle:

from stirrup import Agent
from stirrup.clients.chat_completions_client import ChatCompletionsClient

# Provider is set up when session starts, cleaned up when it ends
client = ChatCompletionsClient(model="gpt-5")
agent = Agent(
    client=client,
    name="my_agent",
    tools=[
        HTTPToolProvider(timeout=60),
        DatabaseToolProvider("postgresql://..."),
    ],
)

async with agent.session() as session:
    # Tools are available here
    await session.run("Fetch data from the API and store in database")
# Providers are cleaned up automatically

Built-in ToolProviders

Stirrup includes several ToolProviders:

Provider Tools Provided Description
LocalCodeExecToolProvider code_exec Local temp directory execution
DockerCodeExecToolProvider code_exec Docker container execution
E2BCodeExecToolProvider code_exec E2B cloud sandbox
WebToolProvider web_fetch, web_search Web tools with shared client
ViewImageToolProvider view_image View images from exec env
MCPToolProvider varies MCP server tools

Mixing Tools and Providers

You can mix regular tools and providers:

from stirrup import Agent
from stirrup.clients.chat_completions_client import ChatCompletionsClient
from stirrup.tools import CALCULATOR_TOOL, WebToolProvider

client = ChatCompletionsClient(model="gpt-5")
agent = Agent(
    client=client,
    name="mixed_agent",
    tools=[
        CALCULATOR_TOOL,           # Regular tool (no lifecycle)
        HTTPToolProvider(),        # Provider (has lifecycle)
        WebToolProvider(),         # Built-in provider
    ],
)

Error Handling

Handle setup/cleanup errors gracefully:

class RobustProvider:
    has_lifecycle = True

    async def __aenter__(self) -> Tool:
        try:
            self._resource = await acquire_resource()
        except ConnectionError as e:
            # Log and re-raise with context
            raise RuntimeError(f"Failed to connect: {e}") from e

        return self._create_tool()

    async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
        if self._resource:
            try:
                await self._resource.close()
            except Exception:
                # Log but don't raise during cleanup
                pass

Next Steps