cnc-mcp
This MCP server lets an AI agent operate Cisco Crosswork Network Controller (CNC) through typed, documented MCP tools.
Read and monitor the network: devices and reachability, topology maps and links, SR/RSVP-TE policies, alarms and events, Data Gateway health, NSO sync state, inventory/collection jobs, device groups, performance dashboards/NPM analytics, OAM trace routes, SWIM state, and the ZTP catalogue.
Change and provision the network when writes are enabled: onboard devices, manage credential profiles/providers, map devices to Data Gateways, drive NSO sync/connect actions, provision SR-TE policies, ODN templates and L3VPNs (SR-MPLS or SRv6), manage tags/locks/locations, alarm suppression and severity settings, config backups/templates/deployments, webhook/Kafka/gRPC subscriptions, device-group membership, PM policies, OAM probes, ZTP files/profiles/devices, and EMS scheduler jobs.
Safety controls: writes are off by default, with an area allowlist, tool denylist, and dry-run mode; destructive tools are annotated.
Built-in playbooks and prompts: device investigation, network health reports, SR-policy explanation, alarm triage, service explanation, SRv6 readiness, and end-to-end L3VPN/SR-policy provisioning workflows.
Agent-friendly output: markdown or JSON responses, paging/filtering conventions, server-side wait/polling tools, and platform error reasons surfaced verbatim.
Provides tools for interacting with the Cisco Crosswork Network Controller (CNC) API, covering device inventory, credential profiles, providers (SR-PCE, NSO, etc.), the topology graph, tags, alarms, users, installed applications, and inventory jobs. Read tools list/get devices, credential profiles, providers, topology summaries/nodes/links, and platform resources, with paging and case-insensitive wildcard filters. Optional write tools (when enabled) create/update/delete devices, credential profiles, and providers, returning Crosswork's job envelope.
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., "@cnc-mcp list all devices that are currently unreachable"
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.
cnc-mcp
An MCP server that lets an AI agent operate Cisco Crosswork Network Controller (CNC) — the SDN controller for Cisco service-provider networks — through its REST APIs.
With this server connected, an agent can answer questions like "which devices are unreachable?", "what does the topology look like?", "which SR policies are down and what path do they take?", "is the Data Gateway collecting?", "is PE1 in sync with NSO?", "is the network ready for SRv6?", and, when writes are enabled, onboard devices, manage credential profiles and providers, map devices to gateways, drive NSO sync and connect actions, provision SR-TE policies through the SR-PCE, provision ODN templates, SR-TE policies and L3VPNs — over SR-MPLS or SRv6 — through NSO's T-SDN function packs (dry-run first), subscribe webhooks and external Kafka/gRPC feeds to alarm/inventory events, inspect collection jobs, manage device groups and their membership, the LCM / Circuit-Style managers, create and activate performance-monitoring policies and read the dashboards and NPM analytics, tune alarm settings (event-type severity, auto-clear, alarm-manager switches), run OAM trace routes, read SWIM state and manage the ZTP catalogue (config files, profiles, serial numbers, static routes, devices) — all through typed, documented tools with the platform's own error reasons surfaced verbatim.
285 tools (187 read, 98 write) over 24 API areas plus seven MCP prompts. Every tool was built from behaviour verified against a live CNC 7.2 instance, not from the documentation alone — see How it was verified.
Contents
Project: CHANGELOG · CONTRIBUTING · SECURITY
Docs: platform facts (the full verified list) · API coverage (every published CNC 7.2 operation against the tools) · RBAC (the API rows a least-privilege account needs)
Related MCP server: Cisco RADKit MCP Server
Quickstart
Requires uv (it fetches a Python 3.11+ on its
own if none is installed) and a Crosswork user (the admin role covers
everything; docs/RBAC.md lists the exact API rows a
least-privilege account needs, and cnc_check_permissions verifies one).
Run it without cloning
uvx --from git+https://github.com/dr-stutters/cnc-mcp cnc-mcpuvx fetches the repository, builds the package into its cache and starts
the server on stdio. Append a ref to the URL to pin what you run — @v0.1.0 (a release tag, once
that release exists) or @main.
Settings are read from CNC_MCP_* environment variables or a .env file in
the server's working directory — Configuration lists every
variable and .env.example is a commented template. Run unconfigured, the
server exits at once with Configuration error — check environment variables (CNC_MCP_BASE_URL), which is the quickest check that the install works.
A PyPI package (plain uvx cnc-mcp) is planned once the project is
registered there; until then the git URL above is the install path.
Register it with an MCP client
Claude Code
claude mcp add cnc -e CNC_MCP_BASE_URL=https://cnc.example.com:30603 \
-- uvx --from git+https://github.com/dr-stutters/cnc-mcp cnc-mcpThe credentials (CNC_MCP_USERNAME / CNC_MCP_PASSWORD, or
CNC_MCP_API_TOKEN) reach the server either as variables exported in the
shell that starts the client, or from a .env file in the server's working
directory. To make that directory explicit — independent of where the client
happens to start the server — pass uv's --directory flag:
claude mcp add cnc -- uvx --directory /home/me/cnc-config \
--from git+https://github.com/dr-stutters/cnc-mcp cnc-mcpwith /home/me/cnc-config/.env holding the CNC_MCP_* settings (copy
.env.example). Claude Code's default local scope and the user scope
keep the registration in your own ~/.claude.json; the project scope
writes a .mcp.json into the repository for everyone who checks it out.
CNC_MCP_USERNAME / CNC_MCP_PASSWORD must never go into a shared client
config — a .mcp.json in a repository, a team-distributed
claude_desktop_config.json, a .cursor/mcp.json that gets committed.
Use -e only for non-secret settings such as CNC_MCP_BASE_URL, and keep
the secrets in a .env (git-ignored here) or in your own shell environment.
Claude Desktop — claude_desktop_config.json (Settings → Developer →
Edit Config):
{
"mcpServers": {
"cnc": {
"command": "uvx",
"args": [
"--directory", "/home/me/cnc-config",
"--from", "git+https://github.com/dr-stutters/cnc-mcp", "cnc-mcp"
],
"env": { "CNC_MCP_BASE_URL": "https://cnc.example.com:30603" }
}
}
}Desktop clients do not start servers in a predictable working directory, so
the --directory form is the reliable way to have the .env found. If the
client reports that it cannot find uvx, use its absolute path (which uvx)
as command.
Cursor — the same mcpServers block in ~/.cursor/mcp.json (per user)
or .cursor/mcp.json (per project, shared if committed).
VS Code — .vscode/mcp.json (per project) or the user-level mcp.json
(MCP: Open User Configuration); same entry, but the top-level key is
servers and the entry carries "type": "stdio".
Write tools are not registered at all until CNC_MCP_ENABLE_WRITES=true,
so a read-only registration cannot be talked into changing anything;
Safety controls covers the area allowlist, the tool
denylist and the dry-run mode that sit on top of that switch.
Clone and run (development)
git clone https://github.com/dr-stutters/cnc-mcp && cd cnc-mcp
make install # uv sync
cp .env.example .env # set CNC_MCP_BASE_URL, USERNAME, PASSWORD
make test && make lint # 2,800+ tests, all HTTP mocked — no CNC needed
make run # start the server on stdio
make inspect # MCP Inspector against it
make cli ARGS="list" # scripts/mcp_cli.py: list | schema | call | prompts | prompt
make rbac-check # the packaged RBAC map still matches the tool source
make build # wheel + sdist into dist/scripts/mcp_cli.py drives the server over the real MCP stdio protocol —
instructions, list [--writes], schema <tool>, call <tool> '<json>',
prompts, prompt <name> '<json>', each with --env NAME=VALUE to start
the server under other settings — so what it prints is exactly what an
agent sees. To register a checkout with
a client, replace the uvx command above with
"command": "uv", "args": ["--directory", "/path/to/cnc-mcp", "run", "cnc-mcp"];
the checkout's own .env is then the configuration.
Docker
docker run -i --rm --env-file .env ghcr.io/dr-stutters/cnc-mcp:latestThe image is published to GHCR by the release workflow on each tag. It
contains no credentials (.dockerignore keeps .env out of the build
context); they are passed at run time with --env-file or -e. To build
locally instead: make docker-build, then docker run -i --rm --env-file .env cnc-mcp. In a client config the entry is "command": "docker" with
"args": ["run", "-i", "--rm", "--env-file", "/path/to/.env", "ghcr.io/dr-stutters/cnc-mcp:latest"].
Every setting, its default and what it does is in Configuration
below; .env.example is the same list as a commented template.
Tools
Read tools — registered in every mode:
Area | Tools |
Devices |
|
Credential profiles |
|
Providers (SR-PCE, NSO, …) |
|
Topology (RESTCONF NBI) |
|
TE state (SR-PCE feed) |
|
SR-TE operations (Optimization Engine) |
|
Platform |
|
Data Gateway |
|
NSO |
|
Inventory extras |
|
Fault |
|
Device configuration |
|
EMF inventory |
|
Platform admin & RBAC |
|
Notifications |
|
Collection service |
|
Device groups |
|
LCM & Circuit-Style (Optimization Engine) |
|
Services (CAT inventory, T-SDN) |
|
Performance monitoring (PM policies, dashboards, NPM) |
|
OAM & probes |
|
SWIM & ZTP |
|
EMS inventory scheduler |
|
Playbooks (one call, composed from the tools above) |
|
Write tools — registered only with CNC_MCP_ENABLE_WRITES=true (and, with
CNC_MCP_WRITE_AREAS, only for the listed areas); deletes carry the MCP
destructive annotation:
Area | Tools |
Devices |
|
Credential profiles |
|
Providers |
|
Data Gateway |
|
NSO |
|
SR-TE operations |
|
Platform admin |
|
Inventory extras |
|
Fault |
|
Device configuration |
|
Notifications |
|
Device groups |
|
LCM |
|
Service provisioning (NSO proxy, T-SDN CFPs) |
|
Performance monitoring (PM policies, retention) |
|
OAM & probes |
|
SWIM & ZTP (the ZTP catalogue) |
|
EMS inventory scheduler |
|
Playbooks |
|
The playbook tools compose the others server-side: each answers with a verdict (healthy / degraded / red / deployed …, with the reasons), one section per underlying tool, and an audit list of the calls it made, so an agent can drill into any section with the individual tool. A section whose call fails is reported as unavailable rather than failing the whole answer. Blind-agent measurements: a "device looks degraded" investigation dropped from 44 tool calls to a handful, a network health overview from 25.
Seven MCP prompts package the operator workflows for clients that expose
them as slash commands: troubleshoot_device, network_health_check,
explain_sr_policy, provision_l3vpn (with an optional srv6_locator),
alarm_triage, explain_service, srv6_readiness.
Each tells the assistant which playbook to start from, where to drill in, and
what to do when the write tools are absent.
Every tool has flat, typed parameters with examples and constraints (unknown
argument names are rejected with a "did you mean" hint), a docstring that
states when to use it, what it returns, and what each error means, and a
response_format of markdown (curated summary, the default) or json
(complete data). The wait_for_* tools poll server-side so an agent
never has to loop on a status check.
Conventions the server also tells agents about at connect time:
Paging is
page_size/page(0-based); list tools return{total, count, page, page_size, has_more, next_page, items}wheretotalcounts matches for the filter andcollection_totalthe whole collection.Filters are exact-match, case-insensitive, with
*as a wildcard.Enums accept friendly values (
admin_state="up",family="sr_pce",protocol="ssh") or the platform's wire values.Writes return Crosswork's job envelope (
job_id,state,impacted). A job the platform rejected comes back asError: …with the platform's reason, never as a silent success.Ordering: credential profile → provider → device. A device's
te_router_idmust match its router-id in the SR-PCE topology for the two to correlate. ZTP: config file → profile → serial numbers → device, torn down in reverse (an in-use serial cannot be deleted).Network-impacting writes are named as such: activating a PM policy starts SNMP/telemetry collection on its devices within seconds; a device group is populated by moving devices out of the leaf that holds them (
Unassigned Devicesfor a device never placed), because the platform's "set members" call removes; reverting an event type's auto-clear deletes the interval rather than restoring a default.
SRv6
CNC 7.2 exposes SRv6 through the same NBIs as SR-MPLS, with no locator
object anywhere in the topology model: a node advertises SRv6 node SIDs
per IGP instance and a link End.X adjacency SIDs, so cnc_list_srv6_locators
derives each locator from a node SID and its block / node lengths
(fc00:0:1::/48 from lb 32 + ln 16, labelled uSID F3216 when the function
length is 16 too). cnc_get_topology_node / cnc_get_topology_link render
the SIDs, their structure and the node's Flex-Algos, cnc_get_topology_summary
counts them, and cnc_list_topology_nodes / cnc_list_sr_policies take a
dataplane filter (sr-mpls | srv6). SR-policy state carries no dataplane
leaf either: a policy is reported as srv6 when it carries an
srv6-binding-sid, IPv6 hops or IPv6 router-id keys (cnc_get_sr_policy
accepts them; cnc_get_te_summary counts by dataplane; cnc_explain_sr_policy
says which dataplane it found). cnc_get_srv6_locator_statistics reads the
Performance dashboard's per-locator outBitRate series (it needs an
SRV6LOCATOR monitoring policy on the device), and cnc_srv6_readiness folds
all of it into a READY / PARTIAL / NONE verdict that names the nodes without a
locator and the adjacencies without an End.X SID.
Provisioning goes through the T-SDN function packs only: srv6_locator on
cnc_create_sr_policy_service, cnc_create_odn_template,
cnc_create_l3vpn_service (service-wide or per endpoint) and
cnc_provision_l3vpn_e2e. The function pack's rules are checked before
anything is sent: an SRv6 policy needs an IPv6 tail-end and a dynamic path —
explicit SID lists, bandwidth and a binding-SID are refused — and NSO
validates neither the locator name nor the tail-end against the routers, so
dry-run first and confirm the locator on the router with
cnc_get_nso_device_config(subtree='segment-routing/srv6'). Not in 7.2, and
the tools say so rather than pretend: PCE-initiated SRv6 policies (the
Optimization Engine RPCs cnc_create_sr_policy / cnc_dryrun_sr_policy are
SR-MPLS only), SRv6 OAM trace routes (cnc_start_oam_trace_route is MPLS
LSP-ping only, so the L3VPN playbook skips the trace for an SRv6 VPN),
explicit SRv6 SID lists, and L2VPN with SRv6-TE.
Verification status: the lab has no SRv6 underlay yet, so the
populated SRv6 renderings are built from the 7.2 YANG / OpenAPI shapes and
exercised on fixtures only; what is verified live is that every reader
answers "no SRv6" on the SR-MPLS lab with its SR-MPLS content unchanged,
that cnc_srv6_readiness answers NONE with the underlay hint, that the
srv6locator PM endpoints answer empty, and — through NSO dry-run, nothing
committed — that the three creates render the expected CLI (srv6 / locator LOC1 binding-sid dynamic behavior ub6-insert-reduced with an IPv6 end-point
for the policy and the ODN template; segment-routing srv6 / locator LOC1 / alloc mode per-vrf under the VRF's address-family for the L3VPN).
How it works
MCP client ──stdio──▶ cnc-mcp ──HTTPS──▶ Tyk gateway (:30603) ──▶ Crosswork services
│
├─ auth.py CAS SSO: ticket-granting ticket → service ticket (an 8 h JWT)
├─ client.py retries, concurrency cap, one transparent re-auth
├─ errors.py platform status/body → actionable "Error: …" hints
├─ crosswork.py JSON-over-POST dialect: query grammar, envelopes, job checks
├─ restconf.py RESTCONF NBI dialect (topology, optimization engine, NSO proxy)
├─ emf.py EMF RESTCONF dialect (fault / inventory / performance)
├─ probe.py "is this API even present on this deployment?"
└─ tools/ one module per API area, registered through safety.pyAuthentication. Crosswork's two-leg CAS flow is implemented in
CrossworkCasAuth: POST /crosswork/sso/v1/tickets yields a ticket-granting
ticket, exchanged for a service ticket that is an 8-hour JWT sent as
Authorization: Bearer. Crosswork never answers 401 — an expired token is a
403 "Unauthorized request", a malformed one a 500 "Middleware error" — so
the auth strategy decides what "re-authenticate" looks like and the client
retries once. httpx's request logging is capped at WARNING because the
second leg's URL contains the ticket.
One gateway, several API dialects. Behind the single NodePort, CNC's
services speak differently: JSON-over-POST …/query bodies with per-service
grammars (inventory, dg-manager, collection, alarms), RESTCONF NBIs under
/crosswork/nbi/* and the NSO proxy, and an EMF RESTCONF that only returns
JSON for exactly Accept: application/json. Each dialect's verified quirks
live in one helper module, so tool modules stay thin.
Safety. safety.register_tool() is the only way a tool is registered: it
forces a read-only/destructive/idempotent decision, refuses to register
write tools unless writes are enabled (and their area allowed), never
registers a disabled tool, and in dry-run mode swaps each write for its
preview — see Safety controls. POSTs are not auto-retried on 5xx
(a lost response might mean the write happened) unless a tool explicitly
marks the call safe to re-send. Tools never raise: every failure is returned
as an Error: … string with the platform's reason, and secrets never appear
in logs, errors, or output beyond what the platform itself masks.
Safety controls
Four layers, each an environment variable, each applied when the tools are registered: a tool a layer excludes is absent from the tool list the agent sees, not merely refused. (Dry-run mode is the exception by design — the write tools stay visible, but harmless.)
Writes are off by default.
CNC_MCP_ENABLE_WRITES=trueregisters the 98 write tools; without it the server is read-only, and the connect-time instructions say so.CNC_MCP_WRITE_AREAS— a comma-separated allowlist of the areas whose write tools are registered when writes are on (empty, the default, means every area). An area is a module insrc/cnc_mcp/tools/; the ones with write tools aredevices,credentials,providers,sr_te_operations,data_gateway,nso,admin,inventory_extras,fault,device_config,notifications,grouping,lcm_csm,service_provisioning,performance,oam,swim_ztp,ems_jobsandcomposite. Read tools are never affected. The write playbooks incompositeneed the sibling that commits for them:cnc_provision_l3vpn_e2eneedsservice_provisioning(andoamfor its optional trace step),cnc_create_sr_policy_e2eneedssr_te_operations—CNC_MCP_WRITE_AREAS=compositeon its own registers neither, and the startup log says which sibling each one lacks.CNC_MCP_DISABLED_TOOLS— a comma-separated denylist of tool names, read or write, that are never registered whatever the other settings say (cnc_delete_device,cnc_restart_microservice).CNC_MCP_DRY_RUN=true— the write tools stay registered but nothing changes on the platform. A write tool that takesdry_runruns with it forced totrueand answers the preview (the device CLI NSO would push, the path the PCE would compute); every other write tool is not executed and answersNOT EXECUTEDwith the arguments it would have sent (secret-looking values redacted); the two write playbooks stop after their preview stage with adry-runverdict. Each write tool's description ends with which of the two applies to it.
An unknown area or tool name is a configuration error at startup, with a
"did you mean" hint, never a silent no-op; an allowlisted area whose tools
are all read-only is logged as a warning. The startup log summarises the
result (Registered 199 of 285 tools (187 read, 12 write); writes on for areas fault; disabled tools: none; dry-run off), the connect-time
instructions tell the agent which mode it is in, and cnc_check_permissions
repeats it next to the account's role.
scripts/mcp_cli.py --env NAME=VALUE (repeatable) starts the server with
extra variables, to try a mode without editing .env:
uv run python scripts/mcp_cli.py --writes --env CNC_MCP_WRITE_AREAS=fault list
uv run python scripts/mcp_cli.py --writes --env CNC_MCP_DRY_RUN=true \
call cnc_create_tag '{"name": "site-a"}'Least-privilege account. The Crosswork gateway checks every request
against the account's role, per API and HTTP method, so the server can run
under a role that grants only what its registered tools send.
docs/RBAC.md — generated from the tool source by
scripts/rbac_map.py, checked in CI against the packaged map — lists the
exact API rows a read-only account needs and what each write area adds,
with ready-made role bodies in docs/rbac/. cnc_check_permissions reads
the running account's role and reports which registered tools it would
refuse and the rows to grant; a 403 from any tool points at it. The role
bodies are the shape the Crosswork role editor submits (verified against a
role built in the UI and read back; minus the empty _id/id the editor
also sends, and with the read-back's limit/allowance_scope row fields),
except that they grant single API ids where a UI tick grants a whole
display-name group — so manage such a role through the API, not the editor
(docs/RBAC.md says what is verified and what is not).
Configuration
Environment variables (or a .env file), prefix CNC_MCP_:
Variable | Default | Purpose |
| (required) | CNC UI/API URL with scheme, e.g. |
| — | Crosswork user; CAS SSO → JWT, refreshed automatically |
| — | Alternative: a pre-issued JWT (cannot be refreshed; expires in ~8 h) |
|
|
|
|
| Write tools are not registered until |
| (all) | Comma-separated areas whose write tools are registered when writes are on, e.g. |
| — | Comma-separated tool names never registered, read or write |
|
| Write tools registered but not executed: forced preview where the tool has |
|
| Per-request read timeout, seconds (>= 1) |
|
| TCP connect timeout, seconds (>= 1) |
|
| Retries for 429 / 5xx / transport errors (idempotent calls; 0-10) |
|
| Base delay for the exponential backoff between retries, seconds (>= 0) |
|
| Cap on in-flight requests to the platform |
|
| Tool responses longer than this are truncated with a note |
|
| Python logging level (stderr only — stdout is the MCP transport) |
How it was verified
Unit tests prove the code; only a live run proves the integration. Four layers were used:
Mocked unit tests (
make test): every tool has a happy-path test through the MCP server (which validates the input schema) asserting the exact request body sent, and an error-path test. All HTTP is mocked withrespx; CI runs them on Python 3.11 and 3.12.Live smoke (
scripts/live_smoke.py): a plan of real tool calls against a running instance. The read phase is side-effect free; the write phase createssmoke-*objects and removes them again, chaining created UUIDs into later steps, and must leave the platform exactly as it found it.scripts/smoke_plan.example.jsonis a sanitised copy of the plan used.Agent scenarios (
scripts/mcp_cli.py): the server driven over the real MCP stdio protocol by assistants that see only the tool list, schemas and instructions — ten operator tasks (health overview, traffic ranking, policy explanation, service audit, a "degraded device" investigation, and five provisioning/operations tasks with full cleanup) run against the lab; every point of friction they reported became a fix (alarm triage rendering and sorting, unknown arguments rejected by name, host names accepted wherever a router-id is a key, modelled-vs-measured PM caveats, parseable truncation, ...).mcp_cli.pydoubles as a manual test client.Live plumbing check (
scripts/live_plumbing_check.py): exercises every dialect helper against the instance — the real 409, the real error-inside-200, the real XML fallback, the real routing signatures — so a platform change that breaks a verified assumption shows up before it breaks a tool.
The instance was a single-VM CNC 7.2.0 deployment with embedded NSO and
Data Gateway, fed by a Cisco Modeling Labs fabric of five IOS-XRd routers
running IS-IS + SR-MPLS (no SRv6 underlay yet — see SRv6 for what
that leaves unverified), one of them acting as SR-PCE (BGP-LS + PCEP, feeding
CNC over gRPC) with two PCE-delegated SR policies between the PEs, gNMI
onboarded on every router, mpls oam and a vpnv4 iBGP pair on the PEs (so an
L3VPN can be committed through the T-SDN function pack and traced end to end). Each
module was also adversarially reviewed against the recorded facts before it
was merged; that review caught bugs the tests had enshrined (a "not found"
check that would have hidden an absent API, an NSO failure that would have
read as success).
Platform facts that shaped the design
The published OpenAPI documents describe a platform that is not quite the one
on the wire, so the client is built around the differences, each verified live:
there are no 401s (an expired token is 403, garbage is 500); failed writes
are HTTP 200 with state: JOB_FAILED in a job envelope; a missing RESTCONF
entry is 409 data-missing while a 404 means "no such route"; the Optimization
Engine answers bad input with a bare, empty 500; NSO device actions are
fire-and-forget. The full list of 21 verified facts is in
docs/platform-facts.md.
Roadmap
The published CNC 7.2 API has 948 operations across 103 OpenAPI documents;
this server covers the inventory (incl. tags, locks, locations), the EMF
inventory, topology, TE state, SR-TE operations, fault management, device
configuration (backups, templates, deployments), platform administration and
RBAC, Data Gateway, NSO, notifications (webhook and external Kafka / gRPC
subscriptions), the collection service, device grouping (user groups,
membership and rules), the LCM / Circuit-Style managers, the CAT service
inventory and T-SDN service provisioning through the NSO proxy, performance
monitoring (policy lifecycle, retention, dashboards) and NPM analytics, OAM
trace routes and Service Health probes, SWIM reads, the ZTP catalogue (config
files, profiles, serial numbers, static routes, devices) and the EMS
inventory scheduler.
docs/COVERAGE.md is the full picture: every documented
operation, whether a tool sends it, and if not why (it is generated by
scripts/api_coverage.py from the OpenAPI set, so its numbers are computed,
not claimed). Planned modules, in the order they become exercisable on a lab:
Module | Scope |
SRv6 on a live underlay | the SRv6 readers and renderings (SRv6) verified against a fabric that runs locators, IS-IS IPv6 and SRv6 policies — which members the SR-PCE feed populates, the endpoint-behaviour strings, a populated |
writes not yet exposed | collection job create, SWIM collect/distribute/activate, ZTP image upload / ownership vouchers / device status patch, config restore, LCM/CSM configuration and RSVP-TE / P2MP policy operations — unverified bodies with real network impact |
not on this build | change automation, health insights, path analytics, service health (unrouted on a single-VM 7.2 deployment — a 404 from the home application; the error text names the missing application) |
Project layout
src/cnc_mcp/
server.py assembly, auth selection, connect-time instructions
auth.py auth strategies incl. CrossworkCasAuth
client.py ApiClient: retries, re-auth, concurrency, raw bodies
errors.py PlatformError and the status/body → hint mapping
config.py Settings (env / .env)
safety.py register_tool(): annotations, write gating (areas, denylist), dry-run wrapper
formatting.py markdown/json response formats, pagination envelope, size cap
polling.py wait_until() for the wait_for_* tools
crosswork.py inventory query grammar, envelopes, job checks, enums, dg/collection/alarm helpers
restconf.py RESTCONF NBI helpers
emf.py EMF RESTCONF helpers
probe.py routing classification and availability probing
tools/ devices, credentials, providers, inventory_extras, physical_inventory,
topology, te_state, sr_te_operations, platform, fault, device_config,
data_gateway, nso, admin, notifications, collection, grouping, lcm_csm,
services, service_provisioning, performance, oam, swim_ztp, ems_jobs,
composite (the playbooks)
data/rbac_map.json which gateway API each tool needs (generated; read by cnc_check_permissions)
scripts/
live_smoke.py live tool-call plan runner (read / write phases, $var chaining)
live_plumbing_check.py live verification of the dialect helpers
mcp_cli.py call the server over the real MCP stdio protocol (list/schema/call/prompts)
api_coverage.py maps the published OpenAPI operations onto the tools -> docs/COVERAGE.md
rbac_map.py tool -> gateway API map -> data/rbac_map.json, docs/RBAC.md, docs/rbac/
smoke_plan.example.json sanitised smoke plan
docs/
platform-facts.md the verified platform behaviours the client is built around
COVERAGE.md every published CNC 7.2 operation against the tools (generated)
RBAC.md, rbac/ the API rows a least-privilege role needs, ready-made role bodies (generated)
tests/ one test module per source module; respx-mockedDevelopment
make test # pytest (respx-mocked HTTP)
make lint # ruff check
make fmt # ruff format + autofix
make rbac # regenerate the RBAC map and docs/RBAC.md from the tool source (offline)
make rbac-check # exit 1 when they are stale (CI runs this)
make docker-build # stdio server image; run with: docker run -i --rm --env-file .env cnc-mcpTo add a tool module: read CLAUDE.md (the conventions are non-negotiable),
copy the pattern of an existing module in src/cnc_mcp/tools/, register it
in tools/__init__.py, give every tool a happy-path and an error-path test,
add its calls to the smoke plan, run make rbac (a new tool without a
regenerated map fails CI), and run the live smoke before merging.
License
MIT © 2026 Mitchell McInnes
Available Tools
19 toolscnc_get_credential_profileGet Credential ProfileARead-onlyIdempotent
Get the full record of one credential profile by name.
Read-only. Profiles have no UUID: the name is the identifier everywhere (devices and providers reference it in their "profile" field). Find names with cnc_list_credential_profiles.
Args: profile: exact profile name (case-insensitive; surrounding whitespace is stripped). A '*' wildcard is accepted by the platform but this tool needs a single exact match.
Returns: str: JSON object {"profile": str, "user_pass": [{"user_name", "password": "******", "type": "ROBOT_USERPASS_SSH|HTTP|HTTPS|...", ...}], "v2_info": {...}?, "v3_info": {...}?}. Secrets are masked by the API. On failure: "Error: Credential profile '' not found ..." when nothing matches, "Error: ... matches several profiles ..." when a wildcard was used, "Error: profile must not be empty ..." for a blank name, or "Error: ".
Inputs: profile (required). Returns: result
cnc_get_deviceGet Device DetailsARead-onlyIdempotent
Get the full inventory record of one device by uuid or host name.
Read-only. Pass exactly one selector. Returns every field Crosswork holds for the node: uuid, host_name, node_ip, admin_state, reachability_state, operational_state, reachability_check, profile, connectivity_info, product_info, routing_info, tag_names, dg_name/dg_uuid, nso_state, errors, creation_time, last_upd_time, ...
Note the read/write asymmetry: node_ip.inet_af reads as a string ('ROBOT_INET_ADDR_TYPE_v4') but is the integer 0 in write bodies, so do not feed this object straight back into a write.
Returns: str: JSON object of the node, or "Error: ..." (not found -> no device matched the selector; ambiguous -> a wildcard host_name matched several devices, use the uuid).
Inputs: uuid, host_name. Returns: result
cnc_get_device_collection_summaryGet Device Collection Status SummaryARead-onlyIdempotent
Count devices by collection status across the whole inventory.
Read-only. This is the "Collection status" widget of the Network Devices page: how many devices are in progress, completed, warning, failed or in maintenance for inventory collection. Use it as a quick health check before drilling into individual devices with cnc_list_devices.
Returns: str: JSON with flat integer counts: {"inprogress": int, "warning": int, "failed": int, "completed": int, "maintenance": int} On failure: "Error: ...".
Inputs: no named parameters. Returns: result
cnc_get_inventory_jobGet Inventory JobARead-onlyIdempotent
Get one inventory job by id: its state, type, timestamps, the objects it touched and the error text when it failed.
Read-only. Use it to check the outcome of a device/credential/provider write. For a job that may still be running prefer cnc_wait_for_inventory_job, which polls until it finishes. The lookup scans the newest few hundred jobs (newest first), so a very old job may not be found even though cnc_list_inventory_jobs can still page to it.
Returns: str: JSON of the job: {"job_id", "state", "type", "creation_time", "completion_time", "created_by", "impacted": [" []"], "impacted_objects": [{"uuid", "name", "ip"}], "error"}. A state of JOB_COMPLETED_WITH_WARNING is a success whose advisory is in "error". "Error: No inventory job with id ..." when the id matches nothing; other failures: "Error: ".
Inputs: job_id (required). Returns: result
cnc_get_providerGet Provider DetailsARead-onlyIdempotent
Get the full record of one provider, by UUID or by exact name.
Read-only. Exactly one of uuid / name must be given. Use it to
inspect a provider's endpoints (connectivity_info), credential
profile (profile), family, reachability and properties (for
SR-PCE: auto-onboard, outgoing-interface, device-profile,
preferred-stack) — for example before cnc_update_provider or
cnc_delete_provider. A uuid lookup scans the (small) provider
collection and matches client-side, since uuid is not a verified
filter field on providers/query.
Returns:
str: JSON object with every provider field as Crosswork returns it
(note connectivity_info[].ipaddrs[].inet_af reads as
'ROBOT_INET_ADDR_TYPE_v4'; write bodies use 0 — don't round-trip a
read object into a write). "Error: ..." when neither or both
selectors are given, when no provider matches (verify with
cnc_list_providers), or on an API failure.
Inputs: name, uuid. Returns: result
cnc_get_topologyGet Topology GraphARead-onlyIdempotent
Get the topology graph: one page of nodes plus one page of links (edges).
Read-only. Crosswork returns the whole graph (up to maxLogicalNodes,
5000 on the lab instance) in one response with no server-side paging,
so this tool re-downloads the graph on every call and pages BOTH lists
client-side: page/page_size window the links, node_page/
node_page_size window the nodes. Each link carries both endpoints
resolved (node UUID, node name, interface), so a links page is
self-contained — you do not need the nodes page to read adjacency.
Use it for node-to-node adjacency (e.g. "what is PE1 connected to?");
for tabular attributes (IPs, reachability, link status/utilisation)
prefer cnc_list_topology_nodes / cnc_list_topology_links. Keep page
sizes modest: the response is capped at the configured size limit.
Args: map_type: 'logical' only ('geo' is rejected with an explanation). page_size: links per page (1-500). page: 0-based page of links. node_page_size: nodes per page (1-1000). node_page: 0-based page of nodes. response_format: markdown (default) or json.
Returns:
str: Markdown listing nodes then "A:ifA <-> B:ifB" per link, or JSON:
{"map_type": "LOGICAL", "attributes": {"totalNodes": int, ...},
"nodes": {"total": int, "count": int, "page": int, "page_size": int,
"items": [{"uuid": str, "name": str}],
"has_more": bool, "next_page": int|null, ...},
"links": {"total": int, "count": int, "page": int, "page_size": int,
"items": [{"uuid": str, "name": "-",
"source": {"node_uuid": str, "node_name": str,
"interface": str},
"target": {"node_uuid": str, "node_name": str,
"interface": str}}],
"has_more": bool, "next_page": int|null, ...}}
Node icon/checksum attributes and edge decoration attributes (the
only attributes the platform returns on /data) are dropped.
On failure: "Error: ". An HTTP 500 "Internal Server
Error" from /v1/topology-service/... means the service rejected the
request body (mapType/viewId/params) — deterministic, do NOT retry.
(It is not the inventory's "NATS request failed" signal.)
Inputs: page, map_type, node_page, page_size, +2 more. Returns: result
cnc_get_topology_summaryGet Topology SummaryARead-onlyIdempotent
Summarize the CNC topology: node/link totals and state breakdowns.
Read-only. Combines three topology-service calls (init,
nodes/summary, edges/summary) into one answer. Use it first to
learn whether the topology is populated at all (an empty topology with
a populated inventory usually means no reachable SR-PCE provider), then
drill in with cnc_get_topology, cnc_list_topology_nodes or
cnc_list_topology_links.
Returns:
str: JSON:
{"total_nodes": int, "unmapped_nodes": int, "max_logical_nodes": int,
"reachability": {"CONN_STATE_REACHABLE": 5, ...},
"link_state": {"Up": 1, "Degraded": 0, "Down": 0},
"node_breakdowns": {: {: }, ...},
"link_breakdowns": {: {: }, ...}}
unmapped_nodes counts nodes with no geographic location (they
render on the logical map only). node_breakdowns carries every
section the platform returned (e.g. device family) keyed by type.
Counts are null and breakdowns empty when the platform returns an
empty body (fresh, unpopulated topology).
On failure: "Error: ". An HTTP 500 "Internal Server
Error" from /v1/topology-service/... means the service rejected the
request body (mapType/viewId/params) — deterministic, do NOT retry.
(It is not the inventory's "NATS request failed" signal.)
Inputs: no named parameters. Returns: result
cnc_list_alarmsList AlarmsARead-onlyIdempotent
List Crosswork platform alarms (device reachability, collection, application health, ...), newest first as the platform orders them.
Read-only. Use it to find out why something is unhealthy before digging into devices or providers. Alarms are paged with a SQL-like criteria string ('select * from alarm limit N page M'); no other filtering is exposed. The platform reports no total, so 'has_more' means the page came back full — request the next page to check.
Args: open_only: True for open alarms only (default), False for all. limit / page: page size and 0-based page number.
Returns: str: Markdown with one line per alarm (category, description, created time, id, acknowledged flag, event count; the Events detail is omitted), or JSON: {"total": null, "count": int, "page": int, "page_size": int, "items": [{"AlarmId": str, "AlarmCategory": str, "Description": str, "Created": str, "Updated": str, "Acknowledge": bool, "object_id": str, "origin_app_id": str, "events_count": int, "Events": [...]}, ...], "has_more": bool, "next_page": int|null} On failure: "Error: ".
Inputs: page, limit, open_only, response_format. Returns: result
cnc_list_applicationsList Installed ApplicationsARead-onlyIdempotent
List the applications installed on the Crosswork platform with their versions (e.g. Crosswork Optimization Engine, Service Health, ...).
Read-only. Use it to check what is installed and at which version before assuming a feature (topology, SR-TE, VPN services) is available on this instance.
Returns: str: Markdown "name (application_id) version — description" lines, or JSON: {"count": int, "items": [{"application_id": str, "application_data": {"version": str, "summary": {"name": str, "description": str}, "category": str, "build_information": {"date_time": str, "publisher": str}}}, ...]} On failure: "Error: ".
Inputs: response_format. Returns: result
cnc_list_credential_profilesList Credential ProfilesARead-onlyIdempotent
List credential profiles known to Crosswork, with optional name filter and paging.
Read-only. Use it to find the profile name to reference when adding devices or providers, or to check which protocols (SSH/HTTP/HTTPS/SNMPv2/...) a profile covers. For one profile's full record use cnc_get_credential_profile.
Secrets are masked by the API ("******"); usernames are returned in clear.
Args: profile: name filter (exact, case-insensitive, '*' wildcard; surrounding whitespace is stripped and a blank filter means no filter). page_size / page: filterData paging (0-based page). response_format: 'markdown' (default) or 'json'.
Returns: str: Markdown "- profile — types: SSH (user), HTTP (user), SNMPv2" lines, or JSON: {"total": int|null, "count": int, "page": int, "page_size": int, "items": [{"profile": str, "user_pass": [{"user_name": str, "password": "", "type": "ROBOT_USERPASS_SSH|HTTP|HTTPS|..."}], "v2_info": {"read_community": "", ...}?}, ...], "has_more": bool, "next_page": int|null, "collection_total": int|null} "total" is the number of profiles matching the filter (null when the platform omits it, i.e. zero matches); "collection_total" is the size of the whole collection regardless of filter. On failure: "Error: " (500 "NATS request failed" -> the query body was rejected; 403 -> token rejected or missing privilege).
Inputs: page, profile, page_size, response_format. Returns: result
cnc_list_devicesList DevicesARead-onlyIdempotent
List network devices (inventory nodes) with optional filters and paging.
Read-only. Use it to discover device uuids/host names before calling cnc_get_device, cnc_update_device or cnc_delete_device. Filters AND together. Unmanaged devices are hidden in the CNC UI's default table but are returned here.
Args: host_name, reachability, admin_state, credential_profile: exact-match filters (case-insensitive, '*' wildcard). Enum filters accept friendly or wire values. page_size, page: paging (page is 0-based). response_format: 'markdown' (default) or 'json'.
Returns: str: Markdown, one line per device: "host_name (uuid) ip=... reach=... oper=... admin=... profile=... dg=..." plus "More available: page=N." when another page exists. Or JSON: {"total": int|null, "count": int, "page": int, "page_size": int, "has_more": bool, "next_page": int|null, "collection_total": int|null, "items": []} 'total' is the number of matches for the filter (absent when zero matched); 'collection_total' is the size of the whole inventory. On failure: "Error: " (unknown enum value -> the accepted values are listed; 500 'NATS request failed' -> the platform could not parse the request).
Inputs: page, host_name, page_size, admin_state, +3 more. Returns: result
cnc_list_inventory_jobsList Inventory JobsARead-onlyIdempotent
List inventory jobs — the audit trail of every device, credential, provider and tag write on Crosswork (each write returns one job).
Read-only. Use it to review recent changes or to find the job_id of a write whose result was lost, then inspect one with cnc_get_inventory_job or wait for it with cnc_wait_for_inventory_job. Paged with the inventory filterData.PageSize/PageNum grammar (assumed for this endpoint; 'total' is null when the platform reports no result_count, and 'has_more' then means the page came back full).
Args: page_size / page: page size and 0-based page number.
Returns: str: Markdown listing, or JSON: {"total": int|null, "count": int, "page": int, "page_size": int, "items": [{"job_id": str, "state": str, "type": str, "creation_time": str, "completion_time": str, "created_by": str, "impacted": [str, ...], "error": str}, ...], "has_more": bool, "next_page": int|null, "collection_total": int|null} States: JOB_COMPLETED and JOB_COMPLETED_WITH_WARNING (a success with an advisory in "error", e.g. a no-op or partially applied write), JOB_FAILED / JOB_CANCELLED / JOB_ABORTED (unsuccessful), JOB_RUNNING and other in-progress states. On failure: "Error: ".
Inputs: page, page_size, response_format. Returns: result
cnc_list_providersList ProvidersARead-onlyIdempotent
List the providers configured in Crosswork (SR-PCE, NSO, WAE, ...).
Read-only. Providers are the external systems CNC integrates with; the SR-PCE provider is what feeds the L3/SR-TE topology (via BGP-LS), and the NSO provider is used for service provisioning. Use this to discover provider UUIDs and check their reachability before touching devices, topology, or services; use cnc_get_provider for the full record.
Filters AND together. Only name and family are supported
filters on this endpoint. Page with page/page_size (0-based).
Args:
name: exact/wildcard name filter (case-insensitive).
family: friendly family name (sr_pce, nso, wae, syslog_storage,
alert, proxy, onc, accedian_proxy) or wire value.
page_size, page: paging; has_more/next_page say whether to
fetch another page.
response_format: markdown (one line per provider: name, uuid,
family, reachability, endpoints, credential profile) or json.
Returns:
str: Markdown listing, or JSON:
{"total": int|null, "count": int, "page": int, "page_size": int,
"items": [{"uuid", "name", "family", "profile",
"reachability_state", "connectivity_info": [...],
"properties": {...}, ...}],
"has_more": bool, "next_page": int|null,
"collection_total": int|null, "offset": int, "next_offset": int|null}
total is the number of providers matching the filter (absent /
null when Crosswork omits it, which it does for zero matches);
collection_total is the size of the whole provider collection.
On failure: "Error: " (unknown family value ->
the list of accepted values; 500 "NATS request failed" -> malformed
request rather than an outage).
Inputs: name, page, family, page_size, +1 more. Returns: result
cnc_list_tagsList TagsARead-onlyIdempotent
List the tags defined on Crosswork (system tags such as 'mdt' plus any user-defined ones), with optional name/category filtering and paging.
Read-only. Use it to learn the exact tag names before filtering devices by tag or attaching tags to devices. The tool sends an empty body and applies the name/category filters and the paging itself after the fetch. Whether tags/query honours a filter body or pages server-side is not verified; if the response carries result_count/total_count larger than the rows returned, the collection was truncated by the server and the output says so. 'total' is the number of fetched tags that matched the filters; 'collection_total' is the number of tags on the platform (from the server's counts when present, else the number fetched).
Args: name: case-insensitive substring of the tag name. category: exact category (case-insensitive), e.g. 'default'. page_size / page: client-side paging over the filtered tags.
Returns: str: Markdown listing, or JSON: {"total": int, "count": int, "page": int, "page_size": int, "items": [{"name": str, "category": str, "created_by": str, "creation_time": str, "tag_type": str}, ...], "has_more": bool, "next_page": int|null, "collection_total": int} On failure: "Error: ".
Inputs: name, page, category, page_size, +1 more. Returns: result
cnc_list_topology_linksList Topology LinksARead-onlyIdempotent
List the links (edges) on the topology map with status and utilisation.
Read-only. Each link carries its type (display name, e.g. "L2 Ethernet" for LT_L2_ETHERNET, "L3 ISIS IPv4 L1", "L3 OSPF IPv4", "L3 BGP EPE IPv4"), status (Up/Degraded/Down), both endpoint node names/UUIDs and interface names, and per-direction utilisation labels such as "0.00015% (1.5Kbps/1Gbps)" with a HEALTHY/... severity. L2 links come from LLDP collection; L3 links need an SR-PCE provider (BGP-LS).
Paging is a row window: startRow = page * page_size,
endRow = startRow + page_size. Past the end the platform returns
totalCount with no rows (reported as an empty page, not an error).
No filtering is available.
Args: page_size: rows per page (1-500). page: 0-based page number. response_format: markdown (default) or json.
Returns:
str: Markdown "status linkType: srcNode:srcIf <-> dstNode:dstIf (util)"
per link, or JSON:
{"total": int, "count": int, "page": int, "page_size": int,
"items": [{"uuid": str, "name": str, "linkType": "L2 Ethernet", "status": "Up",
"sourceNode-name": str, "sourceNode-uuid": str,
"sourceConnector-name": str, "targetNode-name": str,
"targetNode-uuid": str, "targetConnector-name": str,
"targetConnector-uto-label": str,
"targetConnector-uto-severity": "HEALTHY", ...}],
"has_more": bool, "next_page": int|null, ...}
Each item is the element's uuid merged with its attributes.
On failure: "Error: ". An HTTP 500 "Internal Server
Error" from /v1/topology-service/... means the service rejected the
request body (viewId/params) — deterministic, do NOT retry.
(It is not the inventory's "NATS request failed" signal.)
Inputs: page, page_size, response_format. Returns: result
cnc_list_topology_nodesList Topology NodesARead-onlyIdempotent
List the nodes on the topology map as a sorted, paged table.
Read-only. Topology nodes are the devices CNC has placed on the map (fed by inventory + SR-PCE); their UUIDs match the inventory node UUIDs. Use this for per-node attributes (management IP, TE router-id, reachability, family); use cnc_get_topology for adjacency.
Paging is a row window: startRow = page * page_size,
endRow = startRow + page_size. Past the end the platform returns
totalCount with no rows (reported as an empty page, not an error).
No filtering is available on this endpoint — filter client-side or use
the inventory tools. The platform silently accepts an unknown sort
column (undefined order), so sort_by is validated here first.
Args: page_size: rows per page (1-500). page: 0-based page number. sort_by: column to sort on (see the parameter description). sort_ascending: sort direction. response_format: markdown (default) or json.
Returns:
str: Markdown listing, or JSON:
{"total": int, "count": int, "page": int, "page_size": int,
"items": [{"uuid": str, "name": str, "nodeIp": str, "teRouterId": str,
"reachabilityState": "CONN_STATE_REACHABLE"|..., "productType": str,
"deviceFamily": str, "lastUpdateTime": str, ...}],
"has_more": bool, "next_page": int|null, ...}
Each item is the element's uuid merged with its attributes.
On failure: "Error: " (unknown sort column ->
"Error: Unknown sort column ..."). An HTTP 500 "Internal Server
Error" from /v1/topology-service/... means the service rejected the
request body (viewId/params) — deterministic, do NOT retry.
(It is not the inventory's "NATS request failed" signal.)
Inputs: page, sort_by, page_size, sort_ascending, +1 more. Returns: result
cnc_list_usersList UsersARead-onlyIdempotent
List the Crosswork user accounts with their role, status and device access groups.
Read-only. Use it to confirm an account exists (Crosswork answers the same 'Invalid credentials' for an unknown username and a wrong password) and to see which role (PolicyId, e.g. 'admin') and device access groups (e.g. 'ALL-ACCESS') an account carries. The platform returns a dict keyed by username with PascalCase fields; this tool flattens it to a list and never returns the Password field.
Returns: str: Markdown listing, or JSON: {"count": int, "items": [{"username": str, "role": str, "first_name": str, "last_name": str, "status": str, "device_access_groups": [str, ...]}, ...]} On failure: "Error: " (403 -> the configured account lacks the user-administration privilege).
Inputs: response_format. Returns: result
cnc_wait_for_device_reachableWait for Device to Become ReachableARead-onlyIdempotent
Poll a device until its reachability_state is CONN_STATE_REACHABLE.
Read-only convergence wait. Call it right after cnc_create_device (or after fixing credentials / admin state) instead of polling cnc_get_device in a loop. Pass exactly one of uuid / host_name. A new device typically moves UNKNOWN/CHECKING -> REACHABLE within a minute or two once it is attached to a Data Gateway.
Returns: str: On success: "Device () is reachable after Ns." plus a JSON summary (reachability_state, operational_state, dg_name, errors). On timeout (NOT an error): "Not reachable yet after Ns; current reachability_state=..., operational_state=..." plus the same summary — call again to keep waiting, or inspect 'errors' / dg_name. "Error: ..." only for API failures or when no device matches.
Inputs: uuid, host_name, timeout_seconds, interval_seconds. Returns: result
cnc_wait_for_inventory_jobWait For Inventory JobARead-onlyIdempotent
Poll an inventory job until it reaches a terminal state or the timeout elapses.
Read-only. Use it right after a write that came back JOB_RUNNING instead of calling cnc_get_inventory_job in a loop. Terminal states are the verified ones: JOB_COMPLETED and JOB_COMPLETED_WITH_WARNING (both successes; the latter carries an advisory, e.g. a no-op or partially applied write), and JOB_FAILED / JOB_CANCELLED / JOB_ABORTED (failures). Any other state (JOB_RUNNING or an in-progress state not seen before) keeps the tool polling until the timeout.
Returns: str: "Inventory job completed after s." followed by the job JSON (with "impacted_objects" parsed from "impacted") on success; for JOB_COMPLETED_WITH_WARNING the line also carries "Warning: " and the JSON gains a "warning" key. A timeout is NOT an error: "Inventory job not finished after s; current state: JOB_RUNNING. ..." followed by the job JSON — call again to keep waiting. "Error: Inventory job failed (job , state JOB_FAILED): " when the job ended unsuccessfully; "Error: No inventory job with id ..." when the id matches nothing; other API failures: "Error: ...".
Inputs: job_id (required), timeout_seconds, interval_seconds. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 19 tools
Each tool targets a distinct resource and action, and descriptions explicitly partition the topology tools (cnc_get_topology for adjacency vs cnc_list_topology_nodes/cnc_list_topology_links for tabular attributes). The only mild overlap is between the general topology getter and the two specialized topology listers, but the descriptions give clear guidance on when to use each.
Every tool follows the same cnc_<verb>_<noun> convention (list_*, get_*, wait_for_*), with no camelCase or mixed-style deviations. The pattern is fully predictable across all 19 tools.
19 tools is on the heavier side but each covers a genuinely distinct domain (devices, credentials, providers, topology, tags, users, applications, alarms, jobs) with read variants plus helpful summary/wait helpers. It feels slightly large but well-scoped rather than padded.
Read coverage is broad and coherent: devices, credentials, providers, topology, tags, users, applications, alarms and inventory jobs all have list/get surfaces, plus convergence-wait helpers. The notable gap is the complete absence of write operations (create/update/delete) even though descriptions reference them, but the surface appears intentionally read-only.
Related MCP Connectors
Manage ConfigDirector feature flags, remote configs, and targeting rules from AI assistants.
Provides capabilities that let LLM agents perform a range of infrastructure management tasks.
Manage tickets, run agents and workflows, query tables, and search workspace knowledge.
Enables AI assistants to retrieve event data and execute event management operations in Nunify.
Related MCP Servers
- AlicenseBqualityDmaintenanceEnables AI-powered network automation through natural language interactions with Cisco NSO, providing access to device management, configuration retrieval, sync operations, and service orchestration via the RESTCONF API.928 PyPI4MIT
- AlicenseNot gradedqualityCmaintenanceEnables LLMs to interact with network devices via Cisco RADKit, supporting inventory discovery, device attribute inspection, CLI command execution, and SNMP queries.11Apache 2.0
- AlicenseNot gradedqualityDmaintenanceEnables AI agents to interact with the Netdisco network management platform through its complete REST API, supporting device inspection, port queries, VLAN searches, and job management.MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI agents to directly inspect, configure, test, and orchestrate network devices through pyATS/Genie, including show command parsing, config rollback, feature state learning/diffing, fleet-wide command fan-out, and declarative test execution.MIT