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.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.
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.
# 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>
| Tag | Points to |
|---|---|
latest | The most recent release |
X.Y.Z | A specific released version, aligned with the PyPI release |
X.Y | The latest patch of a minor line |
sha-<short> | A specific commit |
Build from source
$ 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.
{
"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_READ_ONLY | true exposes only tools that don't modify RouterOS or its file store | false |
MIKROTIK_SENSITIVE_HIDING | true redacts recognized credentials and private key material | false |
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_MCP__ALLOWED_IPS | Comma-separated client IP addresses and CIDR networks allowed to use the HTTP transports — see Restricting client IPs. | (empty; unrestricted) |
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/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"
$ 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/wiresage/mikrotik-mcp:latest
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.
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.
$ 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.
--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.
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:
environment: MIKROTIK_MCP__ALLOWED_IPS: "192.168.1.0/24" FORWARDED_ALLOW_IPS: "172.18.0.0/16"
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.
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/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
| 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 |
--read-only | Expose only tools that don't modify RouterOS or its file store | false |
--sensitive-hiding | Redact credentials and private key material from client-visible output | false |
--mcp.allowed-ips | Comma-separated client IPs / CIDR networks allowed to use the HTTP transports | (empty) |
--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 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.
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.
.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.