Tools & actions
Tools are what the agent can do. The LLM decides which tool to call and with what arguments — your job is to define the tools and register them with the engine.
The fast path — @gantry_tool
Turn any Python function into an LLM-callable tool in one line.
from gantrygraph import gantry_tool
@gantry_tool
async def search_db(query: str) -> str:
"""Search the product database. Returns matching rows as JSON."""
return db.execute(query)
Three rules:
- Type-annotate every parameter — annotations define the JSON schema the LLM uses to construct calls
- Write a clear docstring — the LLM reads it to decide when to use the tool
- Return a string — the LLM reads the return value as the tool result
Sync functions work too — @gantry_tool handles both:
@gantry_tool
def get_timestamp() -> str:
"""Return the current UTC timestamp."""
import datetime
return datetime.datetime.utcnow().isoformat()
Mark irreversible tools as destructive
Add destructive=True for any action that can't be undone — deleting records,
sending messages, charging a card. The engine automatically requires human
approval before the tool runs.
@gantry_tool(destructive=True)
def drop_table(table: str) -> str:
"""Drop a database table permanently. Cannot be undone."""
db.execute(f"DROP TABLE {table}")
return f"dropped {table}"
# Approval is automatic — no GuardrailPolicy config needed
agent = GantryEngine(llm=..., tools=[drop_table], approval_callback=ask_human)
If approval_callback is not provided, the tool call returns an error and
the agent is told approval is required. See Human approval.
Registering tools
Pass any mix of tool types to GantryEngine:
agent = GantryEngine(
llm=...,
tools=[
search_db, # @gantry_tool decorated function
FileSystemTools(...), # built-in action group
MCPClient("npx ..."), # external MCP server
my_langchain_tool, # any LangChain StructuredTool
],
)
Built-in tool groups
Read and write files — FileSystemTools
Gives the agent file_read, file_write, file_list, and file_delete.
All paths are validated against workspace — the agent can never escape the
directory, even if the LLM constructs a path like ../../etc/passwd.
from gantrygraph.actions import FileSystemTools
fs = FileSystemTools(workspace="/my/project")
See the filesystem guide for a complete working example.
Run shell commands — ShellTools
Gives the agent shell_run. Two layers of protection ship by default:
allowed_commands— allowlist of executables the agent can run. Set it to the minimum your task needs.ShellDenylist— blocks catastrophic commands (rm -rf /, fork bombs,curl | bash, SSH key reads) before the OS sees them.
from gantrygraph.actions import ShellTools
from gantrygraph.security import ShellDenylist
shell = ShellTools(
workspace="/my/project",
allowed_commands=["python", "pytest", "git", "ruff"],
timeout=30.0,
)
Three denylist profiles — choose based on how much you trust the agent:
| Profile | Blocks |
|---|---|
ShellDenylist.default() |
rm -rf /, fork bombs, curl | bash, SSH key reads. Active by default. |
ShellDenylist.strict() |
All of the above + mkfs, fdisk, chmod 777, env dumps |
ShellDenylist.permissive() |
Nothing — disable only if you fully trust the agent's inputs |
# Tighten controls for untrusted task inputs
shell = ShellTools(
workspace="/app",
allowed_commands=["python"],
denylist=ShellDenylist.strict(),
)
See the filesystem guide for a complete example.
Control mouse and keyboard — MouseKeyboardTools
Gives the agent mouse_click, mouse_move, key_press, type_text, and screenshot.
Requires pip install 'gantrygraph[desktop]'.
from gantrygraph.actions import MouseKeyboardTools
tools = MouseKeyboardTools()
Pair with DesktopScreen perception so the agent sees the current screen
before deciding where to click. See the desktop agent guide.
Control a browser — BrowserTools
Gives the agent browser_navigate, browser_click, browser_fill,
browser_get_text, and browser_get_url.
Requires pip install 'gantrygraph[browser]' && playwright install chromium.
from gantrygraph.actions import BrowserTools
tools = BrowserTools(headless=True)
Pair with WebPage perception and share the same instance to avoid
launching two browsers. See the browser agent guide.
Connect external services — MCPClient
Model Context Protocol connects the agent to GitHub, Notion, Slack, Postgres, and hundreds of other services. Tools are discovered automatically — no integration code needed.
from gantrygraph.mcp import MCPClient
# Starts the MCP server as a subprocess; tools are discovered on startup
tools = MCPClient("npx -y @modelcontextprotocol/server-github")
agent = GantryEngine(llm=..., tools=[tools])
See the MCP guide for passing environment variables and connecting to multiple servers simultaneously.
Build a reusable tool group — BaseAction
When multiple tools share state (a database connection, an authenticated client,
a Slack token), group them in a BaseAction subclass instead of using @gantry_tool.
The engine calls close() automatically when the run finishes.
@gantry_tool
def get_timestamp() -> str:
"""Return the current UTC timestamp."""
import datetime
return datetime.datetime.utcnow().isoformat()
0
Pass it like any other tool:
agent = GantryEngine(
llm=...,
tools=[SlackTools(token=os.environ["SLACK_TOKEN"])],
)
Use a secret in tool arguments
If a tool needs an API key, don't pass the real value in the task description.
Use GantrySecrets to define an alias — the agent uses the alias,
the engine substitutes the real value before execution, and the key
never appears in LLM context.
from gantrygraph import GantrySecrets
secrets = GantrySecrets(api_key="sk-real-value-here")
@gantry_tool
async def call_api(endpoint: str, api_key: str) -> str:
"""Call the API. Pass api_key=<<api_key>> for authentication."""
return await client.get(endpoint, headers={"Authorization": api_key})
agent = GantryEngine(llm=..., tools=[call_api], secrets=secrets)
# The agent passes api_key="<<api_key>>" — the engine replaces it with the real key
See Guardrails for the full secrets reference.
See also: Guardrails · Custom tools guide · API reference