Authoring Cogtrix Tools
This guide explains how to add new tools to Cogtrix — either by dropping a .py
file into a directory (file-drop) or by packaging a tool as an installable Python
package (entry-point).
Quick overview
Cogtrix discovers tools from three sources, in this order:
| Track | How | Use when |
|---|---|---|
| Built-in | src/tools/*.py in the package | Contributing to the project itself |
| File-drop | *.py files in a configured directory | Local one-off tools, rapid prototyping |
| Entry-point | Installed package declares cogtrix.tools EP | Distributable, versioned tool packages |
1. Tool module format
Every tool module — built-in, file-drop, or entry-point — uses the same format.
Minimal example
# my_tools.py
from pydantic import BaseModel, Field
class SayHelloInput(BaseModel):
name: str = Field(..., description="Name to greet.")
def say_hello(name: str) -> str:
"""Return a greeting."""
return f"Hello, {name}!"
TOOL_CONFIGS = [
{
"name": "say_hello",
"description": "Greet a person by name.",
"input_schema": SayHelloInput,
"function": say_hello,
"requires_confirmation": False,
}
]
TOOL_CONFIG vs TOOL_CONFIGS
| Variable | Use |
|---|---|
TOOL_CONFIGS | List of dicts — preferred when a module provides multiple tools |
TOOL_CONFIG | Single dict — shorthand for single-tool modules |
Each config dict must contain:
| Key | Type | Required | Description |
|---|---|---|---|
name | str | ✅ | Unique tool name (lowercase, underscores) |
description | str | ✅ | One-sentence description shown in /tools |
input_schema | BaseModel subclass | ✅ | Pydantic schema for the tool’s arguments |
function | callable | ✅ | The Python function that implements the tool |
requires_confirmation | bool | ❌ | If True, user must approve before execution (default False) |
Optional: TOOL_SETUP
If your module needs access to the Cogtrix Config object at load time (e.g. to
read API keys from the config file), define a TOOL_SETUP function:
def TOOL_SETUP(config) -> None:
"""Called automatically by ToolRegistry after this module is loaded."""
global _api_key
_api_key = config.services.get("my_service", {}).get("api_key", "")
TOOL_SETUP is called once per process, right after the module is imported and
before tools are registered.
Optional: is_configured
If your tool requires external credentials or dependencies that may not be
available, define is_configured() to gate registration:
def is_configured() -> bool:
"""Return True only if the tool is ready to use."""
return bool(_api_key)
When is_configured() returns False, the module is silently skipped and its
tools are not added to the catalog.
2. File-drop tools
The simplest way to add a custom tool is to drop a .py file into a directory
and tell Cogtrix to scan it.
Configure the scan directory
In your Cogtrix config file (.cogtrix.yml):
tool_dirs:
- ~/.cogtrix/tools # user-specific tools
- /opt/company/cogtrix/tools # team-shared tools
Or use the environment variable (colon-separated):
export COGTRIX_TOOL_DIRS="$HOME/.cogtrix/tools:/opt/company/cogtrix/tools"
Rules for file-drop modules
- File name must end in
.py - Files whose names start with
_are skipped (use this for helpers) - Symlinks that resolve outside the declared directory are rejected
- The module is imported with a synthetic name
cogtrix_plugin_<stem>so it never conflicts with built-in tool modules
Example directory layout
~/.cogtrix/tools/
my_calendar.py ← loaded
jira_tools.py ← loaded
_helpers.py ← skipped (leading underscore)
3. Entry-point (installable) tools
For tools you want to distribute as a Python package:
Step 1 — Write your plugin class
# my_cogtrix_plugin/tools.py
from pydantic import BaseModel, Field
from cogtrix.plugins import hookimpl # or: from src.plugins import hookimpl
class EchoInput(BaseModel):
text: str = Field(..., description="Text to echo back.")
def echo(text: str) -> str:
"""Echo text back to the caller."""
return text
class MyPlugin:
@hookimpl
def cogtrix_tools(self) -> list[dict]:
return [
{
"name": "echo",
"description": "Echo text back unchanged.",
"input_schema": EchoInput,
"function": echo,
}
]
Note:
hookimplis the pluggy hook implementation marker. It is optional but recommended because it future-proofs your plugin against hook signature changes.
Step 2 — Declare the entry-point
In pyproject.toml:
[project.entry-points."cogtrix.tools"]
my_plugin = "my_cogtrix_plugin.tools:MyPlugin"
In setup.cfg:
[options.entry_points]
cogtrix.tools =
my_plugin = my_cogtrix_plugin.tools:MyPlugin
Step 3 — Install and run
pip install . # or: uv add .
cogtrix # MyPlugin's tools appear in /tools
Alternative: point directly to a module
If your module already has TOOL_CONFIGS at the top level, the entry-point can
point directly to the module (no class needed):
[project.entry-points."cogtrix.tools"]
my_plugin = "my_cogtrix_plugin.tools"
Cogtrix detects that the loaded object is a module and reads TOOL_CONFIGS or
TOOL_CONFIG from it directly.
4. Testing your tool
Unit test
# tests/test_my_tool.py
from my_cogtrix_plugin.tools import echo
def test_echo():
assert echo("hello") == "hello"
Integration test (file-drop)
import tempfile, textwrap, pathlib
from src.plugins.loader import ToolPluginLoader
def test_file_drop():
src = textwrap.dedent('''
from pydantic import BaseModel, Field
class PingInput(BaseModel):
msg: str = Field(..., description="Message.")
def ping(msg: str) -> str:
return msg
TOOL_CONFIGS = [{
"name": "ping",
"description": "Ping.",
"input_schema": PingInput,
"function": ping,
}]
''')
with tempfile.TemporaryDirectory() as tmp:
pathlib.Path(tmp, "ping.py").write_text(src)
loader = ToolPluginLoader()
modules = loader.load_all([tmp])
assert len(modules) == 1
assert modules[0].TOOL_CONFIGS[0]["name"] == "ping"
5. Reference: discovery order
- Built-in tools in
src/tools/are always loaded first. - File-drop directories are scanned in the order they appear in
tool_dirs. - Entry-point plugins are loaded last (order within the group is implementation-defined).
If two tools share the same name, the last one registered wins with a WARNING
logged. Use unique, namespaced names to avoid collisions (e.g. acme_search
rather than search).
6. Reference: full TOOL_CONFIG schema
{
# Required
"name": str, # "my_tool"
"description": str, # "Does X given Y."
"input_schema": type, # class MyToolInput(BaseModel): ...
"function": callable, # def my_tool(...) -> str: ...
# Optional
"requires_confirmation": bool, # default False
}
The function must be synchronous (no async def). For async operations, run
them with asyncio.run() or loop.run_until_complete() inside the function body.