MCP server for llm-nano-vm — run deterministic LLM programs via Model Context Protocol
Project description
nano-vm-mcp
MCP server for llm-nano-vm — run deterministic LLM programs via the Model Context Protocol.
Tools
| Tool | Description |
|---|---|
run_program |
Execute a Program dict → returns trace_id, status, step count, cost |
get_trace |
Retrieve full Trace JSON by trace_id |
list_programs |
List saved programs (id, name, created_at) |
get_program |
Retrieve saved Program JSON by program_id |
delete_program |
Delete a program and all its traces |
Install
pip install nano-vm-mcp
For programs with llm steps, install the LiteLLM extra:
pip install 'nano-vm-mcp[litellm]'
Usage
stdio — Claude Desktop / local MCP client
nano-vm-mcp --transport stdio
claude_desktop_config.json:
{
"mcpServers": {
"nano-vm-mcp": {
"command": "nano-vm-mcp",
"args": ["--transport", "stdio"]
}
}
}
SSE — VPS / remote clients
NANO_VM_MCP_API_KEY=your-secret-token nano-vm-mcp --transport sse --port 8080
MCP client URL: http://<host>:8080/sse
With auth header: Authorization: Bearer your-secret-token
Docker Compose (VPS)
services:
nano-vm-mcp:
image: ghcr.io/ale007xd/nano-vm-mcp:latest
ports:
- "8080:8080"
volumes:
- ./data:/data
environment:
NANO_VM_MCP_DB: /data/nano_vm_mcp.db
NANO_VM_MCP_PORT: 8080
NANO_VM_MCP_API_KEY: your-secret-token
command: ["nano-vm-mcp", "--transport", "sse"]
Configuration
Copy .env.example to .env:
cp .env.example .env
| Variable | Default | Description |
|---|---|---|
NANO_VM_MCP_DB |
nano_vm_mcp.db |
SQLite WAL database path |
NANO_VM_MCP_HOST |
0.0.0.0 |
SSE bind host |
NANO_VM_MCP_PORT |
8080 |
SSE bind port |
NANO_VM_MCP_API_KEY |
(unset) | Bearer token for SSE auth. If unset, all requests are allowed (warning logged) |
NANO_VM_MCP_LLM_MODEL |
(unset) | LiteLLM model string for llm steps (e.g. openrouter/meta-llama/llama-3.3-70b-instruct:free) |
Endpoints
| Path | Auth | Description |
|---|---|---|
GET /health |
none | Liveness probe — always returns {"status": "ok"} |
GET /sse |
bearer | SSE transport entry point |
POST /messages |
bearer | MCP message endpoint |
Example: run a program
import asyncio
from mcp import ClientSession
from mcp.client.sse import sse_client
program = {
"steps": [
{"id": "s1", "type": "tool", "tool": "my_tool", "input": {"query": "hello"}}
]
}
async def main():
headers = {"Authorization": "Bearer your-secret-token"}
async with sse_client("http://localhost:8080/sse", headers=headers) as (r, w):
async with ClientSession(r, w) as session:
await session.initialize()
result = await session.call_tool("run_program", {"program": program, "save_as": "demo"})
print(result.content[0].text)
asyncio.run(main())
Architecture
nano-vm-mcp acts as a Stateful Gateway layered over the llm-nano-vm Execution Kernel.
Every successful execution step is wrapped in a GovernanceEnvelope and persisted as an
immutable audit trail. The kernel and gateway are strictly isolated: the gateway never
touches execution logic, the kernel never touches persistence or policy.
MCP Client
→ nano-vm-mcp (Gateway)
→ GovernedRunProgramHandler ← PolicySnapshot, CapabilityRef resolution
→ llm-nano-vm (Kernel) ← deterministic FSM, ASTEngine, ProjectionLayer
→ GovernanceEnvelope store ← SQLite WAL, append-only audit log
GovernanceEnvelope
Each successful execution step produces a GovernanceEnvelope (frozen Pydantic model)
stored in the governance_envelopes table:
| Field | Type | Description |
|---|---|---|
execution_id |
str |
Session / trace identifier |
step_id |
int |
Step index within the execution |
policy_hash |
str |
SHA-256 of the active PolicySnapshot |
canonical_snapshot_hash |
str |
Merkle/delta hash of CanonicalState at this step |
payload |
dict | list |
Projected (sanitized) step output |
Envelopes are written only on error=None — they form a tamper-evident audit trail of
successful transitions, not of failures.
CapabilityRef and Tombstoning
Sensitive values in CanonicalState are stored as CapabilityRef tokens
(vault://secret/<id>) rather than raw plaintext. On a GDPR erasure event (E_gdpr_erase),
the target ref is tombstoned (is_tombstone=True). All subsequent projections return the
constant [REDACTED_TOMBSTONE], preserving the hash chain without exposing the erased value.
Security
Condition expressions
run_program accepts a full Program dict — including condition steps with
expression strings. As of v0.3.0, these are evaluated by the ASTEngine —
a deterministic sandboxed interpreter built into llm-nano-vm. eval() is
not used. The engine supports a fixed, safe operator set: ==, !=, >,
<, in, not in, and, or, not, contains. Arbitrary Python
expressions outside this set are rejected with a parse error.
Rules for safe use:
- Condition logic must be authored by you, not generated from untrusted input at runtime.
- LLM output may appear as a value being tested (
'yes' in '$decision'), never as the condition expression itself. - If you expose this MCP server to untrusted clients, validate or allowlist condition
expressions before passing them to
run_program.
Capability verification
GovernedToolExecutor intercepts every tool call and verifies the tool name against
the active PolicySnapshot.tool_capabilities before execution. Tools not listed in the
policy raise CapabilityDeniedError — they are never silently executed.
ExecutionVM additionally rejects any tool name not registered in its tool registry
with a VMError. These are two independent enforcement layers.
Avoid registering destructive or privileged tools (filesystem writes, shell exec, database mutations) without an explicit access control layer in your tool implementation.
SSE transport and auth
Set NANO_VM_MCP_API_KEY to enable bearer token authentication on the SSE transport.
The comparison is timing-safe (secrets.compare_digest). If the variable is unset,
a warning is logged to stderr and all requests are allowed — suitable for localhost only.
Do not expose the SSE endpoint to the public internet without NANO_VM_MCP_API_KEY set
or behind a reverse proxy with auth (nginx, Cloudflare Access, VPN).
Roadmap
-
run_program,get_trace,list_programs,get_program,delete_program(v0.1.0) - stdio + SSE transports (v0.1.0)
- SQLite WAL persistence (v0.1.0)
- Bearer token auth for SSE —
NANO_VM_MCP_API_KEY, timing-safe (v0.1.0) -
/healthliveness endpoint (v0.1.0) - Structured error responses + logging (v0.1.0)
-
GovernanceEnvelope— immutable audit trail per execution step (v0.3.0) -
GovernedRunProgramHandler+GovernedToolExecutor+CapabilityDeniedError(v0.3.0) -
PolicySnapshotCRUD — capability-gated tool execution (v0.3.0) -
CapabilityRef+ tombstoning — GDPR erasure with hash-chain preservation (v0.3.0) - ASTEngine in condition steps —
eval()removed from production path (v0.3.0) -
governance_envelopestable — append-only SQLite store with execution index (v0.3.0) -
plan_and_run— intent string → Planner → run (P7) -
POST /mcp/session/{execution_id}/step— full RFC step lifecycle withvm.step() -
RemoteProjectionProvider— IPC connector to Vault for JIT plaintext access - Docker image to GHCR
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nano_vm_mcp-0.3.0.tar.gz.
File metadata
- Download URL: nano_vm_mcp-0.3.0.tar.gz
- Upload date:
- Size: 30.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ab7dc72dd1417015d5e57807ee7c3f97c1239b6322901cdea154ceab5d289746
|
|
| MD5 |
b7b431b645f19a945994478e4c1b55a4
|
|
| BLAKE2b-256 |
b1d5bb780650a413c43f60bfde9eec859493f1b03270e32b34ecf970c8f26bed
|
File details
Details for the file nano_vm_mcp-0.3.0-py3-none-any.whl.
File metadata
- Download URL: nano_vm_mcp-0.3.0-py3-none-any.whl
- Upload date:
- Size: 18.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3b66ce37a3048e3be6ef7ac17a3ebd3fd422f3c4fc414e6f9ad912c72b4ac1da
|
|
| MD5 |
8ed4b9fd3ec6b3b894ba44bc17785c73
|
|
| BLAKE2b-256 |
7c89a17007ed66c07a06ef08dfa1700c402dce526d8c7a9ae7807ce1134dd5dd
|