fcli
What an SR Linux fabric looks like, what is wrong with it, and what changed, straight from gNMI.
This is a ground-up rewrite of fcli. It runs on asyncio throughout, uses no Nornir and makes no blocking gNMI calls.
uv tool install 'fcling[server,mcp,ai]' # the `fcling` command, from PyPI
docker run --rm --network host -v "$PWD:/work" ghcr.io/wdesmedt/fcling -t lab.clab.yml bgp-peers
Or from a checkout:
uv sync
uv run fcling -t lab.clab.yml bgp-peers
uv run fcling -t lab.clab.yml -i role=leaf -o json subif -f oper=down
uv run fcling -c inventory.yml sys-info
uv run fcling -t lab.clab.yml ipv4-rib -a 10.1.3.99 # longest-prefix match
uv run fcling -t lab.clab.yml bgp-rib -r evpn -t 5 -d # with the detail columns
uv run fcling -t lab.clab.yml ifstats -s 2 -f oper=up # rates over 2 s
With the engine running, the same commands answer instantly from what it streams, and can look back:
uv sync --extra server
uv run fcling -t lab.clab.yml serve # streams every node, keeps history
export FCLI_SERVER=http://127.0.0.1:8765
uv run fcling bgp-peers # live, from the engine
uv run fcling bgp-peers --at 14:02 # as it was then
uv run fcling history --since 1h -r bgp-peers --follow # what changed, which commit did it, and keeps watching
What is wrong, and what changed:
uv run fcling checks # every finding, check by check
uv run fcling incidents -v # grouped by root cause, worst first
uv run fcling incidents --at 14:02 # what was wrong then
uv run fcling baseline pre-change -m "before the MTU change"
uv run fcling drift pre-change # what came, went and changed since
uv run fcling config-diff leaf1 12 # what commit 12 did to leaf1's configuration, side by side
Ask, or let your own AI client ask. Both get the same 14 read-only tools: status, reports, incidents, checks, history, configuration diffs, drift, baselines, the lenses and the topology.
uv sync --extra server --extra mcp --extra ai
uv run fcling ask "what is wrong, and what changed just before?" # the first provider with a key
uv run fcling ask --provider openai --effort low "is BGP healthy?"
claude mcp add --transport http fcli http://127.0.0.1:8765/mcp # MCP over HTTP from `fcling serve`
claude mcp add fcli -- fcling --server http://127.0.0.1:8765 mcp # or over stdio
Questions across the whole fabric, and its shape:
uv run fcling -t lab.clab.yml where 10.1.3.13 # every place that knows an IP or MAC
uv run fcling -t lab.clab.yml path leaf1 10.1.3.13 --ni ipvrf-1 # hop by hop, over ECMP and VXLAN
uv run fcling -t lab.clab.yml service macvrf-102 # one service on every node, and what disagrees
uv run fcling -t lab.clab.yml endpoints # every attached host
uv run fcling -t lab.clab.yml topology # tiers, cables, clients, segments
checks and incidents work without a server too: one-shot from the nodes, or from the local history with --at. They exit 1 when there is an error, so they can gate a pipeline.
Reports: sys-info, config-commits, lldp, subif, lag, ifstats, ni, bgp-peers, bgp-rib, ipv4-rib, ipv6-rib, tunnel-table, static-routes, routing-pol, mac, arp, nd, irb, es, es-dest, vxlan, bfd, isis, ospf, resources, components, transceivers, services, bridge-domains, routers. Each is a pure function from gNMI paths to typed records.
Documentation
| Installation and inventory | installing, containerlab and static inventories, selecting nodes, TLS |
| Reports | every report, filters, output formats |
| The web UI | topology, incidents, history, lenses, reports and the agent in a browser |
| The engine | fcling serve: how it streams, the HTTP API and its events, security |
| History | what is kept, --at, commit attribution, baselines and drift |
| Lenses and topology | where, path, service, endpoints, topology, adding a lens |
| Checks and incidents | every check, and how findings are grouped by root cause |
| MCP and the agent | the tools, MCP setup, fcling ask, Claude, OpenAI and Grok |
| Architecture | how it fits together, adding a report or a check, the tests |
Architecture
inventory ──► collect ──► gNMI Get (one per datatype, all nodes at once)
│
▼
Tree (per node) ◄── later: gNMI Subscribe, same tree
│
▼
report: pure function Tree → typed records
│
▼
render: table · json · yaml · csv
| Module | Role |
|---|---|
| gnmi/client.py | grpc.aio client for Capabilities, Get and Subscribe. TLS can be verified, trust-on-first-use (skip_verify) or plaintext. Errors are worded around the node. |
| gnmi/path.py | Paths as hashable values, parsed once. Module prefixes are dropped. |
| tree.py | A node's state as plain JSON. Get payloads and subscription leaves merge into it, with list entries matched by key. |
| report.py | @report(name, reads=[Read(path, datatype, mode)], record=..., columns=...). A report declares its paths and never does I/O. |
follow |
Optional pure function (tree, params) -> reads for dependent reads: the next-hop-groups the routes name, the BGP attribute sets of the paths shown. The collector calls it again until it asks for nothing new. It fetches by key, or the whole table above a limit. |
Param |
An argument that shapes what a report builds (ipv4-rib -a <address>, bgp-rib --paths all). The CLI turns each one into an option. |
| reports/ | The report definitions. Records are flat, frozen pydantic models: one record is one row. |
| collect.py | One-shot engine. Each node is one task, and a node fails on its own. A path a node does not know reads as empty. |
| inventory.py | Loads a containerlab topology or a static YAML inventory, with key=glob filters. |
| engine.py | Long-running engine. ON_CHANGE reads are streamed in Subscribe RPCs of at most 36 paths, the most SR Linux accepts, each with a heartbeat. SAMPLE reads (counters) are polled, and each polled subtree is replaced whole: a SAMPLE stream never says what is gone. follow reads are fetched by key. A reconnect resyncs into a fresh tree. An update only re-runs the reports whose paths it overlaps. |
| delta.py | Changes record by record, matched on each record's key. A change confined to volatile fields (counters, rates) is pushed live but not kept. |
| store.py | SQLite: an append-only change log plus the current state. A restart diffs against it, and the state at any instant is a fold back from now. |
| checks.py | 19 fabric checks: pure functions of a Fabric (every node's records) to Finding records. In the engine, findings are a report like any other, so each one raised or cleared is history. |
| incidents.py | Findings anchored to what they are about (a cable, a session, a node, the underlay between two nodes) and grouped by root cause. The same cause in many places folds into one pattern. |
| views.py | One place for fabric-wide answers from any source (the nodes now, the engine, the history at an instant): checks, incidents, drift, and history attributed to the configuration commit that preceded it. |
| service.py | The questions every surface asks, behind one interface with three implementations: the engine in this process, a running server over HTTP, or the nodes one-shot. |
| tools.py | 14 read-only tools over a Service. Inputs are pydantic models that are validated before anything runs; outputs are compact JSON with row limits. One definition serves both MCP clients and the agent. |
| mcp.py | The tools as an MCP server: streamable HTTP at /mcp on fcling serve (stateless, with DNS-rebinding protection on localhost), or stdio via fcling mcp. |
| agent.py | The in-app agent, provider-neutral: streaming, concurrent tool calls, and append-only conversations kept server-side in the provider's own format. |
| llm.py | The providers. Claude (Anthropic SDK): adaptive thinking, cached prompt and tools, server-side refusal fallback, claude-opus-5-5 at medium. OpenAI (Responses API: store off, reasoning replayed encrypted), gpt-5.6-sol. OPENAI_API=chat switches to Chat Completions for proxies. Grok (Chat Completions at api.x.ai), grok-4.6. The provider is FCLI_LLM_PROVIDER or the first one with a key, Claude first; model and effort come from FCLI_MODEL / FCLI_EFFORT or each provider's own variables. |
| server.py | Starlette: /api/status, /api/reports[/{name}] (live, ?at= or collected on the spot), /api/history, /api/events (SSE, resumable with Last-Event-ID). Bound to localhost; a bearer token when it isn't. |
Because a report only declares paths, the same definition can be fed by a one-shot Get (CLI), by a STREAM subscription (server) or by a recording (tests). Its subscription plan is static data that can be checked without a device.
Static inventory
defaults:
username: admin
password: NokiaSrl1!
tls: {ca_file: ca.pem} # or {skip_verify: true} / {insecure: true}
nodes:
leaf1: {address: 10.0.0.11, labels: {role: leaf}}
spine1: {address: 10.0.0.21, labels: {role: spine}}
Tests
make test lint
tests/fixtures/releases holds real gNMI recordings from the original fcli for SR Linux 25.3 to 26.7. test_release_matrix.py replays them through the new reports and requires row-for-row equality with what the original rendered. fake_gnmi.py is an in-process gNMI target, so the client, TLS, the collector and the CLI are tested without a lab.
test_engine.py and test_server.py stream from that same fake target. They cover the initial sync, refused paths, live changes and deletes, history and state_at, and SSE replay over a real uvicorn.
The gNMI stubs are generated from the openconfig protos (make proto) and committed.
Metadata
Release files for fcling 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| fcling-0.4.0.tar.gz | 2.7 MB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fcling-0.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.0 MB
Release files / fcling-0.4.0.tar.gz
| Download URL | fcling-0.4.0.tar.gz |
|---|---|
| Size | 2.7 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
468d8fac8e025856adfc94ed6d95bdff4511d44b50058be331d37a86c3fbe498
|
|
BLAKE2b-256 checksum How to use checksums |
0d8f6c074a7efabe4b50fd7472e056b0d0149e1ed85778d766782736968c9eaa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logRelease files / fcling-0.4.0-py3-none-any.whl
| Download URL | fcling-0.4.0-py3-none-any.whl |
|---|---|
| Size | 289.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5eff7ace916b4403150539d6dbc4b3d340efbc9276409567e5067e5425be88c2
|
|
BLAKE2b-256 checksum How to use checksums |
8de49694f26123d6085354bc44baa955ca51284404517709c338321717f10395
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logSecurity for fcling 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for fcling-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 16, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.