mcp-usage-kit

UsageKit for MCP

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

Workspace

The repository contains the embeddable crates and the sidecar. It does not include a hosted control plane.

The billing rule

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.

Run the example

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.

Distributed task attribution

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.

Observability and conformance

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.

Does this actually matter?

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.

Security and operational bounds

Verification

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.

License

Licensed under the Apache License, Version 2.0.