| name | tool-creator |
| description | Meta-skill for building AnimaWorks Python external tools: ExternalToolDispatcher, get_credential, and permissions. Use when: adding a module under core/tools, wrapping a Web API, or exposing commands via animaworks-tool. |
tool-creator
Overview
AnimaWorks tools are categorized into three types:
| Type | Location | Discovery |
|---|
| Core tools | core/tools/*.py | discover_core_tools() → TOOL_MODULES (fixed at startup) |
| Shared tools | {data_dir}/common_tools/*.py | discover_common_tools() |
| Personal tools | {anima_dir}/tools/*.py | discover_personal_tools() |
{data_dir} is typically ~/.animaworks/. Personal and shared tools are re-scanned by ExternalToolDispatcher via refresh_tools and can be hot-reloaded. ToolHandler checks the tool creation permission in permissions.json when writing to tools/*.py with write_memory_file.
Personal and shared tools are invoked via Bash with animaworks-tool <tool> <subcommand> [args]. Schema name format is {tool_name}_{action} (e.g., my_tool + query → my_tool_query).
Procedure
Step 1: Design the Tool
- Decide the tool name (snake_case, e.g.,
my_api_tool)
- Define the schema(s) (operations) to provide
- Define required parameters
Step 2: Create the Module File
Create a Python file following the template below.
Single-Schema Tool (Simple)
For file my_tool.py, animaworks-tool my_tool action [args] invokes it; schema name my_tool_action is passed to dispatch.
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger("animaworks.tools")
def get_tool_schemas() -> list[dict]:
"""Return tool schema(s) (required)."""
return [
{
"name": "my_tool_action",
"description": "Description of what this tool does",
"input_schema": {
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "Parameter description",
},
"param2": {
"type": "integer",
"description": "Optional parameter",
"default": 10,
},
},
"required": ["param1"],
},
}
]
def dispatch(name: str, args: dict[str, Any]) -> Any:
"""Execute handling by schema name (recommended)."""
args.pop("anima_dir", None)
if name == "my_tool_action":
return _do_action(
param1=args["param1"],
param2=args.get("param2", 10),
)
raise ValueError(f"Unknown tool: {name}")
def _do_action(param1: str, param2: int = 10) -> dict[str, Any]:
"""Actual logic implementation."""
return {"result": f"Processed {param1} with {param2}"}
Multi-Schema Tool (API Integration, etc.)
Schema names use {tool_name}_{action} format. animaworks-tool myapi query [args] passes myapi_query to dispatch. File name: myapi.py.
from __future__ import annotations
import logging
from typing import Any
logger = logging.getLogger("animaworks.tools")
def get_tool_schemas() -> list[dict]:
return [
{
"name": "myapi_query",
"description": "Send query to API and get results",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"limit": {"type": "integer", "description": "Max results", "default": 10},
},
"required": ["query"],
},
},
{
"name": "myapi_post",
"description": "Send data to API",
"input_schema": {
"type": "object",
"properties": {
"data": {"type": "string", "description": "Data to send"},
},
"required": ["data"],
},
},
]
class MyAPIClient:
"""API client."""
def __init__(self) -> None:
from core.tools._base import get_credential
self._api_key = get_credential(
"myapi", "myapi_tool", env_var="MYAPI_KEY",
)
def query(self, query: str, limit: int = 10) -> list[dict]:
import httpx
resp = httpx.get(
"https://api.example.com/search",
params={"q": query, "limit": limit},
headers={"Authorization": f"Bearer {self._api_key}"},
timeout=30,
)
resp.raise_for_status()
return resp.json()["results"]
def post(self, data: str) -> dict:
import httpx
resp = httpx.post(
"https://api.example.com/data",
json={"data": data},
headers={"Authorization": f"Bearer {self._api_key}"},
timeout=30,
)
resp.raise_for_status()
return resp.json()
def dispatch(name: str, args: dict[str, Any]) -> Any:
args.pop("anima_dir", None)
client = MyAPIClient()
if name == "myapi_query":
return client.query(
query=args["query"],
limit=args.get("limit", 10),
)
elif name == "myapi_post":
return client.post(data=args["data"])
raise ValueError(f"Unknown tool: {name}")
Step 3: Save the File
Save as a personal tool (path in write_memory_file is relative to anima_dir):
write_memory_file(path="tools/my_tool.py", content=<code>)
Writing to tools/ requires personal tool permission in the "Tool creation" section of permissions.json.
Step 4: Enable the Tool
After saving, call refresh_tools for hot reload:
refresh_tools()
The tool becomes available immediately without restarting the session. Personal tools do not need to be listed in permissions.json external_tools; once discovered by refresh_tools, they are callable via Bash with animaworks-tool <tool> <subcommand>.
Step 5: Share (Optional)
To let other Anima use it, share the tool:
share_tool(tool_name="my_tool")
This copies it to ~/.animaworks/common_tools/ and makes it available to all Anima. Sharing requires shared tool permission in permissions.json.
Required Interface
| Function | Required | Description |
|---|
get_tool_schemas() | ✅ Required | Return list of tool schemas. Must include name, description, input_schema (or parameters) |
dispatch(name, args) | 🔵 Recommended | Dispatch by schema name. ExternalToolDispatcher prefers this. Remove anima_dir with args.pop("anima_dir", None) when passing args to other functions |
| Function with same name as schema | 🟡 Alternative | Can be used instead of dispatch() |
cli_main(argv) | ⚪ Optional | For standalone execution via animaworks-tool <tool_name> |
EXECUTION_PROFILE | ⚪ Optional | For long-running tools. Enables background submission via animaworks-tool submit |
Bash Invocation
Anima invokes personal/shared tools via Bash with animaworks-tool <tool> <subcommand>:
animaworks-tool myapi query "search term" [--limit 10]
schema_name = tool_name + "_" + action is passed to dispatch(name, args). In the example above, name="myapi_query".
Schema Definition Conventions
Both input_schema and parameters are supported and normalized (core/tooling/schemas._normalise_schema).
{
"name": "tool_action_name",
"description": "1-2 sentence description",
"input_schema": {
"type": "object",
"properties": { ... },
"required": [ ... ],
},
}
Credential Retrieval (get_credential)
Obtain API keys etc. via get_credential(). Never hardcode.
from core.tools._base import get_credential
api_key = get_credential(
credential_name="myapi",
tool_name="myapi_tool",
key_name="api_key",
env_var="MYAPI_KEY",
)
Resolution order: config.json → vault.json (encrypted vault) → shared/credentials.json → environment variable. ToolConfigError if none found.
Tool Creation Permission in permissions.json
Add the following to permissions.json for tool creation and sharing:
## Tool creation
- Personal tools: yes
- Shared tools: yes
OK, enabled, or true are also valid instead of yes.
Validation Checklist
Security Guidelines
-
Credentials: Obtain via get_credential(). Never hardcode
-
Access control: Do not access other Anima's directories
-
Timeout: Always set timeout for external APIs (recommended: 30 seconds)
-
Logging: Use logging.getLogger("animaworks.tools")
-
Dependencies: Import external libraries inside functions (lazy import)
Mode S (MCP) exposure for core tools
When adding a module under core/tools/, only names listed in core/mcp/server.py _EXPOSED_TOOL_NAMES are exposed to Claude Code via MCP (curated subset). As of 2026-03, that set includes: search_memory, read_memory_file, write_memory_file, archive_memory_file, send_message, post_channel, call_human, delegate_task, submit_tasks, update_task, create_skill, completion_gate (pre-completion self-check). External-service core tools such as Slack, Gmail, and web_search are not on the MCP list; they are reached via use_tool, Bash (animaworks-tool), or skills.
Notes
- Tools are Python code, different from Skills (Markdown procedure documents)
- Tool creation requires personal tools: yes in the "Tool creation" section of permissions.json
- Sharing tools requires shared tools: yes permission
- Created tools are discovered immediately on
refresh_tools call (hot reload)
- Personal tools do not need to be in permissions.json external_tools; once discovered, they are available via Bash with
animaworks-tool <tool> <subcommand>
- Schema names use
{tool_name}_{action} format; keep them unique across tools
- Personal or shared tools with the same name as core tools are shadowed and skipped (
core/tools/__init__.py)