Skip to content

owroc/sonic-mcp

Repository files navigation

SONiC MCP Server — gNMI variant / pyGNMI

An MCP server that exposes a SONiC switch's gNMI interface as tools, so any MCP-capable LLM can read switch state and change configuration. It speaks gNMI natively in Python via pyGNMI — no external binary required — and serves MCP over Streamable HTTP, packaged as a portable Docker container you can run on any Docker host.

┌─────────────┐  Streamable HTTP   ┌──────────────────────┐   gNMI/gRPC   ┌──────────────┐
│ LLM client  │ ─────────────────► │  sonic-mcp container │ ────────────► │ SONiC switch │
│ (Claude,    │   POST /mcp        │  FastMCP + pyGNMI    │   :8080       │ gnmi/telemetry│
│  IDE, etc.) │ ◄───────────────── │                      │ ◄──────────── │  container    │
└─────────────┘                    └──────────┬───────────┘               └──────────────┘
                                       devices.yaml (bind-mounted)

Security posture is lab-grade by default. gNMI runs insecure (plaintext), MCP auth is off, and writes execute live. An LLM with a write-enabled connection to your switches can disrupt the network

Quick start (Docker)

cp devices.example.yaml devices.yaml      # then edit for your fabric
cp .env.example .env                       # set passwords referenced as ${VAR}
docker compose up --build

The MCP endpoint comes up at http://127.0.0.1:8000/mcp and a health probe at http://127.0.0.1:8000/health. The image carries no site data — devices.yaml is bind-mounted and everything else is an env var, so the same image runs in any environment.

Configure

devices.yaml is a validated list of switches. Credentials may be inlined (lab) or referenced from the environment as ${VAR} so secrets stay out of the file:

devices:
  - name: leaf-01
    host: 10.0.0.11
    port: 8080              # SONiC gnmi/telemetry default; build-specific
    insecure: true          # plaintext gRPC (no TLS)
    username: admin
    password: ${LEAF01_PASSWORD}
    tags: [lab, leaf]

  - name: spine-01
    host: 10.0.0.1
    read_only: true          # refuses config changes regardless of server setting
    tags: [prod, spine]

Every tool takes a device argument that must match a name here — an LLM never addresses a raw IP, so the read_only guard can't be sidestepped. Mark production switches read_only: true.

Set the gNMI port. It's build-specific — commonly 8080, but 50051 / 6030 / 57400 are seen. The probe (below) confirms it.

Probe first — this is the gate

SONiC's OpenConfig coverage varies by branch, Set support is a compile-time flag, and path semantics are discovered empirically. Before trusting any tool against real hardware, run the standalone probe (needs only pip install pygnmi pyyaml):

python scripts/probe.py --inventory devices.yaml
python scripts/probe.py --inventory devices.yaml --test-write   # LAB switches ONLY

It writes probe-results.json enumerating, per device: reachability, gNMI version, supported models/encodings, which candidate paths return data, whether Subscribe works, and (with --test-write) whether a reversible Set is accepted.

Local development (without Docker)

python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"                    # runtime + pytest/ruff

SONIC_MCP_INVENTORY_PATH=./devices.yaml python -m sonic_mcp        # HTTP on :8000
SONIC_MCP_TRANSPORT=stdio SONIC_MCP_INVENTORY_PATH=./devices.yaml python -m sonic_mcp

pytest            # 53 tests, no switch required
ruff check src tests

Connect an LLM

Model-agnostic — anything that speaks MCP works.

HTTP client / orchestrator / automation — point your MCP client at http://<host>:8000/mcp.

Claude Desktop (stdio) — edit ~/Library/Application Support/Claude/claude_desktop_config.json. Use the venv Python that has the deps, and an absolute inventory path (Claude Desktop launches with a minimal PATH):

{
  "mcpServers": {
    "sonic-gnmi": {
      "command": "/abs/path/.venv/bin/python",
      "args": ["-m", "sonic_mcp"],
      "env": {
        "SONIC_MCP_TRANSPORT": "stdio",
        "SONIC_MCP_INVENTORY_PATH": "/abs/path/devices.yaml"
      }
    }
  }
}

Tools (19)

Call list_devices first to learn the valid device names (credential-free).

Group Tools
Discovery list_devices, get_capabilities
Read get_interface_status, get_interface_counters, get_lldp_neighbors, get_bgp_neighbors, get_environment, get_device_identity, get_config
Telemetry subscribe_telemetry (bounded by sample count and wall-clock)
Write set_interface_admin, configure_interface_ip, create_vlan
Ops backup_config, diff_config, rollback_config, save_config
Escape hatches gnmi_get, gnmi_set

Safety model

Three independent rails stand between an LLM and a live switch:

  1. Read-only refusal. SONIC_MCP_READ_ONLY=true disables every write server-wide; any device flagged read_only refuses writes regardless. The guard trips before a SetRequest is built.
  2. Dry-run. Every write tool accepts dry_run; a dry run returns the exact SetRequest without touching the Set RPC. SONIC_MCP_DEFAULT_DRY_RUN=true makes preview the default.
  3. Prior-state + backup. The client captures the targeted subtree before each write (inline undo); with SONIC_MCP_AUTO_BACKUP=true a full-config snapshot is also written to the backup dir first.

Every mutation attempt — refused, dry-run, applied, or errored — is recorded to the audit log (SONIC_MCP_AUDIT_LOG_PATH for a durable JSON-lines trail).

gNMI notes & caveats

  • Paths are OpenConfig xpaths. List keys use [key=value]; the leading module: prefix is the gNMI origin. OpenConfig (interfaces, platform) is gNMI's sweet spot.
  • SONiC-native models may need an origin. get_device_identity (DEVICE_METADATA) and create_vlan (VLAN) use SONiC YANG paths whose resolution is build-specific. Validate with get_capabilities / gnmi_get.
  • No standard save. gNMI has no commit/save RPC and SONiC generally maps none — writes hit running config only. Persist with the CLI (config save), or set SONIC_MCP_SAVE_PATH if your build maps one.
  • Rollback is experimental. A get --type config snapshot is a list of notifications, not a set-ready tree, so replaying it as a Set replace may not round-trip. Prefer targeted gnmi_set reverts.

Environment variables

Every field in src/sonic_mcp/settings.py maps to a SONIC_MCP_-prefixed var; the full annotated list is in .env.example. The most-used:

Var Default Purpose
SONIC_MCP_INVENTORY_PATH /config/devices.yaml inventory file path
SONIC_MCP_TRANSPORT http http or stdio
SONIC_MCP_HOST / SONIC_MCP_PORT 0.0.0.0 / 8000 MCP bind address
SONIC_MCP_READ_ONLY false disable all write tools
SONIC_MCP_DEFAULT_DRY_RUN false make writes preview by default
SONIC_MCP_AUTO_BACKUP true snapshot running config before each write
SONIC_MCP_BACKUP_DIR /config/backups snapshot directory
SONIC_MCP_AUDIT_LOG_PATH (unset) JSON-lines change trail
SONIC_MCP_GNMI_TIMEOUT 15 per-RPC seconds
SONIC_MCP_ENCODING json_ietf gNMI value encoding
SONIC_MCP_SAVE_PATH (unset) gNMI Set path that persists running→startup
SONIC_MCP_AUTH_TOKEN (unset) require Authorization: Bearer <token>

Layout

src/sonic_mcp/       package (settings, inventory, gnmi, paths, safety, backup, context, server, tools/)
scripts/probe.py     standalone pre-flight probe (the Phase 2 gate)
tests/               pytest suite (fake gNMI; no switch needed)
Dockerfile           multi-stage, non-root
docker-compose.yml   reference deployment
legacy/              the original single-file server.py (superseded; kept for reference)

About

MCP server exposing SONiC switch management over gNMI (FastMCP + pyGNMI), packaged as a portable Docker container

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors