Usage stats: what's being called, and what never is
You could always rename a tool or rewrite its description, but there was no way to tell
whether the change helped, or whether anything had ever called that tool to begin with.
1.6.0 counts every call the data plane serves and reports it per server, per tool, and
across the whole instance. The number that matters most is usually zero. A tool sitting
at 0 / never is the one worth renaming.
Per-server, per-tool and instance-wide usage stats (#127, closes #126)
Counting happens where a call is actually served, so all three exposed surfaces land in
the same counters:
| Surface | Tool identified by |
|---|---|
/s/<slug>/mcp |
the JSON-RPC body (tools/call → params.name) |
POST /s/<slug>/rest/<tool> |
the path, and only when the bridge serving the request really exposes REST |
/g/<name>/mcp |
the hub's <slug>_<tool> namespace, credited to the member that owns the tool, and only to members the hub is actually serving |
One rulebook (backend/app/usage/attribution.py) decides what counts as a tool call, so
the three surfaces can't drift apart.
Non-tool traffic is counted separately instead of being thrown away. initialize,
tools/list and the SSE GET get their own bucket. That split is the useful part. A
server with connections but zero tool calls means clients are showing up and the model
is choosing not to call anything, which is exactly when a better name or description is
worth trying. A server with nothing at all is a different problem with a different fix.
Traffic that never reached a bridge is never counted. Unknown slug, refused auth,
nothing running, a POST off the group's /mcp endpoint, a REST path on a bridge with no
REST surface. That's the same rule idle bookkeeping already follows. The dashboard
playground doesn't count either, since the panels report what clients did, not what you
did while testing.
How it's stored
Counters accumulate in memory and a background task folds them into
(server, tool, UTC hour) buckets every few seconds. The data plane pays no database
write per request, and usage bookkeeping can never fail a request it only observes. A
hard crash loses at most one flush interval, which is acceptable for statistics and the
reason this is not an audit log.
A bucket holds a server id, a tool name, an hour, a count, and when that count was last
written. The "last call" the panels show is that flush time, not the exact moment of the
call. Arguments and results are never stored.
Tool names come from the client, so cardinality is capped at three layers: the parsed
body by size and element count, the stored name by length (applied after the group
namespace is stripped, so a long slug can't cost a real call its row), and the pending
map by a ceiling that refuses new keys while established ones keep counting. Past any of
those caps, calls pool into one (other tools) row rather than getting dropped, so the
counters stay bounded and a caller still can't make its traffic invisible. Tools a
running server exposes are exempt from the cap.
Retention is the new usage_retention_days setting, default 30, 0 to keep forever.
The same background task prunes hourly. A requested window is clamped to retention, so
asking for a year on 30-day retention returns 30 days instead of 335 days of zeroes that
would read as genuine quiet, and both dashboards say so when a range gets shortened.
Buckets are dropped with their server, and any orphan left behind by a delete/flush race
is swept. Reasoning in docs/adr/0003-usage-counters-at-the-serving-edge.md.
The screens
/usage is the instance-wide dashboard. Stat tiles for tool calls, other requests,
servers with traffic, and tools called. Calls over time as bars, an area line, or split
by server as small multiples on one shared scale. An activity grid bucketing weekday by
hour into your own timezone, with the daily axis labels left in UTC because a daily
bucket spans a UTC day rather than sitting on your timeline. Breakdown tabs for tools
and servers, with search, four sorts, a used-only filter, and either a table or
proportional bars. Sorting by least calls is the fast way to the tool worth renaming.
Every row links to its server.
Each server's detail page carries the same panel scoped to that one server, so a
never-called tool sits at 0 / never right beside the rename editor. Rows key off the
name the bridge is currently serving, so a rename staged in the editor above doesn't
relabel history before you hit Apply.
Charts are LayerChart, the Svelte 5 charting library that
shadcn-svelte's own charts are built on, so scales, axes, tick formatting, the hover
tooltip and resize come from the ecosystem instead of hand-rolled geometry. It reads its
colours from CSS variables, so app.css points those at this app's tokens and the charts
inherit the existing zinc-and-emerald design rather than importing a second one. The
activity punchcard and the proportional row bars stay plain DOM, where a charting library
would add nothing.
API and settings
GET /api/usage?days=7
GET /api/servers/<id>/usage?days=7
Both go through the same visibility policy as every other route, so a member's totals sum
over the servers they own and never the whole box. Both re-authorize after the pre-read
flush and are served no-store. Instance rollups are SQL GROUP BY aggregates rather
than a Python fold over raw buckets, and both endpoints share one window helper, so "the
last 7 days" can't mean two different things on two pages.
Retention lives in Settings → Security → Usage retention.
Versioned GitHub footer (#128)
The running version moved out of the health indicator and into a footer next to a GitHub
link. The footer version refreshes off the health poll, so an SPA tab left open across an
upgrade picks up the new number as soon as the control plane comes back, instead of
showing the version it loaded with.
Connecting from a stdio-only MCP client
The README now covers clients that can only launch a local command, using
mcp-remote as the bridge to a /s/<slug>/mcp or
/g/<name>/mcp URL. It includes the bearer-token form that keeps the space-containing
header value in env to avoid argument-quoting bugs in Windows clients, and the
standalone mcp-remote-client command for testing an endpoint with the host client out
of the way.
Upgrade notes
- No migration step.
usage_bucketis a new table, created on startup by the
existingcreate_allpath. Nothing to run by hand. - No new environment variables. Retention is a runtime setting in the UI, default 30
days. - Counting starts at upgrade. There is no history to backfill, so the panels read
empty until traffic arrives. A tool showing0 / neveron day one doesn't mean
anything yet. - No bridge restarts. Usage is not part of the launch spec or
config_hash, so
nothing bounces on upgrade.
Full Changelog: v1.5.2...v1.6.0
License
MIT © pacnpal