Skip to content

The eval endpoint and MCP

The HTTP endpoints in Call the HTTP API expose one action at a time. The /api/eval endpoint exposes a different surface onto the same shared action layer: it runs a whole TypeScript script that can call many actions, in a hermetic sandbox built from every registered provider’s actions. Both are views of the one capability layer your host assembles — you write your providers once, and the sandbox behind this endpoint is built from ProviderRegistry.toTools() (see Register providers and services and Tools and transports).

POST /api/eval with a script, an initial data object, and an optional callLog. The script runs in a sandbox whose available functions are all your providers’ actions.

Terminal window
curl -X POST http://localhost:3800/api/eval \
-H 'content-type: application/json' \
-d '{
"script": "const note = await notesCreate({ title: data.title, body: data.body }); data.id = note.id;",
"data": { "title": "Groceries", "body": "Milk, eggs, coffee" }
}'

The script has no return statement — it surfaces results by writing to the persistent data object (and by console.log). The response reports how the run ended, the resulting data, the recorded call log, and anything the script logged:

{
"status": "completed",
"data": { "title": "Groceries", "body": "Milk, eggs, coffee", "id": "note_8f2a" },
"callLog": [{ "...": "one entry per action call the script made" }],
"console": [{ "...": "one entry per console.* call" }]
}

status is "completed" when the script ran to the end, or "interrupted" when an action paused the run (see resumption below). On either outcome you get data, callLog, and console back.

Scripts call actions by their aggregated function name. List what is available with GET /api/eval/actions:

Terminal window
curl http://localhost:3800/api/eval/actions
[
{ "functionName": "notesCreate", "name": "Create note", "description": "Creates a note." },
{ "functionName": "notesSearch", "name": "Search notes", "description": "Finds notes by text." }
]

For the precise call signature of one action, request its detail. The response includes generated TypeScript typings describing the function’s parameters and return type — paste them into your editor to write scripts against a typed surface:

Terminal window
curl http://localhost:3800/api/eval/actions/notesCreate
{
"functionName": "notesCreate",
"name": "Create note",
"description": "Creates a note.",
"typings": "declare function notesCreate(input: { title: string; body: string }): Promise<{ id: string; title: string; createdAt: string }>;"
}

An action can pause itself — for example to ask a human for approval. When that happens the run comes back with status: "interrupted", plus two things you need to continue:

  • a callLog recording every call up to the pause, and
  • an interrupt object: { position, data, state }. position is the paused call’s slot in the log, data is what the action is asking you to decide on, and state is its opaque checkpoint.
{
"status": "interrupted",
"data": { "title": "Groceries", "body": "Milk, eggs, coffee" },
"callLog": [/* calls up to the pause; the paused slot is null */],
"console": [],
"interrupt": { "position": 0, "data": { "question": "Approve?" }, "state": { "token": "" } }
}

To resume, call /api/eval again with the same script, the returned callLog, and a resumption that carries the interrupt’s position and state back plus your answer under data. The sandbox replays the recorded calls deterministically instead of re-running them, then delivers your answer to the call that paused so it can complete:

Terminal window
curl -X POST http://localhost:3800/api/eval \
-H 'content-type: application/json' \
-d '{
"script": "<the same script>",
"data": { "title": "Groceries", "body": "Milk, eggs, coffee" },
"callLog": [ /* the callLog returned by the interrupted run */ ],
"resumption": { "position": 0, "data": { "approved": true }, "state": { "token": "…" } }
}'

Because the call log is deterministic, resumption is exact: already-completed calls are not executed again, and only the paused call runs — now with your answer in hand. A run can pause more than once; each time, answer the new interrupt the same way. This is what lets a long or approval-gated workflow survive across multiple HTTP requests. The underlying mechanism is Determinism and resumption.

The vision is that this one shared action layer is reachable through many surfaces, MCP among them, so that agents can call your providers’ capabilities as tools. As of the current server, MCP is not yet wired into the hostcreateServer registers the health, provider, and eval routes plus the interactive docs, and no MCP endpoint. Treat MCP as planned rather than available today; until it lands, agents reach the action layer through the eval endpoint or by consuming providers directly with @grundlag/agent.

For calling a host from application code, you usually don’t hand-write requests at all: a host can generate a strongly-typed TypeScript client built against its exact providers, so you get compile-time types for the actions and entities that host exposes. The same package also reconstructs a host’s actions as tools, so a sandbox or agent can run on the client over HTTP. See Generate a typed client.