Skip to main content
Glama
beagleprint-mcp

beagleprint-mcp

README.md
# beagleprint-mcp

[MCP](https://modelcontextprotocol.io) server for 3D printers connected to a **Mintion Beagle** ("Beagle Print")
print server. The printer (Marlin firmware) is attached to the Beagle over USB, and this server talks to the
Beagle's own local API: no cloud, no OctoPrint or Moonraker needed.

Any MCP client can use it to check the printer, look through the camera, query the firmware, upload G-code and
start, pause, resume or stop prints.

Unofficial project, not affiliated with or endorsed by Mintion.

## Tools

| Tool | Kind | What it does |
|---|---|---|
| `get_status` | read | Connection and print state (printing, idle, paused, finished), heating, fan, filament sensor |
| `get_progress` | read | Current or last job: file, percent, time left and elapsed, layer, height |
| `get_temperatures` | read | Current and target nozzle and bed temperatures |
| `list_files` | read | G-code files stored on the Beagle |
| `camera_snapshot` | read | Current camera picture (JPEG image) |
| `query_gcode` | read | Sends a **query** G-code (`M115`, `M503`, `M105`, `M114`, `M420 V`, `M851`...) and returns the firmware reply |
| `send_gcode` | **destructive** | Sends any G-code line (motion, heating, calibration, `M500`...) and returns the reply |
| `upload_file` | action | Uploads a local `.gcode` file to the Beagle (does not print it) |
| `start_print` | **destructive** | Starts printing a file stored on the Beagle |
| `pause_print` | action | Pauses the current print |
| `resume_print` | action | Resumes a paused print |
| `stop_print` | **destructive** | Cancels the current print |

Safety rules built into the server:

- `query_gcode` only accepts a fixed list of Marlin query commands with no arguments that change anything
  (see [`gcode.py`](src/beagleprint_mcp/gcode.py)). Everything else must go through `send_gcode`, which is marked
  as destructive so clients can ask for confirmation.
- While the printer is printing, only `get_*`, `list_files`, `camera_snapshot`, `pause_print` and `stop_print`
  work. The console, uploads and starting another print are refused.
- Tools carry MCP annotations (`readOnlyHint`, `destructiveHint`), so a client can auto-approve the read
  tools and ask before everything else.

## Requirements

- A Mintion Beagle on your network with the printer connected over USB.
- [uv](https://docs.astral.sh/uv/getting-started/installation/) (or Python 3.10+ and pip).

## Configuration

| Variable | Required | Default | Meaning |
|---|---|---|---|
| `BEAGLE_HOST` | yes | — | IP address or host name of the Beagle (shown in the Beagleprint app or your router) |
| `BEAGLE_USER` | no | `admin` | Beagle user (factory default) |
| `BEAGLE_PASSWORD` | no | `admin` | Beagle password (factory default) |

Check the connection:

```bash
BEAGLE_HOST=192.168.1.50 uvx --from git+https://github.com/ingenioustoad/beagleprint-mcp beagleprint-mcp --check
```

## Installation

### Claude Code

```bash
claude mcp add beagleprint -s user -e BEAGLE_HOST=192.168.1.50 -- uvx --from git+https://github.com/ingenioustoad/beagleprint-mcp beagleprint-mcp
```

To let the read tools run without asking while every action still asks, add this to `permissions.allow` in
`~/.claude/settings.json`:

```json
"mcp__beagleprint__get_status",
"mcp__beagleprint__get_progress",
"mcp__beagleprint__get_temperatures",
"mcp__beagleprint__list_files",
"mcp__beagleprint__camera_snapshot",
"mcp__beagleprint__query_gcode"
```

### Claude Desktop, Cursor and other clients

Add the server to the client's MCP configuration (`claude_desktop_config.json`, `.cursor/mcp.json`...):

```json
{
  "mcpServers": {
    "beagleprint": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/ingenioustoad/beagleprint-mcp", "beagleprint-mcp"],
      "env": { "BEAGLE_HOST": "192.168.1.50" }
    }
  }
}
```

Add `BEAGLE_USER` and `BEAGLE_PASSWORD` to `env` if you changed them on the Beagle.

## How it talks to the Beagle

The Beagle runs a small web server on port 80. This server uses the same endpoints as the Beagle's web page:

- **Commands**: `POST /set3DPiCmd` with a JSON body such as
  `{"pro": "get_prconnectstate", "cmd": 312, "user": "admin", "pwd": "admin"}`. The reply is JSON in the
  response body; `result` is `0` on success.

  | cmd | pro | Purpose |
  |---|---|---|
  | 312 | `get_prconnectstate` | State: `connect_state`, `print_state` (101 printing, 102 idle, 103/104 paused, 105 finished), `filament_open` |
  | 318 | `get_prgresp` | Progress: `file_name`, `progress`, `time_left`, `time_cost`, `layerIndex`, `printingHeight` |
  | 302 | `get_temp` | Temperatures: `tempture_noz`, `tempture_bed`, `des_tempture_noz`, `des_tempture_bed` |
  | 320 | `pri_file` + `operate_cmd: 120` | File list (`filesList`: `name`, `len`, `time`) |
  | 313 | `pr_start` + `filename` | Start a print |
  | 314 / 315 / 317 | `pr_pause` / `pr_resume` / `pr_off` | Pause / resume / stop |
  | 270 | `set_gcode_cmd` + `gcodeCmd` | Send one G-code line to the printer |

- **Upload**: `POST /WebUploadFormFile`, multipart with a single `file` field.
- **Camera**: `GET /images/snapshot0.jpg`.
- **Console**: a websocket on port **7685** with subprotocol `lws-serial-terminal` mirrors the serial line:
  `Send: ...` for what the Beagle sends to the printer and `Recv: ...` for the printer's answers. Command 270
  only acknowledges the request, so to read a reply the server opens the console first, sends the G-code,
  waits for its `Send:` echo and collects the `Recv:` lines up to `ok`. It skips the replies to the `M105`
  the Beagle sends every couple of seconds to poll temperatures.

## Development

```bash
uv sync
uv run pytest
```

## License

[MIT](LICENSE)