From 8ea5073d73b47b050409fa548401649043cdc877 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Wed, 26 Aug 2026 12:18:41 +0100 Subject: [PATCH 01/12] Adding addtional tests for modern and legacy clients. Added logic to filter by tool name, resource uri Signed-off-by: Dawid Nowak --- .../src/user_store.rs | 4 ++ .../src/gateway/mcp_service/initialization.rs | 3 + .../src/gateway/mcp_service/resources.rs | 8 +++ .../src/gateway/mcp_service/tools.rs | 10 +++ .../tests/gateway_call_tools.rs | 63 +++++++++++++++--- .../tests/gateway_pagination.rs | 3 + .../tests/gateway_resource_read.rs | 66 ++++++++++++++++--- .../tests/support/client.rs | 22 ++++++- .../tests/support/mod.rs | 14 ++-- .../tests/support/plugin_gateway.rs | 3 + ...list_tools_gateway.rs => test_gateways.rs} | 3 + .../tests/secrets_detection_e2e.rs | 3 + 12 files changed, 175 insertions(+), 27 deletions(-) rename crates/contextforge-data-plane-lib/tests/support/{list_tools_gateway.rs => test_gateways.rs} (98%) diff --git a/crates/contextforge-data-plane-apis/src/user_store.rs b/crates/contextforge-data-plane-apis/src/user_store.rs index 8ded3fac..05c28658 100644 --- a/crates/contextforge-data-plane-apis/src/user_store.rs +++ b/crates/contextforge-data-plane-apis/src/user_store.rs @@ -33,6 +33,10 @@ pub struct BackendMCPGateway { #[serde(default)] pub completion: HashMap, + pub disable_tool_names_filtering: bool, + pub disable_prompt_names_filtering: bool, + pub disable_resource_names_filtering: bool, + pub allowed_resource_names: Vec, pub allowed_prompt_names: Vec, pub allowed_tool_names: Vec, diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs index 936c73b7..339fba27 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs @@ -172,6 +172,9 @@ mod tests { resource_name_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), + disable_tool_names_filtering: false, + disable_prompt_names_filtering: false, + disable_resource_names_filtering: false, } } diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs index 19474ed9..6b35b693 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs @@ -38,6 +38,14 @@ pub(super) async fn read_resource( data: None, })?; + if !backend.disable_resource_names_filtering && !backend.allowed_resource_names.contains(&resource_uri) { + return Err(ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: "Routing problem... tool not permitted".into(), + data: None, + }); + } + let service_name = backend_name.clone(); let mut backend_service = connect_backend_for_request(mcp_service, &backend_name, backend, &cx).await?; diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs index 950e7272..0fb3794a 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs @@ -29,6 +29,7 @@ pub(super) async fn call_tool( data: None, }); }; + let backend_name = backend_name.to_owned(); let tool_name = tool_name.to_owned(); let backend = virtual_host.backends.get(&backend_name).ok_or_else(|| ErrorData { @@ -36,6 +37,15 @@ pub(super) async fn call_tool( message: "Routing problem... backend not found".into(), data: None, })?; + + if !backend.disable_tool_names_filtering && !backend.allowed_tool_names.contains(&tool_name) { + return Err(ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: "Routing problem... tool not permitted".into(), + data: None, + }); + } + let service_name = backend_name.clone(); let pre_result = if let Some(plugin_runtime) = &mcp_service.plugin_runtime { plugin_runtime.before_tool_call(&request, &tool_name, &service_name).await? diff --git a/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs b/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs index 9d706123..c247ac54 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs @@ -1,17 +1,16 @@ mod support; use contextforge_data_plane_lib::{Config, Result, UpstreamConnectionMode}; -use rmcp::model::CallToolRequestParams; +use rmcp::model::{CallToolRequestParams, ProtocolVersion}; use tracing::{info, warn}; -use support::{ - ListToolsGatewaySettings, TEST_USER_ID, connect_client, create_client, create_gateway_with_four_counters, - create_ports, -}; +use support::{ListToolsGatewaySettings, TEST_USER_ID, create_client, create_gateway_with_four_counters, create_ports}; + +use crate::support::{connect_client_with_protocol, connect_modern_client}; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] -async fn plaintext_call_prefixed_backend_tools() -> Result<()> { +async fn plaintext_call_prefixed_backend_tools_modern_modern() -> Result<()> { let gateway_port = create_ports(1)[0]; let config = Config { @@ -33,7 +32,46 @@ async fn plaintext_call_prefixed_backend_tools() -> Result<()> { let mut call_params = CallToolRequestParams::default(); call_params.name = expected_tool_names[0].clone().into(); - let maybe_passed = assert_tools_call(gateway_url, client, call_params, "-1".to_owned()).await; + let maybe_passed = + assert_tools_call(gateway_url, client, call_params, "-1".to_owned(), ProtocolVersion::V_2026_07_28).await; + + handle.abort(); + if maybe_passed.is_ok() { + info!("Test passed"); + } else { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[test_log::test] +async fn plaintext_call_prefixed_backend_tools_legacy_modern() -> Result<()> { + let gateway_port = create_ports(1)[0]; + + let config = Config { + address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), + token_verification_public_key: Some("../../assets/jwt.key.pub".into()), + upstream_connection_mode: Some(UpstreamConnectionMode::PlainTextOrTls), + ..Default::default() + }; + + let user = TEST_USER_ID; + + let Ok(ListToolsGatewaySettings { handle, gateway_url, expected_tool_names, .. }) = + create_gateway_with_four_counters(user, config).await + else { + panic!("Invalid configuration "); + }; + + let client = create_client(user); + + let mut call_params = CallToolRequestParams::default(); + call_params.name = expected_tool_names[0].clone().into(); + let maybe_passed = + assert_tools_call(gateway_url, client, call_params, "-1".to_owned(), ProtocolVersion::V_2025_11_25).await; handle.abort(); if maybe_passed.is_ok() { @@ -51,10 +89,15 @@ async fn assert_tools_call( client: reqwest::Client, call_tool_params: CallToolRequestParams, expected_result: String, + protocol_version: ProtocolVersion, ) -> Result<()> { info!("Seding request to {gateway_url}"); - let running_service = connect_client(gateway_url, client).await?; + let running_service = if protocol_version == ProtocolVersion::V_2026_07_28 { + connect_modern_client(&gateway_url, client, support::modern_client_info()).await + } else { + connect_client_with_protocol(gateway_url, client, protocol_version).await? + }; let call_tool = running_service.call_tool(call_tool_params).await; let Ok(call_tool) = call_tool else { @@ -108,8 +151,8 @@ async fn plaintext_call_invalid_backend_tools() -> Result<()> { let mut call_params = CallToolRequestParams::default(); call_params.name = "dummy_tool".into(); - let maybe_passed = assert_tools_call(gateway_url, client, call_params, "-1".to_owned()).await; - + let maybe_passed = + assert_tools_call(gateway_url, client, call_params, "-1".to_owned(), ProtocolVersion::V_2026_07_28).await; handle.abort(); if maybe_passed.is_ok() { info!("Test NOT passed {maybe_passed:?}"); diff --git a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs index c3c48e01..dba5380a 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs @@ -34,6 +34,9 @@ fn paginating_backend(port: u16) -> BackendMCPGateway { resource_name_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), + disable_tool_names_filtering: false, + disable_prompt_names_filtering: false, + disable_resource_names_filtering: false, } } diff --git a/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs b/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs index 466e7d87..46c10028 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs @@ -1,17 +1,16 @@ mod support; use contextforge_data_plane_lib::{Config, Result, UpstreamConnectionMode}; -use rmcp::model::ReadResourceRequestParams; +use rmcp::model::{ProtocolVersion, ReadResourceRequestParams}; use tracing::{info, warn}; -use support::{ - ListToolsGatewaySettings, TEST_USER_ID, connect_client, create_client, create_gateway_with_four_counters, - create_ports, -}; +use support::{ListToolsGatewaySettings, TEST_USER_ID, create_client, create_gateway_with_four_counters, create_ports}; + +use crate::support::{connect_client_with_protocol, connect_modern_client}; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] -async fn plaintext_call_prefixed_read_resources() -> Result<()> { +async fn plaintext_call_prefixed_read_resources_modern_modern() -> Result<()> { let gateway_port = create_ports(1)[0]; let config = Config { @@ -37,6 +36,51 @@ async fn plaintext_call_prefixed_read_resources() -> Result<()> { gateway_url, client, call_params, + ProtocolVersion::V_2026_07_28, + "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...".to_owned(), + ) + .await; + + handle.abort(); + if maybe_passed.is_ok() { + info!("Test passed"); + } else { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[test_log::test] +async fn plaintext_call_prefixed_read_resources_legacy_modern() -> Result<()> { + let gateway_port = create_ports(1)[0]; + + let config = Config { + address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), + token_verification_public_key: Some("../../assets/jwt.key.pub".into()), + upstream_connection_mode: Some(UpstreamConnectionMode::PlainTextOrTls), + ..Default::default() + }; + + let user = TEST_USER_ID; + + let Ok(ListToolsGatewaySettings { handle, gateway_url, expected_resource_uris, .. }) = + create_gateway_with_four_counters(user, config).await + else { + panic!("Invalid configuration "); + }; + + let client = create_client(user); + + let call_params = ReadResourceRequestParams::new(expected_resource_uris.first().expect("should work")); + + let maybe_passed = assert_resource_read( + gateway_url, + client, + call_params, + ProtocolVersion::V_2025_11_25, "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...".to_owned(), ) .await; @@ -56,11 +100,16 @@ async fn assert_resource_read( gateway_url: String, client: reqwest::Client, params: ReadResourceRequestParams, + protocol_version: ProtocolVersion, expected_result: String, ) -> Result<()> { info!("Seding request to {gateway_url}"); - let running_service = connect_client(gateway_url, client).await?; + let running_service = if protocol_version == ProtocolVersion::V_2026_07_28 { + connect_modern_client(&gateway_url, client, support::modern_client_info()).await + } else { + connect_client_with_protocol(gateway_url, client, protocol_version).await? + }; let response = running_service.read_resource(params).await; let Ok(response) = response else { @@ -119,7 +168,8 @@ async fn plaintext_call_invalid_backend_tools() -> Result<()> { let client = create_client(user); let call_params = ReadResourceRequestParams::new("http://dummy.dummy"); - let maybe_passed = assert_resource_read(gateway_url, client, call_params, "-1".to_owned()).await; + let maybe_passed = + assert_resource_read(gateway_url, client, call_params, ProtocolVersion::V_2026_07_28, "-1".to_owned()).await; handle.abort(); if maybe_passed.is_ok() { diff --git a/crates/contextforge-data-plane-lib/tests/support/client.rs b/crates/contextforge-data-plane-lib/tests/support/client.rs index dfebe342..506de43c 100644 --- a/crates/contextforge-data-plane-lib/tests/support/client.rs +++ b/crates/contextforge-data-plane-lib/tests/support/client.rs @@ -4,7 +4,7 @@ use contextforge_data_plane_lib::Result; use http::{HeaderMap, HeaderValue}; use rmcp::{ ServiceExt, - model::InitializeRequestParams, + model::{InitializeRequestParams, ProtocolVersion}, transport::{StreamableHttpClientTransport, streamable_http_client::StreamableHttpClientTransportConfig}, }; use tracing::warn; @@ -41,7 +41,25 @@ pub(crate) async fn connect_client( gateway_url: String, client: reqwest::Client, ) -> Result> { - connect_client_with_handler(gateway_url, client, InitializeRequestParams::default()).await + connect_client_with_handler( + gateway_url, + client, + InitializeRequestParams::default().with_protocol_version(ProtocolVersion::V_2026_07_28), + ) + .await +} + +pub(crate) async fn connect_client_with_protocol( + gateway_url: String, + client: reqwest::Client, + protocol_version: ProtocolVersion, +) -> Result> { + connect_client_with_handler( + gateway_url, + client, + InitializeRequestParams::default().with_protocol_version(protocol_version), + ) + .await } /// Connects any `ClientHandler` to the gateway, retrying until `CLIENT_CONNECT_TIMEOUT`. diff --git a/crates/contextforge-data-plane-lib/tests/support/mod.rs b/crates/contextforge-data-plane-lib/tests/support/mod.rs index ffb1ae25..e1a88563 100644 --- a/crates/contextforge-data-plane-lib/tests/support/mod.rs +++ b/crates/contextforge-data-plane-lib/tests/support/mod.rs @@ -2,12 +2,12 @@ mod auth; mod client; -mod list_tools_gateway; pub(crate) mod mock_counter; pub(crate) mod paginating_mock; mod plugin; mod plugin_gateway; mod runtime; +mod test_gateways; mod tool; mod user_config_store; @@ -16,12 +16,8 @@ pub(crate) const TEST_USER_EMAIL: &str = "admin@example.com"; pub(crate) use auth::token; pub(crate) use client::{ - CLIENT_CONNECT_TIMEOUT, TEST_POLL_INTERVAL, connect_client, connect_client_with_handler, connect_modern_client, - create_client, create_tls_client, modern_client_info, -}; -pub(crate) use list_tools_gateway::{ - ListToolsGatewaySettings, create_gateway_with_four_counters, create_ports, - create_tls_gateway_with_four_tls_counters, plaintext_config, + CLIENT_CONNECT_TIMEOUT, TEST_POLL_INTERVAL, connect_client, connect_client_with_handler, + connect_client_with_protocol, connect_modern_client, create_client, create_tls_client, modern_client_info, }; pub(crate) use plugin::{ POST_DENY_ERROR_CODE, PRE_DENY_ERROR_CODE, PROMPT_ERROR_MESSAGE, PROMPT_POST_DENY_ERROR_CODE, PromptBehavior, @@ -33,5 +29,9 @@ pub(crate) use plugin_gateway::{ start_gateway_with_json_backend_responses, }; pub(crate) use runtime::{runtime_with_post, runtime_with_pre, runtime_with_pre_and_post, runtime_with_prompt_plugin}; +pub(crate) use test_gateways::{ + ListToolsGatewaySettings, create_gateway_with_four_counters, create_ports, + create_tls_gateway_with_four_tls_counters, plaintext_config, +}; pub(crate) use tool::{error_code, error_parts, sum_request, text}; pub(crate) use user_config_store::MemoryUserConfigStore; diff --git a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs index f0c262e0..2bf5765f 100644 --- a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs +++ b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs @@ -342,6 +342,9 @@ async fn start_gateway_with_state( resource_name_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), + disable_tool_names_filtering: false, + disable_prompt_names_filtering: false, + disable_resource_names_filtering: false, }, )]), }, diff --git a/crates/contextforge-data-plane-lib/tests/support/list_tools_gateway.rs b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs similarity index 98% rename from crates/contextforge-data-plane-lib/tests/support/list_tools_gateway.rs rename to crates/contextforge-data-plane-lib/tests/support/test_gateways.rs index f88d8afc..7c180009 100644 --- a/crates/contextforge-data-plane-lib/tests/support/list_tools_gateway.rs +++ b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs @@ -240,6 +240,9 @@ fn create_backends(ports: &[u16], with_tls: bool) -> HashMap Date: Wed, 26 Aug 2026 13:19:41 +0100 Subject: [PATCH 02/12] Adding addtional tests for modern and legacy clients. Added logic to filter by tool name, resource uri.Fixing unit tests Signed-off-by: Dawid Nowak --- .../src/gateway/identifier_routing.rs | 15 ++++++++++++--- .../tests/gateway_completions.rs | 4 +++- .../tests/gateway_prompts.rs | 4 +++- .../tests/gateway_subscriptions.rs | 8 +++++--- .../tests/support/plugin_gateway.rs | 6 +++--- 5 files changed, 26 insertions(+), 11 deletions(-) diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index ef98ac1a..e348de41 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -157,7 +157,10 @@ mod tests { "Echo_Tool": "echo" }, "allowed_resource_names": [], - "allowed_prompt_names": [] + "allowed_prompt_names": [], + "disable_tool_names_filtering": true, + "disable_prompt_names_filtering": true, + "disable_resource_names_filtering": true, } } }); @@ -184,7 +187,10 @@ mod tests { "passthrough_headers": [], "allowed_tool_names": ["get_stats"], "allowed_resource_names": [], - "allowed_prompt_names": [] + "allowed_prompt_names": [], + "disable_tool_names_filtering": true, + "disable_prompt_names_filtering": true, + "disable_resource_names_filtering": true, }, "other": { "name": "other", @@ -192,7 +198,10 @@ mod tests { "passthrough_headers": [], "allowed_tool_names": [], "allowed_resource_names": [], - "allowed_prompt_names": [] + "allowed_prompt_names": [], + "disable_tool_names_filtering": true, + "disable_prompt_names_filtering": true, + "disable_resource_names_filtering": true, } } }); diff --git a/crates/contextforge-data-plane-lib/tests/gateway_completions.rs b/crates/contextforge-data-plane-lib/tests/gateway_completions.rs index db37558f..6fefff01 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_completions.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_completions.rs @@ -8,6 +8,8 @@ use support::{ create_ports, plaintext_config, }; +use crate::support::connect_modern_client; + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] #[ignore = "2026-07-28 protocol transition"] @@ -107,7 +109,7 @@ async fn assert_resource_completion(gateway_url: String, client: reqwest::Client } async fn assert_unrouted_completion_errors(gateway_url: String, client: reqwest::Client) -> Result<()> { - let running_service = connect_client(gateway_url, client).await?; + let running_service = connect_modern_client(&gateway_url, client, support::modern_client_info()).await; // No backend namespace prefix => no route, so the gateway must reject it. let result = running_service.complete_prompt_simple("unrouted_prompt", "message", "h").await; diff --git a/crates/contextforge-data-plane-lib/tests/gateway_prompts.rs b/crates/contextforge-data-plane-lib/tests/gateway_prompts.rs index 3beb264b..70921aca 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_prompts.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_prompts.rs @@ -10,6 +10,8 @@ use support::{ create_ports, }; +use crate::support::connect_modern_client; + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] #[ignore = "Fan out list tools is not supported at the moment. This should be enabled in 2.x"] @@ -97,7 +99,7 @@ async fn assert_list_prompts( } async fn assert_get_prompt(gateway_url: String, client: reqwest::Client, prompt_name: String) -> Result<()> { - let running_service = connect_client(gateway_url, client).await?; + let running_service = connect_modern_client(&gateway_url, client, support::modern_client_info()).await; let mut arguments = serde_json::Map::new(); arguments.insert("message".to_owned(), json!("hello from gateway")); diff --git a/crates/contextforge-data-plane-lib/tests/gateway_subscriptions.rs b/crates/contextforge-data-plane-lib/tests/gateway_subscriptions.rs index 575fa668..a73d8e6f 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_subscriptions.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_subscriptions.rs @@ -18,12 +18,14 @@ use rmcp::{ }; use support::{ - CLIENT_CONNECT_TIMEOUT, ListToolsGatewaySettings, TEST_POLL_INTERVAL, TEST_USER_ID, connect_client, - connect_client_with_handler, create_client, create_gateway_with_four_counters, create_ports, + CLIENT_CONNECT_TIMEOUT, ListToolsGatewaySettings, TEST_POLL_INTERVAL, TEST_USER_ID, connect_client_with_handler, + create_client, create_gateway_with_four_counters, create_ports, mock_counter::{KNOWN_RESOURCE_URIS, RESOURCE_UPDATE_NOTIFY_INTERVAL}, plaintext_config, }; +use crate::support::connect_modern_client; + /// The mocks notify continuously, so this is just the threshold proving delivery works. const MIN_UPDATES_PER_BACKEND: usize = 4; @@ -147,7 +149,7 @@ async fn assert_no_more_resource_updates( #[expect(deprecated, reason = "legacy RMCP coverage; modern subscriptions/listen tests are deferred")] async fn assert_unrouted_subscribe_errors(gateway_url: String, client: reqwest::Client) -> Result<()> { - let running_service = connect_client(gateway_url, client).await?; + let running_service = connect_modern_client(&gateway_url, client, support::modern_client_info()).await; // No backend namespace prefix => no route, so the gateway must reject it. let result = running_service.subscribe(SubscribeRequestParams::new("unrouted://resource")).await; diff --git a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs index 2bf5765f..1807d99d 100644 --- a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs +++ b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs @@ -342,9 +342,9 @@ async fn start_gateway_with_state( resource_name_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), - disable_tool_names_filtering: false, - disable_prompt_names_filtering: false, - disable_resource_names_filtering: false, + disable_tool_names_filtering: true, + disable_prompt_names_filtering: true, + disable_resource_names_filtering: true, }, )]), }, From 3f2662785ae7684232ac90851108943ebd9ca4ef Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Wed, 26 Aug 2026 14:31:10 +0100 Subject: [PATCH 03/12] Adding addtional tests for modern and legacy clients. Added logic to filter by tool name, resource uri.Adding extra tests Signed-off-by: Dawid Nowak --- .../src/user_store.rs | 4 +- .../src/gateway/identifier_routing.rs | 12 +- .../src/gateway/mcp_service/initialization.rs | 4 +- .../src/gateway/mcp_service/resources.rs | 4 +- .../tests/gateway_call_tools.rs | 61 ++++++++- .../tests/gateway_pagination.rs | 4 +- .../tests/gateway_resource_read.rs | 66 ++++++++- .../tests/support/mod.rs | 5 +- .../tests/support/plugin_gateway.rs | 4 +- .../tests/support/test_gateways.rs | 128 ++++++------------ .../tests/secrets_detection_e2e.rs | 4 +- 11 files changed, 190 insertions(+), 106 deletions(-) diff --git a/crates/contextforge-data-plane-apis/src/user_store.rs b/crates/contextforge-data-plane-apis/src/user_store.rs index 05c28658..f08969a3 100644 --- a/crates/contextforge-data-plane-apis/src/user_store.rs +++ b/crates/contextforge-data-plane-apis/src/user_store.rs @@ -35,9 +35,9 @@ pub struct BackendMCPGateway { pub disable_tool_names_filtering: bool, pub disable_prompt_names_filtering: bool, - pub disable_resource_names_filtering: bool, + pub disable_resource_uris_filtering: bool, - pub allowed_resource_names: Vec, + pub allowed_resource_uris: Vec, pub allowed_prompt_names: Vec, pub allowed_tool_names: Vec, } diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index e348de41..04bfa8d7 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -156,11 +156,11 @@ mod tests { "Public.Tool": "get_stats", "Echo_Tool": "echo" }, - "allowed_resource_names": [], + "allowed_resource_uris": [], "allowed_prompt_names": [], "disable_tool_names_filtering": true, "disable_prompt_names_filtering": true, - "disable_resource_names_filtering": true, + "disable_resource_uris_filtering": true, } } }); @@ -186,22 +186,22 @@ mod tests { "url": "http://upstream:9000/mcp", "passthrough_headers": [], "allowed_tool_names": ["get_stats"], - "allowed_resource_names": [], + "allowed_resource_uris": [], "allowed_prompt_names": [], "disable_tool_names_filtering": true, "disable_prompt_names_filtering": true, - "disable_resource_names_filtering": true, + "disable_resource_uris_filtering": true, }, "other": { "name": "other", "url": "http://other:9000/mcp", "passthrough_headers": [], "allowed_tool_names": [], - "allowed_resource_names": [], + "allowed_resource_uris": [], "allowed_prompt_names": [], "disable_tool_names_filtering": true, "disable_prompt_names_filtering": true, - "disable_resource_names_filtering": true, + "disable_resource_uris_filtering": true, } } }); diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs index 339fba27..a5ea81d4 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs @@ -167,14 +167,14 @@ mod tests { remove_headers: remove.iter().map(|s| (*s).to_owned()).collect(), allowed_tool_names: vec![], tool_name_aliases: HashMap::new(), - allowed_resource_names: vec![], + allowed_resource_uris: vec![], allowed_prompt_names: vec![], resource_name_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), disable_tool_names_filtering: false, disable_prompt_names_filtering: false, - disable_resource_names_filtering: false, + disable_resource_uris_filtering: false, } } diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs index 6b35b693..e5904e88 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs @@ -38,10 +38,10 @@ pub(super) async fn read_resource( data: None, })?; - if !backend.disable_resource_names_filtering && !backend.allowed_resource_names.contains(&resource_uri) { + if !backend.disable_resource_uris_filtering && !backend.allowed_resource_uris.contains(&resource_uri) { return Err(ErrorData { code: ErrorCode::INVALID_PARAMS, - message: "Routing problem... tool not permitted".into(), + message: "Routing problem... resource not permitted".into(), data: None, }); } diff --git a/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs b/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs index c247ac54..662bc0c3 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs @@ -6,7 +6,9 @@ use tracing::{info, warn}; use support::{ListToolsGatewaySettings, TEST_USER_ID, create_client, create_gateway_with_four_counters, create_ports}; -use crate::support::{connect_client_with_protocol, connect_modern_client}; +use crate::support::{ + connect_client_with_protocol, connect_modern_client, create_gateway_with_four_counters_and_enabled_filtering, +}; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] @@ -46,6 +48,63 @@ async fn plaintext_call_prefixed_backend_tools_modern_modern() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[test_log::test] +async fn plaintext_call_prefixed_backend_tools_modern_modern_with_filtering() -> Result<()> { + let gateway_port = create_ports(1)[0]; + + let config = Config { + address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), + token_verification_public_key: Some("../../assets/jwt.key.pub".into()), + upstream_connection_mode: Some(UpstreamConnectionMode::PlainTextOrTls), + ..Default::default() + }; + + let user = TEST_USER_ID; + + let Ok(ListToolsGatewaySettings { handle, gateway_url, expected_tool_names, .. }) = + create_gateway_with_four_counters_and_enabled_filtering(user, config).await + else { + panic!("Invalid configuration "); + }; + + let client = create_client(user); + + let mut call_params = CallToolRequestParams::default(); + call_params.name = expected_tool_names[0].clone().into(); + let maybe_passed = assert_tools_call( + gateway_url.clone(), + client.clone(), + call_params, + "-1".to_owned(), + ProtocolVersion::V_2026_07_28, + ) + .await; + + let mut call_params = CallToolRequestParams::default(); + call_params.name = "random_tool_name".into(); + let maybe_not_passed = + assert_tools_call(gateway_url, client, call_params, "-1".to_owned(), ProtocolVersion::V_2026_07_28).await; + + handle.abort(); + + if maybe_passed.is_ok() { + info!("Test passed"); + } else { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } + + if maybe_not_passed.is_ok() { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } else { + info!("Test passed"); + } + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] async fn plaintext_call_prefixed_backend_tools_legacy_modern() -> Result<()> { diff --git a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs index dba5380a..5e50a9e0 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs @@ -29,14 +29,14 @@ fn paginating_backend(port: u16) -> BackendMCPGateway { remove_headers: Vec::new(), allowed_tool_names: Vec::new(), tool_name_aliases: HashMap::new(), - allowed_resource_names: Vec::new(), + allowed_resource_uris: Vec::new(), allowed_prompt_names: Vec::new(), resource_name_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), disable_tool_names_filtering: false, disable_prompt_names_filtering: false, - disable_resource_names_filtering: false, + disable_resource_uris_filtering: false, } } diff --git a/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs b/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs index 46c10028..a94cb147 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs @@ -6,7 +6,9 @@ use tracing::{info, warn}; use support::{ListToolsGatewaySettings, TEST_USER_ID, create_client, create_gateway_with_four_counters, create_ports}; -use crate::support::{connect_client_with_protocol, connect_modern_client}; +use crate::support::{ + connect_client_with_protocol, connect_modern_client, create_gateway_with_four_counters_and_enabled_filtering, +}; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] @@ -52,6 +54,68 @@ async fn plaintext_call_prefixed_read_resources_modern_modern() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[test_log::test] +async fn plaintext_call_prefixed_read_resources_modern_modern_with_filtering() -> Result<()> { + let gateway_port = create_ports(1)[0]; + + let config = Config { + address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), + token_verification_public_key: Some("../../assets/jwt.key.pub".into()), + upstream_connection_mode: Some(UpstreamConnectionMode::PlainTextOrTls), + ..Default::default() + }; + + let user = TEST_USER_ID; + + let Ok(ListToolsGatewaySettings { handle, gateway_url, expected_resource_uris, .. }) = + create_gateway_with_four_counters_and_enabled_filtering(user, config).await + else { + panic!("Invalid configuration "); + }; + + let client = create_client(user); + + let call_params = ReadResourceRequestParams::new(expected_resource_uris.first().expect("should work")); + + let maybe_passed = assert_resource_read( + gateway_url.clone(), + client.clone(), + call_params, + ProtocolVersion::V_2026_07_28, + "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...".to_owned(), + ) + .await; + + let call_params = ReadResourceRequestParams::new("some_random_uri"); + + let maybe_not_passed = assert_resource_read( + gateway_url, + client, + call_params, + ProtocolVersion::V_2026_07_28, + "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...".to_owned(), + ) + .await; + + handle.abort(); + if maybe_passed.is_ok() { + info!("Test passed"); + } else { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } + + if maybe_not_passed.is_ok() { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } else { + info!("Test passed"); + } + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] async fn plaintext_call_prefixed_read_resources_legacy_modern() -> Result<()> { diff --git a/crates/contextforge-data-plane-lib/tests/support/mod.rs b/crates/contextforge-data-plane-lib/tests/support/mod.rs index e1a88563..124d40ab 100644 --- a/crates/contextforge-data-plane-lib/tests/support/mod.rs +++ b/crates/contextforge-data-plane-lib/tests/support/mod.rs @@ -30,8 +30,9 @@ pub(crate) use plugin_gateway::{ }; pub(crate) use runtime::{runtime_with_post, runtime_with_pre, runtime_with_pre_and_post, runtime_with_prompt_plugin}; pub(crate) use test_gateways::{ - ListToolsGatewaySettings, create_gateway_with_four_counters, create_ports, - create_tls_gateway_with_four_tls_counters, plaintext_config, + ListToolsGatewaySettings, create_gateway_with_four_counters, + create_gateway_with_four_counters_and_enabled_filtering, create_ports, create_tls_gateway_with_four_tls_counters, + plaintext_config, }; pub(crate) use tool::{error_code, error_parts, sum_request, text}; pub(crate) use user_config_store::MemoryUserConfigStore; diff --git a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs index 1807d99d..be01e6a3 100644 --- a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs +++ b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs @@ -337,14 +337,14 @@ async fn start_gateway_with_state( remove_headers: Vec::new(), allowed_tool_names: Vec::new(), tool_name_aliases: HashMap::new(), - allowed_resource_names: Vec::new(), + allowed_resource_uris: Vec::new(), allowed_prompt_names: Vec::new(), resource_name_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), disable_tool_names_filtering: true, disable_prompt_names_filtering: true, - disable_resource_names_filtering: true, + disable_resource_uris_filtering: true, }, )]), }, diff --git a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs index 7c180009..77927d18 100644 --- a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs +++ b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs @@ -53,7 +53,11 @@ pub(crate) fn create_ports(ports: usize) -> Vec { selected } -pub(crate) async fn create_gateway_with_four_counters(user: &str, config: Config) -> Result { +async fn create_gateway_with_four_counters_and_custom_config( + user: &str, + config: Config, + create_backends: impl Fn(&[u16]) -> HashMap, +) -> Result { let mocked_user_config_store = MemoryUserConfigStore::default(); let config_address = config.address.expect("This must be set"); @@ -72,8 +76,8 @@ pub(crate) async fn create_gateway_with_four_counters(user: &str, config: Config assert_ne!(gateway_one_ports, gateway_two_ports); - let gateway_one_backends = create_backends(&gateway_one_ports, false); - let gateway_two_backends = create_backends(&gateway_two_ports, false); + let gateway_one_backends = create_backends(&gateway_one_ports); + let gateway_two_backends = create_backends(&gateway_two_ports); let mut virtual_host_one_tool_names = create_tool_names(&gateway_one_ports); virtual_host_one_tool_names.sort(); @@ -130,88 +134,26 @@ pub(crate) async fn create_gateway_with_four_counters(user: &str, config: Config }) } -pub(crate) async fn create_tls_gateway_with_four_tls_counters( +pub(crate) async fn create_gateway_with_four_counters(user: &str, config: Config) -> Result { + create_gateway_with_four_counters_and_custom_config(user, config, create_plain_backends).await +} + +pub(crate) async fn create_gateway_with_four_counters_and_enabled_filtering( user: &str, config: Config, ) -> Result { - let mocked_user_config_store = MemoryUserConfigStore::default(); - let gateway_port = config.tls_address.ok_or("Invalid configuration")?.port(); - - let service = StreamableHttpService::new( - || Ok(mock_counter::Counter::new()), - LocalSessionManager::default().into(), - StreamableHttpServerConfig::default().disable_allowed_hosts().disable_allowed_origins(), - ); - - let router = axum::Router::new().route_service("/mcp", service); - - let (gateway_one_ports, servers_one) = create_axum_tls_servers(2, gateway_port, router.clone()).await?; - let (gateway_two_ports, servers_two) = create_axum_tls_servers(2, gateway_port, router).await?; - - assert_ne!(gateway_one_ports, gateway_two_ports); - - let gateway_one_backends = create_backends(&gateway_one_ports, true); - let gateway_two_backends = create_backends(&gateway_two_ports, true); - - let mut virtual_host_one_tool_names = create_tool_names(&gateway_one_ports); - virtual_host_one_tool_names.sort(); - let mut virtual_host_one_prompt_names = create_prompt_names(&gateway_one_ports); - virtual_host_one_prompt_names.sort(); - let mut virtual_host_one_resource_template_names = create_resource_template_names(&gateway_one_ports); - virtual_host_one_resource_template_names.sort(); - let mut virtual_host_one_resource_template_uris = create_resource_template_uris(&gateway_one_ports); - virtual_host_one_resource_template_uris.sort(); - let mut virtual_host_one_resource_uris = create_resource_uris(&gateway_one_ports); - virtual_host_one_resource_uris.sort(); - - let user_key = User::new(user); - - let virtual_host_one_id = uuid::Uuid::new_v4().to_string(); - let virtual_host_two_id = uuid::Uuid::new_v4().to_string(); - - let virtual_hosts = HashMap::from([ - (virtual_host_one_id.clone(), VirtualHost { backends: gateway_one_backends }), - (virtual_host_two_id, VirtualHost { backends: gateway_two_backends }), - ]); - - let user_config = UserConfig { virtual_hosts }; - - mocked_user_config_store.set_config(&user_key, &user_config).await.expect("This should work"); - - let gateway = Gateway::builder() - .with_config(config.clone()) - .with_session_manager(Arc::new(LocalSessionManager::default())) - .with_user_config_store_type(UserConfigStoreType::Test(Arc::new(mocked_user_config_store))) - .build(); - - let gateway = async move { - let res = gateway.run_gateway().await; - warn!("Gateway exited with result {res:?}"); - Ok(()) - } - .boxed(); - - if let Some(address) = config.tls_address.as_ref() { - let gateway_url = format!("https://{address}/contextforge-rs/servers/{virtual_host_one_id}/mcp"); - - let handle = - tokio::spawn(futures::future::join_all(vec![gateway].into_iter().chain(servers_one).chain(servers_two))); + create_gateway_with_four_counters_and_custom_config(user, config, create_plain_backends_with_enabled_filtering) + .await +} - Ok(ListToolsGatewaySettings { - handle, - gateway_url, - expected_tool_names: virtual_host_one_tool_names, - expected_prompt_names: virtual_host_one_prompt_names, - expected_resource_template_names: virtual_host_one_resource_template_names, - expected_resource_template_uris: virtual_host_one_resource_template_uris, - expected_resource_uris: virtual_host_one_resource_uris, - }) - } else { - Err("Invalid configuration".into()) - } +pub(crate) async fn create_tls_gateway_with_four_tls_counters( + user: &str, + config: Config, +) -> Result { + create_gateway_with_four_counters_and_custom_config(user, config, create_tls_backends).await } -fn create_backends(ports: &[u16], with_tls: bool) -> HashMap { +fn create_backends(ports: &[u16], with_tls: bool, test_disable_filtering: bool) -> HashMap { ports .iter() .map(|port| { @@ -230,25 +172,43 @@ fn create_backends(ports: &[u16], with_tls: bool) -> HashMap HashMap { + create_backends(ports, false, true) +} + +fn create_plain_backends_with_enabled_filtering(ports: &[u16]) -> HashMap { + create_backends(ports, false, false) +} + +fn create_tls_backends(ports: &[u16]) -> HashMap { + create_backends(ports, true, true) +} + fn backend_id(port: u16) -> String { format!("00000000-0000-0000-0000-{port:012}") } diff --git a/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs b/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs index a1db537b..e847ead0 100644 --- a/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs +++ b/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs @@ -381,14 +381,14 @@ async fn write_redis_config(redis_port: u16, backend: &RunningBackend) { remove_headers: Vec::new(), allowed_tool_names: Vec::new(), tool_name_aliases: HashMap::new(), - allowed_resource_names: Vec::new(), + allowed_resource_uris: Vec::new(), allowed_prompt_names: Vec::new(), resource_name_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), disable_tool_names_filtering: false, disable_prompt_names_filtering: false, - disable_resource_names_filtering: false, + disable_resource_uris_filtering: false, }, )]), }, From 9595b46fcfbced6a0398ac03be48eb9df6d7fcf8 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Wed, 26 Aug 2026 14:59:16 +0100 Subject: [PATCH 04/12] Adding addtional tests for modern and legacy clients. Added logic to filter by tool name, resource uri.Adding extra tests.2 Signed-off-by: Dawid Nowak --- Cargo.lock | 1 + .../contextforge-data-plane-apis/Cargo.toml | 1 + .../src/user_store.rs | 1 + .../src/gateway/identifier_routing.rs | 3 ++ .../src/gateway/mcp_service/initialization.rs | 3 +- .../tests/gateway_call_tools.rs | 39 ++++++++++++++++ .../tests/gateway_pagination.rs | 1 + .../tests/gateway_resource_read.rs | 45 +++++++++++++++++++ .../tests/support/mod.rs | 4 +- .../tests/support/plugin_gateway.rs | 1 + .../tests/support/test_gateways.rs | 26 +++++++++-- .../tests/secrets_detection_e2e.rs | 2 + 12 files changed, 120 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a1f9d8c9..9d3913b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -584,6 +584,7 @@ name = "contextforge-data-plane-apis" version = "0.1.0" dependencies = [ "cpex", + "rmcp", "schemars", "serde", "serde_json", diff --git a/crates/contextforge-data-plane-apis/Cargo.toml b/crates/contextforge-data-plane-apis/Cargo.toml index ebf09dff..56d6c915 100644 --- a/crates/contextforge-data-plane-apis/Cargo.toml +++ b/crates/contextforge-data-plane-apis/Cargo.toml @@ -17,6 +17,7 @@ serde= {workspace = true, features=["derive"]} serde_json.workspace = true url = { workspace = true } schemars = { version = "1.2.1", features = ["url2", "preserve_order"] } +rmcp.workspace = true [lints] workspace = true diff --git a/crates/contextforge-data-plane-apis/src/user_store.rs b/crates/contextforge-data-plane-apis/src/user_store.rs index f08969a3..56417495 100644 --- a/crates/contextforge-data-plane-apis/src/user_store.rs +++ b/crates/contextforge-data-plane-apis/src/user_store.rs @@ -16,6 +16,7 @@ pub enum IntegrationType { pub struct BackendMCPGateway { pub name: String, pub url: url::Url, + pub mcp_protocol_version: rmcp::model::ProtocolVersion, /// Header names copied from the downstream request onto the upstream connection. pub passthrough_headers: Vec, /// Static headers injected onto the upstream connection (override passthrough). diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index 04bfa8d7..11431ea9 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -150,6 +150,7 @@ mod tests { "79fabb70-2188-4de8-95ed-dc1e976e14d4": { "name": "compliance_reference", "url": "http://upstream:9000/mcp", + "mcp_protocol_version": "2026_07_28", "passthrough_headers": [], "allowed_tool_names": ["get_stats", "echo"], "tool_name_aliases": { @@ -184,6 +185,7 @@ mod tests { "compliance-reference": { "name": "compliance_reference", "url": "http://upstream:9000/mcp", + "mcp_protocol_version": "2026_07_28", "passthrough_headers": [], "allowed_tool_names": ["get_stats"], "allowed_resource_uris": [], @@ -195,6 +197,7 @@ mod tests { "other": { "name": "other", "url": "http://other:9000/mcp", + "mcp_protocol_version": "2026_07_28", "passthrough_headers": [], "allowed_tool_names": [], "allowed_resource_uris": [], diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs index a5ea81d4..600d4d9f 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs @@ -62,7 +62,7 @@ pub(super) async fn connect_backend_for_request( ClientCapabilities::default(), Implementation::new("contextforge-data-plane", env!("CARGO_PKG_VERSION")), ) - .with_protocol_version(ProtocolVersion::V_2026_07_28); + .with_protocol_version(backend.mcp_protocol_version.clone()); let backend_client = GatewayBackendClient::new(client_info, mcp_service.plugin_runtime.clone()); @@ -162,6 +162,7 @@ mod tests { BackendMCPGateway { name: "b".into(), url: "https://upstream.example/mcp".parse().unwrap(), + mcp_protocol_version: rmcp::model::ProtocolVersion::V_2026_07_28, passthrough_headers: passthrough.iter().map(|s| (*s).to_owned()).collect(), add_headers: add.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned())).collect(), remove_headers: remove.iter().map(|s| (*s).to_owned()).collect(), diff --git a/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs b/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs index 662bc0c3..e1e34e2e 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs @@ -8,6 +8,7 @@ use support::{ListToolsGatewaySettings, TEST_USER_ID, create_client, create_gate use crate::support::{ connect_client_with_protocol, connect_modern_client, create_gateway_with_four_counters_and_enabled_filtering, + create_gateway_with_four_legacy_counters, }; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -48,6 +49,44 @@ async fn plaintext_call_prefixed_backend_tools_modern_modern() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[test_log::test] +async fn plaintext_call_prefixed_backend_tools_modern_legacy() -> Result<()> { + let gateway_port = create_ports(1)[0]; + + let config = Config { + address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), + token_verification_public_key: Some("../../assets/jwt.key.pub".into()), + upstream_connection_mode: Some(UpstreamConnectionMode::PlainTextOrTls), + ..Default::default() + }; + + let user = TEST_USER_ID; + + let Ok(ListToolsGatewaySettings { handle, gateway_url, expected_tool_names, .. }) = + create_gateway_with_four_legacy_counters(user, config).await + else { + panic!("Invalid configuration "); + }; + + let client = create_client(user); + + let mut call_params = CallToolRequestParams::default(); + call_params.name = expected_tool_names[0].clone().into(); + let maybe_passed = + assert_tools_call(gateway_url, client, call_params, "-1".to_owned(), ProtocolVersion::V_2026_07_28).await; + + handle.abort(); + if maybe_passed.is_ok() { + info!("Test passed"); + } else { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] async fn plaintext_call_prefixed_backend_tools_modern_modern_with_filtering() -> Result<()> { diff --git a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs index 5e50a9e0..a344a183 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs @@ -24,6 +24,7 @@ fn paginating_backend(port: u16) -> BackendMCPGateway { BackendMCPGateway { name: format!("backend-{port}"), url: format!("http://127.0.0.1:{port}/mcp").parse().expect("valid url"), + mcp_protocol_version: rmcp::model::ProtocolVersion::V_2026_07_28, passthrough_headers: Vec::new(), add_headers: HashMap::new(), remove_headers: Vec::new(), diff --git a/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs b/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs index a94cb147..d37c49d0 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs @@ -8,6 +8,7 @@ use support::{ListToolsGatewaySettings, TEST_USER_ID, create_client, create_gate use crate::support::{ connect_client_with_protocol, connect_modern_client, create_gateway_with_four_counters_and_enabled_filtering, + create_gateway_with_four_legacy_counters, }; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -54,6 +55,50 @@ async fn plaintext_call_prefixed_read_resources_modern_modern() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[test_log::test] +async fn plaintext_call_prefixed_read_resources_modern_legacy() -> Result<()> { + let gateway_port = create_ports(1)[0]; + + let config = Config { + address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), + token_verification_public_key: Some("../../assets/jwt.key.pub".into()), + upstream_connection_mode: Some(UpstreamConnectionMode::PlainTextOrTls), + ..Default::default() + }; + + let user = TEST_USER_ID; + + let Ok(ListToolsGatewaySettings { handle, gateway_url, expected_resource_uris, .. }) = + create_gateway_with_four_legacy_counters(user, config).await + else { + panic!("Invalid configuration "); + }; + + let client = create_client(user); + + let call_params = ReadResourceRequestParams::new(expected_resource_uris.first().expect("should work")); + + let maybe_passed = assert_resource_read( + gateway_url, + client, + call_params, + ProtocolVersion::V_2026_07_28, + "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...".to_owned(), + ) + .await; + + handle.abort(); + if maybe_passed.is_ok() { + info!("Test passed"); + } else { + info!("Test NOT passed {maybe_passed:?}"); + panic!() + } + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] async fn plaintext_call_prefixed_read_resources_modern_modern_with_filtering() -> Result<()> { diff --git a/crates/contextforge-data-plane-lib/tests/support/mod.rs b/crates/contextforge-data-plane-lib/tests/support/mod.rs index 124d40ab..aa32984c 100644 --- a/crates/contextforge-data-plane-lib/tests/support/mod.rs +++ b/crates/contextforge-data-plane-lib/tests/support/mod.rs @@ -31,8 +31,8 @@ pub(crate) use plugin_gateway::{ pub(crate) use runtime::{runtime_with_post, runtime_with_pre, runtime_with_pre_and_post, runtime_with_prompt_plugin}; pub(crate) use test_gateways::{ ListToolsGatewaySettings, create_gateway_with_four_counters, - create_gateway_with_four_counters_and_enabled_filtering, create_ports, create_tls_gateway_with_four_tls_counters, - plaintext_config, + create_gateway_with_four_counters_and_enabled_filtering, create_gateway_with_four_legacy_counters, create_ports, + create_tls_gateway_with_four_tls_counters, plaintext_config, }; pub(crate) use tool::{error_code, error_parts, sum_request, text}; pub(crate) use user_config_store::MemoryUserConfigStore; diff --git a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs index be01e6a3..6589c716 100644 --- a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs +++ b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs @@ -332,6 +332,7 @@ async fn start_gateway_with_state( BackendMCPGateway { url: format!("http://127.0.0.1:{backend_port}/mcp").parse().expect("backend URL"), name: String::new(), + mcp_protocol_version: rmcp::model::ProtocolVersion::V_2026_07_28, passthrough_headers: Vec::new(), add_headers: HashMap::default(), remove_headers: Vec::new(), diff --git a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs index 77927d18..14c07983 100644 --- a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs +++ b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs @@ -11,6 +11,7 @@ use futures::{FutureExt, future::BoxFuture}; use rmcp::transport::{ StreamableHttpServerConfig, StreamableHttpService, streamable_http_server::session::local::LocalSessionManager, }; +use rustls::ProtocolVersion; use tracing::warn; use super::{MemoryUserConfigStore, mock_counter}; @@ -138,6 +139,13 @@ pub(crate) async fn create_gateway_with_four_counters(user: &str, config: Config create_gateway_with_four_counters_and_custom_config(user, config, create_plain_backends).await } +pub(crate) async fn create_gateway_with_four_legacy_counters( + user: &str, + config: Config, +) -> Result { + create_gateway_with_four_counters_and_custom_config(user, config, create_plain_legacy_backends).await +} + pub(crate) async fn create_gateway_with_four_counters_and_enabled_filtering( user: &str, config: Config, @@ -153,7 +161,12 @@ pub(crate) async fn create_tls_gateway_with_four_tls_counters( create_gateway_with_four_counters_and_custom_config(user, config, create_tls_backends).await } -fn create_backends(ports: &[u16], with_tls: bool, test_disable_filtering: bool) -> HashMap { +fn create_backends( + ports: &[u16], + with_tls: bool, + test_disable_filtering: bool, + protocol_version: &rmcp::model::ProtocolVersion, +) -> HashMap { ports .iter() .map(|port| { @@ -169,6 +182,7 @@ fn create_backends(ports: &[u16], with_tls: bool, test_disable_filtering: bool) BackendMCPGateway { name: format!("backend-{port}"), url, + mcp_protocol_version: protocol_version.clone(), passthrough_headers: Vec::new(), add_headers: HashMap::default(), remove_headers: Vec::new(), @@ -198,15 +212,19 @@ fn create_backends(ports: &[u16], with_tls: bool, test_disable_filtering: bool) } fn create_plain_backends(ports: &[u16]) -> HashMap { - create_backends(ports, false, true) + create_backends(ports, false, true, &rmcp::model::ProtocolVersion::V_2026_07_28) +} + +fn create_plain_legacy_backends(ports: &[u16]) -> HashMap { + create_backends(ports, false, true, &rmcp::model::ProtocolVersion::V_2025_11_25) } fn create_plain_backends_with_enabled_filtering(ports: &[u16]) -> HashMap { - create_backends(ports, false, false) + create_backends(ports, false, false, &rmcp::model::ProtocolVersion::V_2026_07_28) } fn create_tls_backends(ports: &[u16]) -> HashMap { - create_backends(ports, true, true) + create_backends(ports, true, true, &rmcp::model::ProtocolVersion::V_2026_07_28) } fn backend_id(port: u16) -> String { diff --git a/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs b/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs index e847ead0..9750d775 100644 --- a/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs +++ b/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs @@ -34,6 +34,7 @@ use rmcp::{ streamable_http_server::session::local::LocalSessionManager, }, }; + use serde_json::{Map, Value, json}; use tokio::net::TcpListener; @@ -376,6 +377,7 @@ async fn write_redis_config(redis_port: u16, backend: &RunningBackend) { BackendMCPGateway { name: "backend".to_owned(), url: backend.url.parse().expect("backend URL parses"), + mcp_protocol_version: rmcp::model::ProtocolVersion::V_2026_07_28, passthrough_headers: Vec::new(), add_headers: HashMap::new(), remove_headers: Vec::new(), From 193525d84741453c344a4fdbd1935e88bc5d428c Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Wed, 26 Aug 2026 15:20:58 +0100 Subject: [PATCH 05/12] Adding addtional tests for modern and legacy clients. Updating APIs Signed-off-by: Dawid Nowak --- schemas/user_config.json | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/schemas/user_config.json b/schemas/user_config.json index 69576e9e..1e959c4b 100644 --- a/schemas/user_config.json +++ b/schemas/user_config.json @@ -38,6 +38,9 @@ "type": "string", "format": "uri" }, + "mcp_protocol_version": { + "$ref": "#/$defs/ProtocolVersion" + }, "passthrough_headers": { "description": "Header names copied from the downstream request onto the upstream connection.", "type": "array", @@ -89,7 +92,16 @@ }, "default": {} }, - "allowed_resource_names": { + "disable_tool_names_filtering": { + "type": "boolean" + }, + "disable_prompt_names_filtering": { + "type": "boolean" + }, + "disable_resource_uris_filtering": { + "type": "boolean" + }, + "allowed_resource_uris": { "type": "array", "items": { "type": "string" @@ -111,11 +123,19 @@ "required": [ "name", "url", + "mcp_protocol_version", "passthrough_headers", - "allowed_resource_names", + "disable_tool_names_filtering", + "disable_prompt_names_filtering", + "disable_resource_uris_filtering", + "allowed_resource_uris", "allowed_prompt_names", "allowed_tool_names" ] + }, + "ProtocolVersion": { + "description": "Represents the MCP protocol version used for communication.\n\nThis ensures compatibility between clients and servers by specifying\nwhich version of the Model Context Protocol is being used.", + "type": "string" } } } \ No newline at end of file From fb4731cea262a9a3e369dae1739a3cd696a35fdb Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Wed, 26 Aug 2026 15:21:42 +0100 Subject: [PATCH 06/12] Adding addtional tests for modern and legacy clients. Updating secrets Signed-off-by: Dawid Nowak --- .secrets.baseline | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.secrets.baseline b/.secrets.baseline index b9b5efff..6eeb6e16 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "(?x)(Cargo\\.lock$|\\.lock$)|^\\.secrets\\.baseline$|^.secrets.baseline$", "lines": null }, - "generated_at": "2026-08-25T17:14:29Z", + "generated_at": "2026-08-26T14:21:23Z", "plugins_used": [ { "name": "AWSKeyDetector" From 4ba12319457a5f3bd25fa38e8a7dc9eed47b7924 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Thu, 27 Aug 2026 13:14:04 +0100 Subject: [PATCH 07/12] Adding addtional tests for modern and legacy clients. Removing allowed tools and resources due to overlap with aliases Signed-off-by: Dawid Nowak --- .../src/user_store.rs | 10 +- .../src/gateway/identifier_routing.rs | 119 ++++++++---------- .../src/gateway/mcp_service/initialization.rs | 8 +- .../src/gateway/mcp_service/resources.rs | 8 -- .../src/gateway/mcp_service/tools.rs | 8 -- .../tests/gateway_call_tools.rs | 62 +-------- .../tests/gateway_pagination.rs | 8 +- .../tests/gateway_resource_read.rs | 67 +--------- .../tests/support/mod.rs | 5 +- .../tests/support/plugin_gateway.rs | 8 +- .../tests/support/test_gateways.rs | 39 ++---- .../tests/secrets_detection_e2e.rs | 8 +- schemas/user_config.json | 37 +----- 13 files changed, 71 insertions(+), 316 deletions(-) diff --git a/crates/contextforge-data-plane-apis/src/user_store.rs b/crates/contextforge-data-plane-apis/src/user_store.rs index 56417495..33913885 100644 --- a/crates/contextforge-data-plane-apis/src/user_store.rs +++ b/crates/contextforge-data-plane-apis/src/user_store.rs @@ -28,19 +28,11 @@ pub struct BackendMCPGateway { #[serde(default)] pub tool_name_aliases: HashMap, #[serde(default)] - pub resource_name_aliases: HashMap, + pub resource_uri_aliases: HashMap, #[serde(default)] pub prompt_name_aliases: HashMap, #[serde(default)] pub completion: HashMap, - - pub disable_tool_names_filtering: bool, - pub disable_prompt_names_filtering: bool, - pub disable_resource_uris_filtering: bool, - - pub allowed_resource_uris: Vec, - pub allowed_prompt_names: Vec, - pub allowed_tool_names: Vec, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index 11431ea9..d5c8050e 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -1,4 +1,4 @@ -use contextforge_data_plane_apis::user_store::VirtualHost; +use contextforge_data_plane_apis::user_store::{BackendMCPGateway, VirtualHost}; use rmcp::{ErrorData, model::ErrorCode, service::ServiceError}; use tracing::warn; @@ -15,21 +15,16 @@ fn route_identifier<'a, N: AsRef>(identifier: &'a str, backend_names: &'a [ }) } -/// Joins a backend name and a backend-local name into the namespaced `{backend}-{rest}` form. -pub(crate) fn prefixed_name(backend_name: &str, rest: &str) -> String { - format!("{backend_name}-{rest}") -} - -/// Resolves an exact control-plane alias to its backend and upstream name. Without an alias, -/// single-backend hosts preserve the upstream name and multi-backend hosts use the legacy prefix. -pub(super) fn resolve_tool_route<'a, N: AsRef>( +fn resolve_route<'a, N: AsRef>( virtual_host: &'a VirtualHost, name: &'a str, backend_names: &'a [N], + name_extractor: impl Fn(&'a str, &'a BackendMCPGateway) -> Option<&'a String>, ) -> Option<(&'a str, &'a str)> { let mut aliases = backend_names.iter().filter_map(|backend_name| { let backend_name = backend_name.as_ref(); - let original_name = virtual_host.backends.get(backend_name)?.tool_name_aliases.get(name)?; + let backend = virtual_host.backends.get(backend_name)?; + let original_name = name_extractor(name, backend)?; Some((backend_name, original_name.as_str())) }); let alias = aliases.next(); @@ -39,60 +34,36 @@ pub(super) fn resolve_tool_route<'a, N: AsRef>( alias.or_else(|| route_identifier(name, backend_names)) } -pub(super) fn resolve_resources_route<'a, N: AsRef>( +/// Resolves an exact control-plane alias to its backend and upstream name. Without an alias, +/// single-backend hosts preserve the upstream name and multi-backend hosts use the legacy prefix. +pub(super) fn resolve_tool_route<'a, N: AsRef>( virtual_host: &'a VirtualHost, name: &'a str, backend_names: &'a [N], ) -> Option<(&'a str, &'a str)> { - let mut aliases = backend_names.iter().filter_map(|backend_name| { - let backend_name = backend_name.as_ref(); - let original_name = virtual_host.backends.get(backend_name)?.resource_name_aliases.get(name)?; - Some((backend_name, original_name.as_str())) - }); - let alias = aliases.next(); - if aliases.next().is_some() { - return None; - } - alias.or_else(|| route_identifier(name, backend_names)) + resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { + backend.tool_name_aliases.get(name) + }) } -pub(super) fn resolve_prompt_route<'a, N: AsRef>( +pub(super) fn resolve_resources_route<'a, N: AsRef>( virtual_host: &'a VirtualHost, name: &'a str, backend_names: &'a [N], ) -> Option<(&'a str, &'a str)> { - let mut aliases = backend_names.iter().filter_map(|backend_name| { - let backend_name = backend_name.as_ref(); - let original_name = virtual_host.backends.get(backend_name)?.prompt_name_aliases.get(name)?; - Some((backend_name, original_name.as_str())) - }); - let alias = aliases.next(); - if aliases.next().is_some() { - return None; - } - alias.or_else(|| route_identifier(name, backend_names)) + resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { + backend.resource_uri_aliases.get(name) + }) } -/// Returns the control-plane alias for an upstream tool when configured. Without an alias, -/// single-backend hosts preserve the upstream name and multi-backend hosts use the legacy prefix. -#[allow(dead_code)] -pub(super) fn exposed_tool_name(virtual_host: &VirtualHost, backend_name: &str, original_name: &str) -> String { - virtual_host - .backends - .get(backend_name) - .and_then(|backend| { - backend - .tool_name_aliases - .iter() - .find_map(|(alias, original)| (original == original_name).then(|| alias.clone())) - }) - .unwrap_or_else(|| { - if virtual_host.backends.len() == 1 { - original_name.to_owned() - } else { - prefixed_name(backend_name, original_name) - } - }) +pub(super) fn resolve_prompt_route<'a, N: AsRef>( + virtual_host: &'a VirtualHost, + name: &'a str, + backend_names: &'a [N], +) -> Option<(&'a str, &'a str)> { + resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { + backend.prompt_name_aliases.get(name) + }) } pub(super) fn backend_forward_error(op: &str, backend_name: &str, error: &ServiceError) -> ErrorData { @@ -112,6 +83,32 @@ pub(super) fn backend_forward_error(op: &str, backend_name: &str, error: &Servic mod tests { use super::*; + /// Joins a backend name and a backend-local name into the namespaced `{backend}-{rest}` form. + fn prefixed_name(backend_name: &str, rest: &str) -> String { + format!("{backend_name}-{rest}") + } + + /// Returns the control-plane alias for an upstream tool when configured. Without an alias, + /// single-backend hosts preserve the upstream name and multi-backend hosts use the legacy prefix. + fn exposed_tool_name(virtual_host: &VirtualHost, backend_name: &str, original_name: &str) -> String { + virtual_host + .backends + .get(backend_name) + .and_then(|backend| { + backend + .tool_name_aliases + .iter() + .find_map(|(alias, original)| (original == original_name).then(|| alias.clone())) + }) + .unwrap_or_else(|| { + if virtual_host.backends.len() == 1 { + original_name.to_owned() + } else { + prefixed_name(backend_name, original_name) + } + }) + } + #[test] fn multi_backend_route_requires_exact_backend_prefix() { let backend_names = vec!["counter-on", "counter-oneee", "counter-one"]; @@ -152,16 +149,10 @@ mod tests { "url": "http://upstream:9000/mcp", "mcp_protocol_version": "2026_07_28", "passthrough_headers": [], - "allowed_tool_names": ["get_stats", "echo"], "tool_name_aliases": { "Public.Tool": "get_stats", "Echo_Tool": "echo" }, - "allowed_resource_uris": [], - "allowed_prompt_names": [], - "disable_tool_names_filtering": true, - "disable_prompt_names_filtering": true, - "disable_resource_uris_filtering": true, } } }); @@ -187,24 +178,12 @@ mod tests { "url": "http://upstream:9000/mcp", "mcp_protocol_version": "2026_07_28", "passthrough_headers": [], - "allowed_tool_names": ["get_stats"], - "allowed_resource_uris": [], - "allowed_prompt_names": [], - "disable_tool_names_filtering": true, - "disable_prompt_names_filtering": true, - "disable_resource_uris_filtering": true, }, "other": { "name": "other", "url": "http://other:9000/mcp", "mcp_protocol_version": "2026_07_28", "passthrough_headers": [], - "allowed_tool_names": [], - "allowed_resource_uris": [], - "allowed_prompt_names": [], - "disable_tool_names_filtering": true, - "disable_prompt_names_filtering": true, - "disable_resource_uris_filtering": true, } } }); diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs index 600d4d9f..f5fad441 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs @@ -166,16 +166,10 @@ mod tests { passthrough_headers: passthrough.iter().map(|s| (*s).to_owned()).collect(), add_headers: add.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned())).collect(), remove_headers: remove.iter().map(|s| (*s).to_owned()).collect(), - allowed_tool_names: vec![], tool_name_aliases: HashMap::new(), - allowed_resource_uris: vec![], - allowed_prompt_names: vec![], - resource_name_aliases: HashMap::new(), + resource_uri_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), - disable_tool_names_filtering: false, - disable_prompt_names_filtering: false, - disable_resource_uris_filtering: false, } } diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs index e5904e88..19474ed9 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs @@ -38,14 +38,6 @@ pub(super) async fn read_resource( data: None, })?; - if !backend.disable_resource_uris_filtering && !backend.allowed_resource_uris.contains(&resource_uri) { - return Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: "Routing problem... resource not permitted".into(), - data: None, - }); - } - let service_name = backend_name.clone(); let mut backend_service = connect_backend_for_request(mcp_service, &backend_name, backend, &cx).await?; diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs index 0fb3794a..328ac0aa 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs @@ -38,14 +38,6 @@ pub(super) async fn call_tool( data: None, })?; - if !backend.disable_tool_names_filtering && !backend.allowed_tool_names.contains(&tool_name) { - return Err(ErrorData { - code: ErrorCode::INVALID_PARAMS, - message: "Routing problem... tool not permitted".into(), - data: None, - }); - } - let service_name = backend_name.clone(); let pre_result = if let Some(plugin_runtime) = &mcp_service.plugin_runtime { plugin_runtime.before_tool_call(&request, &tool_name, &service_name).await? diff --git a/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs b/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs index e1e34e2e..cf543ab5 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_call_tools.rs @@ -6,10 +6,7 @@ use tracing::{info, warn}; use support::{ListToolsGatewaySettings, TEST_USER_ID, create_client, create_gateway_with_four_counters, create_ports}; -use crate::support::{ - connect_client_with_protocol, connect_modern_client, create_gateway_with_four_counters_and_enabled_filtering, - create_gateway_with_four_legacy_counters, -}; +use crate::support::{connect_client_with_protocol, connect_modern_client, create_gateway_with_four_legacy_counters}; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] @@ -87,63 +84,6 @@ async fn plaintext_call_prefixed_backend_tools_modern_legacy() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -#[test_log::test] -async fn plaintext_call_prefixed_backend_tools_modern_modern_with_filtering() -> Result<()> { - let gateway_port = create_ports(1)[0]; - - let config = Config { - address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), - token_verification_public_key: Some("../../assets/jwt.key.pub".into()), - upstream_connection_mode: Some(UpstreamConnectionMode::PlainTextOrTls), - ..Default::default() - }; - - let user = TEST_USER_ID; - - let Ok(ListToolsGatewaySettings { handle, gateway_url, expected_tool_names, .. }) = - create_gateway_with_four_counters_and_enabled_filtering(user, config).await - else { - panic!("Invalid configuration "); - }; - - let client = create_client(user); - - let mut call_params = CallToolRequestParams::default(); - call_params.name = expected_tool_names[0].clone().into(); - let maybe_passed = assert_tools_call( - gateway_url.clone(), - client.clone(), - call_params, - "-1".to_owned(), - ProtocolVersion::V_2026_07_28, - ) - .await; - - let mut call_params = CallToolRequestParams::default(); - call_params.name = "random_tool_name".into(); - let maybe_not_passed = - assert_tools_call(gateway_url, client, call_params, "-1".to_owned(), ProtocolVersion::V_2026_07_28).await; - - handle.abort(); - - if maybe_passed.is_ok() { - info!("Test passed"); - } else { - info!("Test NOT passed {maybe_passed:?}"); - panic!() - } - - if maybe_not_passed.is_ok() { - info!("Test NOT passed {maybe_passed:?}"); - panic!() - } else { - info!("Test passed"); - } - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] async fn plaintext_call_prefixed_backend_tools_legacy_modern() -> Result<()> { diff --git a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs index a344a183..8dbb4161 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs @@ -28,16 +28,10 @@ fn paginating_backend(port: u16) -> BackendMCPGateway { passthrough_headers: Vec::new(), add_headers: HashMap::new(), remove_headers: Vec::new(), - allowed_tool_names: Vec::new(), tool_name_aliases: HashMap::new(), - allowed_resource_uris: Vec::new(), - allowed_prompt_names: Vec::new(), - resource_name_aliases: HashMap::new(), + resource_uri_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), - disable_tool_names_filtering: false, - disable_prompt_names_filtering: false, - disable_resource_uris_filtering: false, } } diff --git a/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs b/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs index d37c49d0..d6648b1d 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_resource_read.rs @@ -6,10 +6,7 @@ use tracing::{info, warn}; use support::{ListToolsGatewaySettings, TEST_USER_ID, create_client, create_gateway_with_four_counters, create_ports}; -use crate::support::{ - connect_client_with_protocol, connect_modern_client, create_gateway_with_four_counters_and_enabled_filtering, - create_gateway_with_four_legacy_counters, -}; +use crate::support::{connect_client_with_protocol, connect_modern_client, create_gateway_with_four_legacy_counters}; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] @@ -99,68 +96,6 @@ async fn plaintext_call_prefixed_read_resources_modern_legacy() -> Result<()> { Ok(()) } -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -#[test_log::test] -async fn plaintext_call_prefixed_read_resources_modern_modern_with_filtering() -> Result<()> { - let gateway_port = create_ports(1)[0]; - - let config = Config { - address: Some(format!("127.0.0.1:{gateway_port}").parse().expect("This should work")), - token_verification_public_key: Some("../../assets/jwt.key.pub".into()), - upstream_connection_mode: Some(UpstreamConnectionMode::PlainTextOrTls), - ..Default::default() - }; - - let user = TEST_USER_ID; - - let Ok(ListToolsGatewaySettings { handle, gateway_url, expected_resource_uris, .. }) = - create_gateway_with_four_counters_and_enabled_filtering(user, config).await - else { - panic!("Invalid configuration "); - }; - - let client = create_client(user); - - let call_params = ReadResourceRequestParams::new(expected_resource_uris.first().expect("should work")); - - let maybe_passed = assert_resource_read( - gateway_url.clone(), - client.clone(), - call_params, - ProtocolVersion::V_2026_07_28, - "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...".to_owned(), - ) - .await; - - let call_params = ReadResourceRequestParams::new("some_random_uri"); - - let maybe_not_passed = assert_resource_read( - gateway_url, - client, - call_params, - ProtocolVersion::V_2026_07_28, - "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...".to_owned(), - ) - .await; - - handle.abort(); - if maybe_passed.is_ok() { - info!("Test passed"); - } else { - info!("Test NOT passed {maybe_passed:?}"); - panic!() - } - - if maybe_not_passed.is_ok() { - info!("Test NOT passed {maybe_passed:?}"); - panic!() - } else { - info!("Test passed"); - } - - Ok(()) -} - #[tokio::test(flavor = "multi_thread", worker_threads = 1)] #[test_log::test] async fn plaintext_call_prefixed_read_resources_legacy_modern() -> Result<()> { diff --git a/crates/contextforge-data-plane-lib/tests/support/mod.rs b/crates/contextforge-data-plane-lib/tests/support/mod.rs index aa32984c..d7cc4829 100644 --- a/crates/contextforge-data-plane-lib/tests/support/mod.rs +++ b/crates/contextforge-data-plane-lib/tests/support/mod.rs @@ -30,9 +30,8 @@ pub(crate) use plugin_gateway::{ }; pub(crate) use runtime::{runtime_with_post, runtime_with_pre, runtime_with_pre_and_post, runtime_with_prompt_plugin}; pub(crate) use test_gateways::{ - ListToolsGatewaySettings, create_gateway_with_four_counters, - create_gateway_with_four_counters_and_enabled_filtering, create_gateway_with_four_legacy_counters, create_ports, - create_tls_gateway_with_four_tls_counters, plaintext_config, + ListToolsGatewaySettings, create_gateway_with_four_counters, create_gateway_with_four_legacy_counters, + create_ports, create_tls_gateway_with_four_tls_counters, plaintext_config, }; pub(crate) use tool::{error_code, error_parts, sum_request, text}; pub(crate) use user_config_store::MemoryUserConfigStore; diff --git a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs index 6589c716..ca49433a 100644 --- a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs +++ b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs @@ -336,16 +336,10 @@ async fn start_gateway_with_state( passthrough_headers: Vec::new(), add_headers: HashMap::default(), remove_headers: Vec::new(), - allowed_tool_names: Vec::new(), tool_name_aliases: HashMap::new(), - allowed_resource_uris: Vec::new(), - allowed_prompt_names: Vec::new(), - resource_name_aliases: HashMap::new(), + resource_uri_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), - disable_tool_names_filtering: true, - disable_prompt_names_filtering: true, - disable_resource_uris_filtering: true, }, )]), }, diff --git a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs index 14c07983..ba641c38 100644 --- a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs +++ b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs @@ -146,14 +146,6 @@ pub(crate) async fn create_gateway_with_four_legacy_counters( create_gateway_with_four_counters_and_custom_config(user, config, create_plain_legacy_backends).await } -pub(crate) async fn create_gateway_with_four_counters_and_enabled_filtering( - user: &str, - config: Config, -) -> Result { - create_gateway_with_four_counters_and_custom_config(user, config, create_plain_backends_with_enabled_filtering) - .await -} - pub(crate) async fn create_tls_gateway_with_four_tls_counters( user: &str, config: Config, @@ -164,7 +156,6 @@ pub(crate) async fn create_tls_gateway_with_four_tls_counters( fn create_backends( ports: &[u16], with_tls: bool, - test_disable_filtering: bool, protocol_version: &rmcp::model::ProtocolVersion, ) -> HashMap { ports @@ -186,25 +177,21 @@ fn create_backends( passthrough_headers: Vec::new(), add_headers: HashMap::default(), remove_headers: Vec::new(), - allowed_tool_names: MOCK_COUNTER_TOOL_NAMES - .iter() - .map(|tool_name| (*tool_name).to_owned()) - .collect(), tool_name_aliases: MOCK_COUNTER_TOOL_NAMES .iter() .map(|tool_name| (format!("backend-{port}.{tool_name}"), (*tool_name).to_owned())) .collect(), - allowed_resource_uris: MOCK_COUNTER_RESOURCE_URIS + + resource_uri_aliases: MOCK_COUNTER_RESOURCE_URIS + .iter() + .map(|resource_uri| (format!("backend-{port}.{resource_uri}"), (*resource_uri).to_owned())) + .collect(), + prompt_name_aliases: MOCK_COUNTER_PROMPT_NAMES .iter() - .map(|resource_name| (*resource_name).to_owned()) + .map(|resource_uri| (format!("backend-{port}.{resource_uri}"), (*resource_uri).to_owned())) .collect(), - allowed_prompt_names: Vec::new(), - resource_name_aliases: HashMap::new(), - prompt_name_aliases: HashMap::new(), + completion: HashMap::new(), - disable_tool_names_filtering: test_disable_filtering, - disable_prompt_names_filtering: test_disable_filtering, - disable_resource_uris_filtering: test_disable_filtering, }, ) }) @@ -212,19 +199,15 @@ fn create_backends( } fn create_plain_backends(ports: &[u16]) -> HashMap { - create_backends(ports, false, true, &rmcp::model::ProtocolVersion::V_2026_07_28) + create_backends(ports, false, &rmcp::model::ProtocolVersion::V_2026_07_28) } fn create_plain_legacy_backends(ports: &[u16]) -> HashMap { - create_backends(ports, false, true, &rmcp::model::ProtocolVersion::V_2025_11_25) -} - -fn create_plain_backends_with_enabled_filtering(ports: &[u16]) -> HashMap { - create_backends(ports, false, false, &rmcp::model::ProtocolVersion::V_2026_07_28) + create_backends(ports, false, &rmcp::model::ProtocolVersion::V_2025_11_25) } fn create_tls_backends(ports: &[u16]) -> HashMap { - create_backends(ports, true, true, &rmcp::model::ProtocolVersion::V_2026_07_28) + create_backends(ports, true, &rmcp::model::ProtocolVersion::V_2026_07_28) } fn backend_id(port: u16) -> String { diff --git a/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs b/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs index 9750d775..bbc02351 100644 --- a/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs +++ b/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs @@ -381,16 +381,10 @@ async fn write_redis_config(redis_port: u16, backend: &RunningBackend) { passthrough_headers: Vec::new(), add_headers: HashMap::new(), remove_headers: Vec::new(), - allowed_tool_names: Vec::new(), tool_name_aliases: HashMap::new(), - allowed_resource_uris: Vec::new(), - allowed_prompt_names: Vec::new(), - resource_name_aliases: HashMap::new(), + resource_uri_aliases: HashMap::new(), prompt_name_aliases: HashMap::new(), completion: HashMap::new(), - disable_tool_names_filtering: false, - disable_prompt_names_filtering: false, - disable_resource_uris_filtering: false, }, )]), }, diff --git a/schemas/user_config.json b/schemas/user_config.json index 1e959c4b..564dd734 100644 --- a/schemas/user_config.json +++ b/schemas/user_config.json @@ -71,7 +71,7 @@ }, "default": {} }, - "resource_name_aliases": { + "resource_uri_aliases": { "type": "object", "additionalProperties": { "type": "string" @@ -91,46 +91,13 @@ "type": "string" }, "default": {} - }, - "disable_tool_names_filtering": { - "type": "boolean" - }, - "disable_prompt_names_filtering": { - "type": "boolean" - }, - "disable_resource_uris_filtering": { - "type": "boolean" - }, - "allowed_resource_uris": { - "type": "array", - "items": { - "type": "string" - } - }, - "allowed_prompt_names": { - "type": "array", - "items": { - "type": "string" - } - }, - "allowed_tool_names": { - "type": "array", - "items": { - "type": "string" - } } }, "required": [ "name", "url", "mcp_protocol_version", - "passthrough_headers", - "disable_tool_names_filtering", - "disable_prompt_names_filtering", - "disable_resource_uris_filtering", - "allowed_resource_uris", - "allowed_prompt_names", - "allowed_tool_names" + "passthrough_headers" ] }, "ProtocolVersion": { From 9f0685f3c610c3ae9aad211766623843d9df8e56 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Thu, 27 Aug 2026 22:04:58 +0100 Subject: [PATCH 08/12] Adding addtional tests for modern and legacy clients. Alias based routing refactoring Signed-off-by: Dawid Nowak --- .../src/user_store.rs | 42 ++++++++++++++-- .../src/gateway/identifier_routing.rs | 50 ++++++++++++------- .../src/gateway/mcp_service/initialization.rs | 8 +-- .../tests/gateway_pagination.rs | 11 ++-- .../tests/support/plugin_gateway.rs | 8 +-- .../tests/support/test_gateways.rs | 12 +++-- .../tests/secrets_detection_e2e.rs | 8 +-- 7 files changed, 97 insertions(+), 42 deletions(-) diff --git a/crates/contextforge-data-plane-apis/src/user_store.rs b/crates/contextforge-data-plane-apis/src/user_store.rs index 33913885..c9723595 100644 --- a/crates/contextforge-data-plane-apis/src/user_store.rs +++ b/crates/contextforge-data-plane-apis/src/user_store.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -12,6 +12,40 @@ pub enum IntegrationType { Mcp, } +#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default, Eq)] +pub struct NameAlias { + downstream_prefixed_name: String, + upstream_name: String, +} + +impl PartialEq for NameAlias { + fn eq(&self, other: &Self) -> bool { + self.downstream_prefixed_name == other.downstream_prefixed_name + } +} + +impl std::hash::Hash for NameAlias { + fn hash(&self, state: &mut H) { + self.downstream_prefixed_name.hash(state); + } +} + +impl NameAlias { + pub fn new(downstream_prefixed_name: String, upstream_name: String) -> Self { + Self { downstream_prefixed_name, upstream_name } + } + pub fn with_downstream_prefixed_name(downstream_prefixed_name: String) -> Self { + NameAlias { downstream_prefixed_name, upstream_name: String::new() } + } + pub fn get_upstream_name(&self) -> &str { + &self.upstream_name + } + + pub fn get_downstream_prefixed_name(&self) -> &str { + &self.downstream_prefixed_name + } +} + #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)] pub struct BackendMCPGateway { pub name: String, @@ -26,11 +60,11 @@ pub struct BackendMCPGateway { #[serde(default)] pub remove_headers: Vec, #[serde(default)] - pub tool_name_aliases: HashMap, + pub tool_name_aliases: HashSet, #[serde(default)] - pub resource_uri_aliases: HashMap, + pub resource_uri_aliases: HashSet, #[serde(default)] - pub prompt_name_aliases: HashMap, + pub prompt_name_aliases: HashSet, #[serde(default)] pub completion: HashMap, } diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index d5c8050e..42824886 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -1,4 +1,4 @@ -use contextforge_data_plane_apis::user_store::{BackendMCPGateway, VirtualHost}; +use contextforge_data_plane_apis::user_store::{BackendMCPGateway, NameAlias, VirtualHost}; use rmcp::{ErrorData, model::ErrorCode, service::ServiceError}; use tracing::warn; @@ -19,13 +19,13 @@ fn resolve_route<'a, N: AsRef>( virtual_host: &'a VirtualHost, name: &'a str, backend_names: &'a [N], - name_extractor: impl Fn(&'a str, &'a BackendMCPGateway) -> Option<&'a String>, + name_extractor: impl Fn(&'a str, &'a BackendMCPGateway) -> Option<&'a str>, ) -> Option<(&'a str, &'a str)> { let mut aliases = backend_names.iter().filter_map(|backend_name| { let backend_name = backend_name.as_ref(); let backend = virtual_host.backends.get(backend_name)?; - let original_name = name_extractor(name, backend)?; - Some((backend_name, original_name.as_str())) + let upstream_name = name_extractor(name, backend)?; + Some((backend_name, upstream_name)) }); let alias = aliases.next(); if aliases.next().is_some() { @@ -42,7 +42,10 @@ pub(super) fn resolve_tool_route<'a, N: AsRef>( backend_names: &'a [N], ) -> Option<(&'a str, &'a str)> { resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { - backend.tool_name_aliases.get(name) + backend + .tool_name_aliases + .get(&NameAlias::with_downstream_prefixed_name(name.to_owned())) + .map(NameAlias::get_upstream_name) }) } @@ -52,7 +55,10 @@ pub(super) fn resolve_resources_route<'a, N: AsRef>( backend_names: &'a [N], ) -> Option<(&'a str, &'a str)> { resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { - backend.resource_uri_aliases.get(name) + backend + .resource_uri_aliases + .get(&NameAlias::with_downstream_prefixed_name(name.to_owned())) + .map(NameAlias::get_upstream_name) }) } @@ -62,7 +68,10 @@ pub(super) fn resolve_prompt_route<'a, N: AsRef>( backend_names: &'a [N], ) -> Option<(&'a str, &'a str)> { resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { - backend.prompt_name_aliases.get(name) + backend + .prompt_name_aliases + .get(&NameAlias::with_downstream_prefixed_name(name.to_owned())) + .map(NameAlias::get_upstream_name) }) } @@ -98,15 +107,18 @@ mod tests { backend .tool_name_aliases .iter() - .find_map(|(alias, original)| (original == original_name).then(|| alias.clone())) - }) - .unwrap_or_else(|| { - if virtual_host.backends.len() == 1 { - original_name.to_owned() - } else { - prefixed_name(backend_name, original_name) - } + .find_map(|alias| (alias.get_upstream_name() == original_name).then(|| alias.clone())) }) + .map_or_else( + || { + if virtual_host.backends.len() == 1 { + original_name.to_owned() + } else { + prefixed_name(backend_name, original_name) + } + }, + |a| a.get_downstream_prefixed_name().to_owned(), + ) } #[test] @@ -149,10 +161,10 @@ mod tests { "url": "http://upstream:9000/mcp", "mcp_protocol_version": "2026_07_28", "passthrough_headers": [], - "tool_name_aliases": { - "Public.Tool": "get_stats", - "Echo_Tool": "echo" - }, + "tool_name_aliases": [ + {"downstream_prefixed_name":"Public.Tool", "upstream_name":"get_stats"}, + {"downstream_prefixed_name":"Echo_Tool", "upstream_name":"echo"} + ] } } }); diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs index f5fad441..f64b479e 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs @@ -156,6 +156,8 @@ fn is_protected_header(name: &http::HeaderName) -> bool { #[cfg(test)] mod tests { + use std::collections::HashSet; + use super::*; fn backend(passthrough: &[&str], add: &[(&str, &str)], remove: &[&str]) -> BackendMCPGateway { @@ -166,9 +168,9 @@ mod tests { passthrough_headers: passthrough.iter().map(|s| (*s).to_owned()).collect(), add_headers: add.iter().map(|(k, v)| ((*k).to_owned(), (*v).to_owned())).collect(), remove_headers: remove.iter().map(|s| (*s).to_owned()).collect(), - tool_name_aliases: HashMap::new(), - resource_uri_aliases: HashMap::new(), - prompt_name_aliases: HashMap::new(), + tool_name_aliases: HashSet::new(), + resource_uri_aliases: HashSet::new(), + prompt_name_aliases: HashSet::new(), completion: HashMap::new(), } } diff --git a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs index 8dbb4161..271cd79a 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_pagination.rs @@ -1,6 +1,9 @@ mod support; -use std::{collections::HashMap, sync::Arc}; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; use contextforge_data_plane_apis::{ User, @@ -28,9 +31,9 @@ fn paginating_backend(port: u16) -> BackendMCPGateway { passthrough_headers: Vec::new(), add_headers: HashMap::new(), remove_headers: Vec::new(), - tool_name_aliases: HashMap::new(), - resource_uri_aliases: HashMap::new(), - prompt_name_aliases: HashMap::new(), + tool_name_aliases: HashSet::new(), + resource_uri_aliases: HashSet::new(), + prompt_name_aliases: HashSet::new(), completion: HashMap::new(), } } diff --git a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs index ca49433a..be8e0738 100644 --- a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs +++ b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, sync::{Arc, Mutex as StdMutex, OnceLock}, time::{Duration, Instant}, }; @@ -336,9 +336,9 @@ async fn start_gateway_with_state( passthrough_headers: Vec::new(), add_headers: HashMap::default(), remove_headers: Vec::new(), - tool_name_aliases: HashMap::new(), - resource_uri_aliases: HashMap::new(), - prompt_name_aliases: HashMap::new(), + tool_name_aliases: HashSet::new(), + resource_uri_aliases: HashSet::new(), + prompt_name_aliases: HashSet::new(), completion: HashMap::new(), }, )]), diff --git a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs index ba641c38..f81a3cc0 100644 --- a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs +++ b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; use contextforge_data_plane_apis::{ User, - user_store::{BackendMCPGateway, UserConfig, VirtualHost}, + user_store::{BackendMCPGateway, NameAlias, UserConfig, VirtualHost}, }; use contextforge_data_plane_lib::{ Config, Gateway, Result, UpstreamConnectionMode, UserConfigStore, UserConfigStoreType, @@ -179,16 +179,20 @@ fn create_backends( remove_headers: Vec::new(), tool_name_aliases: MOCK_COUNTER_TOOL_NAMES .iter() - .map(|tool_name| (format!("backend-{port}.{tool_name}"), (*tool_name).to_owned())) + .map(|tool_name| NameAlias::new(format!("backend-{port}.{tool_name}"), tool_name.to_string())) .collect(), resource_uri_aliases: MOCK_COUNTER_RESOURCE_URIS .iter() - .map(|resource_uri| (format!("backend-{port}.{resource_uri}"), (*resource_uri).to_owned())) + .map(|resource_uri| { + NameAlias::new(format!("backend-{port}.{resource_uri}"), resource_uri.to_string()) + }) .collect(), prompt_name_aliases: MOCK_COUNTER_PROMPT_NAMES .iter() - .map(|resource_uri| (format!("backend-{port}.{resource_uri}"), (*resource_uri).to_owned())) + .map(|prompt_name| { + NameAlias::new(format!("backend-{port}.{prompt_name}"), prompt_name.to_string()) + }) .collect(), completion: HashMap::new(), diff --git a/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs b/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs index bbc02351..8ed8aebb 100644 --- a/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs +++ b/crates/contextforge-data-plane/tests/secrets_detection_e2e.rs @@ -4,7 +4,7 @@ #![cfg(feature = "plugins")] use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fs, net::TcpStream as StdTcpStream, path::PathBuf, @@ -381,9 +381,9 @@ async fn write_redis_config(redis_port: u16, backend: &RunningBackend) { passthrough_headers: Vec::new(), add_headers: HashMap::new(), remove_headers: Vec::new(), - tool_name_aliases: HashMap::new(), - resource_uri_aliases: HashMap::new(), - prompt_name_aliases: HashMap::new(), + tool_name_aliases: HashSet::new(), + resource_uri_aliases: HashSet::new(), + prompt_name_aliases: HashSet::new(), completion: HashMap::new(), }, )]), From 730d0b1390bc9a3cf185f2edd896f830fc7804e5 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Thu, 27 Aug 2026 22:11:24 +0100 Subject: [PATCH 09/12] Adding addtional tests for modern and legacy clients. Alias based routing refactoring.2 Signed-off-by: Dawid Nowak --- .../src/gateway/identifier_routing.rs | 15 ++++++++------- .../src/gateway/mcp_service/prompts.rs | 8 +++++++- .../src/gateway/mcp_service/resources.rs | 8 +++++++- .../src/gateway/mcp_service/tools.rs | 8 +++++++- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index 42824886..c9651812 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -20,7 +20,7 @@ fn resolve_route<'a, N: AsRef>( name: &'a str, backend_names: &'a [N], name_extractor: impl Fn(&'a str, &'a BackendMCPGateway) -> Option<&'a str>, -) -> Option<(&'a str, &'a str)> { +) -> Result, Box> { let mut aliases = backend_names.iter().filter_map(|backend_name| { let backend_name = backend_name.as_ref(); let backend = virtual_host.backends.get(backend_name)?; @@ -29,9 +29,9 @@ fn resolve_route<'a, N: AsRef>( }); let alias = aliases.next(); if aliases.next().is_some() { - return None; + return Err(format!("Multiple backends found for {name}").into()); } - alias.or_else(|| route_identifier(name, backend_names)) + Ok(alias.or_else(|| route_identifier(name, backend_names))) } /// Resolves an exact control-plane alias to its backend and upstream name. Without an alias, @@ -40,7 +40,7 @@ pub(super) fn resolve_tool_route<'a, N: AsRef>( virtual_host: &'a VirtualHost, name: &'a str, backend_names: &'a [N], -) -> Option<(&'a str, &'a str)> { +) -> Result, Box> { resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { backend .tool_name_aliases @@ -53,7 +53,7 @@ pub(super) fn resolve_resources_route<'a, N: AsRef>( virtual_host: &'a VirtualHost, name: &'a str, backend_names: &'a [N], -) -> Option<(&'a str, &'a str)> { +) -> Result, Box> { resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { backend .resource_uri_aliases @@ -66,7 +66,7 @@ pub(super) fn resolve_prompt_route<'a, N: AsRef>( virtual_host: &'a VirtualHost, name: &'a str, backend_names: &'a [N], -) -> Option<(&'a str, &'a str)> { +) -> Result, Box> { resolve_route(virtual_host, name, backend_names, |name: &'a str, backend: &'a BackendMCPGateway| { backend .prompt_name_aliases @@ -177,7 +177,7 @@ mod tests { ); assert_eq!( Some(("79fabb70-2188-4de8-95ed-dc1e976e14d4", "get_stats")), - resolve_tool_route(&virtual_host, "Public.Tool", &backend_ids) + resolve_tool_route(&virtual_host, "Public.Tool", &backend_ids).expect("this should work") ); } @@ -209,6 +209,7 @@ mod tests { assert_eq!( Some(("compliance-reference", "get_stats")), resolve_tool_route(&virtual_host, "compliance-reference-get_stats", &backend_names) + .expect("this should work") ); } } diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/prompts.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/prompts.rs index e8b9a18e..f3b208d1 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/prompts.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/prompts.rs @@ -21,7 +21,13 @@ pub(super) async fn get_prompt( let mcp_call_validator = AuthorizedCallValidator::new("get_prompt", &cx); let (virtual_host, _claims) = mcp_call_validator.validate_stateless()?; let backend_names: Vec<&str> = virtual_host.backends.keys().map(String::as_str).collect(); - let Some((backend_name, prompt_name)) = resolve_prompt_route(virtual_host, &request.name, &backend_names) else { + let Some((backend_name, prompt_name)) = + resolve_prompt_route(virtual_host, &request.name, &backend_names).map_err(|e| ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: format!("Routing problem... {e}").into(), + data: None, + })? + else { return Err(ErrorData { code: ErrorCode::INVALID_PARAMS, message: "Routing problem... promtp not found".into(), diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs index 19474ed9..b4e02799 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/resources.rs @@ -22,7 +22,13 @@ pub(super) async fn read_resource( let mcp_call_validator = AuthorizedCallValidator::new("read_resource", &cx); let (virtual_host, _claims) = mcp_call_validator.validate_stateless()?; let backend_names: Vec<&str> = virtual_host.backends.keys().map(String::as_str).collect(); - let Some((backend_name, resource_uri)) = resolve_resources_route(virtual_host, &request.uri, &backend_names) else { + let Some((backend_name, resource_uri)) = resolve_resources_route(virtual_host, &request.uri, &backend_names) + .map_err(|e| ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: format!("Routing problem... {e}").into(), + data: None, + })? + else { return Err(ErrorData { code: ErrorCode::INVALID_PARAMS, message: "Routing problem... resource not found".into(), diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs index 328ac0aa..b4a82561 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/tools.rs @@ -22,7 +22,13 @@ pub(super) async fn call_tool( let mcp_call_validator = AuthorizedCallValidator::new("call_tool", &cx); let (virtual_host, _claims) = mcp_call_validator.validate_stateless()?; let backend_names: Vec<&str> = virtual_host.backends.keys().map(String::as_str).collect(); - let Some((backend_name, tool_name)) = resolve_tool_route(virtual_host, &request.name, &backend_names) else { + let Some((backend_name, tool_name)) = + resolve_tool_route(virtual_host, &request.name, &backend_names).map_err(|e| ErrorData { + code: ErrorCode::INVALID_PARAMS, + message: format!("Routing problem... {e}").into(), + data: None, + })? + else { return Err(ErrorData { code: ErrorCode::INVALID_PARAMS, message: "Routing problem... tool not found".into(), From 107eeb7a092f5c58b1c547679f619f6b1f70afe6 Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Fri, 28 Aug 2026 09:12:19 +0100 Subject: [PATCH 10/12] Adding addtional tests for modern and legacy clients. Alias based routing refactoring.3 Signed-off-by: Dawid Nowak --- .../src/gateway/identifier_routing.rs | 30 +++++++++---------- .../tests/gateway_plugins.rs | 15 +++------- .../tests/support/plugin_gateway.rs | 28 ++++++++++++++--- .../tests/support/test_gateways.rs | 18 +++++++---- 4 files changed, 56 insertions(+), 35 deletions(-) diff --git a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs index c9651812..0f8333f7 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/identifier_routing.rs @@ -2,19 +2,6 @@ use contextforge_data_plane_apis::user_store::{BackendMCPGateway, NameAlias, Vir use rmcp::{ErrorData, model::ErrorCode, service::ServiceError}; use tracing::warn; -/// Preserves identifiers for a single backend. For multiple backends, splits a -/// `{backend}-{identifier}` namespace so duplicate identifiers remain routable. -fn route_identifier<'a, N: AsRef>(identifier: &'a str, backend_names: &'a [N]) -> Option<(&'a str, &'a str)> { - if let [backend] = backend_names { - return Some((backend.as_ref(), identifier)); - } - - backend_names.iter().find_map(|backend| { - let backend = backend.as_ref(); - identifier.strip_prefix(backend)?.strip_prefix('-').map(|rest| (backend, rest)) - }) -} - fn resolve_route<'a, N: AsRef>( virtual_host: &'a VirtualHost, name: &'a str, @@ -31,7 +18,7 @@ fn resolve_route<'a, N: AsRef>( if aliases.next().is_some() { return Err(format!("Multiple backends found for {name}").into()); } - Ok(alias.or_else(|| route_identifier(name, backend_names))) + Ok(alias) } /// Resolves an exact control-plane alias to its backend and upstream name. Without an alias, @@ -92,6 +79,19 @@ pub(super) fn backend_forward_error(op: &str, backend_name: &str, error: &Servic mod tests { use super::*; + /// Preserves identifiers for a single backend. For multiple backends, splits a + /// `{backend}-{identifier}` namespace so duplicate identifiers remain routable. + fn route_identifier<'a, N: AsRef>(identifier: &'a str, backend_names: &'a [N]) -> Option<(&'a str, &'a str)> { + if let [backend] = backend_names { + return Some((backend.as_ref(), identifier)); + } + + backend_names.iter().find_map(|backend| { + let backend = backend.as_ref(); + identifier.strip_prefix(backend)?.strip_prefix('-').map(|rest| (backend, rest)) + }) + } + /// Joins a backend name and a backend-local name into the namespaced `{backend}-{rest}` form. fn prefixed_name(backend_name: &str, rest: &str) -> String { format!("{backend_name}-{rest}") @@ -206,7 +206,7 @@ mod tests { "compliance-reference-get_stats", exposed_tool_name(&virtual_host, "compliance-reference", "get_stats") ); - assert_eq!( + assert_ne!( Some(("compliance-reference", "get_stats")), resolve_tool_route(&virtual_host, "compliance-reference-get_stats", &backend_names) .expect("this should work") diff --git a/crates/contextforge-data-plane-lib/tests/gateway_plugins.rs b/crates/contextforge-data-plane-lib/tests/gateway_plugins.rs index 51560f6a..7385703f 100644 --- a/crates/contextforge-data-plane-lib/tests/gateway_plugins.rs +++ b/crates/contextforge-data-plane-lib/tests/gateway_plugins.rs @@ -400,7 +400,7 @@ async fn stateless_tool_error_round_trips() { let rmcp::service::ServiceError::McpError(error) = error else { panic!("expected backend MCP error, got {error:?}"); }; - assert_eq!(ErrorCode::METHOD_NOT_FOUND, error.code); + assert_eq!(ErrorCode::INVALID_PARAMS, error.code); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] @@ -416,17 +416,10 @@ async fn stateless_alias_and_namespaced_tool_names_route() { support::modern_client_info(), ) .await; - let alias = expected_tool_names - .iter() - .find(|name| std::path::Path::new(name).extension().is_some_and(|ext| ext.eq_ignore_ascii_case("sum"))) - .expect("sum alias is advertised"); - let backend_port = alias - .strip_prefix("backend-") - .and_then(|name| name.strip_suffix(".sum")) - .expect("alias contains the backend port"); - let namespaced_name = format!("00000000-0000-0000-0000-{backend_port:0>12}-sum"); + let alias = expected_tool_names.iter().find(|name| name.ends_with("sum")).expect("sum alias is advertised"); + let alias_result = service.call_tool(sum_request(alias, 1, 2)).await.expect("alias routes"); - let namespaced_result = service.call_tool(sum_request(&namespaced_name, 3, 4)).await.expect("namespace routes"); + let namespaced_result = service.call_tool(sum_request(alias, 3, 4)).await.expect("namespace routes"); assert_eq!("3", text(&alias_result)); assert_eq!("7", text(&namespaced_result)); handle.abort(); diff --git a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs index be8e0738..4e7af915 100644 --- a/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs +++ b/crates/contextforge-data-plane-lib/tests/support/plugin_gateway.rs @@ -6,7 +6,7 @@ use std::{ use contextforge_data_plane_apis::{ User, - user_store::{BackendMCPGateway, UserConfig, VirtualHost}, + user_store::{BackendMCPGateway, NameAlias, UserConfig, VirtualHost}, }; use contextforge_data_plane_cpex::CpexRuntimeRegistry; use contextforge_data_plane_lib::{Config, Gateway, UpstreamConnectionMode, UserConfigStore, UserConfigStoreType}; @@ -194,6 +194,17 @@ impl ServerHandler for TestBackend { } } +pub const TOOL_NAMES: &[&str] = &[ + "progress_counter_tokens", + "progress_sum", + "sum", + "progress_counter_tokens", + "reflect_text", + "wait_for_cancellation", +]; +pub const RESOURCE_URIS: &[&str] = &[""]; +pub const PROMPT_NAMES: &[&str] = &["review_bundle", "review"]; + pub(crate) struct RunningGateway { pub(crate) backend_state: BackendState, pub(crate) backend_name: String, @@ -336,9 +347,18 @@ async fn start_gateway_with_state( passthrough_headers: Vec::new(), add_headers: HashMap::default(), remove_headers: Vec::new(), - tool_name_aliases: HashSet::new(), - resource_uri_aliases: HashSet::new(), - prompt_name_aliases: HashSet::new(), + tool_name_aliases: TOOL_NAMES + .iter() + .map(|n| NameAlias::new(n.to_string(), n.to_string())) + .collect(), + resource_uri_aliases: RESOURCE_URIS + .iter() + .map(|n| NameAlias::new(n.to_string(), n.to_string())) + .collect(), + prompt_name_aliases: PROMPT_NAMES + .iter() + .map(|n| NameAlias::new(n.to_string(), n.to_string())) + .collect(), completion: HashMap::new(), }, )]), diff --git a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs index f81a3cc0..6c1ecfa4 100644 --- a/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs +++ b/crates/contextforge-data-plane-lib/tests/support/test_gateways.rs @@ -169,7 +169,7 @@ fn create_backends( let backend_id = backend_id(*port); ( - backend_id, + backend_id.clone(), BackendMCPGateway { name: format!("backend-{port}"), url, @@ -179,19 +179,24 @@ fn create_backends( remove_headers: Vec::new(), tool_name_aliases: MOCK_COUNTER_TOOL_NAMES .iter() - .map(|tool_name| NameAlias::new(format!("backend-{port}.{tool_name}"), tool_name.to_string())) + .map(|tool_name| { + let backend_id = backend_id.clone(); + NameAlias::new(format!("{backend_id}-{tool_name}"), tool_name.to_string()) + }) .collect(), resource_uri_aliases: MOCK_COUNTER_RESOURCE_URIS .iter() .map(|resource_uri| { - NameAlias::new(format!("backend-{port}.{resource_uri}"), resource_uri.to_string()) + let backend_id = backend_id.clone(); + NameAlias::new(format!("{backend_id}-{resource_uri}"), resource_uri.to_string()) }) .collect(), prompt_name_aliases: MOCK_COUNTER_PROMPT_NAMES .iter() .map(|prompt_name| { - NameAlias::new(format!("backend-{port}.{prompt_name}"), prompt_name.to_string()) + let backend_id = backend_id.clone(); + NameAlias::new(format!("{backend_id}-{prompt_name}"), prompt_name.to_string()) }) .collect(), @@ -221,7 +226,10 @@ fn backend_id(port: u16) -> String { fn create_tool_names(ports: &[u16]) -> Vec { ports .iter() - .flat_map(|port| MOCK_COUNTER_TOOL_NAMES.iter().map(move |name| format!("backend-{port}.{name}"))) + .flat_map(|port| { + let backend_id = backend_id(*port); + MOCK_COUNTER_TOOL_NAMES.iter().map(move |name| format!("{backend_id}-{name}")) + }) .collect() } From d9fea3a25d9a84dfe16ca633ead35a07b64851fb Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Fri, 28 Aug 2026 09:18:02 +0100 Subject: [PATCH 11/12] Adding addtional tests for modern and legacy clients. Lifecycle mode Signed-off-by: Dawid Nowak --- .../src/gateway/mcp_service/initialization.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs index f64b479e..b919a26a 100644 --- a/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs +++ b/crates/contextforge-data-plane-lib/src/gateway/mcp_service/initialization.rs @@ -69,7 +69,10 @@ pub(super) async fn connect_backend_for_request( serve_client_with_lifecycle_and_ct( backend_client, transport, - ClientLifecycleMode::Discover { preferred_versions: vec![ProtocolVersion::V_2026_07_28] }, + ClientLifecycleMode::Auto { + preferred_versions: vec![backend.mcp_protocol_version.clone()], + legacy_version: Some(backend.mcp_protocol_version.clone()), + }, cx.ct.clone(), ) .await From 98d0f3a8692e810ec9b730030ba7c23db2fb76eb Mon Sep 17 00:00:00 2001 From: Dawid Nowak Date: Fri, 28 Aug 2026 10:31:01 +0100 Subject: [PATCH 12/12] Adding addtional tests for modern and legacy clients. APIs change Signed-off-by: Dawid Nowak --- schemas/user_config.json | 42 ++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/schemas/user_config.json b/schemas/user_config.json index 564dd734..359d0e9b 100644 --- a/schemas/user_config.json +++ b/schemas/user_config.json @@ -65,25 +65,28 @@ "default": [] }, "tool_name_aliases": { - "type": "object", - "additionalProperties": { - "type": "string" + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/NameAlias" }, - "default": {} + "default": [] }, "resource_uri_aliases": { - "type": "object", - "additionalProperties": { - "type": "string" + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/NameAlias" }, - "default": {} + "default": [] }, "prompt_name_aliases": { - "type": "object", - "additionalProperties": { - "type": "string" + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/NameAlias" }, - "default": {} + "default": [] }, "completion": { "type": "object", @@ -103,6 +106,21 @@ "ProtocolVersion": { "description": "Represents the MCP protocol version used for communication.\n\nThis ensures compatibility between clients and servers by specifying\nwhich version of the Model Context Protocol is being used.", "type": "string" + }, + "NameAlias": { + "type": "object", + "properties": { + "downstream_prefixed_name": { + "type": "string" + }, + "upstream_name": { + "type": "string" + } + }, + "required": [ + "downstream_prefixed_name", + "upstream_name" + ] } } } \ No newline at end of file