The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Cisco SD WAN MCP Server listing page.
cisco-sdwan-mcp is an MCP server for
Cisco Catalyst SD-WAN Manager (vManage), built with
FastMCP.
It gives an LLM client a working view of your SD-WAN fabric — inventory, device health, control and data plane state, alarms, path quality, templates and policies — so you can ask "why is the Frankfurt branch down?" and get an answer backed by real controller data instead of a guess.
Read-only by default. The configuration-changing tools are not registered unless you explicitly enable them, and even then every call requires a human to approve it.
| Area | Tools |
|---|---|
| Inventory | list_devices, get_device, get_fabric_summary, list_inventory |
| Device health | check_device_health, get_system_status, get_control_connections, get_bfd_sessions, get_omp_peers, get_interfaces |
| Alarms & events | get_alarm_summary, list_alarms, list_events |
| Path quality | find_degraded_tunnels, get_tunnel_statistics, get_interface_statistics |
| Templates & policy | list_device_templates, get_device_template, list_feature_templates, list_policies, get_template_input_variables |
| Config groups & profiles | list_config_groups, get_config_group, get_device_config_group, get_config_group_device_variables, list_feature_profiles, get_feature_profile, get_parcel, get_parcel_schema, get_device_running_config |
| Configuration (opt-in) | attach_device_template, activate_vsmart_policy, update_parcel, update_cli_addon_config, deploy_config_group, get_task_status |
Plus four workflow prompts and three resources.
Three design decisions are worth knowing up front, because they shape every tool:
detailed=true when you want
everything.count (what
matched) alongside returned (what you got), so "3 devices are down" is
never confused with "3 devices are down in the first 100 I looked at".Make a dedicated vManage account. Give it a read-only role to start. The account's privileges are the real security boundary — see Write protection.
From PyPI, if you only want to run it:
From a checkout, if you want to change it:
The three settings you must fill in:
The server reads .env from the repository root at startup — set
SDWAN_ENV_FILE to load a different file. Variables already present in the
environment (compose env_file, Kubernetes secrets) are never overwritten by
it, and the startup log names the file it used.
The server starts over HTTP on 0.0.0.0:8000. The MCP endpoint is at
http://localhost:8000/mcp, a health probe at http://localhost:8000/healthz,
and this README at http://localhost:8000/.
Startup logs confirm what it will talk to before any client connects:
Point an MCP client at it (see Connecting an MCP client)
and ask for get_fabric_summary. It is one round trip and exercises
authentication, TLS and reachability at once:
Everything is environment-driven; .env.example is the annotated reference.
| Variable | Default | Description |
|---|---|---|
SDWAN_VMANAGE_URL | — | Controller URL, e.g. https://vmanage.example.com:8443. Required (or use SDWAN_VMANAGE_HOST) |
SDWAN_VMANAGE_HOST | — | Hostname instead of a full URL |
SDWAN_VMANAGE_PORT | 443 | Port, when using SDWAN_VMANAGE_HOST |
SDWAN_USERNAME | — | vManage username. Required |
SDWAN_PASSWORD | — | vManage password. Required |
SDWAN_VERIFY_SSL | true | TLS certificate verification |
SDWAN_CA_BUNDLE | — | Path to a CA bundle — the right answer for a private CA |
SDWAN_TIMEOUT | 60 | Seconds to wait for vManage |
SDWAN_PAGE_SIZE | 100 | Default cap on records per tool call |
SDWAN_ENABLE_WRITES | false | Register the configuration tools — see below |
| Variable | Default | Description |
|---|---|---|
MCP_SERVER_NAME | cisco-sdwan-mcp | Name advertised to MCP clients |
MCP_TRANSPORT | http | http or stdio |
MCP_HOST | 0.0.0.0 | Bind address (HTTP only) |
MCP_PORT | 8000 | Bind port (HTTP only) |
MCP_AUTH | none | How MCP clients authenticate to this server |
LOG_LEVEL | INFO | Python log level |
SDWAN_USERNAME/SDWAN_PASSWORDauthenticate this server to vManage.MCP_AUTHgoverns how clients authenticate to this server. They are unrelated, and you generally want both.
vManage very often presents a self-signed or private-CA certificate. In descending order of preference:
SDWAN_CA_BUNDLE at the controller's CA — verification stays on.certificates/, which the Docker build installs into the
container trust store automatically.SDWAN_VERIFY_SSL=false.
The server logs a warning naming the host each time it does this, because
it means anything on the path can read the credentials.The tools that change configuration are gated twice.
Gate 1 — registration. With SDWAN_ENABLE_WRITES unset or false, the
module holding them is never imported. They do not appear in the tool list, so
a model cannot call them by mistake, misinterpretation or prompt injection.
The server is read-only by construction, not by policy.
Gate 2 — confirmation. With writes enabled, each call still asks the user
through MCP elicitation, naming the template or policy and the devices
affected, before anything reaches vManage. Clients that do not implement
elicitation cannot silently proceed — the call is refused unless the caller
passes confirm=true, which puts the decision in a human's hands either way.
The vManage account is the real boundary.
SDWAN_ENABLE_WRITEScontrols which tools exist in this server; it does nothing about what the account can do through any other path. If a change must be impossible, use a read-only vManage role — do not rely on this flag alone.
vManage applies configuration asynchronously: a write returns a task_id,
meaning accepted, not applied. Poll get_task_status(task_id) until it
reports done.
The intended flow for a template push, with a review step in the middle:
And for a config-group fabric, where edits and deploys are separate steps:
Every tool takes limit (cap on records) and most take detailed (return all
vManage fields instead of the summary set).
| Tool | What it answers |
|---|---|
get_fabric_summary() | Device counts by type, reachability and version, plus every unreachable device. Start here for open questions. |
list_devices(device_type, reachability, site_id) | Devices vManage is currently talking to. |
get_device(device) | One device's full record. Accepts hostname, system IP or chassis number. |
list_inventory(category, unattached_only) | Everything provisioned, including devices that never onboarded, have invalid certificates or carry no template. |
| Tool | What it answers |
|---|---|
check_device_health(device) | Triage in one call — system status, control connections and BFD, with a problems list naming what is wrong. |
get_system_status(device) | Uptime, CPU, memory, disk, last reboot reason. |
get_control_connections(device) | Connections to vSmart/vBond/vManage. Check first when a device will not come up. |
get_bfd_sessions(device, state) | Data-plane tunnels to other edges. Check when sites reach controllers but not each other. |
get_omp_peers(device) | OMP peering — control up but OMP down means no overlay routes. |
get_interfaces(device, vpn_id, interface_name) | Interface status, addressing and error counters. |
These poll the device through vManage, so they reflect live state but cost a
round trip to the edge. Prefer check_device_health over three separate calls.
| Tool | What it answers |
|---|---|
get_alarm_summary(hours) | Counts by severity and component, top rules, most affected devices. Cheap — call before listing. |
list_alarms(hours, severity, active_only) | The alarms themselves. |
list_events(hours, severity, component) | Raw event stream — noisier, but shows flaps and transitions that never became alarms. |
| Tool | What it answers |
|---|---|
find_degraded_tunnels(hours, max_loss_percent, max_latency_ms, max_jitter_ms) | Tunnels breaching thresholds, worst first, each saying which threshold it broke. |
get_tunnel_statistics(device, hours) | Raw per-tunnel loss/latency/jitter/vQoE. |
get_interface_statistics(device, hours, interface_name) | Historical throughput and error counters. |
These read vManage's statistics database — fast, but only as fresh as the last collection cycle (30 minutes on most deployments). For live state, use the device health tools.
| Tool | What it answers |
|---|---|
list_device_templates(device_type, attached_only) | Templates and their attachment counts. |
get_device_template(template_id) | One template's definition plus attached devices. |
list_feature_templates(template_type) | The building blocks. |
list_policies(policy_scope) | Centralized (vSmart) or localized policies, and which is active. |
get_template_input_variables(template_id, device_ids) | Read-only preview of the values an attachment would push. |
The UX-2.0 configuration model: a config group bundles feature profiles
(system, transport, service, cli, policy-object), each profile holds parcels
— the actual configuration payloads — and a device belongs to at most one
group. On a fabric managed this way the template endpoints report nothing
useful; these tools are the equivalent surface. vManage reports membership on
the WAN-edge inventory as "managed-by": "Config-Group <name>", which is also
what get_device and list_inventory surface.
| Tool | What it answers |
|---|---|
list_config_groups() | Groups, their profiles, device counts and up-to-date state. |
get_config_group(group) | One group (by name or ID) with its member devices and which are awaiting a deploy. |
get_device_config_group(device) | Which group manages a device — and therefore whether templates must keep away. |
get_config_group_device_variables(group, device_ids) | The per-device values a deploy would resolve; the config-group twin of get_template_input_variables. |
list_feature_profiles(profile_type) | Profiles of one type, or all five. |
get_feature_profile(profile_type, profile_id) | The parcel tree, each node with a ready-to-use parcelPath. |
get_parcel(profile_type, profile_id, parcel_path, parcel_id) | One parcel's payload — the actual knobs. |
get_parcel_schema(profile_type, parcel_path) | Every field a parcel type can hold; consult before editing. |
get_device_running_config(device) | The device's current configuration, for pre/post-deploy diffs. |
With writes enabled, the config-group counterparts to a template push are
update_parcel (edit structured knobs read-modify-write), update_cli_addon_config
(append/replace raw IOS-XE lines in a CLI add-on profile, returns a diff) and
deploy_config_group (push the group to named member devices — the only step
that touches production). Edits mark members configGroupUpToDate: false
until deployed. attach_device_template refuses devices a config group
manages, so the two configuration models cannot fight over a device.
Reusable workflows that encode the order an engineer actually works in — control plane before data plane, evidence before conclusions.
| Prompt | Use it for |
|---|---|
troubleshoot_device(device, symptom) | Structured device triage, stopping at the first real cause |
fabric_health_report(hours) | A whole-fabric report: devices, alarms, path quality, recommendations |
analyse_path_quality(hours, site) | Tunnel performance, clustered by color / site / device to point at the cause |
review_template_change(template_id) | Pre-change review with an explicit go/no-go — recommends only, never attaches |
| URI | Contents |
|---|---|
sdwan://config | Connection settings in effect — controller, user, TLS mode, whether writes are on. Never includes the password. |
sdwan://devices | Current fabric inventory with per-device status |
sdwan://device/{identifier} | One device's full record, by hostname or system IP |
claude_desktop_config.json).vscode/mcp.jsonWith an OAuth mode (github, google, oauth-proxy, …) no header is needed —
MCP clients discover the flow and open the login screen themselves.
From PyPI — nothing to clone, uvx fetches the package on first run:
From a checkout:
MCP_AUTH applies. Everything in deploy/ assumes it.MCP_HOST/MCP_PORT and
MCP_AUTH do not apply; the process is secured by your OS user.Deploying anywhere or serving multiple users → HTTP. One client on your own machine → either works.
Authentication of clients to this server is off by default and selected at
startup with MCP_AUTH. The factory lives in cisco_sdwan_mcp/auth.py; all modes are
backed by FastMCP's built-in providers.
MCP_AUTH | Use case |
|---|---|
none (default) | Local development, or network-level protection (IAM, VPN, mTLS) |
static | Fixed bearer tokens — quick tests only, never production |
jwt | You already have an IdP issuing JWTs (Keycloak, Okta, Entra ID, Cognito…) |
introspection | Your IdP issues opaque tokens (RFC 7662) |
oauth-proxy | Full OAuth 2.1 login flow via any OAuth provider |
github, google, azure, auth0, workos | Full login flow via a hosted identity provider, preconfigured |
The full variable reference for every mode is in .env.example.
Notes:
/healthz and / stay public — probes and humans don't carry tokens; the
MCP endpoint returns 401 without a valid token.cisco_sdwan_mcp/auth.py and register it in
_BUILDERS (provider docs).A server exposing your WAN topology should not run
MCP_AUTH=noneon a reachable network. Seedeploy/README.md.
Or manually:
Helper scripts build the image, replace any container of the same name, and
start the server at http://localhost:8000/mcp, passing your .env through:
The image includes a HEALTHCHECK against /healthz, so docker ps shows
container health out of the box.
Drop any .crt/.pem root CA files into certificates/. The build adds them
to the container trust store and runs update-ca-certificates automatically —
which covers both a TLS-intercepting proxy and a vManage certificate signed
by your internal CA. Leave the directory empty if you don't need it.
For a proxy, HTTP_PROXY/HTTPS_PROXY/NO_PROXY are predefined Docker build
args and need no Dockerfile edits:
The container is a plain HTTP server on port 8000 with a /healthz probe, so
it runs anywhere. deploy/ ships raw Kubernetes manifests, a Helm chart, a
Styrmin driver and a Cloud Run service definition — see
deploy/README.md for full walkthroughs, including the
SD-WAN-specific parts: reaching a management-network controller from the cloud,
private-CA handling, and vManage's per-account session limits.
This repository is also a Styrmin
Application Driver — driver.styrmin.yml and values.j2.yml at the root are
what Styrmin reads when it clones it. See
deploy/styrmin.md.
/healthz deliberately does not check vManage. A brief controller outage
should not restart pods — tools report the problem per call, and the server
recovers on its own.
Capabilities live in three packages, one module per concern. Each package's
__init__.py imports its modules so the decorators run — add a module, add one
import line.
Then add my_tools to the import list in cisco_sdwan_mcp/tools/__init__.py.
Use @sdwan_tool rather than @mcp.tool — it registers the tool and
converts SD-WAN failures into a readable {"error", "message"} result. Reach
for the shared helpers rather than reimplementing them:
| Helper | Purpose |
|---|---|
resolve_device_id(device) | Hostname / system IP / chassis → the system IP vManage's real-time endpoints need |
client.get_data(path, params) | GET and unwrap vManage's {"data": [...]} envelope |
project(records, fields) | Trim wide records to what matters |
envelope(records, limit=...) | Add count/returned/truncation notes |
build_query(hours=..., rules=...) | Build the JSON query param alarms/events/statistics need |
count_by(records, field) | Tally a field into a summary |
The docstring is what the model reads to decide whether to call your tool —
say what question it answers, not just which endpoint it hits. Keep write
operations in config_tools.py so the registration gate keeps covering them.
The suite runs against a fake vManage (httpx.MockTransport) rather than a
live controller, so it covers the things that actually break in the field:
SDWAN_ENABLE_WRITES=true and present with ittests/conftest.py holds the fake controller; use it as the pattern for your
own tools.
| Symptom | Cause and fix |
|---|---|
ConfigurationError: No controller configured | SDWAN_VMANAGE_URL (or SDWAN_VMANAGE_HOST) is unset. |
AuthenticationError: rejected the credentials | Wrong username/password, or the account is locked. vManage returns HTTP 200 with the login page for a bad password — the client detects that and reports it as an auth failure. |
AuthenticationError: may lack the required role | Authentication worked but the account lacks privileges for that endpoint. Template and policy endpoints need more than a bare read-only role. |
AuthenticationError: issued no JSESSIONID cookie | The URL points at a proxy that strips cookies, not at vManage itself. |
APIError: cannot reach <host> | DNS, routing, firewall or the wrong port. vManage commonly listens on 8443, not 443. |
APIError: timed out after 60s | Real-time endpoints poll the device itself. Raise SDWAN_TIMEOUT, or scope the query to one device. |
| TLS / certificate verify failed | Private CA. Set SDWAN_CA_BUNDLE or add the CA to certificates/. SDWAN_VERIFY_SSL=false is a lab-only last resort. |
APIError: response was not valid JSON | The endpoint doesn't exist on this vManage version — API paths vary across releases. |
| A write tool "doesn't exist" | Expected: SDWAN_ENABLE_WRITES is not true. |
A write returns applied: false with confirm=True guidance | The client doesn't support MCP elicitation, so it cannot ask you to approve. |
| Empty results everywhere, no error | The account may be scoped to a tenant or device group with no devices. Check sdwan://config and try list_inventory. |
Set LOG_LEVEL=DEBUG for more detail. Note that vManage error bodies can be
verbose — check what yours returns before enabling debug logs in a shared
environment.