toolcli — Programmatic Tool Calling
You have a CLI called toolcli for invoking MCP tools through small Python wrappers. Prefer it over direct one-shot MCP calls whenever the task needs filtering, ranking, batching, or any data shaping.
3 Rules (ignore these = silent failure)
toolsis a MAGIC injected namespace — only exists insidetoolcli code/toolcli script execute. NEVER write a.pyfile and run it withpython—toolswill be undefined.- Each call is a FRESH sandbox. Variables don't survive between calls. A multi-step pipeline = ONE wrapper. Share data across calls by writing to
cwd(use--cwd <dir>to make the dir persist). - When you don't know a tool's signature:
MCP responses are dicts with server-specific keys. Don't guess (toolcli list --live # all tools toolcli mcp inspect <srv>.<tool> # INPUT schema toolcli code "r=tools['x'].fn(...); result=list(r.keys())" # PROBE OUTPUT shaperesp['content'],resp['data']). Probe first.
Wrapper Anatomy — [PRE] → [IN] → [POST]
# [PRE] prepare inputs
from pathlib import Path
files = sorted(Path(payload['path']).glob('*.md'))
inputs = [f.read_text(encoding='utf-8', errors='ignore') for f in files]
# [IN] call MCP tools
# Identifier-safe tool names → tools['srv'].method(...)
# Hyphenated tool names → tools['srv']['hyphen-name'](...)
resp = tools['gpt'].generate_embeddings(
inputs=inputs, model='text-embedding-3-small', full=True,
)
# [POST] heavy data → cwd, return only summary
import numpy as np
v = np.array([x['embedding'] for x in resp['items']])
np.save(cwd / 'vectors.npy', v)
result = {'count': len(files), 'dim': v.shape[1],
'usage': resp.get('usage'),
'artifacts': [str(cwd / 'vectors.npy')]}
Injected globals: tools, payload (dict from --payload), cwd (Path).
Return: assign to result (or last top-level expression).
Errors: MCP failures raise RuntimeError — wrap with try/except if needed.
Invoke from Bash
# HEREDOC (preferred — kills quote-hell):
toolcli code --with numpy --cwd ./out <<'PY'
... wrapper body ...
PY
# Inline (short scripts only):
toolcli code "result = 1+1"
# Save reusable wrapper, run by name (description goes BEFORE code):
toolcli script add cluster "Cluster texts via embeddings" \
-f wrapper.py --deps numpy
toolcli script execute cluster --payload '{"texts":[...]}' [--json]
--json on code/script execute emits machine-readable output for chaining (jq, sub-agents).
Anti-Patterns (these fail silently or hang)
python wrapper.py→toolswon't existtools['srv'].hyphen-name(...)→ SyntaxError. Usetools['srv']['hyphen-name'](...)- guessing keys (
resp['content'],resp['data']) → MCP returns server-specific shape; probe first withlist(r.keys()) --cwdredirecting MCP file outputs → tools that save files have their own param (oftenoutput_dir=str(cwd)); pass it explicitly- 120s default timeout on image/video calls → use
--timeout 300+ gpt.generate_embeddingswithoutfull=True→ returns previews silently; numpy gets garbageplt.show()/ interactive GUI → sandbox is headless; usematplotlib.use("Agg")+fig.savefig(cwd / 'p.png')result = full_api_response→ save bulk tocwd, return only counts/paths/summary- Splitting a pipeline across multiple
codecalls → state DIES between calls. ONE wrapper. mcp add <name> <url> --env K=V→--envis for stdio MCPs; HTTP MCPs use--header
Output
ASCII (default): OK/FAIL · STDOUT · RESULT · ARTIFACTS · STDERR. Result is annotated with (object) / (array, N items) / (string) so the shape is always explicit.
JSON (--json): {ok, stdout, stderr, result, artifacts, elapsedMs, exitCode, timedOut} — single line, perfect for chaining.
Quick Reference
toolcli help # full agent guide
toolcli list # what's wired
toolcli list --live # + live tool enumeration
toolcli mcp import ~/.claude/.mcp.json # bulk-import existing MCPs