openwrt-mcp
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., "@openwrt-mcpshow me the connected clients and recent firewall blocks"
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.
openwrt-mcp -- an MCP server for OpenWrt 25.12+
An MCP server that runs on an OpenWrt router, so Claude Code (or any MCP client) can inspect and change it -- behind deny-by-default policies, an audit log, and an automatic rollback for configuration changes.
Local and private. The server makes no outbound connections of its own and sends no
telemetry; it listens on loopback and on a root-only socket, and talks to your client over an
SSH session you set up. The tools that can make the router itself reach out (net_diag,
pkg_query with refresh, pkg_change, sysupgrade check, the uci_apply probes, ubus_call,
exec) say so in their MCP annotations (openWorldHint), and each needs a grant from you.
Secret values are masked in tool output by default, and a new WireGuard key never enters the
conversation unless you ask for it.
Footprint. One static binary with no dependencies: 9.1 to 11.3 MB for the router architectures (9.6 MB on arm64, about 3.7 MB compressed in the release archive), about 10 MB resident on a GL-MT6000 right after start, and 23 tools in about 22 KB of tool descriptions. Every release file is checksummed and carries a build attestation.
Based on GlassOnTin/openwrt-mcp by Ian Williams, which targets GL.iNet firmware 4.x (OpenWrt 21.02,
opkg). This is a port to stock OpenWrt 25.12 (apk, fw4/nftables, procd, netifd, LuCI JS). The security core is upstream's; see Credits for what came from where.
Requirements
Router: stock OpenWrt 25.12.0 or newer (the
apkreleases). Nothing to install on the router beforehand. 24.10 and older (opkg, 21.02-era tooling) are not supported -- use upstream for GL.iNet 4.x firmware.Your PC: OpenSSH (Windows 10 and later, macOS and Linux have it). Installing from a release needs nothing else. Building from source needs Go 1.26+,
sshandtar(Linux, macOS, or Git Bash on Windows).Any CPU OpenWrt and Go share: the release has a binary for each (arm64, armv5/v7, mips/mipsle softfloat, mips64/mips64le, x86, x86-64, riscv64);
install.shreads the router'sDISTRIB_ARCHand picks it, or cross-compiles when you build from source.
Tested on a GL.iNet GL-MT6000 (mediatek/filogic, aarch64_cortex-a53) with OpenWrt 25.12.5.
Nothing in the code is specific to that board.
Related MCP server: ssh-shell-mcp
How it differs from other OpenWrt MCP servers
From each project's own README on 2026-10-07; "not stated" means the README does not say. Corrections are welcome.
this project | ||||
Runs on | the router | a PC, in Docker | a PC, in Docker or Python | a PC, in Node.js |
Reaches the router by | SSH to a key-bound forced command, or loopback HTTP | SSH | SSH | LuCI's HTTP |
Changes configuration | yes, through one tool that checks first and arms a rollback | yes, including packages and a firmware flash | read-only unless | no |
Automatic rollback of a bad change | yes, and it survives a reboot | not stated | not stated | not applicable |
Access control and audit | per-client grants with scopes, expiry and a rate limit, audit log, optional TOTP | command whitelist, audit log | audit log | secrets hidden, no audit log |
Package manager |
|
|
| lists installed and available |
Flashing firmware | never | yes | not stated | no |
Needs on the router | the binary | SSH | SSH |
|
What changed from upstream
upstream 0.5 (21.02 / GL.iNet) | this port (25.12) | |
Packaging |
|
|
Web UI | GL.iNet oui-httpd page | LuCI page, Services -> MCP Server (read-only) |
Transport | HTTP over an | same, plus stdio over SSH via a key-bound forced command -- no tunnel to keep alive |
Rollback snapshot |
| on flash; an unconfirmed change is rolled back at the next start, including after a power cycle |
| set / create / delete | + |
Reload |
|
|
WireGuard | GL | stock |
Packages | -- |
|
Firewall | -- |
|
Services | -- |
|
Scope globs |
| brackets are literal: |
Presets | -- | `openwrt-mcp allow @readonly |
Tools
Tool | Scope | |
| tool | Model, release, uptime, load, memory, storage, temperatures, conntrack, every interface (state, addresses, default route, errors), radios/SSIDs, and what is outstanding. Call first. |
| tool | Whole log buffer filtered (substring, RE2, last N minutes) before the line limit. |
| tool | DHCP leases + static hosts + neighbour table + every AP's association list, joined by MAC: name, IP, SSID, signal, rates, connected time, lease. |
| tool |
|
|
| ping / traceroute / nslookup (optionally from a given interface), routes, policy rules, neighbours. |
|
|
|
|
| Stage, check, commit, reload, rollback armed. |
| tool | Make a pending change permanent / undo it now. |
| tool / | procd services; stopping or disabling dropbear, network, rpcd or openwrt-mcp is refused. |
| tool | installed, upgradable, search, info, files, owner, policy, |
|
| apk add/del/upgrade; simulates unless |
| tool | Every |
| live path | keep_current (drop the new default) or use_new (install it; rollback-armed for UCI configs). |
|
| list (preserved files), test (validate an image in /tmp), check ( |
| tool | Peers with handshake age, endpoint, traffic, config/kernel mismatch, duplicate names. |
|
| Keypair, next free address, peer saved and hot-added. The private key and config go to a root-only file; you collect them with |
|
| By name, key or section; refuses a peer connected in the last 3 minutes unless forced. |
| (ungated) | Discovery: every object, method and argument signature. |
|
| Anything else on the bus. Replies over 8 KB have long arrays pruned. |
|
| One program, no shell. A grant for |
| (ungated) | Opens the TOTP window for MFA-gated tools. |
Prompts. Clients that show MCP prompts (as slash commands, usually) get five recipes:
router-health, who-is-online, secure-my-router, wifi-doctor and upgrade-plan. Each is a
few lines that name the tools to call and end in a report or a proposal, never a change. They use
only what the @readonly preset grants, so a read-only client can follow them to the end, and they
cost nothing until used. Listing or fetching one runs nothing on the router, so no policy applies
and nothing is audited.
Install
On the router, from a release (nothing needed beyond a default 25.12 image):
wget -O /tmp/install-router.sh https://github.com/stanislav-testhub/openwrt-mcp/releases/latest/download/install-router.sh
sh /tmp/install-router.shIt picks the archive for the router's DISTRIB_ARCH, checks it against the release's
SHA256SUMS (refusing on a mismatch), and installs. VERSION=v1.2.0 selects a release other
than the latest. Each release file also carries a build attestation:
gh attestation verify <file> --repo stanislav-testhub/openwrt-mcp.
From a PC, building from source (needs Go, ssh and tar; Git Bash works on Windows):
git clone https://github.com/stanislav-testhub/openwrt-mcp
cd openwrt-mcp
ROUTER=root@192.168.1.1 SSH_PORT=22 SSH_KEY=~/.ssh/id_ed25519 ./install.sh installROUTER, SSH_PORT and SSH_KEY are your router's root login (defaults: root@192.168.1.1,
22, your ssh defaults). ./install.sh install --release [vX.Y.Z] skips the build and has the
router fetch a verified release instead; that is also what happens when no Go toolchain is found.
Either way the same installer runs on the router.
It installs:
/usr/bin/openwrt-mcp;the init script;
the default config, never overwriting an existing one;
the
keep.dentry;the LuCI page.
It then enables and starts the service.
It refuses to restart the daemon while a
uci_applyawaits confirmation, because that would roll it back, unlessFORCE=1.It checks that the binary fits on the overlay first.
./install.sh uninstall [--purge] reverses it.
Why not an .apk: 25.12 packages are apk-tools v3 ADB archives. Building one needs the
OpenWrt SDK or a host apk-tools with mkpkg (the router's apk has no mkpkg), and an
unsigned package then needs --allow-untrusted anyway. A static binary loses nothing by being
installed as files: /etc/config survives sysupgrade on its own, and /lib/upgrade/keep.d/openwrt-mcp
carries the binary, init script, rc.d links, state and LuCI page. (An owut/ASU image
rebuilds packages, not loose files -- the keep.d entry is what preserves this across one.)
Connect
openwrt-mcp connect (recommended)
The release has a binary for your PC too (openwrt-mcp_<version>_windows_amd64.zip, darwin_arm64.tar.gz
and so on; a Linux PC can use the linux_ archive). Unpack it anywhere on your PATH; it needs
OpenSSH, which Windows 10 and later, macOS and Linux have. Then, on your PC:
openwrt-mcp connect --client claude-code --host 192.168.1.1 # or claude-desktop, codex, cursor, gemini, vscodeIt makes a dedicated ed25519 key (~/.ssh/openwrt_mcp, never overwriting one), prints the two
commands to run on the router once, and shows the entry to add to your client. Add --write and it
does that last step: for Claude Code and Codex by running their own mcp add, for Claude Desktop,
Cursor, Gemini CLI and VS Code by merging one openwrt entry into their JSON file (other servers
stay; the original is kept once as <file>.before-openwrt-mcp; a file with comments is left alone and
the snippet printed instead). The entry stores ssh and its arguments as a list, so spaces in a key
path need no quoting, and it sets keep-alives so a router that rebooted is noticed.
openwrt-mcp connect doctor --host 192.168.1.1checks the chain in order and stops at the first break, with one fix for it:
[ ok ] ssh reachable 192.168.1.1:22
[ ok ] host key
[ ok ] key accepted /home/you/.ssh/openwrt_mcp
[FAIL] forced command the key logged in, but nothing answered as an MCP server (...)
fix: the key is probably authorized without its forced command ...
[skip] daemon socketThe steps are: SSH reachable, host key, key accepted, forced command, daemon socket, initialize,
tools/list. Wrong key, a missing forced command, a stopped daemon and a disabled socket each name
their own step. The weekly reboot disconnects the bridge; the client reconnects on its next start.
stdio over SSH, by hand
A dedicated key, bound on the router to the MCP bridge and nothing else:
ssh-keygen -t ed25519 -N '' -f ~/.ssh/openwrt_mcp
ssh root@router "openwrt-mcp authorize-key claude-code '$(cat ~/.ssh/openwrt_mcp.pub)'"
ssh root@router "openwrt-mcp allow claude-code @readonly 30d"
claude mcp add openwrt -- ssh -T -i ~/.ssh/openwrt_mcp -o BatchMode=yes root@routerauthorize-key writes a line like
command="/usr/bin/openwrt-mcp stdio --client claude-code",no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 ...
into /etc/dropbear/authorized_keys: that key gets the MCP server as client claude-code and
cannot open a shell or forward ports. It refuses a key that is already a root login.
The bridge pipes the session into a root-only unix socket (/var/run/openwrt-mcp/mcp.sock,
0600 in a 0700 directory). The daemon runs the tools, so a rollback armed in one session
outlives it. The client name on the socket is asserted, not proven -- sound because only root
can connect, and root can already edit the policy file; the SSH key is the real
authentication.
On Windows, the built-in OpenSSH client works for this (ssh.exe); use a full path to the key.
HTTP over a tunnel
Unchanged from upstream: openwrt-mcp pair <client> prints a bearer token once;
ssh -N -L 8730:127.0.0.1:8730 root@router; then
claude mcp add --transport http openwrt http://127.0.0.1:8730/mcp --header "Authorization: Bearer $TOK".
The daemon refuses to bind anything but loopback.
Make the policy mean something
If the agent can also run ssh root@router ... through its shell tool, the policy is
advisory. Deny that in the client (Claude Code: a permissions.deny rule for
Bash(ssh * root@<router>*)), or keep the root key where the agent cannot read it.
Policies
Deny by default. A policy grants one client a list of tools, scope globs (* and ? are
wildcards; brackets are literal), a calls/minute ceiling and an expiry. Changes to
/etc/config/openwrt-mcp take effect without a restart.
openwrt-mcp allow claude-code @readonly 30d # every read tool; ubus_call on an allow-list of read methods
openwrt-mcp allow claude-code @operator 7d # + uci_apply/confirm/rollback, service_control, pkg_change,
# pkg_config_resolve, wg_new/remove_client, sysupgrade backup
openwrt-mcp allow claude-code uci_apply 'dhcp.* wireless.*.disabled' 30d
openwrt-mcp allow claude-code service_control 'dnsmasq.restart adguardhome.*' 30d
openwrt-mcp revoke claude-code # remove every policy for the client
openwrt-mcp prune --older-than 7d # delete grants that expired over a week ago (audited)
openwrt-mcp policies | status [--all] | clientsGranting the same tools and scopes again replaces the client's expired grant instead of adding
a block. status counts expired grants in one line; --all lists them.
Neither preset includes exec or unrestricted ubus_call; both bypass the safety rails. The
read-only ubus list is explicit object.method pairs, not *.list-style globs, because
session.list returns LuCI session ids. uci_get '*' reads every config, but the values of
secret options (Wi-Fi keys, WireGuard keys, passwords, tokens) come back as <redacted>; see
Safety model. exec is not masked.
A refusal names the uncovered scope and prints the exact allow line that would cover it.
An exec scope is argv[0] taken literally, so a grant for find or awk looks harmless and
is a root shell: both run other programs, and so do the shells, env, nice, flock,
timeout, xargs, tar, ssh, lua, ucode, apk and opkg. allow refuses such a grant
until you add --shell-equivalent, judging by base name so /usr/bin/../bin/sh and /bin/*
count too; a ubus_call grant that can reach file.exec is treated the same way.
openwrt-mcp policies and status mark these grants, which also covers a policy written by
hand. The list is a floor, not a promise: a grant that writes files (wget, cp, dd) or
starts an interpreter that is not on it can still be abused.
Second factor
Unchanged from upstream: openwrt-mcp mfa enrol <client> prints an otpauth:// URI; then in a
policy list mfa_tools 'uci_apply', option mfa_window '15m'. One code opens a time-boxed
window; codes are single-use; unlocks live in memory only.
Wrong codes are limited: in the config server section, option mfa_max_failures '5' consecutive
wrong codes lock that client out of mfa_unlock for option mfa_lockout '300' seconds ('5m' also
works), doubling on each repeat to one hour. A correct code during the lockout is refused and not
spent. A client with no secret counts the same, so the limit reveals nothing about enrolment, and
the counters are in memory (a restart, mfa enrol or a rotated secret clears them). Each wrong code
is audited as ERROR and a lockout as DENIED; the code itself is never logged. The price: whoever
holds a client's token can keep that client locked out for up to an hour.
Safety model
Rollback that survives the thing most likely to happen.
uci_applysnapshots the touched configs to/etc/openwrt-mcp/rollback/<token>/(fsynced), writes the pending record before committing, commits, reloads, and arms a timer (default 90 s, max 600 s). Nouci_confirm-> files restored atomically, staged changes reverted, services reloaded. If the router is power-cycled inside the window, procd starts the daemon after network (S95) and it rolls back at startup, forcing aconfig.changefor each restored config.Nobody else's edits. Every committing tool refuses to run while the configs it would commit have uncommitted changes from another session, and while an unconfirmed apply is pending.
Checked before it reloads. The dry run runs each service's own checker on the staged config where one exists (
fw4 checkfor the firewall) and lists only the problems the change adds. A real apply refuses them unlessforce=true; a config that was already broken can still be fixed. dnsmasq, dropbear and the rest have no checker that sees staged changes: they are reported "not checked", not "passed".No silent overwrite.
uci_getends with the config's revision. Pass it back asexpected_revisionsand the apply is refused withCONFLICTif the config changed meanwhile (LuCI, another client).The management path is guarded. A change to the LAN interface or its bridge, the SSH listener, or the firewall zone and rule that let SSH in is refused unless the call names a
probe(ping the gateway, resolve a name) orforce=true. Probes run after the reload and are reported with the result; the rollback window defaults to 180 s for such a change. The rule set is static (LAN plus whatever dropbear names), so it covers the common layout, not every topology.History and restore. Confirming a change keeps the version of the config from before it (newest
history_keep, default 5, per config, in/etc/openwrt-mcp/history/).uci_get history=listshows them,history=diff:<id>compares one with now, anduci_apply restore=<id>puts one back through the same snapshot, checks and rollback timer. Entries contain secrets, like the snapshots:0600, masked when shown, and part of asysupgradebackup.option history_keep '0'turns it off.No self-promotion.
uci_applyandpkg_config_resolverefuse the openwrt-mcp policy config whatever the grant, so a client cannot write itself a wider policy. Policies change only throughopenwrt-mcp allow/revokeon the router.@operatoris powerful.uci_applyon*reaches every UCI config, includingdropbear,rpcdanduhttpd, andpkg_changeinstalls packages as root. Grant it for a working session (2h), and keep long-lived grants to narrow scopes such as'dhcp.* wireless.*.disabled'.Names are validated against UCI's grammar, so a section like
lan.ipaddr=xcannot make uci act on a different key from the scope the policy approved.No shell, anywhere. Every command is an argv; targets and names that could be read as options (
-f) are refused.Lifelines.
service_controlwill not stop or disable dropbear, network, rpcd or openwrt-mcp.sysupgradehas no flash action -- validate an image withtest, flash by hand.wg_remove_clientwill not drop a tunnel that handshook in the last 3 minutes unless forced.Credentials stay out of the conversation.
wg_new_clientwrites the new client's config (private key included) to a root-only file in RAM, beside the socket, and returns the public key and the command to run.openwrt-mcp wg-show <name>on the router prints the config and a QR code in your terminal and deletes the file; an uncollected file goes after 24 hours or at the next reboot. Onlyreveal=trueputs the key in the result, and so in the model's context and the provider's logs. Asysupgradebackup is created 0600 before anything is written to it and only the newest one is kept. The audit log records arguments and a summary, never tool output, and redacts secret-looking fields. The server private key is read fromwg show dumpand discarded.Secrets are masked in tool output.
uci_get,uci_apply(diff and errors),pkg_config_diff,pkg_config_resolve,uci_confirm,uci_rollback,system_statusandubus_callkeep the shape of a line and replace the value of a secret option (key,key1..key4,psk,password,sae_password,passphrase,private_key,preshared_key,token,pwd, anything containingsecret...) with<redacted>. The audit log uses the same rule.exec,logreadand the diagnostics are not masked, andwg_new_clientis exempt on purpose. In theconfig serversection:list redact_extra 'vendor_blob'adds names;option redact_output '0'turns masking off, and is audited. A masked diff does not show what changed in a secret.Router output is untrusted. Host names, SSIDs, log lines and DNS answers are chosen by other devices. Every result has terminal escape sequences, control characters, bidi and zero-width characters and invalid UTF-8 removed, and lines are cut at 1024 bytes (not
exec,ubus_call,wg_new_clientor the configuration tools).network_clients,logreadandnet_diagstart with an[untrusted text: ...]line. That lowers the odds a model obeys such text; it does not remove them, so keep write scopes out of sessions that read it.State stays off the web. The state directory, audit log and socket are refused if they sit under
/www, acgi-bindirectory or uhttpd'shome, even through a symlink.A threat model is in SECURITY.md.
Upstream's findings still hold: rpcd ACLs do not bind a root process on the local ubus socket, so they are not relied on; rpcd's own apply/rollback keeps its state in rpcd memory and tmpfs, which is why the snapshot is ours and on flash.
Verified
On a GL-MT6000, stock OpenWrt 25.12.5, installed with install.sh's steps and driven from
Windows exactly as Claude Code does (ssh.exe with a forced-command key, stdio bridge):
Install: procd service with respawn,
S95/K10links, socket0600in a0700directory, HTTP on127.0.0.1only, forced-command line in dropbear'sauthorized_keys,@readonly+ time-limited@operatorpolicies picked up without a restart.Reads against live data: all 23 tools listed with schemas and server instructions;
system_status,network_clients,wg_list_clients(duplicate-name warning),service_list,pkg_query upgradable,pkg_config_diff(11 real.apk-newfiles, settings-level diffs),firewall_show check,net_diag pingfrombr-WAN,logreadfiltering,uci_get,sysupgrade list,ubus_call.Writes with rollback (on a scratch UCI config):
uci_applyincludingset_list-> automatic rollback at the deadline, file restored byte-identical andpending.jsonremoved; a second apply refused while one is pending;uci_confirm;uci_rollbackon demand;dry_run.WireGuard:
wg_new_clientwith a preshared key (next free address, peer committed to UCI and hot-added to the kernel, config + QR, CGNAT endpoint warning), listed, thenwg_remove_client--/etc/config/networkback byte-identical, kernel peer gone.Crash recovery: the daemon
SIGKILLed with an unconfirmed apply -> procd respawned it after 6 s and it rolled the change back at startup.Refusals:
exec,ubus_call session.list, out-of-scopeuci_apply(each naming the grant line), anyuci_applyon the policy config, unauthenticated HTTP (audited).Output safety (1.1.0), on the same board:
uci_get wirelessandubus_call network.wireless statusreturn<redacted>for every Wi-Fikey, whilessid,encryptionand look-alike names such aswpa_disable_eapol_key_retriesare untouched (numbers, booleans and arrays in the JSON reply too). Auci_applydry run that sets a key shows it redacted and leaves nothing staged.network_clientsandlogreadstart with the untrusted-text marker. Five wrongmfa_unlockcodes from a client with no secret lock it out for 5 minutes (the sixth is refused with a retry time); the audit log records fourERRORentries thenDENIED, and none of the submitted codes appear in it. A syslog line written withubus call log writethat held CSI and OSC sequences, a colour code, BEL and a bidi override (all present in the router's own log) came back throughlogreadwith only the text and the tab left, and an over-long line was cut at 1024 bytes with a[+N bytes]note. The syntax ofuci changes(+=,-=,'\'', a bare-pathfor a delete) was captured from the router and is a test fixture.Reliable changes (1.2.0), on the same board, driven through the daemon's stdio bridge:
fw4 checksees changes staged in uci (it exits 0 for an invalid value and prints[!]lines, which is what validation reads): a dry run that sets a bogus zone value reports it as a NEW problem, the real apply is refused, and nothing is left staged or snapshotted. A staleexpected_revisionsis aCONFLICT; the current one passes; one for a config the call does not change is refused. A dry run on the LAN interface names the management path, and a real apply without a probe is refused with nothing staged.add_listof an element already insystem.ntp.serveris skipped. On a scratch option inluci: an apply with apingand aresolveprobe reports both OK after the reload and arms the rollback; confirming it leaves a history entry (0700directory,0600files);history=diffshows what the change added;restorebrings the file back byte-identical with its mode, runs its probe, and leaves an entry of its own; an apply with a failing probe reportsPROBE FAILEDwith the rollback still armed, anduci_rollbackrestores the file byte-identical.service_control restartof a daemon reports "Settled after" its state.Adoption and credentials (1.3.0), on the same board: the build was installed with
install.shandopenwrt-mcp versionreported 1.3.0.connect doctorfrom the Windows PC, with its real OpenSSH, passed all seven steps and listed 23 tools; with a key the router does not know it stopped at key accepted, and with a closed port at ssh reachable, each with its fix.connect --writefor Cursor made the key with the PC's ownssh-keygen, merged the entry into a JSON file, and a second run reported the entry unchanged.diagprinted the version, board, policy shape and audit lines with the SSH client's address asip-1and no audit arguments;--detailadded the masked summaries.sysupgrade backupcreated the archive-rw-------(4.3 MB, sosysupgrade -bkeeps the mode of a file that already exists), and the second backup removed the first. The daemon held about 10 MB resident right after the restart.Session cleanup (1.3.0), on the same board: after a daemon restart the stdio bridge started on the new build exited at once, and the client's next call opened a fresh session without an error; bridges still running the previous build stayed until their client's next request.
statusfolded 45 expired grants into one line;pruneremoved them, wrote onepruneaudit entry, and the daemon reloaded with the live grants only.
Unit and end-to-end tests (real MCP client over in-memory transport and over the bridge
handshake) run on any OS against a fake router: apply/confirm/rollback/timeout, restart
inside the window, dry run, refusal on foreign staged edits, list ops, WireGuard
add/list/remove against a captured 25.12 config, client join, log filtering, apk simulation,
.apk-new resolution with rollback, preset contents, scope glob semantics.
Not yet verified on hardware (covered by the fake-router tests only): pkg_change with commit,
the LuCI page rendering, the web-root guard on a real /www path or symlink, a real power cycle (the
recovery path is the same one the SIGKILL test exercises), and keep.d across a real sysupgrade. For
1.2.0 also: a management-path change applied with probes (only its refusal without one was run),
a probe failure left to the rollback timer instead of uci_rollback, and history across a
sysupgrade. For 1.3.0 also: the wg_new_client hand-over and wg-show (the test router's wg0 was
not up, and starting it was not part of the check), connect --write against a real Claude Code,
Codex, Claude Desktop, Gemini or VS Code (only the file merge and the command line are tested),
anything on macOS, and the release workflow and the installer's download path (a release has to
exist first). The file modes of the WireGuard hand-over are asserted on Linux in CI. Reports from
other boards are welcome.
Credits
GlassOnTin/openwrt-mcp (Ian Williams, MIT) -- the base of this project. Kept from it: the on-router static Go daemon under procd, standing deny-by-default policies with scope globs, rate limits and expiry, bearer tokens stored as digests, TOTP windows, the JSONL audit log with secret redaction, loopback-only HTTP, ubus reply pruning, and the confirm-or-roll-back idea for
uci_apply. The git history of the upstream project is preserved in this repository.jsebgiraldo/openwrt_ssh_mcp -- an off-router Python server (SSH per call, regex command whitelist), reviewed for ideas. Four were taken and reimplemented here:
validating a firmware image before flashing (
sysupgrade -T->sysupgradeactiontest);firmware and board information (part of
system_status);package management (rewritten for
apk:pkg_query,pkg_change);ping / traceroute / nslookup (
net_diag).
Not taken: OpenThread border-router tools (hardware-specific),
opkgandiptablescommands (gone in 25.12), a flash-firmware tool (irreversible and unrecoverable remotely), and regex whitelisting of shell strings (argv + policy scopes instead).
Contributing and security
Bug reports and pull requests are welcome -- see CONTRIBUTING.md. Please report vulnerabilities privately as described in SECURITY.md, not in a public issue. Planned work is in ROADMAP.md.
Reporting a bug: on the router run openwrt-mcp diag and paste the output into the issue. It
prints the version, board, daemon state, the shape of the policy and the last 20 audit lines
(--audit N), with every IPv4, IPv6 and MAC address, host name, DHCP name, SSID, domain and
WireGuard peer name replaced by a placeholder (ip-1, mac-1, host-1, ssid-1) that stays
the same for the same value. Audit arguments are never printed, and the scope, summary and error
text of an entry only with --detail, masked the same way. Names are matched against what the
router itself says they are, so read it once before pasting: a name that is only in free text, not
in the router's configuration, is not recognised.
Licence
MIT -- see LICENSE. Copyright (c) 2026 Ian Williams (upstream) and Stanislav Chupin (OpenWrt 25.12 port).
This server cannot be deployed
Maintenance
Related MCP Connectors
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
Remote streamable-HTTP MCP server running on a single Cloudflare Worker. Your assistant gets live Airbnb, Amazon, Booking.com, Google Flights, Maps and Reddit data, social search on X, Instagram and TikTok, the Meta Ad Library, and image/video generation without any keys. Connect your own accounts to let it send WhatsApp or Telegram messages, work an IMAP inbox, manage Meta Ads campaigns and publish to X and LinkedIn. OAuth 2.1 with PKCE; stored credentials are AES-256-GCM encrypted.
Remote MCP for Copilot CLI switch gate MCP, structured receipts, audit logs, and reviewer-ready evid
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
Related MCP Servers
- AlicenseAqualityFmaintenanceEnables AI agents to manage OpenWRT routers remotely via SSH, supporting system monitoring, network management, OpenThread Border Router configuration, and package management through natural language commands.1916MIT
- AlicenseNot gradedqualityDmaintenanceAI-native SSH orchestration server enabling Claude or any MCP agent to execute commands, manage files, tunnels, and fleet operations across multiple hosts with security policies and audit logs.2Apache 2.0
- AlicenseNot gradedqualityCmaintenanceProvides full administrative control of OpenWRT routers via SSH, with a preview-confirm safety model for mutating operations.13 npmMIT
- AlicenseAqualityDmaintenanceMCP server for monitoring and controlling WiFi routers (OpenWrt) via SSH, enabling device listing, status checks, WiFi configuration, rebooting, and more.717 npmMIT