Skip to main content
Glama
fabric-testbed

mflib Slice Metrics MCP Server

Official

mflib Slice Metrics MCP Server

Python 3.11+ License: MIT MCP

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


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 --build

No 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 20

Look for:

Grafana OK: http://fabric_prometheus-grafana:3000 is readable
Datasource discovered: 'Prometheus' (uid=...)
Starting mflib-mcp on http://0.0.0.0:8000/mcp

The 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/mcp

406 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

mflib_cpu

8

utilization, load average, core count

mflib_memory

12

RAM/swap used, cache, paging, page faults, OOM kills

mflib_memory_internals

13

slab, vmalloc, hugepages, writeback, NFS

mflib_disk

11

throughput, IOPS, latency, queue depth

mflib_filesystem

8

free space, inodes, read-only / error state

mflib_network

19

bandwidth, packets, errors, link state, ARP, conntrack

mflib_sockets

5

TCP/UDP socket counts

mflib_netstat

11

connections, retransmits, ICMP, TCP errors

mflib_system

16

uptime, processes, PIDs/threads vs limits, interrupts, systemd

mflib_time

4

clock sync / NTP drift

mflib_hardware

3

temperature, cooling devices, power supplies

mflib_exporter

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

memorynot memory_internals, despite the name

slab, vmalloc, hugepages

memory_internals

"is a disk filling up"

filesystem variant space_used_pct

"how busy is the disk"

disk variant io_util; "how slow" → wait_time

process counts, PID/thread limits

system

"is monitoring working"

exporter — this is scrape health, not node health

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 (node1Node1).

  • 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. A percent panel is already a percentage.

  • A range series carries values, not timestamped pairs: values[i] is the sample at window.start + i * window.step, and null is a gap — not zero.

  • truncated: true means series were dropped (>60). downsampled: true means the step widened to fit the response budget; the window is unchanged.

  • Zero series is a real answer, not a reason to invent one.

  • job defaults to node and should stay there. The measurement node also scrapes docker, ping and prometheus, 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_CONTAINER

fabric_prometheus-grafana

Grafana's container name — usually the only value that differs between deployments

GRAFANA_PORT

3000

Grafana's port inside the network

GRAFANA_BASE_URL

composed from the two above

Full-URL override; needed only when Grafana is not reachable as container:port (e.g. through an SSH tunnel)

GRAFANA_DS_UID

empty — discovered at startup

Prometheus datasource UID. Leave empty: mflib's Grafana provisioning gives the datasource no fixed uid:, so it is regenerated whenever Grafana restarts. Set it only on a Grafana whose provisioning pins one

GRAFANA_TOKEN

empty

Only if this Grafana disables anonymous reads

GRAFANA_VERIFY_TLS

1

0 only for a self-signed Grafana over a tunnel

MCP_PORT

8000

Host-side port, published on loopback

MCP_PATH

/mcp

URL path the endpoint is served at. Must match what the proxy forwards: use / if its location /mcp strips the prefix (the way mflib's location /grafana/ does), /mcp if it passes the prefix through

MONITORING_NETWORK

fabric_prometheus

The compose network Grafana runs on

MFLIB_TOOL_PREFIX

mflib_

Prefix on tool names; "" for bare names

MFLIB_VERBOSE_TOOLS

1

0 for terse tool descriptions — smaller surfaces reduce malformed tool calls on small models

LOG_LEVEL / LOG_FORMAT

INFO / text

json for structured logs

ACCESS_LOG_USER_HEADER

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 credentials

They 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/mcp

It 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 stack

Troubleshooting

Symptom

Cause

Startup says Grafana NOT reachable

GRAFANA_BASE_URL is wrong, or this container is not on Grafana's compose network. Check docker network ls and MONITORING_NETWORK

Every tool returns empty

Prometheus is not scraping the slice, or GRAFANA_DS_UID was pinned to a stale value. Check mflib_list_nodes() first — empty there means the datasource, not the tools. Leaving GRAFANA_DS_UID empty lets the server discover and re-resolve it

Every request 404s through the proxy

MCP_PATH does not match what the proxy forwards. A location /mcp that rewrites the prefix away delivers /; set MCP_PATH=/

list_nodes returns nothing

Prometheus has no node_uname_info; node_exporter is not running on the slice nodes

A node name returns no data

ambiguous or unknown, so it was passed through unchanged. Call mflib_list_nodes() for the exact spelling

mflib_hardware returns no series

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 node variable became multi upstream

curl hangs against /mcp

-I/HEAD is unanswered. Use a GET with --max-time

Tools missing in a client

the name is too long once the client's mcp__<server>__ prefix is added. Shorten the server name

400 Request Header Or Cookie Too Large

the proxy's header buffers are too small for the portal's token. On nginx: large_client_header_buffers 4 32k

docker compose logs --tail 40                 # startup probe + per-request log
docker compose exec mflib-mcp env | grep GRAFANA

License

MIT

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Exposes 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.
    -
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables querying Prometheus metrics, performing cluster diagnostics, and analyzing resource usage through natural language, with automatic Kubernetes connectivity.
    MIT
  • F
    license
    Not graded
    quality
    B
    maintenance
    Enables 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.
    -