Provider-neutral usage infrastructure for MCP 2026-07-28 servers.
mcp-usage-kit is an Apache-2.0 Tower layer for Rust MCP servers. It records
terminal delivery instead of counting HTTP requests, so multi-round-trip calls
bill once, task polling does not multiply an invoice, errors are free, and
discovery traffic remains free.
cargo add mcp-usage-kit
| Documentation | Landing page | Conformance | Changelog | Security policy |
mcp-usage-core - pure protocol classification, pricing, terminal-delivery,
task-attribution, quota, and spend-cap decisions.mcp-usage-export - synchronous usage recording, concurrent aggregation,
idempotent retry, a log exporter, and a provider-neutral meter event exporter.mcp-usage-tower - API-key authentication, mandatory header/version guards,
authorization-aware caching, task attribution, SSE observation, metrics, and
the generic Tower layer.mcp-usage-store - atomic Redis, Valkey, and PostgreSQL task attribution for
horizontally scaled servers.mcp-usage-kit - the public facade and runnable rmcp example.mcp-usage-edge - a metering sidecar that fronts an MCP server written in any
language.mcp-overbilling - measures how much a request-counting meter overbills real
MCP traffic. Not published; it is a measurement harness, not a dependency.The repository contains the embeddable crates and the sidecar. It does not include a hosted control plane.
The meter records usage only after it observes a terminal result:
| Exchange | Usage |
|---|---|
tools/call with resultType: complete |
configured units |
Interim MRTR input_required result |
0 |
| Task creation and progress polls | 0 |
| Completed task | originating tool’s units, once per taskId |
| Failed/cancelled task or JSON-RPC error | 0 |
| Discovery and subscription traffic | 0 |
Cache-served resources/read |
configured units on delivery |
The edge refuses to trust mirrored headers from a protocol revision older than 2026-07-28. It also decodes the MCP Base64 name sentinel before looking up a price. See the MCP specification for the wire-level definitions these rules are derived from.
MCP_API_KEY="$(openssl rand -base64 32)" cargo run -p mcp-usage-kit --example rmcp_server
The example exposes an rmcp Streamable HTTP service at
http://127.0.0.1:3000/mcp, prices its add tool at two units, and flushes
aggregated usage to structured logs every ten seconds.
In an application, wrap the rmcp::StreamableHttpService directly:
use std::sync::Arc;
use mcp_usage_kit::{
BillingPipeline, EdgeConfig, InMemoryTenantStore, LogExporter,
MeterLayer, Tenant,
};
use tower::Layer;
let tenants = Arc::new(InMemoryTenantStore::new());
tenants.insert("secret-api-key", Tenant::new("acme", "cus_acme"));
let billing = Arc::new(BillingPipeline::new(LogExporter::new()));
let edge = EdgeConfig::new(tenants).with_recorder(billing.clone());
let metered_rmcp_service = MeterLayer::new(edge).layer(rmcp_service);
Call BillingPipeline::flush from a background task. Export failures retain the
same aggregate quantity and provider event identifier for the next retry; they
never change an MCP response. Cancellation and panic restore the in-progress
batch through a drop guard. LogExporter is stateless and logging-only; use an
application-owned exporter when exported batches must be captured.
Implement MeterEventProvider for the billing service your application uses,
then wrap it in MeterEventExporter. Providers receive unresolved aggregates in
their original order and return one MeterEventOutcome per aggregate. Accepted
and permanently rejected events are not resubmitted; retryable and ambiguous
events keep their stable identifiers. Permanent rejections enter a bounded,
identifier-deduplicated dead letter queue for application-owned reconciliation.
Verified asynchronous rejections can enter the same queue through
quarantine_async_rejection. Alert on dropped_dead_letters because a nonzero
value means reconciliation data was discarded.
Provider implementations own authentication, transport, wire encoding,
timestamp acceptance, response classification, and asynchronous event
verification. They must use stable aggregate identifiers for idempotency and use
only static, low-cardinality codes with no secrets or customer data in outcomes
and batch-wide errors. See
docs/integration-contracts.md for the complete
contract.
Enable redis or postgres on mcp-usage-kit when durable tasks can be polled
through more than one application instance. Both stores hash tenant and task
identifiers before persistence. The edge resolves the price when a task is
created, and the stores retain only that integer price plus a fixed method
category. Tool names, prompt names, resource URIs, and extension method strings
are not persisted. Records expire and are atomically claimed so only one
instance records a completed task.
The in-memory store remains the default for single-process deployments. See
mcp-usage-store for setup and operational
requirements. Exact dependency and provider assumptions are recorded in
docs/integration-contracts.md.
EdgeMetrics::render_prometheus emits low-cardinality Prometheus text without a
registry dependency. The optional opentelemetry feature registers pull-based
observable counters with an application-owned meter. Neither path exports API
keys, tenant identifiers, customer identifiers, methods, or tool names.
Machine-readable versioned test vectors live under
mcp-usage-core/conformance.
They are independent of Rust and can be reused by gateways and servers in other
languages.
The table above says request counting is the wrong unit. That is a measurable claim, so it is measured:
cargo run -p mcp-overbilling
Identical MCP traffic, billed two ways. Against the strongest version of a
request counter - one that reads Mcp-Name and applies your own price book per
request, and is blind only to what came back:
| HTTP requests | Correct | Request counting | |
|---|---|---|---|
| Discovery | 7 | 0 | 7 |
| Multi-round-trip | 8 | 35 | 155 |
| Task polling | 12 | 40 | 80 |
| Errors | 9 | 5 | 135 |
| Retries | 9 | 72 | 76 |
| Total | 45 | 152 | 453 |
2.98x the correct invoice. The suite holds this project to the same
standard: every scenario declares what should be billed and why, and CI fails
if terminal-delivery metering drifts from it. It also contains traffic where
request counting is correct, because a comparison built only from cases that
favour one answer is not evidence. See
mcp-overbilling.
cacheScope: "public" are not shared across
tenants by default. Sharing them means trusting the origin’s assertion that a
body does not depend on who asked for it, and resources/read is cacheable, so
one mislabelled result would disclose resource contents across tenants. Opt in
with EdgeConfig::with_public_cache_sharing when that assertion holds.EdgeConfig::with_credential_forwarding when the origin
performs its own check against the same key.X-Content-Type-Options: nosniff and
cache directives that keep them out of shared downstream caches.EdgeConfig::with_auth_failure_limit bounds sustained guessing across the
edge; only failures consume the budget, so enabling it cannot lock out callers
holding valid keys. It returns an error for a zero-duration window. Per-address
limits still belong in the proxy in front, because the edge has no client
identity it can trust.InMemoryTenantStore::insert refuses obviously weak API keys, since a digest
lookup only protects a secret that was hard to guess. insert_unchecked is the
explicit escape hatch for fixtures.EdgeConfig::deferred rather than dropped. Later authenticated requests drain
it automatically; drain it explicitly after graceful shutdown.traceparent headers pass through unchanged.413 Payload Too Large; configure it with EdgeConfig::with_max_request_body.cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-targets --all-features
cargo bench -p mcp-usage-tower --bench classification
The integration suite wraps a real rmcp 3.1 Streamable HTTP service and
asserts that the configured tool units reach the billing exporter.
Licensed under the Apache License, Version 2.0.