Skip to content

Commit a3947e6

Browse files
committed
revert: remove server-side empty parameter omission logic
This reverts commit b90c893. The original implementation was incorrect as it made the server second-guess client intent by omitting parameters that clients explicitly provided. Empty values are valid and should be passed through unchanged. Root cause has been fixed at the client level in MCP Inspector PR: modelcontextprotocol/inspector#772 Closes #65
1 parent b81066b commit a3947e6

File tree

4 files changed

+9
-713
lines changed

4 files changed

+9
-713
lines changed

README.md

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ This enables AI assistants to interact with REST APIs through a standardized int
1717
- **Flexible Spec Loading**: Support for both URL-based and local file OpenAPI specifications
1818
- **HTTP Client Integration**: Built-in HTTP client with configurable base URLs and request handling
1919
- **Parameter Mapping**: Intelligent mapping of OpenAPI parameters (path, query, body) to MCP tool parameters
20-
- **Smart Parameter Handling**: Optional array parameters with empty values are automatically omitted from HTTP requests for OpenAPI compliance
2120
- **Output Schema Support**: Automatic generation of output schemas from OpenAPI response definitions
2221
- **Structured Content**: Returns parsed JSON responses as structured content when output schemas are defined
2322
- **Dual Usage Modes**: Use as a standalone MCP server or integrate as a Rust library
@@ -258,32 +257,6 @@ Example generated tools for Petstore API:
258257
- `updatePet`: Update an existing pet
259258
- `deletePet`: Delete a pet
260259

261-
### Parameter Handling
262-
263-
The server implements intelligent parameter handling to ensure OpenAPI specification compliance:
264-
265-
#### Array Parameters
266-
- **Empty Optional Arrays**: Optional array parameters with empty values (`[]`) are automatically omitted from HTTP requests
267-
- **Non-Empty Optional Arrays**: Optional arrays with values are included normally
268-
- **Required Arrays**: Required array parameters are always processed, even when empty
269-
- **Arrays with Defaults**: Optional arrays with default values are always included, even when empty
270-
271-
#### Examples
272-
```json
273-
// These parameters...
274-
{
275-
"requiredTags": [], // Required array - included as "?requiredTags="
276-
"optionalTags": [], // Optional array - omitted entirely
277-
"optionalWithDefault": [], // Optional with default - included as "?optionalWithDefault="
278-
"nonEmptyOptional": ["tag1"] // Non-empty optional - included as "?nonEmptyOptional=tag1"
279-
}
280-
281-
// ...generate this HTTP request:
282-
// GET /endpoint?requiredTags=&optionalWithDefault=&nonEmptyOptional=tag1
283-
```
284-
285-
This behavior ensures that HTTP requests conform to OpenAPI specifications where optional parameters should be omitted when not needed, while preserving required parameters and those with explicit defaults.
286-
287260
### Output Schema Support
288261

289262
The server now generates output schemas for all tools based on OpenAPI response definitions. This enables:

crates/rmcp-openapi/src/http_client.rs

Lines changed: 9 additions & 226 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ impl HttpClient {
132132

133133
// Add query parameters with proper URL encoding
134134
if !extracted_params.query.is_empty() {
135-
Self::add_query_parameters(&mut url, &extracted_params.query, tool_metadata);
135+
Self::add_query_parameters(&mut url, &extracted_params.query);
136136
}
137137

138138
info!("Final URL: {}", url);
@@ -154,12 +154,12 @@ impl HttpClient {
154154

155155
// Add request-specific headers (these override default headers)
156156
if !extracted_params.headers.is_empty() {
157-
request = Self::add_headers(request, &extracted_params.headers, tool_metadata);
157+
request = Self::add_headers(request, &extracted_params.headers);
158158
}
159159

160160
// Add cookies
161161
if !extracted_params.cookies.is_empty() {
162-
request = Self::add_cookies(request, &extracted_params.cookies, tool_metadata);
162+
request = Self::add_cookies(request, &extracted_params.cookies);
163163
}
164164

165165
// Add request body if present
@@ -369,24 +369,12 @@ impl HttpClient {
369369
}
370370

371371
/// Add query parameters to the request using proper URL encoding
372-
fn add_query_parameters(
373-
url: &mut Url,
374-
query_params: &HashMap<String, QueryParameter>,
375-
tool_metadata: &ToolMetadata,
376-
) {
372+
fn add_query_parameters(url: &mut Url, query_params: &HashMap<String, QueryParameter>) {
377373
{
378374
let mut query_pairs = url.query_pairs_mut();
379375
for (key, query_param) in query_params {
380-
// Skip empty optional array parameters
381-
if tool_metadata.should_omit_empty_array_parameter(key, &query_param.value) {
382-
continue;
383-
}
384376
if let Value::Array(arr) = &query_param.value {
385-
if arr.is_empty() {
386-
// For empty arrays that should be included (required or with defaults),
387-
// add the parameter with an empty value
388-
query_pairs.append_pair(key, "");
389-
} else if query_param.explode {
377+
if query_param.explode {
390378
// explode=true: Handle array parameters - add each value as a separate query parameter
391379
for item in arr {
392380
let item_str = match item {
@@ -437,13 +425,8 @@ impl HttpClient {
437425
fn add_headers(
438426
mut request: RequestBuilder,
439427
headers: &HashMap<String, Value>,
440-
tool_metadata: &ToolMetadata,
441428
) -> RequestBuilder {
442429
for (key, value) in headers {
443-
// Skip empty optional array parameters
444-
if tool_metadata.should_omit_empty_array_parameter(key, value) {
445-
continue;
446-
}
447430
let value_str = match value {
448431
Value::String(s) => s.clone(),
449432
Value::Number(n) => n.to_string(),
@@ -459,15 +442,10 @@ impl HttpClient {
459442
fn add_cookies(
460443
mut request: RequestBuilder,
461444
cookies: &HashMap<String, Value>,
462-
tool_metadata: &ToolMetadata,
463445
) -> RequestBuilder {
464446
if !cookies.is_empty() {
465447
let cookie_header = cookies
466448
.iter()
467-
.filter(|(key, value)| {
468-
// Skip empty optional array parameters
469-
!tool_metadata.should_omit_empty_array_parameter(key, value)
470-
})
471449
.map(|(key, value)| {
472450
let value_str = match value {
473451
Value::String(s) => s.clone(),
@@ -480,9 +458,7 @@ impl HttpClient {
480458
.collect::<Vec<_>>()
481459
.join("; ");
482460

483-
if !cookie_header.is_empty() {
484-
request = request.header(header::COOKIE, cookie_header);
485-
}
461+
request = request.header(header::COOKIE, cookie_header);
486462
}
487463
request
488464
}
@@ -925,7 +901,7 @@ mod tests {
925901
};
926902

927903
let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
928-
HttpClient::add_query_parameters(&mut url, &extracted_params.query, &tool_metadata);
904+
HttpClient::add_query_parameters(&mut url, &extracted_params.query);
929905

930906
let url_string = url.to_string();
931907

@@ -973,7 +949,7 @@ mod tests {
973949
};
974950

975951
let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
976-
HttpClient::add_query_parameters(&mut url, &extracted_params.query, &tool_metadata);
952+
HttpClient::add_query_parameters(&mut url, &extracted_params.query);
977953

978954
let url_string = url.to_string();
979955

@@ -1093,11 +1069,7 @@ mod tests {
10931069
let mut url_exploded = client
10941070
.build_url(&tool_metadata, &extracted_params_exploded)
10951071
.unwrap();
1096-
HttpClient::add_query_parameters(
1097-
&mut url_exploded,
1098-
&extracted_params_exploded.query,
1099-
&tool_metadata,
1100-
);
1072+
HttpClient::add_query_parameters(&mut url_exploded, &extracted_params_exploded.query);
11011073
let url_exploded_string = url_exploded.to_string();
11021074

11031075
// Test explode=false (should generate comma-separated values)
@@ -1122,7 +1094,6 @@ mod tests {
11221094
HttpClient::add_query_parameters(
11231095
&mut url_not_exploded,
11241096
&extracted_params_not_exploded.query,
1125-
&tool_metadata,
11261097
);
11271098
let url_not_exploded_string = url_not_exploded.to_string();
11281099

@@ -1139,192 +1110,4 @@ mod tests {
11391110
println!("Exploded URL: {url_exploded_string}");
11401111
println!("Non-exploded URL: {url_not_exploded_string}");
11411112
}
1142-
1143-
#[test]
1144-
fn test_omit_empty_optional_array_query_parameters() {
1145-
let base_url = Url::parse("https://api.example.com").unwrap();
1146-
let client = HttpClient::new().with_base_url(base_url).unwrap();
1147-
1148-
// Create tool metadata with optional and required array parameters
1149-
let tool_metadata = crate::ToolMetadata {
1150-
name: "test".to_string(),
1151-
title: None,
1152-
description: "test".to_string(),
1153-
parameters: json!({
1154-
"type": "object",
1155-
"properties": {
1156-
"optional_array": {"type": "array", "items": {"type": "string"}},
1157-
"required_array": {"type": "array", "items": {"type": "string"}},
1158-
"optional_with_default": {
1159-
"type": "array",
1160-
"items": {"type": "string"},
1161-
"default": ["default"]
1162-
},
1163-
"optional_string": {"type": "string"}
1164-
},
1165-
"required": ["required_array"]
1166-
}),
1167-
output_schema: None,
1168-
method: "GET".to_string(),
1169-
path: "/test".to_string(),
1170-
};
1171-
1172-
let mut query_params = HashMap::new();
1173-
query_params.insert(
1174-
"optional_array".to_string(),
1175-
QueryParameter::new(json!([]), true),
1176-
); // Empty optional array - should be omitted
1177-
query_params.insert(
1178-
"required_array".to_string(),
1179-
QueryParameter::new(json!([]), true),
1180-
); // Empty required array - should be included
1181-
query_params.insert(
1182-
"optional_with_default".to_string(),
1183-
QueryParameter::new(json!([]), true),
1184-
); // Empty optional with default - should be included
1185-
query_params.insert(
1186-
"optional_string".to_string(),
1187-
QueryParameter::new(json!(""), true),
1188-
); // Empty string - should be included (not array)
1189-
1190-
let extracted_params = ExtractedParameters {
1191-
path: HashMap::new(),
1192-
query: query_params,
1193-
headers: HashMap::new(),
1194-
cookies: HashMap::new(),
1195-
body: HashMap::new(),
1196-
config: crate::tool_generator::RequestConfig::default(),
1197-
};
1198-
1199-
let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1200-
HttpClient::add_query_parameters(&mut url, &extracted_params.query, &tool_metadata);
1201-
1202-
let url_string = url.to_string();
1203-
// Verify empty optional array is omitted
1204-
assert!(!url_string.contains("optional_array"));
1205-
1206-
// Verify empty required array is included
1207-
assert!(url_string.contains("required_array"));
1208-
1209-
// Verify empty optional with default is included
1210-
assert!(url_string.contains("optional_with_default"));
1211-
1212-
// Verify empty string is included (not an array)
1213-
assert!(url_string.contains("optional_string"));
1214-
}
1215-
1216-
#[test]
1217-
fn test_omit_empty_optional_array_headers() {
1218-
let tool_metadata = crate::ToolMetadata {
1219-
name: "test".to_string(),
1220-
title: None,
1221-
description: "test".to_string(),
1222-
parameters: json!({
1223-
"type": "object",
1224-
"properties": {
1225-
"x-optional-array": {"type": "array", "items": {"type": "string"}},
1226-
"x-required-array": {"type": "array", "items": {"type": "string"}},
1227-
"x-optional-string": {"type": "string"}
1228-
},
1229-
"required": ["x-required-array"]
1230-
}),
1231-
output_schema: None,
1232-
method: "GET".to_string(),
1233-
path: "/test".to_string(),
1234-
};
1235-
1236-
let mut headers = HashMap::new();
1237-
headers.insert("x-optional-array".to_string(), json!([])); // Should be omitted
1238-
headers.insert("x-required-array".to_string(), json!([])); // Should be included
1239-
headers.insert("x-optional-string".to_string(), json!("value")); // Should be included
1240-
1241-
let request = HttpClient::new()
1242-
.client
1243-
.request(reqwest::Method::GET, "https://api.example.com");
1244-
let request = HttpClient::add_headers(request, &headers, &tool_metadata);
1245-
1246-
// We can't easily inspect the headers without sending the request,
1247-
// but we can verify the function doesn't panic and returns a valid RequestBuilder
1248-
assert!(request.build().is_ok());
1249-
}
1250-
1251-
#[test]
1252-
fn test_omit_empty_optional_array_cookies() {
1253-
let tool_metadata = crate::ToolMetadata {
1254-
name: "test".to_string(),
1255-
title: None,
1256-
description: "test".to_string(),
1257-
parameters: json!({
1258-
"type": "object",
1259-
"properties": {
1260-
"optional_array_cookie": {"type": "array", "items": {"type": "string"}},
1261-
"required_array_cookie": {"type": "array", "items": {"type": "string"}},
1262-
"optional_string_cookie": {"type": "string"}
1263-
},
1264-
"required": ["required_array_cookie"]
1265-
}),
1266-
output_schema: None,
1267-
method: "GET".to_string(),
1268-
path: "/test".to_string(),
1269-
};
1270-
1271-
let mut cookies = HashMap::new();
1272-
cookies.insert("optional_array_cookie".to_string(), json!([])); // Should be omitted
1273-
cookies.insert("required_array_cookie".to_string(), json!([])); // Should be included
1274-
cookies.insert("optional_string_cookie".to_string(), json!("value")); // Should be included
1275-
1276-
let request = HttpClient::new()
1277-
.client
1278-
.request(reqwest::Method::GET, "https://api.example.com");
1279-
let request = HttpClient::add_cookies(request, &cookies, &tool_metadata);
1280-
1281-
// Verify the function doesn't panic and returns a valid RequestBuilder
1282-
assert!(request.build().is_ok());
1283-
}
1284-
1285-
#[test]
1286-
fn test_non_empty_optional_arrays_are_included() {
1287-
let base_url = Url::parse("https://api.example.com").unwrap();
1288-
let client = HttpClient::new().with_base_url(base_url).unwrap();
1289-
1290-
let tool_metadata = crate::ToolMetadata {
1291-
name: "test".to_string(),
1292-
title: None,
1293-
description: "test".to_string(),
1294-
parameters: json!({
1295-
"type": "object",
1296-
"properties": {
1297-
"optional_array": {"type": "array", "items": {"type": "string"}}
1298-
},
1299-
"required": []
1300-
}),
1301-
output_schema: None,
1302-
method: "GET".to_string(),
1303-
path: "/test".to_string(),
1304-
};
1305-
1306-
let mut query_params = HashMap::new();
1307-
query_params.insert(
1308-
"optional_array".to_string(),
1309-
QueryParameter::new(json!(["value1", "value2"]), true),
1310-
); // Non-empty optional array - should be included
1311-
1312-
let extracted_params = ExtractedParameters {
1313-
path: HashMap::new(),
1314-
query: query_params,
1315-
headers: HashMap::new(),
1316-
cookies: HashMap::new(),
1317-
body: HashMap::new(),
1318-
config: crate::tool_generator::RequestConfig::default(),
1319-
};
1320-
1321-
let mut url = client.build_url(&tool_metadata, &extracted_params).unwrap();
1322-
HttpClient::add_query_parameters(&mut url, &extracted_params.query, &tool_metadata);
1323-
1324-
let url_string = url.to_string();
1325-
1326-
// Verify non-empty optional array is included
1327-
assert!(url_string.contains("optional_array=value1"));
1328-
assert!(url_string.contains("optional_array=value2"));
1329-
}
13301113
}

0 commit comments

Comments
 (0)