Installation
Several ways in: the MCP Registry one-liner (recommended), a Docker container, or a manual Python install — plus HTTP transports for remote agents. Pick whichever matches your environment; all of them speak the same MCP protocol.
22.
Prerequisites
- Python 3.10+ — only if you're not running via Docker.
- MikroTik RouterOS device with API/SSH access enabled.
- Network reachability from your machine to the router's SSH port.
- An MCP-capable client — Claude Desktop, MCP Inspector, or any tool that speaks MCP.
MCP Registry (recommended)
MikroTik MCP is listed on the MCP Registry — a community catalog of MCP servers. Registry-aware clients (Claude Desktop, VS Code, Cursor) can install it in one command without hand-editing config files.
$ claude mcp add io.github.jeff-nasseri/mikrotik-mcp
The client fetches the server metadata from the registry, installs
mcp-server-mikrotik from PyPI, and prompts you for the required environment variables —
MIKROTIK_HOST, MIKROTIK_USERNAME, and MIKROTIK_PASSWORD.
mcp-server-mikrotik package.
Docker
The fastest path when you'd rather not touch a Python toolchain. The image bundles Python, the server, and all dependencies — you just provide credentials.
Official prebuilt image (GHCR)
A multi-arch image (linux/amd64 + linux/arm64) is published to the GitHub
Container Registry — pull it directly instead of building from source. You can substitute
ghcr.io/jeff-nasseri/mikrotik-mcp:latest anywhere mikrotik-mcp appears below.
# Latest release $ docker pull ghcr.io/jeff-nasseri/mikrotik-mcp:latest # A specific version (matches the PyPI / git tag) $ docker pull ghcr.io/jeff-nasseri/mikrotik-mcp:0.10.1
| Tag | Points to |
|---|---|
latest | The most recent release |
X.Y.Z | A specific released version (e.g. 0.10.1), aligned with the PyPI release |
X.Y | The latest patch of a minor line (e.g. 0.10) |
sha-<short> | A specific commit |
Build from source
$ git clone https://github.com/jeff-nasseri/mikrotik-mcp.git $ cd mikrotik-mcp $ docker build -t mikrotik-mcp .
Run with stdio (IDE / desktop client)
stdio transport is what Claude Desktop, Cursor, and most IDE integrations expect. The container reads MCP messages from stdin and writes responses to stdout — short-lived, one process per client.
{
"mcpServers": {
"mikrotik-mcp-server": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "MIKROTIK_HOST=192.168.88.1",
"-e", "MIKROTIK_USERNAME=sshuser",
"-e", "MIKROTIK_PASSWORD=your_password",
"-e", "MIKROTIK_PORT=22",
"mikrotik-mcp"
]
}
}
}
Run with SSE or streamable HTTP
For remote agents, REST consumers, and anything that isn't on the same machine.
Exposes a long-running HTTP server with a /health probe.
$ docker run --rm -p 8000:8000 \ -e MIKROTIK_HOST=192.168.88.1 \ -e MIKROTIK_USERNAME=sshuser \ -e MIKROTIK_PASSWORD=your_password \ -e MIKROTIK_MCP__TRANSPORT=sse \ mikrotik-mcp # Available at: # SSE → http://localhost:8000/sse # Streamable HTTP → http://localhost:8000/mcp # Health → GET http://localhost:8000/health
Environment variables
| Variable | Description | Default |
|---|---|---|
MIKROTIK_HOST | Device IP / hostname | 192.168.88.1 |
MIKROTIK_USERNAME | SSH username | admin |
MIKROTIK_PASSWORD | SSH password | (empty) |
MIKROTIK_PORT | SSH port | 22 |
MIKROTIK_INVENTORY | Multiple devices, written inline in YAML (or its JSON subset). Takes precedence over MIKROTIK_INVENTORY_FILE and the four variables above — see Inventory. | (empty) |
MIKROTIK_INVENTORY_FILE | Path inside the container to a YAML inventory file — mount it as a volume | (empty) |
MIKROTIK_MCP__TRANSPORT | Transport — stdio · sse · streamable-http | stdio |
MIKROTIK_MCP__HOST | HTTP listen address | 0.0.0.0 |
MIKROTIK_MCP__PORT | HTTP listen port | 8000 |
MIKROTIK_MCP__ALLOWED_HOSTS | Comma-separated Host-header allowlist for the HTTP transports (DNS-rebinding protection). Append :* to allow any port; set * to disable the check. | (empty) |
MIKROTIK_MCP__ALLOWED_ORIGINS | Comma-separated Origin-header allowlist for the HTTP transports. | (empty) |
MIKROTIK_PASSWORD as an environment variable makes it visible via
docker inspect. For safer alternatives, see
SECURITY.md.
Docker Compose
For a long-running, self-hosted service, use an HTTP-based transport (sse or
streamable-http) so MCP clients can connect over the network. The stdio
transport is meant for direct IDE integration where the client attaches to the process — not for a
standalone background service.
services: mikrotik-mcp: image: ghcr.io/jeff-nasseri/mikrotik-mcp:latest container_name: mikrotik-mcp restart: unless-stopped ports: - "8000:8000" environment: MIKROTIK_HOST: "192.168.88.1" MIKROTIK_USERNAME: "admin" MIKROTIK_PASSWORD: "change-me" MIKROTIK_PORT: "22" MIKROTIK_MCP__TRANSPORT: "streamable-http" MIKROTIK_MCP__HOST: "0.0.0.0" MIKROTIK_MCP__PORT: "8000"
$ docker compose up -d
Reachable at http://localhost:8000/mcp
(streamable HTTP) or /sse (if you set MIKROTIK_MCP__TRANSPORT: sse), and
GET http://localhost:8000/health returns OK.
Managing several devices
To manage a fleet, give the container an inventory instead of the four
single-device variables. The inventory is a YAML file on your host, and it must be
mounted into the container as a volume — the image ships /config as the
mount point. A mounted file is also the safer channel: unlike an environment variable, its
contents do not show up in docker inspect. A ready-to-copy template ships at the
repository root as
inventory.example.yml.
- title: office-core host: 192.168.88.1 port: 22 username: admin password: change-me region: NL tags: [branch] - title: branch-berlin host: 192.168.89.1 username: admin key_filename: /config/keys/id_ed25519
$ docker run --rm -i \ -v "$PWD/inventory.yaml:/config/inventory.yaml:ro" \ -e MIKROTIK_INVENTORY_FILE=/config/inventory.yaml \ ghcr.io/jeff-nasseri/mikrotik-mcp:latest
services: mikrotik-mcp: image: ghcr.io/jeff-nasseri/mikrotik-mcp:latest restart: unless-stopped ports: - "8000:8000" volumes: - ./inventory.yaml:/config/inventory.yaml:ro environment: MIKROTIK_INVENTORY_FILE: "/config/inventory.yaml" MIKROTIK_MCP__TRANSPORT: "streamable-http" MIKROTIK_MCP__HOST: "0.0.0.0" MIKROTIK_MCP__PORT: "8000"
If a mount is inconvenient, the inventory can go inline in MIKROTIK_INVENTORY
using YAML flow syntax (no escaped quotes; JSON also works, since it is a YAML subset), or via
the --inventory / --inventory-file entrypoint arguments. Inline always
wins over the file. When an inventory is set it takes precedence and MIKROTIK_HOST /
MIKROTIK_USERNAME / MIKROTIK_PASSWORD / MIKROTIK_PORT are
ignored — the entrypoint also stops applying its 192.168.88.1 default. Tools then
take a device argument naming the title to target; see the
Inventory reference.
mcpuser), so the
mounted file must be readable by that uid — chmod 644 inventory.yaml is usually
enough; and if inventory.yaml does not exist on the host when the container
starts, Docker silently creates a directory in its place — create the file before
starting the container.
Behind a reverse proxy
The HTTP transports apply DNS-rebinding protection by validating the request's Host
header. When the server is reached on a custom domain or non-localhost IP — e.g. through a reverse
proxy — requests to /mcp are rejected with HTTP 421 "Invalid Host header"
(while /health stays exempt) unless you allowlist that host.
environment: MIKROTIK_MCP__TRANSPORT: "streamable-http" MIKROTIK_MCP__HOST: "0.0.0.0" # comma-separated; append :* to allow the host on any port MIKROTIK_MCP__ALLOWED_HOSTS: "mcp.example.com, mcp.example.com:*" # Origin allowlist for browser-based clients MIKROTIK_MCP__ALLOWED_ORIGINS: "https://app.example.com"
- Append
:*to a host (e.g.mcp.example.com:*) to allow it on any port. - Set
MIKROTIK_MCP__ALLOWED_HOSTS: "*"to disable the host check entirely. - If left unset on a non-localhost bind, the check auto-disables (with a logged warning) so the server still works out of the box.
Manual install
If you prefer to run from a Python venv — useful for development and debugging.
# Clone the repository $ git clone https://github.com/jeff-nasseri/mikrotik-mcp.git $ cd mikrotik-mcp # Create & activate virtual environment $ python -m venv .venv $ source .venv/bin/activate # Windows: .venv\Scripts\activate # Install in editable mode $ pip install -e . # Run the server (stdio, default) $ mcp-server-mikrotik # Or with SSE / streamable HTTP transport $ mcp-server-mikrotik --mcp.transport sse $ mcp-server-mikrotik --mcp.transport streamable-http
CLI options
| Flag | Description | Default |
|---|---|---|
--host | MikroTik device IP / hostname | from config |
--username | SSH username | from config |
--password | SSH password | from config |
--key-filename | SSH key file (preferred over password) | from config |
--port | SSH port | 22 |
--mcp.transport | stdio · sse · streamable-http | stdio |
--mcp.host | HTTP listen address | 0.0.0.0 |
--mcp.port | HTTP listen port | 8000 |
--key-filename ~/.ssh/mikrotik_ed25519
and register the matching public key under /user ssh-keys on the device.
HTTP-based transports (sse, streamable-http)
expose a GET /health endpoint for health checks. This endpoint is not available in
stdio mode.
Testing the install
Once the server is up, the quickest sanity check is the MCP Inspector — it speaks MCP, lists every tool, and lets you invoke them by hand.
$ npx @modelcontextprotocol/inspector \ uvx mcp-server-mikrotik \ --host 192.168.88.1 \ --username admin \ --password "<PASSWORD>" \ --port 22
A browser tab opens with the tool catalog. Try mikrotik_list_vlan_interfaces with empty
arguments — if you get back a JSON list, you're in. Contributors can also run the project's
pytest suite against a RouterOS container; see
testing.md.
Integration test suite
The project ships a pytest integration suite that runs against a temporary RouterOS container, so you'll need Docker installed and running. Install the dev dependencies, then run the tests:
# 1. Install test dependencies $ pip install -r requirements-dev.txt # 2. Run the full suite $ pytest -v # Run only integration-marked tests $ pytest -m integration -v
pytest -v spins up a MikroTik RouterOS container, runs the integration tests (create,
list, and delete user), then tears the container down automatically. Tests are marked with
@pytest.mark.integration by default.
Context length optimization
MikroTik MCP ships 174 tools. At full verbosity the tool schema can occupy ≈ 54,000 tokens — more than the entire context window of many local LLMs (LM Studio, Ollama, and similar).
To keep that manageable, every tool carries a short title annotation
(per the MCP tool-annotations spec) plus a trimmed one-line description. The function signature
already carries full type information, so the description only needs to convey what the
tool does. This cuts the description token budget by roughly 75% — bringing the
schema for a 64k-token model from ≈ 54k → ≈ 48k tokens and leaving real room for
the conversation.
src/mcp_mikrotik/app.py to drop those categories entirely — the dropped tools never
reach the schema at all.
Developer notes
The title comes from the MCP Tool Annotations spec
(2025-03-26). Every tool is registered through an annotate() helper (in
src/mcp_mikrotik/app.py) that attaches a short human-readable title while preserving the
read-only / destructive / idempotent / open-world hints:
# before @mcp.tool(name="create_queue_type", annotations=WRITE) # after @mcp.tool(name="create_queue_type", annotations=annotate(WRITE, "Create Queue Type"))
When adding a tool, always pass a title via annotate() — e.g.
annotations=annotate(READ, "My New Tool") — rather than a bare constant like
annotations=READ, which omits the title. See the
contributing guide for the full tool-authoring workflow.