package topup
Install
dune-project
Dependency
Authors
Maintainers
Sources
md5=e4790e3bed2adaa833c9872010695530
sha512=674b5aec1a7abb8c9b39d928498a3fe9ce65969937dc02ab559a934ec1651b1ecc4573d49ca1ed308af1a14d765366a8861045d59672115f3a54d5a03f7b41e5
doc/README.html
topup
A persistent OCaml toplevel exposed as an MCP server. Lets an LLM keep typed working memory across conversation turns.
Why
LLM coding assistants in typed compiled languages pay two recurring costs: a build cycle for every idea, and re-derivation (or re-serialization) of intermediate values turn after turn. topup cuts both: phrases run inside a long-lived toplevel, and bindings persist. The model carries only names and types in its context; the toplevel holds the values. The OCaml type system makes that recall sound — the compiler catches a stale or mistyped reference before evaluation.
See DESIGN.md for the longer rationale, the phase-2 roadmap, and how topup relates to neighbouring projects. For driving toplevels on remote machines (one MCP registration, any number of hosts), see MULTIHOST.md.
Install
Requires OCaml 5.1+.
From opam:
opam install topupThis installs two drivers on your PATH: topup (bytecode) and topup-opt (native). The full MCP protocol reference is published at https://ocaml.org/p/topup (or build it locally with opam exec -- dune build @doc).
Or build from source with dune:
opam exec -- dune build @all
opam exec -- dune runtestThe from-source binary lands at _build/default/bin/main.bc.exe.
Register with Claude Code (user-scoped):
claude mcp add topup $(pwd)/_build/default/bin/main.bc.exeOr drop a project-local .mcp.json at the repo root:
{
"mcpServers": {
"topup": {
"command": "/absolute/path/to/_build/default/bin/main.bc.exe"
}
}
}Restart Claude Code (or run /mcp → Reconnect) so the new server is picked up.
Optional: the /caml slash command
This repo ships a small Claude Code skill that wraps topup's MCP tools behind /caml. It is project-scoped (lives under .claude/skills/caml/) so it activates automatically when Claude Code is run inside a topup checkout. To use it from anywhere, copy it to your user-scope skills directory:
mkdir -p ~/.claude/skills/caml
cp .claude/skills/caml/SKILL.md ~/.claude/skills/caml/SKILL.mdThen /caml 1 + 1;;, /caml #env, /caml #reset, etc. work from any project. The skill calls mcp__topup__* directly, so the topup MCP server must still be registered.
Tools
Tool | Effect |
|---|---|
| Evaluate one or more phrases. Returns |
| Evaluate a list of sources in one round trip. Returns |
| List current bindings as |
| Inspect a single binding. |
| Discard the toplevel environment. |
| Interrupt the running evaluation. |
| Copy a file from local to a registered remote, in-band over JSON-RPC. |
| Reverse direction. Defaults to |
Example
eval { source: "let xs = List.init 100 (fun i -> i * i);;" }
→ value_repr: "[0; 1; 4; 9; …]", type: "int list"
eval { source: "List.length xs;;" }
→ value_repr: "100", type: "int"
env { filter: "xs" }
→ [{ name: "xs", type: "int list", … }]
eval { source: "let rec spin n = spin n;;", timeout: 0.3 }
→ error: { phase: "runtime", message: "evaluation timed out" }Workspace pattern
A topup session is a workspace, not a calculator. Bindings persist across eval calls, so the productive shape is multi-eval — name each intermediate, read it back across turns, and use env to recall what you have:
(* eval 1 — bring in a library and load raw input *)
#require "csv";;
let raw = Csv.load "boundary.csv";;
(* raw : string list list *)
(* eval 2 — derive in-memory; named, persists for later turns *)
let parsed =
List.map
(function [day; n] -> day, int_of_string n | _ -> assert false)
(List.tl raw)
;;
(* parsed : (string * int) list *)
(* checkpoint — save-point before something experimental *)
(* mcp: checkpoint { label: "parsed" } *)
(* eval 3 — derive again; restore { label: "parsed" } rewinds here *)
let by_day = List.fold_left (fun acc (d, n) -> ...) [] parsed;;
(* env — recall the workspace state across turns *)
(* mcp: env {} → raw, parsed, by_day with their types *)Discoverability nudges that make the pattern stick:
- Reach for an existing opam package (
#require "csv";;,#require "re";;) before hand-rolling. The session inherits findlib, so anythingopam install-able is available. - Use
checkpoint { label }as a save-point before risky steps;restore { label }rewinds the whole workspace, not just one binding. Snapshots live under~/.topup/checkpoints/and survive server restarts. - On a remote (
host: "myhost"),push_file/pull_filecarry inputs and outputs across the boundary in-band over the same JSON-RPC channel — noscp. eval_batchruns a list of phrases in one round-trip — useful for the inner loop of a build-up, especially when routed.
Troubleshooting #require
Two opam/runtime quirks bite when #require'ing packages with C stubs.
stublibs path mismatch. opam stores shared stubs at <opam-root>/<switch>/lib/stublibs, but the bytecode runtime's ld.conf (read once at startup) lists <opam-root>/<switch>/lib/ocaml/stublibs. On a system with no active switch — e.g. OPAMSWITCH=<switch> selects but doesn't activate — #require fails with Cannot load required shared library dllintegers_stubs.so. Launch topup through opam exec --switch=<sw> -- so opam sets the runtime env, or append the missing path to ld.conf.
CAML_LD_LIBRARY_PATH is parsed once at startup. Setting it from inside a running session (Unix.putenv ...) has no effect on later #require calls. Export it in the shell before launching topup.
Status
Phase-1 MVP: bytecode toplevel (topup) plus an opt-in native driver (topup-opt, phrases compiled with ocamlopt -shared and Dynlink-loaded — name echoes the ocamlc/ocamlopt split). MCP over stdio (default) or a Unix domain socket (--socket <path>, one client at a time, state persists across connections). One MCP registration drives the local in-process toplevel, named local subprocess sessions with pre-warming and replica pools, and any number of SSH-tunneled remote toplevels, with per-call host / session routing (see MULTIHOST.md). checkpoint/restore ship for branching exploration. compile_to_binary promotes a stable session into a standalone native ELF via a synthesized dune project — see DESIGN.md for the phase-2 roadmap.
AI usage
topup is developed in close collaboration with Anthropic's Claude. See AI_USAGE.md for scope, provenance, and what that means for downstream users and contributors. The deployed binary has no runtime LLM dependency.