Skip to main content
Glama
marcinn2

OpenSprinkler MCP server

README.md
# OpenSprinkler MCP Server

MCP server for managing OpenSprinkler controller via Claude Desktop or any MCP-compatible client.

Restriction: Only one controller can be managed at a time. This can be changed in the future if needed, but for now it is a limitation of the design.

[![OpenSprinkler MCP Server – quality and maintenance score on Glama](https://glama.ai/mcp/servers/marcinn2/opensprinkler-mcp/badges/card.svg)](https://glama.ai/mcp/servers/marcinn2/opensprinkler-mcp)

## Available Tools

| Tool | Description |
|------|-------------|
| `get_controller_status` | Get controller status (device time, enabled state, rain delay) |
| `get_stations` | Get all stations with their current status |
| `run_station` | Start a station for a specified duration |
| `stop_station` | Stop a running station |
| `stop_all_stations` | Stop all running stations immediately |
| `set_rain_delay` | Set rain delay in hours (0 to clear) |
| `enable_controller` | Enable or disable the controller |
| `reboot_controller` | Reboot the OpenSprinkler controller |
| `view_logs` | Get watering history logs for a date range |
| `get_programs` | Get all watering programs |
| `get_program` | Get full details of a single program by ID |
| `add_program` | Add a new watering program |
| `delete_program` | Delete a watering program by ID |
| `get_options` | Get controller options and settings |

---

## Configuration

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `OPEN_SPRINKLER_HOST` | Hostname or IP address of OpenSprinkler | `localhost` |
| `OPEN_SPRINKLER_PASSWORD` | MD5 hash of the controller password | _(empty)_ |
| `OPEN_SPRINKLER_PORT` | Port number | `80` |
| `OPEN_SPRINKLER_TLS` | Set to `true` to use HTTPS | `false` |
| `OPEN_SPRINKLER_TIMEOUT_MS` | Abort a controller request after this many milliseconds | `10000` |
| `PORT` | HTTP port for SSE mode | `3000` |
| `HOST` | Interface to bind in SSE mode. Use `127.0.0.1` to accept only local connections | `0.0.0.0` |
| `MCP_AUTH_TOKEN` | Bearer token required on all HTTP requests (optional) | _(unset — no auth)_ |
| `MCP_ALLOWED_HOSTS` | Comma-separated `Host` header allowlist. When set, other hosts get `403` — this is what blocks DNS-rebinding attacks | _(unset — not validated)_ |
| `MCP_ALLOWED_ORIGINS` | Comma-separated `Origin` allowlist for browser clients. Requests with no `Origin` are unaffected | _(unset — not validated)_ |

### Secure Connection (TLS/HTTPS)

If your OpenSprinkler is behind a reverse proxy with HTTPS (e.g. nginx, Traefik), set:

```bash
OPEN_SPRINKLER_HOST=opensprinkler.example.com
OPEN_SPRINKLER_PORT=443
OPEN_SPRINKLER_TLS=true
```

The server will then connect via `https://opensprinkler.example.com:443`.

> **Note:** OpenSprinkler firmware does not natively support TLS — HTTPS requires a reverse proxy in front of the controller.

---

## Transport Modes

The server supports two transport modes, selected by a command-line argument.

### stdio (default)

Used when launched directly by an MCP client (e.g. Claude Desktop). No argument needed:

```bash
node dist/index.js
```

### SSE / HTTP (`--sse`)

Starts an HTTP server that exposes the MCP endpoint over Streamable HTTP (SSE). Use this when you want to run the server as a persistent network service — for example in Docker or Kubernetes — and connect to it from multiple clients or over the network.

```bash
node dist/index.js --sse
```

The server listens on `http://${HOST}:${PORT}/mcp` (default `0.0.0.0:3000`).

Connect your MCP client to: `http://<host>:3000/mcp`

### Bearer Authentication (HTTP mode only)

Set `MCP_AUTH_TOKEN` to require a bearer token on every HTTP request. If the variable is not set the server accepts all connections (useful for private networks or local use).

```bash
MCP_AUTH_TOKEN=mysecrettoken TRANSPORT=http node dist/index.js
```

Clients must send the header:

```
Authorization: Bearer mysecrettoken
```

Requests without a valid token receive `401 Unauthorized`. Tokens are compared in constant time. The stdio transport is unaffected — authentication is not applicable there.

### Network Hardening (HTTP mode only)

These tools can start and stop watering, disable the controller, reboot it and delete
programs, so anyone who can reach the HTTP port can do all of that. In HTTP mode the
server binds `0.0.0.0` by default and prints a warning at startup when no token is set.

For anything beyond a trusted private network:

```bash
MCP_AUTH_TOKEN=mysecrettoken \
HOST=127.0.0.1 \
MCP_ALLOWED_HOSTS=localhost:3000,127.0.0.1:3000 \
node dist/index.js --sse
```

- **`MCP_AUTH_TOKEN`** is the primary control — without it the endpoint is open.
- **`MCP_ALLOWED_HOSTS`** defends against DNS rebinding: a malicious web page can make a
  browser on your network reach `127.0.0.1:3000`, but the request still carries the
  attacker's hostname in the `Host` header, which the allowlist rejects.
- **`HOST=127.0.0.1`** keeps the listener off the network entirely. Leave it at the
  `0.0.0.0` default inside Docker/Kubernetes, where the container needs to accept
  connections from outside its network namespace.

### Legacy SSE (backport compatibility)

For testing with tools that implement the older MCP SSE transport (e.g. [`mcptools`](https://github.com/f/mcptools), older inspector versions), the server automatically handles legacy clients on the **same `/mcp` endpoint** — no extra flag needed.

**How the server distinguishes clients:**

| Request | Interpreted as |
|---------|---------------|
| `GET /mcp` (no `mcp-session-id` header) | Legacy SSE — opens an event-stream |
| `POST /mcp?sessionId=<id>` | Legacy SSE — client sending a message |
| `POST /mcp` (with or without `mcp-session-id` header) | Modern Streamable HTTP |

**Connecting a legacy client (example with `mcptools`):**

```bash
# Start the server
node dist/index.js --sse

# In another terminal
mcp connect http://localhost:3000/mcp
```

> **Note:** `SSEServerTransport` is deprecated in the MCP SDK and this compatibility layer is intended for testing only. Production clients should use the modern Streamable HTTP transport.

You can enable verbose debug output during testing by setting `NODE_ENV=development`:

```bash
NODE_ENV=development node dist/index.js --sse
```

This prints every request, response status, and session lifecycle event to stderr.

---

## Docker

See [`docs/docker/`](docs/docker/) for the full deployment guide, Docker Compose file, Claude Desktop configuration options, and troubleshooting steps.

---

## Kubernetes

See [`docs/kubernetes/`](docs/kubernetes/) for manifests. Adjust the `ConfigMap` and `Secret` for your environment, then:

```bash
kubectl apply -k docs/kubernetes/
```

---

## Claude Desktop (Node.js, no Docker)

```json
{
  "mcpServers": {
    "opensprinkler": {
      "command": "node",
      "args": ["/path/to/opensprinkler-mcp/dist/index.js"],
      "env": {
        "OPEN_SPRINKLER_HOST": "192.168.1.100",
        "OPEN_SPRINKLER_PASSWORD": "your_password"
      }
    }
  }
}
```

---

## Password

The OpenSprinkler firmware stores whatever string was written when the password was last set. The official app MD5-hashes the password before sending it, so the stored value is an MD5 hash — and `OPEN_SPRINKLER_PASSWORD` must be the **MD5 hash** of your password, not the plain text.

Generate it with:

```bash
echo -n "your_password" | md5
```

---

## Contributing

Contributions are welcome. Please open an issue or pull request on [GitHub](https://github.com/marcinn2/opensprinkler-mcp).

## Acknowledgements

This project is inspired by the work at [github.com/Lumeo-sd/opensprinkler-mcp-sdr](https://github.com/Lumeo-sd/opensprinkler-mcp-sdr). All code was automatically re-created from scratch using AI tools due to lack of LICENSE statement in the original repository.


## Disclaimer

- I am not affiliated with the manufacturers, sellers or any of the device brands. This plugin is a personal project that I maintain in my free time.
- Refer to the licence for more information.

TDQS

A3.7/5.0

Scored across 15 tools

Disambiguation4/5

Most tools target distinct resources and actions: stations, programs, rain delay, controller status, and logs are clearly separated. The only mild overlap is between get_controller_status and get_options, since both touch controller-level settings, but their descriptions are distinct enough to avoid serious misselection.

Naming Consistency4/5

The naming largely follows a consistent verb_noun pattern: get_* for reads, plus run_station, stop_station, set_rain_delay, enable_controller, and add/delete_program. The main inconsistency is view_logs instead of get_logs, and stop_all_stations is a slightly expanded variant of stop_station.

Tool Count5/5

Fifteen tools is at the upper end of the ideal range, but each tool covers a meaningful controller operation without redundancy. The count feels appropriate for the scope of an irrigation controller server, and no tools are mere duplicates.

Completeness4/5

The tool surface covers the core workflows well: station control, rain delay, controller status, logs, and program lookup/creation/deletion. The most notable gap is the lack of an update_program tool, so existing programs cannot be edited directly, though agents could work around this by deleting and re-adding.

Maintenance

ActivityMaintained
ResponsivenessNo issues