From 00f92027922f8ca6d2f293dbbcc849c3c868efdb Mon Sep 17 00:00:00 2001 From: Alex Phonpradith <@openai.com> Date: Mon, 2 Nov 2026 23:04:31 -0800 Subject: [PATCH 1/2] fix: preserve tools with dependentRequired property-name maps --- .../rmcp/src/transport/common/mcp_headers.rs | 33 ++++++++++++++- .../src/transport/streamable_http_client.rs | 40 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/crates/rmcp/src/transport/common/mcp_headers.rs b/crates/rmcp/src/transport/common/mcp_headers.rs index fe75b34b9..294abb7bc 100644 --- a/crates/rmcp/src/transport/common/mcp_headers.rs +++ b/crates/rmcp/src/transport/common/mcp_headers.rs @@ -211,8 +211,8 @@ fn has_off_chain_annotation(schema: &JsonObject, on_chain: bool) -> bool { "$defs" | "definitions" | "dependentSchemas" | "patternProperties" => { schema_map_has_off_chain_annotation(value, false) } - // Instance data, not subschemas. - "default" | "const" | "enum" | "examples" => false, + // These contain instance data or property names, not subschemas. + "default" | "const" | "enum" | "examples" | "dependentRequired" => false, _ => subschema_has_off_chain_annotation(value, false), }) } @@ -735,6 +735,35 @@ mod tests { assert!(validate_param_header_annotations(&schema).is_err()); } + #[test] + fn accepts_property_name_in_dependent_required() { + let schema = json!({ + "type": "object", + "properties": { + "x-mcp-header": { "type": "string" }, + "tenant": { "type": "string", "x-mcp-header": "Tenant" } + }, + "dependentRequired": { "x-mcp-header": ["tenant"] } + }); + assert_eq!( + validate_param_header_annotations(schema.as_object().unwrap()), + Ok(()) + ); + } + + #[test] + fn rejects_header_annotation_inside_dependent_schema() { + let schema = json!({ + "type": "object", + "dependentSchemas": { + "tenant": { + "properties": { "region": { "type": "string", "x-mcp-header": "Region" } } + } + } + }); + assert!(validate_param_header_annotations(schema.as_object().unwrap()).is_err()); + } + #[test] fn ignores_annotation_key_in_default_value() { let schema = schema_with(json!({ diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs index db44ab3fb..8b964be94 100644 --- a/crates/rmcp/src/transport/streamable_http_client.rs +++ b/crates/rmcp/src/transport/streamable_http_client.rs @@ -2603,6 +2603,46 @@ mod tests { ); } + #[test] + fn cache_tools_keeps_property_names_in_dependent_required() { + let valid = tool( + "valid", + json!({ + "type": "object", + "properties": { + "x-mcp-header": { "type": "string" }, + "tenant": { "type": "string", "x-mcp-header": "Tenant" } + }, + "dependentRequired": { "x-mcp-header": ["tenant"] } + }), + ); + let mut message = ServerJsonRpcMessage::response( + ServerResult::ListToolsResult(ListToolsResult::with_all_items(vec![valid])), + NumberOrString::Number(1), + ); + let mut cache = HashMap::new(); + + cache_tools_from_response(&mut cache, &mut message, &ProtocolVersion::V_2026_07_28); + + let ServerJsonRpcMessage::Response(response) = message else { + panic!("expected tools/list response"); + }; + let ServerResult::ListToolsResult(result) = response.result else { + panic!("expected tools/list result"); + }; + assert_eq!( + ( + result + .tools + .iter() + .map(|tool| tool.name.as_ref()) + .collect::>(), + cache.keys().map(String::as_str).collect::>(), + ), + (vec!["valid"], vec!["valid"]) + ); + } + #[test] fn cache_tools_keeps_nested_annotations_and_drops_array_annotations() { let nested = tool( From 79df442aa09e61987c037fc306ccd01facfa9807 Mon Sep 17 00:00:00 2001 From: Alex Phonpradith <@openai.com> Date: Mon, 9 Nov 2026 14:33:33 -0800 Subject: [PATCH 2/2] fix: validate legacy dependency schema maps --- .../rmcp/src/transport/common/mcp_headers.rs | 44 +++++++++++- .../src/transport/streamable_http_client.rs | 72 ++++++++++--------- 2 files changed, 80 insertions(+), 36 deletions(-) diff --git a/crates/rmcp/src/transport/common/mcp_headers.rs b/crates/rmcp/src/transport/common/mcp_headers.rs index 294abb7bc..98828841a 100644 --- a/crates/rmcp/src/transport/common/mcp_headers.rs +++ b/crates/rmcp/src/transport/common/mcp_headers.rs @@ -208,7 +208,7 @@ fn has_off_chain_annotation(schema: &JsonObject, on_chain: bool) -> bool { schema.iter().any(|(key, value)| match key.as_str() { "x-mcp-header" => !on_chain, "properties" => schema_map_has_off_chain_annotation(value, on_chain), - "$defs" | "definitions" | "dependentSchemas" | "patternProperties" => { + "$defs" | "definitions" | "dependencies" | "dependentSchemas" | "patternProperties" => { schema_map_has_off_chain_annotation(value, false) } // These contain instance data or property names, not subschemas. @@ -751,6 +751,48 @@ mod tests { ); } + #[test] + fn accepts_property_names_in_legacy_dependencies() { + for dependency in [ + json!(["tenant"]), + json!({ "required": ["tenant"] }), + json!(true), + json!(false), + ] { + let schema = json!({ + "type": "object", + "properties": { + "x-mcp-header": { "type": "string" }, + "tenant": { "type": "string", "x-mcp-header": "Tenant" } + }, + "dependencies": { "x-mcp-header": dependency } + }); + assert_eq!( + validate_param_header_annotations(schema.as_object().unwrap()), + Ok(()), + "dependency must not turn its property name into an annotation: {schema}" + ); + } + } + + #[test] + fn rejects_annotations_in_legacy_dependency_schemas() { + for name in ["tenant", "default", "properties", "x-mcp-header"] { + let schema = json!({ + "type": "object", + "dependencies": { + (name): { + "properties": { "region": { "type": "string", "x-mcp-header": "Region" } } + } + } + }); + assert!( + validate_param_header_annotations(schema.as_object().unwrap()).is_err(), + "annotation must be rejected regardless of the dependency name: {schema}" + ); + } + } + #[test] fn rejects_header_annotation_inside_dependent_schema() { let schema = json!({ diff --git a/crates/rmcp/src/transport/streamable_http_client.rs b/crates/rmcp/src/transport/streamable_http_client.rs index 8b964be94..80316a924 100644 --- a/crates/rmcp/src/transport/streamable_http_client.rs +++ b/crates/rmcp/src/transport/streamable_http_client.rs @@ -2604,43 +2604,45 @@ mod tests { } #[test] - fn cache_tools_keeps_property_names_in_dependent_required() { - let valid = tool( - "valid", - json!({ - "type": "object", - "properties": { - "x-mcp-header": { "type": "string" }, - "tenant": { "type": "string", "x-mcp-header": "Tenant" } - }, - "dependentRequired": { "x-mcp-header": ["tenant"] } - }), - ); - let mut message = ServerJsonRpcMessage::response( - ServerResult::ListToolsResult(ListToolsResult::with_all_items(vec![valid])), - NumberOrString::Number(1), - ); - let mut cache = HashMap::new(); + fn cache_tools_keeps_property_names_in_dependency_maps() { + for keyword in ["dependentRequired", "dependencies"] { + let valid = tool( + "valid", + json!({ + "type": "object", + "properties": { + "x-mcp-header": { "type": "string" }, + "tenant": { "type": "string", "x-mcp-header": "Tenant" } + }, + (keyword): { "x-mcp-header": ["tenant"] } + }), + ); + let mut message = ServerJsonRpcMessage::response( + ServerResult::ListToolsResult(ListToolsResult::with_all_items(vec![valid])), + NumberOrString::Number(1), + ); + let mut cache = HashMap::new(); - cache_tools_from_response(&mut cache, &mut message, &ProtocolVersion::V_2026_07_28); + cache_tools_from_response(&mut cache, &mut message, &ProtocolVersion::V_2026_07_28); - let ServerJsonRpcMessage::Response(response) = message else { - panic!("expected tools/list response"); - }; - let ServerResult::ListToolsResult(result) = response.result else { - panic!("expected tools/list result"); - }; - assert_eq!( - ( - result - .tools - .iter() - .map(|tool| tool.name.as_ref()) - .collect::>(), - cache.keys().map(String::as_str).collect::>(), - ), - (vec!["valid"], vec!["valid"]) - ); + let ServerJsonRpcMessage::Response(response) = message else { + panic!("expected tools/list response"); + }; + let ServerResult::ListToolsResult(result) = response.result else { + panic!("expected tools/list result"); + }; + assert_eq!( + ( + result + .tools + .iter() + .map(|tool| tool.name.as_ref()) + .collect::>(), + cache.keys().map(String::as_str).collect::>(), + ), + (vec!["valid"], vec!["valid"]) + ); + } } #[test]