Expand description
Billing-attribution semantics for MCP 2026-07-28.
This crate answers one question: given an MCP request and the response it got, what is owed? It performs no I/O, spawns nothing, knows nothing about HTTP, and never touches a billing provider. Keeping attribution separate makes it possible to test the billing rules without a network.
§Why request counts are insufficient
Before 2026-07-28, metering an MCP server was counting requests. That revision made counting requests wrong in three separate ways:
- Multi Round-Trip Requests. A server that needs user input mid-call now
returns an interim result and the client retries the whole call. One
logical
tools/callbecomes N HTTP requests, each carryingMcp-Method: tools/call. Counting requests bills N for one unit of work. - Tasks. A long job returns a handle and the client polls
tasks/getuntil it finishes. A ten-minute job on a two-second poll interval is ~300 requests for one unit of work. - Caching.
tools/listand friends carryttlMsandcacheScopeso they can stop reaching the origin. Billing discovery traffic charges for the act of connecting.
The fix is to stop counting requests. See charge for the rule and the
reasoning behind every claim here.
§Shape of a call site
use mcp_usage_core::{Call, Charge, Method, PriceBook, name, peek, version};
// 1. Refuse to price on headers a legacy origin will never validate.
let trust = version::assess(Some("2026-07-28"));
assert!(trust.is_trusted());
// 2. Classify from headers alone. No body, no allocation on the common path.
let method = Method::parse("tools/call");
let tool = name::decode("get_weather").expect("valid header value");
// 3. Decide once the response is known.
let body = serde_json::json!({ "result": { "resultType": "complete" } });
let charge = mcp_usage_core::decide(
&Call::new(method, Some(tool.into_owned())),
&peek::response(&body),
&PriceBook::flat(1),
);
assert!(matches!(charge, Charge::Billable(_)));
assert_eq!(charge.units(), 1);§Where the state lives
Nowhere in here. Durable tasks require the caller to retain two small pieces
of state: pre-priced TaskAttribution keyed by taskId and the task IDs
already billed. Resolving the price when the task is created avoids
persisting a tool name, prompt name, resource URI, or extension method text.
This crate accepts the former in decide_with_task_attribution and surfaces
the latter as Billable::idempotency_key. Enforcement belongs to the
caller, which is the only layer that knows whether it runs beside the origin
or as a shared proxy.
Re-exports§
pub use charge::Billable;pub use charge::Call;pub use charge::Charge;pub use charge::FreeReason;pub use charge::TaskAttribution;pub use charge::TaskOriginKind;pub use charge::decide;pub use charge::decide_with_task_attribution;pub use charge::decide_with_task_origin;pub use legacy::LegacyClassificationError;pub use legacy::classify_body;pub use limits::LimitDecision;pub use limits::LimitReason;pub use limits::Limits;pub use limits::Usage;pub use limits::assess_limits;pub use method::Method;pub use peek::RequestPeek;pub use peek::ResponsePeek;pub use peek::ResultType;pub use peek::TaskPeek;pub use peek::TaskStatus;pub use price::PriceBook;pub use version::HeaderTrust;pub use version::ProtocolVersion;pub use version::TrustFailure;pub use version::validate_header;
Modules§
- charge
- The billing decision: what a request/response pair is worth.
- legacy
- Classifying a request from its body, for clients older than the mirrored headers.
- limits
- Pure quota and spend-cap admission decisions.
- method
- The MCP method taxonomy, and the classifications the meter needs from it.
- name
- Decoding the
Mcp-Nameheader value. - peek
- The bounded body inspections the spec forces on a meter, and nothing more.
- price
- The price book: how many billable units a delivered call is worth.
- version
- Protocol version, and the guard that decides whether the mirrored headers may be trusted at all.