ptmcp
by zoujiapeng
README.md
# ptmcp — 后台操控 Cisco Packet Tracer 的 MCP(面向 agent)
在**不抢焦点、不弹任何对话框**的前提下,让 agent 建拓扑、把链路画成绿的、跑 CLI、存 `.pkt`。
在 PT 8.0.0.0212 上实测通过;设计目标是一台陌生电脑上装好 PT 后,agent 拿到本仓库就能干活。
```
pip install -e .
python -m ptmcp bootstrap # 安装 PT 侧脚本模块并自检
python -m ptmcp status
```
MCP 客户端接入:
```
claude mcp add --scope user --transport stdio ptmcp -- python -m ptmcp serve
```
## 它解决的问题
| 症状 | 原因 | 本仓库的做法 |
|---|---|---|
| 链路一直是红的 | 交换机↔交换机用直通线,PT 判 MDI/MDX 错 | `catalog.pick_cable()` 自动选交叉线(8101),主机/路由接交换机用直通(8100) |
| 脚本引擎卡死、请求全超时 | `fileSaveAs()` 覆盖已存在文件 → "Overwrite File?" 模态框阻塞引擎 | `pt.save()` 先删目标、存到唯一临时名、再 `os.replace` |
| `fileNew()` 弹"是否保存" | 默认带提示 | 统一用 `fileNew(false)` |
| `addDevice` 后链路/端口对象失效 | `fileNew()` 会让缓存的工作区句柄失效 | 先 `fileNew(false)`,再取工作区句柄 |
| 拿不到 CLI 输出 | PT 的 IPC 不暴露终端回显(`getCurrentHistory().history` 只有命令串) | `pt_ping()` 在设备 CLI 里真跑 ping(人打开 CLI 能看到 Reply),可达性用 `pt_verify_l2()` 做机器判定 |
## 工具(MCP)
| tool | 作用 |
|---|---|
| `pt_status()` | 画布上的设备/链路 + 引擎是否在轮询 |
| `pt_new()` | 清空画布(无对话框) |
| `pt_build(spec_json, save_path)` | 建图:设备 / 链路(自动选线)/ IP / CLI,并逐条报链路 up/down |
| `pt_wait_links(spec_json, timeout_s)` | 轮询到全部链路 up(STP 收敛需要几秒到几十秒) |
| `pt_cli(device, commands)` | 在 IOS 设备上执行 CLI |
| `pt_ping(source, target_ip, count)` | 在设备 CLI 里执行 ping |
| `pt_save(path)` | 存 `.pkt`,永不弹覆盖对话框 |
| `pt_run_js(code)` | 逃生口:直接跑 JS(结尾 `throw new Error(text)` 回传) |
拓扑规格:
```json
{
"devices": [{"name": "S1", "model": "2950T-24", "x": 260, "y": 140},
{"name": "PC0", "model": "PC-PT", "x": 150, "y": 400}],
"links": [{"a": "S1", "a_port": "FastEthernet0/1",
"b": "PC0", "b_port": "FastEthernet0"}],
"ips": [{"device": "PC0", "port": "FastEthernet0",
"ip": "192.168.2.1", "mask": "255.255.255.0"}],
"cli": {"S1": ["vlan 2", "name VLAN2", "exit"]},
"save": "C:\\pt\\lab.pkt"
}
```
`links[].cable` 可省略(`auto`):交换机组之间自动交叉、其余直通。
## 通道原理
PT 的 Script Module 是唯一能在 PT 进程内跑代码的正规入口。本仓库复用
[MCP-Packet-Tracer](https://github.com/Mats2208/MCP-Packet-Tracer) 的 `V5.2.pts`
(MIT,bootstrap 自动下载并校验 SHA-256)作为 PT 侧的执行器,Python 侧只往文件邮箱写命令:
```
%LOCALAPPDATA%\packet-tracer-mcp\bridge\req_<id>.js -> PT 执行
%LOCALAPPDATA%\packet-tracer-mcp\bridge\res_<id>.txt <- 结果
%LOCALAPPDATA%\packet-tracer-mcp\bridge\alive.txt <- 心跳
```
因此 PT 可以最小化、被遮挡、甚至窗口关掉,自动化照跑。
## 一次性 GUI 步骤
PT 8 没有"静默注册脚本模块"的接口,模块只能通过界面登记一次:
`Extensions → Scripting → Configure PT Script Modules → 选中 MCP BUILDER → Settings → On Startup → Start`
登记过一次、设为 On Startup 后,之后每次启动 PT 都会自动轮询,agent 全程无需人介入。
## 已验证 / 未验证
实测(PT 8.0.0.0212):
- 案例27/28/29 三张拓扑由 JSON 生成并保存;案例28 的 S1–S2 干道自动选交叉线后 `allUp: true`
- `pt_verify_l2()` 读回:`S1:FastEthernet0/13 up=true mode=2(trunk)` / `S2` 同
- `pt_save()` 全程无对话框(含覆盖场景)
- PC(PC-PT)没有 `getIpcTerminalLine`,PC 的 ping 输出**读不到**;需要机器判定时用 `pt_verify_l2()`,
需要"看得见的 ping"时用 `pt_ping()` 把命令打在 IOS 设备 CLI 上
未验证:
- PT 9.x(API 可能有差异)
- 无人值守下自动登记脚本模块(PT 无此接口)
## 致谢
- [MCP-Packet-Tracer](https://github.com/Mats2208/MCP-Packet-Tracer)(MIT)提供的 PT 侧脚本模块
- [PTBuilder](https://github.com/kimmknight/PTBuilder)(Kim Knight)——PT 脚本引擎 API 的开山实践
MIT License.
## 1.1:像 CLI 一样用它
新增的读回层让 agent 不再"盲配"——PT 会把每台设备序列化成 XML,我们从中还原出 CLI 的输出:
```powershell
python -m ptmcp show S1 run # show running-config(真配置行)
python -m ptmcp show S1 vlan # show vlan(VLAN 库:id/name)
python -m ptmcp show S1 vtp # show vtp status(mode/domain/revision)
python -m ptmcp show S1 interfaces # 每端口 up/protocol/mode/vlan/native/duplex
python -m ptmcp show S1 mac # MAC 列表
```
批量执行(一次往返,复杂脚本也不费时):
```powershell
python -m ptmcp batch ops.json
```
```json
[{"op":"cli","device":"S1","commands":["vlan 30","name VOICE","exit"]},
{"op":"port","device":"S1","port":"GigabitEthernet0/2","mode":1,"access_vlan":30},
{"op":"ip","device":"PC1","port":"FastEthernet0","ip":"10.0.0.1","mask":"255.255.255.0"},
{"op":"power","device":"R1","on":true},
{"op":"rename","device":"S1","to":"CORE"},
{"op":"module","device":"R1","slot":0,"model":"HWIC-2T"}]
```
设备与 UI 操作:
```powershell
python -m ptmcp power R1 off | on # 物理电源
python -m ptmcp module R1 0 HWIC-2T # 插模块(自动断电/上电 + skipBoot)
python -m ptmcp rename S1 CORE
python -m ptmcp mode simulation # logical/physical/realtime/simulation
python -m ptmcp panels --left 0 --bottom 1
python -m ptmcp window maximize
python -m ptmcp clip # 读写 PT 剪贴板
python -m ptmcp ui # 一次看全 UI 可控项 + 当前状态
```
MCP 工具同步新增:`pt_show / pt_batch / pt_power / pt_rename / pt_add_module /
pt_ui_controls / pt_view_state / pt_switch_mode / pt_panels / pt_window /
pt_clipboard / pt_status_line`。
### 边界(实测,不猜)
- **菜单项枚举不到**:Qt 的 QMenu 是懒构建,PT 8 里未展开时 `count()` 返回 0。菜单能做到的事都有等价的脚本入口(面板显隐、模式切换、窗口几何、剪贴板、`writeToPT`);只有必须"真点一下"的,才交给 computer-use 层。
- **CLI 屏幕文本读不到**:`getCurrentHistory().history` 只有命令串,没有回显;所以读配置走 `serializeToXml`,读端口状态走 IPC 对象。
- **PC 没有 IOS 控制台**,其 ping 只能靠 GUI 的 Command Prompt;机器判定用 `pt_verify_l2`。
## 1.2:动手前先预检(把"红链路"挡在建图之前)
```powershell
python -m ptmcp precheck spec.json # 只体检,不改画布
python -m ptmcp precheck spec.json --fix # 顺手把线缆/端口修好
```
检查项(每一条都来自我们实际踩过的坑):
| code | 含义 | 后果 |
|---|---|---|
| `bad-model` | 型号不在 PT 目录里 | addDevice 直接失败 |
| `dup-name` | 设备重名 | PT 静默改名,后续按名字全查不到 |
| `overlap` | 两台设备同一个坐标 | 图标叠在一起 |
| `no-device` | 画布上没这台(live 模式) | 连线失败 |
| `no-port` | 端口名不存在 | `createLink` 返回 false,链路凭空消失 |
| `port-busy` | 端口已被占用 | 同上 |
| `bad-cable` | 交换机↔交换机用了直通线 | **两端永远 down,图上是红线** |
`--fix` 会做两件确定性修复:按设备类型重算线缆,并把冲突/不存在的端口换到同设备下一个空闲端口(修复时会预留端口,避免两条链路又撞到一起)。
实测:
```
before = 8 issues
after --fix → 剩余 2 issues(bad-model / dup-name / overlap 属于人必须决策的)
links after fix: S1:FastEthernet0/5 <-> S2:FastEthernet0/5 cable=cross
S1:FastEthernet0/6 <-> S2:FastEthernet0/6 cable=cross
```
`pt.build()` 现在会自动先跑 precheck(新建画布时跳过 live 检查,否则会误报),报告里带 `precheck.ok`。
## 1.3:路由配置读回(配置级)
```powershell
python -m ptmcp routing R1
```
```json
{"protocols":[{"proto":"ospf 1","lines":["log-adjacency-changes",
"network 10.1.0.0 0.0.255.255 area 0","network 10.0.0.0 0.0.0.3 area 0"]}],
"static_routes":[], "interfaces":["10.1.2.1 255.255.255.0", "..."]}
```
### 这里有一条硬边界(本轮实测确认)
把路由器 XML 全量 dump 出来查过:里面只有 `ENABLED_ROUTER`,**没有路由表、没有 OSPF 邻居、没有 trunk allowed-vlan、没有 STP 状态**。
这些是 IOS 进程里的动态状态,PT 只把它们送到 CLI 屏幕,而屏幕文本读不到。所以:
- **能读**:running-config、VLAN 库、VTP 状态、端口 up/proto/mode/vlan/native、MAC、路由**配置**
- **读不到**:`show ip route` 的学到的路由、`show ip ospf neighbor`、`show interfaces trunk` 的 allowed 列表、STP 端口角色
- 需要这些时:`pt_cli` 把命令打进设备,人/computer-use 看屏幕;机器判定仍用 `pt_verify_l2` + `pt_ping`
## 1.4:自动修复闭环(assert → repair → re-assert)
```powershell
python -m ptmcp fixloop spec.json --plan # 先看会改什么,不动手
python -m ptmcp fixloop spec.json # 直接闭环修复(默认 3 轮)
```
它读 spec 里的 `asserts`,和实时设备状态比对,把**能确定修复**的偏差翻译成 `pt_batch` 的 ops,一次往返执行,然后重新断言;直到通过 / 没有可自动修的 / 轮数用完。
**能自动修**:access vlan、trunk 模式与 native、两端 trunk 一致性、缺失 VLAN、VTP mode/domain、管理性 down 的接口(`no shutdown`)。
**不自动修(列为 manual)**:`same_vlan`/`diff_vlan` 与布线矛盾(这是设计问题)、线缆类型错(要重建)、缺硬件。
实测(故意把 S1 改坏:native 1、f0/2 落回 VLAN1):
```
改动前 asserts:failed = 3
- S1:FastEthernet0/2 vlan=1 mode=1, expected access 3
- S1:FastEthernet0/13 trunk native=1 (need 2)
- trunk mismatch S1:FastEthernet0/13-S2:FastEthernet0/13: native 1 vs 2
fixloop:
round 1 7 ops(S1/S2 的 port 属性 + IOS CLI)
round 2 already correct
ok=True applied=1 final: 11 passed / 0 failed
```
一轮就能收敛,因为修复同时改**运行态属性**(立即生效,链路判定用)和**下发 CLI**(重启后仍然成立)。
## 1.5:全新复杂拓扑实测(园区双核心)
`specs/campus.json`:**15 设备 / 14 链路** —— 2×2911(单臂路由,VLAN10+30 各两条子接口 + OSPF)、4×2960 接入(trunk 上行)、8 台 PC + 1 台服务器。
```
python -m ptmcp build specs\campus.json
→ precheck: True | allUp: True | asserts: 41 passed / 0 failed | 7.2s
→ saved C:\pt\campus_v1.pkt (70,377 bytes)
```
用到的断言:4 个 trunk 检查、3 个 access VLAN、3 组同 VLAN 可达、1 组跨 VLAN 隔离、2 个 VLAN 库存在性。
过程中真实遇到并修掉的坑:
1. **3560-24PS 没有 Gi0/3-4**(只有 24×Fa + 2×Gi),我第一版把上行接在不存在的端口上 → 4 条链路凭空消失。precheck 的 live 检查在建新画布时是跳过的,所以没拦住——这是已知盲点,改进方向是给每个型号内置端口表。
2. **PT 8 的 3560 不接受 `switchport` 系列命令**(`no switchport` 可以,反向不行),trunk 配不上。最后改用 2911 单臂路由(成熟可靠),3560 这类设备在 PT 8 上的 L3 交换配置仍属未打通项。
3. **3560 的 FastEthernet 端口对象没有 `getAdminOpMode`**(返回 -1),导致断言误判。已加**运行配置回退**:API 拿不到就用 `show run` 里的 `switchport mode trunk` 判定,并修了 `_long_if` 把 `FastEthernet0/1` 误转成 `FastEthernetstEthernet0/1` 的 bug。
### 已知小瑕疵
`pt_save` 为规避覆盖弹窗会先写临时名再改名,因此 **PT 标题栏显示的是临时文件名**(`...tmp.pkt`),磁盘上的 `campus_v1.pkt` 是正确的;重开该文件标题即正常。
## 1.6:完全不依赖 computer use(实测)
之前的说法是"注册模块要手动点一次界面"。**这个前提被推翻了**:把 `V5.2.pts` 放进
`<PT 安装目录>\extensions\MCPBuilder\` 之后,PT **启动时会自动加载并启动脚本模块**,
不需要任何点击。
因此整条链路现在可以零 GUI 自动化跑完:
```powershell
python -m ptmcp bootstrap # 找 PT、校验并安装 .pts(纯文件操作)
python -m ptmcp start # 若未运行则拉起 PT,并等待脚本引擎上线
python tests/headless.py # 冷启动 → 建图 → 全绿 → 断言 → 存 .pkt → 截图
```
冷启动实测(先 `taskkill` 掉 PT,再全自动跑,两次结果一致):
```
[1/6] killing any running Packet Tracer...
[2/6] launching Packet Tracer (no GUI automation)... engine alive after 4.4s
[3/6] building the topology... 15 devices / 14 links in 72.2s
[4/6] waiting for every link to come up... all_up=True after 0.3s
[5/6] assertions... 41 passed / 0 failed
[6/6] saving .pkt + screenshot (PrintWindow, no input)...
C:\pt\campus_v1.pkt (70,313 bytes) + C:\pt\campus_v1.png
```
截图不再是 computer-use:`ptmcp/shot.py` 用 Win32 `PrintWindow(PW_RENDERFULLCONTENT)`
把窗口渲染到位图再转 PNG —— 不移动鼠标、不发送按键、不用 UIA,PT 被遮挡也能拍。
### 这一轮修掉的真实缺陷:冷启动丢配置
冷启动时 `vlan 10 / name USERS` 会**静默丢失**:交换机还在 boot,CLI 送进去被丢弃。
现在 `build()` 分两阶段:先建设备/链路/IP,再 `wait_ready()` 轮询 `isBooting()` 直到设备就绪,
最后才下发 CLI。代价是冷启动建图时间从 7s 变成 ~72s(等设备启动),热启动仍是 8s 级。
## 1.7:同机横评(三个都装了)与一个硬结论
按"把他们全部安装"实测,结论不是"都装上就完事":
| | ptmcp(本仓库) | cisco-pt-mcp 0.1.6 | packet-tracer-mcp 0.9.0 |
|---|---|---|---|
| 安装 | 复制 `.pts` + `pip install -e .` | 复制 `.pts` + `uvx`/pip | pip + 它自己的 `V5.2.pts` |
| 工具数 | 26 | **18** | **61** |
| 冷启动无人值守 | ✅ 4.3s 起来并自动轮询 | ❌ `getNetwork` 30s 超时:`Packet Tracer plugin did not connect within 30s`(要先手点 Extensions 菜单打开它的窗口) | ❌ PT 8.0 上 `addDevice` 报 `ReferenceError` |
| PT 8.0 建图 | ✅ 15 设备/14 链路全绿 | 未验证 | ❌ |
| 断言 / 自动修复 | ✅ 8 类 + fix loop | ❌ | ❌ |
**两个 PT 侧扩展不能共存**(本项目实测):把 `cisco-pt-mcp.pts` 放进
`extensions\` 后,PT 启动时我们的模块**不再轮询**(`alive.txt` 停更、请求超时);
移除它的 `.pts` 后,我们的引擎 4 秒内恢复。所以在 PT 8.0 上只能二选一。
### 从他们的开源代码里学到、并已并入本项目的接口
`cisco-pt-mcp` 的扩展源码是公开的(MIT),里面给出了我们之前没找到的两个入口:
```js
ipc.simulation() // setSimulationMode / forward / resetSimulation
// getFrameInstanceCount / getFrameInstanceAt
ipc.appWindow().getUserCreatedPDU().addSimplePdu(src, dst) // 真·ICMP PDU
ipc.commandLog() // getEntryCount / getEntryAt / entry.getCommand()
```
`ptmcp/pdu.py` 已实现 `ping(source, destination)`(建 PDU → 步进仿真 → 判定)
和 `command_log(device)`。**状态:实验性**——PDU 能建、能步进、能读到逐帧状态,
但"成功"判定还不稳(PT 的 PDU 列表会累积旧 PDU,`resetSimulation()` 不清空它们,
导致帧序列混入上一次的流量)。这条要真正可用,还得先解决 PDU 清理。
## 1.8:把"终端"接进来(并整合竞品的强项)
### 终端操作(已实测)
```powershell
python -m ptmcp term S1 # 提示符/模式/待输入行
python -m ptmcp run S1 "enable" "conf t" "hostname CORE1" "exit"
python -m ptmcp type S1 "show vlan brief" # 逐字符输入(等于人打字)
```
`run()` 会把**每条命令执行后的提示符**回传,这就是"命令真的在终端里跑了"的证据:
```
enable :: S1(config)#
configure terminal :: S1(config)#
vlan 40 :: CORE1(config-vlan)#
interface FastEthernet0/3 :: CORE1(config-if)#
switchport mode trunk :: CORE1(config-if)#
```
同一台设备随后读回:`FastEthernet0/3 = access vlan 40`、`GigabitEthernet0/1 = trunk`。
`pt_term_println(device, "文本")` 还能往终端**显示区**写一行,人打开 CLI 就能看到。
**读不到的仍然是"屏幕回显"**:`TerminalLine` 只给 prompt/mode/pending input,
它的 history 对象里不是文本。所以判定一律走设备状态(`show run`/VLAN/端口)或 PDU。
### 整合竞品的强项(MIT,直接调用,不重写)
`ptmcp/borrow.py` 调用 `packet-tracer-mcp` 的生成器,把它们多年调过的 IOS 配置生成逻辑
接到我们自己的终端执行器上:
```python
from ptmcp.borrow import vlan_cli, subinterface_cli, acl_cli, nat_cli, hardening_cli, port_security_cli
lines = vlan_cli([{"vlan_id": 40, "name": "CAMPUS"}],
[{"switch": "S1", "port": "FastEthernet0/3", "vlan_id": 40}],
[{"switch": "S1", "port": "GigabitEthernet0/1"}], "2960-24TT")
# -> vlan 40 / name CAMPUS / exit / interface ... / switchport mode access / ...
term.run("S1", ["enable", "configure terminal"] + lines)
```
没装 `packet-tracer-mcp` 时会明确报错并给出安装命令,ptmcp 其余功能不受影响。
### PDU ping:仍未定论(不吹)
`pt_pdu_ping(PC0, PC1)` 能建 PDU、能步进、能读到逐帧状态(`PC0:buffered/PC0:dropped`),
但**正例也没通过**:把 PC1 也划到 VLAN 2 后仍然 `ok=false`。所以这条现在是"能下发、能观察、
不能作准",README 里标记实验性,判定仍以 `pt_asserts` + `pt_verify_l2` 为准。
## 1.9:PDU ping 跑通了(并抓到我自己的两个错)
经过三处修正,`pt_pdu_ping` 现在能给可信结论:
1. **判定规则**:PT 在目的主机上把帧标成 `sent`,回程才在源主机出现 `accepted`。
所以成功 = "目的设备上出现过帧" **且** "其后源设备上有 accepted 帧"。
2. **realtime 等待**:主机启动与 ARP 都发生在实时时钟里;一进仿真模式时钟就冻结。
必须先在实时模式等到 `isBooting()=false`,再切仿真步进(否则首帧必丢)。
3. **模式卫生**:`ping()` 结束一定切回 realtime,否则后续建图/断言全乱。
实测(干净的两主机小实验):
```
H1 -> H2: ok=true reached_destination=true got_reply=true
trace: H1:buffered → H1:sent → SW:sent → H2:sent → SW:sent → H1:accepted
```
**它立刻指出了我真实存在的配置问题**(这正是它的价值):
```
PC11 -> PC12 : ok=true ✅ 同 VLAN
PC21 -> SRV1 : ok=false ❌ R1:dropped (服务器在 A2 后面,走 R1 Gi0/2.30)
PC11 -> PC41 : ok=false ❌ R2:dropped (跨路由器,OSPF 路径)
```
`SRV1` 原本被我配在 `10.30.0.0/24`,但它实际挂在 `R1 Gi0/2` 这条链路的 VLAN30
(`10.31.0.0/24`)——**是 spec 写错了**,已改成 `10.31.0.10/24`。跨路由器那条
(`PC41`)仍未通,说明 `R1↔R2` 的 OSPF 没有真正起效,这是下一个要查的点。
### 一个我自己造出来的坑(已回滚)
为了让 CLI 更稳,我曾加过"读终端模式,不是 config 就补 enable/configure terminal"。
结果 **`getIpcTerminalLine().getMode()` 会把还没初始化的控制台弄坏**:之后所有设备的
`getPrompt()` 都变空,`enterCommand` 变成静默 no-op,配置全部不生效。
已回滚成 PTBuilder 的 `enterCommand("!", "global")`,配置立刻恢复(`hostname applied: True`,
prompt 变回 `S1(config)#`)。教训:**没初始化前不要碰终端对象**。
## 1.10:陌生电脑装什么、能不能"一句话搞定"
结论:**装完之后可以一句话建图并全绿,但全新机器上有 4 个前置**,其中 3 个可自动、1 个只能点一次。
`install.ps1` 把能自动的都做了(本机实测通过):
```
== 1/5 Packet Tracer == found C:\Program Files\Cisco Packet Tracer 8.0 version 8.0.0.0212
== 2/5 Python package == package installed
== 3/5 PT script module == copied to ...\extensions\MCPBuilder
== 4/5 start Packet Tracer == already running and polling
== 5/5 smoke test == allUp: true
```
前置条件:
| # | 条件 | 谁来做 |
|---|---|---|
| 1 | 已装 Packet Tracer(**仅 8.x 验证过**,PT 9 的脚本 API 不同) | 人工 |
| 2 | Python 3.10+、能访问 PyPI | `install.ps1` 自动 |
| 3 | 能写 `C:\Program Files\...\extensions\` | **必须管理员 PowerShell**,脚本会检测并提示 |
| 4 | PT 首次启动的 NetAcad 登录,点一次 "Continue as a Guest" | **只能人工**(之后 PT 记住,不再问) |
| 5 | Windows(PrintWindow/tasklist/路径都是 Win32) | — |
| 6 | 可选:`pip install packet-tracer-mcp`(借用它的 ACL/NAT 生成器) | `install.ps1` 之外单独装 |
已知功能缺口(会影响"跟本机一样"这句话):跨 VLAN / 跨路由的 `pt_pdu_ping` 还没跑通
(`R1:dropped` / `R2:dropped`),3560 的三层交换配置在 PT 8 上配不上;这两条见 `HANDOFF.md`。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues