Skip to main content

fcli

ci PyPI

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

Ask, or let your own AI client ask. Both get the same 8 read-only tools: status, reports, incidents, checks, history, drift and baselines.

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 8 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.

Roadmap

  1. Inventory, async gNMI client, node tree, recorded fixtures
  2. Declared-path reports with typed records, and a one-shot CLI. 27 reports are ported, and every one with recorded data matches the original row for row. services, bridge-domains, routers and endpoints move to phase 6: the old getters built display strings and web-UI tiles, and these should be typed records over ni, irb, es, mac and arp/nd.
  3. Long-running engine: subscriptions planned from the declared paths, record-level changes in SQLite, HTTP/SSE, plus fcling serve, --server, --at and history. On both labs the engine's live records match a one-shot run for every report.
  4. Checks, incident correlation, timeline and baselines. Findings live in the engine and are kept as history. Incidents carry "since". --at works for findings and incidents, baselines and drift work through the store, and history is attributed to commits. On both labs the engine's findings match a one-shot run.
  5. MCP served from the engine, and the in-app agent on the same tools: /mcp, fcling mcp, /api/chat and fcling ask. All of them use the same tools over one Service, with Claude, OpenAI or Grok as the model.
  6. Lenses and topology: where, path, service, endpoints and topology, from the nodes, the engine or the history. They are served over /api/lenses and /api/topology, and five more tools use them. services, bridge-domains and routers are typed reports. endpoints became a lens, because placing a host needs every node's tables.
  7. Web UI, served by fcling serve with no build step: a navigation panel; topology, incidents and history; the lenses, with path and where drawn on the topology; every report as a live table; and the agent beside any view. All of it works live or at an instant.

Metadata

Release files for fcling 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fcling 0.3.0
File Size Uploaded Provenance
fcling-0.3.0.tar.gz 2.4 MB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for fcling 0.3.0
File Interpreter ABI Platform
fcling-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 2.6 MB

Release files / fcling-0.3.0.tar.gz

Download URL fcling-0.3.0.tar.gz
Size 2.4 MB
Tags Source
SHA-256 checksum
How to use checksums
e174f70df70cb11ad21ef6d90460749ed240aa7225934f4801697fd5b18463e8
BLAKE2b-256 checksum
How to use checksums
701883b4ea87964edc2c113d2def50840ee326a7a3f389aa844c47174aec2e79
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 13, 2026.

Transparency log

Release files / fcling-0.3.0-py3-none-any.whl

Download URL fcling-0.3.0-py3-none-any.whl
Size 261.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ec41c46d01c65d429179f986f38ebff48a5c1f2863585b4faf7124708318f9d3
BLAKE2b-256 checksum
How to use checksums
1d6ac913ce3fa6bf16e13c9fa57a41fbe509fb053a6715ae2e6a82e11b2e3979
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 13, 2026.

Transparency log

Security for fcling 0.3.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.

Select from 2 files

Attestation for fcling-0.3.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 13, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page