PNETLab MCP Server
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., "@PNETLab MCP Server Build a two-router OSPF lab with two PCs and boot it"
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.
PNETLab MCP Server
Give Claude (or any MCP client) programmatic control of a PNETLab network-emulation lab — build topologies, wire links, push device configs, and boot the lab, all from natural language.
An MCP (Model Context Protocol) server that exposes PNETLab lab operations as tools an LLM agent can call. Ask Claude to "build me a two-router OSPF lab with a PC on each side and boot it" and it will create the lab, add the nodes, cable them, inject startup-configs, and start everything — driving the same PNETLab API the web UI uses.
It is a drop-in-compatible fork of eveng-mcp-server — same tool names and shapes — but the client speaks PNETLab's session-bound API instead of EVE-NG's path-addressed REST API. The stock EVE-NG MCP server does not work against a PNETLab box; this fork bridges the gap. Verified against a live PNETLab 5.3.x server.
Table of contents
Related MCP server: gns3-mcp
Why this exists
PNETLab is a popular fork of EVE-NG, but it quietly replaced EVE-NG's clean REST API with a session-bound model borrowed from its Laravel front-end:
You log in, then open exactly one lab (which binds it to your server-side session).
Every subsequent operation targets that open lab via
/api/labs/session/*— there is no way to address a lab by path in the URL.Lab/folder listing and teardown live in a separate Laravel "store" admin API (
/store/...,/api/folders).
The consequence: tooling written for EVE-NG's API returns 404s and empty topologies against PNETLab. This server re-implements the client to speak PNETLab's dialect, while keeping an EVE-NG-style lab_path argument on every tool so agents (and humans) don't have to think about session binding. The client tracks which lab is open and re-binds only when lab_path changes.
Features
31 tools covering the full lifecycle: labs (create/clone/rename/move/delete), folders, images, nodes, links, configs, live console (read and write), backup/restore (
.zipexport/import), and running-lab control.Auto lab-binding — every tool takes a
lab_pathand opens it for you; no explicit "open lab" dance required.IOL interface translation — pass
e0/0/e0/1and the server converts to PNETLab's0/16/32/48indices automatically.Startup-config injection — nodes are created with configs enabled, so
push_configloads at boot without a link-destroying edit.Live console typing via the Guacamole WebSocket tunnel — for the things startup-config can't do (VPCS IPs, dot1Q subinterface addressing, live
write mem).Config export — scrape a running device's live running-config back into its startup-config so
write memsurvives a wipe.Resilient sessions — transparently re-authenticates and re-opens the lab when PNETLab rotates your single-session cookie, then retries the call.
How PNETLab differs from EVE-NG
The reason a dedicated fork is necessary:
Concern | EVE-NG (upstream) | PNETLab (this fork) |
Login |
|
|
Target a lab | path in every URL: | open one lab → all ops go to |
Open a lab | implicit |
|
List labs |
|
|
Link nodes | create network + attach interfaces |
|
Push config |
|
|
Tear down lab | delete via path |
|
Delete lab file |
| path-in-URL removed ( |
Session model | stateless per request | single-session-per-account; opening a lab evicts other sessions |
Requirements
Python 3.10+
A reachable PNETLab server (tested on 5.3.x) and a login account.
Python packages (installed automatically):
mcp,httpx,pydantic,websockets,pillow.websockets+pilloware only needed for the console tools (send_console,read_console); everything else is plain HTTP.
An MCP client — Claude Code, Claude Desktop, or any MCP-compatible host.
Use a dedicated MCP account. PNETLab allows only one active session per account. If the MCP logs in as the same user you use in the browser, the two will keep evicting each other. Create a separate admin-role local account (e.g.
mcp-user) for the server.
Installation
Clone and install into a virtual environment:
git clone https://github.com/<your-username>/pnetlab-mcp-server.git
cd pnetlab-mcp-server
python -m venv .venv
# Windows (PowerShell):
.venv\Scripts\Activate.ps1
# macOS/Linux:
source .venv/bin/activate
pip install -e .Or, with the dev extras (pytest, ruff, respx) for hacking on the server:
pip install -e ".[dev]"This installs the pnetlab-mcp-server console script and the pnetlab_mcp_server module. Either of these starts the server (it speaks MCP over stdio):
python -m pnetlab_mcp_server
# or
pnetlab-mcp-serverYou normally don't run it by hand — your MCP client launches it for you (see below).
Configuration
All configuration is via environment variables. Copy .env.example and adjust:
# Base URL of the PNETLab server
PNETLAB_HOST=https://pnetlab.example
# Use a DEDICATED admin-role account for the MCP (e.g. mcp-user), separate from
# the one you log into the web UI with. PNETLab is single-session-per-account,
# so sharing 'admin' makes the MCP and your browser evict each other.
PNETLAB_USERNAME=mcp-user
PNETLAB_PASSWORD=pnet
# Verify the TLS certificate (set to 'false' for self-signed labs)
PNETLAB_VERIFY_SSL=true
# 1 = HTML5 console, 0 = native — affects the console link format
PNETLAB_HTML_CONSOLE=1Variable | Default | Description |
|
| Base URL of the PNETLab server. |
|
| Local (offline) login username. Prefer a dedicated |
|
| Local (offline) login password (sent plaintext over the offline endpoint). |
|
|
|
|
|
|
Register with an MCP client
Claude Code (CLI)
claude mcp add pnetlab \
--env PNETLAB_HOST=https://pnetlab.example \
--env PNETLAB_USERNAME=mcp-user \
--env PNETLAB_PASSWORD=pnet \
-- python -m pnetlab_mcp_serverPoint at the venv's Python if the module isn't on your global PATH — e.g. .../pnetlab-mcp-server/.venv/Scripts/python.exe on Windows or .../.venv/bin/python on macOS/Linux.
Claude Desktop / generic MCP config
Add to your client's mcpServers map (for Claude Desktop: claude_desktop_config.json):
{
"mcpServers": {
"pnetlab": {
"command": "python",
"args": ["-m", "pnetlab_mcp_server"],
"env": {
"PNETLAB_HOST": "https://pnetlab.example",
"PNETLAB_USERNAME": "mcp-user",
"PNETLAB_PASSWORD": "pnet",
"PNETLAB_VERIFY_SSL": "true"
}
}
}
}On Windows, if you installed into a venv, use the full interpreter path so the module resolves:
{
"mcpServers": {
"pnetlab": {
"command": "C:\\path\\to\\pnetlab-mcp-server\\.venv\\Scripts\\python.exe",
"args": ["-m", "pnetlab_mcp_server"],
"env": {
"PNETLAB_HOST": "https://pnetlab.example",
"PNETLAB_USERNAME": "mcp-user",
"PNETLAB_PASSWORD": "pnet"
}
}
}
}Restart the client; the pnetlab tools appear once the server connects.
👀 Watching the MCP work — join its session
Read this if you open the lab in your browser to watch what the MCP is doing.
PNETLab is multi-session: every account that opens a lab gets its own private running instance (its own pod). The MCP signs in as its own account (mcp-user), so everything it does at runtime — starting nodes, wiring links, live console activity, VPCS IPs — happens inside the mcp-user session.
The consequence: if you open the same lab in your browser as your normal account, PNETLab gives you a separate, empty pod. You will not see the nodes the MCP started or any of its live activity — you'd be looking at your own independent copy of the topology.
To see what the MCP is doing, join its session instead of starting your own:
Make the lab joinable — in the lab's settings, set Joinable to allow it (and add your email to the joinable list if the lab restricts who can join).
Find the MCP's session — ask the agent to run
list_running_labs, or open the Running Labs page in the web UI, to see themcp-usersession for that lab.Join it — open the lab and choose to join the existing (
mcp-user) session rather than start a new one.
Now you share the same running pod: the nodes the MCP starts, the links it wires, and the console output all appear in your browser in real time.
Why not just share one account? PNETLab allows only one active session per account — if the MCP and your browser used the same login, they'd constantly evict each other. That's exactly why the MCP uses a separate
mcp-user, and why joining is the right way to observe its work.
Tool reference
31 tools, grouped by concern. Every tool that operates on a lab takes a lab_path (leading slash and .unl suffix are added for you, so readme-demo, /readme-demo, and /readme-demo.unl all work).
Lab management
Tool | Parameters | Description |
|
| List labs and subfolders in a workspace folder. Returns each lab's |
|
| Full topology: lab metadata, all nodes (status + the interface index→name map you need for |
|
| Create a new lab and open it as the active session lab. |
|
| Bind an existing lab to the session. Usually unnecessary (every tool auto-opens its |
|
| Tear down a lab's running session (stop & wipe nodes, unbind). By default the |
|
| Clone a lab (topology + configs) into the same folder under a new name. Great for templating. |
|
| Rename a lab's |
|
| Move a lab into another workspace folder (must already exist — see |
| (none) | List every running lab session on the server (lab path, session id, pod, running-node count). A lab appears here whenever it has an open pod — that's what blocks deleting its |
|
| Fully stop a running lab across all its open sessions/pods (stops nodes + destroys pods) so it can be deleted — even one running under another user. |
|
| Export a lab to a |
|
| Import a lab from a local |
Images / templates
Tool | Parameters | Description |
| (none) | List installed node templates/images (uninstalled |
|
| Get a template's add-node option schema (valid ram/ethernet/image/nvram) so |
Folders
Tool | Parameters | Description |
|
| Create a workspace subfolder for organizing labs. |
|
| Delete a folder and everything in it (blocked with |
Node management
Tool | Parameters | Description |
|
| Add a node. Created with startup-config enabled so |
|
| Start a single node. |
|
| Stop a single node. |
|
| Start every node in the lab. |
|
| Stop every node in the lab. |
|
| Node status ( |
|
| Delete a single node (and its links). Delete before re-wiring neighbors — editing links after can drop bindings. |
|
| Wipe a node — clear NVRAM/runtime so it reboots fresh from the stored startup-config. Factory-reset a device, force a pushed config to reload, or free a stuck console line. |
Common templates (from list_images on the reference box):
|
| What it is | Suggested |
|
| Virtual PC (lightweight test host) |
|
|
| Cisco IOL L3 (router) |
|
|
| Cisco IOL L2 (switch) |
|
|
| Classic Dynamips IOS routers |
|
|
| Docker container node | (varies) |
Configuration
Tool | Parameters | Description |
|
| Store a startup-config (loads on next boot). For IOL L2 switches, include |
|
| Return a node's current startup-config text. |
|
| Save a running node's live running-config into its startup-config (PNETLab "Export CFG"). |
Connectivity
Tool | Parameters | Description |
|
| Point-to-point link between two node interfaces. Accepts an index or an |
|
| Remove the link on a node's interface (deletes the underlying network). For a p2p link this disconnects both ends, so you pass just one side. |
|
| Type commands on a node's live console via the Guacamole tunnel — for what |
|
| Read the live console — returns a PNG screenshot of the terminal (a vision model reads it). Optionally types |
Worked examples
These are real calls run against a live PNETLab 5.3.x box while writing this README, with real returned output.
1. List your labs
list_labs(folder="/"){
"folder": "/",
"labs": [
{ "name": "mcp-verify.unl", "path": "/mcp-verify.unl", "modified": "08 Jul 2026 00:48" },
{ "name": "bgp-vlan-sites.unl", "path": "/bgp-vlan-sites.unl", "modified": "08 Jul 2026 01:53" }
],
"subfolders": []
}2. Discover installed images
list_images(){
"c3725": "Cisco IOS 3725 (Dynamips)",
"c7200": "Cisco IOS C7200 (Dynamips)",
"docker": "Docker.io",
"i86bi_linux_l2": "Cisco IOL L2 (Switch)",
"i86bi_linux_l3": "Cisco IOL L3 (Router)",
"vpcs": "Virtual PC (VPCS)"
}3. Build a router + PC lab end-to-end
// Create and auto-open the lab
create_lab(name="ospf-lab", description="Two-node demo", author="you")
// Add a Cisco IOL router — note the interface map that comes back
add_node(lab_path="/ospf-lab.unl", template="i86bi_linux_l3", node_type="iol",
name="R1", image="i86bi_linux_l3-L3-ADVENTERPRISEK9-M-15.4-2T.bin",
left=350, top=150)
// → { "id": 1, "name": "R1", "type": "iol", "console_port": 30017,
// "interfaces": { "0": "e0/0", "16": "e0/1", "32": "e0/2", "48": "e0/3" } }
// Add a VPCS test host
add_node(lab_path="/ospf-lab.unl", template="vpcs", node_type="vpcs",
name="PC1", icon="Desktop.png", ram=128, left=100, top=150)
// → { "id": 2, "name": "PC1", "type": "vpcs", "console_port": 30018,
// "interfaces": { "0": "eth0" } }
// Cable PC1 eth0 <-> R1 e0/0 (pass the name; it's converted to index 0)
connect_nodes(lab_path="/ospf-lab.unl",
node1_id=2, node1_interface=0,
node2_id=1, node2_interface="e0/0")
// → "Connected node 2 (if 0->0) to node 1 (if e0/0->0)."
// Inject R1's startup-config (loads on boot because the node has config enabled)
push_config(lab_path="/ospf-lab.unl", node_id=1, config="""hostname R1
!
interface Ethernet0/0
ip address 10.9.9.1 255.255.255.0
no shutdown
!
end
""")
// → "Configuration pushed to node 1."
// Boot everything
start_all(lab_path="/ospf-lab.unl")4. Configure a VPCS host (startup-config is ignored — use the live console)
VPCS won't apply an injected config, so set its IP on the running console:
start_node(lab_path="/ospf-lab.unl", node_id=2)
send_console(lab_path="/ospf-lab.unl", node_id=2,
commands=["ip 10.9.9.10/24 10.9.9.1", "save"])5. Add a dot1Q subinterface (dropped from startup-config — use the console)
send_console(lab_path="/ospf-lab.unl", node_id=1, commands=[
"enable",
"configure terminal",
"interface e0/0.10",
"encapsulation dot1Q 10",
"ip address 10.1.10.1 255.255.255.0",
"end",
"write memory"
])6. Persist runtime changes, then read them back
export_config(lab_path="/ospf-lab.unl", node_id=0) // export ALL running nodes
get_node_config(lab_path="/ospf-lab.unl", node_id=1) // returns the saved startup-config text7. Inspect a full topology
get_lab returns lab metadata, every node (with status, console port, and the interface index→name map), and all networks — the one call an agent uses to understand a lab before touching it:
get_lab(lab_path="/bgp-vlan-sites.unl"){
"lab": { "name": "bgp-vlan-sites", "filename": "bgp-vlan-sites.unl", "id": "e89dbdaa-…" },
"nodes": {
"1": { "id": 1, "name": "R1", "type": "iol", "status": 0, "template": "i86bi_linux_l3",
"ethernets": { "0": {"name":"e0/0","network_id":3}, "16": {"name":"e0/1","network_id":4}, … } },
"3": { "id": 3, "name": "SW-A", "type": "iol", "status": 0, "template": "i86bi_linux_l2", … },
"6": { "id": 6, "name": "PC-A2","type": "vpcs", "status": 2, … } // status 2 = running
// …
},
"networks": { "1": {"id":1,"name":"Network 1","type":"bridge"}, … }
}8. Tear down the running lab
// Default: destroy the running session but KEEP the .unl file on disk
delete_lab(lab_path="/ospf-lab.unl")
// → { "code": 200, "message": "success" }
// Opt-in: destroy the session AND permanently delete the .unl file
delete_lab(lab_path="/ospf-lab.unl", delete_file=true)
// → { "destroyed": { "code": 200, "message": "success" },
// "file_deleted": { "code": 200, "status": "success", "message": "Lab has been deleted (60022)." } }Deleting the
.unlfile. EVE-NG's path-in-URLDELETE /api/labs/<path>was removed in PNETLab (returns60038). Deletion is instead a body-based call —DELETE /api/labswith a JSON body{"path": "/lab.unl"}(the same request PNETLab's own file manager makes). Confirmed against a live 5.3.x box:
The lab's own running session is torn down first (this tool does that for you via
factory/destroy).The path goes in the JSON body, not the URL, and the
.unlsuffix is required (without it: "Lab File is not founded").It's a per-lab delete: works for labs at the workspace root or in a subfolder, and is not blocked by other running labs.
Gotchas & hard-won lessons
These are baked into the client so you rarely hit them, but knowing them helps:
IOL interface indices are
0 / 16 / 32 / 48, not0/1/2/3. IOL groups ports in fours:e0/0→0,e0/1→16,e0/2→32,e0/3→48 (portgroup*16).connect_nodesaccepts either the index or ane0/xname and converts for you. VPCS uses index0(eth0).Never edit a node after wiring it. A partial
nodes/editsilently drops the node's interface bindings (its links). To avoid it, nodes are created with startup-config already enabled (config=1), sopush_confignever needs a follow-up edit.VPCS ignores injected startup-config. Pushing a "config" stores it but the node won't apply it at boot — set the VPCS IP on the live console via
send_console(ip 10.x.x.x/24 gw, thensave).IOL L2 defaults to VTP server mode, so VLANs live in
vlan.datand an injectedvlan Nline is ignored. Addvtp mode transparentbeforevlan Nin switch configs so VLANs persist in running/startup config.dot1Q subinterface IPs get dropped from startup-config on IOL. Add them live with
send_console, thenexport_configto persist.The console is single-connection. A node's telnet console allows only one client. Close any open browser console tab for a node before
send_console, or guacd will refuse the tunnel (the client retries a few times with backoff to ride out transient contention).delete_labkeeps the file by default. It destroys the running session/instance and leaves the.unlon disk. Passdelete_file=trueto also permanently remove the file (works at the root or in a subfolder, and isn't blocked by other running labs). The legacy EVE-NG path-in-URLDELETE /api/labs/<path>route is gone (returns60038); deletion usesDELETE /api/labswith{path}in the JSON body instead.Single session per account. Opening a lab (any tool call) evicts other sessions for that user. Use a dedicated
mcp-userso the MCP and your browser don't fight. If the cookie is rotated mid-run, the client auto-re-logins, re-opens the lab, and retries once.
Troubleshooting
Symptom | Likely cause & fix |
| Wrong |
Calls work, then start failing with | Your session cookie was rotated (someone else logged in as the same user). Use a dedicated |
TLS / certificate errors | Self-signed lab cert → set |
| You wired an IOL port with the wrong index. Use |
A pushed VLAN or subinterface IP vanished | IOL L2 VTP (add |
| Node isn't running, or its console is held by an open browser tab (single-connection). Close the tab and retry; ensure |
A VPCS host has no IP after boot | Expected — VPCS ignores startup-config. Set it with |
| The |
| The lab path must include the |
Run the server with INFO logging (it logs to stderr by default) to watch auth, lab-open, and retry events while debugging.
Architecture
src/pnetlab_mcp_server/
├── __main__.py # entry point: configures logging, runs the MCP server over stdio
├── server.py # FastMCP tool definitions (the 31 tools) — thin wrappers over the client
├── guac_screen.py# Guacamole draw-instruction compositor (read_console screenshots)
├── client.py # PNetLabClient: async httpx client that speaks PNETLab's session API
└── models.py # Pydantic models (NodeType, AddNodeInput) for tool inputsserver.pybuilds aFastMCP("pnetlab-mcp-server")instance and registers each tool with@mcp.tool. Tools do input shaping, call the client, and format the result as JSON text. A single lazily-constructedPNetLabClientis shared across calls.client.pyhides PNETLab's session model behind an EVE-NG-shaped interface. Key pieces:login()→ offline login at/store/public/auth/login/login._ensure_lab()→ opens the requested lab viafactory/createonly when it differs from the currently bound lab._request()→ wraps every call with transparent re-auth + lab re-open + single retry on session expiry (90001).iol_interface_index()→ thee0/x⇄0/16/32/48translation.send_console()→ opens a Guacamole WebSocket tunnel, keeps it alive by echoingsyncframes, and types keystrokes; retries on single-connection contention.
The MCP layer speaks stdio; the client layer speaks HTTPS + WSS to PNETLab.
Development & testing
Install dev extras and run the linter:
pip install -e ".[dev]"
ruff check .The tests/ directory contains live integration probes (they hit a real PNETLab box using your PNETLAB_* env vars — not offline unit tests). The most useful is a full lifecycle drive:
# Requires PNETLAB_HOST / PNETLAB_USERNAME / PNETLAB_PASSWORD in the environment.
python tests/live_phase3.pyIt runs: login → list templates → create lab → add VPCS + IOL nodes → connect → push & read config → start → stop → delete → list, printing a pass/fail summary. Other probe_*.py / guac_*.py scripts were used to reverse-engineer individual endpoints and the Guacamole console tunnel.
These scripts create and destroy a throwaway lab. Point them at a lab server you control.
Project status
Alpha. The full lifecycle — login → create/clone/rename/move/delete labs & folders → list templates → add/connect/disconnect/wipe/delete nodes → push/get/export config → start/stop → read topology → console read + write → backup/restore (.zip) → list/stop running labs — is implemented and verified against a live PNETLab 5.3.x server. read_console returns a rendered screenshot of the terminal (Guacamole draws text as image tiles, so there's no text on the wire — the framebuffer is reconstructed and a vision model reads it). API endpoint shapes were reverse-engineered against one build; other PNETLab versions may differ.
Not yet supported: live packet capture (wireshark/capture). It's a browser Guacamole session running Wireshark in a container (console_guac_link?node_id=N&type=wireshark returns the guac link, and the framebuffer can be screenshotted the same way read_console works). The blocker is the POST /api/labs/session/wireshark/capture trigger, which returns "Please capture again" to a bare API call regardless of node state/ordering — the web UI performs an additional handshake step that hasn't been fully reverse-engineered yet.
Contributions, bug reports, and version-compatibility notes are welcome.
License
Apache-2.0. Fork of axiom-works-ai/eveng-mcp-server (Apache-2.0).
Available Tools
31 toolsadd_nodeA
Add a node to a lab. Common templates: 'vpcs' (type vpcs), 'i86bi_linux_l3' Cisco IOL router / 'i86bi_linux_l2' IOL switch (type iol), 'c3725' etc. (type dynamips). Use list_images for what's installed. Nodes are created with startup-config ENABLED so push_config loads on boot. Note: VPCS ignores startup-config — set its IP on the live console.
Inputs: lab_path (required), template (required), cpu, ram, +11 more. Returns: result
clone_labA
Clone an existing lab into the same folder under a new name (copies the whole topology + configs). Great for templating — duplicate a base lab N times. 'name' is the clone's new name (no .unl needed).
Inputs: name (required), source_lab_path (required). Returns: result
connect_nodesA
Create a point-to-point link between two node interfaces. Interfaces may be given as an index or an interface name. IMPORTANT: IOL interfaces are indexed 0/16/32/48 (e0/0=0, e0/1=16, e0/2=32, e0/3=48); VPCS uses 0. You can pass 'e0/1' and it is converted automatically. Use get_lab to see each node's interface map. Do NOT connect nodes after editing them — wire the topology, then start.
Inputs: lab_path (required), node1_id (required), node2_id (required), node1_interface (required), +3 more. Returns: result
create_folderA
Create a workspace subfolder for organizing labs. path='/' for the root.
Inputs: name (required), path. Returns: result
create_labC
Create a new lab and open it as the active session lab.
Inputs: name (required), body, path, author, +2 more. Returns: result
delete_folderA
Delete a workspace folder AND everything inside it (irreversible). Blocked with 'error_folder_running' if any lab under it is running.
Inputs: folder_path (required). Returns: result
delete_labA
Tear down a lab's running session — stops and wipes its nodes and unbinds it (PNETLab factory/destroy). By default the .unl file is KEPT on disk. Pass delete_file=true to ALSO permanently delete the .unl file from disk (irreversible). Works for labs at the workspace root or in a subfolder, and is not blocked by other running labs.
Inputs: lab_path (required), delete_file. Returns: result
delete_nodeA
Delete a single node from a lab (and its links). Prefer deleting a node BEFORE re-wiring its neighbors — on PNETLab, editing links after the fact can drop interface bindings. Use get_lab to find the node_id.
Inputs: node_id (required), lab_path (required). Returns: result
disconnect_nodesA
Remove the link on a node's interface (deletes the underlying network). For a point-to-point link this disconnects BOTH ends, so you only pass one side. Interface may be an index or an 'e0/1'-style name (IOL 0/16/32/48). Use get_lab to see which interfaces are connected.
Inputs: node_id (required), lab_path (required), node_interface (required). Returns: result
download_labA
Export a lab to a .zip backup and save it to a local file. Pass save_path as a file or directory (defaults to the current directory). Returns the saved path. Round-trips with upload_lab.
Inputs: lab_path (required), save_path. Returns: result
export_configA
Save a RUNNING node's live running-config into its stored startup-config (PNETLab 'Export CFG'), so changes made on the device (write mem) survive a wipe/reopen. The node must be running. Pass node_id=0 to export ALL running nodes at once. Also refreshes what get_node_config returns.
Inputs: lab_path (required), node_id. Returns: result
get_labB
Get a lab's full topology: metadata, all nodes (with status and the interface index->name map you need for connect_nodes), and networks.
Inputs: lab_path (required). Returns: result
get_node_configB
Get a node's current startup-config text.
Inputs: node_id (required), lab_path (required). Returns: result
get_node_statusB
Get a node's status (0=stopped,1=building,2=running), console URL, and interfaces.
Inputs: node_id (required), lab_path (required). Returns: result
get_template_optionsA
Get a node template's add-node option schema — the valid/default fields for that image (e.g. ram, ethernet count, image filename, nvram). Use before add_node to pick correct parameters. Get template keys from list_images.
Inputs: template (required). Returns: result
list_imagesA
List installed node templates/images (missing/uninstalled ones filtered out).
Inputs: no named parameters. Returns: result
list_labsA
List labs and subfolders in a workspace folder (default '/'). Returns each lab's name, full path (use with open_lab/get_lab), and last-modified time. Pass a folder path to list a subfolder.
Inputs: folder. Returns: result
list_running_labsA
List every currently-running lab session on the server (across all users): lab path, session id, pod, and running-node count. A lab shows here whenever it has an open pod — that's what blocks deleting its .unl ('error_lab_running').
Inputs: no named parameters. Returns: result
move_labA
Move a lab into another workspace folder. The destination folder must already exist (use create_folder first). Pass folder='/' for the root.
Inputs: folder (required), lab_path (required). Returns: result
open_labA
Bind an existing lab to the session so subsequent tools operate on it. Usually unnecessary — every tool auto-opens its lab_path — but useful to switch the active lab explicitly.
Inputs: lab_path (required). Returns: result
push_configA
Push a startup-config to a node (stored; loads on next boot). For IOL L2 switches, include 'vtp mode transparent' before 'vlan N' or the VLAN won't persist. VPCS nodes ignore startup-config — configure their IP on the live console instead.
Inputs: config (required), node_id (required), lab_path (required). Returns: result
read_consoleA
Read a node's LIVE console as a screenshot (the read side send_console lacks). Returns a PNG image of the current terminal for you to read. Optionally pass 'commands' (list of lines) to type first, then it captures the output — do type+read in ONE call to avoid console contention. Node must be running; close any open browser console tab for it. Great for verifying 'show' commands, ping results, and boot state.
Inputs: node_id (required), lab_path (required), commands.
rename_labB
Rename a lab (changes its .unl filename). PNETLab sanitizes the new name (special characters are stripped). Pass the new name without .unl.
Inputs: lab_path (required), new_name (required). Returns: result
send_consoleA
Type commands on a node's LIVE console via the Guacamole tunnel — for what push_config CAN'T do: VPCS IPs, IOL dot1Q subinterface IPs (dropped from startup-config), live 'write mem', etc. Node must be running. Pass commands as a list of lines (each sent + Enter); for IOS include 'enable' and 'configure terminal' as needed, e.g. ['enable','configure terminal','interface e0/0.10','ip address 10.1.10.1 255.255.255.0','end','write memory']. For VPCS: ['ip 10.1.10.10/24 10.1.10.1','save']. This is write-only (no output is read back) — verify with export_config/get_node_config. Close any open browser console tab for the node first (one connection allowed).
Inputs: node_id (required), commands (required), lab_path (required). Returns: result
start_allC
Start all nodes in a lab.
Inputs: lab_path (required). Returns: result
start_nodeC
Start a single node in a lab.
Inputs: node_id (required), lab_path (required). Returns: result
stop_allC
Stop all nodes in a lab.
Inputs: lab_path (required). Returns: result
stop_labA
Fully stop a running lab across ALL its open sessions/pods (stops nodes and destroys the pods). Use this to clear 'error_lab_running' before deleting a lab that's still open — including one running under another user's session. Find running labs with list_running_labs.
Inputs: lab_path (required). Returns: result
stop_nodeB
Stop a single node in a lab.
Inputs: node_id (required), lab_path (required). Returns: result
upload_labA
Import a lab from a local .zip (previously produced by download_lab) into a workspace folder. Pass the local zip path and the destination folder (default '/'). The folder must already exist.
Inputs: zip_file_path (required), folder. Returns: result
wipe_nodeA
Wipe a node — clear its NVRAM/runtime state so it reboots fresh from the stored startup-config (PNETLab 'Wipe'). Use to factory-reset a device or force a pushed config to reload. Also frees a stuck console line.
Inputs: node_id (required), lab_path (required). Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 31 tools
Each tool has a clearly distinct purpose. For example, add_node/clone_lab/create_lab cover different operations, connect_nodes/disconnect_nodes are opposites, and send_console/read_console handle input/output separately. Even similar verbs like start_all vs start_node are distinguished by scope.
All 31 tools use a consistent verb_noun pattern (e.g., add_node, delete_lab, get_lab, start_all). No mixing of camelCase, snake_case, or other conventions; the naming is uniform and predictable.
At 31 tools, the server covers a wide domain (lab lifecycle, node management, console interaction, configuration). While above the typical 3-15 range, each tool serves a specific function needed for network simulation, making the count reasonable for the scope.
The tool set covers CRUD for labs and nodes, console interaction, config management, and import/export. Minor gaps exist: no direct network management (beyond connections) and no tool to retrieve a node's running config (only startup-config). However, core workflows are well-supported.
Maintenance
Related MCP Connectors
AI network lab: build and validate real multi-vendor router and switch labs from a prompt.
Provides capabilities that let LLM agents perform a range of infrastructure management tasks.
MCP-Native LLM Orchestration Agent
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Related MCP Servers
- AlicenseAqualityCmaintenanceWraps ipspace/netlab as an engine to give LLMs access to validated, lab-tested network device configurations instead of hallucinated ones.87Apache 2.0
- AlicenseCqualityCmaintenanceEnables AI agents to control GNS3 network emulation labs. Supports building topologies, managing devices, capturing packets, and automating device CLIs.1002MIT
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to control Cisco Packet Tracer in real time, allowing natural language-driven creation and configuration of network topologies.6MIT
- AlicenseNot gradedqualityBmaintenanceEnables LLMs to turn natural-language prompts into validated, deployed Cisco network topologies, including routing, switching, DHCP, VLANs, security auditing, and live device inspection.MIT