Capturing
A DevTools Network tab for your local Ollama.
It watches the HTTP traffic between your tools and a local Ollama and presents it like a request inspector: models, endpoints, status codes, tokens per second, latency, and full bodies. Then it hands that same captured data to any MCP client, read-only, so an LLM can answer questions about your own inference activity. It observes by packet inspection — your apps talk to Ollama exactly as they did before.

Most local-LLM observability asks you to change something first: point at a different port, wrap the client, install a middleware. That buys visibility only for the code you control, and only after you have modified it. This one reads the traffic itself, so an editor extension, a closed-source client and your own script all show up the same way, with no cooperation from any of them.
Capture is packet inspection through WinDivert, at the network layer, below the application that made the request. The app reassembles the TCP stream, parses the HTTP exchange, and derives its metrics from the response it actually saw — token counts and tokens per second are read out of the payload.
No library is added to your project and no import is rewritten, so there is nothing to install into a tool you do not own. An editor extension or a closed-source client is observed on exactly the same terms as your own code.
Capture reads a copy off the wire rather than sitting between your tool and Ollama. There is no extra hop on the request path to add latency, and no second process whose failure could take an inference down with it.
Your client keeps pointing at 127.0.0.1:11434. Open the app and traffic appears; close it and your setup is exactly as you left it, because no configuration anywhere had to learn that the inspector exists.
What you inspect is what Ollama received. The request object your code built travels untouched, and the payload on screen is the one that crossed the wire — down to the streamed chunks, reassembled in order.


Inference dashboards are easy to fake. Latency can be timed from the client side, throughput extrapolated from a partial stream, token counts estimated from character length — and the result looks identical to a measurement. This one derives its figures from the response it captured, which is why you can compare two runs and trust that the difference is real.


One request, thirty-one seconds apart, and the clearest way to see the rule at work. A dashboard willing to guess would have filled those four fields immediately with a running average and a plausible rate. This one waits until it has something to read, so a number appearing on screen tells you the measurement exists.
| Field | Where the value comes from |
|---|---|
| Request latency | Measured across the captured exchange |
| Token counts | Read out of the response body |
| Tokens per second | Derived from those two, never estimated |
| HTTP status code | Observed on the wire |
| Request and response bodies | Stored verbatim, up to 16 MiB |
| Streaming chunks | Reassembled in order — 1017 of them in the capture above |
| Loaded models | Confirmed against the Ollama API |
An MCP server that returns request bodies in its discovery response burns the context window before the model has asked a question. So the contract is staged: the first call describes what exists, the second returns summaries light enough to page through, and only the third — once the model has picked a single stable id — is allowed to return a body, and even then only the byte range it asked for.
resolve_inference_contextOrientation. What models, endpoints and statuses exist at all, and over what time range.
Inputs
Outputs
Cheap because: Aggregates only. It never includes a body, a header or any per-inference detail.
search_inferencesNarrowing. Page through candidates under filters combined with AND.
Inputs
Outputs
Cheap because: Summaries carry stable fields only; heavy headers and bodies are omitted by construction.
get_inference_contextDetail, bounded. One known id, only the sections asked for, and a body read in slices.
Inputs
Outputs
Cheap because: The client sets the byte window. A large body is read across several calls instead of arriving whole and unasked.
Page order is at DESC, id DESC — the id tie-breaker matters because several inferences can share a timestamp, and without it a page boundary could drift between two identical queries. The opaque cursor stores the timestamp, the id and the active filters, and reusing a cursor under different filters is rejected rather than silently answered, because answering it would break the stability the cursor exists to provide.
{
"mcpServers": {
"dllm-network": {
"command": "C:\\path\\to\\dllm-network-mcp.exe"
}
}
}The sidecar takes no flags — it resolves the database location itself. Register it by absolute path and restart the client.
The GUI and the MCP sidecar are not two modules of one program. They are two separate OS processes with separate lifetimes: you start the GUI, and your MCP client starts the sidecar whenever it feels like it. An in-memory store cannot be shared across that gap, so the storage medium is chosen by the inter-process problem — not by any wish to keep a long history.
The file both processes open
%LOCALAPPDATA%\dllm-network\telemetry.dbdllm-networkDSN options
Opened once per session through sqlite.Open. It is the only connection allowed to write, and writes arrive batched from a separate drain goroutine so the capture loop never waits on disk.
dllm-network-mcpDSN options
A standalone stdio binary with no flags, launched by your MCP client. It resolves the database path through the same shared resolver the GUI uses, so the two can never disagree about where the file is.
Anything you connect to the sidecar — Claude Desktop, Claude Code, your own client — reaches your telemetry through a connection SQLite itself refuses to accept a write statement on. query_only(true) applies as a pragma at the connection level, so the guarantee holds regardless of what the caller asks for, and it is locked in by a regression test rather than by convention. Point an LLM at your inference history knowing the worst it can do is read it.
These hold because the linter stops a build that would break them, so each one is a property of the shipped binary rather than a promise about how the code is maintained. The rule that enforces it is named on every card.
The read side depends on a reader port and has no route into the write-side capture pipeline at all. That makes "read-only" a shape of the program rather than a convention someone has to keep remembering, which is what makes the sidecar safe to hand to an LLM.
mcp-not-captureinternal/mcp/**The MCP SDK exists in exactly one place, so a breaking change upstream has one package to land in. Every other package builds and tests without the SDK present at all.
sdk-confined-to-mcpeverywhere except internal/mcp/**The inference type stays free of any storage driver, so a test can exercise the domain with nothing running behind it. How an inference is stored is entirely the sqlite package’s business.
inference-domain-purityinternal/telemetry/inference/**The same type is kept free of the MCP SDK, so what a protocol wants an inference to look like never reaches the model. Adding an HTTP transport beside the stdio one touches no domain code.
inference-domain-purityinternal/telemetry/inference/**The document explains why each line exists and the linter decides whether it held, and the repo states the tie-break plainly: if the two disagree, the linter wins and the doc is wrong.
Two artefacts come out of one repository: the tray app that captures, and the stdio binary that serves what it captured. Build the sidecar once, register it by absolute path, and make sure the GUI has run at least once — the sidecar only ever reads the database it finds.
$ go build -o dllm-network-mcp.exe ./cmd/dllm-network-mcpThe tray app, the MCP sidecar and the capture pipeline. Apache 2.0.
The data flow, the WAL seam and the four enforced boundaries, in the repo’s own words.
Build, register and use the MCP server; the three-tool reference and troubleshooting.
Confirmed-versus-inferred semantics and exactly which fields passive mode cannot give you.