| name | create-atk-plugin |
| description | Creates an ATK plugin for a project. Use when asked to make a tool installable via ATK, create plugin files, add ATK support, or configure lifecycle management for a dev tool. |
Creating an ATK Plugin
ATK (AI Toolkit) is a CLI that manages AI development tools through a declarative YAML manifest. Users install
plugins with atk add, configure with atk setup, and manage lifecycle with
atk start/stop/install/uninstall/status/logs.
- ATK Home:
~/.atk/ โ contains manifest.yaml and plugins/ directory
- Plugin directory:
~/.atk/plugins/<name>/ โ contains plugin.yaml, .env, lifecycle scripts
- Install ATK:
uv tool install atk-cli
CLI Reference
| Command | Purpose |
|---|
atk add <source> | Add plugin (local path, git URL, or registry name) |
atk setup [plugin] | Configure environment variables interactively |
atk install [plugin] | Run install lifecycle |
atk uninstall <plugin> | Run uninstall lifecycle (keeps manifest entry) |
atk start [plugin] | Start service |
atk stop [plugin] | Stop service |
atk restart [plugin] | Stop then start (no separate restart lifecycle) |
atk status [plugin] | Show plugin status |
atk logs <plugin> | View service logs |
atk mcp show <plugin> | Show MCP configuration (use --json for raw JSON) |
atk run <plugin> <script> | Run a custom script from the plugin directory |
atk remove <plugin> | Stop + uninstall + delete plugin entirely |
atk upgrade [plugin] | Update to latest version |
atk uninstall vs atk remove: uninstall runs cleanup but keeps the plugin directory and manifest entry (use to
test idempotency). remove is a full wipe โ stops, uninstalls, deletes directory and manifest entry.
Part 1: Generic โ Applies to All Plugin Types
The Zero-Friction Principle
ATK exists so users can run atk add <name> and have a working tool with no debugging, no manual setup, no guesswork.
Every plugin must uphold this contract.
If ATK says "installed", it works
When install.sh exits 0, the service must be fully operational. If any dependency is missing or any step fails, the
script must exit non-zero with a clear error. ATK interprets exit 0 as success โ lying about success leaves the user
with a broken tool and no idea why.
Fail fast with actionable errors
if ! command -v ollama &>/dev/null; then
echo "Warning: Ollama not found, embeddings may not work"
fi
if ! command -v ollama &>/dev/null; then
echo "ERROR: Ollama is required but not installed."
echo " macOS: brew install ollama"
echo " Linux: curl -fsSL https://ollama.com/install.sh | sh"
echo " Other: https://ollama.com/download"
echo "Then run: atk install <name>"
exit 1
fi
Check every dependency before doing work
Verify prerequisites before any expensive operation (clone, build, start):
- External tools: installed AND running if needed
- Models/data: available or downloadable; fail if download fails
- Network: connectivity if the install needs to download anything
Health checks must verify the service
No sleep 5 and hope. Use retry loops that actually hit the endpoint:
for i in $(seq 1 15); do
if curl -sf http://localhost:8787/health >/dev/null 2>&1; then
echo " โ
API: http://localhost:8787"
break
fi
[ "$i" -eq 15 ] && { echo " โ API failed to start"; exit 1; }
sleep 2
done
This applies to both install.sh and start.sh โ if they exit 0, the service must be up and healthy.
plugin.yaml Schema
schema_version: "2026-01-23"
name: my-plugin
description: What this plugin does
vendor:
name: Author / Upstream Name
url: https://github.com/org/repo
docs: https://docs.example.com
service:
type: docker-compose
compose_file: docker-compose.yml
unit_name: my-service
ports:
- port: 8080
name: api
protocol: http
description: Main API endpoint
env_vars:
- name: MY_API_KEY
required: true
secret: true
description: API key for the service
- name: MY_OPTION
required: false
default: "some-value"
description: Optional configuration
lifecycle:
install: ./install.sh
uninstall: ./uninstall.sh
start: docker compose up -d
stop: docker compose down
status: docker compose ps --filter "status=running" --services | grep -q my-service
logs: docker compose logs -f
health_endpoint: http://localhost:8080/health
mcp:
transport: stdio
command: uv
args: [ "run", "--directory", "$ATK_PLUGIN_DIR", "server.py" ]
env:
- MY_API_KEY
Required fields
schema_version: Always "2026-01-23"
name: Plugin identifier
description: Human-readable description
Service types
| Type | Default lifecycle | When to use |
|---|
docker-compose | docker compose up/down | Docker-based tools |
docker | docker run/stop | Single container tools |
systemd | systemctl start/stop | System services |
script | Must define all lifecycle commands | Everything else |
env_vars rules
Each declared var is prompted at atk add/setup and stored in .env. ATK injects all .env values into every
lifecycle
command via os.environ.
Fields: name (required), description, required (default: false), default, secret (default: false)
IMPORTANT: Only declare vars that are actually consumed:
- By lifecycle scripts (read as
$VAR_NAME in shell)
- By the application at runtime (read from
os.environ)
mcp section
If the plugin exposes an MCP server:
transport: stdio (command-based) or sse (URL-based)
command/args: For stdio. Use $ATK_PLUGIN_DIR for paths โ ATK substitutes it with the plugin's absolute path
endpoint: For SSE
env: List of env var names to inject into the MCP process at runtime. Only include vars the MCP server reads
from os.environ โ do NOT list vars only used by lifecycle scripts
Full env var pipeline: plugin.yaml env_vars โ prompted at atk add โ stored in ~/.atk/plugins/<name>/.env
โ mcp.env names are the filter: only vars listed there are pulled from .env and injected into the MCP process
at runtime. A var in env_vars but absent from mcp.env is stored in .env but never reaches the MCP server.
To verify the full pipeline worked: run atk mcp show <name> --json and confirm every expected var appears in the
"env" object.
stdio example:
mcp:
transport: stdio
command: uv
args: [ "run", "--directory", "$ATK_PLUGIN_DIR", "server.py" ]
env:
- MY_API_KEY
sse example:
mcp:
transport: sse
endpoint: http://localhost:8080/mcp
atk mcp show <plugin> --json outputs raw JSON for MCP client configuration:
{
"my-plugin": {
"command": "uv",
"args": [
"run",
"--directory",
"/Users/.../.atk/plugins/my-plugin",
"server.py"
],
"env": {
"MY_API_KEY": "secret-value"
}
}
}
Lifecycle Events: Rules and Patterns
General rules:
- All scripts run with
cwd=plugin_dir โ paths are relative to the plugin directory
.env vars are merged into the environment before any command runs
- Exit 0 = success; for
status, exit 0 = running, non-zero = stopped
- ATK checks required env vars before
start and install. Checks port conflicts before start.
- If
install is defined, uninstall MUST also be defined (enforced by schema validation)
- No restart command โ ATK runs
stop then start for restart
- For simple one-liners, put the command directly in
plugin.yaml instead of creating a separate script
install โ The most critical script
Install IS update. There is no separate update command. atk install must converge to desired state every time.
Idempotency rule: Always build from scratch. Always rm -rf and fresh clone/install โ no conditional
"if exists, pull; else clone" logic.
External package managers silently skip when already installed โ you MUST force re-install. Bare
uv tool install pkg@latest, npm install -g pkg@latest, and pip install pkg are no-ops once the
package is present, even if upstream has shipped a newer version. The user runs atk install <plugin>
expecting an upgrade and gets nothing. Required flags per manager:
| Manager | Wrong (silently skips) | Right (always pulls latest) |
|---|
| uv tool | uv tool install pkg@latest | uv tool install pkg@latest --reinstall |
| Homebrew | brew install pkg | brew upgrade pkg || brew install pkg |
| npm global | npm install -g pkg@latest | npm install -g pkg@latest --force |
| pipx | pipx install pkg | pipx install pkg --force |
| cargo | cargo install pkg | cargo install pkg --force |
If your install relies on a curl ... \| sh install script (codanna-style), that's usually fine โ
those scripts typically replace the binary unconditionally. But verify before shipping.
Use set -e in install.sh: fail fast on errors.
start
Always clean stale runtime files (sockets, PID files) before starting โ daemons often refuse to start if these
exist, even if the old process is dead.
stop
Do NOT use set -e in stop.sh โ processes may already be stopped, and that's fine. Partial cleanup is better
than no cleanup.
status
Exit 0 = running, non-zero = stopped. Keep it simple.
uninstall
Must remove ALL resources the plugin created: containers, images, volumes, vendor clones, data directories.
Do NOT use set -e in uninstall.sh โ same reasoning as stop.
Environment Variable Audit Checklist
Before finalizing your plugin, verify every env var:
| Question | If no |
|---|
| Is this var read by any lifecycle script? | Remove from env_vars |
| Is this var read by the application at runtime? | Remove from env_vars |
Is this var read by the MCP server from os.environ? | Remove from mcp.env |
| Does the var have a concrete consumer? | Remove it โ phantom vars waste user time |
To verify "Is this var read by the MCP server from os.environ?": Check the upstream README or source code. For
npx-based packages: find the source with npm view @scope/package repository.url, then search for
process.env.VAR_NAME (Node.js) or os.environ (Python). Do not assume โ a var absent from the server's runtime
consumption is a phantom var that silently wastes user configuration time.
Common mistake: Vars used only during install (e.g., to write config files) belong in env_vars but NOT in
mcp.env. Only vars the MCP server reads at runtime belong in mcp.env.
OAuth and Interactive Auth
When a plugin cannot hand the user a static token to paste, install.sh is the right place to
walk them through the auth flow and cache the result in .env. Tokens written there are picked
up by ATK on every subsequent command โ no special handling needed downstream.
Three approaches, in order of preference: (1) static credential โ user pastes an existing token
at atk add, nothing special in install; (2) Device Authorization Flow (RFC 8628) โ script shows
the user a URL + code, polls for approval, writes the token to .env; (3) browser redirect relay
(e.g. mcp-remote) โ handled at MCP connect time, install does nothing for auth.
For tokens that install obtains on the user's behalf, declare them required: false in
plugin.yaml so the user can also paste an existing token to skip the flow.
ATK_NONINTERACTIVE=1: any install.sh that prompts or opens a browser must exit 0 immediately
when this var is set. Agents use it during testing. Hanging instead of respecting it is a bug.
Plugin Documentation Conventions
ATK uses two documentation files by convention. Neither is declared in plugin.yaml โ they
are discovered by filename. Both are content, not behavior.
README.md โ Help file
Always include a README.md in your plugin directory. This is the canonical help file:
atk help <plugin> reads and renders it in the terminal.
A good plugin README includes:
- Name and one-line description โ what this plugin is for
- Overview โ brief explanation of the upstream tool and why it's useful
- Installation โ the exact
atk add command and any prerequisites (e.g., "requires Docker")
- Environment variables โ a table with names, defaults, and descriptions
- Usage โ how to interact with the plugin after install (e.g.,
atk mcp show <plugin>, atk logs, web UI URL)
- MCP tools โ if the plugin exposes MCP tools, list them with brief descriptions.
REQUIRED: dump the live server's tool list and compare it against your README before committing.
For stdio servers: start the server, send
{"jsonrpc":"2.0","id":1,"method":"tools/list"}, record the output.
Never copy tool names from upstream docs alone โ the running server is the ground truth.
- Links โ upstream documentation, repository
Example structure:
# My Plugin
One-line description of what it does.
## Overview
Brief description of the upstream tool and its purpose.
## Installation
Requires: Docker, [any other prereqs]
```bash
atk add my-plugin
Environment Variables
| Variable | Default | Description |
|---|
| MY_API_KEY | โ | API key (required) |
| MY_OPTION | "value" | Optional setting |
Usage
After install: http://localhost:8080
MCP config: atk mcp show my-plugin
Links
SKILL.md โ Agent skill file
If your plugin exposes an MCP server, include a SKILL.md with instructions for AI
agents on how to use it effectively. ATK injects this file into agent configuration when
running atk mcp --claude (and future agent integrations).
A good SKILL.md includes:
- What this MCP is for and why it exists
- What the tools do (brief, usage-oriented โ not a copy of the API docs)
- When to use it and when not to
- Any critical usage patterns or caveats
Keep it concise. Agents read this as system-level instruction, not documentation.
# My Plugin โ Skill
Brief description of what this MCP provides and its purpose.
## Tools
- **tool_name**: What it does and when to use it
- **other_tool**: What it does and when to use it
## Usage Patterns
Key patterns or workflows an agent should know about.
## Notes
Any caveats, limitations, or non-obvious behaviors.
Common Plugin Patterns
Pattern: MCP-only (no service)
schema_version: "2026-01-23"
name: GitHub MCP
description: GitHub API integration via MCP
env_vars:
- name: GITHUB_TOKEN
required: true
secret: true
description: GitHub personal access token
mcp:
transport: stdio
command: npx
args: [ "-y", "@github/mcp-server" ]
env:
- GITHUB_TOKEN
No service, no lifecycle โ just atk setup then atk mcp show <name>. Warnings from atk start/stop are expected
and harmless for MCP-only plugins.
Pattern: Docker service with MCP bridge
service:
type: docker-compose
compose_file: docker-compose.yml
lifecycle:
install: docker compose pull && docker compose up -d
uninstall: docker compose down --rmi local --volumes
start: docker compose up -d
stop: docker compose down
status: docker compose ps --filter "status=running" --services | grep -q my-service
logs: docker compose logs -f
health_endpoint: http://localhost:8080/health
mcp:
transport: stdio
command: uv
args: [ "run", "--directory", "$ATK_PLUGIN_DIR", "server.py" ]
env:
- MY_API_KEY
Pattern: Build from upstream source
When the plugin builds from a vendor repo (not pre-built images), use a custom install.sh:
#!/bin/bash
set -e
PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENDOR_URL="https://github.com/org/repo.git"
VENDOR_REF="v2.3.1"
rm -rf "$PLUGIN_DIR/vendor"
git clone --depth 1 --branch "$VENDOR_REF" "$VENDOR_URL" "$PLUGIN_DIR/vendor/Repo"
docker compose build
docker compose up -d
Always pin to a specific tag or commit. Using main or latest means upstream breaking changes silently break
your plugin.
uninstall.sh must remove the vendor clone, built images, and volumes.
Custom Scripts (atk run)
Plugins can ship auxiliary scripts alongside their lifecycle commands. Any script placed in the plugin directory is
runnable by users with atk run <plugin> <script>. ATK looks for:
plugins/<name>/<script>
plugins/<name>/<script>.sh
Example โ a backup.sh shipped with a database plugin:
#!/bin/bash
docker compose exec my-plugin pg_dump mydb > "$ATK_PLUGIN_DIR/backup.sql"
echo "Backup saved to $ATK_PLUGIN_DIR/backup.sql"
Users run it with: atk run my-plugin backup
User Customization (custom/)
ATK supports user overrides โ plugin authors do NOT need to create these:
~/.atk/plugins/<name>/custom/overrides.yaml โ deep-merged into plugin.yaml
~/.atk/plugins/<name>/custom/docker-compose.override.yml โ auto-injected into docker compose commands
Testing Protocol
Always test through ATK itself. Do not just validate YAML โ run the full lifecycle:
atk add <source>
atk status
atk stop <name>
atk status
atk start <name>
atk status
atk mcp show <name>
atk uninstall <name> --force
atk install <name>
atk status
atk remove <name> --force
Note: If atk add fails mid-install (files copied but manifest not written), the directory
~/.atk/plugins/<name>/ becomes an orphan. A subsequent atk add will fail with
"Plugin directory already exists" even though atk remove reports success. Fix: rm -rf ~/.atk/plugins/<name>.
What to verify at each step
| Command | Check |
|---|
atk add | Exit 0, env var prompts work, install completes, health checks pass |
atk status | Service plugins: shows running, all ports marked โ, ENV โ. MCP-only plugins (no service: block): shows mcp-only, no ports column โ this is correct and expected, not an error. |
atk stop | Exit 0, service actually stopped |
atk start | Exit 0, service restarts cleanly |
atk mcp show | Correct JSON: transport, command, args, env all match plugin.yaml |
atk uninstall | Exit 0, all resources cleaned up (containers, volumes, vendor) |
atk install | Exit 0, full re-setup from scratch works (idempotency) |
Practical notes
- Port conflicts: Check that no other containers are using the same ports before testing.
- Health checks take time: Docker Compose health checks may need 5โ30 seconds. Use
--retry loops.
set -e in scripts: Use in install.sh. Do NOT use in stop.sh or uninstall.sh.
- Lifecycle one-liners: For simple commands, put them inline in
plugin.yaml instead of creating scripts.
- Unverified plugin prompt: Local and non-registry plugins show a
โ Unverified plugin confirmation on atk add.
Pass -y to skip it non-interactively. Env var prompts follow: printf "VAR1\nVAR2\n" | atk add -y ./plugins/<name>.
Part 2: Installation Type โ Local Path
Used when: Installing a plugin from the local filesystem โ typically during plugin development, or for
plugins not published anywhere.
Directory Structure
Point atk add at any directory containing plugin.yaml:
my-plugin/ โ atk add ./my-plugin
โโโ plugin.yaml # Required
โโโ install.sh # Optional
โโโ docker-compose.yml
โโโ README.md
Or, when adding ATK support to an existing project in-place:
project-root/
โโโ .atk/ โ atk add ./.atk (from project root)
โโโ plugin.yaml
โโโ ...
ATK also accepts a single YAML file: atk add ./plugin.yaml
How ATK Adds a Local Plugin
ATK copies the entire source directory to ~/.atk/plugins/<name>/. The full source directory becomes the plugin
directory โ all files inside it are available.
atk add ./.atk
atk add ./my-project/.atk
atk add ./plugins/my-plugin
Development Workflow
Local source is ideal for iterating on a plugin:
atk remove <name> --force
atk add -y ./.atk
Troubleshooting โ "Plugin directory already exists" after atk remove
Symptom: atk add fails with exit 5: Plugin directory '<name>' already exists, even though
atk remove completed successfully (or reported nothing to remove).
Cause: atk remove only cleans up plugins that are tracked in the manifest
(~/.atk/manifest.yaml). If a previous atk add failed after copying files but before writing
the manifest entry, the directory ~/.atk/plugins/<name>/ remains as a stale orphan โ invisible
to atk remove, but still blocking the next atk add.
Fix: Manually remove the orphaned directory:
rm -rf ~/.atk/plugins/<name>
Then re-run atk add normally.
Part 3: Installation Type โ Registry Plugin
Used when: Publishing a plugin to the ATK registry so users can install it with just atk add <name>.
Directory Structure
Plugin files live directly in atk-registry/plugins/<name>/ โ no .atk/ subdirectory:
atk-registry/
โโโ plugins/
โโโ my-plugin/ โ this entire directory is the plugin
โโโ plugin.yaml # Required
โโโ install.sh # Optional
โโโ docker-compose.yml
โโโ README.md # Strongly recommended
When ATK fetches a registry plugin, it sparse-checkouts plugins/<name>/ and copies its contents to
~/.atk/plugins/<name>/ โ same result as local and git installs, just sourced from the registry repo.
Self-Contained Requirement
Registry plugins must be completely self-contained. All lifecycle scripts, Dockerfiles, compose files, and config
must be inside the plugin directory. No references to files outside plugins/<name>/.
index.yaml
Never edit index.yaml manually. CI auto-generates it by running scripts/generate_index.py on every push.
Testing a Registry Plugin Locally
cd atk-registry
atk add ./plugins/<name>
atk status
atk stop <name>
atk start <name>
atk mcp show <name>
atk uninstall <name> --force
atk install <name>
atk remove <name> --force
Publishing
- Place plugin files in
atk-registry/plugins/<name>/
- Validate:
make validate
- Commit and push โ CI auto-generates
index.yaml
Pre-Publish Validation Checklist
Part 4: Installation Type โ In-Repo Plugin (Git URL)
Used when: A project author adds ATK support directly to their own tool's repository so users can install it with
atk add github.com/org/repo or atk add git@github.com:org/repo.
Directory Structure
Create a .atk/ directory at the root of the project repository:
project-root/
โโโ .atk/
โโโ plugin.yaml # Required
โโโ install.sh # Optional: custom install logic
โโโ uninstall.sh # Optional: full cleanup
โโโ start.sh # Optional: start service
โโโ stop.sh # Optional: stop service
โโโ status.sh # Optional: health check
โโโ README.md # Strongly recommended
How ATK Fetches a Git Plugin โ CRITICAL
When a user runs atk add github.com/org/repo, ATK does NOT clone the full repository. It:
- Sparse-clones the repo (minimal metadata only)
- Checks out only the
.atk/ directory
- Copies the contents of
.atk/ to ~/.atk/plugins/<name>/
- Discards the clone
The full repository is NOT available in the plugin directory. Only files inside .atk/ are copied.
If your plugin needs the full repo (e.g., to run a Python project), install.sh must clone it explicitly.
Git shorthand is supported: github.com/org/repo โ https://github.com/org/repo
Plugin Requires the Full Repo at Runtime
If your plugin IS the project in the repository, install.sh must clone the full repo:
#!/bin/bash
set -e
PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
rm -rf "$PLUGIN_DIR/src"
git clone --depth 1 https://github.com/org/my-tool.git "$PLUGIN_DIR/src"
cd "$PLUGIN_DIR/src"
uv sync
Reference the clone via $ATK_PLUGIN_DIR (substituted at runtime to the plugin's absolute path).
Testing
atk add ./.atk
atk add github.com/org/repo
atk remove <name> --force