Skip to main content

mcp_usage_tower/
layer.rs

1//! Generic Tower [`Layer`](tower::Layer) implementation.
2
3use std::convert::Infallible;
4use std::error::Error as StdError;
5use std::future::Future;
6use std::pin::Pin;
7use std::sync::Arc;
8use std::task::{Context, Poll};
9use std::time::Duration;
10
11use bytes::{Buf, Bytes, BytesMut};
12use http::header::{AUTHORIZATION, CACHE_CONTROL, CONTENT_TYPE, X_CONTENT_TYPE_OPTIONS};
13use http::{HeaderMap, Request, Response, StatusCode};
14use http_body::{Body, Frame};
15use http_body_util::combinators::UnsyncBoxBody;
16use http_body_util::{BodyExt, Full};
17use mcp_usage_core::{
18    Call, Charge, Method, ResultType, TaskAttribution, TaskStatus, TrustFailure,
19    decide_with_task_attribution, validate_header,
20};
21use mcp_usage_export::{NoopRecorder, RecordOutcome, SharedRecorder, UsageEvent};
22use serde_json::{Value, json};
23use thiserror::Error;
24use tower::{Layer, Service};
25
26use crate::auth::{AuthFailureLimit, Tenant, TenantStore, hash_api_key};
27use crate::cache::{CachedResponse, RequestMetadata, ResponseCache, cache_hints, inspect_request};
28use crate::classify::{ClassificationError, classify_protocol_headers};
29use crate::deferred::DeferredCompletions;
30use crate::metrics::EdgeMetrics;
31use crate::task::{InMemoryTaskStore, TaskAttributionStore};
32use crate::{METHOD_HEADER, NAME_HEADER, PROTOCOL_VERSION_HEADER};
33
34/// Erased body error returned by [`MeterService`].
35pub type BoxError = Box<dyn StdError + Send + Sync>;
36/// Response body returned by [`MeterService`].
37pub type MeterBody = UnsyncBoxBody<Bytes, BoxError>;
38
39const DEFAULT_CACHE_ENTRIES: usize = 512;
40const DEFAULT_CACHE_TTL: Duration = Duration::from_secs(24 * 60 * 60);
41/// Terminal accounting parked for want of somewhere to await.
42const DEFAULT_DEFERRED_CAPACITY: usize = 4_096;
43/// Parked completions run at the start of each request.
44///
45/// Small on purpose. This borrows the request path to make progress, so it has
46/// to stay cheap; an application under load has no shortage of requests to
47/// amortize the backlog across.
48const DEFAULT_DEFERRED_DRAIN_PER_REQUEST: usize = 2;
49
50/// Invalid edge configuration.
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
52pub enum EdgeConfigError {
53    /// Authentication failure windows must advance over time.
54    #[error("authentication failure window must be greater than zero")]
55    InvalidAuthFailureWindow,
56}
57
58/// Runtime configuration shared by every cloned metering service.
59#[derive(Clone)]
60pub struct EdgeConfig {
61    tenants: Arc<dyn TenantStore>,
62    recorder: SharedRecorder,
63    tasks: Arc<dyn TaskAttributionStore>,
64    cache: Arc<ResponseCache>,
65    metrics: Arc<EdgeMetrics>,
66    meter_name: String,
67    max_request_body: usize,
68    max_response_capture: usize,
69    cache_max_entries: usize,
70    cache_max_ttl: Duration,
71    share_public_cache: bool,
72    forward_credentials: bool,
73    deferred: Arc<DeferredCompletions>,
74    deferred_drain_per_request: usize,
75    auth_failure_limit: Option<Arc<AuthFailureLimit>>,
76    strict_protocol_version: bool,
77}
78
79impl std::fmt::Debug for EdgeConfig {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        f.debug_struct("EdgeConfig")
82            .field("meter_name", &self.meter_name)
83            .field("max_request_body", &self.max_request_body)
84            .field("max_response_capture", &self.max_response_capture)
85            .field("share_public_cache", &self.share_public_cache)
86            .field("forward_credentials", &self.forward_credentials)
87            .field("strict_protocol_version", &self.strict_protocol_version)
88            .field("deferred_pending", &self.deferred.len())
89            .finish_non_exhaustive()
90    }
91}
92
93impl EdgeConfig {
94    /// Construct an edge using API-key tenants and a no-op billing recorder.
95    #[must_use]
96    pub fn new(tenants: Arc<dyn TenantStore>) -> Self {
97        Self {
98            tenants,
99            recorder: Arc::new(NoopRecorder),
100            tasks: Arc::new(InMemoryTaskStore::new()),
101            cache: Arc::new(ResponseCache::new(
102                DEFAULT_CACHE_ENTRIES,
103                DEFAULT_CACHE_TTL,
104                false,
105            )),
106            metrics: Arc::new(EdgeMetrics::default()),
107            meter_name: "mcp_units".to_owned(),
108            max_request_body: 1024 * 1024,
109            max_response_capture: 1024 * 1024,
110            cache_max_entries: DEFAULT_CACHE_ENTRIES,
111            cache_max_ttl: DEFAULT_CACHE_TTL,
112            share_public_cache: false,
113            forward_credentials: false,
114            deferred: Arc::new(DeferredCompletions::new(DEFAULT_DEFERRED_CAPACITY)),
115            deferred_drain_per_request: DEFAULT_DEFERRED_DRAIN_PER_REQUEST,
116            auth_failure_limit: None,
117            strict_protocol_version: false,
118        }
119    }
120
121    /// Rebuild the cache from every input that shapes it.
122    ///
123    /// The builders below are order-independent because each one records its
124    /// setting and then rebuilds from all of them. Constructing the cache in
125    /// place inside a single builder would let a later call silently discard an
126    /// earlier one.
127    fn rebuild_cache(&mut self) {
128        self.cache = Arc::new(ResponseCache::new(
129            self.cache_max_entries,
130            self.cache_max_ttl,
131            self.share_public_cache,
132        ));
133    }
134
135    /// Refuse requests that predate the mirrored headers instead of reading their body.
136    ///
137    /// By default a legacy request is classified from its JSON-RPC body, which is what
138    /// lets clients older than [`mcp_usage_core::version::HEADER_VALIDATION_SINCE`] -
139    /// still the majority - reach the origin at all. The body is the document the origin
140    /// executes, so pricing on it cannot be gamed the way pricing on unvalidated headers
141    /// can; the cost is one bounded JSON parse per legacy request.
142    ///
143    /// Set this when a deployment would rather serve only modern clients than spend that
144    /// parse, or when an upstream proxy already guarantees the revision.
145    #[must_use]
146    pub fn with_strict_protocol_version(mut self, strict: bool) -> Self {
147        self.strict_protocol_version = strict;
148        self
149    }
150
151    /// Install the hot-path usage recorder.
152    #[must_use]
153    pub fn with_recorder(mut self, recorder: SharedRecorder) -> Self {
154        self.recorder = recorder;
155        self
156    }
157
158    /// Install a durable task-attribution store.
159    #[must_use]
160    pub fn with_task_store(mut self, tasks: Arc<dyn TaskAttributionStore>) -> Self {
161        self.tasks = tasks;
162        self
163    }
164
165    /// Set the provider meter event name.
166    #[must_use]
167    pub fn with_meter_name(mut self, meter_name: impl Into<String>) -> Self {
168        self.meter_name = meter_name.into();
169        self
170    }
171
172    /// Configure cache capacity and the maximum honored server TTL.
173    #[must_use]
174    pub fn with_cache(mut self, max_entries: usize, max_ttl: Duration) -> Self {
175        self.cache_max_entries = max_entries;
176        self.cache_max_ttl = max_ttl;
177        self.rebuild_cache();
178        self
179    }
180
181    /// Share results the origin marks `cacheScope: "public"` across tenants.
182    ///
183    /// Disabled by default. A `"public"` result is an assertion by the origin
184    /// that the body does not depend on who asked for it; honoring it puts one
185    /// entry in a bucket every authorization context reads, so an origin that
186    /// mislabels a tenant-specific result turns that mistake into a cross-tenant
187    /// disclosure at the edge. Cacheable methods include `resources/read`, so
188    /// the blast radius is resource contents, not just discovery listings.
189    ///
190    /// Enable this only when the origin's public results are genuinely
191    /// tenant-independent. Leaving it off costs cache hit rate and nothing else:
192    /// a cache may always decline to reuse a response.
193    #[must_use]
194    pub fn with_public_cache_sharing(mut self, share: bool) -> Self {
195        self.share_public_cache = share;
196        self.rebuild_cache();
197        self
198    }
199
200    /// Forward the API key to the inner service after authenticating it.
201    ///
202    /// Disabled by default: the edge consumes the credential, so passing it on
203    /// hands the inner service a secret it was not issued. That is harmless when
204    /// the origin runs in-process, and an exposure the moment this layer fronts
205    /// an origin in another trust domain.
206    ///
207    /// Enable it when the origin performs its own check against the same key.
208    #[must_use]
209    pub fn with_credential_forwarding(mut self, forward: bool) -> Self {
210        self.forward_credentials = forward;
211        self
212    }
213
214    /// Bound bytes accepted from one MCP request body.
215    #[must_use]
216    pub fn with_max_request_body(mut self, bytes: usize) -> Self {
217        self.max_request_body = bytes;
218        self
219    }
220
221    /// Bound bytes retained while observing a response. Oversized responses
222    /// continue streaming but fail toward not charging and not caching.
223    #[must_use]
224    pub fn with_max_response_capture(mut self, bytes: usize) -> Self {
225        self.max_response_capture = bytes;
226        self
227    }
228
229    /// Shared edge counters.
230    #[must_use]
231    pub fn metrics(&self) -> Arc<EdgeMetrics> {
232        self.metrics.clone()
233    }
234
235    /// Terminal accounting parked because it could not finish without awaiting.
236    ///
237    /// Only a durable task store parks anything: its futures perform real I/O,
238    /// and the body is released from `Drop`, where nothing may await. Every
239    /// subsequent authenticated request runs a bounded number of them, so an
240    /// application that ignores this still converges. Draining explicitly is
241    /// better, and draining after graceful shutdown is what stops a departing
242    /// process from taking durable-task charges with it.
243    ///
244    /// Take the handle before passing the configuration to the layer.
245    #[must_use]
246    pub fn deferred(&self) -> Arc<DeferredCompletions> {
247        self.deferred.clone()
248    }
249
250    /// Bound how much parked accounting each authenticated request runs.
251    ///
252    /// Zero leaves draining entirely to [`EdgeConfig::deferred`]. Raising it
253    /// clears a backlog sooner at the cost of latency on the requests that do
254    /// the work.
255    #[must_use]
256    pub fn with_deferred_drain_per_request(mut self, completions: usize) -> Self {
257        self.deferred_drain_per_request = completions;
258        self
259    }
260
261    /// Bound how many completions may be parked before the oldest is discarded.
262    #[must_use]
263    pub fn with_deferred_capacity(mut self, capacity: usize) -> Self {
264        self.deferred = Arc::new(DeferredCompletions::new(capacity));
265        self
266    }
267
268    /// Refuse further guessing after `max_failures` bad credentials in `window`.
269    ///
270    /// Disabled by default, because the useful ceiling depends entirely on how
271    /// many clients an edge serves. Only failures consume the budget, so a
272    /// caller with a valid key is never affected: exhausting it turns a wrong
273    /// key's `401` into a `429` and nothing more.
274    ///
275    /// This bounds sustained guessing across the whole edge. It is not
276    /// per-client limiting, which needs a client identity this layer cannot
277    /// trust, and belongs in the proxy in front of it.
278    ///
279    /// # Errors
280    ///
281    /// Returns [`EdgeConfigError::InvalidAuthFailureWindow`] when `window` is
282    /// zero.
283    pub fn with_auth_failure_limit(
284        mut self,
285        max_failures: u64,
286        window: Duration,
287    ) -> Result<Self, EdgeConfigError> {
288        if window.is_zero() {
289            return Err(EdgeConfigError::InvalidAuthFailureWindow);
290        }
291        self.auth_failure_limit = Some(Arc::new(AuthFailureLimit::new(max_failures, window)));
292        Ok(self)
293    }
294}
295
296/// Tower layer that installs [`MeterService`].
297#[derive(Debug, Clone)]
298pub struct MeterLayer {
299    config: Arc<EdgeConfig>,
300}
301
302impl MeterLayer {
303    /// Construct a metering layer from its shared configuration.
304    #[must_use]
305    pub fn new(config: EdgeConfig) -> Self {
306        Self {
307            config: Arc::new(config),
308        }
309    }
310}
311
312impl<S> Layer<S> for MeterLayer {
313    type Service = MeterService<S>;
314
315    fn layer(&self, inner: S) -> Self::Service {
316        MeterService {
317            inner,
318            config: self.config.clone(),
319        }
320    }
321}
322
323/// Service produced by [`MeterLayer`].
324#[derive(Debug, Clone)]
325pub struct MeterService<S> {
326    inner: S,
327    config: Arc<EdgeConfig>,
328}
329
330impl<S, RequestBody, ResponseBody> Service<Request<RequestBody>> for MeterService<S>
331where
332    RequestBody: Body<Data = Bytes> + Send + 'static,
333    S: Service<Request<Full<Bytes>>, Response = Response<ResponseBody>> + Clone + Send + 'static,
334    S::Future: Send + 'static,
335    S::Error: Send + 'static,
336    ResponseBody: Body<Data = Bytes> + Send + 'static,
337    ResponseBody::Error: StdError + Send + Sync + 'static,
338{
339    type Response = Response<MeterBody>;
340    type Error = S::Error;
341    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
342
343    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
344        self.inner.poll_ready(cx)
345    }
346
347    #[expect(
348        clippy::too_many_lines,
349        reason = "the request lifecycle stays linear so security-gate ordering is auditable"
350    )]
351    fn call(&mut self, request: Request<RequestBody>) -> Self::Future {
352        let replacement = self.inner.clone();
353        let mut inner = std::mem::replace(&mut self.inner, replacement);
354        let config = self.config.clone();
355
356        Box::pin(async move {
357            let (mut parts, request_body) = request.into_parts();
358            // Classification runs before authentication so a malformed request is
359            // refused without consulting the tenant store. A legacy request cannot be
360            // classified from headers at all, so it is deferred to the body - but only
361            // after the credential gate below, so unauthenticated traffic never buys a
362            // JSON parse.
363            let classification =
364                match classify_request_headers(&parts.headers, config.strict_protocol_version) {
365                    Ok(classification) => classification,
366                    Err(message) => {
367                        config.metrics.rejected();
368                        return Ok(error_response(
369                            StatusCode::BAD_REQUEST,
370                            "INVALID_MCP_HEADERS",
371                            &message,
372                        ));
373                    }
374                };
375            let api_key = match extract_api_key(&parts.headers) {
376                Ok(api_key) => api_key,
377                Err(error) => {
378                    let message = match error {
379                        CredentialError::Missing => "missing API key",
380                        CredentialError::Invalid => "invalid or ambiguous API key headers",
381                    };
382                    return Ok(refuse_credential(&config, message));
383                }
384            };
385            let Some(tenant) = config.tenants.authenticate(api_key) else {
386                return Ok(refuse_credential(&config, "invalid API key"));
387            };
388
389            // Only authenticated traffic may borrow the request path to drive
390            // earlier backend work. Malformed headers and failed credentials
391            // return above without polling a deferred completion.
392            if config.deferred_drain_per_request > 0 {
393                config
394                    .deferred
395                    .drain_some(config.deferred_drain_per_request)
396                    .await;
397            }
398            let authorization_context = hash_api_key(api_key);
399            config.metrics.classified();
400
401            let request_bytes =
402                match collect_request_body(request_body, config.max_request_body).await {
403                    Ok(bytes) => bytes,
404                    Err(RequestBodyError::TooLarge) => {
405                        config.metrics.rejected();
406                        return Ok(error_response(
407                            StatusCode::PAYLOAD_TOO_LARGE,
408                            "BODY_TOO_LARGE",
409                            "MCP request body exceeds the configured limit",
410                        ));
411                    }
412                    Err(RequestBodyError::Read) => {
413                        config.metrics.rejected();
414                        return Ok(error_response(
415                            StatusCode::BAD_REQUEST,
416                            "INVALID_BODY",
417                            "failed to read MCP request body",
418                        ));
419                    }
420                };
421            let legacy = classification.is_from_body();
422            let protocol = match classification.resolve(&request_bytes) {
423                Ok(protocol) => protocol,
424                Err(message) => {
425                    config.metrics.rejected();
426                    return Ok(error_response(
427                        StatusCode::BAD_REQUEST,
428                        "INVALID_MCP_HEADERS",
429                        &message,
430                    ));
431                }
432            };
433            let metadata = inspect_request(&protocol.call, &request_bytes);
434            if let Some(key) = metadata.cache_key
435                && let Some(cached) =
436                    config
437                        .cache
438                        .get(key, &authorization_context, metadata.request_id.as_ref())
439            {
440                config.metrics.cache_hit();
441                let mut cached_request = metadata;
442                cached_request.cache_key = None;
443                let content_type = cached
444                    .headers
445                    .get(CONTENT_TYPE)
446                    .and_then(|value| value.to_str().ok())
447                    .unwrap_or_default()
448                    .to_owned();
449                let completion = Completion {
450                    config,
451                    tenant,
452                    authorization_context,
453                    legacy,
454                    call: protocol.call,
455                    request: cached_request,
456                    response_status: cached.status,
457                    response_version: cached.version,
458                    response_headers: cached.headers.clone(),
459                    content_type,
460                };
461                return Ok(cached_response(cached, completion));
462            }
463            if metadata.cache_key.is_some() {
464                config.metrics.cache_miss();
465            }
466
467            // The credential was consumed by the gate above. Removing both
468            // headers unconditionally is safe: `extract_api_key` rejects a
469            // request carrying both, so at most one of them is present.
470            if !config.forward_credentials {
471                parts.headers.remove(crate::API_KEY_HEADER);
472                parts.headers.remove(AUTHORIZATION);
473            }
474            let request = Request::from_parts(parts, Full::new(request_bytes));
475            let response = inner.call(request).await?;
476            let (parts, body) = response.into_parts();
477            let completion = Completion {
478                config,
479                tenant,
480                authorization_context,
481                legacy,
482                call: protocol.call,
483                request: metadata,
484                response_status: parts.status,
485                response_version: parts.version,
486                response_headers: cacheable_headers(&parts.headers),
487                content_type: parts
488                    .headers
489                    .get(CONTENT_TYPE)
490                    .and_then(|value| value.to_str().ok())
491                    .unwrap_or_default()
492                    .to_owned(),
493            };
494            let observed = ObservedBody::new(body, completion);
495            let boxed = observed
496                .map_err(|error| -> BoxError { Box::new(error) })
497                .boxed_unsync();
498            Ok(Response::from_parts(parts, boxed))
499        })
500    }
501}
502
503struct ClassifiedCall {
504    call: Call,
505}
506
507/// The outcome of looking at the request headers alone.
508enum Classification {
509    /// The revision mandates header/body validation, so the headers priced the call
510    /// and the body is never parsed.
511    FromHeaders(ClassifiedCall),
512    /// A legacy revision. The call is priced from the body once it has been read.
513    FromBody,
514}
515
516impl Classification {
517    /// Whether the call was priced from the body rather than from trusted headers.
518    const fn is_from_body(&self) -> bool {
519        matches!(self, Self::FromBody)
520    }
521
522    /// Finish classification, reading the body only when the headers could not be
523    /// trusted. Called after the credential gate.
524    fn resolve(self, body: &[u8]) -> Result<ClassifiedCall, String> {
525        match self {
526            Self::FromHeaders(classified) => Ok(classified),
527            Self::FromBody => {
528                let (method, name) =
529                    mcp_usage_core::classify_body(body).map_err(|error| error.to_string())?;
530                Ok(ClassifiedCall {
531                    call: Call::new(method, name),
532                })
533            }
534        }
535    }
536}
537
538fn classify_request_headers(headers: &HeaderMap, strict: bool) -> Result<Classification, String> {
539    let version = optional_header_str(headers, PROTOCOL_VERSION_HEADER)?;
540    let method = optional_header_str(headers, METHOD_HEADER)?;
541    let name = optional_header_str(headers, NAME_HEADER)?;
542
543    // Trust is settled before anything else, because "this client is old" and "this
544    // client is broken" deserve different answers.
545    if let Err(failure) = validate_header(version) {
546        return match failure {
547            // A version that is not a date is not a revision we can reason about. It is
548            // a malfunctioning or probing client, not a legacy one, so it is refused
549            // whatever the fallback setting - reading its body would reward the garbage.
550            TrustFailure::MalformedVersion(_) => {
551                Err(ClassificationError::UntrustedVersion(failure.to_string()).to_string())
552            }
553            // Absent or pre-validation revisions are the ordinary legacy client. Price
554            // it from the body unless this deployment asked to serve only modern ones.
555            TrustFailure::MissingVersionHeader | TrustFailure::UnvalidatedRevision(_) => {
556                if strict {
557                    Err(ClassificationError::UntrustedVersion(failure.to_string()).to_string())
558                } else {
559                    Ok(Classification::FromBody)
560                }
561            }
562        };
563    }
564
565    // Trusted revision: the origin is obliged to reject header/body disagreement, so the
566    // headers price the call and the body is never parsed. A modern request with a
567    // missing or malformed Mcp-Method is still an error - it claimed to mirror headers.
568    let classified = classify_protocol_headers(version, method, name).map_err(|e| e.to_string())?;
569    Ok(Classification::FromHeaders(ClassifiedCall {
570        call: Call::new(
571            classified.method,
572            classified.name.map(std::borrow::Cow::into_owned),
573        ),
574    }))
575}
576
577fn optional_header_str<'a>(headers: &'a HeaderMap, name: &str) -> Result<Option<&'a str>, String> {
578    unique_header(headers, name)?
579        .map(|value| {
580            value
581                .to_str()
582                .map_err(|_| format!("{name} is not a visible ASCII header value"))
583        })
584        .transpose()
585}
586
587fn unique_header<'a>(
588    headers: &'a HeaderMap,
589    name: &str,
590) -> Result<Option<&'a http::HeaderValue>, String> {
591    let mut values = headers.get_all(name).iter();
592    let value = values.next();
593    if values.next().is_some() {
594        return Err(format!("multiple {name} headers are not allowed"));
595    }
596    Ok(value)
597}
598
599#[derive(Debug, Clone, Copy, PartialEq, Eq)]
600enum CredentialError {
601    Missing,
602    Invalid,
603}
604
605fn extract_api_key(headers: &HeaderMap) -> Result<&str, CredentialError> {
606    let direct =
607        unique_header(headers, crate::API_KEY_HEADER).map_err(|_| CredentialError::Invalid)?;
608    let authorization =
609        unique_header(headers, AUTHORIZATION.as_str()).map_err(|_| CredentialError::Invalid)?;
610    let value = match (direct, authorization) {
611        (None, None) => return Err(CredentialError::Missing),
612        (Some(_), Some(_)) => return Err(CredentialError::Invalid),
613        (Some(value), None) => value.to_str().map_err(|_| CredentialError::Invalid)?,
614        (None, Some(value)) => bearer_token(value.to_str().map_err(|_| CredentialError::Invalid)?)
615            .ok_or(CredentialError::Invalid)?,
616    };
617    if value.is_empty() {
618        return Err(CredentialError::Invalid);
619    }
620    Ok(value)
621}
622
623/// The token from an `Authorization: Bearer` credential.
624///
625/// The scheme name is matched case-insensitively, because RFC 7235 defines it
626/// that way and real clients send `bearer`. Matching it exactly turns a
627/// conforming request into a `401` that looks, from the caller's side, exactly
628/// like a wrong key. RFC 7235 also permits more than one space before the
629/// token, while RFC 6750's `token68` cannot itself begin with one, so leading
630/// space is separator rather than credential.
631fn bearer_token(value: &str) -> Option<&str> {
632    let (scheme, token) = value.split_once(' ')?;
633    scheme
634        .eq_ignore_ascii_case("bearer")
635        .then(|| token.trim_start_matches(' '))
636}
637
638/// Refuse a request whose credential did not authenticate.
639///
640/// The failure budget is consulted only here, on the path a caller reaches by
641/// presenting a credential that did not work. A valid key never passes through
642/// this function, which is what makes a global budget safe: guessing cannot
643/// throttle callers who are not guessing.
644fn refuse_credential(config: &EdgeConfig, message: &str) -> Response<MeterBody> {
645    if let Some(limit) = config.auth_failure_limit.as_ref() {
646        if limit.is_exhausted() {
647            config.metrics.throttled();
648            return error_response(
649                StatusCode::TOO_MANY_REQUESTS,
650                "TOO_MANY_FAILED_CREDENTIALS",
651                "too many failed authentication attempts",
652            );
653        }
654        limit.record_failure();
655    }
656    config.metrics.unauthenticated();
657    error_response(StatusCode::UNAUTHORIZED, "UNAUTHENTICATED", message)
658}
659
660fn error_response(status: StatusCode, code: &str, message: &str) -> Response<MeterBody> {
661    let bytes = Bytes::from(json!({"error":{"code":code,"message":message}}).to_string());
662    let mut response = Response::new(
663        Full::new(bytes)
664            .map_err(|never: Infallible| -> BoxError { match never {} })
665            .boxed_unsync(),
666    );
667    *response.status_mut() = status;
668    let headers = response.headers_mut();
669    headers.insert(
670        CONTENT_TYPE,
671        http::HeaderValue::from_static("application/json"),
672    );
673    // The message can echo a rejected header value back to the caller, so pin
674    // the interpretation to JSON rather than leaving it to content sniffing.
675    headers.insert(
676        X_CONTENT_TYPE_OPTIONS,
677        http::HeaderValue::from_static("nosniff"),
678    );
679    headers.insert(CACHE_CONTROL, http::HeaderValue::from_static("no-store"));
680    response
681}
682
683fn cached_response(cached: CachedResponse, completion: Completion) -> Response<MeterBody> {
684    let body = Bytes::from(cached.body.to_string());
685    let shares_public = completion.config.share_public_cache;
686    let observed = ObservedBody::new(Full::new(body), completion);
687    let mut response = Response::new(
688        observed
689            .map_err(|never: Infallible| -> BoxError { match never {} })
690            .boxed_unsync(),
691    );
692    *response.status_mut() = cached.status;
693    *response.version_mut() = cached.version;
694    *response.headers_mut() = cached.headers;
695    let headers = response.headers_mut();
696    headers.insert("x-mcp-usage-cache", http::HeaderValue::from_static("hit"));
697    headers.insert(
698        X_CONTENT_TYPE_OPTIONS,
699        http::HeaderValue::from_static("nosniff"),
700    );
701    if !shares_public {
702        // Nothing here is safe for a shared cache to reuse across callers, and a
703        // CDN in front of the edge has no idea the body is keyed on an API key.
704        headers.insert(CACHE_CONTROL, http::HeaderValue::from_static("private"));
705    }
706    response
707}
708
709fn cacheable_headers(headers: &HeaderMap) -> HeaderMap {
710    let mut kept = HeaderMap::new();
711    if let Some(content_type) = headers.get(CONTENT_TYPE) {
712        kept.insert(CONTENT_TYPE, content_type.clone());
713    }
714    kept
715}
716
717struct Completion {
718    config: Arc<EdgeConfig>,
719    tenant: Tenant,
720    authorization_context: String,
721    /// The request predated mirrored headers, so its response predates `resultType`.
722    legacy: bool,
723    call: Call,
724    request: RequestMetadata,
725    response_status: StatusCode,
726    response_version: http::Version,
727    response_headers: HeaderMap,
728    content_type: String,
729}
730
731impl Completion {
732    #[expect(
733        clippy::too_many_lines,
734        reason = "terminal accounting stays linear so fail-free ordering remains auditable"
735    )]
736    async fn finish(self, bytes: Bytes) {
737        let Some(body) = terminal_response(&self.content_type, &bytes) else {
738            self.config.metrics.unrecognized_response();
739            return;
740        };
741        let response = mcp_usage_core::peek::response(&body);
742        // A legacy server cannot say "complete"; on those revisions a result is the
743        // delivered work. Without this every legacy call would meter as free.
744        let response = if self.legacy {
745            response.with_legacy_delivery()
746        } else {
747            response
748        };
749
750        let response_task_id = response
751            .task
752            .as_ref()
753            .and_then(|task| task.task_id.as_deref());
754        let requested_task_id = self.request.requested_task_id.as_deref();
755        if matches!(self.call.method, Method::TasksGet)
756            && let (Some(requested), Some(returned)) = (requested_task_id, response_task_id)
757            && requested != returned
758        {
759            // Never attribute one task's result to a differently requested
760            // task. A conforming origin cannot produce this mismatch.
761            self.config.metrics.unrecognized_response();
762            return;
763        }
764
765        if matches!(response.result_type, ResultType::Task)
766            && let Some(task_id) = response
767                .task
768                .as_ref()
769                .and_then(|task| task.task_id.as_deref())
770            && let Err(error) = self
771                .config
772                .tasks
773                .insert(
774                    &self.tenant.id,
775                    task_id,
776                    TaskAttribution::from_call(&self.call, &self.tenant.prices),
777                )
778                .await
779        {
780            self.config.metrics.record_failure();
781            tracing::error!(error = %error, "failed to retain MCP task attribution");
782        }
783
784        let cleanup_task_id = response_task_id.or(requested_task_id);
785        let terminal_task = response.task.as_ref().is_some_and(|task| {
786            matches!(
787                task.status,
788                TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled
789            )
790        });
791        let completed_task = response
792            .task
793            .as_ref()
794            .is_some_and(|task| matches!(task.status, TaskStatus::Completed));
795        let task_origin =
796            if matches!(self.call.method, Method::TasksGet) && completed_task && !response.is_error
797            {
798                if let Some(task_id) = response_task_id {
799                    match self.config.tasks.claim(&self.tenant.id, task_id).await {
800                        Ok(origin) => origin,
801                        Err(error) => {
802                            self.config.metrics.record_failure();
803                            tracing::error!(error = %error, "failed to claim MCP task attribution");
804                            None
805                        }
806                    }
807                } else {
808                    None
809                }
810            } else {
811                None
812            };
813        let charge = decide_with_task_attribution(
814            &self.call,
815            &response,
816            &self.tenant.prices,
817            task_origin.as_ref(),
818        );
819        match charge {
820            Charge::Billable(billable) => {
821                let usage = UsageEvent::now(
822                    &self.tenant.id,
823                    &self.tenant.billing_customer_id,
824                    &self.config.meter_name,
825                    billable.units,
826                    billable.idempotency_key,
827                );
828                match self.config.recorder.record(usage) {
829                    Ok(RecordOutcome::Recorded | RecordOutcome::ZeroUnits) => {
830                        self.config.metrics.billed(billable.units);
831                    }
832                    Ok(RecordOutcome::Duplicate) => {
833                        self.config.metrics.duplicate();
834                    }
835                    Err(error) => {
836                        self.config.metrics.record_failure();
837                        tracing::error!(error = %error, "failed to buffer MCP usage");
838                        if let (Some(task_id), Some(origin)) = (response_task_id, task_origin)
839                            && let Err(store_error) = self
840                                .config
841                                .tasks
842                                .insert(&self.tenant.id, task_id, origin)
843                                .await
844                        {
845                            self.config.metrics.record_failure();
846                            tracing::error!(
847                                error = %store_error,
848                                "failed to restore MCP task attribution after recorder rejection"
849                            );
850                        }
851                    }
852                }
853            }
854            Charge::Free(_) => {
855                self.config.metrics.free();
856                if terminal_task
857                    && !completed_task
858                    && let Some(task_id) = cleanup_task_id
859                    && let Err(error) = self.config.tasks.remove(&self.tenant.id, task_id).await
860                {
861                    self.config.metrics.record_failure();
862                    tracing::error!(error = %error, "failed to remove MCP task attribution");
863                }
864            }
865        }
866
867        if self.response_status.is_success()
868            && !self.request.is_continuation
869            && has_media_type(&self.content_type, "application/json")
870            && let Some(key) = self.request.cache_key
871            && let Some((ttl, scope)) = cache_hints(&body)
872        {
873            self.config.cache.insert(
874                key,
875                &self.authorization_context,
876                scope,
877                ttl,
878                CachedResponse {
879                    status: self.response_status,
880                    version: self.response_version,
881                    headers: self.response_headers,
882                    body,
883                },
884            );
885        }
886    }
887}
888
889struct ObservedBody<B> {
890    inner: Pin<Box<B>>,
891    completion: Option<Completion>,
892    finishing: Option<Pin<Box<dyn Future<Output = ()> + Send>>>,
893    captured: BytesMut,
894    overflowed: bool,
895    metrics: Arc<EdgeMetrics>,
896    deferred: Arc<DeferredCompletions>,
897}
898
899impl<B> ObservedBody<B> {
900    fn new(inner: B, completion: Completion) -> Self {
901        let metrics = completion.config.metrics.clone();
902        let deferred = completion.config.deferred.clone();
903        Self {
904            inner: Box::pin(inner),
905            completion: Some(completion),
906            finishing: None,
907            captured: BytesMut::new(),
908            overflowed: false,
909            metrics,
910            deferred,
911        }
912    }
913
914    /// Build the terminal-accounting future, consuming the pending completion.
915    ///
916    /// Returns `None` when there is nothing left to account for, either because
917    /// accounting already ran or because the captured body overflowed.
918    fn start_completion(&mut self) -> Option<Pin<Box<dyn Future<Output = ()> + Send>>> {
919        let completion = self.completion.take()?;
920        if self.overflowed {
921            completion.config.metrics.unrecognized_response();
922            return None;
923        }
924        let captured = std::mem::take(&mut self.captured).freeze();
925        Some(Box::pin(completion.finish(captured)))
926    }
927}
928
929impl<B> Drop for ObservedBody<B> {
930    /// Account for a response the transport stopped polling before end-of-stream.
931    ///
932    /// A body that declares a `Content-Length` is not polled again once that
933    /// many bytes have been written, so `poll_frame` never observes the
934    /// end-of-stream that terminal accounting used to depend on. That is the
935    /// ordinary case for a fixed-length JSON result, and without this the meter
936    /// records nothing at all. Dropping the body is the one signal every
937    /// transport does give us.
938    ///
939    /// Nothing here may await, so the future is polled once with a no-op waker.
940    /// That settles it outright for every synchronous recorder and for the
941    /// in-process task store, whose futures never yield.
942    ///
943    /// A durable store performs real I/O, so its first poll pends. That work is
944    /// parked on [`DeferredCompletions`] to be driven from somewhere that can
945    /// await, because the paths that pend are the ones carrying durable-task
946    /// attribution: abandoning them loses the charge for every durable task.
947    fn drop(&mut self) {
948        let mut context = Context::from_waker(std::task::Waker::noop());
949        let unfinished = if let Some(mut finishing) = self.finishing.take() {
950            finishing
951                .as_mut()
952                .poll(&mut context)
953                .is_pending()
954                .then_some(finishing)
955        } else if let Some(mut finishing) = self.start_completion() {
956            finishing
957                .as_mut()
958                .poll(&mut context)
959                .is_pending()
960                .then_some(finishing)
961        } else {
962            None
963        };
964        if let Some(finishing) = unfinished {
965            self.metrics.deferred();
966            if !self.deferred.park(finishing) {
967                // The queue was full, so the oldest accounting was discarded.
968                self.metrics.record_failure();
969            }
970        }
971    }
972}
973
974impl<B> Body for ObservedBody<B>
975where
976    B: Body<Data = Bytes>,
977{
978    type Data = Bytes;
979    type Error = B::Error;
980
981    fn poll_frame(
982        mut self: Pin<&mut Self>,
983        cx: &mut Context<'_>,
984    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
985        if let Some(finishing) = self.finishing.as_mut() {
986            return match finishing.as_mut().poll(cx) {
987                Poll::Ready(()) => {
988                    self.finishing = None;
989                    Poll::Ready(None)
990                }
991                Poll::Pending => Poll::Pending,
992            };
993        }
994        match self.inner.as_mut().poll_frame(cx) {
995            Poll::Ready(Some(Ok(frame))) => {
996                if let Some(data) = frame.data_ref()
997                    && !self.overflowed
998                {
999                    let limit = self
1000                        .completion
1001                        .as_ref()
1002                        .map_or(0, |completion| completion.config.max_response_capture);
1003                    if self.captured.len().saturating_add(data.remaining()) <= limit {
1004                        self.captured.extend_from_slice(data.chunk());
1005                    } else {
1006                        self.captured.clear();
1007                        self.overflowed = true;
1008                    }
1009                }
1010                Poll::Ready(Some(Ok(frame)))
1011            }
1012            Poll::Ready(None) => {
1013                if let Some(mut finishing) = self.start_completion() {
1014                    return match finishing.as_mut().poll(cx) {
1015                        Poll::Ready(()) => Poll::Ready(None),
1016                        Poll::Pending => {
1017                            self.finishing = Some(finishing);
1018                            Poll::Pending
1019                        }
1020                    };
1021                }
1022                Poll::Ready(None)
1023            }
1024            other => other,
1025        }
1026    }
1027
1028    fn is_end_stream(&self) -> bool {
1029        self.inner.is_end_stream()
1030    }
1031
1032    fn size_hint(&self) -> http_body::SizeHint {
1033        self.inner.size_hint()
1034    }
1035}
1036
1037/// A JSON-RPC response, if these bytes are one.
1038///
1039/// Both transports apply the same test, because "terminal" means a JSON-RPC
1040/// response and not merely well-formed JSON. A bare `0` is valid JSON and is not
1041/// a response to anything.
1042fn is_json_rpc_response(value: &Value) -> bool {
1043    value.get("result").is_some() || value.get("error").is_some()
1044}
1045
1046pub(crate) fn terminal_response(content_type: &str, bytes: &[u8]) -> Option<Value> {
1047    if has_media_type(content_type, "application/json") {
1048        return serde_json::from_slice(bytes)
1049            .ok()
1050            .filter(is_json_rpc_response);
1051    }
1052    if !has_media_type(content_type, "text/event-stream") {
1053        return None;
1054    }
1055    let text = std::str::from_utf8(bytes)
1056        .ok()?
1057        .replace("\r\n", "\n")
1058        .replace('\r', "\n");
1059    let mut terminal = None;
1060    for event in text.split("\n\n") {
1061        let data = event
1062            .lines()
1063            .filter_map(|line| line.strip_prefix("data:").map(str::trim_start))
1064            .collect::<Vec<_>>()
1065            .join("\n");
1066        if data.is_empty() {
1067            continue;
1068        }
1069        if let Ok(value) = serde_json::from_str::<Value>(&data)
1070            && is_json_rpc_response(&value)
1071        {
1072            terminal = Some(value);
1073        }
1074    }
1075    terminal
1076}
1077
1078fn has_media_type(content_type: &str, expected: &str) -> bool {
1079    content_type
1080        .split(';')
1081        .next()
1082        .is_some_and(|media_type| media_type.trim().eq_ignore_ascii_case(expected))
1083}
1084
1085#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1086enum RequestBodyError {
1087    TooLarge,
1088    Read,
1089}
1090
1091async fn collect_request_body<B>(body: B, limit: usize) -> Result<Bytes, RequestBodyError>
1092where
1093    B: Body<Data = Bytes>,
1094{
1095    let initial_capacity = usize::try_from(body.size_hint().lower())
1096        .unwrap_or(limit)
1097        .min(limit);
1098    let mut captured = BytesMut::with_capacity(initial_capacity);
1099    let mut body = Box::pin(body);
1100    while let Some(frame) = body.as_mut().frame().await {
1101        let frame = frame.map_err(|_| RequestBodyError::Read)?;
1102        let Ok(data) = frame.into_data() else {
1103            continue;
1104        };
1105        let Some(next_len) = captured.len().checked_add(data.len()) else {
1106            return Err(RequestBodyError::TooLarge);
1107        };
1108        if next_len > limit {
1109            return Err(RequestBodyError::TooLarge);
1110        }
1111        captured.extend_from_slice(&data);
1112    }
1113    Ok(captured.freeze())
1114}
1115
1116/// Internals reachable from the crate's property tests.
1117#[cfg(test)]
1118pub(crate) mod testing {
1119    pub(crate) use super::terminal_response;
1120}
1121
1122#[cfg(test)]
1123mod tests {
1124    use super::*;
1125    use crate::{InMemoryTenantStore, Tenant};
1126    use mcp_usage_core::PriceBook;
1127    use mcp_usage_export::{
1128        AggregatedUsage, BatchExporter, BillingPipeline, ExportFuture, RecordError, UsageRecorder,
1129    };
1130    use tower::{ServiceBuilder, ServiceExt, service_fn};
1131
1132    #[derive(Debug, Default)]
1133    struct CaptureExporter {
1134        exported: std::sync::Mutex<Vec<AggregatedUsage>>,
1135    }
1136
1137    impl CaptureExporter {
1138        fn exported(&self) -> Vec<AggregatedUsage> {
1139            self.exported
1140                .lock()
1141                .unwrap_or_else(std::sync::PoisonError::into_inner)
1142                .clone()
1143        }
1144    }
1145
1146    impl BatchExporter for CaptureExporter {
1147        fn export<'a>(&'a self, batch: &'a [AggregatedUsage]) -> ExportFuture<'a> {
1148            Box::pin(async move {
1149                self.exported
1150                    .lock()
1151                    .unwrap_or_else(std::sync::PoisonError::into_inner)
1152                    .extend_from_slice(batch);
1153                Ok(())
1154            })
1155        }
1156    }
1157
1158    #[derive(Debug, Default)]
1159    struct RejectOnceRecorder {
1160        rejected: std::sync::atomic::AtomicBool,
1161        recorded: std::sync::Mutex<Vec<UsageEvent>>,
1162    }
1163
1164    impl UsageRecorder for RejectOnceRecorder {
1165        fn record(&self, event: UsageEvent) -> Result<RecordOutcome, RecordError> {
1166            if !self
1167                .rejected
1168                .swap(true, std::sync::atomic::Ordering::SeqCst)
1169            {
1170                return Err(RecordError::Poisoned);
1171            }
1172            self.recorded
1173                .lock()
1174                .unwrap_or_else(std::sync::PoisonError::into_inner)
1175                .push(event);
1176            Ok(RecordOutcome::Recorded)
1177        }
1178    }
1179
1180    fn request(method: &str, name: Option<&str>, body: &Value, key: &str) -> Request<Full<Bytes>> {
1181        let mut body = body.clone();
1182        if let Some(object) = body.as_object_mut() {
1183            object
1184                .entry("jsonrpc")
1185                .or_insert_with(|| Value::String("2.0".to_owned()));
1186            object
1187                .entry("method")
1188                .or_insert_with(|| Value::String(method.to_owned()));
1189        }
1190        let mut builder = Request::builder()
1191            .header(PROTOCOL_VERSION_HEADER, "2026-07-28")
1192            .header(METHOD_HEADER, method)
1193            .header(crate::API_KEY_HEADER, key);
1194        if let Some(name) = name {
1195            builder = builder.header(NAME_HEADER, name);
1196        }
1197        builder
1198            .body(Full::new(Bytes::from(body.to_string())))
1199            .unwrap()
1200    }
1201
1202    fn response(body: &Value) -> Response<Full<Bytes>> {
1203        Response::builder()
1204            .header(CONTENT_TYPE, "application/json")
1205            .body(Full::new(Bytes::from(serde_json::to_vec(&body).unwrap())))
1206            .unwrap()
1207    }
1208
1209    async fn consume(response: Response<MeterBody>) -> Bytes {
1210        response.into_body().collect().await.unwrap().to_bytes()
1211    }
1212
1213    /// Release a body the way a transport does once `Content-Length` is met:
1214    /// take the data, then drop without ever polling to end-of-stream.
1215    fn release_without_end_of_stream(response: Response<MeterBody>) {
1216        let mut body = Box::pin(response.into_body());
1217        let mut context = Context::from_waker(std::task::Waker::noop());
1218        let _ = body.as_mut().poll_frame(&mut context);
1219        drop(body);
1220    }
1221
1222    /// A future that pends exactly once, the way a store doing I/O behaves.
1223    struct YieldOnce(bool);
1224
1225    impl Future for YieldOnce {
1226        type Output = ();
1227        fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
1228            if self.0 {
1229                Poll::Ready(())
1230            } else {
1231                self.0 = true;
1232                context.waker().wake_by_ref();
1233                Poll::Pending
1234            }
1235        }
1236    }
1237
1238    /// A task store that yields before answering, standing in for Redis or
1239    /// `PostgreSQL` without requiring either to be running.
1240    #[derive(Debug, Default)]
1241    struct YieldingTaskStore(InMemoryTaskStore);
1242
1243    impl TaskAttributionStore for YieldingTaskStore {
1244        fn insert<'a>(
1245            &'a self,
1246            tenant_id: &'a str,
1247            task_id: &'a str,
1248            attribution: TaskAttribution,
1249        ) -> crate::TaskStoreFuture<'a, ()> {
1250            Box::pin(async move {
1251                YieldOnce(false).await;
1252                self.0.insert(tenant_id, task_id, attribution).await
1253            })
1254        }
1255        fn get<'a>(
1256            &'a self,
1257            tenant_id: &'a str,
1258            task_id: &'a str,
1259        ) -> crate::TaskStoreFuture<'a, Option<TaskAttribution>> {
1260            Box::pin(async move {
1261                YieldOnce(false).await;
1262                self.0.get(tenant_id, task_id).await
1263            })
1264        }
1265        fn claim<'a>(
1266            &'a self,
1267            tenant_id: &'a str,
1268            task_id: &'a str,
1269        ) -> crate::TaskStoreFuture<'a, Option<TaskAttribution>> {
1270            Box::pin(async move {
1271                YieldOnce(false).await;
1272                self.0.claim(tenant_id, task_id).await
1273            })
1274        }
1275        fn remove<'a>(
1276            &'a self,
1277            tenant_id: &'a str,
1278            task_id: &'a str,
1279        ) -> crate::TaskStoreFuture<'a, ()> {
1280            Box::pin(async move {
1281                YieldOnce(false).await;
1282                self.0.remove(tenant_id, task_id).await
1283            })
1284        }
1285    }
1286
1287    /// Drive a `tools/call` that creates a task, then a `tasks/get` that reports
1288    /// it completed, releasing both bodies the way a real transport does.
1289    async fn run_task_lifecycle<S>(service: S)
1290    where
1291        S: tower::Service<Request<Full<Bytes>>, Response = Response<MeterBody>, Error = Infallible>
1292            + Clone
1293            + Send
1294            + 'static,
1295        S::Future: Send,
1296    {
1297        release_without_end_of_stream(
1298            service
1299                .clone()
1300                .oneshot(request(
1301                    "tools/call",
1302                    Some("long_job"),
1303                    &json!({"id":1,"params":{"name":"long_job"}}),
1304                    "secret",
1305                ))
1306                .await
1307                .unwrap(),
1308        );
1309        release_without_end_of_stream(
1310            service
1311                .oneshot(request(
1312                    "tasks/get",
1313                    None,
1314                    &json!({"id":2,"params":{"taskId":"task-1"}}),
1315                    "secret",
1316                ))
1317                .await
1318                .unwrap(),
1319        );
1320    }
1321
1322    fn task_lifecycle_service(
1323        config: EdgeConfig,
1324    ) -> impl tower::Service<
1325        Request<Full<Bytes>>,
1326        Response = Response<MeterBody>,
1327        Error = Infallible,
1328        Future = impl Send,
1329    > + Clone
1330    + Send
1331    + 'static {
1332        let stage = Arc::new(std::sync::atomic::AtomicU64::new(0));
1333        ServiceBuilder::new()
1334            .layer(MeterLayer::new(config))
1335            .service(service_fn(move |_request: Request<Full<Bytes>>| {
1336                let n = stage.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1337                async move {
1338                    let result = if n == 0 {
1339                        json!({"resultType":"task","taskId":"task-1","status":"working"})
1340                    } else {
1341                        json!({"resultType":"complete","taskId":"task-1","status":"completed","result":{}})
1342                    };
1343                    Ok::<_, Infallible>(response(&json!({"jsonrpc":"2.0","id":n,"result":result})))
1344                }
1345            }))
1346    }
1347
1348    fn task_tenants() -> Arc<InMemoryTenantStore> {
1349        let tenants = Arc::new(InMemoryTenantStore::new());
1350        tenants.insert_unchecked(
1351            "secret",
1352            Tenant::new("acme", "cus_acme")
1353                .with_prices(PriceBook::flat(1).with_name("long_job", 50)),
1354        );
1355        tenants
1356    }
1357
1358    #[tokio::test]
1359    async fn a_store_that_yields_is_parked_rather_than_abandoned() {
1360        // A durable store performs I/O, so its future pends on the single poll
1361        // `Drop` can give it. Before this was parked, every durable-task charge
1362        // was lost: the attribution insert never landed, so the completing poll
1363        // had nothing to price.
1364        let config = EdgeConfig::new(task_tenants())
1365            .with_task_store(Arc::new(YieldingTaskStore::default()))
1366            .with_deferred_drain_per_request(0);
1367        let metrics = config.metrics();
1368        let deferred = config.deferred();
1369
1370        run_task_lifecycle(task_lifecycle_service(config)).await;
1371
1372        assert_eq!(
1373            metrics.snapshot().billed,
1374            0,
1375            "nothing can be billed while the store has not answered"
1376        );
1377        assert!(
1378            !deferred.is_empty(),
1379            "the unfinished accounting must be parked"
1380        );
1381        assert_eq!(deferred.dropped(), 0);
1382
1383        // Draining is the context that can await, so this is where it lands.
1384        assert!(deferred.drain().await > 0);
1385
1386        let snapshot = metrics.snapshot();
1387        assert_eq!(snapshot.billed, 1, "the completed task must be charged");
1388        assert_eq!(
1389            snapshot.billed_units, 50,
1390            "priced from the originating call"
1391        );
1392        assert_eq!(snapshot.record_failures, 0);
1393        assert!(deferred.is_empty());
1394    }
1395
1396    #[tokio::test]
1397    async fn a_later_request_drains_parked_accounting_on_its_own() {
1398        // An application that never calls `drain` must still converge, so every
1399        // request runs a bounded amount of parked work.
1400        let config =
1401            EdgeConfig::new(task_tenants()).with_task_store(Arc::new(YieldingTaskStore::default()));
1402        let metrics = config.metrics();
1403        let deferred = config.deferred();
1404        let service = task_lifecycle_service(config);
1405
1406        run_task_lifecycle(service.clone()).await;
1407        assert_eq!(metrics.snapshot().billed, 0);
1408
1409        // Ordinary unrelated traffic, with no drain call anywhere.
1410        for id in 3..8 {
1411            release_without_end_of_stream(
1412                service
1413                    .clone()
1414                    .oneshot(request(
1415                        "tools/list",
1416                        None,
1417                        &json!({"id":id,"params":{}}),
1418                        "secret",
1419                    ))
1420                    .await
1421                    .unwrap(),
1422            );
1423        }
1424
1425        assert_eq!(
1426            metrics.snapshot().billed,
1427            1,
1428            "subsequent requests must complete the parked accounting"
1429        );
1430        assert_eq!(metrics.snapshot().billed_units, 50);
1431        assert!(deferred.is_empty());
1432    }
1433
1434    #[tokio::test]
1435    async fn malformed_and_unauthenticated_requests_do_not_poll_deferred_work() {
1436        let tenants = Arc::new(InMemoryTenantStore::new());
1437        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
1438        let config = EdgeConfig::new(tenants);
1439        let deferred = config.deferred();
1440        let polled = Arc::new(std::sync::atomic::AtomicUsize::new(0));
1441        assert!(deferred.park(Box::pin({
1442            let polled = Arc::clone(&polled);
1443            async move {
1444                polled.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1445            }
1446        })));
1447        let service = ServiceBuilder::new()
1448            .layer(MeterLayer::new(config))
1449            .service(service_fn(|_request: Request<Full<Bytes>>| async {
1450                Ok::<_, Infallible>(response(&json!({
1451                    "jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}
1452                })))
1453            }));
1454
1455        let mut malformed = request("tools/list", None, &json!({"id":1,"params":{}}), "secret");
1456        malformed.headers_mut().insert(
1457            PROTOCOL_VERSION_HEADER,
1458            http::HeaderValue::from_static("invalid"),
1459        );
1460        assert_eq!(
1461            service.clone().oneshot(malformed).await.unwrap().status(),
1462            StatusCode::BAD_REQUEST
1463        );
1464        assert_eq!(polled.load(std::sync::atomic::Ordering::Relaxed), 0);
1465
1466        assert_eq!(
1467            service
1468                .clone()
1469                .oneshot(request(
1470                    "tools/list",
1471                    None,
1472                    &json!({"id":2,"params":{}}),
1473                    "wrong",
1474                ))
1475                .await
1476                .unwrap()
1477                .status(),
1478            StatusCode::UNAUTHORIZED
1479        );
1480        assert_eq!(polled.load(std::sync::atomic::Ordering::Relaxed), 0);
1481        assert_eq!(deferred.len(), 1);
1482
1483        consume(
1484            service
1485                .oneshot(request(
1486                    "tools/list",
1487                    None,
1488                    &json!({"id":3,"params":{}}),
1489                    "secret",
1490                ))
1491                .await
1492                .unwrap(),
1493        )
1494        .await;
1495        assert_eq!(polled.load(std::sync::atomic::Ordering::Relaxed), 1);
1496        assert!(deferred.is_empty());
1497    }
1498
1499    #[tokio::test]
1500    async fn recorder_rejection_restores_claimed_task_origin_for_a_later_poll() {
1501        let tasks = Arc::new(InMemoryTaskStore::new());
1502        let recorder = Arc::new(RejectOnceRecorder::default());
1503        let metrics_config = EdgeConfig::new(task_tenants())
1504            .with_task_store(tasks.clone())
1505            .with_recorder(recorder.clone());
1506        let metrics = metrics_config.metrics();
1507        let service = task_lifecycle_service(metrics_config);
1508
1509        release_without_end_of_stream(
1510            service
1511                .clone()
1512                .oneshot(request(
1513                    "tools/call",
1514                    Some("long_job"),
1515                    &json!({"id":1,"params":{"name":"long_job"}}),
1516                    "secret",
1517                ))
1518                .await
1519                .unwrap(),
1520        );
1521        release_without_end_of_stream(
1522            service
1523                .clone()
1524                .oneshot(request(
1525                    "tasks/get",
1526                    None,
1527                    &json!({"id":2,"params":{"taskId":"task-1"}}),
1528                    "secret",
1529                ))
1530                .await
1531                .unwrap(),
1532        );
1533        assert_eq!(
1534            tasks.len(),
1535            1,
1536            "the rejected charge must restore its origin"
1537        );
1538        assert!(recorder.recorded.lock().unwrap().is_empty());
1539
1540        release_without_end_of_stream(
1541            service
1542                .oneshot(request(
1543                    "tasks/get",
1544                    None,
1545                    &json!({"id":3,"params":{"taskId":"task-1"}}),
1546                    "secret",
1547                ))
1548                .await
1549                .unwrap(),
1550        );
1551        assert!(tasks.is_empty());
1552        let captured = recorder.recorded.lock().unwrap();
1553        assert_eq!(captured.len(), 1);
1554        assert_eq!(captured[0].units, 50);
1555        let snapshot = metrics.snapshot();
1556        assert_eq!(snapshot.billed, 1);
1557        assert_eq!(snapshot.record_failures, 1);
1558    }
1559
1560    #[tokio::test]
1561    async fn missing_completed_task_id_preserves_origin_for_a_later_valid_poll() {
1562        let tasks = Arc::new(InMemoryTaskStore::new());
1563        let config = EdgeConfig::new(task_tenants()).with_task_store(tasks.clone());
1564        let metrics = config.metrics();
1565        let stage = Arc::new(std::sync::atomic::AtomicU64::new(0));
1566        let service = ServiceBuilder::new()
1567            .layer(MeterLayer::new(config))
1568            .service(service_fn(move |_request: Request<Full<Bytes>>| {
1569                let n = stage.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1570                async move {
1571                    let result = match n {
1572                        0 => json!({
1573                            "resultType":"task",
1574                            "taskId":"task-1",
1575                            "status":"working"
1576                        }),
1577                        1 => json!({
1578                            "resultType":"complete",
1579                            "status":"completed",
1580                            "result":{}
1581                        }),
1582                        _ => json!({
1583                            "resultType":"complete",
1584                            "taskId":"task-1",
1585                            "status":"completed",
1586                            "result":{}
1587                        }),
1588                    };
1589                    Ok::<_, Infallible>(response(&json!({
1590                        "jsonrpc":"2.0",
1591                        "id":n,
1592                        "result":result
1593                    })))
1594                }
1595            }));
1596
1597        release_without_end_of_stream(
1598            service
1599                .clone()
1600                .oneshot(request(
1601                    "tools/call",
1602                    Some("long_job"),
1603                    &json!({"id":1,"params":{"name":"long_job"}}),
1604                    "secret",
1605                ))
1606                .await
1607                .unwrap(),
1608        );
1609        release_without_end_of_stream(
1610            service
1611                .clone()
1612                .oneshot(request(
1613                    "tasks/get",
1614                    None,
1615                    &json!({"id":2,"params":{"taskId":"task-1"}}),
1616                    "secret",
1617                ))
1618                .await
1619                .unwrap(),
1620        );
1621
1622        assert_eq!(
1623            tasks.len(),
1624            1,
1625            "the malformed poll must not consume the origin"
1626        );
1627        assert_eq!(metrics.snapshot().billed, 0);
1628
1629        release_without_end_of_stream(
1630            service
1631                .oneshot(request(
1632                    "tasks/get",
1633                    None,
1634                    &json!({"id":3,"params":{"taskId":"task-1"}}),
1635                    "secret",
1636                ))
1637                .await
1638                .unwrap(),
1639        );
1640
1641        assert!(tasks.is_empty());
1642        let snapshot = metrics.snapshot();
1643        assert_eq!(snapshot.billed, 1);
1644        assert_eq!(snapshot.billed_units, 50);
1645    }
1646
1647    #[tokio::test]
1648    async fn a_synchronous_store_never_parks_anything() {
1649        // The default in-process store answers without yielding, so the common
1650        // deployment must not pay for the queue at all.
1651        let config = EdgeConfig::new(task_tenants());
1652        let metrics = config.metrics();
1653        let deferred = config.deferred();
1654
1655        run_task_lifecycle(task_lifecycle_service(config)).await;
1656
1657        let snapshot = metrics.snapshot();
1658        assert_eq!(snapshot.billed, 1);
1659        assert_eq!(snapshot.billed_units, 50);
1660        assert_eq!(snapshot.deferred, 0, "nothing should have been parked");
1661        assert!(deferred.is_empty());
1662    }
1663
1664    /// A legacy client sends no mirrored headers at all. Before the body fallback the
1665    /// edge refused it outright, which shut out every client older than the mirrored
1666    /// headers - including the default configuration of MCP Inspector.
1667    #[tokio::test]
1668    async fn a_legacy_client_is_metered_from_its_body() {
1669        let tenants = Arc::new(InMemoryTenantStore::new());
1670        tenants.insert_unchecked(
1671            "secret",
1672            Tenant::new("acme", "cus_acme")
1673                .with_prices(PriceBook::flat(1).with_name("expensive", 25)),
1674        );
1675        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
1676        let config = EdgeConfig::new(tenants).with_recorder(billing.clone());
1677        let service = ServiceBuilder::new()
1678            .layer(MeterLayer::new(config))
1679            .service(service_fn(|_request: Request<Full<Bytes>>| async {
1680                Ok::<_, Infallible>(response(&json!({
1681                    "jsonrpc":"2.0","id":1,
1682                    "result":{"resultType":"complete","content":[]}
1683                })))
1684            }));
1685
1686        // No MCP-Protocol-Version, no Mcp-Method, no Mcp-Name: only a JSON-RPC body.
1687        let legacy = Request::builder()
1688            .header(crate::API_KEY_HEADER, "secret")
1689            .body(Full::new(Bytes::from(
1690                json!({"jsonrpc":"2.0","id":1,"method":"tools/call",
1691                       "params":{"name":"expensive"}})
1692                .to_string(),
1693            )))
1694            .unwrap();
1695
1696        let response = service.oneshot(legacy).await.unwrap();
1697        assert_eq!(response.status(), StatusCode::OK);
1698        consume(response).await;
1699        billing.flush().await.unwrap();
1700        assert_eq!(
1701            billing.exporter().exported()[0].units,
1702            25,
1703            "the body named the expensive tool, so the expensive price applies"
1704        );
1705    }
1706
1707    /// The trap the fallback opens if nobody looks: a legacy server answers without a
1708    /// `resultType`, which the charge logic reads as unrecognized and bills as free.
1709    /// Admitting legacy clients without this would have handed out unlimited free usage.
1710    #[tokio::test]
1711    async fn a_legacy_response_without_a_result_type_is_still_billed() {
1712        let tenants = Arc::new(InMemoryTenantStore::new());
1713        tenants.insert_unchecked(
1714            "secret",
1715            Tenant::new("acme", "cus_acme").with_prices(PriceBook::flat(7)),
1716        );
1717        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
1718        let config = EdgeConfig::new(tenants).with_recorder(billing.clone());
1719        let service = ServiceBuilder::new()
1720            .layer(MeterLayer::new(config))
1721            .service(service_fn(|_request: Request<Full<Bytes>>| async {
1722                // No resultType: exactly what a server answering a legacy client sends.
1723                Ok::<_, Infallible>(response(&json!({
1724                    "jsonrpc":"2.0","id":1,"result":{"content":[]}
1725                })))
1726            }));
1727
1728        let legacy = Request::builder()
1729            .header(crate::API_KEY_HEADER, "secret")
1730            .body(Full::new(Bytes::from(
1731                json!({"jsonrpc":"2.0","id":1,"method":"tools/call",
1732                       "params":{"name":"anything"}})
1733                .to_string(),
1734            )))
1735            .unwrap();
1736
1737        let response = service.oneshot(legacy).await.unwrap();
1738        consume(response).await;
1739        billing.flush().await.unwrap();
1740        assert_eq!(
1741            billing.exporter().exported()[0].units,
1742            7,
1743            "a legacy result is delivered work and must be billed"
1744        );
1745    }
1746
1747    /// The same absence on a modern response must stay free: there it means the meter
1748    /// did not understand the result, not that work was delivered.
1749    #[tokio::test]
1750    async fn a_modern_response_without_a_result_type_is_still_free() {
1751        let tenants = Arc::new(InMemoryTenantStore::new());
1752        tenants.insert_unchecked(
1753            "secret",
1754            Tenant::new("acme", "cus_acme").with_prices(PriceBook::flat(7)),
1755        );
1756        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
1757        let config = EdgeConfig::new(tenants).with_recorder(billing.clone());
1758        let service = ServiceBuilder::new()
1759            .layer(MeterLayer::new(config))
1760            .service(service_fn(|_request: Request<Full<Bytes>>| async {
1761                Ok::<_, Infallible>(response(&json!({
1762                    "jsonrpc":"2.0","id":1,"result":{"content":[]}
1763                })))
1764            }));
1765
1766        let response = service
1767            .oneshot(request(
1768                "tools/call",
1769                Some("anything"),
1770                &json!({"id":1,"params":{"name":"anything"}}),
1771                "secret",
1772            ))
1773            .await
1774            .unwrap();
1775        consume(response).await;
1776        billing.flush().await.unwrap();
1777        assert!(
1778            billing.exporter().exported().is_empty(),
1779            "an unrecognized modern result must not be billed"
1780        );
1781    }
1782
1783    /// The reason pricing on a legacy body is safe, stated as a test: the name is read
1784    /// from the document the origin executes. Claiming a cheap tool in the only place
1785    /// the meter looks means calling the cheap tool.
1786    #[tokio::test]
1787    async fn a_legacy_client_cannot_underpay_by_lying_about_the_tool() {
1788        let tenants = Arc::new(InMemoryTenantStore::new());
1789        tenants.insert_unchecked(
1790            "secret",
1791            Tenant::new("acme", "cus_acme")
1792                .with_prices(PriceBook::flat(1).with_name("expensive", 25)),
1793        );
1794        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
1795        let config = EdgeConfig::new(tenants).with_recorder(billing.clone());
1796        let seen: Arc<std::sync::Mutex<Option<String>>> = Arc::new(std::sync::Mutex::new(None));
1797        let observed = seen.clone();
1798        let service = ServiceBuilder::new()
1799            .layer(MeterLayer::new(config))
1800            .service(service_fn(move |request: Request<Full<Bytes>>| {
1801                let observed = observed.clone();
1802                async move {
1803                    let bytes = request.into_body().collect().await.unwrap().to_bytes();
1804                    let body: Value = serde_json::from_slice(&bytes).unwrap();
1805                    *observed.lock().unwrap() =
1806                        Some(body["params"]["name"].as_str().unwrap().into());
1807                    Ok::<_, Infallible>(response(&json!({
1808                        "jsonrpc":"2.0","id":1,
1809                        "result":{"resultType":"complete","content":[]}
1810                    })))
1811                }
1812            }));
1813
1814        // Mirrored headers claim the cheap call; the body asks for the expensive one.
1815        // On a trusted revision the origin would reject the disagreement. Here the
1816        // headers are ignored entirely, so the body decides both price and execution.
1817        let lying = Request::builder()
1818            .header(crate::API_KEY_HEADER, "secret")
1819            .header(METHOD_HEADER, "tools/list")
1820            .header(NAME_HEADER, "cheap")
1821            .body(Full::new(Bytes::from(
1822                json!({"jsonrpc":"2.0","id":1,"method":"tools/call",
1823                       "params":{"name":"expensive"}})
1824                .to_string(),
1825            )))
1826            .unwrap();
1827
1828        let response = service.oneshot(lying).await.unwrap();
1829        assert_eq!(response.status(), StatusCode::OK);
1830        consume(response).await;
1831        billing.flush().await.unwrap();
1832        assert_eq!(
1833            billing.exporter().exported()[0].units,
1834            25,
1835            "billed for what the origin ran, not what the headers claimed"
1836        );
1837        assert_eq!(
1838            seen.lock().unwrap().as_deref(),
1839            Some("expensive"),
1840            "the origin ran the tool the body named"
1841        );
1842    }
1843
1844    /// Strict deployments keep the old behaviour.
1845    #[tokio::test]
1846    async fn strict_mode_still_refuses_legacy_clients() {
1847        let tenants = Arc::new(InMemoryTenantStore::new());
1848        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
1849        let config = EdgeConfig::new(tenants).with_strict_protocol_version(true);
1850        let service = ServiceBuilder::new()
1851            .layer(MeterLayer::new(config))
1852            .service(service_fn(|_request: Request<Full<Bytes>>| async {
1853                Ok::<_, Infallible>(response(&json!({
1854                    "jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}
1855                })))
1856            }));
1857
1858        let legacy = Request::builder()
1859            .header(crate::API_KEY_HEADER, "secret")
1860            .body(Full::new(Bytes::from(
1861                json!({"jsonrpc":"2.0","id":1,"method":"tools/list"}).to_string(),
1862            )))
1863            .unwrap();
1864        assert_eq!(
1865            service.oneshot(legacy).await.unwrap().status(),
1866            StatusCode::BAD_REQUEST
1867        );
1868    }
1869
1870    /// The fallback must not become a way to spend server CPU without a credential.
1871    #[tokio::test]
1872    async fn an_unauthenticated_legacy_request_is_refused_before_its_body_is_read() {
1873        let tenants = Arc::new(InMemoryTenantStore::new());
1874        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
1875        let config = EdgeConfig::new(tenants);
1876        let service = ServiceBuilder::new()
1877            .layer(MeterLayer::new(config))
1878            .service(service_fn(|_request: Request<Full<Bytes>>| async {
1879                Ok::<_, Infallible>(response(&json!({
1880                    "jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}
1881                })))
1882            }));
1883
1884        // Unparseable body, no credential: the credential gate must answer first.
1885        let anonymous = Request::builder()
1886            .body(Full::new(Bytes::from_static(b"{{{ not json")))
1887            .unwrap();
1888        assert_eq!(
1889            service.oneshot(anonymous).await.unwrap().status(),
1890            StatusCode::UNAUTHORIZED,
1891            "an anonymous caller must not reach the JSON parser"
1892        );
1893    }
1894
1895    #[tokio::test]
1896    async fn complete_tool_call_is_recorded_after_body_consumption() {
1897        let tenants = Arc::new(InMemoryTenantStore::new());
1898        tenants.insert_unchecked(
1899            "secret",
1900            Tenant::new("acme", "cus_acme")
1901                .with_prices(PriceBook::flat(1).with_name("expensive", 25)),
1902        );
1903        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
1904        let config = EdgeConfig::new(tenants).with_recorder(billing.clone());
1905        let service = ServiceBuilder::new()
1906            .layer(MeterLayer::new(config))
1907            .service(service_fn(|_request: Request<Full<Bytes>>| async {
1908                Ok::<_, Infallible>(response(&json!({
1909                    "jsonrpc":"2.0","id":1,
1910                    "result":{"resultType":"complete","content":[]}
1911                })))
1912            }));
1913
1914        let response = service
1915            .oneshot(request(
1916                "tools/call",
1917                Some("expensive"),
1918                &json!({"jsonrpc":"2.0","id":1,"params":{"name":"expensive"}}),
1919                "secret",
1920            ))
1921            .await
1922            .unwrap();
1923        assert_eq!(billing.pending_buckets(), 0, "not billed before delivery");
1924        consume(response).await;
1925        assert_eq!(billing.pending_buckets(), 1);
1926        billing.flush().await.unwrap();
1927        assert_eq!(billing.exporter().exported()[0].units, 25);
1928    }
1929
1930    #[tokio::test]
1931    async fn private_cache_never_crosses_authorization_contexts_and_rewrites_response_id() {
1932        let tenants = Arc::new(InMemoryTenantStore::new());
1933        tenants.insert_unchecked("key-a", Tenant::new("a", "cus_a"));
1934        // Even two credentials mapped to the same tenant are distinct
1935        // authorization contexts for private-cache purposes.
1936        tenants.insert_unchecked("key-b", Tenant::new("a", "cus_a"));
1937        let calls = Arc::new(std::sync::atomic::AtomicU64::new(0));
1938        let inner_calls = calls.clone();
1939        let config = EdgeConfig::new(tenants);
1940        let service = ServiceBuilder::new()
1941            .layer(MeterLayer::new(config))
1942            .service(service_fn(move |_request: Request<Full<Bytes>>| {
1943                inner_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1944                async {
1945                    Ok::<_, Infallible>(response(&json!({
1946                        "jsonrpc":"2.0","id":1,
1947                        "result":{"resultType":"complete","tools":[],"ttlMs":60000,"cacheScope":"private"}
1948                    })))
1949                }
1950            }));
1951
1952        let first = service
1953            .clone()
1954            .oneshot(request(
1955                "tools/list",
1956                None,
1957                &json!({"jsonrpc":"2.0","id":1,"params":{}}),
1958                "key-a",
1959            ))
1960            .await
1961            .unwrap();
1962        consume(first).await;
1963
1964        let hit = service
1965            .clone()
1966            .oneshot(request(
1967                "tools/list",
1968                None,
1969                &json!({"jsonrpc":"2.0","id":99,"params":{}}),
1970                "key-a",
1971            ))
1972            .await
1973            .unwrap();
1974        assert_eq!(hit.headers()["x-mcp-usage-cache"], "hit");
1975        let hit_body: Value = serde_json::from_slice(&consume(hit).await).unwrap();
1976        assert_eq!(hit_body["id"], 99);
1977
1978        let other = service
1979            .oneshot(request(
1980                "tools/list",
1981                None,
1982                &json!({"jsonrpc":"2.0","id":2,"params":{}}),
1983                "key-b",
1984            ))
1985            .await
1986            .unwrap();
1987        consume(other).await;
1988        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 2);
1989    }
1990
1991    #[tokio::test]
1992    async fn cache_hits_for_billable_resources_are_still_metered_on_delivery() {
1993        let tenants = Arc::new(InMemoryTenantStore::new());
1994        tenants.insert_unchecked(
1995            "secret",
1996            Tenant::new("acme", "cus_acme")
1997                .with_prices(PriceBook::flat(1).with_name("file:///report", 4)),
1998        );
1999        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
2000        let calls = Arc::new(std::sync::atomic::AtomicU64::new(0));
2001        let inner_calls = calls.clone();
2002        let service = ServiceBuilder::new()
2003            .layer(MeterLayer::new(
2004                EdgeConfig::new(tenants).with_recorder(billing.clone()),
2005            ))
2006            .service(service_fn(move |_request: Request<Full<Bytes>>| {
2007                inner_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2008                async {
2009                    Ok::<_, Infallible>(response(&json!({
2010                        "jsonrpc":"2.0","id":1,
2011                        "result":{
2012                            "resultType":"complete",
2013                            "contents":[],
2014                            "ttlMs":60000,
2015                            "cacheScope":"private"
2016                        }
2017                    })))
2018                }
2019            }));
2020
2021        for id in [1, 2] {
2022            consume(
2023                service
2024                    .clone()
2025                    .oneshot(request(
2026                        "resources/read",
2027                        Some("file:///report"),
2028                        &json!({"jsonrpc":"2.0","id":id,"params":{"uri":"file:///report"}}),
2029                        "secret",
2030                    ))
2031                    .await
2032                    .unwrap(),
2033            )
2034            .await;
2035        }
2036        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 1);
2037        billing.flush().await.unwrap();
2038        assert_eq!(billing.exporter().exported()[0].units, 8);
2039    }
2040
2041    #[tokio::test]
2042    async fn trace_context_reaches_the_rmcp_service_unchanged() {
2043        let tenants = Arc::new(InMemoryTenantStore::new());
2044        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
2045        let seen = Arc::new(std::sync::atomic::AtomicBool::new(false));
2046        let inner_seen = seen.clone();
2047        let service = ServiceBuilder::new()
2048            .layer(MeterLayer::new(EdgeConfig::new(tenants)))
2049            .service(service_fn(move |request: Request<Full<Bytes>>| {
2050                inner_seen.store(
2051                    request.headers().get("traceparent").is_some_and(|value| {
2052                        value == "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
2053                    }),
2054                    std::sync::atomic::Ordering::Relaxed,
2055                );
2056                async {
2057                    Ok::<_, Infallible>(response(&json!({
2058                        "jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}
2059                    })))
2060                }
2061            }));
2062        let mut request = request(
2063            "tools/call",
2064            Some("tool"),
2065            &json!({"id":1,"params":{"name":"tool"}}),
2066            "secret",
2067        );
2068        request.headers_mut().insert(
2069            "traceparent",
2070            http::HeaderValue::from_static(
2071                "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
2072            ),
2073        );
2074        consume(service.oneshot(request).await.unwrap()).await;
2075        assert!(seen.load(std::sync::atomic::Ordering::Relaxed));
2076    }
2077
2078    #[tokio::test]
2079    async fn task_completion_uses_origin_price_and_repeat_poll_is_free() {
2080        let tenants = Arc::new(InMemoryTenantStore::new());
2081        tenants.insert_unchecked(
2082            "secret",
2083            Tenant::new("acme", "cus_acme")
2084                .with_prices(PriceBook::flat(1).with_name("long_job", 50)),
2085        );
2086        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
2087        let stage = Arc::new(std::sync::atomic::AtomicU64::new(0));
2088        let inner_stage = stage.clone();
2089        let service = ServiceBuilder::new()
2090            .layer(MeterLayer::new(
2091                EdgeConfig::new(tenants).with_recorder(billing.clone()),
2092            ))
2093            .service(service_fn(move |_request: Request<Full<Bytes>>| {
2094                let n = inner_stage.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2095                async move {
2096                    let result = if n == 0 {
2097                        json!({"resultType":"task","taskId":"task-1","status":"working"})
2098                    } else {
2099                        json!({"resultType":"complete","taskId":"task-1","status":"completed","result":{}})
2100                    };
2101                    Ok::<_, Infallible>(response(&json!({"jsonrpc":"2.0","id":n,"result":result})))
2102                }
2103            }));
2104
2105        consume(
2106            service
2107                .clone()
2108                .oneshot(request(
2109                    "tools/call",
2110                    Some("long_job"),
2111                    &json!({"id":1,"params":{"name":"long_job"}}),
2112                    "secret",
2113                ))
2114                .await
2115                .unwrap(),
2116        )
2117        .await;
2118        for id in [2, 3] {
2119            consume(
2120                service
2121                    .clone()
2122                    .oneshot(request(
2123                        "tasks/get",
2124                        None,
2125                        &json!({"id":id,"params":{"taskId":"task-1"}}),
2126                        "secret",
2127                    ))
2128                    .await
2129                    .unwrap(),
2130            )
2131            .await;
2132        }
2133        billing.flush().await.unwrap();
2134        let exported = billing.exporter().exported();
2135        assert_eq!(exported.len(), 1);
2136        assert_eq!(exported[0].units, 50);
2137    }
2138
2139    #[tokio::test]
2140    async fn mismatched_task_response_cannot_bill_another_tasks_origin() {
2141        let tenants = Arc::new(InMemoryTenantStore::new());
2142        tenants.insert_unchecked(
2143            "secret",
2144            Tenant::new("acme", "cus_acme")
2145                .with_prices(PriceBook::flat(1).with_name("expensive", 100)),
2146        );
2147        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
2148        let stage = Arc::new(std::sync::atomic::AtomicU64::new(0));
2149        let inner_stage = stage.clone();
2150        let service = ServiceBuilder::new()
2151            .layer(MeterLayer::new(
2152                EdgeConfig::new(tenants).with_recorder(billing.clone()),
2153            ))
2154            .service(service_fn(move |_request: Request<Full<Bytes>>| {
2155                let n = inner_stage.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2156                async move {
2157                    let result = match n {
2158                        0 => json!({"resultType":"task","taskId":"task-1","status":"working"}),
2159                        1 => json!({"resultType":"task","taskId":"task-2","status":"working"}),
2160                        _ => json!({
2161                            "resultType":"complete",
2162                            "taskId":"task-2",
2163                            "status":"completed",
2164                            "result":{}
2165                        }),
2166                    };
2167                    Ok::<_, Infallible>(response(&json!({"jsonrpc":"2.0","id":n,"result":result})))
2168                }
2169            }));
2170
2171        for (name, id) in [("cheap", 1), ("expensive", 2)] {
2172            consume(
2173                service
2174                    .clone()
2175                    .oneshot(request(
2176                        "tools/call",
2177                        Some(name),
2178                        &json!({"id":id,"params":{"name":name}}),
2179                        "secret",
2180                    ))
2181                    .await
2182                    .unwrap(),
2183            )
2184            .await;
2185        }
2186        consume(
2187            service
2188                .oneshot(request(
2189                    "tasks/get",
2190                    None,
2191                    &json!({"id":3,"params":{"taskId":"task-1"}}),
2192                    "secret",
2193                ))
2194                .await
2195                .unwrap(),
2196        )
2197        .await;
2198        assert_eq!(billing.flush().await.unwrap(), 0);
2199    }
2200
2201    #[tokio::test]
2202    async fn accounting_runs_when_the_transport_stops_polling_early() {
2203        // A transport is not obliged to poll a body to end-of-stream. hyper
2204        // stops as soon as the declared Content-Length has been written, so
2205        // `poll_frame` never returns `Poll::Ready(None)` for an ordinary
2206        // fixed-length JSON result. Terminal accounting must survive that:
2207        // dropping the body is the only signal every transport gives us.
2208        //
2209        // This deliberately drives the body by hand rather than through
2210        // `BodyExt::collect`, which always polls to `None` and would hide the
2211        // regression this test exists to catch.
2212        let tenants = Arc::new(InMemoryTenantStore::new());
2213        tenants.insert_unchecked(
2214            "secret",
2215            Tenant::new("acme", "cus_acme").with_prices(PriceBook::flat(1).with_name("tool", 7)),
2216        );
2217        let billing = Arc::new(BillingPipeline::new(CaptureExporter::default()));
2218        let config = EdgeConfig::new(tenants).with_recorder(billing.clone());
2219        let metrics = config.metrics();
2220        let service = ServiceBuilder::new()
2221            .layer(MeterLayer::new(config))
2222            .service(service_fn(|_request: Request<Full<Bytes>>| async {
2223                Ok::<_, Infallible>(response(&json!({
2224                    "jsonrpc":"2.0","id":1,
2225                    "result":{"resultType":"complete","content":[]}
2226                })))
2227            }));
2228
2229        let response = service
2230            .oneshot(request(
2231                "tools/call",
2232                Some("tool"),
2233                &json!({"id":1,"params":{"name":"tool"}}),
2234                "secret",
2235            ))
2236            .await
2237            .unwrap();
2238
2239        let mut body = Box::pin(response.into_body());
2240        let mut context = Context::from_waker(std::task::Waker::noop());
2241        // Take the single data frame and then stop, exactly as a transport does
2242        // once it has satisfied Content-Length.
2243        assert!(
2244            matches!(
2245                body.as_mut().poll_frame(&mut context),
2246                Poll::Ready(Some(Ok(_)))
2247            ),
2248            "expected one data frame"
2249        );
2250        assert_eq!(
2251            metrics.snapshot().billed,
2252            0,
2253            "nothing should be billed before the body is released"
2254        );
2255
2256        drop(body);
2257
2258        let snapshot = metrics.snapshot();
2259        assert_eq!(
2260            snapshot.billed, 1,
2261            "the delivery must still be accounted for"
2262        );
2263        assert_eq!(snapshot.billed_units, 7);
2264        assert_eq!(snapshot.record_failures, 0);
2265        assert_eq!(billing.pending_buckets(), 1);
2266    }
2267
2268    #[tokio::test]
2269    async fn a_fully_consumed_body_is_accounted_for_exactly_once() {
2270        // The guard against the Drop path double-counting what `poll_frame`
2271        // already recorded.
2272        let tenants = Arc::new(InMemoryTenantStore::new());
2273        tenants.insert_unchecked(
2274            "secret",
2275            Tenant::new("acme", "cus_acme").with_prices(PriceBook::flat(1).with_name("tool", 5)),
2276        );
2277        let config = EdgeConfig::new(tenants);
2278        let metrics = config.metrics();
2279        let service = ServiceBuilder::new()
2280            .layer(MeterLayer::new(config))
2281            .service(service_fn(|_request: Request<Full<Bytes>>| async {
2282                Ok::<_, Infallible>(response(&json!({
2283                    "jsonrpc":"2.0","id":1,
2284                    "result":{"resultType":"complete","content":[]}
2285                })))
2286            }));
2287
2288        consume(
2289            service
2290                .oneshot(request(
2291                    "tools/call",
2292                    Some("tool"),
2293                    &json!({"id":1,"params":{"name":"tool"}}),
2294                    "secret",
2295                ))
2296                .await
2297                .unwrap(),
2298        )
2299        .await;
2300
2301        let snapshot = metrics.snapshot();
2302        assert_eq!(snapshot.billed, 1);
2303        assert_eq!(snapshot.billed_units, 5);
2304    }
2305
2306    #[tokio::test]
2307    async fn public_results_cross_tenants_only_when_sharing_is_enabled() {
2308        // Same scenario as the private-scope test above, but with the origin
2309        // declaring `cacheScope: "public"`. The origin call count is the
2310        // observable: two calls means tenant B was served from the origin, one
2311        // means it was served tenant A's cached body.
2312        for (share, expected_origin_calls) in [(false, 2), (true, 1)] {
2313            let tenants = Arc::new(InMemoryTenantStore::new());
2314            tenants.insert_unchecked("key-a", Tenant::new("a", "cus_a"));
2315            tenants.insert_unchecked("key-b", Tenant::new("b", "cus_b"));
2316            let calls = Arc::new(std::sync::atomic::AtomicU64::new(0));
2317            let inner_calls = calls.clone();
2318            let service = ServiceBuilder::new()
2319                .layer(MeterLayer::new(
2320                    EdgeConfig::new(tenants).with_public_cache_sharing(share),
2321                ))
2322                .service(service_fn(move |_request: Request<Full<Bytes>>| {
2323                    inner_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2324                    async {
2325                        Ok::<_, Infallible>(response(&json!({
2326                            "jsonrpc":"2.0","id":1,
2327                            "result":{
2328                                "resultType":"complete","tools":[],
2329                                "ttlMs":60000,"cacheScope":"public"
2330                            }
2331                        })))
2332                    }
2333                }));
2334
2335            for key in ["key-a", "key-b"] {
2336                consume(
2337                    service
2338                        .clone()
2339                        .oneshot(request(
2340                            "tools/list",
2341                            None,
2342                            &json!({"jsonrpc":"2.0","id":1,"params":{}}),
2343                            key,
2344                        ))
2345                        .await
2346                        .unwrap(),
2347                )
2348                .await;
2349            }
2350
2351            assert_eq!(
2352                calls.load(std::sync::atomic::Ordering::Relaxed),
2353                expected_origin_calls,
2354                "with_public_cache_sharing({share})"
2355            );
2356        }
2357    }
2358
2359    #[tokio::test]
2360    async fn the_consumed_credential_does_not_reach_the_inner_service() {
2361        let tenants = Arc::new(InMemoryTenantStore::new());
2362        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
2363
2364        for (forward, expected) in [(false, false), (true, true)] {
2365            for bearer in [false, true] {
2366                let seen = Arc::new(std::sync::atomic::AtomicBool::new(false));
2367                let inner_seen = seen.clone();
2368                let service = ServiceBuilder::new()
2369                    .layer(MeterLayer::new(
2370                        EdgeConfig::new(tenants.clone()).with_credential_forwarding(forward),
2371                    ))
2372                    .service(service_fn(move |request: Request<Full<Bytes>>| {
2373                        let headers = request.headers();
2374                        inner_seen.store(
2375                            headers.contains_key(crate::API_KEY_HEADER)
2376                                || headers.contains_key(AUTHORIZATION),
2377                            std::sync::atomic::Ordering::Relaxed,
2378                        );
2379                        async {
2380                            Ok::<_, Infallible>(response(&json!({
2381                                "jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}
2382                            })))
2383                        }
2384                    }));
2385
2386                let mut call = request(
2387                    "tools/call",
2388                    Some("tool"),
2389                    &json!({"id":1,"params":{"name":"tool"}}),
2390                    "secret",
2391                );
2392                if bearer {
2393                    // Exactly one credential header may be present.
2394                    call.headers_mut().remove(crate::API_KEY_HEADER);
2395                    call.headers_mut().insert(
2396                        AUTHORIZATION,
2397                        http::HeaderValue::from_static("Bearer secret"),
2398                    );
2399                }
2400                consume(service.oneshot(call).await.unwrap()).await;
2401
2402                assert_eq!(
2403                    seen.load(std::sync::atomic::Ordering::Relaxed),
2404                    expected,
2405                    "forwarding={forward} bearer={bearer}"
2406                );
2407            }
2408        }
2409    }
2410
2411    #[tokio::test]
2412    async fn generated_responses_pin_their_content_type_and_stay_out_of_shared_caches() {
2413        let tenants = Arc::new(InMemoryTenantStore::new());
2414        tenants.insert_unchecked("key-a", Tenant::new("a", "cus_a"));
2415        let service = ServiceBuilder::new()
2416            .layer(MeterLayer::new(EdgeConfig::new(tenants)))
2417            .service(service_fn(|_request: Request<Full<Bytes>>| async {
2418                Ok::<_, Infallible>(response(&json!({
2419                    "jsonrpc":"2.0","id":1,
2420                    "result":{"resultType":"complete","tools":[],"ttlMs":60000,"cacheScope":"private"}
2421                })))
2422            }));
2423
2424        // The rejection path echoes the offending header value back to the caller.
2425        let mut malformed = request("tools/list", None, &json!({"id":1,"params":{}}), "key-a");
2426        malformed.headers_mut().insert(
2427            PROTOCOL_VERSION_HEADER,
2428            http::HeaderValue::from_static("<not-a-date>"),
2429        );
2430        let rejected = service.clone().oneshot(malformed).await.unwrap();
2431        assert_eq!(rejected.status(), StatusCode::BAD_REQUEST);
2432        assert_eq!(rejected.headers()[X_CONTENT_TYPE_OPTIONS], "nosniff");
2433        assert_eq!(rejected.headers()[CACHE_CONTROL], "no-store");
2434
2435        consume(
2436            service
2437                .clone()
2438                .oneshot(request(
2439                    "tools/list",
2440                    None,
2441                    &json!({"id":1,"params":{}}),
2442                    "key-a",
2443                ))
2444                .await
2445                .unwrap(),
2446        )
2447        .await;
2448        let hit = service
2449            .oneshot(request(
2450                "tools/list",
2451                None,
2452                &json!({"id":2,"params":{}}),
2453                "key-a",
2454            ))
2455            .await
2456            .unwrap();
2457        assert_eq!(hit.headers()["x-mcp-usage-cache"], "hit");
2458        assert_eq!(hit.headers()[X_CONTENT_TYPE_OPTIONS], "nosniff");
2459        assert_eq!(hit.headers()[CACHE_CONTROL], "private");
2460        consume(hit).await;
2461    }
2462
2463    #[tokio::test]
2464    async fn sustained_guessing_is_refused_without_affecting_valid_keys() {
2465        let tenants = Arc::new(InMemoryTenantStore::new());
2466        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
2467        let config = EdgeConfig::new(tenants)
2468            .with_auth_failure_limit(3, std::time::Duration::from_secs(60))
2469            .unwrap();
2470        let metrics = config.metrics();
2471        let service = ServiceBuilder::new()
2472            .layer(MeterLayer::new(config))
2473            .service(service_fn(|_request: Request<Full<Bytes>>| async {
2474                Ok::<_, Infallible>(response(&json!({
2475                    "jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}
2476                })))
2477            }));
2478
2479        let guess = |key: &'static str| {
2480            let service = service.clone();
2481            async move {
2482                service
2483                    .oneshot(request(
2484                        "tools/list",
2485                        None,
2486                        &json!({"id":1,"params":{}}),
2487                        key,
2488                    ))
2489                    .await
2490                    .unwrap()
2491                    .status()
2492            }
2493        };
2494
2495        for _ in 0..3 {
2496            assert_eq!(guess("wrong").await, StatusCode::UNAUTHORIZED);
2497        }
2498        assert_eq!(
2499            guess("wrong").await,
2500            StatusCode::TOO_MANY_REQUESTS,
2501            "sustained guessing must stop being answered"
2502        );
2503
2504        // A valid key never consumed the budget, so it is still served.
2505        let allowed = service
2506            .clone()
2507            .oneshot(request(
2508                "tools/list",
2509                None,
2510                &json!({"id":9,"params":{}}),
2511                "secret",
2512            ))
2513            .await
2514            .unwrap();
2515        assert_eq!(
2516            allowed.status(),
2517            StatusCode::OK,
2518            "throttling guessers must not lock out legitimate callers"
2519        );
2520        consume(allowed).await;
2521
2522        let snapshot = metrics.snapshot();
2523        assert_eq!(snapshot.unauthenticated, 3);
2524        assert_eq!(snapshot.throttled, 1);
2525    }
2526
2527    #[tokio::test]
2528    async fn credential_failures_are_counted_apart_from_header_failures() {
2529        let tenants = Arc::new(InMemoryTenantStore::new());
2530        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
2531        let config = EdgeConfig::new(tenants);
2532        let metrics = config.metrics();
2533        let service = ServiceBuilder::new()
2534            .layer(MeterLayer::new(config))
2535            .service(service_fn(|_request: Request<Full<Bytes>>| async {
2536                Ok::<_, Infallible>(response(&json!({"result": {}})))
2537            }));
2538
2539        let unknown_key = service
2540            .clone()
2541            .oneshot(request(
2542                "tools/list",
2543                None,
2544                &json!({"id":1,"params":{}}),
2545                "wrong",
2546            ))
2547            .await
2548            .unwrap();
2549        assert_eq!(unknown_key.status(), StatusCode::UNAUTHORIZED);
2550
2551        // Classification runs before authentication, so this never reaches the
2552        // credential gate and must land on the other counter. A malformed version is
2553        // the case that still rejects on headers alone: a legacy one now falls back to
2554        // the body instead.
2555        let mut malformed = request("tools/list", None, &json!({"id":1,"params":{}}), "secret");
2556        malformed.headers_mut().insert(
2557            PROTOCOL_VERSION_HEADER,
2558            http::HeaderValue::from_static("not-a-date"),
2559        );
2560        assert_eq!(
2561            service.oneshot(malformed).await.unwrap().status(),
2562            StatusCode::BAD_REQUEST
2563        );
2564
2565        let snapshot = metrics.snapshot();
2566        assert_eq!(snapshot.unauthenticated, 1);
2567        assert_eq!(snapshot.rejected, 1);
2568    }
2569
2570    #[test]
2571    fn the_bearer_scheme_is_matched_case_insensitively() {
2572        // RFC 7235 makes the scheme name case-insensitive, so all of these are
2573        // the same credential and none of them is a wrong key.
2574        for header in [
2575            "Bearer secret",
2576            "bearer secret",
2577            "BEARER secret",
2578            "BeArEr secret",
2579            // RFC 7235 allows more than one space before the token.
2580            "Bearer   secret",
2581        ] {
2582            assert_eq!(bearer_token(header), Some("secret"), "{header}");
2583        }
2584
2585        // A different scheme is still not a bearer token.
2586        for header in ["Basic secret", "Bearersecret", "secret", "", " secret"] {
2587            assert_ne!(bearer_token(header), Some("secret"), "{header}");
2588        }
2589    }
2590
2591    #[tokio::test]
2592    async fn a_lowercase_bearer_credential_authenticates() {
2593        let tenants = Arc::new(InMemoryTenantStore::new());
2594        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
2595        let service = ServiceBuilder::new()
2596            .layer(MeterLayer::new(EdgeConfig::new(tenants)))
2597            .service(service_fn(|_request: Request<Full<Bytes>>| async {
2598                Ok::<_, Infallible>(response(&json!({
2599                    "jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}
2600                })))
2601            }));
2602
2603        let mut call = request("tools/list", None, &json!({"id":1,"params":{}}), "secret");
2604        call.headers_mut().remove(crate::API_KEY_HEADER);
2605        call.headers_mut().insert(
2606            AUTHORIZATION,
2607            http::HeaderValue::from_static("bearer secret"),
2608        );
2609
2610        assert_eq!(
2611            service.oneshot(call).await.unwrap().status(),
2612            StatusCode::OK
2613        );
2614    }
2615
2616    #[test]
2617    fn zero_authentication_failure_window_is_rejected() {
2618        let tenants = Arc::new(InMemoryTenantStore::new());
2619        assert_eq!(
2620            EdgeConfig::new(tenants)
2621                .with_auth_failure_limit(3, Duration::ZERO)
2622                .unwrap_err(),
2623            EdgeConfigError::InvalidAuthFailureWindow
2624        );
2625    }
2626
2627    #[test]
2628    fn extracts_terminal_response_from_sse_without_confusing_notifications() {
2629        let stream = b"data: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/progress\"}\n\n\
2630                       : keepalive\n\n\
2631                       data: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"resultType\":\"complete\"}}\n\n";
2632        let terminal = terminal_response("text/event-stream", stream).unwrap();
2633        assert_eq!(terminal["id"], 1);
2634    }
2635
2636    #[test]
2637    fn extracts_terminal_response_from_sse_with_mixed_line_endings() {
2638        let stream = b"data: {\"jsonrpc\":\"2.0\",\"method\":\"notifications/progress\"}\r\r\
2639                       : keepalive\r\n\r\n\
2640                       data: {\"jsonrpc\":\"2.0\",\"id\":7,\r\
2641                       data: \"result\":{\"resultType\":\"complete\"}}\n\n";
2642        let terminal = terminal_response("text/event-stream", stream).unwrap();
2643        assert_eq!(terminal["id"], 7);
2644    }
2645
2646    #[test]
2647    fn well_formed_json_that_is_not_a_response_is_not_terminal() {
2648        // Found by the property test in `properties.rs`. `application/json` used
2649        // to accept anything that parsed, so a bare scalar counted as a terminal
2650        // response while the SSE path required a JSON-RPC shape. Both now apply
2651        // the same test. Nothing was billable either way, but the two transports
2652        // classified the same body differently.
2653        for body in [
2654            b"0".as_slice(),
2655            b"null".as_slice(),
2656            b"\"a string\"".as_slice(),
2657            b"[]".as_slice(),
2658            b"{}".as_slice(),
2659            br#"{"jsonrpc":"2.0","id":1}"#.as_slice(),
2660        ] {
2661            assert!(
2662                terminal_response("application/json", body).is_none(),
2663                "{} is not a JSON-RPC response",
2664                String::from_utf8_lossy(body)
2665            );
2666        }
2667
2668        // The shapes that are responses still are.
2669        assert!(
2670            terminal_response(
2671                "application/json",
2672                br#"{"jsonrpc":"2.0","id":1,"result":{"resultType":"complete"}}"#,
2673            )
2674            .is_some()
2675        );
2676        assert!(
2677            terminal_response(
2678                "application/json",
2679                br#"{"jsonrpc":"2.0","id":1,"error":{"code":-32020}}"#,
2680            )
2681            .is_some()
2682        );
2683    }
2684
2685    #[test]
2686    fn content_type_matching_requires_an_exact_media_type() {
2687        assert!(has_media_type(
2688            "Application/JSON; charset=utf-8",
2689            "application/json"
2690        ));
2691        assert!(!has_media_type("application/jsonp", "application/json"));
2692        assert!(terminal_response("application/jsonp", b"{}").is_none());
2693    }
2694
2695    #[tokio::test]
2696    async fn oversized_request_bodies_are_rejected_before_the_origin() {
2697        let tenants = Arc::new(InMemoryTenantStore::new());
2698        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
2699        let calls = Arc::new(std::sync::atomic::AtomicU64::new(0));
2700        let inner_calls = calls.clone();
2701        let service = ServiceBuilder::new()
2702            .layer(MeterLayer::new(
2703                EdgeConfig::new(tenants).with_max_request_body(16),
2704            ))
2705            .service(service_fn(move |_request: Request<Full<Bytes>>| {
2706                inner_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2707                async { Ok::<_, Infallible>(response(&json!({"result": {}}))) }
2708            }));
2709
2710        let response = service
2711            .oneshot(request(
2712                "tools/call",
2713                Some("tool"),
2714                &json!({"id":1,"params":{"name":"tool","payload":"too large"}}),
2715                "secret",
2716            ))
2717            .await
2718            .unwrap();
2719        assert_eq!(response.status(), StatusCode::PAYLOAD_TOO_LARGE);
2720        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 0);
2721    }
2722
2723    #[tokio::test]
2724    async fn ambiguous_or_duplicate_security_headers_are_rejected() {
2725        let tenants = Arc::new(InMemoryTenantStore::new());
2726        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
2727        let service = ServiceBuilder::new()
2728            .layer(MeterLayer::new(EdgeConfig::new(tenants)))
2729            .service(service_fn(|_request: Request<Full<Bytes>>| async {
2730                Ok::<_, Infallible>(response(&json!({"result": {}})))
2731            }));
2732
2733        let mut ambiguous = request("tools/list", None, &json!({"id":1,"params":{}}), "secret");
2734        ambiguous.headers_mut().insert(
2735            AUTHORIZATION,
2736            http::HeaderValue::from_static("Bearer secret"),
2737        );
2738        assert_eq!(
2739            service.clone().oneshot(ambiguous).await.unwrap().status(),
2740            StatusCode::UNAUTHORIZED
2741        );
2742
2743        let mut duplicate = request("tools/list", None, &json!({"id":1,"params":{}}), "secret");
2744        duplicate
2745            .headers_mut()
2746            .append(METHOD_HEADER, http::HeaderValue::from_static("tools/list"));
2747        assert_eq!(
2748            service.oneshot(duplicate).await.unwrap().status(),
2749            StatusCode::BAD_REQUEST
2750        );
2751    }
2752
2753    #[tokio::test]
2754    async fn a_header_body_mismatch_cannot_use_the_cache() {
2755        let tenants = Arc::new(InMemoryTenantStore::new());
2756        tenants.insert_unchecked("secret", Tenant::new("acme", "cus_acme"));
2757        let calls = Arc::new(std::sync::atomic::AtomicU64::new(0));
2758        let inner_calls = calls.clone();
2759        let service = ServiceBuilder::new()
2760            .layer(MeterLayer::new(EdgeConfig::new(tenants)))
2761            .service(service_fn(move |_request: Request<Full<Bytes>>| {
2762                inner_calls.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2763                async {
2764                    Ok::<_, Infallible>(response(&json!({
2765                        "id":1,
2766                        "result":{
2767                            "resultType":"complete",
2768                            "tools":[],
2769                            "ttlMs":60000,
2770                            "cacheScope":"private"
2771                        }
2772                    })))
2773                }
2774            }));
2775
2776        consume(
2777            service
2778                .clone()
2779                .oneshot(request(
2780                    "tools/list",
2781                    None,
2782                    &json!({"id":1,"params":{}}),
2783                    "secret",
2784                ))
2785                .await
2786                .unwrap(),
2787        )
2788        .await;
2789        consume(
2790            service
2791                .oneshot(request(
2792                    "tools/list",
2793                    None,
2794                    &json!({"id":2,"method":"resources/list","params":{}}),
2795                    "secret",
2796                ))
2797                .await
2798                .unwrap(),
2799        )
2800        .await;
2801        assert_eq!(calls.load(std::sync::atomic::Ordering::Relaxed), 2);
2802    }
2803}