mit einem Klick
creating-tools
// Scaffold and build new tool integrations in tools/. Use when asked to create a new tool, add an API integration, or build a new client for an external service.
// Scaffold and build new tool integrations in tools/. Use when asked to create a new tool, add an API integration, or build a new client for an external service.
Investigate reported auth, credential, permission, API proxy, 401, 403, 503, OAuth, token, or secret-resolution failures by querying VictoriaLogs first. Use when a user says an integration auth failed, a tool got unauthorized/forbidden/service unavailable, an api-proxy request failed, or asks why credentials/secrets are not working.
Researches, plans, implements, validates, and opens a focused PR for one selected self-improvement gap. Use when executing one chosen backlog item from the nightly self-improvement workflow.
Analyzes a day of Slack-thread user sessions to discover opportunities for new skills, personas, tools, workflows, and system-level improvements. Use alongside gap-analysis for the nightly self-improvement workflow.
Run comprehensive QA and integration tests against the local Centaur stack. Use when asked to QA the stack, run integration tests, verify a deployment, or check stack health after a refactor.
| name | creating-tools |
| description | Scaffold and build new tool integrations in tools/. Use when asked to create a new tool, add an API integration, or build a new client for an external service. |
Scaffold and implement new tool integrations following the established conventions.
Every tool lives at tools/<name>/ with exactly these files:
tools/<name>/
├── __init__.py # Empty file
├── .env.example # Document required secrets (one per line: KEY=description)
├── client.py # API client class + _client() factory function
├── cli.py # Typer CLI for standalone use
└── pyproject.toml # Package metadata + [tool.ai-v2] section
pyproject.toml[project]
name = "<name>"
description = "<One-line description of what the tool does>"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"httpx>=0.27.0",
"typer>=0.12.0",
"rich>=13.0.0",
"python-dotenv>=1.0.0",
]
[project.scripts]
<name> = "<name>.cli:app"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ai-v2]
module = "client.py"
The [tool.ai-v2] module = "client.py" line is required — the tool manager uses it to discover and register the tool.
Add extra dependencies only if needed (e.g., websockets, pydantic). The base set (httpx, typer, rich, python-dotenv) covers most tools.
client.pyRules:
load_dotenv() — secrets come from secret() helper or env vars at runtimesecret from shared.tool_sdk — never use os.getenv() for API keys_client() factory function at module bottom — this is how the tool manager instantiates the client_ are excluded from tool registration (use for internal helpers)close, __enter__, __exit__) are also excluded"""<Name> API client."""
import httpx
from shared.tool_sdk import secret
class <Name>Client:
"""Client for <Name> API."""
def __init__(self, api_key: str | None = None, timeout: float = 30.0):
self._api_key = api_key
self.base_url = "https://api.example.com"
self.timeout = timeout
self._client: httpx.Client | None = None
@property
def client(self) -> httpx.Client:
if self._client is None:
self._client = httpx.Client(timeout=self.timeout)
return self._client
def _get_api_key(self) -> str | None:
if self._api_key:
return self._api_key
return secret("<NAME>_API_KEY", "")
def _request(self, endpoint: str, params: dict | None = None) -> dict | list:
api_key = self._get_api_key()
if not api_key:
raise RuntimeError("<NAME>_API_KEY not set.")
url = f"{self.base_url}{endpoint}"
headers = {"Authorization": f"Bearer {api_key}"}
try:
response = self.client.get(url, params=params, headers=headers)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as e:
raise RuntimeError(f"API error: {e.response.status_code} - {e.response.text}")
except httpx.RequestError as e:
raise RuntimeError(f"Request failed: {e}")
def search(self, query: str, limit: int = 10) -> dict:
"""Search for items."""
return self._request("/search", params={"q": query, "limit": limit})
def close(self):
if self._client:
self._client.close()
self._client = None
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def _client() -> <Name>Client:
api_key = secret("<NAME>_API_KEY", "")
if not api_key:
raise RuntimeError("<NAME>_API_KEY not set.")
return <Name>Client(api_key=api_key)
cli.pyRules:
load_dotenv() at the very top — CLIs run standalone and need to load .envtyper for the CLI frameworkrich or shared.cli_tables for formatted output--json and --markdown output flags on every command"""CLI for <Name> API."""
from dotenv import load_dotenv
load_dotenv()
import json
import typer
from rich.console import Console
from shared.cli_tables import Table
app = typer.Typer(name="<name>", help="<Description>")
console = Console()
def get_client():
from .client import <Name>Client
return <Name>Client()
@app.command()
def search(
query: str = typer.Argument(..., help="Search query"),
limit: int = typer.Option(10, "--limit", "-n", help="Max results"),
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
):
"""Search for items."""
client = get_client()
data = client.search(query, limit=limit)
if json_output:
print(json.dumps(data, indent=2))
return
# ... rich table output ...
if __name__ == "__main__":
app()
__init__.pyEmpty file:
.env.exampleNAME_API_KEY=your-api-key-here
If this is a credentialed tool, add the secret to 1Password:
ENV_VAR name (e.g., COINGECKO_API_KEY)tools/README.mdAdd a row to the "Available Plugins" table with the tool name, description, and required secrets.
.env file (tools/<name>/.env) — per-tool overrides for local dev.env file (repo root) — central file for all secretshttp://secrets:8100) — production (accessed via secret())Always use secret("KEY") in client.py — it handles all resolution layers. Never use os.getenv() or os.environ for API keys.
Skip _get_api_key() and auth headers. The _client() factory can be simpler:
def _client() -> DefillLlamaClient:
return DefiLlamaClient()
Some tools need multiple credentials:
def _client() -> CoinbaseClient:
return CoinbaseClient(
api_key=secret("COINBASE_API_KEY"),
api_secret=secret("COINBASE_API_SECRET"),
passphrase=secret("COINBASE_API_PASSPHRASE"),
)
1Password sometimes returns multi-line blobs. If your API is sensitive to whitespace:
def _clean_secret(value: str) -> str:
return value.strip().split("\n")[0].strip()
Name methods clearly (create_, delete_, update_). The tool-qa skill skips these during automated testing, but they're still registered for agent use.
After creating the tool:
POST /admin/reload-tools) and check GET /tools includes your tooltool-qa skill to systematically test all methodssource .env
curl -s "http://localhost:8000/tools/<name>" \
-H "Authorization: Bearer $API_SECRET_KEY" | jq
curl -s -X POST "http://localhost:8000/tools/<name>/search" \
-H "Authorization: Bearer $API_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "test", "limit": 3}' | jq
Tools are hot-reloaded — no container restart needed. On merge to main:
git pull on the servertools/POST /admin/reload-tools