9.4. Eval
A served view answers a fixed set of calls: smiles mw, smiles formula,
and so on. Eval lets a caller go further and send a whole Morloc expression
that composes the installed functions in ways you never exported: weigh a list
of molecules, filter it, pair the results with formulas. The server
typechecks the expression, compiles it, runs it, and returns the result.
A conventional API serves a fixed list of endpoints. With eval, a server
offers every composition of its allowed functions, including ones you never
anticipated, and because Morloc’s types span all the languages involved, each
composition is typechecked before it runs. A client can read the function
signatures from /discover or the MCP tool list and build a new pipeline from
them.
Eval only ever composes functions that are already installed. A caller cannot define a new function in a foreign language, read a file directly, or import anything you did not allow. The rest of this section shows how to turn it on, how to use it, and the guard-rails around it.
9.4.1. Turning eval on
Eval is off until you enable it with a list of the modules an expression may import:
$ mim view eval --allow smiles,root-py
$ mim start -p 8005:8005 --force
smiles provides the chemistry, and root-py provides the basics an
expression needs: map, filter, zip, arithmetic, and comparison. Morloc
has no implicit prelude, so even + must come from an imported module. The
allow-list is separate from the MCP and API views; a module can be importable
by eval without being served, and the reverse. mim view eval --off turns eval
off again.
9.4.2. Evaluating an expression
mim eval sends an expression to the running server of the default
environment (or --env) and prints the server’s JSON reply. Top-level items
on one line are separated with ;:
$ mim eval 'import root-py; import smiles (mw); map mw ["CCO", "NC1=NC=NC2=C1N=CN2", "c1ccccc1"]'
A longer expression is easier to keep in a file. The file holds imports and a
single expression, with where (or let) bindings laid out as in any Morloc
source; it is an expression, not a module, so it has no module line and
declares nothing:
import root-py
import smiles (mw, formula)
zip (map formula heavy) (map mw heavy)
where
heavy = filter (\s -> mw s > 100.0) mols
mols = ["CCO", "NC1=NC=NC2=C1N=CN2", "c1ccccc1"]
mim eval takes the expression as its argument, so pass the file’s contents:
$ mim eval "$(cat heavy.loc)"
The reply is {"status":"ok","result":"…"}, where result is the
expression’s value as the program would print it. mim eval reaches the
server on 127.0.0.1 at the port recorded when it started, so run it on the
machine that serves; -p overrides the port.
Remote callers use the same capability. Over HTTP it is POST /eval with the
expression in a JSON object:
$ curl -s http://localhost:8005/eval \
-H "Authorization: Bearer $MORLOC_MCP_TOKEN" \
-d '{"expr": "import root-py; import smiles (mw); map mw [\"CCO\"]"}'
Over MCP, eval is one extra tool named eval, taking an expression string,
whose description lists the modules it may import. An assistant that has read
the other tools' schemas can write an expression that chains them.
9.4.3. Guard-rails
An eval expression is code written by whoever can reach the server, so it runs under restrictions a program you compile yourself does not have:
-
Allow-listed imports only. Each
importmust name a module on the allow-list. Renaming does not help:import M as Nis checked againstM. Modules that an allowed module imports internally are fine. -
No local modules. Imports resolve only to installed modules, never to files on the server’s disk.
-
No new code. An expression cannot
sourceforeign functions or declare types, typeclasses, or instances. -
No direct IO. IO intrinsics such as
@writeand@openmay not appear in the expression. A function from an allowed module may still do IO internally, so the IO a caller can reach is exactly what your modules export. -
Resource limits. Each evaluation runs in its own process, capped at 2 GB of compiler memory and 30 seconds of CPU time.
-
Read-only container. A Docker or Podman server runs with a read-only root filesystem, so an expression cannot change the installed software.
Each request is typechecked and compiled from scratch before it runs, so even a small expression takes a moment. Nothing about one request is kept for the next.
The token rule is stricter for eval than for the rest of the server. On the
default loopback bind, eval needs no token. On an exposed server, eval runs
only for callers presenting the token, even if you started the server with
--allow-no-auth; without a token it is locked: not listed among the MCP
tools, reported as locked by /discover, and refused with 403.
--eval-allow-no-auth lifts this, for a deployment where something in front
of the server already checks callers. When a token is set, mim eval sends
the one in MORLOC_MCP_TOKEN, or the one given with --auth-token.
9.4.4. Trying expressions locally
The morloc eval command applies the same checks without a server, which is
how to see what a caller will get. Pass the allow-list with
--eval-allowed-modules:
$ morloc eval --eval-allowed-modules root-py -e 'import root-py; @write "x" 1'
IO intrinsics may not be used directly in a sandboxed eval expression; wrap the intrinsic in an exported function of an allow-listed module instead
$ morloc eval --eval-allowed-modules root-py -e 'import root-cpp; 1 + 2'
module 'root-cpp' is not in the eval allow-list
Inside the environment, mim run — morloc eval --eval-allowed-modules
smiles,root-py heavy.loc evaluates the file above the way the server would.
Without --eval-allowed-modules, morloc eval is the unrestricted local
command described in morloc eval, which also covers writing where, let,
and do blocks on one line with braces.