From b437692f656a06ba9dc326e2550df7591db9f3e9 Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Wed, 20 Aug 2025 09:22:07 -0400 Subject: [PATCH 1/6] fix: generate openAI-compatible json schemas for list types --- crates/apollo-mcp-server/src/operations.rs | 199 ++++++++++++--------- 1 file changed, 113 insertions(+), 86 deletions(-) diff --git a/crates/apollo-mcp-server/src/operations.rs b/crates/apollo-mcp-server/src/operations.rs index 736e372f..4c3960bd 100644 --- a/crates/apollo-mcp-server/src/operations.rs +++ b/crates/apollo-mcp-server/src/operations.rs @@ -1044,25 +1044,36 @@ fn type_to_schema( custom_scalar_map, definitions, ); + let items_schema = if list_type.is_non_null() { + inner_type_schema + } else { + Schema::Object(SchemaObject { + subschemas: Some(Box::new(SubschemaValidation { + one_of: Some(vec![ + inner_type_schema, + Schema::Object(SchemaObject { + instance_type: Some(SingleOrVec::Single(Box::new( + InstanceType::Null, + ))), + ..Default::default() + }), + ]), + ..Default::default() + })), + ..Default::default() + }) + }; + schema_factory( None, Some(InstanceType::Array), None, - list_type.is_non_null().then(|| ArrayValidation { - items: Some(SingleOrVec::Single(Box::new(inner_type_schema.clone()))), - ..Default::default() - }), - (!list_type.is_non_null()).then(|| SubschemaValidation { - one_of: Some(vec![ - inner_type_schema, - Schema::Object(SchemaObject { - instance_type: Some(SingleOrVec::Single(Box::new(InstanceType::Null))), - ..Default::default() - }), - ]), + Some(ArrayValidation { + items: Some(SingleOrVec::Single(Box::new(items_schema))), ..Default::default() }), None, + None, ) } } @@ -1556,7 +1567,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -1570,14 +1581,16 @@ mod tests { "properties": Object { "id": Object { "type": String("array"), - "oneOf": Array [ - Object { - "type": String("string"), - }, - Object { - "type": String("null"), - }, - ], + "items": Object { + "oneOf": Array [ + Object { + "type": String("string"), + }, + Object { + "type": String("null"), + }, + ], + }, }, }, }, @@ -1593,8 +1606,8 @@ mod tests { }, ), } - "###); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + "#); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "required": [ @@ -1603,18 +1616,20 @@ mod tests { "properties": { "id": { "type": "array", - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } } } } - "###); + "#); } #[test] @@ -1708,7 +1723,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -1719,14 +1734,16 @@ mod tests { "properties": Object { "id": Object { "type": String("array"), - "oneOf": Array [ - Object { - "type": String("string"), - }, - Object { - "type": String("null"), - }, - ], + "items": Object { + "oneOf": Array [ + Object { + "type": String("string"), + }, + Object { + "type": String("null"), + }, + ], + }, }, }, }, @@ -1742,25 +1759,27 @@ mod tests { }, ), } - "###); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + "#); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { "id": { "type": "array", - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ] + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } } } } - "###); + "#); } #[test] @@ -1848,7 +1867,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -1859,22 +1878,26 @@ mod tests { "properties": Object { "id": Object { "type": String("array"), - "oneOf": Array [ - Object { - "type": String("array"), - "oneOf": Array [ - Object { - "type": String("string"), - }, - Object { - "type": String("null"), + "items": Object { + "oneOf": Array [ + Object { + "type": String("array"), + "items": Object { + "oneOf": Array [ + Object { + "type": String("string"), + }, + Object { + "type": String("null"), + }, + ], }, - ], - }, - Object { - "type": String("null"), - }, - ], + }, + Object { + "type": String("null"), + }, + ], + }, }, }, }, @@ -1890,33 +1913,37 @@ mod tests { }, ), } - "###); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + "#); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { "id": { "type": "array", - "oneOf": [ - { - "type": "array", - "oneOf": [ - { - "type": "string" - }, - { - "type": "null" + "items": { + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] } - ] - }, - { - "type": "null" - } - ] + }, + { + "type": "null" + } + ] + } } } } - "###); + "#); } #[test] From 04cee88afe8ddccf9527250793dcf33a69d6f346 Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Fri, 22 Aug 2025 10:10:53 -0400 Subject: [PATCH 2/6] chore: add changeset --- .changesets/fix_openai_incompatibility.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changesets/fix_openai_incompatibility.md diff --git a/.changesets/fix_openai_incompatibility.md b/.changesets/fix_openai_incompatibility.md new file mode 100644 index 00000000..d5e37298 --- /dev/null +++ b/.changesets/fix_openai_incompatibility.md @@ -0,0 +1,5 @@ +### fix: generate openAI-compatible json schemas for list types - @DaleSeo PR #272 + +The MCP server is generating JSON schemas that don't match OpenAI's function calling specification. It puts `oneOf` at the array level instead of using `items` to define the JSON schemas for the GraphQL list types. While some other LLMs are more flexible about this, it technically violates the [JSON Schema specification](https://json-schema.org/understanding-json-schema/reference/array) that OpenAI strictly follows. + +This PR updates the list type handling logic to move `oneOf` inside `items` for GraphQL list types. From ee83b8c785a5a1e4791641c33daf9770c5fa8f2f Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Fri, 22 Aug 2025 13:45:27 -0400 Subject: [PATCH 3/6] test: update test snapshots after OpenAI compatibility fix --- .../src/custom_scalar_map.rs | 48 +++--- crates/apollo-mcp-server/src/operations.rs | 159 +++++++++--------- 2 files changed, 104 insertions(+), 103 deletions(-) diff --git a/crates/apollo-mcp-server/src/custom_scalar_map.rs b/crates/apollo-mcp-server/src/custom_scalar_map.rs index c5d73a5b..69cd820a 100644 --- a/crates/apollo-mcp-server/src/custom_scalar_map.rs +++ b/crates/apollo-mcp-server/src/custom_scalar_map.rs @@ -94,33 +94,33 @@ mod tests { fn empty_file() { let result = CustomScalarMap::from_str("").err().unwrap(); - insta::assert_debug_snapshot!(result, @r###" - CustomScalarConfig( - Error("EOF while parsing a value", line: 1, column: 0), - ) - "###) + insta::assert_debug_snapshot!(result, @r#" + CustomScalarConfig( + Error("EOF while parsing a value", line: 1, column: 0), + ) + "#) } #[test] fn only_spaces() { let result = CustomScalarMap::from_str(" ").err().unwrap(); - insta::assert_debug_snapshot!(result, @r###" - CustomScalarConfig( - Error("EOF while parsing a value", line: 1, column: 4), - ) - "###) + insta::assert_debug_snapshot!(result, @r#" + CustomScalarConfig( + Error("EOF while parsing a value", line: 1, column: 4), + ) + "#) } #[test] fn invalid_json() { let result = CustomScalarMap::from_str("Hello: }").err().unwrap(); - insta::assert_debug_snapshot!(result, @r###" - CustomScalarConfig( - Error("expected value", line: 1, column: 1), - ) - "###) + insta::assert_debug_snapshot!(result, @r#" + CustomScalarConfig( + Error("expected value", line: 1, column: 1), + ) + "#) } #[test] @@ -135,13 +135,13 @@ mod tests { .err() .unwrap(); - insta::assert_debug_snapshot!(result, @r###" - CustomScalarJsonSchema( - Object { - "test": Bool(true), - }, - ) - "###) + insta::assert_debug_snapshot!(result, @r#" + CustomScalarJsonSchema( + Object { + "test": Bool(true), + }, + ) + "#) } #[test] @@ -161,7 +161,7 @@ mod tests { .err() .unwrap(); - insta::assert_debug_snapshot!(result, @r###" + insta::assert_debug_snapshot!(result, @r#" CustomScalarJsonSchema( Object { "type": String("object"), @@ -172,7 +172,7 @@ mod tests { }, }, ) - "###) + "#) } #[test] diff --git a/crates/apollo-mcp-server/src/operations.rs b/crates/apollo-mcp-server/src/operations.rs index 4c3960bd..9cdc4475 100644 --- a/crates/apollo-mcp-server/src/operations.rs +++ b/crates/apollo-mcp-server/src/operations.rs @@ -1283,7 +1283,7 @@ mod tests { .unwrap() .unwrap(); - insta::assert_debug_snapshot!(operation, @r###" + insta::assert_debug_snapshot!(operation, @r#" Operation { tool: Tool { name: "MutationName", @@ -1315,7 +1315,7 @@ mod tests { }, operation_name: "MutationName", } - "###); + "#); } #[test] @@ -1337,7 +1337,7 @@ mod tests { .unwrap() .unwrap(); - insta::assert_debug_snapshot!(operation, @r###" + insta::assert_debug_snapshot!(operation, @r#" Operation { tool: Tool { name: "MutationName", @@ -1369,7 +1369,7 @@ mod tests { }, operation_name: "MutationName", } - "###); + "#); } #[test] @@ -1392,7 +1392,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -1414,7 +1414,7 @@ mod tests { }, ), } - "###); + "#); insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", @@ -1443,7 +1443,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -1469,8 +1469,8 @@ mod tests { }, ), } - "###); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + "#); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -1479,7 +1479,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -1502,7 +1502,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -1531,8 +1531,8 @@ mod tests { }, ), } - "###); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + "#); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "required": [ @@ -1544,7 +1544,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -1652,7 +1652,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -1684,8 +1684,8 @@ mod tests { }, ), } - "###); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + "#); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "required": [ @@ -1700,7 +1700,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -1802,7 +1802,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -1831,8 +1831,8 @@ mod tests { }, ), } - "###); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + "#); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -1844,7 +1844,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -1966,7 +1966,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r##" Tool { name: "QueryName", description: Some( @@ -2010,7 +2010,7 @@ mod tests { }, ), } - "###); + "##); } #[test] @@ -2033,7 +2033,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r##" Tool { name: "QueryName", description: Some( @@ -2072,7 +2072,7 @@ mod tests { }, ), } - "###); + "##); } #[test] @@ -2091,7 +2091,7 @@ mod tests { false, false, ); - insta::assert_debug_snapshot!(operation, @r###" + insta::assert_debug_snapshot!(operation, @r#" Err( TooManyOperations { source_path: Some( @@ -2100,7 +2100,7 @@ mod tests { count: 2, }, ) - "###); + "#); } #[test] @@ -2150,7 +2150,7 @@ mod tests { false, false, ); - insta::assert_debug_snapshot!(operation, @r###" + insta::assert_debug_snapshot!(operation, @r#" Err( NoOperations { source_path: Some( @@ -2158,7 +2158,7 @@ mod tests { ), }, ) - "###); + "#); } #[test] @@ -2177,13 +2177,13 @@ mod tests { false, false, ); - insta::assert_debug_snapshot!(operation, @r###" + insta::assert_debug_snapshot!(operation, @r" Err( NoOperations { source_path: None, }, ) - "###); + "); } #[test] @@ -2217,7 +2217,7 @@ mod tests { .ok_or("Expected warning about unknown type in logs".to_string()) }); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -2241,7 +2241,7 @@ mod tests { }, ), } - "###); + "#); } #[test] @@ -2275,7 +2275,7 @@ mod tests { .ok_or("Expected warning about custom scalar without map in logs".to_string()) }); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r##" Tool { name: "QueryName", description: Some( @@ -2306,7 +2306,7 @@ mod tests { }, ), } - "###); + "##); } #[test] @@ -2344,7 +2344,7 @@ mod tests { .ok_or("Expected warning about custom scalar missing in logs".to_string()) }); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r##" Tool { name: "QueryName", description: Some( @@ -2375,7 +2375,7 @@ mod tests { }, ), } - "###); + "##); } #[test] @@ -2401,7 +2401,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r##" Tool { name: "QueryName", description: Some( @@ -2433,7 +2433,7 @@ mod tests { }, ), } - "###); + "##); } #[test] @@ -2584,7 +2584,7 @@ mod tests { insta::assert_snapshot!( operation.tool.description.unwrap(), - @r###" + @r#" Get a list of A The returned value is an array of type `A` --- @@ -2636,7 +2636,7 @@ mod tests { type Z { zzz: Int } - "### + "# ); } @@ -2671,7 +2671,7 @@ mod tests { insta::assert_snapshot!( operation.tool.description.unwrap(), - @r###"Overridden tool #description"### + @"Overridden tool #description" ); } @@ -2704,7 +2704,7 @@ mod tests { insta::assert_snapshot!( operation.tool.description.unwrap(), - @r###"The returned value is optional and has type `String`"### + @"The returned value is optional and has type `String`" ); } @@ -2729,11 +2729,11 @@ mod tests { insta::assert_snapshot!( operation.tool.description.unwrap(), - @r###" - The returned value is optional and has type `String` - --- - The returned value is optional and has type `RealEnum` - "### + @r" + The returned value is optional and has type `String` + --- + The returned value is optional and has type `RealEnum` + " ); } @@ -2758,15 +2758,16 @@ mod tests { insta::assert_snapshot!( operation.tool.description.unwrap(), - @r###" - """the description for the enum""" - enum RealEnum { - """ENUM_VALUE_1 is a value""" - ENUM_VALUE_1 - """ENUM_VALUE_2 is a value""" - ENUM_VALUE_2 - } - "### + @r#" + --- + """the description for the enum""" + enum RealEnum { + """ENUM_VALUE_1 is a value""" + ENUM_VALUE_1 + """ENUM_VALUE_2 is a value""" + ENUM_VALUE_2 + } + "# ); } @@ -2791,7 +2792,7 @@ mod tests { insta::assert_snapshot!( operation.tool.description.unwrap(), - @r###""### + @"" ); } @@ -2838,7 +2839,7 @@ mod tests { .unwrap() .unwrap(); - insta::assert_debug_snapshot!(operation.tool, @r###" + insta::assert_debug_snapshot!(operation.tool, @r##" Tool { name: "Test", description: Some( @@ -2881,7 +2882,7 @@ mod tests { }, ), } - "###); + "##); } #[test] @@ -2907,7 +2908,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_debug_snapshot!(tool, @r###" + insta::assert_debug_snapshot!(tool, @r#" Tool { name: "QueryName", description: Some( @@ -2933,7 +2934,7 @@ mod tests { }, ), } - "###); + "#); } #[test] @@ -2957,7 +2958,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -2967,7 +2968,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -3027,7 +3028,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -3041,7 +3042,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -3103,7 +3104,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -3113,7 +3114,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -3136,7 +3137,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -3146,7 +3147,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -3169,7 +3170,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -3179,7 +3180,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -3202,7 +3203,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -3216,7 +3217,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -3267,7 +3268,7 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": { @@ -3281,7 +3282,7 @@ mod tests { } } } - "###); + "#); } #[test] @@ -3304,11 +3305,11 @@ mod tests { .unwrap(); let tool = Tool::from(operation); - insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r###" + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r#" { "type": "object", "properties": {} } - "###); + "#); } } From a0efb1e59d15bb7fd8c240571396b05be2c1c7dc Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Fri, 22 Aug 2025 13:50:19 -0400 Subject: [PATCH 4/6] refactor: use schema_factory consistently --- crates/apollo-mcp-server/src/operations.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/apollo-mcp-server/src/operations.rs b/crates/apollo-mcp-server/src/operations.rs index 9cdc4475..826a70e8 100644 --- a/crates/apollo-mcp-server/src/operations.rs +++ b/crates/apollo-mcp-server/src/operations.rs @@ -1047,8 +1047,12 @@ fn type_to_schema( let items_schema = if list_type.is_non_null() { inner_type_schema } else { - Schema::Object(SchemaObject { - subschemas: Some(Box::new(SubschemaValidation { + schema_factory( + None, + None, + None, + None, + Some(SubschemaValidation { one_of: Some(vec![ inner_type_schema, Schema::Object(SchemaObject { @@ -1059,9 +1063,9 @@ fn type_to_schema( }), ]), ..Default::default() - })), - ..Default::default() - }) + }), + None, + ) }; schema_factory( From 3f2c5f6c48b8d891af58142c37b7db5622519bd6 Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Fri, 22 Aug 2025 14:44:15 -0400 Subject: [PATCH 5/6] test: add tests for input object lists --- crates/apollo-mcp-server/src/operations.rs | 222 +++++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/crates/apollo-mcp-server/src/operations.rs b/crates/apollo-mcp-server/src/operations.rs index 826a70e8..7cec37e0 100644 --- a/crates/apollo-mcp-server/src/operations.rs +++ b/crates/apollo-mcp-server/src/operations.rs @@ -3316,4 +3316,226 @@ mod tests { } "#); } + + #[test] + fn nullable_list_of_nullable_input_objects() { + let operation = Operation::from_document( + RawOperation { + source_text: "query QueryName($objects: [RealInputObject]) { id }".to_string(), + persisted_query_id: None, + headers: None, + variables: None, + source_path: None, + }, + &SCHEMA, + None, + MutationMode::None, + false, + false, + ) + .unwrap() + .unwrap(); + let tool = Tool::from(operation); + + insta::assert_debug_snapshot!(tool, @r##" + Tool { + name: "QueryName", + description: Some( + "The returned value is optional and has type `String`", + ), + input_schema: { + "type": String("object"), + "properties": Object { + "objects": Object { + "type": String("array"), + "items": Object { + "oneOf": Array [ + Object { + "$ref": String("#/definitions/RealInputObject"), + }, + Object { + "type": String("null"), + }, + ], + }, + }, + }, + "definitions": Object { + "RealInputObject": Object { + "type": String("object"), + "required": Array [ + String("required"), + ], + "properties": Object { + "optional": Object { + "description": String("optional is a input field that is optional"), + "type": String("string"), + }, + "required": Object { + "description": String("required is a input field that is required"), + "type": String("string"), + }, + }, + }, + }, + }, + annotations: Some( + ToolAnnotations { + title: None, + read_only_hint: Some( + true, + ), + destructive_hint: None, + idempotent_hint: None, + open_world_hint: None, + }, + ), + } + "##); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r##" + { + "type": "object", + "properties": { + "objects": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/definitions/RealInputObject" + }, + { + "type": "null" + } + ] + } + } + }, + "definitions": { + "RealInputObject": { + "type": "object", + "required": [ + "required" + ], + "properties": { + "optional": { + "description": "optional is a input field that is optional", + "type": "string" + }, + "required": { + "description": "required is a input field that is required", + "type": "string" + } + } + } + } + } + "##); + } + + #[test] + fn non_nullable_list_of_non_nullable_input_objects() { + let operation = Operation::from_document( + RawOperation { + source_text: "query QueryName($objects: [RealInputObject!]!) { id }".to_string(), + persisted_query_id: None, + headers: None, + variables: None, + source_path: None, + }, + &SCHEMA, + None, + MutationMode::None, + false, + false, + ) + .unwrap() + .unwrap(); + let tool = Tool::from(operation); + + insta::assert_debug_snapshot!(tool, @r##" + Tool { + name: "QueryName", + description: Some( + "The returned value is optional and has type `String`", + ), + input_schema: { + "type": String("object"), + "required": Array [ + String("objects"), + ], + "properties": Object { + "objects": Object { + "type": String("array"), + "items": Object { + "$ref": String("#/definitions/RealInputObject"), + }, + }, + }, + "definitions": Object { + "RealInputObject": Object { + "type": String("object"), + "required": Array [ + String("required"), + ], + "properties": Object { + "optional": Object { + "description": String("optional is a input field that is optional"), + "type": String("string"), + }, + "required": Object { + "description": String("required is a input field that is required"), + "type": String("string"), + }, + }, + }, + }, + }, + annotations: Some( + ToolAnnotations { + title: None, + read_only_hint: Some( + true, + ), + destructive_hint: None, + idempotent_hint: None, + open_world_hint: None, + }, + ), + } + "##); + insta::assert_snapshot!(serde_json::to_string_pretty(&serde_json::json!(tool.input_schema)).unwrap(), @r##" + { + "type": "object", + "required": [ + "objects" + ], + "properties": { + "objects": { + "type": "array", + "items": { + "$ref": "#/definitions/RealInputObject" + } + } + }, + "definitions": { + "RealInputObject": { + "type": "object", + "required": [ + "required" + ], + "properties": { + "optional": { + "description": "optional is a input field that is optional", + "type": "string" + }, + "required": { + "description": "required is a input field that is required", + "type": "string" + } + } + } + } + } + "##); + } } From fca294f1086e52a05d8bb2763c3a5a327f79c76d Mon Sep 17 00:00:00 2001 From: Dale Seo Date: Fri, 22 Aug 2025 14:48:46 -0400 Subject: [PATCH 6/6] test: revert the unrelated changes in custom_scalar_map.rs --- .../src/custom_scalar_map.rs | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/crates/apollo-mcp-server/src/custom_scalar_map.rs b/crates/apollo-mcp-server/src/custom_scalar_map.rs index 69cd820a..c5d73a5b 100644 --- a/crates/apollo-mcp-server/src/custom_scalar_map.rs +++ b/crates/apollo-mcp-server/src/custom_scalar_map.rs @@ -94,33 +94,33 @@ mod tests { fn empty_file() { let result = CustomScalarMap::from_str("").err().unwrap(); - insta::assert_debug_snapshot!(result, @r#" - CustomScalarConfig( - Error("EOF while parsing a value", line: 1, column: 0), - ) - "#) + insta::assert_debug_snapshot!(result, @r###" + CustomScalarConfig( + Error("EOF while parsing a value", line: 1, column: 0), + ) + "###) } #[test] fn only_spaces() { let result = CustomScalarMap::from_str(" ").err().unwrap(); - insta::assert_debug_snapshot!(result, @r#" - CustomScalarConfig( - Error("EOF while parsing a value", line: 1, column: 4), - ) - "#) + insta::assert_debug_snapshot!(result, @r###" + CustomScalarConfig( + Error("EOF while parsing a value", line: 1, column: 4), + ) + "###) } #[test] fn invalid_json() { let result = CustomScalarMap::from_str("Hello: }").err().unwrap(); - insta::assert_debug_snapshot!(result, @r#" - CustomScalarConfig( - Error("expected value", line: 1, column: 1), - ) - "#) + insta::assert_debug_snapshot!(result, @r###" + CustomScalarConfig( + Error("expected value", line: 1, column: 1), + ) + "###) } #[test] @@ -135,13 +135,13 @@ mod tests { .err() .unwrap(); - insta::assert_debug_snapshot!(result, @r#" - CustomScalarJsonSchema( - Object { - "test": Bool(true), - }, - ) - "#) + insta::assert_debug_snapshot!(result, @r###" + CustomScalarJsonSchema( + Object { + "test": Bool(true), + }, + ) + "###) } #[test] @@ -161,7 +161,7 @@ mod tests { .err() .unwrap(); - insta::assert_debug_snapshot!(result, @r#" + insta::assert_debug_snapshot!(result, @r###" CustomScalarJsonSchema( Object { "type": String("object"), @@ -172,7 +172,7 @@ mod tests { }, }, ) - "#) + "###) } #[test]