Skip to content

Repository files navigation

MicroPipe

CI License: MIT

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.

Install

pip install .
# or
pip install -e .
# or straight from GitHub
pip install git+https://github.com/MachuraHarry/micropipe

Requires Python >= 3.7. No third-party dependencies (stdlib only).

Layout

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

Pipe compatibility

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 ones a > b are comparisons, exactly as in pipe,
  • block scoping: if/while/for share 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 brackets x [] 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-form min/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_stdio builtins connect MCP servers and expose each tool as mcp0_<tool>, mcp1_<tool>, ... (first arg per tool call is positional in the order of the tool's required schema parameters).

Run locally

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]
EOF

DSL ↔ MCP end-to-end

Start 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 stdio

Then 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_)

MicroPython compatibility

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 urequests if importable and falls back to a raw-socket implementation (http only, no TLS). MicroPython has no subprocess, so mcp_use_stdio is CPython-only — on the ESP32 use mcp_use_sse with 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 .mpy with mpy-cross and freeze the modules into the firmware; the code already uses ujson/urequests fallbacks.

Flashing / first boot

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.bin

Notes 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 mpremote transfer — can hang the ESP32 silently at boot (no banner, no REPL). A fresh erase-flash + write-flash recovers it.
  • While main.py is running its loop the device never reaches the REPL prompt, so mpremote exec / raw-REPL commands time out. Watch the serial output directly (e.g. mpremote repl or a raw serial reader) instead.
  • WiFi credentials live in /wifi_config.json: {"ssid": "...", "password": "..."}boot.py reads it and connects on boot.

ESP32 deployment

  1. Install mpremote (pip install mpremote) and connect the device.
  2. ./tools/deploy.sh uploads pipedsl/, wifi.py, boot.py, main.py (uses mpremote, falls back to ampy; set MP_PORT to override the port).
  3. Create /wifi_config.json (see above) with your SSID/password.
  4. Start the MCP server on your LAN with --mode http --port 8000, set MCP_ENDPOINT in main.py to 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

Pipe gotchas

  • a > f (fn x: x * 2) > print is not a pipeline — pipe's > is a comparison there (it fails with cannot apply '>' between LIST and BUILTIN). Pipelines are the multi-line indented form:
    result: [1, 2, 3]
        > map (fn x: x * 2)
        > print
    
  • f [1, 2, 3] is a parse error ([ starts an index expression); use f ([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 call 10(20, 30) — put commas between list elements.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages