Content
# Zest
Zest is a programming language designed for LLM tool calls. It transpiles
to Python and executes against a long-lived Python namespace, giving an
agent free access to the Python ecosystem (pandas, numpy, requests,
BeautifulSoup, ...) over a tighter syntactic surface than raw Python.
## Honest results
Token savings on the bundled benchmarks (10 single-shot tasks, 3 multi-turn
flows; all output-equivalent to the Python comparison):
| Comparison | Savings |
| --------------------------------------------------------------- | --------- |
| Single-shot vs Python | **13.4%** |
| Multi-turn vs naive Python (no session — re-emits helpers) | **43.9%** |
| Multi-turn vs **session-aware Python MCP** (the fair compare) | **8.8%** |
The third row is the one that matters. The 43.9% headline from
v0.1-v0.4 was technically correct but framing-misleading: most of
that gap came from the *registry existing at all*, not from the
language. A properly-built Python MCP that holds globals across
calls (the bundled `python-mcp` server is exactly this) closes most
of the gap.
The honest language-level wins are roughly **9-15%** on token
output, concentrated in:
- **Pipelines** — `xs | f | g` is shorter than `g(f(xs))`, especially
for 3+ stages.
- **Bare-keyed records** — `{name: "x"}` saves quotes per key.
- **Auto-print** — no `print(...)` ceremony for the terminal value.
- **Output truncation** — but a Python MCP can equally adopt this
(Zest's `zest_repr` is reused by the bundled `python-mcp`).
The bigger story across **18 real agent test sessions** isn't visible
in the source-token benchmark: **retry rate dropped from 4-5/turn
to 0.25-0.7/turn** as teaching errors were iterated on. Each retry
re-emits the full source. A teaching error that turns a misuse into
a one-round-trip fix saves more tokens in production than any single
syntactic feature.
## What's in the box
| Feature | Why it's there |
| -------------------------------------------------- | ----------------------------------------------------------------- |
| Pipelines (`xs \| f \| g`), bare-keyed records, auto-print | The language-level token wins |
| Triple-quoted strings, format-spec interpolation, slice / spread / computed-key syntax | Quality-of-life parity with Python |
| ~80-name prelude with type-aware dispatch | `where` does `.query` on a DataFrame, `filter` on a list, etc. |
| Persistent session storage on disk (`--session=name`) | Helpers and bindings survive MCP restarts |
| Output truncation (`zest_repr`) | DataFrames cap to ~30 lines regardless of size |
| Teaching errors at parse + runtime | Lower retry rate; the *real* token-cost lever |
| MCP server (`zest-mcp`) over stdio | Drop-in for Claude Desktop / Claude Code |
| Sibling `python-mcp` for fair comparison | The control arm; lets you test "does Zest add value here?" |
## Quick start
```bash
uv venv && source .venv/bin/activate
uv pip install -e ".[dev]"
pytest # 391 tests
zest run examples/01_hello.zest
zest repl
```
```zest
# 30-second tour
xs = [1, 2, 3, 4, 5]
xs | map (_+1) | filter (_>3) | sum # 18
double(x) = x * 2 # one-line def
process(text) => # multi-line def
out = text | trim # last expr returns
out | upper
# Records have bare keys — no quotes. Spread + computed keys work too.
people = [{name: "alice", age: 30}, {name: "bob", age: 25}]
people | filter (_.age > 28) | map (_.name) | join ", "
{...defaults, timeout: 60, (key): val} # spread + computed
# Three lambda forms; pick the shortest that fits.
xs | map (_+1) # implicit `_`
xs | map {_+1} # brace lambda
xs | map (p => p.score) # 1-arg arrow
# Item assignment + slice syntax.
xs[0] = 99
df["revenue"] = df["price"] * df["qty"]
"hello world"[:5]
# Multi-line if-expression with intermediate bindings (v0.4):
result = if x > 3 =>
a = compute(x)
b = process(a)
b
else =>
fallback(x)
# Stateful iteration via reduce / accumulate (no `for` loops):
rows | reduce(((s, r) => {...s, (r.id): r.value}), {init: {}})
# Errors propagate; `?` catches and substitutes None.
config = read "config.json"? | json?
if x > 0 => "pos" else => "neg"
match v =>
1 => "one"
_ => "other"
```
## MCP integration
```json
{
"mcpServers": {
"zest": {
"command": "/path/to/zest-lang/.venv/bin/zest-mcp",
"args": [],
"env": { "ZEST_SESSION": "default" }
}
}
}
```
Or via Claude Code:
```bash
claude mcp add zest --scope local --env ZEST_SESSION=default \
/path/to/zest-mcp
```
The LLM gets three tools:
* `zest_run(source)` — execute Zest source. Returns truncated stdout,
the terminal value's repr, and the list of registered functions.
* `zest_inspect()` — list registered helpers.
* `zest_drop(name)` — drop a registered helper.
The `zest_run` tool description (~3000 tokens, prompt-engineered) is
the most load-bearing file in the project — when agents misuse the
language, the fix is almost always there, not in the runtime.
### Comparing against a Python MCP
The bundled `python-mcp` is a control-arm server with the same shape
(persistent globals, auto-imports, output truncation, registered
function tracking). Use it to A/B Zest against an apples-to-apples
Python baseline:
```bash
ZEST_PY_SESSION=default python-mcp # serve on stdio
```
The benchmark harness (`benchmarks/compare.py`) reports a three-way
column: Zest vs naive Python vs session-aware Python. The session-aware
column is the honest comparison.
## Roadmap
What the python-MCP comparison reveals: invest in features only a
*language* can deliver. Infrastructure-level features (registry,
truncation, auto-imports) can be replicated in any MCP.
### Highest-leverage v0.5+ work
| Item | Why it matters |
| -------------------------------------- | ------------------------------------------------------------------------------------------- |
| **Tool-description self-tuning** | Log every parse / runtime error, feed back into the description automatically. Compounds the retry-rate win. |
| **Diff-based output between turns** | When the same large DataFrame appears across turns, send `<unchanged from turn N>` instead of re-truncating. Per-turn savings unique to a language with a structured envelope. |
| **`@memo`-decorated helpers** | Cache expensive helpers across turns. Fits the registry model and saves real compute on long sessions. |
| **Real-world benchmark corpus** | Synthetic 13-task corpus may understate or overstate real wins. Real Claude tool-call transcripts (with permission) would tell us. |
### Lower priority
| Item | Note |
| -------------------------------------- | ------------------------------------------------------------------------------------------- |
| Multi-session multiplexing | MCP plumbing, not language-level — defer. |
| Tokenizer-aware prelude renaming | Audited: only 5 names cost more than 1 BPE token; renames cost more readability than tokens. |
| `!`-marked confirmable side effects | Niche. Could be useful for production safety; not a token win. |
## How the language got here — 18 sessions
Shipped at v0.1 in one commit (per the original `HANDOFF.md`), then
iterated 18 times on real agent test sessions. Each session surfaced
2-7 friction points; each follow-up commit closed them as either
runtime bugs, teaching gaps, or genuine missing features. Cumulative
adds (alphabetical, not chronological):
- **Language**: item assignment, slice syntax, spread (record + list),
computed keys, multi-line if-expression bodies, 1-arg lambda
shorthand, triple-quoted strings, format-spec interpolation,
dotted pipe-stage callables, method-call kwargs unwrapping.
- **Prelude (~30 added beyond v0.1)**: `reduce`/`fold`/`accumulate`,
`count_by`/`group_by`, `tail`/`head`, `sort`, `len`, `chars`,
`flat_map`, `contains`, `keys`/`values`, `dataframe`, `rows`,
`assoc`/`dissoc`, `findall`/`search`/`re_match`/`sub`,
`dict`/`list`/`set`/`tuple` (constructors).
- **Teaching errors** (the lever that drove retry rate down):
Python keyword shapes, `=`-kwargs, list comprehensions, slice,
raw string prefixes, regex/division ambiguity, prelude-name
shadowing, unknown escapes, multi-bare-arg, ternary, walrus,
`..`-typo for spread, dict-set vs `set` constructor, multi-line
lambda body, method-kwargs collision, reserved keywords as
variable names. Plus context-aware "did you mean" suggestions.
- **Discoverability**: `@help` and `@help name` for prelude lookup.
- **Persistence**: named sessions on disk (`--session=name`), source-
replay-based so calls don't re-fire on restart.
## Layout
```
src/zest/
lexer.py # tokenizer: indents, interpolation, regex, units
ast.py # dataclass nodes
parser.py # recursive descent
transpiler.py # Zest AST → Python AST
runtime.py # ZestSession, RunResult, registry, helpers
errors.py # teaching-grade error formatting + suggestions
repr.py # truncated repr for DataFrames / ndarrays / strings
prelude.py # ~100 names + pipeline dispatch
cli.py # zest run / zest repl
mcp_server.py # MCP stdio server
python_mcp_server.py # CONTROL ARM: same shape, plain Python
tool_description.py # the LLM-facing `zest_run` description (~3000 tokens)
benchmarks/
compare.py # tiktoken cl100k_base, three-way comparison
single/ # 10 paired .zest / .py tasks
multi/ # 3 flows × {turn{1,2,3}.zest, turn{1,2,3}.py,
# session_turn{1,2,3}.py}
examples/ # runnable .zest demos
tests/ # 391 tests across lexer/parser/transpiler/runtime/integration
```
## License
MIT
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available