Configuration & Setup
How toolkits collect configuration from users — API keys, paths, large data files, custom setup logic.
Why configuration matters
Many toolkits need information beyond their bundled code. A few real examples:
- An astrophysics toolkit needs a path to ~2GB of opacity data files.
- An LLM-querying toolkit needs an OpenAI API key.
- A simulation toolkit needs to know how many CPU workers to spawn.
- A file-handling toolkit needs a designated workspace directory.
Toolbase gives you a consistent way to collect this configuration from users at install time — so every toolkit looks and feels the same to the user installing it, no matter who wrote it.
Two tiers, one system
There are two layers:
Declarative — toolkit.yaml
For simple values: paths, strings, integers, secrets, env var checks. Just list them. Toolbase prompts the user, validates types, and stores the values. No logic.
Script — setup.py
For everything else: file validation, downloads, multi-step setup, custom detection. Plain Python with a helper called ctx for consistent UI.
Use the simplest tier that works for your toolkit. Most toolkits will only need declarative.
Tier 1: Declarative configuration
Add a config section to your toolkit.yaml:
config:
- name: data_dir
type: path
description: "Where to store outputs"
default: "~/.my-toolkit/data"
- name: max_workers
type: integer
description: "Number of parallel workers"
default: 4
- name: api_endpoint
type: string
description: "API endpoint URL"
default: "https://api.example.com"yamlWhen the user installs your toolkit, Toolbase prompts them for each value. They can press Enter to accept defaults. Values are stored as YAML at ~/.toolbase/config/<toolkit>.yaml, which is the canonical home — users can hand-edit at any time, or use toolbase config set <toolkit> <key> <value> from the CLI.
Supported types
| Type | Description |
|---|---|
| path | File or directory path. ~ is expanded. |
| string | Plain text value |
| secret | Hidden input (for API keys, passwords) |
| integer | Whole number with optional min/max bounds |
| float | Decimal number with optional min/max bounds |
| boolean | true / false |
| choice | One of a fixed list of options |
Secrets and environment variables
Toolbase stores all toolkit configuration — including secrets like API keys — in ~/.toolbase/config/<toolkit>.yaml (mode 0600, readable only by you). Use type: secret in your config: block:
config:
- name: api_key
description: "Your NASA API key. Get one at https://api.nasa.gov."
type: secret
required: trueyamlSecrets are hidden during the install-time prompt and masked in toolbase config show. The values reach your tools through the same state-injection mechanism as any other declared field — never via the LLM's context, never via command-line arguments.
If you specifically want a value to come from a shell environment variable (for example, in CI where the secret is provided by your platform's secret manager), the setup.py script in your toolkit can read it and forward to the canonical config:
import os
def setup(ctx):
api_key = os.environ.get("MY_TOOLKIT_API_KEY")
if api_key:
ctx.set_config("api_key", api_key)
# else: fall through to whatever the user has already configured
return TruepythonThis pattern is the recommended way to bridge environment-only secrets into Toolbase. The canonical store stays the YAML file; the env var is just the source for that one provisioning event.
What declarative does not do
- File existence or content validation
- Downloading data
- Multi-step or conditional flows
- Any custom logic
If you need any of those, use Tier 2.
Tier 2: Setup scripts ( setup.py )
Drop a setup.py file at the root of your toolkit and add this to your toolkit.yaml:
setup_script: trueyamlThe file exposes two functions:
# setup.py
def setup(ctx) -> bool:
"""
Interactive setup. Called when the user runs:
toolbase setup my-toolkit
Return True on success, False on cancel/failure.
"""
return validate(ctx)
def validate(ctx) -> bool:
"""
Check whether the toolkit is ready to serve. Called by:
toolbase serve
Return True if everything's in place, False otherwise.
"""
return TruepythonThe ctx object is a toolbox Toolbase gives you. Using it is what makes every toolkit's setup feel consistent — you don't need to import Rich, manage config files, or roll your own download progress bars.
Two ways state-fields get values
Both Tier-1 and Tier-2 produce the same outcome — values that reach @define_tool(state=[...]) parameters at serve time. The difference is where the value comes from:
User-supplied values — declare them in the config: block. The user fills them via prompts at install, or later via toolbase config set <toolkit> <key> <value> or by hand-editing the YAML. Use this for API keys, paths, worker counts — anything the user has an opinion about.
Derived values — set them from setup.py via ctx.set_config('key', value). Use this for things the user shouldn't be prompted for: auto-detected hardware (use_gpu), download paths derived from ctx.data_dir, version strings read from a downloaded data file, etc.
Both reach tools the same way. The platform stores both in the same canonical YAML file. The user sees both via toolbase config show <toolkit>. The distinction is purely about who decides the value — the user (declared) or the toolkit (derived).
The SetupContext API
The ctx object is passed into your setup and validate functions.
Output
ctx.info("Checking dependencies...") # Blue info line
ctx.warn("This will take a while") # Yellow warning
ctx.error("Path not found") # Red error
ctx.hint("Try: toolbase setup aster") # Dim hint after an error
ctx.success("Setup complete!") # Green successpythonInput
# Strings
name = ctx.prompt("Enter name:", default="aster")
# Typed prompts
path = ctx.prompt_path("Data path:", must_exist=True)
port = ctx.prompt_int("Port:", default=8080, min=1, max=65535)
key = ctx.prompt_secret("API key:")
# Yes/no
proceed = ctx.confirm("Download 2GB?", default=False)
# Menu
choice = ctx.choice("How would you like to proceed?", [
("download", "Download automatically"),
("path", "I have the data, let me provide the path"),
("cancel", "Cancel"),
])pythonReading and writing config
# Read
path = ctx.get_config('data_path')
path = ctx.get_config('data_path', default='~/data')
# Write — persists to ~/.toolbase/config/<toolkit>.yaml.
# Local snapshot is updated too, so a subsequent get_config
# in the same setup() call sees the new value.
ctx.set_config('data_path', '/data/foo')pythonDownloads
ctx.download(
url="https://data.example.com/aster/opacity.tar.gz",
destination=ctx.data_dir / 'opacity',
description="Opacity data",
size_hint="2.3GB",
extract=True, # Auto-extract .tar.gz, .zip
sha256="abc123...", # Optional checksum
)pythonProgress bars, extraction, and checksum verification are handled for you.
Useful paths
| Path | Description |
|---|---|
| ctx.toolkit_path | Where the toolkit is installed |
| ctx.data_dir | Per-toolkit data directory (auto-created) |
| ctx.cache_dir | Per-toolkit cache directory |
Raw Python
SetupContext is not a walled garden. You can drop to plain Python whenever you want — call subprocess, query a database, sniff environment, anything. Use ctx when you want consistent UI; ignore it when you don't.
A complete example
Here's how a toolkit that needs to download a large data file (and lets the user override) might look:
# setup.py
from pathlib import Path
DATA_URL = "https://data.example.com/my-toolkit/dataset_v1.tar.gz"
def setup(ctx) -> bool:
existing = ctx.get_config('dataset_path')
if existing and Path(existing).expanduser().exists():
ctx.info(f"Dataset already configured: {existing}")
return True
choice = ctx.choice(
"Dataset is required. How would you like to proceed?",
[
("download", "Download automatically (~500MB)"),
("path", "I have the data, provide the path"),
("cancel", "Cancel"),
]
)
if choice == "download":
dest = ctx.data_dir / 'dataset'
ctx.download(
url=DATA_URL, destination=dest,
description="Dataset", size_hint="500MB", extract=True,
)
ctx.set_config('dataset_path', str(dest))
elif choice == "path":
path = ctx.prompt_path("Path to dataset:", must_exist=True)
ctx.set_config('dataset_path', str(path))
else:
return False
return validate(ctx)
def validate(ctx) -> bool:
path = ctx.get_config('dataset_path')
if not path:
ctx.error("dataset_path not configured")
ctx.hint("Run: toolbase setup my-toolkit")
return False
if not Path(path).expanduser().exists():
ctx.error(f"Dataset path missing: {path}")
return False
return TruepythonMore patterns — see the Recipes page.
User experience
Here's what users see when your toolkit needs setup:
On install
Tier-1 prompts run inline during install (one prompt per declared config: field). If the toolkit also has a setup.py, it runs after.
$ toolbase install aster
Installing aster 1.0.0...
✓ Downloaded
✓ Environment created (venv, Python 3.12)
✓ Dependencies installed
api_key (required) — Your NASA Exoplanet Archive API key.
> <user types or presses Esc to defer>
max_workers — Number of parallel workers (default: 4)
> <Enter accepts default>
Running aster setup script...
Downloading opacity data...
[####################] 2.3 GB / 2.3 GB
✓ aster setup script complete.
✓ Successfully installed aster v1.0.0textOn setup
$ toolbase setup my-toolkit --no-input
Configuring my-toolkit (non-interactive mode: filling defaults)
✓ config written: ~/.toolbase/config/my-toolkit.yaml
Running my-toolkit setup script...
running setup script
api_key from Tier-1: <set>
persisted worker_count=4
downloading https://data.example.com/blob.tar.gz →
~/.toolbase/data/my-toolkit/downloaded-blob.tar.gz
data ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 56/56 bytes ? 0:00:00
download complete: 56 bytes
setup complete
✓ my-toolkit setup complete.textA few things worth knowing about that output:
--no-inputtriggers the non-interactive path. Tier-1 declarative fields fill from defaults; Tier-2setup.pyprompts likewise fall back to declared defaults. Without--no-input(TTY mode), the user gets one prompt per Tier-1 field that lacks a default, then the setup script runs with full interactivity.- The lines
running setup script,api_key from Tier-1: <set>,persisted worker_count=4, etc. come from the toolkit's ownctx.info(...)calls — that's how each toolkit gives its setup its own voice. - The
data ━━━━━━ 56/56 bytesline is the Rich progress bar fromctx.download(...). Real-sized downloads get speed and ETA columns too. - The trailing
✓ <toolkit> setup complete.is the CLI's own summary line, not from the toolkit'ssetup.py.
On serve
If a toolkit isn't configured, Toolbase skips it rather than crashing. The user sees exactly what's wrong:
$ toolbase serve
Checking installed toolkits...
✗ aster validate(ctx) failed — Opacity data at /data/opacity is invalid or incomplete
Edit: ~/.toolbase/config/aster.yaml
Or: toolbase config edit aster
… simple-api loading (venv)
Toolkit launch results:
✓ simple-api ready (3 tools)
Starting MCP server with 1 toolkit (3 tools)...textAnti-patterns
A few things to avoid — these break consistency and the validator will warn about them:
- Don't use
input()in setup.py. Usectx.prompt(). Otherwise your prompts won't look like every other toolkit. - Don't write to
~/.toolbase/config/<toolkit>.yamldirectly from setup.py. Usectx.set_config(). The helper handles atomic writes, comment preservation, and mode 0600; bypassing it can corrupt the file or leak secrets. (Hand-editing the file outside setup.py is fine — that's exactly what the file-canonical principle is for.) - Don't skip
validate(). If your toolkit needs config, validate will save your users from cryptic runtime errors. - Don't crash on setup failure. Catch exceptions and report nicely with
ctx.error(). Network errors and missing files should be surfaced as actionable messages, not tracebacks.
Troubleshooting
setup.py errors
If your setup.py has a syntax error or import problem, toolbase install and toolbase setup will surface the Python traceback inline (with file + line number) and write the full traceback to a per-run log under ~/.toolbase/logs/setup-<toolkit>-<timestamp>.log. Fix the file and re-run; the install pipeline never leaves the toolkit in a half-installed state.
Next
Configuration is half the picture. The other half is stateful tools — how config values get injected into your tool functions without the AI agent ever seeing them.