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 Create a network with two routers and a switch, then start all nodes"
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
一个 MCP 服务器,让 Claude(以及其他 LLM Agent)可以通过自然语言程序化控制 PNETLab v6 网络实验环境 -- 创建拓扑、 添加节点、连线、推送配置、启动节点并读取状态、驱动设备控制台、注入链路故障。
它复刻了 axiom-works-ai/eveng-mcp-server
的工具集,但对接的是 PNETLab v6 的会话级 API,而不是 v6 已经移除的经典
EVE-NG API。
⚠️ 生成与测试说明
本项目代码完全由 AI 生成。基础功能已经过实际测试,但未做全面覆盖测试。建议在正式使用前让 Agent 自行针对你的 PNETLab 环境跑一轮验证。
为什么需要它
PNETLab v6(6.0.0+)是一次 Laravel 重写,保留了旧 EVE-NG 引擎(仍在 /api/
下),但是:
删除了经典登录(
/api/auth/login)、/api/status、/api/labs/、/api/folders/、/api/users/。把实验操作迁移到会话级 API:
/api/labs/session/*。替换了登录方式,改为 Laravel 端点:
POST /store/public/auth/login/login。
所以 eveng-mcp-server(走经典 API)在 v6 上无法工作。本服务器是针对
PNETLab 6.0.0-100 逆向工程并实际验证过的。
Related MCP server: EVE-NG MCP Server
安装
pip install -e .安装后提供 pnetlab-mcp-server 命令。
配置
设置环境变量(服务器在第一次调用工具时才会懒加载登录):
变量 | 示例 | 用途 |
|
| PNETLab v6 地址 |
|
| Agent 使用的工作账号(请用专用账号) |
|
| 工作账号密码 |
|
| (可选)在浏览器里查看的账号 |
|
| 查看账号密码 |
为什么要专用工作账号? v6 每个用户账号只能有一个活跃的实验会话。如果
Agent 和你的浏览器都用 admin,open_lab 会报 20039 "sandbox already exists"。给 Agent 一个独立账号(例如 mcp,admin 角色),你的浏览器就自由了。
在浏览器里实时查看。 当设置了 PNETLAB_VIEWER_* 时,open_lab 会自动把
查看账号加入 Agent 的实验会话 -- 于是两者共享同一个实时拓扑。Agent 打开实验
后,只要用查看账号登录 PNETLab 网页 UI 并打开该实验(或访问
/legacy/topology):你就能看到 Agent 的拓扑,刷新即可看到它的改动。
join_viewer 用于在你先打开了浏览器时重新加入;close_lab 也会让查看账号退出。
Claude Code(.claude.json)
{
"mcpServers": {
"pnetlab": {
"command": "pnetlab-mcp-server",
"env": {
"PNETLAB_HOST": "http://192.168.231.128",
"PNETLAB_USERNAME": "mcp",
"PNETLAB_PASSWORD": "pnet",
"PNETLAB_VIEWER_USERNAME": "admin",
"PNETLAB_VIEWER_PASSWORD": "pnet"
}
}
}
}添加后重启 Claude Code。
工具
列表与模板
工具 | 作用 |
| 列出已安装节点模板,结构化返回 |
| 列出某模板可用磁盘镜像(如 |
| 取模板完整可编辑选项(镜像、qemu 版本、各字段默认值) |
| 列出网络类型(bridge、pnet0..9、ovs) |
实验会话
工具 | 作用 |
| 按文件名打开实验,如 |
| 离开当前实验会话(同时让查看账号退出) |
| (重新)把查看账号加入 Agent 的会话,以便在浏览器里查看 |
| 信息 + 完整拓扑 + 节点状态。 |
| 每个节点的运行状态(0=已停止、1=启动中、2=运行中) |
节点
工具 | 作用 |
| 添加节点。默认自动套用模板自带默认值(image/ram/cpu/qemu_*/console/config_script),与 GUI 建的节点一致;只需 |
| 修改已有节点字段(镜像、内存、qemu_* 等)。改 image/ram 下次启动生效 |
| 两接口点对点链路。精简返回 |
| 启动节点(不传 id 启动全部)。 |
| 停止节点(不传 id 停止全部) |
| 删除节点(需先停止) |
| 推送启动配置(下次启动生效) |
| 取控制台 host:port。实际下发命令请用下面的控制台工具 |
链路与故障注入
工具 | 作用 |
| 删除链路(断开两端接口) |
| 链路 Up/Down(接口 suspend,等同拔线;仅对运行中节点生效)。状态可从 |
| 注入丢包/延迟/抖动/限速(双向;仅对运行中节点生效) |
控制台交互(免手写 telnet)
工具 | 作用 |
| 高层:自动登录 + 发命令 + 读到提示符返回输出。内置 IAC 协商、ANSI/回显清理。会话复用 |
| 低层:发送原始文本(首次自动登录)。配合 |
| 读取控制台待输出 |
| 关闭一个/全部控制台会话 |
重要注意事项(v6 专属)
每个账号一个会话。 v6 每个账号只能有一个活跃实验会话。Agent 必须用 专用账号(例如
mcp),不能用你浏览时用的账号。配置PNETLAB_VIEWER_*,open_lab会自动把你的浏览账号加入 Agent 的会话 -- 两者共享同一个实时拓扑 (见上文"配置")。所有
/api/labs/session/*调用都用 JSON body。 表单编码的 body 会被 静默丢弃,表现为40000 "missing required fields"。open_lab的 path 没有前导斜杠 -- 是"2pc_1sw.unl",不是"/2pc_1sw.unl"。add_node默认自动套用模板。 建节点时会自动拉取模板自带默认值 (image/ram/cpu/qemu_arch/qemu_nic/qemu_options/qemu_version/console/config_script) 并填入,与 GUI 建的节点完全一致 -- 所以add_node("qemu","mikrotik","R1")就能直接建出可启动、可连控制台的节点,通常无需先list_images。显式传入的 字段覆盖模板默认;template_defaults=false可关闭。想换镜像时再用list_images查可用项,传image=...覆盖。console默认telnet。 PNETLab 通过控制台端口是否在监听来判断节点 "运行中"(状态 2)。console为空时不会起qemu_wrapper_telnet转发器, 端口不监听,于是状态恒为 0、控制台也连不上 -- 看起来像"启动即崩",实际 节点在跑。add_node已默认console="telnet";如需 ssh/winbox/http 等 显式传入即可。open_lab会在服务器上创建/复用一个沙盒文件(labs<name>.unl);沙盒在 首次打开时为空。close_lab释放会话绑定但保留沙盒文件(重新打开没问题)。删除节点前必须先
stop_node。不带 id 的
start_node()/stop_node()会遍历实验的节点 id(API 的 null-id "全部" 路径不可靠)。链路质量/状态在接口层。 v6 的 network 不带 quality 字段;丢包/延迟/ suspend 都是接口级。本服务器的链路工具会自动找到链路两端的接口并下发。 仅对运行中节点生效(数据存库 + 实时下发)。
控制台会话复用。 一个 PNETLab 控制台端口同时只服务一个 telnet 客户端,
run_command/console_send/console_read按节点复用会话;用完调console_close。RouterOS 默认admin/空密码,可在run_command里传username/password覆盖。
架构
LLM agent ──MCP/stdio──► pnetlab-mcp-server ──HTTP/JSON──► PNETLab v6
(本仓库) /store/public/auth/login/login (登录)
/api/labs/session/* (实验操作)
/api/list/templates|networks (列表)
/api/labs/session/interfaces/* (链路质量/状态)
telnet <host>:<console port> (控制台)client.py 是经过验证的 v6 API 客户端(含自实现 telnet 控制台,telnetlib 在
Python 3.13+ 已移除);server.py 把它封装为 MCP 工具。
Available Tools
24 toolsadd_nodeA
Add a node to the open lab. type/template come from list_templates
(e.g. type="vpcs" template="vpcs", or type="qemu" template="mikrotik").
By default (template_defaults=True) the template's built-in defaults are
auto-applied -- image, ram, cpu, qemu_arch/qemu_nic/qemu_options/qemu_version,
console, icon, config_script -- exactly what a GUI-created node inherits. So a
bare add_node("qemu", "mikrotik", "R1") produces a fully bootable node with
the right image and console; you usually don't need list_images first. Any field
you pass explicitly overrides the template default. Pass
template_defaults=False to supply everything yourself.
console defaults to the template's console, falling back to telnet for
qemu/iol/dynamips (needed for status reporting and the console tools). icon
defaults to the template's icon (e.g. mikrotik -> Router.png), falling back to
Desktop.png only when the template has none or template_defaults=False.
Pass an explicit icon to override. Node is added stopped; call start_node to
boot it.
Inputs: name (required), type (required), template (required), cpu, +18 more. Returns: result
close_labA
Leave the current lab session (releases the session binding). Also leaves the viewer account so its browser stops tracking this session.
Inputs: no named parameters. Returns: result
connect_nodesA
Connect two node interfaces with a point-to-point link. src_id/dest_id are node ids (from get_lab); src_if/dest_if are 0-based ethernet indices (0 = eth0, 1 = eth1, ...). Returns the new network_id and both endpoints (use the network_id with set_link_state/set_link_quality/ delete_link).
Inputs: src_id (required), src_if (required), dest_id (required), dest_if (required), +1 more. Returns: result
console_closeA
Close a node's console session, or all of them when node_id is None. Good practice once you are done configuring a node.
Inputs: node_id. Returns: result
console_readA
Read pending output from a node's console until it goes idle (up to
timeout seconds). Pair with console_send for interactive sessions.
Inputs: node_id (required), timeout, password, username. Returns: result
console_sendA
Send raw text to a node's console (auto-opens + logs in on first use). Lower-level than run_command: use it for multi-step interactive sessions where you need to drive the prompt yourself. Set newline=False to send a partial line (e.g. before reading). Read the response with console_read.
Inputs: text (required), node_id (required), newline, password, +1 more. Returns: result
delete_linkA
Delete a link (p2p network) at the topology layer. Unlinks both attached interfaces. Use the network_id returned by connect_nodes (or read from get_lab). The node must be stopped or the interfaces will be hot-unplugged.
Inputs: network_id (required). Returns: result
delete_nodeA
Delete a node from the open lab (stop it first).
Inputs: node_id (required). Returns: result
get_labA
Get the open lab's info, full topology (nodes/networks/connections) and node status. Use this to see what's running and how nodes are wired.
Pass compact=True to drop cosmetic noise (empty style dicts, second-console
fields, empty qemu_options) and thin each node to id/name/type/template/image/
ram/status/console plus an ethernet map of {if -> {name, network_id, suspend}}.
Compact mode is recommended for large topologies -- the full payload can be
80-100KB.
Inputs: compact. Returns: result
get_node_statusA
Get per-node running status (0=stopped, 1=building/booting, 2=running).
Inputs: no named parameters. Returns: result
get_templateA
Get a template's full editable options: available images, qemu versions, and all default field values. Use this when you need more than list_images (e.g. the right qemu_arch/qemu_nic/qemu_options for a node).
Inputs: template (required). Returns: result
join_viewerA
(Re)join the configured viewer account to the agent's current lab session. Call this if you logged into the browser as the viewer before the agent opened the lab, or to refresh the viewer's binding. Lets you watch the live topology in the PNETLab web UI.
Inputs: no named parameters. Returns: result
list_imagesA
List the disk images available for a template (e.g. mikrotik-7.23.2)
plus the default image. Call this before add_node to get a valid image
string for a QEMU node -- without one the node starts and immediately crashes.
For non-QEMU types (vpcs/iol/dynamips/docker) this lists whatever images that backend offers.
Inputs: template (required). Returns: result
list_network_typesA
List available network types (bridge, pnet0..pnet9, ovs, ...).
Inputs: no named parameters. Returns: result
list_templatesA
List installed node templates as structured entries.
Each entry is {template, name, installed} where template is the id
usable in add_node and installed is False when the image is missing
(PNETLab marks these with a .missing suffix).
Inputs: no named parameters. Returns: result
node_consoleA
Get a node's console connection info (telnet/SSH host:port) so the agent can interact with the device CLI. For actual command execution prefer run_command / console_send + console_read, which handle the telnet handshake and login for you.
Inputs: node_id (required). Returns: result
open_labA
Open a lab for editing/running. path is the lab file name with NO leading
slash, e.g. "2pc_1sw.unl". Creates/reuses the lab's sandbox and binds it to this
session. Must be called before add_node / start_node / get_lab / etc.
If PNETLAB_VIEWER_USERNAME is configured, the viewer account (e.g. your admin) is auto-joined to this session so you can watch the live topology in the browser at the PNETLab web UI while the agent works.
Inputs: path (required). Returns: result
push_configA
Push a startup configuration to a node (applied on next boot).
Stores the config text and enables it for boot injection. For this to take effect the node must be (re)started after the push - the config is injected during boot, not into a running node.
Config format is device-specific. For VPCS use plain VPCS commands, one per
line, e.g. ip 192.168.1.1 24. For IOS/IOL routers use the normal
hostname ... / interface ... running-config style. For interactive
CLIs (e.g. RouterOS) prefer run_command.
Note: this tool also flips the node's config flag so PNETLab actually loads the stored config on boot (without that flag the stored config is silently ignored - a known PNETLab configs/edit quirk).
Inputs: config (required), node_id (required). Returns: result
run_commandA
Run a single CLI command on a running node and return its output.
Opens a telnet console, handles the IAC handshake, logs in if the device asks
(default admin/empty password -- pass credentials for devices that differ),
sends the command, and reads until the output goes idle (or wait_for
regex matches). The session is pooled and reused across calls; call
console_close when finished with a node.
Examples: run_command(1, "/ip address print") on a MikroTik; run_command(1, "ping 10.0.0.2 count 4", wait_for="packet-loss") ; run_command(2, "show ip interface brief") on IOS; run_command(3, "ping 10.0.0.1") on VPCS.
Gotcha: a single ? for help often does not trigger under telnet; use
run_command with the full command instead. For long-running monitors, pass
wait_for to match the final summary line.
Inputs: command (required), node_id (required), timeout, password, +2 more. Returns: result
set_link_qualityA
Inject impairment on a link: packet loss (percent), one-way delay (ms), jitter (ms), and/or bandwidth limit (kbit/s). Pass only the dimensions you want to set. Applied to both directions of the link. Only affects running nodes. Clear an impairment by passing 0 for that dimension.
Inputs: network_id (required), loss, delay, jitter, +1 more. Returns: result
set_link_stateA
Bring a link up or down at the topology layer (physical-style fault
injection). up=True restores the link, up=False suspends it. This is
not the same as disabling an interface inside the device CLI -- the device
sees its port go down, matching a real cable pull. Only affects running nodes.
Use the network_id from connect_nodes / get_lab. Current state is readable
from get_lab (each ethernet has a suspend field: 1=suspended).
Inputs: up (required), network_id (required). Returns: result
start_nodeA
Start one node, or every node in the lab if node_id is omitted.
With check=True (single node only), poll status for a few seconds after
starting and report a diagnostic if the node crashes back to stopped (usually
a missing/invalid image or too little ram).
Inputs: check, node_id. Returns: result
stop_nodeA
Stop one node, or every node in the lab if node_id is omitted.
Inputs: node_id. Returns: result
update_nodeA
Edit an existing node's fields. Only the fields you pass are changed.
Use this to fix a node that crashes on boot (e.g. set a missing image), or to tweak ram/qemu_nic without deleting and recreating. Image/ram/qemu_* changes take effect on the next (re)start, not on a running node. Accepts the same field names as add_node.
Inputs: node_id (required), cpu, ram, top, +16 more. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 24 tools
Each tool targets a distinct resource and action. While console tools (run_command, console_send/read, console_close, node_console) share a domain, their purposes are clearly differentiated: high-level command execution, low-level interactive sessions, session management, and connection info retrieval. Similarly, list_images vs get_template are explained with usage guidance.
All tool names follow a consistent verb_noun pattern using lowercase with underscores (e.g., list_templates, open_lab, set_link_state, push_config). The one noun-like name 'node_console' still fits as a getter (get console info) and does not break the overall pattern.
24 tools is on the higher end but appropriate for the complexity of a network emulation server. The count covers lab lifecycle, node lifecycle, link management, console interaction, and template/image introspection. It is slightly heavy but each tool justifies its existence.
The surface covers most workflow needs: open/close/get lab, add/update/delete/start/stop nodes, connect/delete links, link fault injection, console command execution, and config push. Minor gaps include no explicit lab listing/creation/deletion and no standalone network management, but these are workable through existing tools (open_lab creates labs, connect_nodes creates networks).
Maintenance
Related MCP Connectors
MCP server for Pentest-Tools.com: run scans, manage findings and reports via your preffered LLM.
MCP server for AI dialogue using various LLM models via AceDataCloud
An MCP server that provides an API to LLMs to manage their JumpCloud resources.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceAn MCP server that allows LLMs to create, configure, validate, and explain Cisco Packet Tracer network topologies. It provides a comprehensive suite of tools for generating deployment scripts, CLI configurations, and automated network troubleshooting.1,476 PyPI337MIT
- AlicenseBqualityDmaintenanceAn MCP server that gives Claude and other LLM agents programmatic access to EVE-NG network labs, enabling creation, configuration, and management of virtual network topologies through natural language.1420 PyPI2Apache 2.0
- AlicenseNot gradedqualityDmaintenanceMCP server for interacting with NetBox API, enabling users to query and manage NetBox resources such as devices through natural language.MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server that enables AI agents to manage Defined Networking / Managed Nebula infrastructure, supporting tasks like network topology design, host provisioning, and security auditing through natural language.4MIT