The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the PyATS MCP listing page.
Cisco pyATS and Genie already know how to talk to a network — parsing show commands, pushing configuration, learning feature state, running declarative tests. What they didn't have was a way for an AI agent to drive any of it directly. This server closes that gap: it wraps pyATS/Genie as a set of structured, guarded MCP tools that an agent like Claude can call against a real testbed, over the Model Context Protocol's current Streamable HTTP transport.
Point an agent at it and it can look up a device, run and parse a show command, apply configuration with a rollback point, learn and diff a feature's state before and after a change, fan a command out across a fleet — one thread pool or one process per device — run a declarative Blitz or Robot Framework test, or call a device's REST/RESTCONF API directly. Every risky path is guarded before it reaches a device, and every call lands in an in-memory audit log the agent can review mid-session.
mcp>=2.0.0), stateful or stateless, chosen with one environment variable. STDIO is gone.pyats.async_.pcall) when you want real isolation at scale..env, pulled into testbed.yaml at runtime via %ENV{} substitution.testbed.yaml pointed at real or virtual network devices — a physical lab, Cisco Modeling Labs / VIRL / GNS3, or anything else Unicon can reach over SSH/Telnet. pyATS MCP doesn't simulate a network; it drives one.The MCP endpoint is then reachable at http://<host>:<port>/mcp.
All device details and credentials live in a .env file — nothing is hard-coded in the repo.
PYATS_MCP_TRANSPORT_MODE=stateless sets stateless_http=True on the Streamable HTTP transport, so no server-side session state is retained between requests from clients still negotiating the older, handshake-based protocol. Clients speaking the current MCP protocol (2026-07-28, SEP-2575) are handshake-free by default regardless of this setting — that comes from the mcp>=2.0.0 SDK itself, not anything configured here.
Every device in your testbed.yaml uses %ENV{VAR} substitution, so credentials and connection details are read from .env at runtime.
Use the {DEVICENAME}_{FIELD} naming convention:
If a group of devices shares credentials, define group-level vars and reference them across devices:
For devices with unknown OS, set
os: "%ENV{DEVICE_OS}"withDEVICE_OS=genericin.envand optionally addlearn_os: trueunderarguments:— Unicon will detect and cache the OS after the first connection.
Either way, the server is a long-running process you start once and point clients at — it isn't something an agent spawns per session. See below for exactly how each client connects to it.
The server exposes one thing: an MCP endpoint at http://<host>:<port>/mcp (Streamable HTTP). Every client below just needs that URL — no command/args, no local process for the client to manage.
Or drop it straight into .mcp.json (project-scoped, committed to the repo) or ~/.claude.json (user-scoped):
Add a .vscode/mcp.json in the workspace (or run MCP: Add Server from the Command Palette):
Or in ~/.codex/config.toml:
Claude Desktop's claude_desktop_config.json is stdio-only — putting a url field in it doesn't work (it's a known issue, not a supported path). Remote/HTTP servers are added instead as a Custom Connector under Settings → Connectors, and Desktop connects to it from Anthropic's cloud, not your local machine — so it needs a real, publicly-reachable HTTPS URL, not localhost.
To point Desktop at a server running on your own machine anyway, bridge it through mcp-remote as a local stdio proxy:
Once connected, talk to it like you'd talk to someone who already knows the network:
pyats_list_devicespyats_run_show_command, parsed into structured JSONpyats_learn_feature (before) → pyats_configure_with_diff → pyats_learn_feature (after) → pyats_diff_learned_snapshotsshow ip interface brief across every switch" → pyats_run_show_command_multi (or pyats_pcall_show_command for process-per-device isolation at real scale)pyats_rollback_configpyats_run_blitz / pyats_run_robotThe agent chains these itself — you describe the outcome, it picks the tools.
26 tools, grouped by what they do.
Discovery
| Tool | Description |
|---|---|
pyats_list_devices | List all devices in the testbed |
pyats_search_devices | Fuzzy-search devices by name or alias |
Show commands
| Tool | Description |
|---|---|
pyats_run_show_command | Run a validated show command; returns parsed JSON or raw output |
pyats_run_show_command_multi | Run a show command across multiple devices concurrently (thread pool) |
pyats_pcall_show_command | Same, but one OS process per device (pyats.async_.pcall) instead of a shared thread pool |
pyats_show_running_config | Retrieve the full running configuration (raw text) |
pyats_show_logging | Retrieve device system logs via show logging |
pyats_ping_from_network_device | Execute a ping from a network device |
pyats_run_linux_command | Run a command on a Linux host |
Configuration
| Tool | Description |
|---|---|
pyats_configure_device | Apply configuration commands with safety guardrails |
pyats_configure_devices_multi | Apply configuration across multiple devices concurrently (thread pool) |
pyats_pcall_configure_devices | Same, but one OS process per device |
pyats_configure_with_diff | Apply config and return a before/after diff |
pyats_rollback_config | Roll back to the last saved configuration snapshot |
State & diagnostics
| Tool | Description |
|---|---|
pyats_device_health | Snapshot CPU, memory, interfaces, and routing state |
pyats_get_neighbors | Retrieve CDP/LLDP neighbors |
pyats_find_interface_by_ip | Find which interface owns a given IP address |
pyats_learn_feature | Genie device.learn() for a whole feature (interface, ospf, bgp, …), optionally saved as a named snapshot |
pyats_diff_learned_snapshots | Diff two snapshots saved by pyats_learn_feature |
Testing & automation
| Tool | Description |
|---|---|
pyats_clean_device | Genie Clean (Kleenex), restricted to non-destructive connect+execute_command stages; dry_run=True by default |
pyats_run_blitz | Run a declarative pyATS Blitz YAML test |
pyats_run_robot | Run a Robot Framework suite using the pyats.robot/genie.libs.robot keyword libraries |
pyats_run_dynamic_test | Execute a sandboxed pyATS AEtest script |
APIs
| Tool | Description |
|---|---|
pyats_rest_request | Generic REST/RESTCONF/NX-API call via pyATS's rest.connector (a separate connection type from CLI/SSH) |
pyats_xpresso_request | Authenticated call to Cisco XPresso's REST API v2 (test requests, jobs, testbeds, images, …) |
Session
| Tool | Description |
|---|---|
pyats_get_operation_log | Retrieve the in-memory operation log |
reload, erase, write erase, delete, format — the same check runs inside pyats_clean_device, pyats_run_blitz, and pyats_run_robot.os, sys, subprocess, etc.).pyats_clean_device never runs a real Genie Clean stage that reboots, erases, or reimages a device — only connect+execute_command are ever generated — and defaults to dry_run=True; running for real also requires an exact confirmation phrase..env — never stored in the testbed file or source code.See CONTRIBUTING.md for the full setup and PR workflow.
benchmark/ compares STDIO (legacy) against Streamable HTTP in both stateful and stateless mode, against a real testbed. See benchmark/scenarios.py for the scenario list and benchmark/aggregate.py for building the comparison report; benchmark/results/summary.md has the most recent run's numbers.