package topup

  1. Overview
  2. Docs
An OCaml toplevel exposed as an MCP server

Install

dune-project
 Dependency

Authors

Maintainers

Sources

v0.1.3.tar.gz
md5=e4790e3bed2adaa833c9872010695530
sha512=674b5aec1a7abb8c9b39d928498a3fe9ce65969937dc02ab559a934ec1651b1ecc4573d49ca1ed308af1a14d765366a8861045d59672115f3a54d5a03f7b41e5

Description

topup wraps the OCaml toplevel as a Model Context Protocol server, letting an LLM keep typed working memory across conversation turns. Bindings persist between eval calls; the model carries only names and types in its context.

Added to opam-repository:

README

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 topup

This 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 runtest

The 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.exe

Or 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.md

Then /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

eval(source, timeout?)

Evaluate one or more phrases. Returns {value_repr, type, stdout, stderr, warnings, error}.

eval_batch(sources, timeout?)

Evaluate a list of sources in one round trip. Returns {results, stopped_on_error}; stops on the first failing element.

env(filter?)

List current bindings as [(name, type, location, preview?)].

lookup(name)

Inspect a single binding.

reset()

Discard the toplevel environment.

cancel()

Interrupt the running evaluation.

push_file({host, local_path, remote_path?})

Copy a file from local to a registered remote, in-band over JSON-RPC.

pull_file({host, remote_path, local_path?})

Reverse direction. Defaults to $TOPUP_XFER_DIR/<basename>; capped at TOPUP_XFER_MAX_BYTES (16 MiB).

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 anything opam 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_file carry inputs and outputs across the boundary in-band over the same JSON-RPC channel — no scp.
  • eval_batch runs 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.

Dependencies (5)

  1. base64 >= "3.5.0"
  2. yojson >= "2.0.0"
  3. ocamlfind
  4. ocaml >= "5.3"
  5. dune >= "3.6"

Dev Dependencies (1)

  1. odoc with-doc

Used by

None

Conflicts

None