Skip to main content

RequestPolicy

Trait RequestPolicy 

Source
pub trait RequestPolicy: Send + Sync {
    // Required method
    fn check<'life0, 'life1, 'life2, 'async_trait>(
        &'life0 self,
        req: &'life1 OutboundRequest<'life2>,
    ) -> Pin<Box<dyn Future<Output = Result<(), PolicyRefusal>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait,
             'life1: 'async_trait,
             'life2: 'async_trait;
}
Expand description

A rule about what may LEAVE the server, consulted before every outbound backend request on both HTTP surfaces (Phase 128, E1 / D-12).

§Where it runs

Between the base-URL join and the auth application, on the curated single-call surface (http::HttpClient::execute) and on the Code Mode / script-tool surface (code_mode::HttpCodeExecutor::execute_request). The position is what makes the D-12 guarantee structural: the request is fully assembled, and the credential does not exist yet. A refusal returns before the auth provider is called and before anything is sent.

§One invocation per LOGICAL request, not per wire attempt

The curated client’s send_with_retries retries the ALREADY-BUILT request up to three times on a 5xx / connect / timeout. Those retries happen after the hook, so this trait gives exactly one invocation per logical outbound request. A policy counting requests against a rate budget is therefore counting LOGICAL requests; it will under-count wire attempts.

§What E1 does and does not govern

It governs the two HTTP egress surfaces named above. It does NOT intercept SQL connector traffic: a SqlConnector request is a statement plus bound parameters, not a method/path/query, so it needs a different seam and a different trait. A team writing a PHI policy must know that its coverage stops at HTTP egress.

Redirects are governed by construction rather than by this trait: the OpenAPI binary’s shared client is built with reqwest::redirect::Policy::none(), so a redirect surfaces as a response the caller handles rather than as a hop inside the client that this hook never saw (T-128-39a). A client built elsewhere with reqwest’s default redirect policy re-opens that gap — every hop after the first would be invisible here.

§Concurrency and latency

check takes &self and the trait requires Send + Sync, so ONE instance serves every concurrent request. Any per-session state is the implementation’s own responsibility. check is async and the toolkit cannot bound third-party code: a slow policy delays the request path (T-128-44, accepted — the per-request time budget is a separate, deferred piece of work).

§Example

use pmcp_server_toolkit::{async_trait, OutboundRequest, PolicyRefusal, RequestPolicy};

struct AllowlistPrefix(&'static str);

#[async_trait]
impl RequestPolicy for AllowlistPrefix {
    async fn check(&self, req: &OutboundRequest<'_>) -> Result<(), PolicyRefusal> {
        if req.path.starts_with(self.0) {
            Ok(())
        } else {
            // A FIXED message: it names the rule, never the request.
            Err(PolicyRefusal::new("outbound endpoint is not on the allowlist"))
        }
    }
}

Required Methods§

Source

fn check<'life0, 'life1, 'life2, 'async_trait>( &'life0 self, req: &'life1 OutboundRequest<'life2>, ) -> Pin<Box<dyn Future<Output = Result<(), PolicyRefusal>> + Send + 'async_trait>>
where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait,

Allow or refuse one outbound backend request.

§Errors

Return PolicyRefusal to refuse. The request is then never authenticated and never sent, and the refusal’s message is surfaced to the MCP client — so it must carry no byte of the rejected request.

Dyn Compatibility§

This trait is dyn compatible.

In older versions of Rust, dyn compatibility was called "object safety".

Implementors§