mflib Slice Metrics MCP Server
OfficialConnects to Grafana to discover the Prometheus datasource and uses Grafana's datasource proxy to run the fixed PromQL queries for FABRIC slice node metrics.
Queries Prometheus metrics using fixed PromQL queries from the Node Exporter Full dashboard, covering CPU, memory, disk, filesystem, network, system, and other FABRIC slice node measurements.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mflib Slice Metrics MCP ServerWhich node is using the most memory?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mflib Slice Metrics MCP Server
A Model Context Protocol (MCP) server that lets LLM clients ask natural-language questions about the nodes in a FABRIC slice — answered by the fixed PromQL queries of the Node Exporter Full dashboard already running on the slice's mflib measurement node.
Key features
Grounded by construction — every tool runs a fixed PromQL query from the dashboard. The model never writes PromQL; it picks a tool, a variant, and parameters.
15 tools — 12 metric topics covering all 112 panels, plus 3 discovery tools.
Runs where the data is — one container on the measurement node, beside Prometheus and Grafana.
git clone,docker compose up.No auth of its own — it listens on loopback and the mflib portal's proxy authenticates callers, so there is no second token to issue or rotate.
Handles arbitrary node names — slices are built by people, not a naming scheme. Names resolve case-insensitively, unique prefixes work, and
list_nodes()exists because guessing is not possible.Correct multi-node comparison — most dashboard panels are written for one node, so querying several runs the panel once per node rather than averaging them into a meaningless number.
Table of contents
Getting started | Reference | Operations |
Related MCP server: Prometheus MCP Server
Quick start
Prerequisites: an mflib measurement node with Prometheus and Grafana running, Docker, and the Compose v2 plugin.
git clone https://github.com/fabric-testbed/mflib_portal_mcp.git
cd mflib_portal_mcp
docker compose up -d --buildNo configuration step: .env ships with the standard measurement-node values, and
the Prometheus datasource is discovered from Grafana at startup. .env.example
documents every setting if you need to change one.
docker compose logs --tail 20Look for:
Grafana OK: http://fabric_prometheus-grafana:3000 is readable
Datasource discovered: 'Prometheus' (uid=...)
Starting mflib-mcp on http://0.0.0.0:8000/mcpThe container publishes on 127.0.0.1:8000 only. Point the portal's proxy at that port.
Confirm it is serving:
curl -s -o /dev/null -w '%{http_code}\n' --max-time 10 http://127.0.0.1:8000/mcp406 is success — the MCP endpoint refuses a plain GET. Do not use curl -I; the endpoint does not answer HEAD and the request will appear to hang.
How it works
"Which node is using the most memory?"
│
▼ LLM picks tool + variant + params
mflib_memory(variant="ram_used", node=["Node1","Node2","Node3"])
│
▼ loopback, behind the portal's proxy
┌──────────────────────────────────────────────┐
│ mflib-mcp │
│ ├─ resolve names against live label values │
│ ├─ substitute into the panel's FIXED PromQL│
│ └─ run once per node (panels aggregate) │
└──────────────────────────────────────────────┘
│
▼
Grafana ──▶ datasource proxy ──▶ Prometheus (both on the fabric_prometheus network)
│
▼
→ Node2, 14.4 % (grounded, real data)The dashboard's panels, queries, units and legends are extracted once and ship inside the package, so the running server needs no dashboard API and cannot drift mid-flight.
Tools reference
Every topic tool takes variant (which measurement), node, and an optional time window. Call mflib_list_topics() to route a question.
Tool | Variants | Use for |
| 8 | utilization, load average, core count |
| 12 | RAM/swap used, cache, paging, page faults, OOM kills |
| 13 | slab, vmalloc, hugepages, writeback, NFS |
| 11 | throughput, IOPS, latency, queue depth |
| 8 | free space, inodes, read-only / error state |
| 19 | bandwidth, packets, errors, link state, ARP, conntrack |
| 5 | TCP/UDP socket counts |
| 11 | connections, retransmits, ICMP, TCP errors |
| 16 | uptime, processes, PIDs/threads vs limits, interrupts, systemd |
| 4 | clock sync / NTP drift |
| 3 | temperature, cooling devices, power supplies |
| 2 | node_exporter's own scrape health |
Discovery: mflib_list_nodes(), mflib_list_topics(), mflib_list_param_options().
variant is an argument, never a tool name — there is no ram_used tool; it is mflib_memory(variant="ram_used", …).
Routing hints:
Question | Tool and variant |
OOM kills, page faults |
|
slab, vmalloc, hugepages |
|
"is a disk filling up" |
|
"how busy is the disk" |
|
process counts, PID/thread limits |
|
"is monitoring working" |
|
These are virtual machines, so mflib_hardware usually returns no series: VMs expose no physical sensors. That is a real answer, not a failure.
Node names
The single thing that makes this server different from a fixed-fleet one.
Nodes belong to one slice, and whoever built it chose the names — Node1, web-server, alice-vm-3. There is no scheme to infer them from.
mflib_list_nodes()
# {"count": 3, "nodes": ["Node1", "Node2", "Node3"]}Matching is case-insensitive, and a unique prefix resolves (
node1→Node1).An ambiguous prefix is left alone rather than resolved to whichever node sorts first — answering about the wrong machine is worse than answering about none.
An unknown name returns no data, which is not the same as the node being idle.
Pass a list to compare machines:
node=["Node1","Node2"].
That last point matters more than it looks. Most panels on this dashboard are written for a single node — CPU Busy is count()/avg() with no by (instance) — so interpolating a regex that matches three nodes averages them into one number. In testing that produced −198.998 %. The server therefore runs the panel once per node and labels each series.
Reading results
Each result carries panel, unit, description, resolved_queries, params_used, and series. Each series has a name from the dashboard's legend.
Honour
unit. Apercentpanel is already a percentage.A range series carries
values, not timestamped pairs:values[i]is the sample atwindow.start + i * window.step, andnullis a gap — not zero.truncated: truemeans series were dropped (>60).downsampled: truemeans the step widened to fit the response budget; the window is unchanged.Zero series is a real answer, not a reason to invent one.
jobdefaults tonodeand should stay there. The measurement node also scrapesdocker,pingandprometheus, which export entirely different metrics.
MCP client configuration
Clients reach this server through the mflib portal's proxy, which supplies the bearer token. The portal's own instructions are authoritative; the shape is:
{
"mcpServers": {
"mflib": {
"type": "http",
"url": "https://<portal-host>/<slice>/mcp",
"headers": { "Authorization": "Bearer <token from the portal>" }
}
}
}Keep the server name short (mflib is ideal). The model sees mcp__<server-name>__<tool>, and OpenAI-compatible stacks reject function names over 64 characters — a long name silently drops the longest tools, starting with the discovery ones.
Configuration
All settings are environment variables, read once at startup. .env ships with
working defaults; .env.example documents each one in full.
Var | Default | Purpose |
|
| Grafana's container name — usually the only value that differs between deployments |
|
| Grafana's port inside the network |
| composed from the two above | Full-URL override; needed only when Grafana is not reachable as |
| empty — discovered at startup | Prometheus datasource UID. Leave empty: mflib's Grafana provisioning gives the datasource no fixed |
| empty | Only if this Grafana disables anonymous reads |
|
|
|
|
| Host-side port, published on loopback |
|
| URL path the endpoint is served at. Must match what the proxy forwards: use |
|
| The compose network Grafana runs on |
|
| Prefix on tool names; |
|
|
|
|
|
|
| empty | Header the proxy sets to identify callers, logged if named |
On the bind address. This server performs no authentication: the proxy does, then forwards to loopback. The container binds 0.0.0.0 internally, so the loopback guarantee is the port publish in docker-compose.yml (127.0.0.1:8000:8000). Changing that to 0.0.0.0 exposes the slice's metrics to anything that can reach the VM.
Rebuilding the catalog
The shipped catalog is a snapshot of the dashboard. Rebuild it if the dashboard changes:
python step1_fetch_dashboard.py --url http://localhost:3000/grafana # -> catalog/
python step2_annotate.py # -> data/annotated/
python step3_group.py # -> data/groups/step 1 pulls panels, queries, units, legends and template variables over Grafana's HTTP API.
step 2 holds the authored descriptions. These are the source of truth for routing — they are what the model reads to choose a tool.
step 3 assigns every panel to exactly one
(topic, variant)and fails on a duplicate or an omission, so a new panel cannot be silently unreachable.
Testing
pip install -e '.[test]'
pytest # 416 offline tests; no network, no credentialsThey cover the layers where a wrong answer is easiest to hide: PromQL substitution, the parameter-injection guard, node-name resolution (including what it refuses to guess), the per-node fan-out, required-parameter rules, time parsing, request timeouts, catalog loading, datasource-UID handling, and the endpoint's mount path.
End-to-end, against a real measurement node. The published image already carries every dependency, so this needs nothing installed on the node:
docker run --rm --network fabric_prometheus -v "$PWD:/src" -w /src \
mflib-mcp:latest python test_server.py --http http://fabric_prometheus-mflib_mcp:8000/mcpIt performs a real MCP handshake, lists the tools, then runs live queries — node
names, CPU, RAM, filesystem, network, uptime and a 30-minute range query. Add -v
to print the values rather than series counts. It exits non-zero on failure.
Or directly, with the package installed:
python test_server.py # stdio
python test_server.py --http http://127.0.0.1:8000/mcp
python test_server.py --eval --limit 20 # drive an LLM over test questions--eval needs an OpenAI-compatible endpoint (FABRIC_LLM_BASE_URL, FABRIC_LLM_API_KEY, FABRIC_LLM_MODEL) and writes report.html grading which tool and variant the model chose.
Repo layout
mflib_mcp/
__main__.py # entry point: transport, middleware, prompt registration
config.py # all configuration, read once from the environment
registry.py # topic -> variant -> panel, loaded from the shipped catalog
query.py # time parsing, substitution, fan-out, response budget
tools.py # one tool per topic + discovery tools
session.py # how this server reaches Grafana
system.md # served as the `mflib-system` MCP prompt
middleware/ # access log
data/annotated/ # panels: fixed PromQL, units, legends, descriptions
data/groups/ # the topic -> variant taxonomy
step1_fetch_dashboard.py # rebuild: pull the dashboard over Grafana's API
step2_annotate.py # rebuild: authored descriptions
step3_group.py # rebuild: topic/variant taxonomy
docker-compose.yml # one container, loopback-published
test_server.py # end-to-end smoke test + LLM evaluation harness
tests/ # offline unit tests
.env # standard measurement-node values; no setup step needed
.env.example # every setting, documented
integration/ # notes for merging into an existing measurement-node stackTroubleshooting
Symptom | Cause |
Startup says |
|
Every tool returns empty | Prometheus is not scraping the slice, or |
Every request 404s through the proxy |
|
| Prometheus has no |
A node name returns no data | ambiguous or unknown, so it was passed through unchanged. Call |
| expected on VMs — no physical sensors |
Comparing nodes gives one odd number | a panel that aggregates was given a regex. The fan-out prevents this; if you see it, the |
|
|
Tools missing in a client | the name is too long once the client's |
| the proxy's header buffers are too small for the portal's token. On nginx: |
docker compose logs --tail 40 # startup probe + per-request log
docker compose exec mflib-mcp env | grep GRAFANALicense
This server cannot be deployed
Maintenance
Related MCP Connectors
Query OneLens cloud-cost data in natural language: breakdowns, trends, cost centers. Read-only.
Query Honeycomb observability data: traces, events, metrics, SLOs, triggers, and boards.
List datasets, schemas, run APL queries, and use prompts for exploration, anomalies, and monitoring.
Query and audit AppSheet apps in natural language via Knotrik's pre-scanned definitions.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceExposes Kubernetes cluster state with specialized telecom awareness of 5G Core network functions and topologies to MCP-compatible LLMs. It enables natural language analysis of 5G workloads, network slices, UPF data planes, and cluster health.-
- AlicenseNot gradedqualityCmaintenanceEnables querying Prometheus metrics, performing cluster diagnostics, and analyzing resource usage through natural language, with automatic Kubernetes connectivity.MIT
- AlicenseNot gradedqualityCmaintenanceEnables AI clients to query SD-WAN fabric health, devices, tunnels, BFD sessions, OMP peers, alarms, policies, and configuration state via natural language, with deterministic correlation and diagnostics for incident assessment.Apache 2.0
- FlicenseNot gradedqualityBmaintenanceEnables MCP clients to ask plain-language questions and receive answers grounded only in documents the configured role is cleared to read, with the same access-controlled tools available across any client.-