MicroPipe is a faithful re-implementation of the Pipe scripting language (https://pipe-lang.com) that runs on MicroPython (ESP32) and CPython. It adds an MCP client (HTTP + stdio transports) so Pipe programs can call external Model Context Protocol tools — ideal for tiny IoT devices that read sensors and drive actuators through MCP servers.
pip install .
# or
pip install -e .
# or straight from GitHub
pip install git+https://github.com/MachuraHarry/micropipeRequires Python >= 3.7. No third-party dependencies (stdlib only).
pipedsl/ the language + MCP client (package)
lexer.py tokenizer (indentation, strings, numbers, operators, #/-- comments)
parser.py recursive-descent parser, pipe-faithful precedence
ast.py AST node types
evaluator.py evaluator, environments, futures
builtins.py ~55 builtins + mcpN_<tool> bridge
mcp_client.py MCPHTTPClient (urequests or raw-socket) + MCPStdioClient
tools/
mock_mcp_server.py mock MCP server (stdio + http modes) for local testing
tests/ 72 unittest tests
wifi.py boot.py main.py ESP32 example (upload to device root)
tools/deploy.sh uploads pipedsl/ + ESP32 files via mpremote/ampy
The parser and evaluator were verified against the real pipe v0.9.4 binary
(side-by-side comparison harnesses). Behavior matches for:
- strict, fully left-associative precedence (including
**), - vertical pipelines
x > f > g; horizontal onesa > bare comparisons, exactly as in pipe, - block scoping:
if/while/forshare the caller env; only function calls create frames, - Go-style integer division
7 / 2 == 3, float when either operand is float, - indexing
l[1],m["key"], slicing with clamped bounds, nil on out-of-range, empty bracketsx []is a no-op, - map literals
{a: 1, b: 2}with bare-identifier keys, - function/builtin printing as
fn(x, y)/builtin function, ;is rejected as a top-level statement separator,x: [10 20 30]is a one-element list containing a call (10(20, 30)), matching pipe's space-separated call syntax — use commas in lists.
Intentional supersets / differences (lenient by design):
- extra builtins:
sum,reverse,take,drop,unique, list-formmin/max,parse_json,sleep,read_file,write_file, =accepted as an alias for:,- map key order preserved (pipe's order is not guaranteed),
>>parallel pipelines are parsed but executed sequentially,- the
mcp_use_sse/mcp_use_stdiobuiltins connect MCP servers and expose each tool asmcp0_<tool>,mcp1_<tool>, ... (first arg per tool call is positional in the order of the tool's required schema parameters).
python3 -m unittest discover -s tests # 72 tests
python3 - <<'EOF'
from pipedsl import run
run('''print (sum ([1, 2, 3]))''', out=print) # 6
run('''result: [1, 2, 3]
> map (fn x: x * 2)
> print''', out=print) # [2, 4, 6]
EOFStart the mock server (binds 0.0.0.0 so the ESP32 can reach it over the LAN):
python3 tools/mock_mcp_server.py --mode http --port 8000 # default host 0.0.0.0
# or for a stdio subprocess connection:
python3 tools/mock_mcp_server.py --mode stdioThen from a Pipe program:
out: mcp_use_stdio "python3" "tools/mock_mcp_server.py" "--mode" "stdio"
print out
print (mcp0_add 2 3) # -> 5
print (mcp0_echo "hello" true)
print (mcp0_led_set "on")
reading: parse_json (mcp0_sensor_read 1)
print (reading["value"])
Tools are registered per run with prefixes mcp0_<tool>, mcp1_<tool>, ...
Calling mcp_use_sse / mcp_use_stdio again with the same endpoint reuses
the existing connection (no new prefix), so programs that run() in a loop keep
working:
connected 5 tools from http://192.168.178.78:8000/mcp (prefix: mcp0_)
reused 5 tools from http://192.168.178.78:8000/mcp (prefix: mcp0_)
The interpreter runs on CPython (for tests/development) and MicroPython (ESP32). Tested on real hardware: ESP32-D0WD-V3, MicroPython v1.28.0.
str.isalnum()/str.isalpha()do not exist on MicroPython — identifier checks use plain ASCII membership (lexer.py).- The HTTP client prefers
urequestsif importable and falls back to a raw-socket implementation (httponly, no TLS). MicroPython has nosubprocess, somcp_use_stdiois CPython-only — on the ESP32 usemcp_use_ssewith an HTTP MCP server. - Measured on the ESP32: importing the interpreter costs ~49 KB heap, running a small MCP program leaves ~88 KB free — plenty of headroom.
- For tighter deployments you can cross-compile to
.mpywithmpy-crossand freeze the modules into the firmware; the code already usesujson/urequestsfallbacks.
pip install mpremote esptool
# download ESP32_GENERIC firmware from https://micropython.org/download/ESP32_GENERIC/
esptool --port /dev/ttyUSB0 --baud 460800 erase-flash
esptool --port /dev/ttyUSB0 --baud 460800 write-flash 0x1000 ESP32_GENERIC-<date>-v1.x.y.binNotes from real-device testing:
- Erase before flashing when replacing non-MicroPython firmware (e.g. an
ESP-IDF app). A corrupted filesystem — often from an interrupted
mpremotetransfer — can hang the ESP32 silently at boot (no banner, no REPL). A fresherase-flash+write-flashrecovers it. - While
main.pyis running its loop the device never reaches the REPL prompt, sompremote exec/ raw-REPL commands time out. Watch the serial output directly (e.g.mpremote replor a raw serial reader) instead. - WiFi credentials live in
/wifi_config.json:{"ssid": "...", "password": "..."}—boot.pyreads it and connects on boot.
- Install mpremote (
pip install mpremote) and connect the device. ./tools/deploy.shuploadspipedsl/,wifi.py,boot.py,main.py(usesmpremote, falls back toampy; setMP_PORTto override the port).- Create
/wifi_config.json(see above) with your SSID/password. - Start the MCP server on your LAN with
--mode http --port 8000, setMCP_ENDPOINTinmain.pyto your PC's LAN IP, and reset the device.
main.py runs a loop that connects to the MCP server, reads the fake
temperature sensor, and toggles a LED based on a threshold — all logic written
in Pipe and interpreted on-device by MicroPipe.
Verified end-to-end on hardware: the ESP32 boots, joins WiFi, connects to the mock MCP server over the LAN, and prints per iteration, e.g.:
wifi connected: 192.168.178.81
MicroPipe ESP32 demo started
[pipe] connected 5 tools from http://192.168.178.78:8000/mcp (prefix: mcp0_)
[pipe] temp: 27.5
[pipe] led: on
a > f (fn x: x * 2) > printis not a pipeline — pipe's>is a comparison there (it fails withcannot apply '>' between LIST and BUILTIN). Pipelines are the multi-line indented form:result: [1, 2, 3] > map (fn x: x * 2) > printf [1, 2, 3]is a parse error ([starts an index expression); usef ([1, 2, 3])to pass a list literal as a call argument.- Maps index with brackets:
m["key"], not(m "key")(which is a call error). x: [10 20 30]is a one-element list containing a call10(20, 30)— put commas between list elements.