cml-mcp-server
Provides integration with Cisco Modeling Labs (CML 2.x), enabling exploration, creation, operation, and troubleshooting of virtual network labs. Supports managing labs, nodes, links, configurations, packet captures, reachability checks, recipes, and topology exports.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@cml-mcp-serverbuild a lab with two IOS routers connected and test reachability"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
cml-mcp-server
An MCP server for Cisco Modeling Labs (CML 2.x): explore, build, run and troubleshoot network labs from an AI assistant. It's built on stutters-mcp-core and tested against CML 2.10.
Not affiliated with or endorsed by Cisco Systems, Inc. "Cisco" and "Cisco Modeling Labs" are trademarks of Cisco Systems, Inc.
72 tools in three tiers: 39 read, 25 write, 8 dangerous (incl.
cml_api_callfor non-GET), and the promptcml_build_test_lab. Write and dangerous tools (and the prompt) are off by default.Names or ids everywhere. Labs take a title or a UUID, nodes take a label, and links take
r1<->r2orr1:Ethernet0/0<->r2:Ethernet0/0.Preview-first writes. A write call without
confirmchanges nothing. It returns a preview (diffs, chosen interfaces, blockers CML would reject) and a single-use confirm token.Task tools.
cml_build_labbuilds a whole lab from a compact spec (day-0 configs, extra config files, console logins, external connectors),cml_wait_for_convergedreports boot progress (and can wait for a console message),cml_check_reachabilityproves what a lab can ping, andcml_diff_labcompares a lab with another lab or a saved export.Real platforms on the console. IOS/IOS-XE, NX-OS (POAP skipped), FTD CLISH (no
enable,mode='exec'forconfigure manager add ...), Linux. Questions ([confirm],[yes/no]) are answered on request, runaway commands get Ctrl-C.Lab recipes for the other suite servers. FTDv for FMC, an unregistered FTDv HA pair, IOS-XE routers for Catalyst Center, a Cat9kv 802.1X switch (IBNS 2.0) with MAB and EAP-TLS clients for ISE, an AD-user 802.1X client behind an FMC-managed FTD for user-based firewalling:
cml_list_recipes,cml_build_recipe,cml_verify_recipe(pass/fail contract, incl. ISE live sessions),cml_run_recipe_action(e.g. switch the logged-in user) and the promptcml_build_test_lab;cml_plan_licencessays which labs to stop. See src/cml_mcp/recipes/README.md.Test oracles.
cml_diff_node_configshows what another server changed on a device;cml_capture/cml_summarize_capturedecode RADIUS, EAPOL, OSPF, DHCP and more from a link capture.Degraded networks and diagrams.
cml_apply_link_scenarioputs named impairments on a link (wan-good, wan-satellite, lossy-wifi, brownout, or a timedflap);cml_export_topologydraws a lab as Mermaid or draw.io with addresses, andcml_annotate_labmakes a lab self-describing.Spec escape hatch.
cml_search_specandcml_get_definitionread the controller's own OpenAPI document.cml_api_callreaches any endpoint: GET in the read tier, other methods in the dangerous tier.
Quickstart (5 minutes)
You need: a CML account for the server, a regular (non-admin) user is enough (read-only to start: labs shared
with it as lab_view + lab_exec and a resource pool with 0 node licences, see
docs/least-privilege.md). Never the built-in admin.
A. On your laptop (stdio), from a release wheel. Every GitHub release has a wheel with stutters-mcp-core bundled in; it needs only PyPI dependencies.
WHEEL=https://github.com/dr-stutters/cml-mcp-server/releases/download/v0.11.0/cml_mcp_server-0.11.0-py3-none-any.whl
export CML_URL=https://cml.example.com CML_USERNAME=mcp-cml-viewer CML_PASSWORD='...'
uvx --from "$WHEEL" cml-mcp-server check # preflight: reachability, TLS, login, version, admin flag
claude mcp add cml -e CML_URL=$CML_URL -e CML_USERNAME=$CML_USERNAME -e CML_PASSWORD="$CML_PASSWORD" \
-- uvx --from "$WHEEL" cml-mcp-server # stdio is the default transportClaude Desktop (claude_desktop_config.json), same idea:
{
"mcpServers": {
"cml": {
"command": "uvx",
"args": ["--from", "https://github.com/dr-stutters/cml-mcp-server/releases/download/v0.11.0/cml_mcp_server-0.11.0-py3-none-any.whl", "cml-mcp-server"],
"env": {"CML_URL": "https://cml.example.com", "CML_USERNAME": "mcp-cml-viewer", "CML_PASSWORD": "..."}
}
}
}B. On a shared host (HTTP), from the image. Multi-arch (amd64/arm64), signed, with an SBOM:
ghcr.io/dr-stutters/cml-mcp-server:<version> (or :latest).
docker run --rm -e CML_URL=https://cml.example.com -e CML_USERNAME=mcp-cml-viewer -e CML_PASSWORD='...' \
ghcr.io/dr-stutters/cml-mcp-server cml-mcp-server check
docker run --rm ghcr.io/dr-stutters/cml-mcp-server cml-mcp-server token alice # one token per person
cp .env.example .env && chmod 600 .env # CML_*, MCP_AUTH_TOKENS=alice:sha256:...
docker compose up -d # pulls the image; http://localhost:8032/mcpFor a team, use docs/deploying.md: read-only by default, per-user tokens in the audit log, several controllers, the shared node-licence budget, TLS with Caddy, upgrades.
check runs without an MCP client and exits 1 on a failure (--json for scripts, --strict to fail on
warnings, --instance training for one controller). It reports reachability and TLS trust of the controller
API, the login, the CML version against the tested matrix (2.10; 2.7 minimum), the API user's admin flag
against the enabled tiers (an admin behind a read-only server warns; a regular user with the dangerous tier on
warns that admin-only tools will fail) and, with writes on, the node licences the account may start (its
resource pool, else the controller licence):
OK config https://cml.example.com; tiers enabled: read, write
OK reachability[api] CML API answered; logged in as mcp-cml-operator (41 ms)
OK tls certificate verified against the system trust store
OK auth credentials accepted
OK role role 'mcp-cml-operator (regular user)' allows the enabled tier(s): read, write
OK version Cisco Modeling Labs 2.10.0+build.13: tested
WARN licences resource pool 'mcp-cml-operator-pool' leaves 0 node licences free (12 of 12 in use): ...
-> Raise the pool's licences (Tools > Resource Pools; ...) or stop labs (cml_plan_licences says which).cml-mcp-server serve --http serves HTTP (needs MCP_AUTH_TOKENS); cml-mcp-server config shows the
effective settings and where each came from, without contacting CML.
From source
git clone https://github.com/dr-stutters/cml-mcp-server && cd cml-mcp-server
python3 scripts/bundle_core.py vendor # bundles the pinned stutters-mcp-core as cml_mcp._core (needs git)
cp .env.example .env && chmod 600 .env # set CML_URL, CML_USERNAME, CML_PASSWORD
uv sync
uv run cml-mcp-server checkThe shared stutters-mcp-core library (with its tasks extra) is
bundled into the package as cml_mcp._core (core ADR-005): the wheel, the sdist and the Docker image need
nothing but their PyPI dependencies, with no git dependency and no token. uv build writes the wheel to dist/.
The pinned core tag is [tool.bundle-core] in pyproject.toml and cml_status reports it as core_version.
The vendored copy is generated, not committed: scripts/bundle_core.py vendor writes it, check reports drift,
and update --tag vX.Y.Z moves the pin (then uv lock). A plain git checkout has no cml_mcp/_core, so install
from a release wheel rather than git+https://....
Claude Code: stdio (local)
claude mcp add cml -- uv --directory /path/to/cml-mcp-server run cml-mcp-server
# to allow changes:
claude mcp add cml -e MCP_ENABLE_WRITES=true -- uv --directory /path/to/cml-mcp-server run cml-mcp-serverClaude Code: HTTP (shared container)
uv run cml-mcp-server token alice # prints a bearer token + the MCP_AUTH_TOKENS entry
claude mcp add --transport http cml http://cortex:8032/mcp --header "Authorization: Bearer <token>"or in .mcp.json:
{
"mcpServers": {
"cml": {
"type": "http",
"url": "http://cortex:8032/mcp",
"headers": { "Authorization": "Bearer ${CML_MCP_TOKEN}" }
}
}
}Related MCP server: gns3-mcp
Tools
Read (always on)
Tool | What it does |
| Version, API user, compute/controller health problems, licence summary, server config |
| Controller and compute health flags, with a problem list |
| CPU, RAM and disk per compute host; running nodes and allocated resources; licence capacity |
| Smart Licensing registration and authorization, features, expiry |
| Labs with state and counts; one lab with convergence, node states and resources in use |
| Compact wiring summary (node interface -> peer, links, conditioning); |
| Topology YAML (pyATS passwords masked unless |
| Change and state history, newest first, with the fields that changed |
| The pyATS testbed CML generates (passwords masked) |
| Nodes with state, image and resources (optionally live CPU/RAM); one node with interfaces, peers and config |
| Recent serial console output, escape codes stripped, optional |
| Run show, ping and traceroute (or |
| Interfaces with MAC, state and peer; filter free or connected |
| IPs per interface: snooped by CML on external segments, or read from every node's CLI ( |
| Ping targets from nodes (per-OS syntax, VRF, FTD management) and return a pass/fail matrix with average RTT |
| Networks an |
| Sample labs on the controller (filter by text or node type) |
| Links as |
| Decoded packets from a link capture (filter by protocol or text) |
| What can be built: interfaces, resources, boot markers, default configs |
| Installed images |
| Disk images in |
| Wait until a lab or node has booted, with progress notifications; |
| Diff a lab with another lab or a saved YAML: nodes, interfaces, links, config diff ( |
| Running config (from the console) vs day-0, startup, another node or a baseline; secrets masked |
| Decoded summary of a link capture: protocol mix, conversations, RADIUS/EAPOL/OSPF/DHCP/ARP/ICMP highlights |
| Licences in use per lab, what a lab or recipe needs, which labs to stop |
| Map a real site's devices (Catalyst Center / ISE / FMC inventories) to CML node definitions within a node/RAM budget: ready |
| Test-lab recipes (no secrets returned); pass/fail verify contract of a running recipe lab (or a copy: |
| Named link scenarios with netem values and expected round-trip ping deltas; with a lab, its conditioned links and flap jobs |
| Mermaid, draw.io or JSON diagram of a lab with interface addresses (day-0 config, snooped or CLI) |
| A lab's drawn annotations (text, shapes), marking the ones this server wrote |
| OpenAPI search, operation and schema details, raw GET |
Write (MCP_ENABLE_WRITES=true, all preview-first)
Tool | What it does |
| Build a lab from |
| Create a lab from a sample lab |
| Create an empty lab, or create one from topology YAML (node definitions are checked) |
| Start (waits for convergence) or stop a lab |
| Stop several labs (a list, or every running lab except |
| Add a node; change its label, tags, position, resources or console login |
| Replace a node's day-0 config; the preview shows a unified diff |
| Start or stop one node |
| Save running configs into the stored node configs |
| Push config lines to a running device over its console ( |
| Link two nodes (first free interfaces, or the ones you name) |
| Add latency, jitter, loss, bandwidth limits, duplication or corruption to a link, or clear it |
| Take a link down or bring it up (cable pull) |
| Packet capture on a link |
| Capture for N seconds and return the decoded summary in one call |
| Capture the simulated 802.11 medium at a wireless-ap / wireless-client node and decode it (SSIDs, auth/assoc, deauth reasons, EAPOL 4-way handshake, EAP identity/method) |
| Build a packaged test-lab recipe (optionally start it), with annotations; |
| Run one of a recipe's packaged actions on its lab, e.g. |
| Apply a named scenario to a link: wan-good, wan-satellite, lossy-wifi, brownout, or |
| Stop flap jobs and remove conditioning from a lab's links |
| Add notes, per-node address labels and the recipe's purpose/credential references as text annotations (re-runnable) |
| Make an uploaded disk image usable by a node definition (admin) |
Prompts (write tier): cml_replicate_site(site, fidelity, max_nodes) - Catalyst Center site (+ FMC firewalls, ISE NADs)
-> cml_plan_replica -> licence check -> cml_build_lab -> report gaps. cml_build_test_lab(recipe, reuse) - recipe -> licence plan -> build/start ->
converge -> verify -> hand-off block for the FMC / Catalyst Center / ISE server.
Dangerous (MCP_ENABLE_DANGEROUS=true as well)
Tool | What it does |
| Erase node disks ( |
| Delete ( |
| Stream a disk image from |
| Delete an image definition (refused while nodes use it) and/or a drop-folder file (admin) |
| Raw POST, PUT, PATCH or DELETE, preview-first |
Device CLI uses the controller's console server: SSH to the controller with the CML credentials,
then open /<lab>/<node>/0. This is the same path pyATS testbeds use. Device logins come from the
lab's pyATS testbed (node pyATS credentials, or the definition defaults).
CML only accepts config, RAM/CPU and new-interface changes while a node is wiped
(DEFINED_ON_CORE). It only deletes wiped nodes and labs. The previews say so and point to the
tool that fixes it.
Configuration
Variable | Default | Meaning |
| (required) | Controller URL, e.g. |
| API user (JWT login via | |
| A pre-issued JWT instead of username/password | |
| More controllers, each with | |
|
|
|
| CA file to verify the controller certificate | |
|
| Per-request timeout (s) and retries for idempotent calls |
| none / | Client-side throttling |
|
| Default wait for start/stop/convergence (s) |
| detected | Running licensed-node cap. Detected from |
|
| SSH port of the CML console server (used for device CLI) |
|
| Seconds a console call waits for another call on the same node before reporting it busy |
| dotenv file with the | |
| The only directory | |
| Safety rails (docs/safety.md): labs no change may touch; labs the dangerous tools may wipe/delete (title globs or ids) | |
|
|
|
|
|
|
| known_hosts file for the console server's host key. When unset, the key is checked against | |
|
|
|
|
| HTTP listener |
|
| |
|
| Register the write and dangerous tiers |
|
| Run the long tools (image upload, builds/starts with wait, convergence waits, recipe verify, captures) as MCP background tasks for clients that support them; others still call them synchronously |
|
| Confirm-token lifetime (s) |
|
| OpenAPI index cache ( |
|
| Response size budget |
|
| Audit trail of previews and applied writes |
Least-privilege account
Run the server as a regular (non-admin) CML user, never admin. Tested on CML 2.10 with two accounts, both
created by scripts/seed_lab.py --least-privilege. Full guide: docs/least-privilege.md.
Read tier:
mcp-cml-viewer, labs shared with its group aslab_view+lab_exec(consoles needlab_exec), and a resource pool with 0 node licences. Every read tool works exceptcml_get_licensing(admin-only in CML).Write tier:
mcp-cml-operatorowns the labs it builds; its resource pool caps licences, CPU and RAM, and the start previews respect the pool.As a non-admin, licence planning counts only the labs the account can see (
scopesays so), and consoles of shared labs work.
Docker (HTTP)
docker compose up -d # pulls ghcr.io/dr-stutters/cml-mcp-server (MCP_IMAGE_TAG, default latest)
docker compose up -d --build # or build from this checkout (no token: core is bundled)
curl -s http://localhost:8032/health # {"status":"ok"}The container runs as non-root on a read-only root filesystem with all capabilities dropped. The spec cache lives in a named volume. Shared hosts (TLS, several controllers, per-user tokens, the shared licence budget, upgrades): docs/deploying.md.
Development
uv run ruff check && uv run ruff format --check && uv run mypy && uv run pytest
uv run pytest tests/live -m live # against the lab in .env; creates and deletes mcp-test-* labs
uv run python scripts/capture_fixtures.py mcp-test-probe # refresh redacted fixturesscripts/seed_lab.py [--start] [--recreate] creates a reusable lab, mcp-test-seed. It has two
IOL-XE routers running OSPF, an IOL-L2 switch and two Alpine hosts (h1 10.0.10.10 and h2
10.0.20.10, which can ping each other). Device logins are cisco/cisco.
Note: CML caps running licensed nodes across all labs (an unregistered controller in evaluation
mode allows 5; the test controller is now Enterprise with 20 + 50 expansion = 70). Unmanaged
switches and external connectors don't count. cml_plan_licences shows the cap and which labs to stop.
Live tests
Live tests build throwaway labs named mcp-test-* from fast-booting IOL nodes, run every tool,
and always delete what they created. They read the controller from .env (CML_URL plus a login) and skip
without one. Optional inputs (commented in .env.example); the tests that need them skip when they are unset:
Variable | Example | Used by |
|
|
|
|
|
|
|
|
|
|
|
|
-m live_recipes verifies only recipe labs that are already running on the controller.
Licence
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Discover, run, inspect, build, test, and privately reuse AI workflows.
AI-callable tools for API mocking, testing, monitoring, security, and automation.
Discover, compare, route, and execute machine-accessible capabilities for AI agents.
Remote MCP for RunComfy: ComfyUI deployments, hosted models, LoRA training. 31 tools.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA comprehensive toolkit that enables automated interaction with Cisco Modeling Labs (CML) for creating network topologies, configuring devices, and managing lab environments.1-
- AlicenseCqualityCmaintenanceEnables AI agents to control GNS3 network emulation labs. Supports building topologies, managing devices, capturing packets, and automating device CLIs.100MIT
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to interact with Cisco Modeling Labs (CML) using natural language, allowing creation, management, and automation of network labs.833 PyPI75BSD 2-Clause "Simplified"
- FlicenseNot gradedqualityDmaintenanceEnables AI models to generate, validate, and deploy network topologies directly into Cisco Packet Tracer from natural-language prompts.-