Skip to content

Commit 94a2472

Browse files
bartollimcthesw
andcommitted
feat(mcp): add empty properties to GetIndexInfoRequest schema for OpenAI compatibility (#96)
Manual JsonSchema impl for GetIndexInfoRequest that emits properties:{} and additionalProperties:false, satisfying both MCP spec and OpenAI's strict function-calling validation. Co-authored-by: Sworld <61224072+mcthesw@users.noreply.github.com>
2 parents 3be859b + 09fa75f commit 94a2472

2 files changed

Lines changed: 64 additions & 7 deletions

File tree

src/mcp/mod.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,36 @@ pub struct SemanticSearchWithContextRequest {
161161
pub lang: Option<String>,
162162
}
163163

164-
#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
164+
#[derive(Debug, Deserialize, Serialize)]
165165
pub struct GetIndexInfoRequest {}
166166

167+
impl schemars::JsonSchema for GetIndexInfoRequest {
168+
fn schema_name() -> std::borrow::Cow<'static, str> {
169+
std::borrow::Cow::Borrowed("GetIndexInfoRequest")
170+
}
171+
172+
fn schema_id() -> std::borrow::Cow<'static, str> {
173+
std::borrow::Cow::Borrowed(concat!(module_path!(), "::GetIndexInfoRequest"))
174+
}
175+
176+
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
177+
// MCP spec recommends `{"type":"object","additionalProperties":false}` for
178+
// no-parameter tools. We also include an empty `properties` map because
179+
// OpenAI's strict function-calling validation rejects object schemas that
180+
// lack `properties` entirely.
181+
schemars::Schema::from(
182+
serde_json::json!({
183+
"type": "object",
184+
"properties": {},
185+
"additionalProperties": false
186+
})
187+
.as_object()
188+
.unwrap()
189+
.clone(),
190+
)
191+
}
192+
}
193+
167194
#[derive(Debug, Deserialize, Serialize, schemars::JsonSchema)]
168195
pub struct SearchDocumentsRequest {
169196
/// Natural language search query

tests/integration/test_mcp_schema.rs

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
//! Test to verify MCP schema generation for usize fields
22
3-
use codanna::mcp::{AnalyzeImpactRequest, SearchSymbolsRequest, SemanticSearchRequest};
3+
use codanna::mcp::{
4+
AnalyzeImpactRequest, GetIndexInfoRequest, SearchSymbolsRequest, SemanticSearchRequest,
5+
};
46

57
#[test]
68
fn test_mcp_schema_uint_format() {
@@ -14,7 +16,7 @@ fn test_mcp_schema_uint_format() {
1416
println!("{search_json}");
1517

1618
if search_json.contains(r#""format":"uint"#) {
17-
println!("\n❌ WARNING: SearchSymbolsRequest contains 'uint' format!");
19+
println!("\n[WARN] SearchSymbolsRequest contains 'uint' format!");
1820
println!(" This may cause issues with MCP clients like Gemini.");
1921
}
2022

@@ -28,7 +30,7 @@ fn test_mcp_schema_uint_format() {
2830
println!("{semantic_json}");
2931

3032
if semantic_json.contains(r#""format":"uint"#) {
31-
println!("\n❌ WARNING: SemanticSearchRequest contains 'uint' format!");
33+
println!("\n[WARN] SemanticSearchRequest contains 'uint' format!");
3234
}
3335

3436
println!("\n{}", "=".repeat(50));
@@ -41,7 +43,7 @@ fn test_mcp_schema_uint_format() {
4143
println!("{impact_json}");
4244

4345
if impact_json.contains(r#""format":"uint"#) {
44-
println!("\n❌ WARNING: AnalyzeImpactRequest contains 'uint' format!");
46+
println!("\n[WARN] AnalyzeImpactRequest contains 'uint' format!");
4547
}
4648

4749
// Summary
@@ -53,10 +55,38 @@ fn test_mcp_schema_uint_format() {
5355
|| impact_json.contains(r#""format":"uint"#);
5456

5557
if has_uint {
56-
println!(" Schema contains 'uint' format which is not standard JSON Schema.");
58+
println!("[FAIL] Schema contains 'uint' format which is not standard JSON Schema.");
5759
println!(" This causes compatibility issues with MCP clients.");
5860
println!(" Fix: Change usize fields to u32 or u64 in MCP request structs.");
5961
} else {
60-
println!(" No 'uint' format found in schemas.");
62+
println!("[OK] No 'uint' format found in schemas.");
6163
}
6264
}
65+
66+
/// Regression test: `get_index_info` is a no-parameter tool whose inputSchema must satisfy
67+
/// both MCP spec (recommends `additionalProperties: false`) and OpenAI's strict
68+
/// function-calling validation (requires `properties` field).
69+
#[test]
70+
fn test_get_index_info_schema_has_properties() {
71+
let schema = rmcp::schemars::schema_for!(GetIndexInfoRequest);
72+
let json = serde_json::to_string_pretty(&schema).unwrap();
73+
println!("GetIndexInfoRequest schema:\n{json}");
74+
75+
let root: serde_json::Value = serde_json::from_str(&json).unwrap();
76+
77+
assert_eq!(
78+
root.get("type").and_then(|v| v.as_str()),
79+
Some("object"),
80+
"schema must have type=object\nGot:\n{json}"
81+
);
82+
assert!(
83+
root.get("properties").is_some(),
84+
"schema must contain 'properties' for OpenAI compatibility\nGot:\n{json}"
85+
);
86+
assert_eq!(
87+
root.get("additionalProperties").and_then(|v| v.as_bool()),
88+
Some(false),
89+
"schema should set additionalProperties=false per MCP spec\nGot:\n{json}"
90+
);
91+
println!("[OK] GetIndexInfoRequest schema is MCP-spec compliant and OpenAI-compatible.");
92+
}

0 commit comments

Comments
 (0)