Imported from Mats2208/MCP-Packet-Tracer (
skill/SKILL.md). Install upstream withnpx skills add Mats2208/MCP-Packet-Tracer --skill skill. Copyright stays with the author.
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:
- 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.
- Before planning/deploying:
pt_list_devices,pt_list_templates,pt_get_device_details, andpt_list_modules(if installing modules). - 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.
- After every mutation, read back with
pt_query_topology/pt_export_topologyand confirm. - 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/nextevery 500 ms and runs each command via$se('runCode', …)in PT's Script Engine, posting results back to/result. XMLHttpRequestdoes 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 (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_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.
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.1qsubinterfaces, one/24+ DHCP pool per VLAN. 2960 omits trunkencapsulation(dot1q-only); 3560 emits it. - IPv6 dual-stack:
pt_plan_topology(dual_stack=True)→ routers getipv6 addressvia CLI +ipv6 unicast-routing; hosts use SLAAC (configurePcIpv6= enable + auto-config). Static host IPv6 is NOT settable via the PT API (addIpv6Addressfails on HostPort) — SLAAC is the path. - WiFi laptops:
wireless_laptops=Trueswaps each Laptop-PT NIC toPT-LAPTOP-NM-1W(slot"0") →Wireless0, and adds oneAccessPoint-PTper 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 hassetSsid), 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 got192.168.0.13; only with that AP powered off did it take192.168.4.25. The planner now emits aWIRELESS_AMBIGUOUS_ASSOCIATIONwarning when ≥2 APs coexist. Always confirm withpt_inspect_ports; for deterministic addressing usewireless_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 throwsTypeError: Property 'getName' of object R1 is not a function— the element is"R1". To get the object, feed each name toipc.network().getDevice(name).allModuleTypes[name]→ module-type handle (passed toaddModule)reportResult(data)→ exists only whenwait_result=True; POSTs the result back- ❌ there is no global
getDevice(...); ❌ noXMLHttpRequestin the Script Engine
Network / device — var d = ipc.network().getDevice("R1"); // ← correct singular lookup, may be null
ipc.appWindow().getActiveWorkspace().getLogicalWorkspace()→lwlw.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 usapt_move_device; en un solopt_send_rawpodé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 withgetOutput(),enterCommand(cmd),getPrompt(). Use this for console work:getCommandPrompt()exists ONLY on hosts and throwsTypeErroron any router. PCs expose both, sogetCommandLine()covers both worlds. ⚠️ A router deployed by the MCP was never touched by console, so it sits atWould you like to enter the initial configuration dialog? [yes/no]:— apingsent there is eaten as the yes/no answer. Prime it first: answerno, then send an empty command to clearPress 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
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.
| 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:
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 |
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. Omitcable_typeinpt_add_linkto infer (router↔router & switch↔switch →cross; router↔switch, switch↔host →straight). - Exact ports: 2911
GigabitEthernet0/0..0/2but 1941/2901 only0/0..0/1; ISR4321/4331GigabitEthernet0/0/0..; 2960/3560FastEthernet0/1..0/24+GigabitEthernet0/1..0/2; PC/Laptop/ServerFastEthernet0; 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 offEthernet6as an external stub. - IP plan (
pt_plan_topology): LANs/24from192.168.0.0, gateway.1, hosts from.2; router↔router/30from10.0.0.0; DHCP pool per LAN with.1excluded; routingstatic|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_moduledocstring/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 withpt_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); genericPT-*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_batchno longer guesses: it reportsinstalledper module, see round 2 below.)three_router_trianglecloses the ring (R3↔R1) andhub_spokewires R1→every spoke — the orchestrator honors the template shape (was a flat chain before). ⚠️hub_spokeis 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 withTOPOLOGY_DISCONNECTED. Pick a router with more ports, add a module, or usemulti_lan(a chain needs only 3 ports per router).pt://capabilitiesis 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; invalidrouter_modelreturns a degenerate plan with an embeddederrors[]— always checkplan.errorsbefore deploying. - A harmless phantom
Power Distribution Devicecan 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
/mcpreconnect), nothing is listening on:54321until the firstpt_*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 callpt_bridge_statusand 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". UsegetModel()and resolve it against the catalog.addModule()returnsfalseinstead of throwing when the slot does not exist on that model. Check the return value; a silentfalseused to be reported as a successful install.pt_add_moduleandpt_install_modules_batchnow verify it and reportinstalled/failedper 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]:. Apingsent there is eaten as the yes/no answer and never runs. Prime it first: sendno, then an empty command to clearPress RETURN to get started.(pt_verify_connectivitynow does this for you.) - An IOS interface with no IP address stays shut down. The CLI generator only emits
no shutdownfor addressed interfaces, so a link you cabled but never addressed shows up as a red triangle on the canvas and as a down link inpt_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, usewireless_laptops=Falseand cable the laptops. PT_MCP_BRIDGE_TOKENis validated. If the server refuses to start withBridgeTokenError, 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).