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:
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()