Skip to main content

Crate mcp_usage_core

Crate mcp_usage_core 

Source
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/call becomes N HTTP requests, each carrying Mcp-Method: tools/call. Counting requests bills N for one unit of work.
  • Tasks. A long job returns a handle and the client polls tasks/get until it finishes. A ten-minute job on a two-second poll interval is ~300 requests for one unit of work.
  • Caching. tools/list and friends carry ttlMs and cacheScope so 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-Name header 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.