nornir-napalm-mcp

AI asistanlarına Nornir + NAPALM ağ cihazı durumunu sunan FastMCP sunucusu

Dokümantasyon

Nornir-NAPALM MCP Server

Python 3.12+ Nornir 3.4+ NAPALM 5.0+ Netmiko 4.7+ License: MIT Code style: ruff

Nornir-NAPALM-Netmiko FastMCP Server

A FastMCP server that exposes live network device state to AI assistants via NAPALM getters and netmiko CLI commands. Nornir handles inventory loading and concurrent device connections; NAPALM drives the read/getter tools and netmiko drives the CLI read + write-path tools.

Reads are free; writes are gated. nornir_apply_config (dry-run by default) and nornir_save_config are the only tools that touch device state, and every write is policy-screened, pre-change-backed up (fail-closed), transcript-parsed, and audit-logged. Every response is a structured envelope with an explicit success flag (spec §21).

Developers and AI agents: architecture, invariants, testing anchors, and verification gates live in CLAUDE.md. This README is the user-facing reference: features, setup, usage, and the dev workflow.


Features

ToolDescription
nornir_list_inventoryList all devices with hostname, platform, and group membership
nornir_get_factsSystem facts: vendor, model, OS version, serial number
nornir_run_getterRun any NAPALM getter by name (arp_table, bgp_neighbors, vlans, etc.)
nornir_get_configRetrieve running and/or startup configuration from a device
nornir_list_gettersIntrospect available NAPALM getters for each platform in the inventory
nornir_reload_inventoryRe-read YAML inventory from disk
nornir_run_commandRun one read-only netmiko CLI command (READ_ONLY/SAFE_OPERATIONAL only, per device)
nornir_run_commandsRun a batch of read-only netmiko CLI commands; rejected commands fail only themselves
nornir_backup_configCapture and store the running config as an immutable backup (NAPALM, falling back to netmiko)
nornir_list_backupsList stored backups for a device, oldest first
nornir_apply_configPlan (dry-run by default) and apply config lines via netmiko; pre-change backups are mandatory and fail-closed
nornir_save_configPersist running config to startup/NVRAM via netmiko — explicit-only, never called implicitly by apply (spec §11)
nornir_pingServer-originated ICMP ping from the MCP host to selected inventory devices
nornir_ssh_checkServer-originated TCP reachability check against inventory hosts, default port 22
  • Writes are gated. nornir_apply_config and nornir_save_config are the only write tools. Apply dry-runs by default, rejects DANGEROUS/BLOCKED lines per device, always captures a pre-change backup (fail-closed: a failed backup means the device is never touched), and reports transcript errors honestly. Saving to NVRAM is a separate, explicit, audited step.
  • Lazy initialization — server starts even with a broken inventory, exposing the tool catalogue for inspection.
  • Singleton caching — Nornir instance is initialized once and reused across requests. Failed-device quarantine (failed_hosts) is reset before every call so dropped devices are available again on the next request.
  • Connectivity diagnosticsnornir_ping checks ICMP reachability from the MCP server host, and nornir_ssh_check checks TCP reachability (default port 22) from the MCP server host. Neither tool logs into devices, executes CLI commands, or uses Netmiko; both use the Nornir inventory only as an address book for target selection.
  • Flexible filtering — filter by device name, group, or platform on any tool.
  • HTTP and STDIO transport — run locally for Claude Desktop or expose over HTTP.

Safety model

Every CLI command routed through netmiko is classified into one of six categories per platform (ios / eos rulesets — anything else defaults to UNKNOWN and is denied):

CategoryRead tools (nornir_run_command*)Apply (nornir_apply_config)
READ_ONLY (show …)✅ allowedallowed
SAFE_OPERATIONAL (ping, traceroute)✅ allowedallowed
CONFIGURATION (interface, ip route, …)❌ rejected✅ allowed
UNKNOWN❌ rejected (deny by default)✅ allowed (fails on-device if bad)
DANGEROUS (reload)❌ rejected❌ rejected
BLOCKED (write erase, wr e, …)❌ rejected❌ rejected
  • Abbreviated forms (wr e, conf t, rel) are expanded before classification — abbreviated and full forms behave identically.
  • Newline/control-character injection is structurally impossible: multi-line input is rejected before any device is touched.
  • Every change gets an immutable pre-change backup (0600 perms, sha256 sidecar) — if the backup fails, the device is never touched. Backups are the rollback substrate for a future rollback tool.
  • Applied configs are transcript-parsed heuristically; a detected device error is never reported as success, and device_state stays honestly "unknown" (no read-back in v1).
  • All writes are appended to an audit log with change ids and sha256 hashes — never config text.

Prerequisites

RequirementVersionNotes
Python3.12+Required for type hint syntax and pathlib improvements
uvlatestRecommended package manager (install)
NAPALM-supported devicesVendor-specificSSH, eAPI, or NETCONF access to target devices

Setup

Nornir configuration

The server requires a Nornir configuration file, provided via the NORNIR_CONFIG environment variable.

Configuration Setup

  • Copy the included example config to the project root (or any path you prefer):
cp config.example.yaml config.yaml
  • Edit config.yaml to point at your inventory files. A minimal config looks like:
---
inventory:
  plugin: SimpleInventory
  options:
    host_file: "inventory/hosts.yaml"
    group_file: "inventory/groups.yaml"
    defaults_file: "inventory/defaults.yaml"

runner:
  plugin: threaded
  options:
    num_workers: 10

logging:
  enabled: false

Note: The inventory files referenced must exist relative to this config file.


Quickstart: run from GitHub with uvx

No clone or virtualenv needed — uvx downloads the package from GitHub and runs it on the spot. Point NORNIR_CONFIG at your config (above) and:

# STDIO transport (default)
NORNIR_CONFIG=/path/to/config.yaml uvx --from "git+https://github.com/sydasif/nornir-napalm-mcp" nornir-mcp

# HTTP transport
NORNIR_CONFIG=/path/to/config.yaml uvx --from "git+https://github.com/sydasif/nornir-napalm-mcp" nornir-mcp --transport http --host 0.0.0.0 --port 8000

For a persistent install, use uv tool install once, then run nornir-mcp like any other command:

uv tool install --from "git+https://github.com/sydasif/nornir-napalm-mcp" nornir-mcp
NORNIR_CONFIG=/path/to/config.yaml nornir-mcp

To pin a specific revision, append @<ref> (a tag or commit hash) to the URL, e.g. git+https://github.com/sydasif/nornir-napalm-mcp@<commit-sha>.


MCP client configuration

Register this server with any MCP client (Claude Desktop, VS Code, etc.) by adding the following to your project's .mcp.json:

uvx from GitHub (recommended)

{
  "mcpServers": {
    "nornir": {
      "command": "uvx",
      "args": [
        "--from",
        "git+https://github.com/sydasif/nornir-napalm-mcp",
        "nornir-mcp"
      ],
      "env": {
        "NORNIR_CONFIG": "/absolute/path/to/config.yaml"
      }
    }
  }
}

Environment variables

VariableDefaultDescription
NORNIR_CONFIG— (required)Path to the Nornir bootstrap config
NORNIR_MCP_BACKUP_DIR./backupsRoot directory for immutable backups
NORNIR_MCP_AUDIT_DIR./auditRoot directory for the append-only audit log
NORNIR_MCP_MAX_OUTPUT_BYTES65536Per-output truncation budget (spec §21.1)

NAPALM getters

Use nornir_run_getter with any of these:

GetterDescription
arp_tableARP table
bgp_configBGP running configuration
bgp_neighborsBGP neighbors summary
bgp_neighbors_detailBGP neighbors detailed
configRunning/startup/candidate configuration
factsSystem facts (vendor, model, OS, serial, uptime)
interfacesInterface status and details
interfaces_ipIP addresses on interfaces
lldp_neighborsLLDP neighbors summary
lldp_neighbors_detailLLDP neighbors detailed
mac_address_tableMAC address table
ntp_serversNTP server configuration
snmp_informationSNMP configuration
vlansVLAN information

Usage

CLI help

uv run nornir-mcp --help

Note: there is no CLI flag for listing inventory — use the nornir_list_inventory MCP tool instead.

The commands below assume a local checkout (uv run nornir-mcp). Installed from GitHub, replace uv run nornir-mcp with the uvx --from "git+https://github.com/sydasif/nornir-napalm-mcp" form from the Quickstart, or run plain nornir-mcp after uv tool install.

Run as MCP server (STDIO)

NORNIR_CONFIG=/path/to/config.yaml uv run nornir-mcp

Run as HTTP server

NORNIR_CONFIG=/path/to/config.yaml uv run nornir-mcp --transport http --host 0.0.0.0 --port 8000

Run as Python module

NORNIR_CONFIG=/path/to/config.yaml uv run python -m nornir_mcp

Project Structure

nornir-mcp/
├── nornir_mcp/
│   ├── __init__.py       # Package version
│   ├── __main__.py       # python -m nornir_mcp support
│   ├── server.py         # Composition root: FastMCP + tool registration (thin)
│   ├── cli/
│   │   └── main.py       # CLI entry point (argparse, transport selection)
│   ├── core/             # Shared kernel — stateless services + CoreBase
│   │   ├── base.py       # CoreBase: envelope/selection plumbing shared by all tools
│   │   ├── envelope.py   # ToolEnvelope / HostOutcome / StructuredError + truncation
│   │   ├── errors.py     # Categorized exceptions (McpError, ErrorType) with retryable policy
│   │   ├── policy.py     # Command canonicalization + classification (READ/SAFE/CONFIG/DANGEROUS/BLOCKED/UNKNOWN)
│   │   ├── capability.py # Platform capability gate (netmiko device-type mapping)
│   │   ├── storage.py    # Immutable backup storage (0600 files, sha256 sidecars, traversal-safe)
│   │   ├── audit.py      # Append-only JSONL audit logger (hashes only, never config text)
│   │   ├── runner.py     # Nornir init, config loading, singleton caching, execution lock, NornirLike protocol
│   │   └── tasks.py      # Task helpers: device filtering, execution, outcome normalization
│   ├── tools/
│   │   ├── base/         # NornirBase: engine-agnostic server tools (inventory, reload, backups, connectivity)
│   │   │   ├── tool.py   # NornirBase(CoreBase) — 6 server tools + InventoryDevice model
│   │   │   ├── capture.py # Config-capture service (NAPALM preferred, netmiko fallback)
│   │   │   └── connectivity.py # Server-originated ICMP/TCP diagnostics (validate_probe_target, build_ping_command, icmp_ping_host, check_tcp_host)
│   │   ├── napalm/       # NapalmTool: NAPALM-family tools
│   │   │   ├── tool.py   # NapalmTool(NornirBase) — 4 NAPALM tools
│   │   │   └── introspection.py # NAPALM getter discovery per platform (GetterInfo)
│   │   └── netmiko/      # NetmikoTool: CLI read + write-path tools
│   │       ├── tool.py   # NetmikoTool(NornirBase) — 4 CLI tools + gating
│   │       └── changes.py # Write-path orchestration: plan, fail-closed backups, dry-run, transcript parse
│   └── py.typed          # PEP 561 marker for downstream type checking
├── tests/
│   ├── conftest.py       # Fake Nornir stubs, fake netmiko tasks, pytest fixtures
│   ├── core/             # Kernel unit tests (envelope, errors, policy, capability, storage, audit, tasks, runner, locking)
│   ├── tools/
│   │   ├── base/test_tool.py       # NornirBase tools (inventory, reload, backups)
│   │   ├── base/test_connectivity.py # nornir_ping / nornir_ssh_check helpers + tool tests
│   │   ├── napalm/                 # NapalmTool tools + getter introspection
│   │   └── netmiko/               # NetmikoTool tools + change planning / transcript parsing
│   ├── test_e2e.py       # Full-stack tests through the MCP protocol layer + 14-tool surface pin
│   └── test_cli.py       # CLI entry points
├── config.example.yaml   # Example Nornir configuration
├── pyproject.toml        # Build config, dependencies, and tool settings
├── uv.lock               # Locked dependencies
└── README.md

Contributing

Development workflow

# Install dependencies
uv sync

# Add a dependency (or dev dependency)
uv add <package>
uv add --dev <package>

# Update the lockfile
uv lock
# Run tests
uv run pytest

# Run tests with coverage
uv run pytest --cov=nornir_mcp --cov-branch

# Lint
uv run ruff check .
uv run ruff check --fix .

# Format
uv run ruff format .
uv run ruff format --check .

# Type check (strict mode)
uv run mypy .

# Dead code scan
uv run vulture nornir_mcp tests --min-confidence 80
# Local dev (MCP Inspector)
fastmcp dev nornir_mcp/server.py

# Claude Desktop install
fastmcp install nornir_mcp/server.py

# Run STDIO transport
nornir-mcp --transport stdio

# Run HTTP transport
nornir-mcp --transport http --host 0.0.0.0 --port 8000

Code standards

  • Python 3.12+ — use modern syntax (f-strings, match, str.removeprefix)
  • Type hints — required on all function signatures (mypy --strict)
  • Docstrings — Google-style with Args:, Returns:, Raises:
  • Tests — AAA pattern, one assertion per logical check, use pytest fixtures
  • Lintingruff with E, F, I, UP rules

Commit conventions

Use Conventional Commits:

feat(server): add nornir_ping tool
fix(runner): handle missing NORNIR_CONFIG gracefully
refactor: extract _run_nornir_task helper
test: add runner config expansion tests

Companion Lab

Test against real devices using the netlab-demo test lab with Cisco devices via Containerlab.


License

MIT