From 5f1f63ad87e05dfaed0c46c85e37b82ae219b8cb Mon Sep 17 00:00:00 2001 From: Sumanth Kamenani Date: Wed, 5 Aug 2026 15:24:25 -0400 Subject: [PATCH 1/2] Limit MCP dynamic values and add boundary tests Signed-off-by: Sumanth Kamenani --- filters/src/agentic/mcp/mod.rs | 4 + filters/src/agentic/mcp/tests.rs | 124 ++++++++++++++++++++++++++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/filters/src/agentic/mcp/mod.rs b/filters/src/agentic/mcp/mod.rs index 816f89d7bd..a2a211a39e 100644 --- a/filters/src/agentic/mcp/mod.rs +++ b/filters/src/agentic/mcp/mod.rs @@ -371,11 +371,13 @@ fn write_metadata( } if let Some(sid) = &mcp.session_id && !contains_control_chars(sid) + && sid.len() <= max_len { ctx.set_metadata("mcp.session_id", sid.clone()); } if let Some(pv) = &mcp.protocol_version && !contains_control_chars(pv) + && pv.len() <= max_len { ctx.set_metadata("mcp.protocol_version", pv.clone()); } @@ -408,6 +410,7 @@ fn promote_mcp_headers( if let Some(header_name) = &config.headers.protocol_version && let Some(pv) = &mcp.protocol_version && !contains_control_chars(pv) + && pv.len() <= max_len { headers.push((Cow::Owned(header_name.clone()), pv.clone())); } @@ -442,6 +445,7 @@ fn promote_filter_results( if let Some(pv) = &mcp.protocol_version && !contains_control_chars(pv) + && pv.len() <= MAX_DYNAMIC_VALUE_LEN { results.set("protocol_version", pv.clone())?; } diff --git a/filters/src/agentic/mcp/tests.rs b/filters/src/agentic/mcp/tests.rs index 488bfe51f7..76b7f51080 100644 --- a/filters/src/agentic/mcp/tests.rs +++ b/filters/src/agentic/mcp/tests.rs @@ -4,7 +4,7 @@ //! Unit tests for the MCP filter. use bytes::Bytes; -use praxis_filter::{FilterAction, HttpFilter as _}; +use praxis_filter::{FilterAction, HttpFilter as _, builtins::http::payload_processing::MAX_DYNAMIC_VALUE_LEN}; use super::{ McpFilter, @@ -1014,6 +1014,58 @@ async fn initialize_promotes_protocol_version_header_and_result() { ); } +#[tokio::test] +async fn dynamic_values_at_length_limit_are_promoted() { + let filter = make_default_filter(); + let protocol_version = "v".repeat(MAX_DYNAMIC_VALUE_LEN); + let session_id = "s".repeat(MAX_DYNAMIC_VALUE_LEN); + let body_str = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": {"protocolVersion": protocol_version}, + }) + .to_string(); + let mut req = make_mcp_request(&[]); + req.headers.insert("mcp-session-id", session_id.parse().unwrap()); + let mut ctx = crate::test_utils::make_filter_context(&req); + let mut body = Some(Bytes::from(body_str)); + + let action = filter.on_request_body(&mut ctx, &mut body, true).await.unwrap(); + + assert!( + matches!(action, FilterAction::Release), + "256-byte values should release" + ); + assert_dynamic_values_promoted(&ctx, &session_id, &protocol_version); +} + +#[tokio::test] +async fn dynamic_values_over_length_limit_are_omitted() { + let filter = make_default_filter(); + let protocol_version = "v".repeat(MAX_DYNAMIC_VALUE_LEN + 1); + let session_id = "s".repeat(MAX_DYNAMIC_VALUE_LEN + 1); + let body_str = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": {"protocolVersion": protocol_version}, + }) + .to_string(); + let mut req = make_mcp_request(&[]); + req.headers.insert("mcp-session-id", session_id.parse().unwrap()); + let mut ctx = crate::test_utils::make_filter_context(&req); + let mut body = Some(Bytes::from(body_str)); + + let action = filter.on_request_body(&mut ctx, &mut body, true).await.unwrap(); + + assert!( + matches!(action, FilterAction::Release), + "257-byte values should release" + ); + assert_dynamic_values_omitted(&ctx); +} + #[tokio::test] async fn non_initialize_promotes_protocol_version_from_header() { let filter = make_default_filter(); @@ -1130,6 +1182,76 @@ async fn null_protocol_version_header_skips_header_promotion() { // Test Utilities // ----------------------------------------------------------------------------- +fn assert_dynamic_values_omitted(ctx: &praxis_filter::HttpFilterContext<'_>) { + assert_eq!( + ctx.get_metadata("mcp.session_id"), + None, + "257-byte session ID should be omitted from metadata" + ); + assert_eq!( + ctx.get_metadata("mcp.protocol_version"), + None, + "257-byte protocol version should be omitted from metadata" + ); + + let headers: std::collections::HashMap<_, _> = ctx + .extra_request_headers + .iter() + .map(|(k, v)| (k.as_ref(), v.as_str())) + .collect(); + assert!( + !headers.contains_key("x-praxis-mcp-protocol-version"), + "257-byte protocol version should be omitted from headers" + ); + + let results = ctx.filter_results.get("mcp").unwrap(); + assert_eq!( + results.get("protocol_version"), + None, + "257-byte protocol version should be omitted from filter results" + ); + assert_eq!( + results.get("session_present"), + Some("true"), + "oversized session ID should still count as present" + ); +} + +fn assert_dynamic_values_promoted( + ctx: &praxis_filter::HttpFilterContext<'_>, + session_id: &str, + protocol_version: &str, +) { + assert_eq!( + ctx.get_metadata("mcp.session_id"), + Some(session_id), + "256-byte session ID should be in metadata" + ); + assert_eq!( + ctx.get_metadata("mcp.protocol_version"), + Some(protocol_version), + "256-byte protocol version should be in metadata" + ); + + let headers: std::collections::HashMap<_, _> = ctx + .extra_request_headers + .iter() + .map(|(k, v)| (k.as_ref(), v.as_str())) + .collect(); + assert_eq!( + headers.get("x-praxis-mcp-protocol-version"), + Some(&protocol_version), + "256-byte protocol version should be promoted to a header" + ); + + let results = ctx.filter_results.get("mcp").unwrap(); + assert_eq!( + results.get("protocol_version"), + Some(protocol_version), + "256-byte protocol version should be in filter results" + ); +} + fn assert_invalid_params_rejection(action: &FilterAction, expected_id: &serde_json::Value) { let FilterAction::Reject(rejection) = action else { panic!("expected InvalidParams rejection, got {action:?}"); From 1359c0dddd4673898ae913d2ffa8aaf5189e9577 Mon Sep 17 00:00:00 2001 From: Sumanth Kamenani Date: Thu, 6 Aug 2026 13:40:23 -0400 Subject: [PATCH 2/2] fix(mcp): bound method and name filter results Signed-off-by: Sumanth Kamenani --- filters/src/agentic/mcp/mod.rs | 3 +- filters/src/agentic/mcp/tests.rs | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/filters/src/agentic/mcp/mod.rs b/filters/src/agentic/mcp/mod.rs index a2a211a39e..dbd9414b4c 100644 --- a/filters/src/agentic/mcp/mod.rs +++ b/filters/src/agentic/mcp/mod.rs @@ -433,12 +433,13 @@ fn promote_filter_results( ) -> Result<(), FilterError> { let results = ctx.filter_results.entry("mcp").or_default(); let method_str = mcp.method.as_str(); - if !contains_control_chars(method_str) { + if !contains_control_chars(method_str) && method_str.len() <= MAX_DYNAMIC_VALUE_LEN { results.set("method", method_str.to_owned())?; } if let Some(name) = &mcp.name && !contains_control_chars(name) + && name.len() <= MAX_DYNAMIC_VALUE_LEN { results.set("name", name.clone())?; } diff --git a/filters/src/agentic/mcp/tests.rs b/filters/src/agentic/mcp/tests.rs index 76b7f51080..351441ded7 100644 --- a/filters/src/agentic/mcp/tests.rs +++ b/filters/src/agentic/mcp/tests.rs @@ -1066,6 +1066,53 @@ async fn dynamic_values_over_length_limit_are_omitted() { assert_dynamic_values_omitted(&ctx); } +#[tokio::test] +async fn oversized_custom_method_is_omitted_from_filter_results() { + let filter = make_default_filter(); + let method = "m".repeat(MAX_DYNAMIC_VALUE_LEN + 1); + let body_str = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": method, + }) + .to_string(); + let req = make_mcp_request(&[]); + let mut ctx = crate::test_utils::make_filter_context(&req); + let mut body = Some(Bytes::from(body_str)); + + let action = filter.on_request_body(&mut ctx, &mut body, true).await.unwrap(); + + assert!( + matches!(action, FilterAction::Release), + "oversized custom methods should release" + ); + assert_oversized_value_is_omitted(&ctx, "mcp.method", "x-praxis-mcp-method", "method"); +} + +#[tokio::test] +async fn oversized_resource_uri_is_omitted_from_filter_results() { + let filter = make_default_filter(); + let uri = "u".repeat(MAX_DYNAMIC_VALUE_LEN + 1); + let body_str = serde_json::json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "resources/read", + "params": {"uri": uri}, + }) + .to_string(); + let req = make_mcp_request(&[]); + let mut ctx = crate::test_utils::make_filter_context(&req); + let mut body = Some(Bytes::from(body_str)); + + let action = filter.on_request_body(&mut ctx, &mut body, true).await.unwrap(); + + assert!( + matches!(action, FilterAction::Release), + "oversized resource URIs should release" + ); + assert_oversized_value_is_omitted(&ctx, "mcp.name", "x-praxis-mcp-name", "name"); +} + #[tokio::test] async fn non_initialize_promotes_protocol_version_from_header() { let filter = make_default_filter(); @@ -1252,6 +1299,36 @@ fn assert_dynamic_values_promoted( ); } +fn assert_oversized_value_is_omitted( + ctx: &praxis_filter::HttpFilterContext<'_>, + metadata_key: &str, + header_name: &str, + result_key: &str, +) { + assert_eq!( + ctx.get_metadata(metadata_key), + None, + "oversized value should be omitted from metadata" + ); + + let headers: std::collections::HashMap<_, _> = ctx + .extra_request_headers + .iter() + .map(|(key, value)| (key.as_ref(), value.as_str())) + .collect(); + assert!( + !headers.contains_key(header_name), + "oversized value should be omitted from headers" + ); + + let results = ctx.filter_results.get("mcp").unwrap(); + assert_eq!( + results.get(result_key), + None, + "oversized value should be omitted from filter results" + ); +} + fn assert_invalid_params_rejection(action: &FilterAction, expected_id: &serde_json::Value) { let FilterAction::Reject(rejection) = action else { panic!("expected InvalidParams rejection, got {action:?}");