mikrotik-mcp
Allows running RouterOS commands on MikroTik routers over SSH, with profile-based configuration, read-only enforcement, host key verification, and support for key, password, or agent authentication.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mikrotik-mcpshow me the current interfaces and system uptime"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mikrotik-mcp
A Model Context Protocol server that runs RouterOS commands on MikroTik routers over SSH.
Node + TypeScript, no runtime toolchain of its own, and stateless: every tool call re-reads the config and opens and closes its own SSH connection. Nothing is cached, pooled or carried between calls.
It is also self-contained. A profile declares everything it needs, relative paths resolve against the config file, and nothing is inherited from the machine it happens to run on — no ssh-agent, no ~/.ssh/config, no ambient environment. Copy the config directory to a server or a container and it behaves identically.
It speaks JSON-RPC over stdio, so it drops into any MCP harness: Claude Code, Codex, opencode, Cursor, Zed, and anything else that can spawn a command.
Status: the config, policy and SSH layers are covered by tests, including real SSH handshakes against a local test server. It has not yet been run against physical MikroTik hardware — run
mikrotik-mcp --test <profile>against your own router first. Please report anything RouterOS does differently.
Use it
npx -y mikrotik-mcpThe published package is plain Node ESM with no runtime toolchain of its own — Node 18+ is enough.
Client | How to add it |
Claude Code |
|
Codex |
|
opencode |
|
Anything else |
|
If a profile uses password auth, the client also has to pass the variable holding it — see Getting a secret to the server.
Related MCP server: mikrotik-mcp
Quick start
1. Prepare the router
Create a dedicated RouterOS user rather than reusing admin. A read-only group is the real safety mechanism — the server's own readOnly check is only a guardrail.
/user group add name=mcp-read policy=ssh,read,test
/user add name=mcp-readonly group=mcp-read
/ip service enable sshThen give it a key (preferred) or a password:
# key auth — upload your public key first, e.g. with scp
/user ssh-keys import public-key-file=id_mikrotik.pub user=mcp-readonly
# or password auth
/user set mcp-readonly password="..."2. Write a config file
Start from config.example.json. For a first router, put this at ~/.mikrotik-mcp.json:
{
"profiles": {
"core": {
"host": "10.0.0.1",
"username": "mcp-readonly+ct",
"auth": { "type": "key", "path": "./keys/mikrotik" },
"description": "Core router"
}
}
}./keys/mikrotik resolves against the config file's own directory, not the working directory — so a folder holding the config and its keys can be moved anywhere and still work.
Everything else defaults: port 22, known_hosts verification, read-only, 20 s timeout. The +ct suffix tells RouterOS to drop colour codes and paging.
auth has no default and must be declared. That is deliberate: guessing at an agent or a conventionally-named key would make the config depend on the machine it runs on, which is exactly what this server avoids.
3. Trust the host key
The first connection will be refused, on purpose, because the router is not in known_hosts yet. Either add it the normal way:
ssh-keyscan -H 10.0.0.1 >> ~/.ssh/known_hosts…or, if you prefer to pin the key in the config, run the command once and copy the fingerprint out of the error message into hostKey.fingerprintSha256. Verify the fingerprint against the router itself (/ip ssh print shows the host key, or read it on the console) before trusting it.
4. Check it
npx -y mikrotik-mcp --check-configConfig search path (highest precedence first):
C:\work\.mikrotik-mcp.json
loaded C:\Users\you\.mikrotik-mcp.json
Profiles (1):
core
ssh mcp-readonly+ct@10.0.0.1:22
auth agent
host key known-hosts
writes refused (readOnly)
from C:\Users\you\.mikrotik-mcp.jsonThis exits non-zero if a file is unparseable or no profiles were found, so it works in CI too. It reads the same files the server does, but connects to nothing.
5. Prove it against the router
--check-config never connects to anything. To test the whole path — TCP, host key, credentials, RouterOS — run:
npx -y mikrotik-mcp --test coreConnecting to 'core' (mcp-readonly+ct@10.0.0.1:22) as agent...
host key SHA256:9lK… (accepted by policy 'known-hosts')
command /system identity print
exit code 0
took 412ms
name: MikroTik-core
OK.It runs one harmless read (/system identity print), prints FAILED. with the reason if anything goes wrong, and exits non-zero. Do this before wiring up an agent — it tells you whether a problem is in the config, the network, or the router.
6. Point your agent at it
Ask it to run /system resource print on core. It should call mikrotik_list_profiles first, then mikrotik_exec.
Configuration
defaults applies to every profile in the same file; a profile overrides it field by field.
{
"$schema": "https://raw.githubusercontent.com/denver6000/mikrotik-mcp/main/schema/mikrotik-mcp.schema.json",
"defaults": {
"username": "mcp-readonly+ct",
"auth": { "type": "agent" },
"timeoutMs": 20000
},
"profiles": {
"core": {
"host": "10.0.0.1",
"description": "Core router, main uplink",
"tags": ["core", "site-a"]
},
"branch-a": {
"host": "192.168.50.1",
"port": 2222,
"tags": ["branch", "site-b"],
"hostKey": { "policy": "pinned", "fingerprintSha256": "SHA256:abc..." }
},
"lab": {
"host": "10.20.0.1",
"username": "admin+ct",
"auth": { "type": "password", "passwordEnv": "MIKROTIK_LAB_PASSWORD" },
"readOnly": false
}
}
}Adding $schema gives you completion and validation in any editor that speaks JSON Schema.
Profile fields
Field | Default | Meaning |
| — | Hostname or IP. Required. |
|
| SSH port. |
|
| RouterOS user. Append |
| — | Required (on the profile or in |
|
| |
Relative paths in | ||
|
| Refuse commands that are not recognised as read-only. |
|
| Connect + command timeout. |
| — | SSH algorithm overrides for older routers. See below. |
| — | Free-form, searchable by |
Where the config is read from
Every one of these is loaded, highest precedence first. When two files define the same profile name the earlier one wins; profiles with different names merge into one list.
# | Path |
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
Which to use:
One personal file —
~/.mikrotik-mcp.json. Fine for most people; start here.Per-repo file —
.mikrotik-mcp.jsoncommitted alongside a network-automation repo, so anyone cloning it gets the right profiles. Safe to commit: it holds no secrets. Because parents are searched too, a file at the root of a monorepo covers every package under it.Both — the repo file wins for names it defines, and your personal file supplies the rest. This is how you override one router's settings locally without editing a shared file.
An explicit path —
$MIKROTIK_MCP_CONFIGfor CI, or to keep several environments in separate files:MIKROTIK_MCP_CONFIG=prod.json:staging.json.
Files are re-read on every tool call, so edits take effect without restarting the server or the agent. mikrotik_list_profiles reports which file each profile came from and flags any it could not parse.
Authentication
Every profile declares an auth block. There is no implicit fallback.
{ "type": "key", "path": "./keys/mikrotik" } // recommended
{ "type": "key", "path": "./keys/mikrotik", "passphraseEnv": "MTK_KEY_PASS" }
{ "type": "password", "passwordEnv": "MIKROTIK_LAB_PASSWORD" }
{ "type": "agent" } // opt-in; see the caveat below
{ "type": "agent", "socket": "/run/user/1000/ssh-agent" }Secrets never go in the config file. A profile names the environment variable holding a password or passphrase, and the value is read at connect time — so config files stay safe to commit and share.
Paths expand ~ and $VARS; anything still relative resolves against the config file's directory.
Key formats
ssh2 does not accept every key file OpenSSH can. Verified against ssh2 1.17:
Format | Works |
Anything | yes |
RSA PKCS#1 PEM ( | yes |
EC SEC1 PEM ( | yes |
PKCS#8 PEM ( | no |
In practice: generate keys with ssh-keygen -t ed25519 and you will never hit this. If you have a PKCS#8 key, --check-config names the problem and prints the conversion command rather than failing at connect time with "Unsupported key format".
Encrypted keys and the agent
Without an agent, an encrypted key's passphrase has to come from an environment variable — which runs into the subprocess problem below. So a self-contained setup in practice means a dedicated, unencrypted key file.
That is the standard automation answer, and it is safe here only because the blast radius is bounded elsewhere: the key belongs to a RouterOS user in a read-only group, it is used for nothing else, and it sits with chmod 600. On POSIX systems --check-config warns if the file is readable by group or others.
Agent auth remains supported for anyone who prefers it, but it is opt-in and not the default. It ties the profile to the environment the server was launched from: SSH_AUTH_SOCK is not always inherited by a subprocess spawned from a GUI application, and there is no agent at all in a typical container.
Getting a secret to the server
Your MCP client launches this server as a subprocess, so a variable exported in your interactive shell does not necessarily reach it. Either use agent or key auth — which need no variables, and are the reason agent is the default — or declare the variable in the client's own config. The field differs per client:
# Claude Code
claude mcp add mikrotik --env MIKROTIK_LAB_PASSWORD=... -- npx -y mikrotik-mcp# Codex — ~/.codex/config.toml
[mcp_servers.mikrotik]
command = "npx"
args = ["-y", "mikrotik-mcp"]
env = { MIKROTIK_LAB_PASSWORD = "..." }// opencode — note "environment", not "env"
{
"mcp": {
"mikrotik": {
"type": "local",
"command": ["npx", "-y", "mikrotik-mcp"],
"enabled": true,
"environment": { "MIKROTIK_LAB_PASSWORD": "..." }
}
}
}// generic mcpServers config
{
"mcpServers": {
"mikrotik": {
"command": "npx",
"args": ["-y", "mikrotik-mcp"],
"env": { "MIKROTIK_LAB_PASSWORD": "..." }
}
}
}Note the trade-off: this moves the password out of the router config and into the client config, which is usually not a file you want to commit either. Key or agent auth avoids the problem entirely.
Host key verification
On by default. A router whose key is unknown is refused, and the error prints the fingerprint so you can verify it and then trust it.
{ "policy": "pinned", "fingerprintSha256": "SHA256:abc..." } // self-contained; preferred
{ "policy": "known-hosts" } // default
{ "policy": "known-hosts", "knownHostsPath": "./known_hosts" }
{ "policy": "insecure-ignore" } // accept anything — lab use onlyUnder the default known-hosts policy, a known_hosts file beside the config is consulted first, then ~/.ssh/known_hosts as a fallback. Naming knownHostsPath explicitly uses that file only. For a config that must be portable, either ship a known_hosts next to it or pin the fingerprint — the personal-file fallback is a convenience for a workstation, not something to depend on. --check-config prints exactly which files will be consulted.
Setting fingerprintSha256 without a policy implies pinned. Hashed known_hosts entries, wildcards, [host]:port entries and negated patterns are all handled. A host that is known but presents a different key is a hard failure, never a prompt.
Older routers: legacy crypto
Modern SSH clients no longer enable the key exchange and host key algorithms that older RouterOS releases — or routers with strong crypto disabled — may be limited to. If the connection fails during handshake rather than at login, add what that router needs:
{
"host": "10.0.0.1",
"algorithms": {
"kex": { "append": ["diffie-hellman-group14-sha1"] },
"serverHostKey": { "append": ["ssh-rsa"] }
}
}append, prepend and remove are passed through to the SSH layer, as are plain arrays if you want to specify the whole list. Prefer upgrading RouterOS or enabling strong crypto on the router over weakening the client.
Read-only by default
Profiles are readOnly: true unless you say otherwise. Commands are classified before connecting: a command must contain a recognised read action (print, get, find, export, monitor, ping, …) and no recognised write action (set, add, remove, reboot, …). Every command in a ;- or newline-separated chain is checked, so a read cannot smuggle a write along with it. Anything the classifier does not understand is treated as a write and refused.
This is a guardrail against accidents, not a security boundary. A determined agent, or a command phrased in a way the classifier does not model, can get past it. For real enforcement, log in as a RouterOS user whose group grants read access only — the router is the only thing that can enforce that. Set
readOnly: falseon profiles where writes are intended.
Troubleshooting
Symptom | Cause |
| The config is not on the search path. Run |
A path shows | The file exists but is invalid JSON or breaks the schema; the reason is printed under Problems. Unknown keys are rejected, so check for typos like |
| Expected on first contact. Verify the printed fingerprint, then add it to |
| The router presented a different key than last time. Do not bypass this until you know why. |
| The client did not pass it to the subprocess — see Getting a secret to the server. |
| Intended. Set |
| Add an |
| PKCS#8 PEM. Convert it, or regenerate with |
| Either set |
| Wrong user, wrong key, or the RouterOS group lacks the |
Handshake fails before any login prompt | Algorithm mismatch with an older router — see Older routers: legacy crypto. |
A command fails but | RouterOS often reports errors in its output rather than through the exit status, so read the text, not just the status. |
Output is full of escape codes | Add |
Command line
Command | Purpose |
| Start the MCP server on stdio (what your client runs). |
| Show every config path checked, what loaded, and what each profile resolves to. Connects to nothing. Exits non-zero on a bad or empty config. |
| Connect to that router and run one harmless read. Exits non-zero on failure. |
| As expected. |
Tools
mikrotik_list_profiles
Search the configured routers. Returns connection details, which config file each came from, whether it is read-only, and any config problems. Never returns secrets — only the name of the variable holding one.
Input | Type | Notes |
| string, optional | Case-insensitive substring over name, host, description and tags. |
mikrotik_exec
Run one RouterOS command on one router and return its output.
Input | Type | Notes |
| string | Profile name. |
| string | e.g. |
| number, optional | Overrides the profile's timeout. |
Returns stdout, stderr, the exit code and the host key fingerprint, as text and as structured content. Because each call is a fresh connection with no shell state, commands must use absolute RouterOS paths (/ip address print, not print after a cd).
Develop
No extra toolchain: Node runs the TypeScript sources directly via built-in type stripping, so npm install is the whole setup. Node 22.18+ is required for development; the published package supports Node 18+.
npm install
npm run dev # run the server on stdio, straight from src/
npm test # unit tests + a real SSH round-trip against a local ssh2 server
npm run typecheck
npm run schema # regenerate schema/mikrotik-mcp.schema.json from the zod schema
npm run build # emit dist/ with tsc
npm run inspect # open the MCP Inspector against src/Layout
src/
index.ts CLI entry: arg handling + stdio transport
server.ts createServer() — registers every tool
config/
schema.ts zod schema for the config file, and the resolved profile type
paths.ts where config files are looked for; ~ and $VAR expansion
load.ts read, validate, merge, search
report.ts the --check-config report
ssh/
exec.ts one connection, one command, then close
knownHosts.ts known_hosts parsing and host key verification
policy.ts read-only command classification
keyFile.ts offline key inspection: format, fingerprint, permissions
testConnection.ts the --test <profile> probe
tools/
listProfiles.ts mikrotik_list_profiles
exec.ts mikrotik_exec
scripts/
generate-schema.ts zod schema -> JSON Schema
test/ node:test suites; helpers.ts builds a sandboxed config + envEvery entry point takes an injectable LookupEnvironment (env, cwd, home, platform) so tests never touch the real user's config or SSH agent.
Adding a tool
Add
src/tools/<name>.tsexporting aregister…Tool(server, e)function.Call it from
createServer()insrc/server.ts.Add a case to
test/server.test.ts.
Relative imports use explicit .ts extensions so Node can run the sources as-is; tsc rewrites them to .js on build. Keep stdout clean — it is the JSON-RPC channel. Log to stderr only.
Release
version lives in both package.json and SERVER_VERSION in src/server.ts; bump both.
npm test && npm run build
npm publish # prepublishOnly rebuilds dist/Only dist/, schema/, README.md and LICENSE are published.
License
MIT
Available Tools
2 toolsmikrotik_execRun a RouterOS command over SSHADestructive
Run a single RouterOS command on a configured router over SSH and return its output. Stateless: each call opens a fresh connection and closes it before returning, so there is no session, working directory or shell state carried between calls — send absolute command paths such as '/system resource print'. Profiles are read-only by default, in which case commands that would change the router are refused before connecting. Use mikrotik_list_profiles to find profile names.
| Name | Required | Description | Default |
|---|---|---|---|
| command | Yes | RouterOS command, e.g. '/system resource print' or '/ip address print detail'. | |
| profile | Yes | Profile name from mikrotik_list_profiles. | |
| timeoutMs | No | Override the profile's timeout for this call. |
Output Schema
| Name | Required | Description |
|---|---|---|
| ok | Yes | |
| stderr | Yes | |
| stdout | Yes | |
| exitCode | Yes | |
| durationMs | Yes | |
| hostKeyFingerprint | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The description adds substantial behavioral context beyond the annotations. It discloses statelessness (fresh connection per call, no session state) and the profile-based read-only enforcement that can refuse mutation commands. This complements the destructiveHint=true annotation by explaining the default safe path. No contradiction with annotations; readOnlyHint=false is consistent because profiles may be read-write.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is three sentences, each earning its place: purpose, statelessness, and profile guidance. It is front-loaded with the core action and immediately provides essential behavioral constraints. No redundancy or filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a tool with an output schema and moderate complexity, the description is sufficiently complete. It covers the command format, statelessness, profile selection, and the refusal behavior for read-only profiles. Minor gaps (e.g., error handling on SSH failure) are not critical given the output schema likely documents return values.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3. The description adds value by giving a concrete command example ('/system resource print') and emphasizing absolute paths, which clarifies the 'command' parameter. It also ties 'profile' to mikrotik_list_profiles, enriching the schema's minimal description. timeoutMs is adequately covered by the schema; no additional description needed.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: 'Run a single RouterOS command on a configured router over SSH and return its output.' The verb 'run' and resource 'RouterOS command' are specific, and it differentiates from the only sibling (mikrotik_list_profiles) by focusing on execution rather than listing. The statelessness note further clarifies scope.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly directs when to use this tool vs the alternative: 'Use mikrotik_list_profiles to find profile names.' It also provides critical usage guidance—'send absolute command paths'—and describes the conditional refusal behavior for read-only profiles, which tells the agent when a command might be rejected.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mikrotik_list_profilesList MikroTik router profilesARead-only
Search the configured MikroTik router profiles. Config files are re-read on every call. Returns connection details and whether each profile is read-only. Never returns secrets — passwords and passphrases live in environment variables, and only the variable name is shown. Call this first to discover the profile name that mikrotik_exec needs.
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Case-insensitive substring matched against name, host, description and tags. Omit to list all. |
Output Schema
| Name | Required | Description |
|---|---|---|
| issues | Yes | |
| sources | Yes | Config files that were loaded, in precedence order. |
| profiles | Yes | |
| searched | Yes | Every path consulted, whether or not it exists. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Beyond the readOnlyHint annotation, the description adds important behavioral detail: config files are re-read on every call, profiles can be read-only, and secrets are never returned—only environment variable names. This materially changes how an agent should interpret results and is exactly the kind of context annotations cannot convey.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Every sentence earns its place: scope, freshness, return contents, security behavior, and usage sequence are each covered without repetition. The most actionable instruction is front-loaded at the end but clearly tied to the tool's role.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple read-only list tool with one optional parameter and an output schema, the description is complete. It tells the agent what is returned, what is deliberately hidden, how to search, and how this tool connects to its sibling.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already documents the optional query parameter completely with 100% coverage, including case-insensitive substring matching and omission behavior. The description does not add further parameter-level detail, but the schema handles the burden, so the baseline score applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb ('Search') and resource ('configured MikroTik router profiles'), and immediately orients the agent by naming the sibling tool it feeds ('the profile name that mikrotik_exec needs'). This makes it easy to distinguish from mikrotik_exec without inspecting schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly tells the agent to 'Call this first' and explains the purpose: discover the profile name required by mikrotik_exec. This is clear when-to-use guidance with a named alternative, which is sufficient given the single sibling tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
2 tool updates
v0.1.0- First observed
mikrotik_exec - First observed
mikrotik_list_profiles
TDQS
Scored across 2 tools
mikrotik_list_profiles and mikrotik_exec have completely distinct purposes: one discovers connection profiles, the other executes RouterOS commands. The descriptions even cross-reference each other, making misselection very unlikely.
Both tools share a consistent mikrotik_ prefix and use snake_case, which is good. However, list_profiles follows a verb_noun pattern while exec is a bare verb without an object, a minor style inconsistency.
At two tools, the count is slightly below the typical 3-15 range, but the split is sensible: one tool for discovery and one for execution. Each tool earns its place for the server's narrow purpose.
For a server whose stated purpose is running RouterOS commands on configured profiles, the surface is complete: discover profiles, then execute commands. There are no obvious missing operations, and the stateless/read-only behavior is handled clearly.
Maintenance
Related MCP Connectors
- gatewayOAuthai.sealgate
MCP gateway with runtime security policy, tool-call-level control, and audit of agent actions.
Secure remote MCP for supported accounting workflows in authorized KROS companies.
Compliance frameworks (SOC 2, ISO 27001, CMMC, NIST, more) delivered to AI agents as MCP tools.
Scoped, audited SSH exec, sessions, and SFTP on your saved servers without exposing credentials
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables management of MikroTik routers running RouterOS 6 and 7 via SSH, Telnet, or API with automatic command adaptation. Provides over 46 MCP tools for device management, firewall, DHCP, VPN, configuration profiles, and more.3MIT
- AlicenseNot gradedqualityAmaintenanceTurns a MikroTik router into 310 MCP tools for AI-driven configuration over SSH, covering firewall, routing, VPN, and more.523 npm16MIT
- AlicenseNot gradedqualityDmaintenanceMCP server that sends CLI commands to MikroTik RouterOS via SSH. It allows executing any RouterOS CLI command and getting text output back.1-
- FlicenseNot gradedqualityCmaintenanceEnables agents to access network device management interfaces via SSH, keeping credentials on the server side. Supports both interactive terminal sessions and command execution on devices like Huawei VRP, MikroTik, and OpenWrt.1-