Skip to main content
Glama
README.md
# EV3 MCP Server

Host-side [FastMCP](https://github.com/modelcontextprotocol/python-sdk) **stdio** server that any MCP client can use to drive a LEGO Mindstorms EV3 over Wi-Fi SSH. MCP stays on your Mac; only thin `ev3dev2` code runs on the brick (MicroPython by default).

This repo also includes a **CLI chat client** that uses [Hugging Face Inference Providers](https://huggingface.co/docs/inference-providers) (OpenAI-compatible) by default, spawns the local EV3 MCP server, and lets you type natural language like `move forward`.

## Architecture

```
CLI chat (ev3-chat / main.py)  or  Cursor / Claude Desktop
        │ stdio MCP tools
        ▼
ev3_mcp on Mac  ──persistent SSH──►  EV3 brick (ev3dev2 motors)
```

## Requirements

- Python 3.10+
- [uv](https://docs.astral.sh/uv/) (recommended)
- EV3 running [ev3dev](https://www.ev3dev.org/) with `ev3dev2` (MicroPython preferred; `python3-ev3dev2` also works)
- SSH reachability from the host (password auth by default)
- For the chat CLI: a [Hugging Face](https://huggingface.co/settings/tokens) token with **Inference Providers** permission (or any OpenAI-compatible endpoint)

## Setup

```bash
cd /path/to/ev3dev_MCP
uv sync
cp .env.example .env   # edit only when leaving dry-run / set LLM vars for chat
```

**Dry-run is on by default** (`EV3_DRY_RUN=1`): no SSH is opened at startup or on tool calls; tools log the would-be remote Python and return fake success shaped like real results. Flip to `EV3_DRY_RUN=0` only after `EV3_HOST` is reachable and `EV3_PASSWORD` works.

When dry-run is off, the MCP server **connects over SSH immediately on startup** (eager connect) and logs success or failure. Tool calls still reconnect if the session drops.

### Auth

| Method | Env | Notes |
|--------|-----|--------|
| Password (primary) | `EV3_PASSWORD` | Default path — set this in `.env` |
| SSH key (optional fallback) | `EV3_SSH_KEY_PATH` (default `~/.ssh/id_ev3`) | Used only when `EV3_PASSWORD` is unset |

Never commit a real `.env`.

### Motors / safety

| Variable | Default | Purpose |
|----------|---------|---------|
| `EV3_LEFT_MOTOR` / `EV3_RIGHT_MOTOR` | `B` / `C` | Tank drive ports |
| `EV3_PYTHON_BIN` | `micropython` | Brick interpreter (`micropython` or `python3`) |
| `EV3_DEFAULT_SPEED` / `EV3_MAX_SPEED` | `40` / `80` | Default and clamp for `speed_pct` |
| `EV3_DEFAULT_DURATION` / `EV3_MAX_DURATION` | `1.0` / `5.0` | Default and clamp for `duration_s` |

Drive tools use **timed** `on_for_seconds` runs (not infinite spin). Concurrent drive calls are **rejected as busy**; `stop` bypasses the lock and clears it.

## Run the MCP server

```bash
# stdio MCP server (what clients spawn)
uv run python -m ev3_mcp.server

# or after uv sync / pip install -e .
python -m ev3_mcp.server
```

## Run the CLI chat client

The client spawns `ev3_mcp.server` over stdio, calls a chat model via Hugging Face’s OpenAI-compatible router, and runs a prompt-toolkit chat loop.

```bash
# 1. Put HF_TOKEN in .env (https://huggingface.co/settings/tokens)
# 2. Pick a tool-capable OPENAI_MODEL, then:

uv run main.py
# or
uv run ev3-chat
# or
uv run python -m ev3_chat
```

Optional extra MCP server scripts (same pattern as the reference cli_project):

```bash
uv run main.py /path/to/other_mcp_server.py
```

### Chat client env

| Variable | Default | Purpose |
|----------|---------|---------|
| `HF_TOKEN` | *(required)* | Hugging Face token (Inference Providers). Also accepts `OPENAI_API_KEY`. |
| `OPENAI_BASE_URL` | `https://router.huggingface.co/v1` | HF router; set to `http://127.0.0.1:1234/v1` for LM Studio |
| `OPENAI_MODEL` | `meta-llama/Llama-3.3-70B-Instruct` | Tool-capable model id (`model` or `model:provider`) |
| `USE_UV` | `1` | Spawn EV3 server with `uv run` when `1` |

**Tool calling:** the EV3 tools only work if the model/provider supports OpenAI-style `tools`. Prefer instruct models known for function calling; you can append `:fastest`, `:cheapest`, or a provider like `:groq` / `:together`.

Smoke-test MCP only (no LLM required) — lists the 7 EV3 tools:

```bash
uv run mcp_client.py
# or
uv run python -c "from ev3_chat.mcp_client import main; import asyncio; asyncio.run(main())"
```

## Tools

| Tool | Behavior |
|------|----------|
| `move_forward` | Both motors forward for `duration_s` / `speed_pct` |
| `move_backward` | Same, reverse |
| `turn_left` / `turn_right` | Differential turn |
| `stop` | Immediate stop — bypasses command lock |
| `beep` | Speaker beep |
| `list_connected_devices` | Live motors/sensors on the brick (address, driver, mode) |
| `robot_status` | Dry-run flag, SSH state, ports, busy, last error |

## Client wiring

### Cursor / Claude Desktop

Copy from [`mcp.json.example`](mcp.json.example). Point `command`/`args` at this repo and set env (keep dry-run until the brick is ready):

```json
{
  "mcpServers": {
    "ev3": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/ev3dev_MCP", "python", "-m", "ev3_mcp.server"],
      "env": {
        "EV3_DRY_RUN": "1",
        "EV3_HOST": "192.168.1.100",
        "EV3_USER": "robot",
        "EV3_PASSWORD": "maker"
      }
    }
  }
}
```

Claude Desktop uses the same stdio shape in `claude_desktop_config.json`.

### Constraint

The MCP process must run on a machine that can SSH to the EV3 (typically your Mac on the same Wi-Fi). Cloud-only agents that cannot spawn a local stdio server cannot drive the brick in v1.

## Layout

| Path | Role |
|------|------|
| `src/ev3_mcp/` | FastMCP stdio server |
| `src/ev3_chat/` | CLI chat client (`mcp_client`, `core/*`, `main`) |
| `main.py` | Thin entry → `ev3_chat.main` (`uv run main.py`) |
| `mcp_client.py` | Thin smoke-test entry (list tools, no LLM) |

## Out of scope (v2)

Sensors, camera, on-brick MCP, persistent brick daemon, drive-command queuing, SSE/HTTP remote MCP.

TDQS

A4.2/5.0

Scored across 8 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: movement commands (forward/backward/turn/stop) are mutually exclusive, and beep, list_connected_devices, and robot_status cover separate concerns. No overlapping functionality.

Naming Consistency5/5

All tool names use lowercase snake_case and follow a consistent verb-first pattern (move_forward, turn_left, list_connected_devices). Minor variance like 'robot_status' is still in the same style, so the set feels predictable.

Tool Count5/5

With 8 tools, the server is well-scoped for EV3 remote control and status monitoring. Each tool addresses a necessary operation without redundancy or bloat.

Completeness4/5

The core movement and status operations are covered, including safety stop and device enumeration. A minor gap is the lack of direct sensor reading (e.g., reading a sensor value), but the current surface handles common remote-control workflows.

Maintenance

ActivitySlowing
ResponsivenessNo issues