Skip to main content
Glama
omichelbraga

PNETLab MCP Server

by omichelbraga

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.

License: Apache-2.0 Python 3.10+ MCP Status: Alpha

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: netlab-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 (.zip export/import), and running-lab control.

  • Auto lab-binding — every tool takes a lab_path and opens it for you; no explicit "open lab" dance required.

  • IOL interface translation — pass e0/0/e0/1 and the server converts to PNETLab's 0/16/32/48 indices automatically.

  • Startup-config injection — nodes are created with configs enabled, so push_config loads 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 mem survives 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

POST /api/auth/login

POST /store/public/auth/login/login (offline, plaintext, empty captcha)

Target a lab

path in every URL: /api/labs/{path}/…

open one lab → all ops go to /api/labs/session/*

Open a lab

implicit

POST /api/labs/session/factory/create {path}

List labs

GET /api/folders/{path}

GET /api/folders / /api/folders/<path> (no trailing slash)

Link nodes

create network + attach interfaces

POST /api/labs/session/networks/p2p (auto-creates the bridge)

Push config

PUT …/config

POST /api/labs/session/configs/edit {id,data}

Tear down lab

delete via path

POST /api/labs/session/factory/destroy {lab_session}

Delete lab file

DELETE /api/labs/{path} (path in URL)

path-in-URL removed (60038) → DELETE /api/labs with {path} in body

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 + pillow are 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-server

You 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=1

Variable

Default

Description

PNETLAB_HOST

https://pnetlab.example

Base URL of the PNETLab server.

PNETLAB_USERNAME

admin

Local (offline) login username. Prefer a dedicated mcp-user.

PNETLAB_PASSWORD

pnet

Local (offline) login password (sent plaintext over the offline endpoint).

PNETLAB_VERIFY_SSL

true

false disables TLS verification (self-signed certs).

PNETLAB_HTML_CONSOLE

1

1 HTML5 console, 0 native console; changes the console link scheme.


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_server

Point 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:

  1. 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).

  2. 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 the mcp-user session for that lab.

  3. 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

folder="/"

List labs and subfolders in a workspace folder. Returns each lab's name, path, and modified time.

get_lab

lab_path

Full topology: lab metadata, all nodes (status + the interface index→name map you need for connect_nodes), and networks.

create_lab

name, description="", path="/", author="", version="1", body=""

Create a new lab and open it as the active session lab.

open_lab

lab_path

Bind an existing lab to the session. Usually unnecessary (every tool auto-opens its lab_path); handy to switch the active lab.

delete_lab

lab_path, delete_file=false

Tear down a lab's running session (stop & wipe nodes, unbind). By default the .unl file is kept; pass delete_file=true to also permanently delete the .unl from disk. See the note below.

clone_lab

source_lab_path, name

Clone a lab (topology + configs) into the same folder under a new name. Great for templating.

rename_lab

lab_path, new_name

Rename a lab's .unl file (PNETLab sanitizes special characters out of the new name).

move_lab

lab_path, folder

Move a lab into another workspace folder (must already exist — see create_folder).

list_running_labs

(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 .unl.

stop_lab

lab_path

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.

download_lab

lab_path, save_path=""

Export a lab to a .zip backup and save it locally (defaults to the current directory). Round-trips with upload_lab.

upload_lab

zip_file_path, folder="/"

Import a lab from a local .zip (from download_lab) into a workspace folder (must already exist).

Images / templates

Tool

Parameters

Description

list_images

(none)

List installed node templates/images (uninstalled .missing entries filtered out).

get_template_options

template

Get a template's add-node option schema (valid ram/ethernet/image/nvram) so add_node params are right. Template keys come from list_images.

Folders

Tool

Parameters

Description

create_folder

name, path="/"

Create a workspace subfolder for organizing labs.

delete_folder

folder_path

Delete a folder and everything in it (blocked with error_folder_running if any lab under it is running).

Node management

Tool

Parameters

Description

add_node

lab_path, template, name="", node_type="iol", ram=1024, cpu=1, ethernet=1, serial=0, image="", icon="Router.png", console="telnet", left=100, top=200, config="1", delay=0

Add a node. Created with startup-config enabled so push_config loads on boot.

start_node

lab_path, node_id

Start a single node.

stop_node

lab_path, node_id

Stop a single node.

start_all

lab_path

Start every node in the lab.

stop_all

lab_path

Stop every node in the lab.

get_node_status

lab_path, node_id

Node status (0=stopped, 1=building, 2=running), console info, and interfaces.

delete_node

lab_path, node_id

Delete a single node (and its links). Delete before re-wiring neighbors — editing links after can drop bindings.

wipe_node

lab_path, node_id

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):

template

node_type

What it is

Suggested icon

vpcs

vpcs

Virtual PC (lightweight test host)

Desktop.png

i86bi_linux_l3

iol

Cisco IOL L3 (router)

Router.png

i86bi_linux_l2

iol

Cisco IOL L2 (switch)

Switch.png

c3725, c7200

dynamips

Classic Dynamips IOS routers

Router.png

docker

docker

Docker container node

(varies)

Configuration

Tool

Parameters

Description

push_config

lab_path, node_id, config

Store a startup-config (loads on next boot). For IOL L2 switches, include vtp mode transparent before vlan N or the VLAN won't persist.

get_node_config

lab_path, node_id

Return a node's current startup-config text.

export_config

lab_path, node_id=0

Save a running node's live running-config into its startup-config (PNETLab "Export CFG"). node_id=0 exports all running nodes.

Connectivity

Tool

Parameters

Description

connect_nodes

lab_path, node1_id, node1_interface, node2_id, node2_interface, network_name="", network_type="bridge"

Point-to-point link between two node interfaces. Accepts an index or an e0/1-style name (auto-converted). Wire the topology before starting nodes.

disconnect_nodes

lab_path, node_id, node_interface

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.

send_console

lab_path, node_id, commands (list of lines)

Type commands on a node's live console via the Guacamole tunnel — for what push_config can't do. Node must be running. Write-only; verify with export_config or read_console.

read_console

lab_path, node_id, commands=[] (optional)

Read the live console — returns a PNG screenshot of the terminal (a vision model reads it). Optionally types commands first, then captures the output — do type+read in one call to avoid console contention. The read side send_console lacks.


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 text

7. 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 .unl file. EVE-NG's path-in-URL DELETE /api/labs/<path> was removed in PNETLab (returns 60038). Deletion is instead a body-based call — DELETE /api/labs with a JSON body {"path": "/lab.unl"} (the same request PNETLab's own file manager makes). Confirmed against a live 5.3.x box:

  1. The lab's own running session is torn down first (this tool does that for you via factory/destroy).

  2. The path goes in the JSON body, not the URL, and the .unl suffix is required (without it: "Lab File is not founded").

  3. 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, not 0/1/2/3. IOL groups ports in fours: e0/0→0, e0/1→16, e0/2→32, e0/3→48 (portgroup*16). connect_nodes accepts either the index or an e0/x name and converts for you. VPCS uses index 0 (eth0).

  • Never edit a node after wiring it. A partial nodes/edit silently drops the node's interface bindings (its links). To avoid it, nodes are created with startup-config already enabled (config=1), so push_config never 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, then save).

  • IOL L2 defaults to VTP server mode, so VLANs live in vlan.dat and an injected vlan N line is ignored. Add vtp mode transparent before vlan N in 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, then export_config to 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_lab keeps the file by default. It destroys the running session/instance and leaves the .unl on disk. Pass delete_file=true to 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-URL DELETE /api/labs/<path> route is gone (returns 60038); deletion uses DELETE /api/labs with {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-user so 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

Login failed

Wrong PNETLAB_USERNAME/PASSWORD, or the account is a cloud (pnetlab.com) login — this server uses the offline/local login path.

Calls work, then start failing with 90001

Your session cookie was rotated (someone else logged in as the same user). Use a dedicated mcp-user. The client auto-retries once regardless.

TLS / certificate errors

Self-signed lab cert → set PNETLAB_VERIFY_SSL=false.

connect_nodes "succeeds" but there's no link

You wired an IOL port with the wrong index. Use e0/x names (auto-converted) or the 0/16/32/48 indices; check the map from get_lab/add_node.

A pushed VLAN or subinterface IP vanished

IOL L2 VTP (add vtp mode transparent) or dot1Q-in-startup-config (add it live via send_console). See gotchas.

send_console fails / times out

Node isn't running, or its console is held by an open browser tab (single-connection). Close the tab and retry; ensure websockets is installed.

A VPCS host has no IP after boot

Expected — VPCS ignores startup-config. Set it with send_console (ip …/24 gw, save).

list_labs returns nothing for a subfolder

The /api/folders/<path> endpoint is path-sensitive and takes no trailing slash; pass e.g. folder="/My Labs".

delete_lab(delete_file=true) → "Lab File is not founded"

The lab path must include the .unl suffix and be correct (/folder/name.unl). The tool normalizes this for you; check the lab_path you passed exists (list_labs).

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 inputs
  • server.py builds a FastMCP("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-constructed PNetLabClient is shared across calls.

  • client.py hides 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 via factory/create only 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() → the e0/x0/16/32/48 translation.

    • send_console() → opens a Guacamole WebSocket tunnel, keeps it alive by echoing sync frames, 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.py

It 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).

Install Server
A
license - permissive license
A
quality
C
maintenance

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/omichelbraga/pnetlab-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server