--- name: packet-tracer description: >- Use when building, automating, or debugging Cisco Packet Tracer networks through the `packet-tracer` MCP (the `pt_*` tools) — planning topologies from natural language, live-deploying into a running Packet Tracer, adding devices / links / modules, applying ACL / NAT, exporting configs, or writing raw Script-Engine JS via `pt_send_raw`. This is the authoritative reference for the tool catalog, the mandatory discover→plan→validate→deploy workflow, the EXACT PT Script-Engine API (so you never invent method names), and the known rough edges. Load it before driving the MCP so you act from verified facts, not guesses. --- # Packet Tracer MCP — operating guide A Model Context Protocol server (`packet-tracer`) that drives **Cisco Packet Tracer**: plan a topology, validate it, generate IOS/PTBuilder artifacts, and **live-deploy** into a *running* PT over an HTTP bridge. The `pt_*` tools are your only interface — plus raw JS via `pt_send_raw`. ## ⛔ Prime directive: DISCOVER, never invent The single biggest failure mode for a model driving this MCP is **guessing** — a model name, a port name, a slot id, a cable type, a module name, or a Script-Engine API method. Every one of these is **discoverable**, and a wrong guess either fails validation or (for raw JS) **freezes Packet Tracer** (see the modal-freeze rule below). Rules: 1. If you did not read it from a tool result, an MCP resource, or this skill, **you do not know it** — look it up first. 2. Before planning/deploying: `pt_list_devices`, `pt_list_templates`, `pt_get_device_details`, and `pt_list_modules` (if installing modules). 3. Before any raw JS: use only methods in the **Verified Script-Engine API** table below. If you need something not listed, probe it **inside a try/catch** before relying on it. 4. After every mutation, read back with `pt_query_topology` / `pt_export_topology` and confirm. 5. When something isn't in the catalog, say so — do not fabricate a device/module to fill the gap. ## The bridge (mental model) ``` LLM ──▶ MCP server ──▶ HTTP bridge :54321 ──▶ MCP Control Center (PT extension) ──▶ PT Script Engine ``` - The MCP queues JS at `:54321`; the PT extension polls `/next` every 500 ms and runs each command via `$se('runCode', …)` in PT's **Script Engine**, posting results back to `/result`. - **`XMLHttpRequest` does NOT exist in the Script Engine** (only in the extension webview). That's why all I/O goes through the bridge. - Confirm the link first with **`pt_bridge_status`** → must say *"ACTIVE and CONNECTED"*. If "NOT connected", the user opens **Extensions → MCP BUILDER** in PT (or dismisses a stuck error modal). ## Mandatory workflow - New topology, one-shot: **`pt_full_build`**. - New topology, stepwise: `pt_list_devices` → `pt_plan_topology` → `pt_validate_plan` → `pt_live_deploy`. - Edit a live topology: `pt_bridge_status` → `pt_query_topology` → `pt_add_*`/`pt_rename_device`/…. - Add modules: `pt_query_topology` → `pt_list_modules(router_model=…)` → `pt_install_modules_batch`. ## Tool catalog (62) **Discovery / read-only:** `pt_list_devices`, `pt_get_device_details(model|alias)`, `pt_list_templates`, `pt_list_modules(router_model, category)`, `pt_list_projects`, `pt_load_project`, `pt_bridge_status`, `pt_query_topology`*, `pt_export_topology`* (*need bridge). **Pure planning/generation:** `pt_plan_topology`, `pt_estimate_plan`, `pt_validate_plan`, `pt_fix_plan`, `pt_explain_plan`, `pt_generate_script(include_configs)`, `pt_generate_configs`, `pt_full_build(deploy=…)`. `pt_plan_topology`/`pt_full_build` accept `vlans`, `dual_stack`, `ipv6_base`, `wireless_laptops`. **Disk / clipboard:** `pt_export`, `pt_deploy`. **PT project files:** `pt_save_project(filename)` / `pt_open_project(path)` — these write and read the real `.pkt`, which is NOT what `pt_export` does (that one dumps the plan and scripts to disk). `pt_open_project` replaces the current topology. **Live deploy & edit:** `pt_live_deploy` (auto-reconciles dropped devices), `pt_add_device`, `pt_add_link`, `pt_delete_link`, `pt_delete_device`, `pt_rename_device`, `pt_move_device`, `pt_set_port`, `pt_send_raw`, `pt_add_module`, `pt_install_modules_batch`. **ACL / NAT (live):** `pt_apply_acl`, `pt_apply_acl_object`, `pt_remove_acl`, `pt_remove_acl_object`, `pt_apply_nat`, `pt_remove_nat` — all accept `dry_run=True` to preview CLI without touching PT. **Config-driven (live, dry_run):** `pt_apply_vlan` (VLAN/trunk/inter-VLAN subinterfaces), `pt_apply_stp`, `pt_apply_port_security`, `pt_apply_hardening` (hostname/banner/enable-secret/users/SSH), `pt_apply_interface_tuning` (serial clock-rate + OSPF/EIGRP per-interface knobs). **Verification (live):** `pt_diff` (plan vs live), `pt_health_check` (down links, dup IPs, cabled-no-IP). `pt_verify_connectivity` gives **three** verdicts, not two: `CONECTIVIDAD OK` (every packet returned), `CONECTIVIDAD PARCIAL` (some loss — normal on a first ping because of ARP, so re-run once before calling it a fault) and `SIN CONECTIVIDAD`. It works from routers and switches, not only hosts. `pt_health_check` no longer lists layer-2 ports as "cabled without IP", so anything it does report there is a real host that never got its DHCP lease. **Live-state inspection (read the device, not the plan):** `pt_audit_security(device="")` (security posture with severities; never returns passwords or hashes, only the algorithm label), `pt_inspect_ports(device, only_linked)` (per-port line/protocol, MAC, duplex, bandwidth, MTU, CDP, NAT mode, applied ACLs; flags cabled-but-down), `pt_read_vlans(switch)` (real VLAN database, separates your VLANs from PT's factory ones), `pt_device_power(device, on)` (power-cycle with read-back). **Canvas (capture & annotate):** `pt_screenshot(filename, fmt, output_dir)` writes the image to disk and returns the **path** — never the bytes, they would flood the context. `pt_add_note(x, y, text)` writes a label; font size is not settable. `pt_clear_annotations(kind)` removes annotations only, never devices or links (drawings included — redraw circles after clearing notes). There is no drawing *tool*, but PT's `drawCircle` **does** work from raw JS once you pass all seven arguments — see "Drawing IS possible" below. Colours still don't take. Canvas coordinates match `pt_add_device`: routers ~y=100, switches ~y=250, hosts ~y=400. Recipe for a diagram worth showing: `pt_full_build` → `pt_add_note` per subnet and link → `drawCircle` per LAN → `pt_screenshot` → adjust and repeat. **Backup / workspace:** `pt_backup_config(device, include_xml)` (real startup-config + serial, config-register, boot images), `pt_project_metadata(description="")` (saved filename, PT version, description, device/link count; pass `description` to set it), `pt_workspace_options(...)` — tri-state flags, `-1` leaves a setting alone. Turn `auto_cabling=0` before a scripted build if you need links on exact interfaces, and check `external_network_access` before assuming traffic stays in the simulator. **DHCP on a Server-PT:** `pt_configure_dhcp_server(device, network, mask, gateway, dns, start_ip, max_users, pool_name="serverPool", port="FastEthernet0", enabled, drop_factory_pool, remove, dry_run)` creates/edits a pool on a **server** (GUI: Services > DHCP), turns the service on and reads every pool back. Without `network` it only reads. Router DHCP is a different thing: IOS CLI `ip dhcp pool`, which is what `dhcp=True` in the planner emits. Give the server a static IP inside the pool's subnet first. **Telemetry / QoS:** `pt_apply_netflow(device, name, destination_ip, udp_port, version, source_port, monitors, remove, dry_run)` configures a NetFlow exporter directly (not via CLI) and reads it back to confirm. `pt_read_qos(device)` is **read-only**: QoS cannot be created programmatically, so author class-maps and policy-maps with IOS CLI and use this to verify. **Simulation:** `pt_simulation_mode(on)` (Realtime ↔ Simulation), `pt_simulation_step(action, times)` (`forward`/`back`/`reset`), `pt_read_packet_trace(limit, device, include_decisions)` — the event list plus PT's own per-OSI-layer explanation of each decision (same text as the GUI's PDU Details pane). Workflow: `pt_simulation_mode(on=True)` → generate traffic (`pt_verify_connectivity`) → `pt_read_packet_trace`. There is **no `pt_send_pdu`**: PT does not let an extension originate a packet the way the GUI's *Add Simple PDU* button does. Generate traffic with a real ping instead. **Resources:** `pt://catalog/devices`, `/cables`, `/aliases`, `/templates`, `pt://capabilities`. ## Advanced builds (verified live 2026-06-27) - **Inter-VLAN / router-on-a-stick:** `pt_full_build(template="router_on_a_stick", vlans=3)` → N VLANs, trunk uplink, router `.1q` subinterfaces, one `/24` + DHCP pool per VLAN. 2960 omits trunk `encapsulation` (dot1q-only); 3560 emits it. - **IPv6 dual-stack:** `pt_plan_topology(dual_stack=True)` → routers get `ipv6 address` via CLI + `ipv6 unicast-routing`; hosts use **SLAAC** (`configurePcIpv6` = enable + auto-config). Static host IPv6 is NOT settable via the PT API (`addIpv6Address` fails on HostPort) — SLAAC is the path. - **WiFi laptops:** `wireless_laptops=True` swaps each Laptop-PT NIC to `PT-LAPTOP-NM-1W` (slot `"0"`) → `Wireless0`, and adds **one `AccessPoint-PT` per LAN**, each wired to that LAN's own switch. ⚠️ **Wireless addressing is NOT deterministic.** Laptops auto-associate on the default SSID and PT exposes **no SSID API** (verified: neither the AP nor its port has `setSsid`), so with more than one AP a laptop can associate to *any* of them and take a DHCP lease from **another LAN's pool**. Measured on PT 9.0.1: LT10, sitting right next to its own LAN's AP, still associated to the LAN-1 AP and got `192.168.0.13`; only with that AP powered off did it take `192.168.4.25`. The planner now emits a `WIRELESS_AMBIGUOUS_ASSOCIATION` **warning** when ≥2 APs coexist. Always confirm with `pt_inspect_ports`; for deterministic addressing use `wireless_laptops=False`. ## ⚠️ Verified PT Script-Engine API (for `pt_send_raw` / raw JS) These are the **real** signatures (verified against the MCP's runtime patches and live testing). If a method is not here, do **not** assume it exists. **Globals** - `getDevices(filter)` → **Array of NAME STRINGS**, not device objects (filter `""` = all, `"router"` = routers). Calling `.getName()` / `.getPorts()` on an element throws `TypeError: Property 'getName' of object R1 is not a function` — the element *is* `"R1"`. To get the object, feed each name to `ipc.network().getDevice(name)`. - `allModuleTypes[name]` → module-type handle (passed to `addModule`) - `reportResult(data)` → exists **only when `wait_result=True`**; POSTs the result back - ❌ there is **no global `getDevice(...)`**; ❌ no `XMLHttpRequest` in the Script Engine **Network / device** — `var d = ipc.network().getDevice("R1");` // ← correct singular lookup, may be null - `ipc.appWindow().getActiveWorkspace().getLogicalWorkspace()` → `lw` - `lw.addDevice(type, model, x, y)` → autoName · `lw.createLink(d1,p1,d2,p2,cableEnumInt)` - `d.getPorts()` → **Array of port-name strings** — use `.length`, `[i]`, `.join(",")`. ❌ never `.size()`, `.at(i)`, `.getName()` on it (TypeError → modal → freeze). - `d.getPort(name)` → Port | null · `d.getPower()/setPower(bool)/skipBoot()/setName(name)` - `d.moveToLocation(x, y)` → reposiciona en el canvas lógico. Es lo que usa `pt_move_device`; en un solo `pt_send_raw` podés reacomodar decenas de dispositivos sin una llamada por cada uno. - `d.addModule(slot, allModuleTypes[model], modelName)` → bool (**slot is a STRING**) - `d.getProcess("AclProcess")` → AclProcess | null (routers) · `d.enterCommand(cmd, mode)` - `d.getCommandLine()` → console handle with `getOutput()`, `enterCommand(cmd)`, `getPrompt()`. Use **this** for console work: `getCommandPrompt()` exists ONLY on hosts and throws `TypeError` on any router. PCs expose both, so `getCommandLine()` covers both worlds. ⚠️ A router deployed by the MCP was never touched by console, so it sits at `Would you like to enter the initial configuration dialog? [yes/no]:` — a `ping` sent there is eaten as the yes/no answer. Prime it first: answer `no`, then send an empty command to clear `Press RETURN to get started.` - `d.setDhcpFlag(bool)`, `d.setDefaultGateway(ip)` **Port** — `var p = d.getPort("GigabitEthernet0/0");` - `p.getLink()` → link | null (null = free) · `p.setIpSubnetMask(ip, mask)` · `p.setDefaultGateway(ip)` - `p.setDnsServerIp(ip)` · `p.setAclInID(id)`/`p.setAclOutID(id)` (pass `""` to clear) **AclProcess** — `var ap = d.getProcess("AclProcess");` - `ap.addAcl(name)` · `ap.getAcl(name)` → acl|null · `ap.removeAcl(name)` - `acl.addStatement(str)` → bool · `acl.getCommandCount()` → int **DHCP server (Server-PT only)** — verified PT 9.0 + IpcAPI reference (`class_dhcp_server_process`) - `var s = d.getProcess("DhcpServerMain").getDhcpServerProcessByPortName("FastEthernet0");` `getProcess("DhcpServerMain")` is **null** on routers, switches and PCs; a wrong port name → null. ⚠️ `DhcpServerMain` itself has **no** pool methods (`getPoolCount` etc. are undefined there) — that is why issue #23 concluded it was impossible. They live one level down, on `s`. - `s.addPool(name)` (a repeated name does not duplicate) · `s.getPool(name)` → pool|null · `s.removePool(name)` · `s.getPoolCount()` / `s.getPoolAt(i)` · `s.setEnable(bool)` / `s.isEnable()` · `s.addExcludedAddress(ip, ip)`. Avoid `addNewPool(...)`: 8 unnamed arguments in the reference. - Pool: `setNetworkAddress(ip)` · **`setNetworkMask(network, mask)` — TWO args**, one arg throws `Invalid arguments for IPC call` · `setDefaultRouter(ip)` · `setDnsServerIp(ip)` · `setStartIp(ip)` · `setMaxUsers(n)` (recomputes the end from the start, so set start first) · `setEndIp(ip)`. Getters: `getDhcpPoolName/getNetworkAddress/getSubnetMask/getDefaultRouter/getDnsServerIp/ getStartIp/getEndIp/getMaxUsers`. There is **no** setter for TFTP, WLC or domain name. - Behaviour measured live: PT never leases the server's own IP, but **does** lease the gateway's if the range covers it. The factory `serverPool` re-fits itself to the server's subnet with start = network address and no gateway, so next to a named pool it hands out `.1`. After changing a pool, clients keep their old lease — even one outside the new range. Toggling `setDhcpFlag` does not renew; run `ipconfig /release` then `ipconfig /renew` on the host console. **Cable enum ints (for `lw.createLink`)**: straight 8100 · cross 8101 · roll 8102 · fiber 8103 · phone 8104 · cable 8105 · serial 8106 · auto 8107 · console 8108 · wireless 8109 · coaxial 8110 · octal 8111 · cellular 8112 · usb 8113 · custom_io 8114. ### 🔒 ALWAYS wrap raw JS in try/catch ```js try { var d = ipc.network().getDevice("R1"); reportResult("ports="+d.getPorts().join(",")); } catch (e) { reportResult("ERR: " + e); } ``` An **uncaught** error pops a modal `QMessageBox` in PT ("An error occurred… ReferenceError…") that is **modal** and freezes the webview polling loop — the bridge goes "NOT connected" until a human clicks **OK**. A **caught** error never does. (The server also guards commands now, but wrap anyway — it's free insurance and keeps results clean.) ## Validation: read `plan.errors` before you deploy `pt_plan_topology` / `pt_full_build` return `errors[]` and `warnings[]`; `pt_validate_plan` returns the same as typed codes. **An empty `errors[]` is now meaningful** — it did not used to be. The validator only checked per-device facts, so a topology split into islands passed with `valid: true`, deployed, and failed silently. It now walks the graph. | Code | What it means | What to do | |---|---|---| | `TOPOLOGY_DISCONNECTED` | The cabled devices form more than one component — something has no path to the rest | Usually the hub ran out of ports (a 2911 has 3 Gigabit). Use a bigger model, add a module, or fewer routers. **Do not deploy**: the isolated part cannot route | | `OSPF_NO_NETWORKS` | An OSPF process with zero `network` statements | That router has no addressed, linked interface. Fix the links first | | `OSPF_INVALID_ROUTER_ID` | `router-id 0.0.0.0`, which IOS rejects | Same root cause as above | | `WIRELESS_AMBIGUOUS_ASSOCIATION` | **warning** — two or more APs share the default SSID | Not a blocker. Confirm with `pt_inspect_ports` which subnet each wireless host actually landed in | | `IP_CONFLICT` / `INVALID_IP_ADDRESS` | Duplicate or malformed address | Re-address | | `DHCP_GATEWAY_MISMATCH` | **warning** — the pool gateway is on no interface of that router | Check the pool against the router's interfaces | Wireless hosts are excluded from the connectivity graph on purpose: they carry no cable by design, so counting them as islands would flag every wireless topology as broken. ## Big topologies: when the plan does not fit through a tool parameter `pt_live_deploy` takes the plan as a **string argument**, so a plan with ~90 devices (thousands of lines of JSON) cannot practically be passed to it, and `pt_load_project` only hands the JSON back to you. For anything past roughly 60 devices, or for shapes the templates do not cover (mixed IGPs per region, custom cores), build it **locally** and push it straight to the mailbox — the plan never has to travel through a tool call: ```python from src.packet_tracer_mcp.domain.models.plans import TopologyPlan, DevicePlan, LinkPlan from src.packet_tracer_mcp.domain.services.validator import validate_plan from src.packet_tracer_mcp.infrastructure.generator.ptbuilder_generator import ( generate_executable_script, ) from src.packet_tracer_mcp.infrastructure.execution.file_bridge import FileBridge plan = TopologyPlan(...) # devices, links, ospf/eigrp/rip_configs, vlans result = validate_plan(plan) # same rules the MCP uses — check it first script = generate_executable_script(plan) bridge = FileBridge() assert bridge.pt_alive() # Script Engine heartbeat for batch in chunks(script.split(" "), 25): body = "".join(f"try{{{line}}}catch(e){{}}" for line in batch) bridge.send_and_wait(body + "reportResult('ok');", timeout=90.0) ``` Verified: 88 devices, 88 links, 282 JS statements in 12 batches over the file bridge. One `try/catch` per statement so a single failure does not take the batch down. A plan can carry `ospf_configs`, `eigrp_configs` and `rip_configs` at the same time — and one router can appear in two of them, which is how you build a redistribution boundary. **`redistribute` itself is not in the plan model**: push it as extra CLI with `configureIosDevice(name, cli)`. ## Common mistakes → corrections (do not repeat these) | ❌ Wrong | ✅ Right | Why | |---|---|---| | `getDevice("R1")` | `ipc.network().getDevice("R1")` (or `getDevices("")` to list) | no global `getDevice` → ReferenceError → freeze | | `d.getPorts().size()` / `.at(i)` / `.getName()` | `d.getPorts()[i]` (string array) | getPorts returns strings | | `getProcess("DhcpServerMain").getPoolCount()` | `getProcess("DhcpServerMain").getDhcpServerProcessByPortName("FastEthernet0").getPoolCount()` (or `pt_configure_dhcp_server`) | the pools live on the per-port process | | `pt_add_module(slot=0)` | `slot="0"` (string) | int slot silently fails (`===` compare) | | cable `"crossover"` | `"cross"` | alias works but `cross` is canonical | | invent model `"Cisco4500"` | `pt_list_devices` / `pt_get_device_details` first | use real catalog names | | invent module `"NM-4T"` | `pt_list_modules(router_model=…)` first | use real module names | | raw JS unwrapped | wrap in `try/catch` + `reportResult` | uncaught error freezes the bridge | | trust `add_module` "timeout" = failure | verify with `pt_query_topology` | it often succeeds despite timeout | ## Cables, ports, IP conventions - **Cables (15)**: `straight, cross, roll, serial, fiber, console, phone, cable, coaxial, auto, wireless, octal, cellular, usb, custom_io`. Omit `cable_type` in `pt_add_link` to infer (router↔router & switch↔switch → `cross`; router↔switch, switch↔host → `straight`). - **Exact ports**: 2911 `GigabitEthernet0/0..0/2` but 1941/2901 only `0/0..0/1`; ISR4321/4331 `GigabitEthernet0/0/0..`; 2960/3560 `FastEthernet0/1..0/24` + `GigabitEthernet0/1..0/2`; PC/Laptop/Server `FastEthernet0`; HWIC-2T in `"0/x"` → `Serial0/x/0`,`Serial0/x/1`; **Cloud-PT has 8 ports**: `Serial0..3`, `Modem4`, `Modem5`, `Ethernet6`, `Coaxial7`. ⚠️ A cloud only *forwards* over its serial ports once frame relay is configured, and the MCP has no tool for that. For a WAN core that actually passes traffic, link routers to each other with `/30`s and hang the cloud off `Ethernet6` as an external stub. - **IP plan** (`pt_plan_topology`): LANs `/24` from `192.168.0.0`, gateway `.1`, hosts from `.2`; router↔router `/30` from `10.0.0.0`; DHCP pool per LAN with `.1` excluded; routing `static|ospf|eigrp|rip|none` (+ `floating_routes`, `ospf_process_id`, `eigrp_as`). ## Module install by router family `slot` is a **STRING**. Pick a module whose `compatible_with` includes the model — the MCP **rejects an incompatible module up front** when the module declares `compatible_with` (HWIC/NIM/built-ins); generic `PT-*` modules have no declared constraint, so choose sensibly there. Always confirm with `pt_query_topology` after install; a single `pt_add_module` may report a **timeout yet still succeed**. | Router family | Module type | Slot (string) | Ports added | Status | |---|---|---|---|---| | ISR G2 — 2911/2901 | HWIC (`HWIC-2T`, `HWIC-1GE-SFP`) | `"0/0".."0/3"` | `Serial0/x/0`,`Serial0/x/1` | ✅ verified 2911 | | ISR G2 — **1941** | HWIC | **only `"0/0"`, `"0/1"`** — it has 2 slots, not 4 | `Serial0/x/0`,`Serial0/x/1` | ✅ verified 1941 (PT 9.0.1) | | ISR 4000 — ISR4321/4331 | NIM (`NIM-2T`, `NIM-ES2-4`) | **`"0/1"`, `"0/2"`** | `Serial0/1/0`,`Serial0/1/1` | ✅ verified ISR4321 & ISR4331 | | 2811 / 2620XM / 2621XM | NM (`NM-4A/S`, `NM-2FE2W`,…) | **`"1"`** | `Serial1/0..1/3` | ✅ verified 2811 | | Router-PT (generic) | NM (`NM-*`, `PT-ROUTER-NM-*`) | `"1"` | ⚠️ non-standard ids (e.g. `Serial2/0`) | installs, odd port names | > ⚠️ The `pt_add_module` docstring/SERVER_INSTRUCTIONS say NIM slots are `"0"`/`"1"` — **that is > wrong**; the working slot is **`"0/1"`** (chassis/subslot). HWIC = `"0/x"`, NM = `"1"`, NIM = `"0/1"`. > The install *mechanism* (`addModule`) is identical across routers — only the **slot string** differs > by family. Always confirm the result with `pt_query_topology`. Prefer `pt_install_modules_batch` for multiple modules (one power-cycle); individual installs power-cycle the device and can exceed the wait window (and often report a timeout despite succeeding). ## Known rough edges (verified by benchmark against PT 9.0.1) - **Module compatibility is enforced for modules that declare `compatible_with`** (HWIC/NIM/built-ins reject a wrong model); generic `PT-*` modules carry no constraint, so still pick sensibly. - **`pt_add_module` (single) can report a timeout but still succeed** — verify ports, don't blindly retry. (`pt_install_modules_batch` no longer guesses: it reports `installed` per module, see round 2 below.) - **`three_router_triangle` closes the ring (R3↔R1)** and `hub_spoke` wires R1→every spoke — the orchestrator honors the template shape (was a flat chain before). ⚠️ **`hub_spoke` is limited by the hub's port count**: a 2911 has 3 Gigabit ports, so it cannot serve 5 spokes plus its own LAN. Asking for more no longer fails silently — the plan comes back with `TOPOLOGY_DISCONNECTED`. Pick a router with more ports, add a module, or use `multi_lan` (a chain needs only 3 ports per router). - **`pt://capabilities` is derived from the live tool registry** (`supported_live.nat/acl/modules/…`) — it can no longer drift; trust it *and* the tools. - **`pt_live_deploy` "N/N verified" checks device/link existence only** — host IPs may lag a few seconds. - **Invalid `routing=` raises a raw exception**; invalid `router_model` returns a degenerate plan with an embedded `errors[]` — always check `plan.errors` before deploying. - A harmless phantom `Power Distribution Device` can appear after deploy (off-canvas). ### ☠️ Never iterate a native PT object with `for...in` ```js var d = lw.getEllipseItemData(id); for (var k in d) { ... } // ← CRASHES Packet Tracer. The whole app dies. ``` `try/catch` does **not** save you: this is not a JS exception, it is the script engine blowing up and taking the process with it. Verified the hard way against PT 9.0.1 — the app closed with "Cisco Packet Tracer quit unexpectedly". Inspect native objects by calling **known getters** and stringifying the result, never by enumerating keys. If you don't know the shape, probe one accessor at a time. ### Drawing IS possible — `drawCircle` takes SEVEN arguments ```js lw.drawCircle(x, y, ignored, diameter, 0, 0, 0) // x,y = TOP-LEFT corner ``` Returns an ellipse id and really draws. With 3–6 arguments it throws, which is why it was believed unusable — same failure mode as `setHideDevLabel` (needs 2, not 1): the call was fine, the **arity** was wrong. The old note that "the size argument controls stacking order" came from passing the size in slot 3; **slot 4 is the diameter** and slot 3 is ignored. Calibrated on PT 9.0.1 at default zoom: the drawn diameter is roughly **0.71 ×** the argument, so to circle a LAN centred at `cx` use `drawCircle(cx - 173, y, 0, 480, 0,0,0)`. Don't compute it blind — draw, `pt_screenshot`, adjust. Clear with `pt_clear_annotations` (it removes drawings too, so redraw circles after clearing notes). Colours still don't work: the last three arguments accept values but the ellipse comes out with the default outline. ### Verified against PT 9.0.1 (audit round 2) - **The bridge starts lazily.** After the MCP server restarts (a `/mcp` reconnect), nothing is listening on `:54321` until the first `pt_*` call — importing the server deliberately opens no socket. So "not connected" right after a reconnect usually means *nobody has called a tool yet*, **not** that the extension died: just call `pt_bridge_status` and PT resumes polling in seconds. Don't ask the user to reopen MCP BUILDER before trying that. - **`getClassName()` is useless for telling a switch from a router.** PT classifies by behaviour: a 3560 answers `"Router"` (it is multilayer) and a 2960 answers `"CiscoDevice"` — neither ever says "switch". Use `getModel()` and resolve it against the catalog. - **`addModule()` returns `false` instead of throwing** when the slot does not exist on that model. Check the return value; a silent `false` used to be reported as a successful install. `pt_add_module` and `pt_install_modules_batch` now verify it and report `installed` / `failed` per module. - **Renaming to a name that is already taken used to be allowed** and left two devices sharing it, with `getDevice(name)` resolving only to one — the other became unreachable by name. Now rejected. ### Verified against PT 9.0.1 (audit round 3) - **A router deployed by the MCP has never been touched through its console**, so it is still parked at `Would you like to enter the initial configuration dialog? [yes/no]:`. A `ping` sent there is eaten as the yes/no answer and never runs. Prime it first: send `no`, then an empty command to clear `Press RETURN to get started.` (`pt_verify_connectivity` now does this for you.) - **An IOS interface with no IP address stays shut down.** The CLI generator only emits `no shutdown` for addressed interfaces, so a link you cabled but never addressed shows up as a red triangle on the canvas and as a down link in `pt_health_check`. Give it an address, even a throwaway `/30`. - **PT does not associate a wireless host with the nearest AP.** Measured: a laptop with its own LAN's AP right beside it did a *fresh* association and a *fresh* DHCP and still chose the AP of another LAN, taking that LAN's address. Between APs sharing the default SSID the choice is arbitrary, and there is no SSID API to force it (neither the AP nor its port has `setSsid`). If you need deterministic addressing, use `wireless_laptops=False` and cable the laptops. - **`PT_MCP_BRIDGE_TOKEN` is validated.** If the server refuses to start with `BridgeTokenError`, the variable is set to something shorter than 32 chars or outside `[A-Za-z0-9_-]`. Fix it or unset it so the on-disk token is used. It fails loudly on purpose: a bad value there would disable the only real defence the bridge has. - **The layout adapts to the busiest LAN.** Coordinates no longer go negative and LAN clusters no longer overlap, so you do not need to reposition devices by hand after `pt_full_build` — only if you want a specific arrangement. ## Recipes - *"2 routers, 2 switches, 4 PCs, DHCP, static"* → `pt_full_build(routers=2, switches_per_router=1, pcs_per_lan=2, dhcp=True, routing="static", deploy=True)`. - 4 serial ports on R1 (2911) → `pt_install_modules_batch([{device:"R1",slot:"0/0",module:"HWIC-2T"}, {device:"R1",slot:"0/1",module:"HWIC-2T"}])`. - Block telnet to a server on R1 → `pt_apply_acl(router="R1", name_or_number="101", acl_type="extended", entries=[{action:"deny",protocol:"tcp",source:"any",destination:"host 192.168.0.10",dest_port_op:"eq", dest_port:23},{action:"permit",protocol:"ip",source:"any",destination:"any"}], binding_interface="GigabitEthernet0/0", binding_direction="in", dry_run=True)` — preview first. - Read a device's ports safely → `pt_send_raw('try{var d=ipc.network().getDevice("R1");reportResult(d?d.getPorts().join(","):"missing")}catch(e){reportResult("ERR:"+e)}', wait_result=True)`.