Packet Tracer MCP — operating guide

SkillCloud & infra

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.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Packet Tracer MCP — operating guide skill

What this skill tells your AI

The instructions your AI receives, as published by mats2208/mcp-packet-tracer in skill/SKILL.md and read by ahel’s review.

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_devicespt_plan_topologypt_validate_planpt_live_deploy.
  • Edit a live topology: pt_bridge_statuspt_query_topologypt_add_*/pt_rename_device/….
  • Add modules: pt_query_topologypt_list_modules(router_model=…)pt_install_modules_batch.

Tool catalog (61)

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_buildpt_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. 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 / devicevar 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)

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

AclProcessvar ap = d.getProcess("AclProcess");

  • ap.addAcl(name) · ap.getAcl(name) → acl|null · ap.removeAcl(name)
  • acl.addStatement(str) → bool · acl.getCommandCount() → int

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

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.

CodeWhat it meansWhat to do
TOPOLOGY_DISCONNECTEDThe cabled devices form more than one component — something has no path to the restUsually 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_NETWORKSAn OSPF process with zero network statementsThat router has no addressed, linked interface. Fix the links first
OSPF_INVALID_ROUTER_IDrouter-id 0.0.0.0, which IOS rejectsSame root cause as above
WIRELESS_AMBIGUOUS_ASSOCIATIONwarning — two or more APs share the default SSIDNot a blocker. Confirm with pt_inspect_ports which subnet each wireless host actually landed in
IP_CONFLICT / INVALID_IP_ADDRESSDuplicate or malformed addressRe-address
DHCP_GATEWAY_MISMATCHwarning — the pool gateway is on no interface of that routerCheck 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:

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✅ RightWhy
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
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 firstuse real catalog names
invent module "NM-4T"pt_list_modules(router_model=…) firstuse real module names
raw JS unwrappedwrap in try/catch + reportResultuncaught error freezes the bridge
trust add_module "timeout" = failureverify with pt_query_topologyit 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 /30s 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 familyModule typeSlot (string)Ports addedStatus
ISR G2 — 2911/2901HWIC (HWIC-2T, HWIC-1GE-SFP)"0/0".."0/3"Serial0/x/0,Serial0/x/1✅ verified 2911
ISR G2 — 1941HWIConly "0/0", "0/1" — it has 2 slots, not 4Serial0/x/0,Serial0/x/1✅ verified 1941 (PT 9.0.1)
ISR 4000 — ISR4321/4331NIM (NIM-2T, NIM-ES2-4)"0/1", "0/2"Serial0/1/0,Serial0/1/1✅ verified ISR4321 & ISR4331
2811 / 2620XM / 2621XMNM (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

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

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.

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
177
Forks
15
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
packet-tracer
Source
github.com/mats2208/mcp-packet-tracer