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.wiresage/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/wiresage/mikrotik-mcp:latest anywhere mikrotik-mcp appears below.

# pull the published image
# Latest release
$ docker pull ghcr.io/wiresage/mikrotik-mcp:latest

# A specific version (matches the PyPI / git tag)
$ docker pull ghcr.io/wiresage/mikrotik-mcp:<version>
TagPoints to
latestThe most recent release
X.Y.ZA specific released version, aligned with the PyPI release
X.YThe latest patch of a minor line
sha-<short>A specific commit

Build from source

1 · Clone & build
$ git clone https://github.com/wiresage/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_READ_ONLYtrue exposes only tools that don't modify RouterOS or its file storefalse
MIKROTIK_SENSITIVE_HIDINGtrue redacts recognized credentials and private key materialfalse
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)
MIKROTIK_MCP__ALLOWED_IPSComma-separated client IP addresses and CIDR networks allowed to use the HTTP transports — see Restricting client IPs.(empty; unrestricted)
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/wiresage/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/wiresage/mikrotik-mcp:latest
docker-compose.yml — fleet variant
services:
  mikrotik-mcp:
    image: ghcr.io/wiresage/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.

Read-only and sensitive-hiding modes

Two flags narrow what the server exposes, independently of the RouterOS account you connect with.

# both also available as MIKROTIK_READ_ONLY / MIKROTIK_SENSITIVE_HIDING
$ mcp-server-mikrotik --read-only
$ mcp-server-mikrotik --sensitive-hiding

--read-only keeps modifying tools out of the MCP tool catalogue entirely. It limits the catalogue only — use a restricted RouterOS account for actual write protection; that account is the enforcement layer if another SSH client or a server defect bypasses the catalogue.

--sensitive-hiding redacts recognized passwords, passphrases, private and preshared keys, authentication and encryption keys, tokens, secrets, and SNMP communities from tool results, MCP notifications, exceptions, and server logs. It also omits download_file and generate_wireguard_client_config, whose payloads can't be safely field-redacted, and stops export tools from using RouterOS show-sensitive. Public keys, addresses, usernames, and SSIDs stay visible.

LIMIT
Redaction recognises labelled secrets. Arbitrary unlabelled secrets sitting in free-form log messages or comments can't be identified reliably, so don't treat --sensitive-hiding as a guarantee that nothing sensitive can ever reach a client.

Restricting client IP addresses

MIKROTIK_MCP__ALLOWED_IPS limits all HTTP endpoints — /mcp, /sse, /messages/, and /health — to specific IPv4 or IPv6 addresses and networks. Requests from anywhere else get HTTP 403. Leave it unset to keep access unrestricted. Entries are validated at startup, and CIDRs must use a network address — 10.0.0.0/8, not 10.0.0.1/8.

docker-compose.yml
    environment:
      MIKROTIK_MCP__TRANSPORT: "streamable-http"
      MIKROTIK_MCP__ALLOWED_IPS: "192.168.1.50, 10.20.0.0/16, 2001:db8::/48"

The allowlist uses the client address supplied by Uvicorn. For direct connections nothing else is needed. Behind a reverse proxy, set Uvicorn's FORWARDED_ALLOW_IPS to the proxy address or network so the allowlist sees the original address from X-Forwarded-For:

# trust only your proxy
    environment:
      MIKROTIK_MCP__ALLOWED_IPS: "192.168.1.0/24"
      FORWARDED_ALLOW_IPS: "172.18.0.0/16"
SECURITY
Only trust addresses your proxy controls. Setting FORWARDED_ALLOW_IPS=* lets direct clients spoof their source address through forwarded headers, which defeats the allowlist.

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/wiresage/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
--read-onlyExpose only tools that don't modify RouterOS or its file storefalse
--sensitive-hidingRedact credentials and private key material from client-visible outputfalse
--mcp.allowed-ipsComma-separated client IPs / CIDR networks allowed to use the HTTP transports(empty)
--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 196 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.

SCRIPTS
Handing the model a .rsc script instead of tool calls? Check it first with routeros-lsp, the language server for RouterOS scripts. It runs offline and needs no router.