Skip to content

Middleware Cookbook

Jeffrie Budde edited this page Nov 25, 2025 · 1 revision

Middleware Cookbook

Cross-cutting concerns for FastRMCP servers. See Core Concepts for registration order.

Built-ins

  • LoggingMiddleware: with_middleware(LoggingMiddleware::new()) (variants: .requests_only(), .responses_only()).
  • AuthMiddleware: AuthMiddleware::bearer_token("secret") (simplified pending metadata refactor). Use alongside custom auth for production.
  • RateLimitMiddleware: RateLimitMiddleware::new(100, Duration::from_secs(60)) (simplified; per-client planned).

Using Middleware

use fastrmcp::middleware::{AuthMiddleware, LoggingMiddleware, RateLimitMiddleware};
use fastrmcp::prelude::*;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<()> {
    let server = FastMCP::new("secured")
        .with_middleware(LoggingMiddleware::new())          // runs first before_request, last after_response
        .with_middleware(RateLimitMiddleware::new(100, Duration::from_secs(60)))
        .with_middleware(AuthMiddleware::bearer_token("secret"));
    server.run().await
}
  • Order matters: first added = outermost. Put logging first, auth/rate limiting next, business middleware last.
  • Metadata sharing: set values in before_request via context.set_metadata("user", json!("alice")).await; and read them later.

Custom Middleware

use async_trait::async_trait;
use fastrmcp_core::{
    context::Context,
    error::{MCPError, Result},
    middleware::Middleware,
    protocol::{JsonRpcRequest, JsonRpcResponse},
};
use serde_json::json;

pub struct Audit;

#[async_trait]
impl Middleware for Audit {
    async fn before_request(&self, request: &JsonRpcRequest, context: &mut Context) -> Result<()> {
        context.set_metadata("request_method", json!(request.method)).await;
        // Example auth: read bearer token from params.metadata or params._meta
        if let Some(token) = request
            .params
            .as_ref()
            .and_then(|p| p.get("metadata").or_else(|| p.get("_meta")))
            .and_then(|m| m.get("auth").or_else(|| m.get("Authorization")))
            .and_then(|v| v.as_str())
        {
            context.set_metadata("auth_token", json!(token)).await;
        }
        Ok(())
    }

    async fn after_response(&self, _req: &JsonRpcRequest, resp: &mut JsonRpcResponse, ctx: &Context) -> Result<()> {
        if let Some(method) = ctx.get_metadata("request_method").await {
            tracing::info!(method=?method, "completed");
        }
        // mutate response if needed
        Ok(())
    }

    async fn on_error(&self, req: &JsonRpcRequest, err: &MCPError, _ctx: &Context) -> Result<()> {
        tracing::warn!(method=%req.method, error=%err, "request failed");
        Ok(())
    }

    fn name(&self) -> &str { "Audit" }
}

Patterns & Tips

  • Auth: In before_request, validate tokens/headers from request.params["metadata"] and return MCPError::InvalidRequest("unauthorized") on failure.
  • Rate limiting: store counters in middleware struct (e.g., Arc<DashMap>). Keep hot paths non-blocking.
  • Response shaping: Use after_response to attach metadata or strip sensitive fields; remember reverse execution order.
  • Error handling: Decide whether to block (Err(...)) or log and continue. Be explicit to avoid silent drops.
  • Testing: Middleware runs for every request; keep operations async and fast to avoid head-of-line blocking.

Related: Security for auth/rate-limit guidance • Observability for tracing fields.

Back to Home

Clone this wiki locally