0x04. Give an AI assistant control of your MikroTik router over MCP (SSH)
0x00. Intro
In the previous MCP post I let an AI agent drive a Proxmox cluster. The same idea works on a router: hand the assistant a menu of “tools” it can call, and you can ask — in plain English — “show me the firewall rules” or “which DHCP leases are active right now” and get a real answer from the live device.
The glue is again MCP — the Model Context Protocol, a standard way to give an LLM a set of callable tools plus a transport to call them over. This time the tools wrap a MikroTik RouterOS device: list interfaces, read the firewall, inspect DHCP leases, add address-list entries, and so on. Connect a client like Claude Code or OpenCode, and the agent can query — and, if you let it, change — the router from a chat prompt.
A bit of vocabulary up front:
- RouterOS — the operating system on MikroTik routers. Everything is driven from a single CLI grammar (
/interface print,/ip firewall filter print, …), reachable over SSH. - Bearer token — a long random secret sent in an HTTP
Authorizationheader; whoever holds it is “authorized”. Think of it as a password for an API. - Reverse proxy — a small web server that sits in front of another service, terminates TLS, checks credentials, and forwards the request onward.
This is a generic, reproducible how-to: pick the right MCP, give it a locked-down router account, run it on localhost, put an authenticated TLS proxy in front, and connect a client. All examples use placeholders — user automation, router 192.168.88.1, MCP host mcp.example.com. Substitute your own.
Letting an LLM touch your router is exactly as sharp as it sounds. The whole design below is about keeping it under control: a dedicated least-privilege RouterOS user, key-only auth, a localhost-only server, a bearer-token proxy in front, and changes made only through safe-mode so you never lose control of a remote router.
0x01. Why SSH, not the REST API
RouterOS v7 does ship a REST API, and reaching for it first is tempting. Don’t. In practice the REST surface is incomplete and inconsistent across versions — plenty of menus are missing, some return awkwardly shaped data, and behaviour shifts between point releases. You end up fighting the transport instead of the router.
The SSH CLI, by contrast, is the complete and stable interface to RouterOS. Anything you can do on the device you can do over SSH, the grammar is uniform, and it doesn’t change under you. So the sensible choice for an MCP is one that talks to the router the same way you would — over SSH — rather than one bolted onto the flaky REST endpoint.
That’s the MCP we’ll use.
0x02. The MCP server
We’ll use jeff-nasseri/mikrotik-mcp — an open-source server written in Python on top of paramiko (a pure-Python SSH library). It opens an SSH session to the router and exposes the RouterOS CLI as MCP tools.
Two things make it a good fit:
- It supports three MCP transports —
stdio(local, one process per client),sse(legacy HTTP), andstreamable-http(the modern remote HTTP transport). We wantstreamable-httpso one running server can sit behind a proxy and serve remote clients. - It’s configured entirely through
MIKROTIK_*environment variables, so there’s no config file to babysit:
| Variable | Purpose | Default |
|---|---|---|
MIKROTIK_HOST | Router address | 192.168.88.1 |
MIKROTIK_USERNAME | RouterOS username | admin |
MIKROTIK_PASSWORD | Password (leave unset when using a key) | (empty) |
MIKROTIK_KEY_FILENAME | Path to the SSH private key | (empty) |
MIKROTIK_PORT | SSH port | 22 |
MIKROTIK_MCP__TRANSPORT | stdio / sse / streamable-http | stdio |
MIKROTIK_MCP__HOST | Bind address for HTTP transports | 0.0.0.0 |
MIKROTIK_MCP__PORT | Bind port for HTTP transports | 8000 |
MIKROTIK_MCP__ALLOWED_HOSTS | Host allow-list (DNS-rebinding protection) | (empty) |
Note the double underscore in MCP__* — that’s how nested settings are spelled in the environment.
0x03. A dedicated, least-privilege router user + SSH key
Do not point the MCP at admin, and do not give it a password. Create a purpose-built RouterOS user, restrict where it may connect from, and log it in with an ed25519 key only.
On the router, create the user. RouterOS ships a few default groups (read, write, full); pick the least that covers what you actually want the agent to do — read if you only want it to observe, write if it should also change config. Lock it to your management subnet with address= so the account is useless from anywhere else:
/user add name=automation group=read address=192.168.88.0/24 comment="AI MCP (read-only)"
Start with
group=read. Move towriteonly once you trust the setup — and even then, keep the address restriction.
Now give that user a key instead of a password. The full keygen / upload / import walkthrough is in the previous post on RouterOS SSH keys — the short version, run on the machine that will host the MCP:
# generate a dedicated unattended key (no passphrase for a service key)
ssh-keygen -t ed25519 -f ~/.ssh/mikrotik_automation -C "automation@mcp" -N ''
# copy the PUBLIC half to the router (note -O: RouterOS needs the legacy scp protocol)
scp -O ~/.ssh/mikrotik_automation.pub automation@192.168.88.1:
Then, on the router, import the public key against the new user (uploading alone does nothing — see article 0x03 for why):
/user ssh-keys import public-key-file=mikrotik_automation.pub user=automation
From here the private key ~/.ssh/mikrotik_automation never leaves the MCP host, and the router only holds the harmless public half. The MCP will point at the private key through MIKROTIK_KEY_FILENAME — no password anywhere.
0x04. Install and run the MCP server
On the MCP host (any Linux box with Python 3.8+), install into a virtual environment. The package is mcp-server-mikrotik:
apt update
apt install -y python3 python3-venv python3-pip
# isolated environment under /opt
python3 -m venv /opt/mikrotik-mcp
/opt/mikrotik-mcp/bin/pip install mcp-server-mikrotik
That installs a console command, mcp-server-mikrotik. Run it with the environment pointed at the router, the dedicated user, the private key, and the modern HTTP transport bound to localhost only:
MIKROTIK_HOST=192.168.88.1 \
MIKROTIK_USERNAME=automation \
MIKROTIK_KEY_FILENAME=/root/.ssh/mikrotik_automation \
MIKROTIK_PORT=22 \
MIKROTIK_MCP__TRANSPORT=streamable-http \
MIKROTIK_MCP__HOST=127.0.0.1 \
MIKROTIK_MCP__PORT=9100 \
/opt/mikrotik-mcp/bin/mcp-server-mikrotik
The important bit is MIKROTIK_MCP__HOST=127.0.0.1: the server is now reachable only from the box itself, never directly from the network. Confirm it:
ss -tlnp | grep 9100 # expect: LISTEN 127.0.0.1:9100 (localhost only)
To make it a proper service, drop the same environment into a systemd unit at /etc/systemd/system/mikrotik-mcp.service:
# /etc/systemd/system/mikrotik-mcp.service
[Unit]
Description=MikroTik MCP server (SSH)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment="MIKROTIK_HOST=192.168.88.1"
Environment="MIKROTIK_USERNAME=automation"
Environment="MIKROTIK_KEY_FILENAME=/root/.ssh/mikrotik_automation"
Environment="MIKROTIK_PORT=22"
Environment="MIKROTIK_MCP__TRANSPORT=streamable-http"
Environment="MIKROTIK_MCP__HOST=127.0.0.1"
Environment="MIKROTIK_MCP__PORT=9100"
Environment="MIKROTIK_MCP__ALLOWED_HOSTS=mcp.example.com,127.0.0.1,localhost"
ExecStart=/opt/mikrotik-mcp/bin/mcp-server-mikrotik
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now mikrotik-mcp.service
0x05. Securing it: TLS + bearer token with Caddy
There’s a catch with streamable-http: it has no built-in authentication. Anything that can reach the port can drive the router. That’s exactly why we bound it to 127.0.0.1 — and why we now put a reverse proxy in front to add the two things it lacks: TLS and an auth gate.
Caddy (a small, modern web server) does both. It terminates HTTPS on a public name and only forwards requests that carry the right bearer token; everything else gets a 401. Install it from the official repo:
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| tee /etc/apt/sources.list.d/caddy-stable.list
apt update
apt install -y caddy
Generate a token (32 random bytes) and hand it to Caddy through a systemd environment file at /etc/caddy/mcp_env:
# token → env file that systemd loads for Caddy
echo "MCP_TOKEN=$(openssl rand -hex 32)" > /etc/caddy/mcp_env
chmod 600 /etc/caddy/mcp_env
# make systemd load that env file for Caddy
mkdir -p /etc/systemd/system/caddy.service.d
cat > /etc/systemd/system/caddy.service.d/override.conf << 'EOF'
[Service]
EnvironmentFile=/etc/caddy/mcp_env
EOF
systemctl daemon-reload
Then the Caddyfile at /etc/caddy/Caddyfile. With a real DNS name, Caddy fetches a Let’s Encrypt certificate automatically; the @unauthorized matcher rejects any request without the exact bearer token, and everything valid is forwarded to the localhost MCP:
# /etc/caddy/Caddyfile
mcp.example.com {
@unauthorized {
not header Authorization "Bearer {env.MCP_TOKEN}"
}
respond @unauthorized 401 {
body "Unauthorized"
close
}
reverse_proxy 127.0.0.1:9100
}
systemctl enable --now caddy
One more knob to know about: mikrotik-mcp has DNS-rebinding protection — behind a proxy it may reject requests whose Host header it doesn’t recognise. If you hit that, add your proxy name to the allow-list via MIKROTIK_MCP__ALLOWED_HOSTS (already set in the systemd unit above, e.g. mcp.example.com,127.0.0.1,localhost).
A quick sanity check from the server itself:
# no token → rejected
curl -s https://mcp.example.com/mcp
# Unauthorized
# with the token → auth passes (a 406/400 here just means "now send proper MCP headers")
curl -s -H "Authorization: Bearer $(grep -oP '(?<=MCP_TOKEN=).*' /etc/caddy/mcp_env)" \
https://mcp.example.com/mcp
0x06. Connecting a client
The server now speaks Streamable HTTP behind TLS and a bearer token, so any MCP client that supports remote HTTP servers can use it. In both clients below the URL points at the Caddy name (https://mcp.example.com/mcp) and the token goes in an Authorization: Bearer header.
Claude Code
Claude Code reads MCP servers from a .mcp.json in the project root (for a machine-wide server, use the global ~/.claude.json). Keep the token in an environment variable so it never lands in the file:
{
"mcpServers": {
"mikrotik": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer ${MIKROTIK_MCP_TOKEN}"
}
}
}
}
OpenCode
OpenCode reads its config from opencode.jsonc (Linux/macOS: ~/.config/opencode/opencode.jsonc; Windows: C:\Users\<USERNAME>\.config\opencode\opencode.jsonc). Add an mcp block of type remote:
// ~/.config/opencode/opencode.jsonc
{
"mcp": {
"mikrotik": {
"type": "remote",
"url": "https://mcp.example.com/mcp",
"enabled": true,
"headers": {
"Authorization": "Bearer <MIKROTIK_MCP_TOKEN>"
}
}
}
}
Tip: rather than pasting the token into the file, OpenCode can read it from the environment — set the header to "Bearer {env:MIKROTIK_MCP_TOKEN}" and export MIKROTIK_MCP_TOKEN in your shell.
Restart OpenCode (it caches MCP connections on startup) and verify:
opencode mcp list
# Should show: mikrotik connected
0x07. Try it, and a safety close
With the client connected, ask in plain language and watch the agent translate it into RouterOS CLI calls over the SSH-backed MCP:
- “List the router’s interfaces and their status.”
- “Show the firewall filter rules on the input chain.”
- “Which DHCP leases are active right now?”
If you started with group=read, that’s the whole loop — a question in the client, a live answer from the router, nothing changed. Grant write later, deliberately, only once you trust the setup and have a way to review what the agent does.
A few rules worth keeping:
- Least privilege first. A read-only user answers most questions and can’t break anything. Widen scope only when you must.
- Keep the endpoint authenticated.
streamable-httphas no auth of its own — the bearer-token proxy is not optional. Bind the MCP to127.0.0.1and never let the raw port face the network. - Never expose it to untrusted networks. TLS + token on a trusted management network is fine; the open internet is not the place for a tool that can reconfigure your router.
That’s it: a prompt in the client, a real answer from RouterOS, and — when you choose — real changes, all through one authenticated endpoint, without opening WinBox or typing a single CLI command yourself.
Refs
- mikrotik-mcp (jeff-nasseri) — https://github.com/jeff-nasseri/mikrotik-mcp
- Model Context Protocol — https://modelcontextprotocol.io/
- MikroTik RouterOS — SSH access — https://help.mikrotik.com/docs/display/ROS/SSH
- MikroTik RouterOS — user management — https://help.mikrotik.com/docs/display/ROS/User
- MikroTik RouterOS — REST API — https://help.mikrotik.com/docs/display/ROS/REST+API
- Caddy reverse proxy — https://caddyserver.com/docs/