Skip to main content

legacy-switch-mcp

CI OpenSSF Scorecard PyPI License: MIT Python 3.11+ MCP server

Use at your own risk. This tool can change VLANs, reboot switches and factory-reset them, and a bad change can cut you off from the switch. It has been proven on hardware on exactly two models (USRobotics 7724, D-Link DGS-1100-08) and is fake-switch tested only elsewhere. Read the plan before applying it, list your uplinks in protected_ports, and keep the maintenance switch off unless you need it. Provided as is, without warranty (see LICENSE).

An MCP server for two old "smart" switch families that no other MCP covers: the USRobotics 7724 (24-port gigabit, port-based VLANs) and the D-Link DGS-1100 EasySmart series (802.1Q). Both are Cameo-built and share a UDP discovery protocol (see docs/PROTOCOL.md).

It reads state and makes changes the careful way: plan, then apply. A plan writes nothing; it shows the exact differences and the risks (for VLANs: which connected ports would stop reaching each other, which ports leave the management VLAN). Applying needs the plan id and confirm=true, re-reads the switch and refuses if it changed, saves a snapshot, writes, reads back to verify, and returns a rollback plan.

Install

No clone needed (needs uv). First create ~/.config/switch-mcp/switches.json and a password for each switch (see Setup below), then add the server to your MCP client:

claude mcp add switch -- uvx legacy-switch-mcp                                    # Claude Code
claude mcp add switch --env SWITCH_MCP_TOOLSETS=read -- uvx legacy-switch-mcp     # look-only (fewest tokens)

Claude Desktop, Cursor and other clients take the same command in their JSON config:

{"mcpServers": {"switch": {"command": "uvx", "args": ["legacy-switch-mcp"], "env": {"SWITCH_MCP_TOOLSETS": "read"}}}}

The package is on PyPI and in the official MCP Registry as io.github.Nebulous-Ideas/legacy-switch-mcp. To run unreleased code, use uvx --from git+https://github.com/Nebulous-Ideas/legacy-switch-mcp legacy-switch-mcp.

Tools

Tool What it does
list_switches, list_interfaces configured switches; IPv4 interfaces on this host
discover(interface), probe(host) UDP discovery (broadcast+multicast), or ask one switch by IP
get_state(switch) parsed state: VLANs, ports, mirror (and D-Link system/trunk)
read_page(switch, page), overview(switch) raw page data
backup_state, list_snapshots, describe_switch save the parsed state; list saved states; markdown description for a wiki
backup_config, backup_firmware, list_files download the configuration (both) / firmware (D-Link) file into the state directory, mode 0600; the content is never returned
plan_password, plan_ip, plan_reboot, plan_factory_reset, plan_restore_config maintenance plans (both vendors; reboot D-Link only). Applying needs the server started with SWITCH_MCP_ENABLE_MAINTENANCE=1
plan_vlan_group, plan_port, plan_mirror, plan_trunk, plan_system USRobotics changes (plan only)
dlink_plan_vlan, dlink_plan_vlan_delete, dlink_plan_pvid, dlink_plan_port, dlink_plan_mirror, dlink_plan_trunk, dlink_plan_trunk_delete, dlink_plan_system, dlink_plan_save D-Link changes (plan only)
apply_plan(plan_id, confirm=true) apply a plan from this server session

Setup

uv sync --extra dev && uv run pytest
mkdir -p ~/.config/switch-mcp && cp switches.example.json ~/.config/switch-mcp/switches.json   # then edit the addresses

Passwords are never tool arguments and are never returned. Provide each as SWITCH_MCP_PASSWORD_<NAME> (name upper-cased, - becomes _) or in ~/.config/switch-mcp/passwords.json ({"usr-switch": "..."}, mode 0600; a looser mode is refused).

Factory-default passwords (so a reset switch is usable): both vendors document admin as the default (USRobotics quick-install guide; D-Link EasySmart manuals, which also give 10.90.90.90 as the default D-Link address and 192.168.0.1 for the USRobotics). Change it after a reset. This repository contains no other credentials; tests use a throwaway pw.

Register with Claude Code only for switch work (every MCP server adds its tool descriptions to every request of the session):

claude mcp add switch -- uv run --project /path/to/legacy-switch-mcp legacy-switch-mcp

Keeping the session cost small

Only the tools that can be used are registered. Measured with all 33 tools registered the schema text was about 4,350 tokens; now:

Setup Tools Tokens per request (approx.)
default: read + files + a set for each vendor in switches.json, maintenance off 28 (both vendors) 2,450
default + SWITCH_MCP_ENABLE_MAINTENANCE=1 (adds the 5 maintenance plan tools) 33 2,850
SWITCH_MCP_TOOLSETS=read,files,dlink (D-Link work only) 23 1,800
SWITCH_MCP_TOOLSETS=read (look only, no plan or apply tools) 10 600
SWITCH_MCP_TOOLSETS takes a comma list of read, files, usr, dlink, maintenance, or all; apply_plan is registered whenever any plan set is. A typo is an error, not a silent fallback.
Maintenance plan tools are not registered at all unless the maintenance switch is on (or maintenance is named in SWITCH_MCP_TOOLSETS); a registered one still refuses to apply without the switch.
For day-to-day looking: claude mcp add switch --env SWITCH_MCP_TOOLSETS=read -- uv run ...; for a change session leave it unset; for maintenance add --env SWITCH_MCP_ENABLE_MAINTENANCE=1.

Testing

uv sync --extra dev && uv run pytest            # 118 tests against the fake switches; no hardware needed
uv run python scripts/e2e_check.py <dlink-switch> <trunk-group> <port> <port>   # real MCP stdio check against a D-Link, on IDLE ports only

e2e_check.py starts the server as a client would, round-trips a system-location change and a trunk on the two idle ports you name (each rolled back), downloads a config, and checks that a reboot plan is refused while maintenance is off. Run it only against a switch where those ports have no link; it leaves the switch as it found it. Hardware-only checks (outages, factory reset, password) are in scripts/maint.py, scripts/rotate_password.py and scripts/recover_after_reset.py; see docs/ADDING-SWITCHES.md for the proof sequence.

Safety

See also SECURITY.md (threat model: plain-HTTP switches on a trusted management network; how to report a problem; supply-chain measures).

  • Protected ports: list uplinks in switches.json ("protected_ports": [24]); plans that touch them are refused unless allow_protected=true.
  • Rollback survives failure: the rollback plan is written to disk before any write, so if the switch stops answering mid-change it can still be applied from a new session.
  • Where a switch comes back. After a factory reset (or if you lose track of a changed address) the switch returns to its vendor default: D-Link 10.90.90.90, USRobotics 192.168.0.1, both with the password admin (both proven on hardware). To reach it, put a host on the same layer-2 segment in the same subnet. discover(interface) finds switches by MAC on the local segment even when their address is not the one you expect, so you can search before you guess. The tool only works on layer 2: it cannot reach a switch through a router.
  • Back up before the first write to a switch you cannot reach another way (backup_config, backup_state). A change that cuts the management path (a wrong address, VLAN or uplink) cannot be undone over the network; the way back is the default address above or a factory reset.
  • Audit log: every plan, refusal and apply is appended to ~/.local/state/switch-mcp/audit.jsonl (no credentials).
  • Maintenance is off by default. Password, IP address, reboot, factory reset and config restore plan normally but refuse to apply unless the server was started with SWITCH_MCP_ENABLE_MAINTENANCE=1 (claude mcp add switch --env SWITCH_MCP_ENABLE_MAINTENANCE=1 -- uv run ...). The low-level writers refuse too. Passwords are generated by the server, verified by logging in, stored in passwords.json (old one kept in passwords.prev.json; a passwords.pending.json exists while a change is in flight) and never shown or logged. An IP change refuses unless allow_ip_change=true, if the address answers a ping, or if it is outside the gateway's subnet, and updates switches.json only after logging in at the new address. Reboot, reset and restore drop every port, so they are refused while protected_ports exist unless allow_protected=true; a factory reset first saves the parsed state and the configuration file; a restore only accepts a file this server saved for the same switch (checksum checked).
  • Only ten D-Link CGIs (VLAN add/delete, PVID, port, mirror, system name/location/timeout, save, three trunk CGIs) and five USRobotics forms (VLAN group, port, mirror, trunk, system name/location/timeout) can ever be posted in normal operation. Behind the maintenance switch: five more D-Link CGIs (password, IP, reboot, reset, restore) and four USRobotics forms (password, system with IP, reset, restore); two D-Link CGIs only download a file. Firmware upgrade, 802.1Q-mode changes and anything else cannot be posted.
  • Plans live in server memory for 10 minutes; snapshots go to ~/.local/state/switch-mcp/.
  • One web session at a time (these switches evict a second login), always logged out. If a human has the web page open, close it first.
  • The switches apply changes live; they may not keep them across a power loss unless saved from their own web page.

Status

Feature State
Discovery (both vendors) proven on hardware
Reads (both) proven on hardware
USRobotics writes: VLAN group, port, mirror proven on hardware (round trips, switch left as found)
USRobotics write: trunk proven on hardware 2026-11-08 (ports 17+18, both idle; answers in ~11 s, so the write has a 45 s timeout)
USRobotics write: system name/location/timeout proven on hardware 2026-11-08 (location set and restored; the page also holds the IP settings, which are read back and re-sent unchanged, and did not change)
D-Link writes: VLAN add/delete, PVID, port, mirror proven on hardware 2026-11-08 (unused port 8, VLAN 999; every write verified by read-back and rolled back, switch left identical to its backup)
D-Link writes: system name/location/timeout, save configuration proven on hardware 2026-11-08 (location set and restored; save has no read-back)
D-Link writes: trunks proven on hardware 2026-11-08 (idle ports 6+7: trunking switched on, group created, then removed and trunking switched off; no ping loss on the other ports)
The MCP tool layer (33 tools over stdio) proven against a real D-Link with scripts/e2e_check.py (plan -> apply -> rollback through the registered tools, gate refusal, downloads)

Found on hardware (the fake switches could not have told us)

  • USRobotics login check was wrong: the reply to a wrong password has no login form (it only appears on the next page), so every password looked valid. Logins now fetch a real page and require it not to be the login form. This also made "verify the new password by logging in" meaningful for the USRobotics.
  • D-Link mirror disable: posting only cEn=0 to PortMirroring.cgi gets no answer at all and applies nothing (ReadTimeout, repeatable). The switch answers in 0.1 s when the form also carries the current sniffer port, mode and source ports. Fixed; the fake now hangs the same way so a regression fails a test.
  • USRobotics trunk: the write applies but takes about 11.4 s to answer, longer than the 10 s default timeout; it was reported as "lost contact" although it had worked. Trunk writes now wait up to 45 s.

Maintenance operations (behind SWITCH_MCP_ENABLE_MAINTENANCE=1)

Item State
Config download (USRobotics GET config.bin, D-Link backup.cgi with pswType) proven on hardware 2026-11-08, read-only. The D-Link file contains the admin password in clear text: it is stored 0600 and never returned
Firmware download (D-Link FWBackup.cgi, ~740 KB) proven on hardware 2026-11-08, read-only. The USRobotics web interface has no firmware page
USRobotics system name/location/timeout proven on hardware 2026-11-08 (location changed and restored; the IP settings were re-sent unchanged and did not change)
Password change (both) proven on hardware 2026-11-08 (run by the operator with scripts/rotate_password.py, verified: the old password is refused, the stored new one works)
IP change (both) proven on hardware 2026-11-08: D-Link 198.51.100.3 -> .4 -> .3 and USRobotics 192.0.2.3 -> .9 -> .3 (shown here as documentation addresses), each verified by logging in at the new address; switches.json updated, old address stopped answering
Reboot (D-Link; the USRobotics has no reboot page) proven on hardware: back in about 7 s, ~2 s of ping loss behind it, uptime reset, saved password survived (save the configuration first: dlink_plan_save)
Factory reset + config restore, D-Link proven on hardware: reset -> 10.90.90.90/admin -> upload the saved config file -> back at its own address with its rotated password and a state identical to the pre-test backup (~4 s of cluster-link ping loss)
Factory reset, USRobotics proven on hardware, but the recovery is different: after the reset, uploading the switch's own config.bin (it reboots and answers a timeout) restored none of the VLAN groups (only group 1 remained), flow control (back to Enable), name/location, address (192.168.0.1) or password (admin). Recovery means re-applying a saved state (backup_state) over the web pages: maintenance.reapply_usr_state, used by scripts/recover_after_reset.py usr-3com <state.json> --state. The same steps were done by hand on 2026-11-08 and the switch ended identical to its backup; the packaged function itself is fake-switch tested only
Config restore, USRobotics the upload is accepted and the switch reboots (~30 s), but see above: do not rely on it to restore settings after a reset

Operational notes from the outage tests

  • In the test setup the D-Link carried only a redundant second link and the USRobotics carried the main LAN plus the first link: a D-Link reboot/restore/reset cost a few seconds of ping loss on that link, a USRobotics reset/restore takes the whole LAN down for about a minute. Do one switch at a time.
  • After a USRobotics factory reset its password is admin again and the stored entry must say so before plan_password can rotate it (the recovery sets it; rotate again afterwards).
  • Reaching a reset switch at its factory address needs a temporary host route on the machine running the tools (ip addr add <x>/32 + ip route add <default>/32 dev <bridge>), not a /8 or /24 alias that would capture other traffic.

Not implemented

Item Why
Support for mcp 2.x planned: the SDK renamed FastMCP to MCPServer and changed other APIs, so the package pins mcp<2 for now. A migration branch will be tested against the fake switches before the pin is widened.
Firmware upgrade (D-Link FWUpgrade.cgi; the vendor utility uses TFTP) the upload protocol behind the button is unknown (the page only has buttons), and a bad image or a power cut during the write can brick the switch. backup_firmware keeps a copy of the running image.
The vendor utility's SOAP/UDP channel for writes (SetDeviceCfg, TFTP) not captured; the web channel covers everything above.
DHCP (instead of a static address), 802.1Q mode switch no need so far; the D-Link set_ip.cgi supports DHCP, which would make the switch's address unpredictable.

See docs/ADDING-SWITCHES.md for how to add another switch, another model or firmware, or a new vendor.

  • tplink-easysmart-mcp: TP-Link Easy Smart switches over their web CGI, with dry-run, protected ports and lockout-aware failures.
  • A second TP-Link Easy Smart server (tplink-easy-smart-switch-mcp, by qwe7002-ai) and a Netgear XS512EM server (netgear-xs512em, by supere989); find them by name on GitHub.
  • NetContext-MCP: SSH to older CLI switches (Cisco, Aruba) with legacy algorithms.

The gated-write pattern here (dry run, risk checks, session-eviction caveat) follows the same approach; ideas flow both ways.

See docs/PROTOCOL.md for the wire formats and the page/CGI map and docs/IDEAS.md for what was borrowed from the projects above and a sketch of a multi-vendor umbrella (an idea only).


A Nebulous Ideas project, built by the Nebulous Collective: the people and agents who work on it.

Metadata

Release files for legacy-switch-mcp 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for legacy-switch-mcp 0.1.2
File Size Uploaded Provenance Actions
legacy_switch_mcp-0.1.2.tar.gz 144.4 kB With provenance Details for legacy_switch_mcp-0.1.2.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for legacy-switch-mcp 0.1.2
File Interpreter ABI Platform Actions
legacy_switch_mcp-0.1.2-py3-none-any.whl Python 3 none any Details for legacy_switch_mcp-0.1.2-py3-none-any.whl

Total release size: 192.6 kB

Release files / legacy_switch_mcp-0.1.2.tar.gz

Download URL legacy_switch_mcp-0.1.2.tar.gz
Size 144.4 kB
Tags Source
SHA-256 checksum
How to use checksums
a62fc061e1ed053edf2d5c857a685a01aeae7cfe4abe4d4bbf94bfebf00bec65
BLAKE2b-256 checksum
How to use checksums
6334840d9c0d6c96baa8c0f7c065595604d45d2e42e18a360724fb2ffd24c175
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.

Transparency log

Release files / legacy_switch_mcp-0.1.2-py3-none-any.whl

Download URL legacy_switch_mcp-0.1.2-py3-none-any.whl
Size 48.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4b4ec85bb3fcea7eeaeed046c3d0c67549558d2bdeee78acac001211343eb0fe
BLAKE2b-256 checksum
How to use checksums
f2e21dad65bb32e762b3932fc8d5d4b01b5bbc8c7a234ba498ebecece7735029
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.

Transparency log

Security for legacy-switch-mcp 0.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for legacy_switch_mcp-0.1.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 11, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page