Skip to content
Back to skills

Tool Creator

ASecurity

A meta-skill for creating Python external tool modules for AnimaWorks.core/integrationsHandles integrations, get_credential, and permissions. Use when: core/integrationsWhen you need to add a new module, implement a Web API wrapper, or develop a custom tool called by animaworks-tool.

  • 264 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added May 29, 2026
ai-agentspythongobashgitapisecurity

Works with

  • claude code
  • cli
  • api
  • mcp

Security analysis

A100/100

Scanned October 2, 2026

npx -y skills add xuiltul/animaworks --skill tool-creator --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Tool Creator?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Tool Creator
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/xuiltul-tool-creator/badge)](https://www.skillsdirectory.com/skills/xuiltul-tool-creator)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: tool-creator
description: >-
  A meta-skill for creating Python external tool modules for AnimaWorks.core/integrationsHandles integrations, get_credential, and permissions.
  Use when: core/integrationsWhen you need to add a new module, implement a Web API wrapper, or develop a custom tool called by animaworks-tool.
---


# tool-creator

## Overview

AnimaWorks tools are divided into three types:

| Type | Location | Discovery method |
|------|--------|----------|
| **Core tools** | `core/integrations/*.py` (files with the `_` prefix are excluded) | `discover_core_tools()` → `TOOL_MODULES` (package import) |
| **Shared tools** | `{data_dir}/common_tools/*.py` | `discover_common_tools()` |
| **Personal tools** | `{anima_dir}/tools/*.py` | `discover_personal_tools()` |

`{data_dir}` is usually `~/.animaworks/`.

- **Dispatch**: `ExternalToolDispatcher` has deprecated `_DISPATCH_TABLE` and standardized on `dispatch(name, args)` in each module (or a function with the same name as the schema) (`core/tooling/dispatch.py`).
- **Merging**: At startup, both `_discover_personal_tools()` and `refresh_tools` for `AgentCore` merge in the order **shared → personal**, with **personal tools overriding entries with the same name** (`{**common, **personal}`). The merged result is held in `_personal_tools` of `ExternalToolDispatcher` (the name is historical, but it **also includes shared tools**).
- **Core conflicts**: Files with the same name as core `TOOL_MODULES` are **skipped** when discovering shared and personal tools (warning log only).
- **Writing tool files**: When writing to `tools/*.py` via `write_memory_file`, the **tool_creation.personal** requirement of `permissions` (**`permissions.json` takes precedence**) must be met (`core/tooling/handler_memory.py`).

## Execution Path (How the LLM Calls It)

| Mode | Typical Path |
|--------|-----------|
| **A (LiteLLM, etc.)** | Integrated tool **`use_tool(tool_name, action, args)`** → module's `dispatch` (`core/tooling/handler.py`). Details are designed so that each tool's skill is read via **`read_memory_file`** (`USE_TOOL` of `core/tooling/schemas/skill.py`). |
| **S (Agent SDK)** | Via Claude Code's built-in **Bash** with `animaworks-tool <ツール> …`, or via MCP (only a curated subset is exposed to MCP; see "Adding Core Tools to the Repository" below). |
| **Anthropic fallback, etc.** | `include_use_tool=False` configuration may exist in `build_tool_list` → externally, this assumes **Bash + animaworks-tool** or skills. |

At startup, the merged map above is passed to `ToolHandler`, so common and personal tools placed **before process startup** are available from the beginning via `use_tool` / `ExternalToolDispatcher`. For **`.py` newly added during a session**, `use_tool` will not recognize the tool name unless a rescan is performed via `refresh_tools` (because the map is not updated).

## Procedure

### Step 1: Tool Design

1. Decide the tool name (module name) (snake_case, e.g., `my_api_tool`). This becomes the first argument of `animaworks-tool my_api_tool …`.
2. Decide the **actions** (subcommands). Schema names should generally follow **`{tool_name}_{action}`** (e.g., `myapi_query`). In `use_tool`, use `tool_name="myapi"`, `action="query"`.
3. Define parameters using JSON Schema (`input_schema` or `parameters`).

### Step 2: Creating the Module File

#### Single-Action Example

```python
from __future__ import annotations

import logging
from typing import Any

logger = logging.getLogger(__name__)


def get_tool_schemas() -> list[dict]:
    """ツールスキーマを返す。個人・共有ツールでは必須推奨(スキーマ読み込み・ログ用)。"""
    return [
        {
            "name": "my_tool_action",
            "description": "このツールが何をするかの説明",
            "input_schema": {
                "type": "object",
                "properties": {
                    "param1": {
                        "type": "string",
                        "description": "パラメータの説明",
                    },
                    "param2": {
                        "type": "integer",
                        "description": "オプションパラメータ",
                        "default": 10,
                    },
                },
                "required": ["param1"],
            },
        }
    ]


def dispatch(name: str, args: dict[str, Any]) -> Any:
    """スキーマ名に応じた処理を実行する(推奨)。"""
    args.pop("anima_dir", None)  # フレームワークから注入。必要なら Path(anima_dir) で利用
    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]:
    return {"result": f"Processed {param1} with {param2}"}
```

When calling from `animaworks-tool`, **`cli_main` must be implemented** afterward (see the "`cli_main`" section below).

#### Multiple Actions + Authentication (API Integration)

The resolution order for `get_credential(credential_name, tool_name, key_name="api_key", env_var=...)` is **`config.json`'s `credentials.{credential_name}`** (`api_key` or `keys[key_name]`) → **the `shared` section of `vault.json`** (the key name is the string passed as argument `env_var`) → **`shared/credentials.json` (legacy, key is `env_var`)** → **environment variable `env_var`** (`core/credentials.py`).

```python
from __future__ import annotations

import logging
from typing import Any

logger = logging.getLogger(__name__)


def get_tool_schemas() -> list[dict]:
    return [
        {
            "name": "myapi_query",
            "description": "APIにクエリを送信して結果を取得する",
            "input_schema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "検索クエリ"},
                    "limit": {"type": "integer", "description": "最大件数", "default": 10},
                },
                "required": ["query"],
            },
        },
        {
            "name": "myapi_post",
            "description": "APIにデータを送信する",
            "input_schema": {
                "type": "object",
                "properties": {
                    "data": {"type": "string", "description": "送信データ"},
                },
                "required": ["data"],
            },
        },
    ]


class MyAPIClient:
    def __init__(self) -> None:
        from core.credentials 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.0,
        )
        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.0,
        )
        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))
    if name == "myapi_post":
        return client.post(data=args["data"])
    raise ValueError(f"Unknown tool: {name}")
```

**Per-Anima authentication** (Chatwork, etc.): There is a pattern where the Anima name is taken from `args.get("anima_dir")` and an **Anima-specific key** like `CHATWORK_API_TOKEN__{anima_name}` is resolved via `resolve_env_style_credential(...)` (e.g., `resolve_identity` of `core/integrations/_chatwork_identity.py`. If not registered, raise an error without falling back). The same key naming can be used in custom tools.

#### `cli_main` (for animaworks-tool)

`animaworks-tool <tool_name> …` cannot run the CLI for core, common, or personal modules unless the module has `cli_main` (see `cli_dispatch` in `cli/tool_dispatch.py`). The typical pattern is to parse subcommands with `argparse` and call `dispatch(f"{tool}_{action}", args_dict)` internally. If you want to generate usage from a schema, also refer to `auto_cli_guide` in `core/integrations/_base.py`.

### Step 3: Saving the File

Personal tools:

```
write_memory_file(path="tools/my_tool.py", content=<コード>)
```

`tool_creation.personal` must be permitted.

### Step 4: Tool Activation (Hot Reload)

Only when adding or changing `tools/*.py` or `common_tools/*.py` **after** process startup:

```
refresh_tools()
```

The file-based map of `ExternalToolDispatcher` within the same session is rescanned, and new module names are resolved from `use_tool` (usually unnecessary for files that existed before startup).

### Step 5: Sharing (Optional)

```
share_tool(tool_name="my_tool")
```

Copied to `~/.animaworks/common_tools/`. `tool_creation.shared` is required. Other Animas each need their own `refresh_tools` (or automatic discovery at restart).

## Required Interfaces

| Function / Constant | Required | Description |
|-------------|------|------|
| `get_tool_schemas()` | **Strongly recommended** for personal and shared | For schema loading and guide generation. Even in core, some modules have empty lists (e.g., `web_search` is `[]`). **Important**: `ExternalToolDispatcher.dispatch` (the path that passes the schema name directly via `tool_use`) matches modules only for schemas included in the **`name` list of `get_tool_schemas()`** in core. Empty modules are not hit on the core side via that path. On the other hand, **`use_tool`** imports the module directly from `TOOL_MODULES` and calls `dispatch`, so **even with an empty schema list, execution is possible if `dispatch` exists**. Custom tools should be aware of both paths; defining the schema is usually safer. |
| `dispatch(name, args)` | **Recommended** | `ExternalToolDispatcher._call_module` takes priority. |
| Function with the same name as the schema name | Alternative | Used when `dispatch` is absent: `getattr(mod, name)(**args)`. |
| `cli_main(argv)` | **Required for CLI use** | `animaworks-tool` entry. |
| `EXECUTION_PROFILE` | Optional | `expected_seconds`, `background_eligible`; in core tools, **`gated: True`** can require an allowlist for send-type operations (`core/tooling/permissions.py`). |

## Invocation and schema names

- **`use_tool`**: Passed to the module’s `dispatch` (or a function with the same name) by `schema_name = f"{tool_name}_{action}"`. Authorization checks: **core**: `tool_registry` (`tool_name` must be included in the result of `get_permitted_tools`); **file-based (shared and personal)**: the merged `_personal_tools` must contain `tool_name` (`_handle_use_tool` of `core/tooling/handler.py`).
- **`animaworks-tool`**: If the first token is `submit`, it is submitted as a background job (see below). **Core** imports from `TOOL_MODULES` and `cli_main`; **shared and personal** load from a file and `cli_main`. An unknown first argument may fall back to the main CLI (`animaworks`) (`cli/tool_dispatch.py`).
- **Gated subcommands (core only)**: If the relevant action in `EXECUTION_PROFILE` has `"gated": True`, it is blocked in both the CLI and dispatch unless **`{tool_name}_{action}`** (e.g., `gmail_send`) is included in the allowed set of `permissions`. **File-based personal and shared tools** are not in `TOOL_MODULES`, so they are not subject to this gate mechanism.

## Schema Normalization

`_normalise_schema` of `core/tooling/schemas/loader.py` receives `input_schema` / `parameters` and normalizes them to `parameters` in the internal representation.

## permissions (tool_creation, external tools)

- **Loading**: `load_permissions(anima_dir)` (`core/config/schemas.py`). **`permissions.json` takes priority**. Only when absent, parse `permissions.md` to generate JSON and migrate (`migrate_permissions_md_to_json`).
- **Tool creation** (JSON example):

```json
{
  "version": 1,
  "tool_creation": {
    "personal": true,
    "shared": false
  }
}
```

The Markdown "Tool Creation" section (`個人ツール` / `共有ツール` lines) also becomes the same structure during migration.

- **External tools (core)**: `external_tools` collects, via `get_permitted_tools`, the **module names of core `TOOL_MODULES`** and the **`{tool}_{action}`** strings for gate release (e.g., `gmail_send`). In `use_tool`, core tools use names in this set on the `tool_registry` side; **personal and shared tools** are executed even outside the core set if the name exists in **`_personal_tools` after startup merge or `refresh_tools`** (files with the same name as core are skipped during discovery, so no conflict).

## EXECUTION_PROFILE

- **`background_eligible: True`**: `animaworks-tool submit <tool> <subcommand> …` registers the input from `task_type="command"` into the TaskStore, and `PendingTaskExecutor` retrieves and executes the attempt (see `_handle_submit` in `cli/tool_dispatch.py`). Execution results are also saved to `state/background_tasks/{task_id}.json`. Profile references are only applied to **importable core modules** (file tools are less likely to be flagged as warnings at submit time).
- **`gated: True`**: For the corresponding core tool actions, explicit permission for `tool_action` must be granted via permissions.

```python
EXECUTION_PROFILE: dict[str, dict[str, object]] = {
    "pipeline": {"expected_seconds": 1800, "background_eligible": True},
    "send": {"expected_seconds": 15, "background_eligible": False, "gated": True},
}
```

## Adding Core Tools to the Repository

1. Add `core/integrations/{name}.py` (items starting with `_` are excluded from scanning).
2. `TOOL_MODULES` is automatically registered via `discover_core_tools()`. No manual list for `core/integrations/__init__.py` is needed.
3. The curated allowlist for MCP is `MCP_TOOL_NAMES` in `core/tooling/surface.py`. `resolve_tool_surface` performs filtering based on trigger and role in MCP usage modes (S / C / D / G / X). Examples: `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`. **External service core tools such as Slack / Gmail / `web_search` do not appear in MCP** — they typically go through **`use_tool` / Bash (`animaworks-tool`) / skills**.
4. Add tests to `tests/`. If schemas or reference documents are auto-generated, also check the targets in `scripts/generate_reference.py`.
5. For destructive operations, consider updating `gated: True` and the permission descriptions.

## Validation Checklist

- [ ] File name: snake_case, `.py`, no leading `_` (to be included in scanning)
- [ ] Add `from __future__ import annotations` at the top (project convention)
- [ ] `get_tool_schemas()` returns the correct schema name (personal, shared)
- [ ] Process all schemas via `dispatch` or the schema-name function
- [ ] If `anima_dir` is not used, avoid side effects with `args.pop("anima_dir", None)`
- [ ] Implement `cli_main` and verify operation with `animaworks-tool`
- [ ] Add `timeout=` for external HTTP
- [ ] Authentication uses `get_credential` (or per-anima resolution of the same type as core)
- [ ] Use `logging.getLogger(__name__)` for logging (recommended)

## Security

1. Do not embed secrets in code. Use `get_credential` / vault / config.
2. Do not touch other Animas' directories.
3. In core, design "write/send" operations with `gated` and permissions as a set.

## Reference Implementations

- Thin entry + `_client` / `_cli` split: `core/integrations/chatwork.py`, `slack.py`, `discord.py`
- Authentication and API: `core/integrations/gmail.py`, `github.py`, `notion.py`, `google_calendar.py`, `google_tasks.py`
- Long-running and pipeline: `core/integrations/image_gen.py` (facade, `image/` subpackage + `EXECUTION_PROFILE`)
- Search and local LLM: `core/integrations/web_search.py` (if `get_tool_schemas` is empty, it will not match via the core path in `ExternalToolDispatcher.dispatch`; `use_tool` is possible with `dispatch`), `x_search.py`, `local_llm.py`
- Dispatcher and CLI entry: `core/tooling/dispatch.py`, `cli/tool_dispatch.py` (`cli_dispatch` / `_handle_submit`)

## Notes

- Tools are executable Python. They are distinct from skills (Markdown).
- Only tools added **after startup** require **`refresh_tools`** (files that existed before startup are already scanned at startup).
- Personal and shared files with the same name as core files are not adopted.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.