End-to-end flows that compose toolkit's primitives. Each recipe is real and copy-pasteable — set the placeholder values once and the commands work as-is.
Prerequisites. A working
toolkitinstall (brew install jingle2008/tap/toolkit) and a populated config:toolkit init # scaffolds ~/.config/toolkit/config.yaml $EDITOR ~/.config/toolkit/config.yaml toolkit doctor # confirms repo-path, kubeconfig, etc. are wired
Expose every category as a tool an AI agent can call directly. No shell-out, no scraping --help.
Claude Desktop — ~/Library/Application Support/Claude/claude_desktop_config.json:
Claude Code — ~/.claude.json (or your project's .claude/settings.json):
{
"mcpServers": {
"toolkit": {
"command": "toolkit",
"args": ["mcp"]
}
}
}Codex CLI — ~/.codex/config.toml. Same pattern as Claude Code, but TOML, and the key is mcp_servers (snake_case) rather than mcpServers:
[mcp_servers.toolkit]
command = "toolkit"
args = ["mcp"]If you need environment overrides (e.g., a different TOOLKIT_ENV_REALM for an agent session), add an env table inline:
[mcp_servers.toolkit]
command = "toolkit"
args = ["mcp"]
env = { TOOLKIT_ENV_REALM = "oc1", TOOLKIT_ENV_REGION = "us-phoenix-1" }(Claude Desktop / Claude Code support the same idea via "env": { ... } inside the server block.)
Restart the client; you should see toolkit in the MCP servers list. The agent can now call list_tenants, list_gpu_pools, list_dacs, etc.
Show me every GPU pool with more than 8 nodes and group by capacity type.
Which tenants are missing from the production realm but exist in dev?
Audit limit overrides for tenant
acme-corpand tell me which ones differ from the regional default.
The agent will fan out to the right tools and combine the results. Filter narrowing happens via the filter argument (fuzzy substring) the agent passes on each call.
A single toolkit mcp server boots in one env (env-type / env-region / env-realm from your config; the matching env_type / env_region / env_realm snake_case keys are what each MCP tool accepts in its input JSON for per-call overrides) but every read tool accepts per-call env overrides. So a single server can answer cross-env questions:
Compare the base-model lineup in
oc1/us-phoenix-1versusoc1/us-ashburn-1.
For mutation tools, env overrides are off by default — the operator's startup env is the maximum blast radius. Opt in (carefully) with --mutation-env-override-allowed at server start if your agent needs multi-realm authority.
The TUI exposes cordon/drain/reboot via keyboard shortcuts; the CLI exposes them as scriptable subcommands so you can wrap them in change-management runbooks.
NODE=gpu-node-42
# 1. Preview each step before touching anything.
toolkit cordon $NODE --dry-run
toolkit drain $NODE --dry-run
toolkit reboot $NODE --dry-run
# 2. Execute. -y skips the interactive prompt for runbook automation.
toolkit cordon $NODE -y
toolkit drain $NODE -y
toolkit reboot $NODE -y
# 3. Wait for the node to come back. `get gpunode` reflects live status —
# isReady is the bool we want; the human-readable status string (from
# GetStatus()) is computed in the renderer and not in the JSON envelope.
until toolkit get gpunode -f "$NODE" -o json | jq -e '.[] | .isReady == true' >/dev/null; do
sleep 30
done
# 4. Re-enable scheduling.
toolkit uncordon $NODE -yEvery mutation writes a structured line to the log (configured via --log-file / log-file: — defaults to toolkit.log). Set log-format: json to make it jq-friendly:
jq 'select(.msg=="mutation") | {ts, action, target, phase, dry_run, error}' toolkit.logTypical output (pretty-printed):
{ "ts": "...", "action": "cordon", "target": "gpu-node-42", "phase": "begin", "dry_run": false }
{ "ts": "...", "action": "cordon", "target": "gpu-node-42", "phase": "done", "dry_run": false }
{ "ts": "...", "action": "drain", "target": "gpu-node-42", "phase": "begin", "dry_run": false }
{ "ts": "...", "action": "drain", "target": "gpu-node-42", "phase": "done", "dry_run": false }phase is begin / done / failed; dry_run: true lines have no
phase and are written by --dry-run previews. error is set only on
phase: failed.
If you've wired MCP per Recipe 1, ask the agent:
Drain gpu-node-42, reboot it, and re-enable scheduling once it's back. Don't do anything until I say "confirm".
The agent will call cordon_node, drain_node, etc. — but each call requires confirm: true. Without it, the call refuses, logs the refusal, and returns an error explaining that confirm=true is required. You can preview the plan before authorizing the destructive set.
toolkit get plus csv output produces RFC-4180-compliant CSV (commas, quotes, newlines inside fields are properly quoted via encoding/csv).
toolkit get tenant -o csv > tenants.csv
open tenants.csv # macOS: opens in Numbers / Excelget dac is grouped by tenant. Combine with get tenant via jq to produce a single sheet:
toolkit get tenant -o json > /tmp/tenants.json
toolkit get dac -o json > /tmp/dacs.json
jq -nr '
[
"TENANT,IS_INTERNAL,DAC_COUNT,DAC_NAMES",
(
input as $tenants
| input as $dacs
| $tenants[]
| . as $t
| ($dacs | map(select(.tenant == $t.name)) | length) as $count
| ($dacs | map(select(.tenant == $t.name).name) | join(";")) as $names
| [$t.name, ($t.is_internal // false), $count, $names]
| @csv
)
]
| join("\n")
' /tmp/tenants.json /tmp/dacs.json > tenant_dac_audit.csvOr, if you just want "tenants with no DACs":
toolkit get tenant -o json | jq -r '.[] | select(.is_internal | not) | .name' > /tmp/external_tenants.txt
toolkit get dac -o json | jq -r '.[].tenant' | sort -u > /tmp/tenants_with_dacs.txt
comm -23 <(sort /tmp/external_tenants.txt) /tmp/tenants_with_dacs.txttoolkit --env-realm oc1 --env-region us-ashburn-1 get tenant -o tsv > /tmp/ash.tsv
toolkit --env-realm oc1 --env-region us-phoenix-1 get tenant -o tsv > /tmp/phx.tsv
diff <(cut -f1 /tmp/ash.tsv | sort) <(cut -f1 /tmp/phx.tsv | sort)TSV (not CSV) for this: cut -f only understands a single character, and tabs never appear inside tenant names.
Wrap toolkit get gpupool -o json and post a Slack blocks message to a webhook on a schedule.
#!/usr/bin/env bash
# /usr/local/bin/toolkit-gpu-digest.sh
set -euo pipefail
WEBHOOK="${SLACK_WEBHOOK_URL:?missing SLACK_WEBHOOK_URL}"
payload=$(
toolkit get gpupool -o json \
| jq -c '
def totals:
(map(.size) | add) as $declared
| (map(.actualSize // .size) | add) as $live
| (length) as $pools
| { declared: $declared, live: $live, pools: $pools };
. as $pools
| totals as $t
| {
blocks: [
{ type: "header",
text: { type: "plain_text", text: "GPU pools — daily digest" } },
{ type: "section",
text: {
type: "mrkdwn",
text: ("*\($t.pools)* pools — declared *\($t.declared)* nodes, live *\($t.live)*")
} },
{ type: "divider" },
{ type: "section",
text: {
type: "mrkdwn",
text: (
$pools
| map("• `\(.name)` — \(.shape) — \(.size) nodes (\(.capacityType // \"?\"))")
| join("\n")
)
} }
]
}
'
)
curl -sS -X POST -H "Content-type: application/json" --data "$payload" "$WEBHOOK"Linux / cron — crontab -e:
0 9 * * 1-5 SLACK_WEBHOOK_URL='https://hooks.slack.com/services/...' /usr/local/bin/toolkit-gpu-digest.shmacOS / launchd — ~/Library/LaunchAgents/com.toolkit.gpu-digest.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key> <string>com.toolkit.gpu-digest</string>
<key>ProgramArguments</key> <array>
<string>/usr/local/bin/toolkit-gpu-digest.sh</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>SLACK_WEBHOOK_URL</key>
<string>https://hooks.slack.com/services/...</string>
</dict>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key> <integer>9</integer>
<key>Minute</key> <integer>0</integer>
</dict>
</dict>
</plist>Load it: launchctl load ~/Library/LaunchAgents/com.toolkit.gpu-digest.plist.
The Slack message lands once a day with one section per pool. If you want partial-load awareness (e.g., one Terraform source can't be parsed), pipe through the existing toolkit stderr — the get gpupool command prints warning: ... lines for those and exits zero, so a wrapper script can branch on whether stderr was non-empty and post a follow-up alert.
-o jsonfor tooling,-o csv/-o tsvfor spreadsheets,-o tablefor humans. Match the consumer.- Always preview mutations with
--dry-runfirst. Especially in scripts — a typo'd node name shouldn't drain prod. toolkit doctoris your CI precondition. Wrap any automation that ends in a mutation withif ! toolkit doctor; then exit 1; fiso a bad config fails fast.- The audit log is the truth. When debugging "did that mutation actually fire?", check the structured log, not the CLI's stdout — stdout is for humans, the log carries the metadata (env, target, dry-run flag, status, error).
{ "mcpServers": { "toolkit": { "command": "toolkit", "args": ["mcp"] } } }