MikroTik MCP / Docs / Installation

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.

PRE-FLIGHT
You'll need a RouterOS device with SSH/API access enabled, a username with write policy, and (ideally) SSH key auth. Default SSH port is 22.

Prerequisites

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.

# One-line install for registry-aware clients
$ 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.

NOTE
If your client does not support registry-based install, use the Docker or manual methods below — both pull the same 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.

# pull the published image
# 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
TagPoints to
latestThe most recent release
X.Y.ZA specific released version (e.g. 0.10.1), aligned with the PyPI release
X.YThe latest patch of a minor line (e.g. 0.10)
sha-<short>A specific commit

Build from source

1 · Clone & build
$ 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.

~/.cursor/mcp.json  or  claude_desktop_config.json
{
  "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.

# SSE on port 8000
$ 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

VariableDescriptionDefault
MIKROTIK_HOSTDevice IP / hostname192.168.88.1
MIKROTIK_USERNAMESSH usernameadmin
MIKROTIK_PASSWORDSSH password(empty)
MIKROTIK_PORTSSH port22
MIKROTIK_INVENTORYMultiple 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_FILEPath inside the container to a YAML inventory file — mount it as a volume(empty)
MIKROTIK_MCP__TRANSPORTTransport — stdio · sse · streamable-httpstdio
MIKROTIK_MCP__HOSTHTTP listen address0.0.0.0
MIKROTIK_MCP__PORTHTTP listen port8000
MIKROTIK_MCP__ALLOWED_HOSTSComma-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_ORIGINSComma-separated Origin-header allowlist for the HTTP transports.(empty)
SECURITY
Passing 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.

docker-compose.yml
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"
# start it
$ 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.

inventory.yaml
- 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 with the mounted inventory
$ 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
docker-compose.yml — fleet variant
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.

TRAPS
Two mistakes the entrypoint catches with an explicit error rather than starting a server with no devices: the container runs as uid 1000 (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.

# allowlist the host(s) clients use to reach the server
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"

Manual install

If you prefer to run from a Python venv — useful for development and debugging.

# Source install
# 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

FlagDescriptionDefault
--hostMikroTik device IP / hostnamefrom config
--usernameSSH usernamefrom config
--passwordSSH passwordfrom config
--key-filenameSSH key file (preferred over password)from config
--portSSH port22
--mcp.transportstdio · sse · streamable-httpstdio
--mcp.hostHTTP listen address0.0.0.0
--mcp.portHTTP listen port8000
TIP
Prefer SSH keys over passwords for production. Pass --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.

# Inspector against a fresh install
$ 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:

# from a source checkout
# 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.

FURTHER
Need only a subset of tools (say, just DNS and WireGuard)? Comment out the unused scope imports in 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:

# src/mcp_mikrotik/app.py
# 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.