ensp-mcp
Provides tools for authoring, reading, and validating Huawei eNSP topologies as .topo files, and for interacting with running eNSP device consoles by capturing screens and injecting keyboard input.
This server does not currently have a deployable release on Glama. Follow the repository's setup instructions to install and configure it for your MCP client, including any required credentials and local dependencies.
ensp-mcp
An MCP server for Huawei eNSP (Enterprise Network Simulation Platform). It gives an agent three things eNSP does not expose on its own: the ability to author topologies as files, the ability to read and drive any running device's console, and the ability to talk to the router itself and get its exact output as text.
Everything here was reverse-engineered against a real install — eNSP
V1.3.00.100 with Oracle VirtualBox 7.2.18 on Windows 11. No eNSP SDK, no
undocumented DLL calls, no GUI automation: just the .topo format, the guest
agent's wire protocol, and the VirtualBox control interface eNSP already drives.
What was reverse-engineered
1. The .topo format
A topology is XML. It declares <?xml version="1.0" encoding="UNICODE" ?> —
and that declaration is a lie. The file must be UTF-8 without a BOM.
All 40 example topologies shipped with eNSP 1.3.00.100 are UTF-8, as are the
files eNSP itself saves, and an older release (V1.2.00.390) is the same.
Get this wrong and eNSP shows no error at all: it loads the topology as
zero devices and the status bar reads 总数:0. A UTF-16LE file — which is what
the encoding="UNICODE" attribute suggests, and what some third-party
generators emit — is silently discarded. This server wrote UTF-16 until that
was measured; ensp_read_topology now reports encoding, has_bom and
loads_in_ensp so the mistake is visible.
The <topo> element's version attribute is optional — plenty of shipped
examples have a bare <topo>. When present it is written to match the installed
eNSP (ensp_status reports it), falling back to 1.3.00.100.
<?xml version="1.0" encoding="UNICODE" ?>
<topo version="1.3.00.100">
<devices>
<dev id="UUID" name="R1" poe="0" model="AR2220" settings="" system_mac=""
com_port="2000" bootmode="0" cx="150.000000" cy="250.000000"
edit_left="177" edit_top="304">
<slot number="slot17" isMainBoard="1">
<interface sztype="Ethernet" interfacename="GE" count="1" />
<interface sztype="Ethernet" interfacename="GE" count="2" />
<interface sztype="Serial" interfacename="Serial" count="2" />
</slot>
<slot isMainBoard="0" id="3" type="521" />
</dev>
</devices>
<lines>
<line srcDeviceID="UUID-A" destDeviceID="UUID-B">
<interfacePair lineName="Copper" srcIndex="0" tarIndex="0" />
</line>
</lines>
<shapes>…</shapes>
<txttips>…</txttips>
</topo>Coordinates are floats (cx="150.000000"); the inline-editor offsets
edit_left/edit_top are integers.
The one non-obvious rule: srcIndex/tarIndex are indices into the device
model's flattened interface list, in the order the <interface> elements
appear — not slot numbers, not port numbers on the silkscreen. models.py
carries that index space per model.
2. The device console is a TCP service, not a screen
This is the finding that matters, and it corrects an earlier conclusion in this project (and in other write-ups of eNSP) that the console could only be reached by screenshotting the VirtualBox screen.
The VRP process runs inside the guest, and it serves a plain-text console on
guest TCP 40000. On a live eNSP host the console connection shows up in
netstat as:
TCP 192.168.56.1:51442 192.168.56.2:40000 ESTABLISHED <eNSP_VBoxServer>so the eNSP client itself talks to the router over this socket. Connecting to it directly returns the router's real output:
<Huawei>display interface brief
Interface PHY Protocol InUti OutUti inErrors outErrors
Ethernet0/0/0 down down 0% 0% 0 0
...
Ethernet0/0/8 down down 0% 0% 0 0
NULL0 up up(s) 0% 0% 0 0
<Huawei>That is exact text — no screenshots, no OCR, no window automation, and no elevation. It also removes the character-drop and paging problems that make screen-scraping unreliable.
Reading policy: text only. Device state is read over this socket and
nowhere else. The screen is never scraped, and the text console is never
allowed to fall back to it: if the console cannot be reached, or a command
does not come back, the call fails loudly (non-zero exit / ok: false)
instead of returning a picture or a boot banner. A freshly booted device
answers with Press any key to get started, and a single throw-away
newline is not enough to get past it — if that newline arrives before the
device reaches the banner it is lost, and the first command is then eaten as
the key press. The client therefore waits for a real prompt and verifies
that every command was echoed back before it reports success.
The VirtualBox virtual screen is a different thing: for an AR device it
shows the Tiny Core Linux guest (Core Linux, kernel 3.0.21-tinycore,
hostname box) sitting at a login prompt, because the router runs in a
separate process on that same machine. The screen tools
(ensp_console_screen / ensp_console_type / ...) therefore drive the guest,
not the router — they are kept as boot diagnostics ("did the guest come
up?"), and they are not a way to read device state.
3. How the guest agent talks to the host
The guest boots Tiny Core Linux and runs agentd static (see
/opt/bootlocal.sh inside the device image). agentd is the piece that
negotiates with the host, and it uses four channels:
# | Channel | Direction | Purpose |
1 | serial ( | both | configuration handshake |
2 | control ( | host → guest | start the VRP processes |
3 | media ( | guest → host | the guest dials back to the host |
4 | console ( | host → guest | the router's own VRP CLI |
Channel 1 — the configuration handshake. agentd writes a 4-byte
challenge htonl(rand()) to the serial port and then blocks reading the
reply. The reply is 28 bytes followed by 27 bytes:
Offset | Size | Meaning |
| 4 | the challenge, echoed verbatim (otherwise |
| 2 | media port, network byte order (the guest applies |
| 4 | the guest's own IP |
| 4 | netmask (0 ⇒ the guest skips |
| 10 | device id, first 10 bytes |
| 27 | device id, last 27 bytes — 37 bytes total, the "ssid" key |
The guest then configures eth0 itself with
ioctl(SIOCSIFADDR/SIOCSIFNETMASK/SIOCSIFFLAGS) using the IP we supplied, and
runs ip_test/ping_test. The only value that has to be correct is the
challenge; everything else is ours to choose.
Channel 2 — control. The guest listens on TCP 56789 and speaks fixed
44-byte frames: [u32 big-endian code][37-byte ssid][3 bytes padding].
Code | Direction | Meaning |
0 | guest → host | boot (carries the ssid) |
1 | host → guest | "go" — releases the start state |
2 / 3 | guest → host | prepare OK / failed |
4 / 5 | guest → host | DEV0 started / failed |
6 / 7 | guest → host | DEV1 started / failed |
8 / 9 | guest → host | DEV2 started / failed |
The socket must stay open for the life of the device: agentd registers
atexit(power_off) and joins its message thread, so closing the control
socket powers the whole guest off.
Channel 3 — the media server. This is the counter-intuitive one: the
guest connects back to the host on 192.168.56.1:<media port>, where the
port is the one we handed over in A[4:6]. send_ssid() then pushes every
SysV message-queue item of type 7 as a 244-byte frame
([240 bytes payload][37-byte ssid at offset 178]). With no listener the
guest logs "Failed to connect to remote media server: Connection refused."
and the SSID never reaches the host.
Device images. The router itself is three ELF binaries per device —
dev0, dev1, dev2 — unpacked at boot from dev_base_0/1/2 by agentd's
decompress(). They are genuine Huawei VRP (the image contains
hi3ms.huawei.com endpoints and a GVRP stack). Their resource.cfg pins the
interface sockets to a fixed address; if that address is not the one the guest
actually owns, VRP exits with Start port 0 fail /
VPORT_Hal_PortEnable failed!. scripts/patch_resource.py rewrites it.
Related MCP server: ENSP-MCP AI
Operations
The runbook — prerequisites, a lab in five minutes, verifying with ping, and a
troubleshooting table — is docs/operations.md.
Install
Requires Python 3.10+ and Oracle VirtualBox. eNSP itself is only needed for the runtime tools.
uv sync
uv run ensp-mcp # the MCP server
uv run enspctl --help # the command lineenspctl — drive eNSP from the command line
The MCP server is for agents. enspctl is the same thing for your shell: it
authors topologies, brings a whole topology up as a lab (one VirtualBox
machine per device, cloned from eNSP's own device image), and talks to the
routers.
$ enspctl topo create --spec spec.json -o lab.topo
wrote lab.topo (1767 bytes, utf-8)
2 devices, 1 links, loads_in_ensp=True
$ enspctl lab create demo --topo lab.topo --root ./demo --with-vms
lab 'demo' created in ./demo
R1 model=AR2220 vm=demo-R1 ip=192.168.56.101 pipe=\\.\pipe\enspctl-demo-R1
R2 model=AR2220 vm=demo-R2 ip=192.168.56.102 pipe=\\.\pipe\enspctl-demo-R2
demo-R1: linked clone from AR_Base/AR_Base_Link ...
demo-R2: linked clone from AR_Base/AR_Base_Link ...
$ enspctl lab up --root ./demo
patching resource.cfg ...
R1: 9 socket line(s) -> local 192.168.56.101, dest 192.168.56.1
R2: 9 socket line(s) -> local 192.168.56.102, dest 192.168.56.1
preparing the guest agent drivers ...
starting machines ...
demo-R1: starting
demo-R2: starting
R1: control: <- boot
R1: control: -> 1 (go)
R1: control: <- prepare OK
R1: control: <- DEV0 started
...
R1 started
R2 started
lab is up; press Ctrl-C to stop the drivers
$ enspctl run R1 "display version" --root ./demo
=== display version
Huawei Versatile Routing Platform Software
VRP (R) software, Version 5.130 (AR200 V200R003C00)
...
$ enspctl lab status --root ./demo
lab demo (./demo)
device vm ip state agentd vrp
R1 demo-R1 192.168.56.101 running yes yes
R2 demo-R2 192.168.56.102 running yes yes
$ enspctl lab down --root ./demoCommands
Command | Purpose |
| build a topology from a JSON spec |
| parse a topology into JSON |
| check a spec |
| create a lab (add |
| patch, start and drive the whole lab |
| start without rewriting the guest images |
| stop the machines |
| show the devices and whether they answer |
| rewrite each device's |
| stop and remove the machines |
| every eNSP guest on the host-only subnet |
| run VRP commands, get the exact text |
| interactive VRP session |
| shorthand for |
| the eNSP / VirtualBox environment |
| the device model catalogue |
| launch the eNSP application |
| run the agentd control handshake by hand |
That is the whole MCP surface except the guest-screen tools: ensp_console_screen,
ensp_console_type, ensp_console_key and ensp_console_send_lines stay on the MCP
server, because the screen is a bitmap of the guest's Linux console and must never be
used to read device state.
What a lab does
A lab owns a directory with the topology, one VirtualBox machine per device and a manifest. For every device it:
clones eNSP's own
AR_Basemachine, so the guest really is an eNSP device (the difference disk carries/dev/upload:agentd,resource.cfg, the VRP binaries);puts it on the host-only segment and assigns it a free address;
puts its COM2 UART in client mode, so VirtualBox dials a pipe server the lab runs — preferred over letting VirtualBox create the pipe, since the host then owns the instance and a guest that connects first cannot lock it out;
rewrites
resource.cfgso VRP's interface sockets point at the addresses the lab handed out — otherwise VRP exits withVPORT_Hal_PortEnable failed!. The rewrite is remembered next to the machine, solab updoes not pay for the (slow) image round trip twice, and the image keeps a stable name instead of growing a-patchedsuffix on every run. If a rewrite fails,lab upaborts instead of starting a lab whose guests cannot bind their sockets.
lab up then drives every guest the way the eNSP client would: the serial
handshake, the control frames, and the media server.
The lab's machines are not the ones the eNSP application manages, and they do not appear in the eNSP canvas.
enspctlis a parallel, scriptable path to the same devices.
NAT mode, for when the host-only adapter is broken
A host-only NIC in VirtualBox needs an NDIS filter. Once that filter stops working, every machine with a host-only NIC refuses to start:
VBoxManage.exe: error: Failed to open/create the internal network
'HostInterfaceNetworking-VirtualBox Host-Only Ethernet Adapter'
(VERR_INTNET_FLT_IF_NOT_FOUND)enspctl lab create ... --nic nat puts the devices on VirtualBox's NAT
instead. Every service the host needs — the VRP console, the agentd control
channel and the per-interface traffic ports — is forwarded with --natpf1 to a
port on the loopback, the guest's view of the host becomes the NAT gateway
alias 10.0.2.2, and resource.cfg's dest: is rewritten to match.
$ enspctl lab create mux --topo lab.topo --root ./mux --nic nat \
--subnet 10.0.2 --start-ip 101
$ enspctl lab up --root ./mux
R1: 9 socket line(s) -> local 10.0.2.101, dest 10.0.2.2
...
R1: started
$ enspctl run R1 "display version" --root ./muxThe price: the guest hard-codes 192.168.56.1 (the host address of the
media server and of the interface traffic), so the media channel does not work
behind NAT. The devices boot, stay up and answer on the text console, but the
layer-2 traffic between two devices still does not come up.
Pushing a whole configuration
$ enspctl run SW1 --file examples/mux-vlan/mux-vlan-sw1.cfg --root ./mux--file takes one command per line; blank lines and lines starting with #
are skipped.
Launch it from the project's own venv, not from uvx
uvx --from <dir> <entrypoint> looks like the obvious way to run this, and it
works — until you edit the source. uv builds a wheel, caches it under
%LOCALAPPDATA%\uv\cache\archive-v0\<hash>\Lib\site-packages\ensp_mcp\, and
reuses that cached build as long as the version number is unchanged. Editing
a module has no effect on a uvx-launched server, which is a genuinely
confusing failure: the process restarts, yet keeps running the old code.
The venv is an editable install, so pointing the client straight at it picks up source changes immediately:
- insert:
- id: mcp-ensp
name: '@deepseek-ai/dsh-mcp-client'
config:
serverName: ensp
transport: stdio
command: '<clone>\.venv\Scripts\python.exe'
args: ['-m', 'ensp_mcp']
toolCallTimeoutMs: 120000
failOnStartupError: false
reconnect:
enabled: true
maxAttempts: 10Replace <clone> with the checkout path. Any console script that runs the same
interpreter works too — for example uv pip install -e . plus
<clone>\.venv\Scripts\ensp-mcp.exe.
If you do prefer uvx, add --refresh so each launch rebuilds.
DSH applies cordis.patch.yml with patchReload: live, so this block takes
effect without restarting the web server.
Tools
Topology (offline, no eNSP required)
Tool | Purpose |
| device catalogue with interface index order |
| spec → |
|
|
| check a spec, warn about missing device images |
| launch the eNSP GUI on a file |
Runtime
Tool | Purpose |
| is eNSP/VirtualBox present, what is running |
| device VMs and their console channels |
Console — the real VRP CLI
Tool | Purpose |
| find the devices on the host-only subnet and their open ports |
| run VRP commands, get the router's exact text back |
| run the guest agentd control handshake and report what the device says |
Console — the VirtualBox screen (diagnostic only)
Tool | Purpose |
| capture the guest's screen; returns the PNG |
| type text (+ optional Enter) |
| send a named key ( |
| send a configuration block, one line at a time |
These drive the guest's Linux console, not the router. They are here to
answer "did the device boot?", and they are not a way to read device
state: the screen is a bitmap, and this project does not OCR it. Use
ensp_vrp_command for anything that talks to the router — it returns exact
text, it does not drop characters, and it is not affected by the paging
prompt.
// ensp_vrp_command(guest_ip="192.168.56.101", commands=["display version"])
{
"ok": true,
"output": "=== display version\nHuawei Versatile Routing Platform Software\n\
VRP (R) software, Version 5.130 (AR200 V200R003C00)\n..."
}Driving a device without the eNSP client
scripts/host_side.py replaces the eNSP client for one device: it is the
pipe server, the media server and the control client in one process.
# the device's UART must point at a pipe we create (see evidence/agentd-protocol.md)
VBoxManage startvm MYR2 --type headless
python scripts/host_side.py --pipe '\\.\pipe\MYR2-COM2' --guest-ip 192.168.56.101
python scripts/vrp_cli.py --guest 192.168.56.101 display versionscripts/patch_resource.py rewrites a guest's resource.cfg so VRP's
interface sockets point at an address the guest actually owns — without it
VRP exits with VPORT_Hal_PortEnable failed!.
The guest's payload format
The device software in /dev/upload/dev_base_0|1|2 is not compressed:
the guest's agentd decrypts it with a plain XOR against a 4096-byte key
that lives in agentd's .data section (the key restarts at the beginning of
every 4096-byte block). scripts/pack_payload.py takes the key straight out
of an agentd binary and encrypts or decrypts a payload file with it — a
round trip is byte-identical, which is how the format was confirmed.
python scripts/pack_payload.py key path/to/agentd
python scripts/pack_payload.py decrypt dev_base_2 dev2 --agentd path/to/agentd
file dev2 # ELF 32-bit LSB executable, Intel i386A worked example
$ python scripts/vrp_cli.py --list
192.168.56.2
192.168.56.3
192.168.56.4
192.168.56.5
192.168.56.101
$ python scripts/vrp_cli.py --guest 192.168.56.101 'display version' 'display clock'
=== display version
display version
Huawei Versatile Routing Platform Software
VRP (R) software, Version 5.130 (AR200 V200R003C00)
Copyright (C) 2011-2012 HUAWEI TECH CO., LTD
Huawei AR201 Router uptime is 0 week, 0 day, 0 hour, 1 minute
...
<Huawei>
=== display clock
display clock
2026-10-20 20:51:22
Tuesday
Time Zone(China-Standard-Time) : UTC-08:00
<Huawei>Verification
Three suites, all green:
Suite | What it covers |
| module level: catalogue, |
| the text console: the boot banner must not swallow a command, a command that did not run must fail loudly, and the text path must never touch the screen code |
| drives the server over JSON-RPC stdio like an agent does: |
mcp_client.py is the real acceptance test — it asserts the server advertises all
11 tools and that ensp_console_screen comes back as content types=['text', 'image']
with mimeType: image/png.
The tools were then exercised through the DSH harness itself, where they register
as mcp__ensp__*:
ensp_status→ reportsVBoxManage, the eNSP client path, running device VMs;ensp_list_devices→R1, withvrdp_port: 3389andserial_pipe_is_console: false;ensp_list_models→ 41 models with per-index interface lists;ensp_create_topology→ wrote a 6,274-byte UTF-16LE topology;ensp_read_topology→ read it back, resolving"GE0/0/1"to index 1;ensp_validate_topology→ warned thatCE6800needs an external image;ensp_open_topology→ rejected a missing path before spawning anything;ensp_console_screen→ returned the live 720x400 screen of a running AR clone;ensp_console_type/ensp_console_key/ensp_console_send_lines→ the typed text appeared on the guest's screen (box login: mcp).
Three bugs were found and fixed, and the third is the instructive one:
Port numbering. eNSP numbers ports continuously across the
<interface>elements that declare them, so AR2220's "GE x1" followed by "GE x2" is GE0/0/0, GE0/0/1, GE0/0/2. Numbering each group from zero produced a duplicate GE0/0/0 and shifted every link index after it.ensp_processesmatched itself. The server runs asensp-mcp.exe, so a prefix match on"ensp"madeensp_statusreportensp_running: truewith eNSP closed. It now matches exact process names.Wrong file encoding. The writer emitted UTF-16LE, copying a third-party generator instead of reading what eNSP produces. eNSP accepted the file silently and showed zero devices. Caught by opening a generated topology in eNSP on a second machine — the one thing the automated suites could not do, because eNSP requires elevation and refuses injected input from an unelevated process. The suites now assert the absence of a BOM, and
ensp_read_topologyreportsloads_in_ensp.
The lesson worth keeping: the test suites verified structure, round-tripping and the MCP protocol, but never that eNSP accepted a generated file. Format conformance is not the same as the consumer accepting the output.
Known limitations
The screen tools are diagnostic only.
ensp_console_screenis a bitmap of the guest's Linux console andensp_console_typetypes into that console, so they drop characters and give no text back. They are never used to read device state and never act as a fallback for the text console — useensp_vrp_commandinstead, it returns exact text.The VRP console serves one client at a time. When the eNSP client is driving a device it holds that device's console session, and a second client connects but receives nothing. The tools report that rather than hanging.
Devices whose images are absent.
NE40E,NE5000E,NE9000,CE6800,CE12800,CXandUSG6000Vneed an external image that the eNSP installer does not ship (CE.img,NE40E.img,vfw_usg.vdi…). Topologies using them are valid but those devices cannot start until the image is imported.Windows-oriented. Discovery uses the Windows registry and
tasklist; the topology tools are portable.
Provenance
All measurements were taken on a real installation.
evidence/agentd-protocol.md— the guest agent's four channels, recovered by disassembling the guest'sagentdbinary and confirmed against a live guest. (The binary itself is a vendor file from the eNSP image and is not redistributed; the notes give its SHA-256 and where to extract it.)evidence/boot_capture.log, produced byevidence/capture_boot.py— the serial pipe captured from the moment the VM is powered on.evidence/console-*.png— the guest's virtual screen.evidence/host-cmd-channel.md— the host component the eNSP client drives (eNSP_VBoxServer.exe): where it reads its configuration, the events it waits for, and the fixed-size UDP datagrams it exchanges. Not used byensp-mcp; it documents what a GUI-free replacement would have to reproduce.
MIT.
This server cannot be deployed
Maintenance
Related MCP Connectors
Run and manage H Company's Computer-Use Agents from any MCP client.
Drive real devices from your AI Coding tool. Embed a client SDK (Unity, Godot, Flutter, iOS/macOS, Android, React Native, Web) in your app, then capture screenshots, traverse the UI tree, inject taps and key events, and run automated test tasks on the physical device over a secure relay.
AI network lab: build and validate real multi-vendor router and switch labs from a prompt.
Cloud iOS simulators and Android emulators your agent can create, drive, and throw away.
Related MCP Servers
- AlicenseBqualityDmaintenanceGenerates eNSP network topologies, deploys Huawei VRP configurations, and provides verification plans for lab scenarios.124MIT
- FlicenseBqualityBmaintenanceEnables AI clients to manage Huawei eNSP simulated network devices, execute CLI commands, apply configurations, and perform network diagnostics through natural language, with topology upload and visualization via a web UI.209-
- AlicenseAqualityBmaintenanceEnables building Huawei eNSP network topologies and configuring lab devices through console ports, including device creation, wiring, layout, validation, and batch configuration.167MIT
- AlicenseBqualityCmaintenanceEnables AI assistants to programmatically generate Huawei eNSP .topo files that include USG6000V firewalls and S3700 switches, and then, once devices are started in eNSP, deliver configurations, run verification, and diagnose faults over Telnet. Ships with one-click health self-checks and topology-generation skills for natural-language-driven lab setup.311MIT