use super::{DispatcherResult, McpMiddleware, MiddlewareError, RequestContext, SessionInjection};
use std::sync::Arc;
use turul_mcp_session_storage::SessionView;
#[derive(Default, Clone)]
pub struct MiddlewareStack {
middleware: Vec<Arc<dyn McpMiddleware>>,
}
impl MiddlewareStack {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, middleware: Arc<dyn McpMiddleware>) {
self.middleware.push(middleware);
}
pub fn len(&self) -> usize {
self.middleware.len()
}
pub fn is_empty(&self) -> bool {
self.middleware.is_empty()
}
pub fn has_pre_session_middleware(&self) -> bool {
self.middleware.iter().any(|m| m.runs_before_session())
}
pub async fn execute_before_session(
&self,
ctx: &mut RequestContext<'_>,
) -> Result<(), MiddlewareError> {
for middleware in &self.middleware {
if middleware.runs_before_session() {
let mut injection = SessionInjection::new();
middleware
.before_dispatch(ctx, None, &mut injection)
.await?;
}
}
Ok(())
}
pub async fn execute_before(
&self,
ctx: &mut RequestContext<'_>,
session: Option<&dyn SessionView>,
) -> Result<SessionInjection, MiddlewareError> {
let mut combined_injection = SessionInjection::new();
for middleware in &self.middleware {
if middleware.runs_before_session() {
continue;
}
let mut injection = SessionInjection::new();
middleware
.before_dispatch(ctx, session, &mut injection)
.await?;
for (key, value) in injection.state() {
combined_injection.set_state(key.clone(), value.clone());
}
for (key, value) in injection.metadata() {
combined_injection.set_metadata(key.clone(), value.clone());
}
}
Ok(combined_injection)
}
pub async fn execute_after(
&self,
ctx: &RequestContext<'_>,
result: &mut DispatcherResult,
) -> Result<(), MiddlewareError> {
for middleware in self.middleware.iter().rev() {
middleware.after_dispatch(ctx, result).await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use serde_json::json;
struct CountingMiddleware {
id: String,
counter: Arc<std::sync::Mutex<Vec<String>>>,
}
#[async_trait]
impl McpMiddleware for CountingMiddleware {
async fn before_dispatch(
&self,
_ctx: &mut RequestContext<'_>,
_session: Option<&dyn SessionView>,
injection: &mut SessionInjection,
) -> Result<(), MiddlewareError> {
self.counter
.lock()
.unwrap()
.push(format!("before_{}", self.id));
injection.set_state(&self.id, json!(true));
Ok(())
}
async fn after_dispatch(
&self,
_ctx: &RequestContext<'_>,
_result: &mut DispatcherResult,
) -> Result<(), MiddlewareError> {
self.counter
.lock()
.unwrap()
.push(format!("after_{}", self.id));
Ok(())
}
}
struct ErrorMiddleware {
error_on_before: bool,
}
#[async_trait]
impl McpMiddleware for ErrorMiddleware {
async fn before_dispatch(
&self,
_ctx: &mut RequestContext<'_>,
_session: Option<&dyn SessionView>,
_injection: &mut SessionInjection,
) -> Result<(), MiddlewareError> {
if self.error_on_before {
Err(MiddlewareError::unauthorized("Test error"))
} else {
Ok(())
}
}
}
#[tokio::test]
async fn test_middleware_execution_order() {
let counter = Arc::new(std::sync::Mutex::new(Vec::new()));
let mut stack = MiddlewareStack::new();
stack.push(Arc::new(CountingMiddleware {
id: "first".to_string(),
counter: counter.clone(),
}));
stack.push(Arc::new(CountingMiddleware {
id: "second".to_string(),
counter: counter.clone(),
}));
let mut ctx = RequestContext::new("test/method", None);
let injection = stack.execute_before(&mut ctx, None).await.unwrap();
assert_eq!(injection.state().len(), 2);
assert!(injection.state().contains_key("first"));
assert!(injection.state().contains_key("second"));
let mut result = DispatcherResult::Success(json!({"ok": true}));
stack.execute_after(&ctx, &mut result).await.unwrap();
let log = counter.lock().unwrap();
assert_eq!(log[0], "before_first");
assert_eq!(log[1], "before_second");
assert_eq!(log[2], "after_second"); assert_eq!(log[3], "after_first");
}
#[tokio::test]
async fn test_middleware_error_stops_chain() {
let counter = Arc::new(std::sync::Mutex::new(Vec::new()));
let mut stack = MiddlewareStack::new();
stack.push(Arc::new(CountingMiddleware {
id: "first".to_string(),
counter: counter.clone(),
}));
stack.push(Arc::new(ErrorMiddleware {
error_on_before: true,
}));
stack.push(Arc::new(CountingMiddleware {
id: "third".to_string(),
counter: counter.clone(),
}));
let mut ctx = RequestContext::new("test/method", None);
let result = stack.execute_before(&mut ctx, None).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
MiddlewareError::unauthorized("Test error")
);
let log = counter.lock().unwrap();
assert_eq!(log.len(), 1);
assert_eq!(log[0], "before_first");
}
struct ToolFilteringMiddleware;
#[async_trait]
impl McpMiddleware for ToolFilteringMiddleware {
async fn before_dispatch(
&self,
_ctx: &mut RequestContext<'_>,
_session: Option<&dyn SessionView>,
_injection: &mut SessionInjection,
) -> Result<(), MiddlewareError> {
Ok(())
}
async fn after_dispatch(
&self,
_ctx: &RequestContext<'_>,
result: &mut DispatcherResult,
) -> Result<(), MiddlewareError> {
if let DispatcherResult::Success(val) = result
&& let Some(tools) = val.get_mut("tools")
&& let Some(arr) = tools.as_array_mut()
{
arr.retain(|t| t["name"] != "secret_tool");
}
Ok(())
}
}
#[tokio::test]
async fn test_after_dispatch_success_mutation_visible() {
let mut stack = MiddlewareStack::new();
stack.push(Arc::new(ToolFilteringMiddleware));
let ctx = RequestContext::new("tools/list", None);
let mut result = DispatcherResult::Success(json!({
"tools": [
{"name": "public_tool"},
{"name": "secret_tool"},
{"name": "another_tool"}
]
}));
stack.execute_after(&ctx, &mut result).await.unwrap();
let val = result.success().unwrap();
let tools = val["tools"].as_array().unwrap();
assert_eq!(tools.len(), 2);
assert!(tools.iter().all(|t| t["name"] != "secret_tool"));
}
#[tokio::test]
async fn test_after_dispatch_success_to_error_mutation_visible() {
struct RejectingMiddleware;
#[async_trait]
impl McpMiddleware for RejectingMiddleware {
async fn before_dispatch(
&self,
_ctx: &mut RequestContext<'_>,
_session: Option<&dyn SessionView>,
_injection: &mut SessionInjection,
) -> Result<(), MiddlewareError> {
Ok(())
}
async fn after_dispatch(
&self,
_ctx: &RequestContext<'_>,
result: &mut DispatcherResult,
) -> Result<(), MiddlewareError> {
if result.is_success() {
*result = DispatcherResult::Error("rejected by policy".to_string());
}
Ok(())
}
}
let mut stack = MiddlewareStack::new();
stack.push(Arc::new(RejectingMiddleware));
let ctx = RequestContext::new("tools/list", None);
let mut result = DispatcherResult::Success(json!({"tools": []}));
stack.execute_after(&ctx, &mut result).await.unwrap();
assert!(result.is_error());
assert_eq!(result.error().unwrap(), "rejected by policy");
}
#[tokio::test]
async fn test_empty_stack() {
let stack = MiddlewareStack::new();
assert!(stack.is_empty());
assert_eq!(stack.len(), 0);
let mut ctx = RequestContext::new("test/method", None);
let injection = stack.execute_before(&mut ctx, None).await.unwrap();
assert!(injection.is_empty());
let mut result = DispatcherResult::Success(json!({"ok": true}));
stack.execute_after(&ctx, &mut result).await.unwrap();
}
}