Skip to main content
Glama

cnc-mcp

CI License: MIT

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

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-mcp

uvx 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-mcp

The 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-mcp

with /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:latest

The 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

cnc_list_devices · cnc_get_device · cnc_get_device_collection_summary · cnc_wait_for_device_reachable

Credential profiles

cnc_list_credential_profiles · cnc_get_credential_profile

Providers (SR-PCE, NSO, …)

cnc_list_providers · cnc_get_provider

Topology (RESTCONF NBI)

cnc_get_topology_summary · cnc_list_topology_nodes · cnc_get_topology_node · cnc_list_node_interfaces · cnc_get_node_interface · cnc_list_topology_links · cnc_get_topology_link · cnc_list_srv6_locators

TE state (SR-PCE feed)

cnc_get_te_summary · cnc_list_sr_policies · cnc_get_sr_policy · cnc_list_p2mp_policies · cnc_get_p2mp_policy · cnc_list_rsvp_te_tunnels · cnc_get_rsvp_te_tunnel · cnc_get_link_performance_metrics · cnc_get_sr_policy_performance_metrics · cnc_get_rsvp_tunnel_performance_metrics

SR-TE operations (Optimization Engine)

cnc_list_sr_policies_on_nodes · cnc_list_sr_policies_on_interface · cnc_get_sr_policy_routes · cnc_get_sr_policy_metrics · cnc_preview_sr_policy_route · cnc_dryrun_sr_policy · cnc_get_sr_policy_path_notification_state · cnc_wait_for_sr_policy_oper_state

Platform

cnc_list_tags · cnc_list_users · cnc_list_applications · cnc_list_alarms · cnc_list_inventory_jobs · cnc_get_inventory_job · cnc_wait_for_inventory_job

Data Gateway

cnc_list_data_gateways · cnc_get_data_gateway · cnc_list_data_gateway_pools · cnc_get_data_gateway_load_metrics · cnc_list_data_gateway_outages · cnc_get_data_gateway_health · cnc_get_data_gateway_global_parameters · cnc_list_data_destinations · cnc_list_data_gateway_files

NSO

cnc_is_nso_configured · cnc_get_nso_policy · cnc_list_nso_devices · cnc_get_nso_device · cnc_check_device_nso_state · cnc_check_nso_device_sync · cnc_get_nso_device_config · cnc_wait_for_device_nso_state

Inventory extras

cnc_get_device_summary · cnc_get_inventory_config · cnc_get_collection_cadence · cnc_get_device_tags

Fault

cnc_get_alarm · cnc_search_alarms · cnc_list_events · cnc_list_device_alarms · cnc_get_alarm_settings · cnc_get_alarm_manager_settings · cnc_list_event_types · cnc_get_event_type_recommendation · cnc_list_alarm_suppression_policies

Device configuration

cnc_get_device_config_preferences · cnc_list_device_backups · cnc_get_device_backup · cnc_list_config_backup_jobs · cnc_get_config_backup_job · cnc_list_config_templates · cnc_get_config_template · cnc_list_template_deployments · cnc_get_template_deployment · cnc_wait_for_config_backup_job · cnc_wait_for_template_deployment

EMF inventory

cnc_list_ems_nodes · cnc_get_ems_node · cnc_list_ems_interfaces · cnc_get_ems_interface · cnc_get_ems_inventory_summary

Platform admin & RBAC

cnc_get_platform_version · cnc_get_cluster_health · cnc_list_cluster_nodes · cnc_get_cluster_node · cnc_list_microservices · cnc_list_application_status · cnc_list_app_manager_jobs · cnc_list_app_manager_events · cnc_get_maintenance_status · cnc_list_certificates · cnc_check_certificate_expiry · cnc_get_login_banner · cnc_get_session_config · cnc_list_active_sessions · cnc_get_user · cnc_list_roles · cnc_get_role_tasks · cnc_get_role_permissions · cnc_get_password_policy · cnc_list_secured_apis · cnc_check_permissions

Notifications

cnc_list_notification_streams · cnc_list_notification_subscriptions · cnc_get_notification_subscription · cnc_list_kafka_subscriptions

Collection service

cnc_get_collection_job_count · cnc_get_collection_job_summary · cnc_get_collection_job_state · cnc_list_export_collection_jobs · cnc_list_sensor_templates · cnc_get_collection_health

Device groups

cnc_list_group_rule_conditions · cnc_list_root_groups · cnc_get_group_hierarchy · cnc_get_group_details · cnc_list_group_devices · cnc_list_group_rules · cnc_list_group_ports

LCM & Circuit-Style (Optimization Engine)

cnc_list_lcm_domains · cnc_get_lcm_config · cnc_list_lcm_managed_interfaces · cnc_get_lcm_recommendation · cnc_get_lcm_recommendation_preview · cnc_list_csm_bandwidth_pools · cnc_list_cs_policy_paths · cnc_list_cs_policies_on_nodes · cnc_list_cs_policies_on_interface

Services (CAT inventory, T-SDN)

cnc_list_service_types · cnc_get_service_counts · cnc_list_services · cnc_get_service · cnc_get_service_plan · cnc_wait_for_service_plan · cnc_list_vpn_services · cnc_get_vpn_service · cnc_get_vpn_service_health · cnc_get_vpn_underlay_transport · cnc_list_sub_services · cnc_find_services_on_transport · cnc_list_function_packs

Performance monitoring (PM policies, dashboards, NPM)

cnc_list_performance_policies · cnc_get_performance_policy · cnc_get_performance_policy_history · cnc_list_performance_policy_devices · cnc_list_performance_policy_templates · cnc_get_performance_retention · cnc_get_performance_health_settings · cnc_get_performance_statistics · cnc_get_performance_top_n · cnc_list_performance_top_n_columns · cnc_get_performance_summary · cnc_get_lsp_utilization · cnc_get_lsp_delay · cnc_get_interface_delay · cnc_get_srv6_locator_statistics

OAM & probes

cnc_get_oam_settings · cnc_list_oam_trace_routes · cnc_get_oam_trace_route · cnc_wait_for_oam_trace_route · cnc_get_probe_status

SWIM & ZTP

cnc_get_swim_preferences · cnc_list_software_images · cnc_get_device_running_images · cnc_get_swim_job · cnc_list_ztp_profiles · cnc_list_ztp_devices · cnc_list_ztp_serial_numbers · cnc_list_ztp_static_routes · cnc_get_ztp_device_policy · cnc_list_ztp_config_files · cnc_list_ztp_images

EMS inventory scheduler

cnc_list_inventory_scheduler_jobs · cnc_get_inventory_scheduler_job · cnc_wait_for_inventory_scheduler_job

Playbooks (one call, composed from the tools above)

cnc_investigate_device · cnc_network_health_report · cnc_explain_sr_policy · cnc_alarm_triage · cnc_explain_service · cnc_srv6_readiness

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

cnc_create_device · cnc_update_device · cnc_delete_device · cnc_enable_device_gnmi

Credential profiles

cnc_create_credential_profile · cnc_update_credential_profile · cnc_delete_credential_profile

Providers

cnc_create_provider · cnc_update_provider · cnc_delete_provider

Data Gateway

cnc_map_devices_to_data_gateway

NSO

cnc_nso_device_action (check-sync / sync-from / connect / compare-config …) · cnc_nso_sync_to_device · cnc_sync_inventory_with_nso

SR-TE operations

cnc_create_sr_policy · cnc_update_sr_policy · cnc_delete_sr_policy · cnc_set_sr_policy_path_notifications

Platform admin

cnc_set_login_banner · cnc_set_maintenance_mode · cnc_restart_microservice

Inventory extras

cnc_create_tag · cnc_delete_tag · cnc_assign_tags · cnc_unassign_tags · cnc_set_device_location · cnc_clear_device_location · cnc_lock_device · cnc_unlock_device

Fault

cnc_acknowledge_alarm · cnc_annotate_alarm · cnc_clear_alarm · cnc_create_alarm_suppression_policy · cnc_update_alarm_suppression_policy · cnc_delete_alarm_suppression_policy · cnc_set_event_type_severity · cnc_set_event_type_autoclear · cnc_revert_event_type_autoclear · cnc_set_event_type_recommendation · cnc_update_alarm_manager_settings · cnc_update_gnmi_alarm_settings

Device configuration

cnc_backup_device_config · cnc_delete_config_backup_job · cnc_delete_device_backup · cnc_create_config_template · cnc_delete_config_template · cnc_deploy_config_template · cnc_delete_template_deployment

Notifications

cnc_create_webhook_subscription · cnc_delete_notification_subscription · cnc_create_external_subscription (Kafka / gRPC) · cnc_delete_external_subscription · cnc_clear_notification_subscriptions_by_topic

Device groups

cnc_create_device_group · cnc_update_device_group · cnc_delete_device_group · cnc_set_device_group_members · cnc_move_group_members

LCM

cnc_pause_lcm_recommendations

Service provisioning (NSO proxy, T-SDN CFPs)

cnc_create_odn_template · cnc_delete_odn_template · cnc_create_sr_policy_service · cnc_update_sr_policy_service · cnc_delete_sr_policy_service · cnc_create_sid_list · cnc_delete_sid_list · cnc_create_l3vpn_service · cnc_delete_vpn_service · cnc_provision_service · cnc_delete_service · cnc_resync_service_inventory — the three creates take srv6_locator for SRv6 transport (see SRv6)

Performance monitoring (PM policies, retention)

cnc_create_performance_policy · cnc_update_performance_policy · cnc_activate_performance_policy · cnc_deactivate_performance_policy · cnc_delete_performance_policy · cnc_update_performance_retention · cnc_reset_performance_retention

OAM & probes

cnc_start_oam_trace_route · cnc_reactivate_probe

SWIM & ZTP (the ZTP catalogue)

cnc_upload_ztp_config_file · cnc_update_ztp_config_file · cnc_delete_ztp_config_file · cnc_create_ztp_profile · cnc_update_ztp_profile · cnc_delete_ztp_profile · cnc_add_ztp_serial_numbers · cnc_delete_ztp_serial_numbers · cnc_create_ztp_static_route · cnc_delete_ztp_static_route · cnc_create_ztp_device · cnc_update_ztp_device · cnc_delete_ztp_device

EMS inventory scheduler

cnc_run_inventory_scheduler_job · cnc_suspend_inventory_scheduler_job · cnc_resume_inventory_scheduler_job

Playbooks

cnc_provision_l3vpn_e2e (dry-run → commit → plan → CAT status → OAM trace; srv6_locator for an SRv6 VPN, which skips the MPLS-only trace) · cnc_create_sr_policy_e2e (dry-run → create → wait UP → routes) — both take dry_run=true to stop after the preview

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} where total counts matches for the filter and collection_total the 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 as Error: … with the platform's reason, never as a silent success.

  • Ordering: credential profile → provider → device. A device's te_router_id must 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 Devices for 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.py

Authentication. 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.)

  1. Writes are off by default. CNC_MCP_ENABLE_WRITES=true registers the 98 write tools; without it the server is read-only, and the connect-time instructions say so.

  2. 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 in src/cnc_mcp/tools/; the ones with write tools are devices, 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_jobs and composite. Read tools are never affected. The write playbooks in composite need the sibling that commits for them: cnc_provision_l3vpn_e2e needs service_provisioning (and oam for its optional trace step), cnc_create_sr_policy_e2e needs sr_te_operations — CNC_MCP_WRITE_AREAS=composite on its own registers neither, and the startup log says which sibling each one lacks.

  3. 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).

  4. CNC_MCP_DRY_RUN=true — the write tools stay registered but nothing changes on the platform. A write tool that takes dry_run runs with it forced to true and answers the preview (the device CLI NSO would push, the path the PCE would compute); every other write tool is not executed and answers NOT EXECUTED with the arguments it would have sent (secret-looking values redacted); the two write playbooks stop after their preview stage with a dry-run verdict. 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

CNC_MCP_BASE_URL

(required)

CNC UI/API URL with scheme, e.g. https://host:30603

CNC_MCP_USERNAME / CNC_MCP_PASSWORD

—

Crosswork user; CAS SSO → JWT, refreshed automatically

CNC_MCP_API_TOKEN

—

Alternative: a pre-issued JWT (cannot be refreshed; expires in ~8 h)

CNC_MCP_VERIFY_TLS

true

false for self-signed lab certificates

CNC_MCP_ENABLE_WRITES

false

Write tools are not registered until true

CNC_MCP_WRITE_AREAS

(all)

Comma-separated areas whose write tools are registered when writes are on, e.g. fault,service_provisioning

CNC_MCP_DISABLED_TOOLS

—

Comma-separated tool names never registered, read or write

CNC_MCP_DRY_RUN

false

Write tools registered but not executed: forced preview where the tool has dry_run, recorded otherwise

CNC_MCP_TIMEOUT_SECONDS

30

Per-request read timeout, seconds (>= 1)

CNC_MCP_CONNECT_TIMEOUT_SECONDS

10

TCP connect timeout, seconds (>= 1)

CNC_MCP_MAX_RETRIES

3

Retries for 429 / 5xx / transport errors (idempotent calls; 0-10)

CNC_MCP_RETRY_BACKOFF_SECONDS

1.0

Base delay for the exponential backoff between retries, seconds (>= 0)

CNC_MCP_MAX_CONCURRENT_REQUESTS

5

Cap on in-flight requests to the platform

CNC_MCP_MAX_RESPONSE_CHARS

40000

Tool responses longer than this are truncated with a note

CNC_MCP_LOG_LEVEL

INFO

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:

  1. 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 with respx; CI runs them on Python 3.11 and 3.12.

  2. 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 creates smoke-* 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.json is a sanitised copy of the plan used.

  3. 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.py doubles as a manual test client.

  4. 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 srv6locator series — then the dry-run-only provisioning steps committed and reverted

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-mocked

Development

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-mcp

To 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 tools
cnc_get_credential_profileGet Credential ProfileA
Read-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 DetailsA
Read-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 SummaryA
Read-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 JobA
Read-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 DetailsA
Read-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 GraphA
Read-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 SummaryA
Read-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 AlarmsA
Read-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 ApplicationsA
Read-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 ProfilesA
Read-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 DevicesA
Read-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 JobsA
Read-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 ProvidersA
Read-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 TagsA
Read-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_nodesList Topology NodesA
Read-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 UsersA
Read-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 ReachableA
Read-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 JobA
Read-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

    A4.3/5.0

    Scored across 19 tools

    Disambiguation4/5

    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.

    Naming Consistency5/5

    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.

    Tool Count4/5

    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.

    Completeness4/5

    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

    Related MCP Servers

    • A
      license
      B
      quality
      D
      maintenance
      Enables 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.
      9
      28 PyPI
      4
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables 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
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables 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