Skip to content

ToolKit & tools

caw gives you two ways to hand the agent Python tools without writing or running an MCP server yourself: stateless functions and the declarative ToolKit class. Both are MCP HTTP servers under the hood, started and stopped automatically with the session.

Stateless tools

Decorate plain functions with @tool and pass them via stateless_tools=:

from caw import Agent, tool

@tool(description="Add two numbers")
def add(a: int, b: int) -> int:
    return a + b

@tool(description="Multiply two numbers")
def multiply(a: int, b: int) -> int:
    return a * b

agent = Agent(
    system_prompt="You have access to math tools. Use them to answer questions.",
    stateless_tools=[add, multiply],
)

Full example — examples/tools_simple.py:

"""Stateless tools demo: pass plain functions directly to an agent."""

import os

os.environ["CAW_LOG"] = "full"

from caw import Agent, tool


@tool(description="Add two numbers")
def add(a: int, b: int) -> int:
    return a + b


@tool(description="Multiply two numbers")
def multiply(a: int, b: int) -> int:
    return a * b


def main():
    agent = Agent(
        system_prompt="You have access to math tools. Use them to answer questions.",
        stateless_tools=[add, multiply],
        data_dir="caw_data",
    )

    with agent.start_session() as session:
        session.send("List every tool you have access to by name.")
        session.send("What is 3 + 4? Then multiply the result by 5.")

        traj = session.trajectory
        print(f"\nTurns: {traj.num_turns}, Tool calls: {traj.total_tool_calls}")


if __name__ == "__main__":
    main()

ToolKit: stateful, declarative tool servers

Subclass ToolKit, decorate methods with @tool, and caw exposes them as a single MCP server. Instance state (self) persists across tool calls for the whole session:

from caw import Agent, ToolKit, tool

class UserDB(ToolKit, server_name="user_db", display_name="User Database"):
    def __init__(self):
        self.users = ["Alice", "Bob"]

    @tool(description="List all users")
    async def list_users(self) -> str:
        return ", ".join(self.users)

    @tool(description="Add a user")
    async def add_user(self, name: str) -> str:
        self.users.append(name)
        return f"Added {name}"

db = UserDB()
agent = Agent(system_prompt="You have a user database.", tool_servers=[db])
traj = agent.completion("Add Eve to the user database, then list all users")

You can pass the ToolKit instance directly in tool_servers= (caw calls as_server() for you), or call agent.add_tool_server(db) later. Methods may be sync or async.

Full example — examples/toolkit.py:

"""Custom tool server demo: a stateful user database exposed via ToolKit."""

import os

os.environ["CAW_LOG"] = "full"

from caw import Agent, ToolKit, tool


class UserDB(ToolKit, server_name="user_db", display_name="User Database"):
    def __init__(self):
        self.users = ["Alice", "Bob", "Charlie"]
        self.count = 0

    @tool(description="List all users in the database")
    async def list_users(self) -> str:
        self.count += 1
        return f"Users: {', '.join(self.users)} (queried {self.count} time(s))"

    @tool(description="Add a user to the database")
    async def add_user(self, name: str) -> str:
        self.users.append(name)
        return f"Added {name}. Total users: {len(self.users)}"


def main():
    db = UserDB()
    agent = Agent(
        system_prompt="You have access to a user database. Use the tools to answer questions about users.",
        tool_servers=[db],
        data_dir="caw_data",
    )

    with agent.start_session() as session:
        session.send("How many users are in the database? List them.")
        session.send("Add a user named Diana, then list all users again.")

        traj = session.trajectory
        print(f"\nTurns: {traj.num_turns}, Tool calls: {traj.total_tool_calls}")

    # State persists across turns (server stayed alive for the whole session)
    print(f"Final DB state: users={db.users}, count={db.count}")


if __name__ == "__main__":
    main()

Thread safety

By default a ToolKit's methods may run concurrently. If your state isn't safe for that, declare thread_safe=True in the subclass options and caw serializes calls with a lock:

class Counter(ToolKit, server_name="counter", thread_safe=True):
    ...

Tool permission groups

Independently of which tools you add, you can restrict the agent's built-in tools (read, write, exec, web, …) with ToolGroup:

from caw import Agent, ToolGroup

# Read-only: Read/Glob/Grep, but no Bash/Write/Edit/WebSearch.
agent = Agent(tools=ToolGroup.READER)

# Everything except writes:
agent = Agent(tools=ToolGroup.ALL - ToolGroup.WRITER)

Groups combine with | (union) and - (subtract). The default for automated runs is ToolGroup.ALL - ToolGroup.INTERACTION. Full example — examples/tool_groups.py:

"""Tool groups demo: restrict an agent to read-only tools."""

import os

os.environ["CAW_LOG"] = "full"

from caw import Agent, ToolGroup


def main():
    # Only allow Read, Glob, Grep — no Bash, Write, Edit, WebSearch, etc.
    agent = Agent(tools=ToolGroup.READER, data_dir="caw_data")

    traj = agent.completion(
        "List every tool you have access to by name. "
        "Then answer: can you use the Bash tool? Can you use the Write tool? "
        "Can you use the Edit tool? Can you use the WebSearch tool?"
    )
    print(traj.result)
    print(f"\nis_complete: {traj.is_complete}")
    print(f"is_usage_limited: {traj.is_usage_limited}")


if __name__ == "__main__":
    main()