MikroTik MCP / Docs / Language server

Language server

routeros-lsp checks RouterOS scripts (.rsc) while you write them — in an editor, in Claude Code, or from the command line — and tells you what the router would reject: syntax errors, unknown menus, commands and parameters, and variable-scope mistakes, with completion and hover from the RouterOS command list.

SEPARATE
routeros-lsp is a sibling of MikroTik MCP, not part of the MCP server. It checks script text offline and does not need a router, so you can use either one without the other. Source: wiresage/routeros-lsp · MIT.

What it reports

Diagnostics are pushed when a document opens or changes. Each one carries its rule id as code and routeros-lsp as source.

AreaReports
SyntaxUnterminated strings, invalid escapes, unbalanced ( ) [ ] { }, two values with no space between them, whitespace before =, a # that is not a comment, two commands on a line without ;, characters RouterOS rejects, line breaks inside ( ), a byte order mark.
Variables:set of a name that is not declared in scope, reads of variables that are declared nowhere or whose scope has ended (including a local used on a later line of an imported file), invalid variable names, keywords used as names.
Menus and commandsAn unknown menu, command or parameter with a “did you mean”, ambiguous abbreviations, repeated parameters, too many unnamed arguments, missing required parameters. The menu path is tracked the way RouterOS does it, so relative commands after /ip address are checked in that menu.

The complete list, with default severities and sources, is on Diagnostic rules.

LABELLED
Where the scripting documentation is silent, or a real router behaves differently, the behaviour was measured on a router and written down in ambiguities.md rather than guessed. A finding that rests on it says so in its message, so a quirk of one RouterOS version is never presented as the language specification.

Install

Python 3.10 or newer, and nothing else — the server has no runtime dependencies.

# Install the command
$ pipx install routeros-lsp      # or: uv tool install routeros-lsp
$ routeros-lsp --version
# Or run it without installing
$ uvx routeros-lsp --stdio
# Try the unreleased master
$ pipx install git+https://github.com/wiresage/routeros-lsp.git

The server speaks LSP over stdio: routeros-lsp --stdio.

Editors and Claude Code

Claude Code

The plugin/ directory of the repository is a Claude Code plugin. It starts the server for .rsc files (language id routeros) and expects the routeros-lsp command on your PATH. Its .lsp.json tells Claude Code how to start the server:

plugin/.lsp.json
{
  "routeros": {
    "command": "routeros-lsp",
    "args": ["--stdio"],
    "extensionToLanguage": { ".rsc": "routeros" },
    "startupTimeout": 15000
  }
}

Settings go into the same file, under initializationOptions (see Configuration):

# e.g. treat files as one script, silence one rule
{
  "routeros": {
    "command": "routeros-lsp",
    "args": ["--stdio"],
    "extensionToLanguage": { ".rsc": "routeros" },
    "initializationOptions": {
      "scope": "script",
      "severities": { "vars/undeclared-read": "off" }
    }
  }
}

Any LSP client

Start the command below for the language id routeros from any LSP client.

SettingValue
Commandrouteros-lsp
Arguments--stdio
Transportstdio, JSON-RPC with Content-Length framing
Language idrouteros
File extension.rsc

The server writes nothing but protocol messages to stdout; log output goes to the client through window/logMessage.

What the editor gets

RequestProvides
textDocument/completionMenus and commands at the start of a line (global commands after :), parameters of the command being written (required ones first, flags without =), known values after name=, variables in scope after $, also inside strings.
textDocument/hoverDescription of a menu, command or parameter from the command database, with type, values and required parameters; where a variable was declared.
textDocument/documentSymbolFunctions (:global name do={ }), variables declared with :global and :local, and the menu path headers that divide an export into sections.
textDocument/foldingRangeBlocks, commands continued with a backslash, multi-line strings, runs of comment lines.

Check from the command line

The same analysis runs without an editor — for a pre-commit hook, a CI job, or a script an AI agent just wrote. A directory means every .rsc file below it, and - means standard input.

# Three ways to call it
$ routeros-lsp check backup.rsc scripts/        # files and directories
$ routeros-lsp check --strict --format json export.rsc
$ cat export.rsc | routeros-lsp check -         # standard input

Take this script:

guest-vlan.rsc
# Guest VLAN
:local vlanId 200
/interface vlan add name=vlan200 vlan-id=$vlanId interface=ether1
/ip adress add address=192.168.200.1/24 interface=vlan200
/ip pool add name=pool-200 ranges=192.168.200.10-192.168.200.100
/ip firewall nat add chain=srcnat action=masquerade # WAN
:set vlanId 201

Findings are printed as path:line:column: severity: message [rule], in file order. The summary goes to stderr, so stdout can be piped. Columns count characters, from 1.

# Messages shortened here
$ routeros-lsp check guest-vlan.rsc
guest-vlan.rsc:3:42: warning: $vlanId is a local variable of an earlier line … [vars/local-out-of-scope]
guest-vlan.rsc:4:5: warning: 'adress' is not a menu or command in /ip of the RouterOS 7.21.4 command database. Did you mean 'address'? … [schema/unknown-command]
guest-vlan.rsc:6:53: error: This command takes no unnamed arguments, and a '#' only starts a comment … [schema/unexpected-argument]
guest-vlan.rsc:7:6: error: ':set vlanId' needs 'vlanId' to be declared first … [vars/set-undeclared]
Checked 1 file: 2 errors, 2 warnings.
$ echo $?
1

With --format json the output is an array with one object per finding, carrying path, line, column, endLine, endColumn, severity, rule, source and message.

Exit status

StatusMeaning
0Nothing to fix.
1Errors were found. With --strict, warnings count too.
2A file cannot be read, or an option is wrong. The other files are still checked.

Options

The settings of the editor integration are command-line options, and go through the same validation. A wrong value is an error with exit status 2 instead of a line in a log.

OptionWhat it does
--strictWarnings fail the run too (exit status 1).
--format jsonJSON array instead of text lines.
--scope import|scriptHow variable scope is judged. See below.
--schema bundled|off|FILEThe command database; off turns the command checks off.
--severity RULE=LEVELChange or silence one rule. Repeatable.
--context-variable NAMEA variable RouterOS provides to the script. Repeatable.

routeros-lsp check --help lists them all. In CI, add --strict so warnings fail the job too.

Configuration

The server reads its settings from initializationOptions in the initialize request and from settings in workspace/didChangeConfiguration. The object can be given bare or nested under a routeros key. A change takes effect at once and every open document is checked again; anything the server does not recognise is reported on the client's log and ignored.

SettingValuesDefaultWhat it does
severitiesobject of rule id to error, warning, information, hint or off{}Changes the severity of a rule or turns it off. See Diagnostic rules.
scopeimport or scriptimportHow variable scope is judged. See below.
contextVariablesarray of names[]Variables RouterOS provides to the script, so they are not reported as undeclared. The DHCP lease script variables leaseBound, leaseServerName, leaseActMAC and leaseActIP are always included.
schemabundled, off or the path of a filebundledThe command database used to check and complete menus, commands and parameters.

Scope: import or script

ScopeUse it for
import defaultA file that is imported with /import, or pasted into the terminal. Every line is a scope of its own: commands separated by ; on the same line share it, and a block or a backslash join keeps its commands on one line. A :local on one line is gone on the next, and :set of it is a syntax error.
scriptThe source of a /system script, a scheduler or a netwatch: one scope for the whole text.

Severities

schema/unknown-command and schema/unknown-parameter are warnings by default, because the bundled database comes from a RouterOS CHR and some menus and parameters exist only on real hardware. Once you have a database generated from your own router (below), you can make them errors:

# settings
{ "severities": { "schema/unknown-command": "error", "schema/unknown-parameter": "error" } }

The command database

Menus, commands and parameters come from a database generated from a RouterOS 7.21.4 CHR with /console/inspect. A CHR has no wireless, LTE or switch-chip packages. To cover your own hardware, generate the database from your router — nothing is executed or changed on it; the tool reads /console/inspect and compiles probe lines with /import … dry-run. It takes a few minutes.

# From a clone of the repository
$ pip install paramiko
$ ROUTEROS_PASSWORD=... python tools/gen_schema.py --host 192.0.2.1 --user admin --out my-router.json.gz

Then point the schema setting at the file:

# settings
{ "schema": "/path/to/my-router.json.gz" }

How it works

TextParserBinder CheckerFindingsEditor Completion · hoverCommand database .rscsyntax treescopes vars · commandsrule · severitydiagnostics symbols · foldingRouterOS 7.21.4
Python 3.10+, no runtime dependencies. The JSON-RPC framing and the LSP handling are written by hand.
StageWhat it does
ParserHand-written, scannerless recursive descent, because RouterOS lexes by context: # is a comment only where a command starts, and { } is a block or an array depending on where it stands. It recovers from errors, so the rest of the file is still analysed.
BinderScopes, declarations and variable references, with the scope rules RouterOS applies: a scope per line in an imported file, and function bodies that see nothing of the code around them.
CheckersVariable rules, and command rules that track the menu path through the file as RouterOS does and resolve every word of a command line against the command database.
ServerLSP over stdio. A burst of edits is analysed once, for the newest text, and a document's analysis is cached by its revision and the settings.

Analysing a file takes about 0.3 s per 5,000 lines and 1.5 s per 20,000 lines on a laptop. The architecture notes have the rest.

Accuracy and limits

RouterOS can check a script without running it: /import file.rsc verbose=yes dry-run reports every error with line and column and executes nothing. That is the test oracle for the project. More than 800 recorded router answers are replayed by tests/conformance/, and the checker agreed with RouterOS 7.21.4 on 16,900 command lines generated from the database without reporting a single one that the router accepts. It does miss about 780 that the router rejects, almost all of the kinds below.

What is not checked

Not yet

Go to definition, references, rename, signature help, semantic tokens, code actions, formatting, several RouterOS versions at once, and following /import into other files. The plan is in proposal.md.

With MikroTik MCP

Typed tools cover the operations in the API reference. When a change is better shipped as a reviewable .rsc — a whole site config, an export you edited — check it first, then apply it through the same assistant.

Write
Your editor or AI agent drafts a .rsc script.
Check
routeros-lsp reports what the router would reject — in the editor, or with routeros-lsp check.
Apply
MikroTik MCP uploads and imports it: mikrotik_upload_file, then mikrotik_import_configuration.

In Claude Code you can have both: add the MCP server (Installation) and the plugin above, then ask:

# a prompt for a client that can run commands and use MCP tools
Write the guest VLAN as guest-vlan.rsc. Run routeros-lsp check on it and fix what it
reports, then upload it to the router and import it.
READ IT
A clean check means the script is well-formed, not that it does what you meant — and parameter values are not checked. Read the script before it runs; the import executes whatever the file contains.