Skip to content

Commit fde440c

Browse files
committed
Avoid O(N!) allocations for recusrive union variants
1 parent 94d89b0 commit fde440c

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

mcp/mcp-server/src/it/java/software/amazon/smithy/java/mcp/server/McpServerIntegrationTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,6 +2182,24 @@ void testUnionMemberNamesInSchema() {
21822182
assertEquals(3, memberNames.size(), "Should have exactly 3 members");
21832183
}
21842184

2185+
@Test
2186+
void testRecursiveOneOfSchemaTerminatesWithoutInfiniteLoop() {
2187+
initializeLatestProtocol();
2188+
var schemas = getMcpEchoToolSchemas();
2189+
var inputSchemaNode = schemas.inputSchema().getSchemaNode();
2190+
2191+
// The recursiveTreeNode field should exist and have a oneOf array
2192+
var treeNodeSchema = inputSchemaNode.path("properties")
2193+
.path("echo")
2194+
.path("properties")
2195+
.path("recursiveTreeNode");
2196+
assertFalse(treeNodeSchema.isMissingNode(), "recursiveTreeNode should be in the schema");
2197+
2198+
var oneOf = treeNodeSchema.path("oneOf");
2199+
assertFalse(oneOf.isMissingNode(), "recursiveTreeNode should have oneOf");
2200+
assertEquals(10, oneOf.size(), "Recursive @oneOf should have 10 variants");
2201+
}
2202+
21852203
// ========== Helper Methods ==========
21862204

21872205
private void initializeWithProtocolVersion(ProtocolVersion protocolVersion) {

mcp/mcp-server/src/it/resources/META-INF/smithy/main.smithy

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,21 @@ structure Echo {
170170
// Helper to make CircleWithNested reachable for schema generation
171171
circleWithNested: CircleWithNested
172172

173+
// Recursive @oneOf document (for testing cycle detection in schema generation)
174+
recursiveTreeNode: TreeNode
175+
176+
// Helpers to make recursive @oneOf targets reachable for schema generation
177+
recNodeA: RecNodeA
178+
recNodeB: RecNodeB
179+
recNodeC: RecNodeC
180+
recNodeD: RecNodeD
181+
recNodeE: RecNodeE
182+
recNodeF: RecNodeF
183+
recNodeG: RecNodeG
184+
recNodeH: RecNodeH
185+
recNodeI: RecNodeI
186+
recNodeJ: RecNodeJ
187+
173188
// Required field to test required validation
174189
@required
175190
requiredField: String
@@ -255,3 +270,29 @@ map ShapeWithOneOfMap {
255270
key: String
256271
value: ShapeWithOneOf
257272
}
273+
274+
/// A recursive @oneOf document where multiple members cycle back, causing O(N!) schema blowup
275+
@oneOf(discriminator: "__type", members: [
276+
{name: "nodeA", target: RecNodeA},
277+
{name: "nodeB", target: RecNodeB},
278+
{name: "nodeC", target: RecNodeC},
279+
{name: "nodeD", target: RecNodeD},
280+
{name: "nodeE", target: RecNodeE},
281+
{name: "nodeF", target: RecNodeF},
282+
{name: "nodeG", target: RecNodeG},
283+
{name: "nodeH", target: RecNodeH},
284+
{name: "nodeI", target: RecNodeI},
285+
{name: "nodeJ", target: RecNodeJ}
286+
])
287+
document TreeNode
288+
289+
structure RecNodeA { value: String, child: TreeNode }
290+
structure RecNodeB { value: String, child: TreeNode }
291+
structure RecNodeC { value: String, child: TreeNode }
292+
structure RecNodeD { value: String, child: TreeNode }
293+
structure RecNodeE { value: String, child: TreeNode }
294+
structure RecNodeF { value: String, child: TreeNode }
295+
structure RecNodeG { value: String, child: TreeNode }
296+
structure RecNodeH { value: String, child: TreeNode }
297+
structure RecNodeI { value: String, child: TreeNode }
298+
structure RecNodeJ { value: String, child: TreeNode }

mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,11 +694,16 @@ private SerializableShape createJsonDocumentSchema(Schema member, Set<ShapeId> v
694694
}
695695
}
696696

697-
private JsonOneOfSchema createJsonOneOfSchema(
697+
private SerializableShape createJsonOneOfSchema(
698698
OneOfTrait oneOfTrait,
699699
Schema documentMember,
700700
Set<ShapeId> visited
701701
) {
702+
var targetId = (documentMember.isMember() ? documentMember.memberTarget() : documentMember).id();
703+
if (!visited.add(targetId)) {
704+
return JsonObjectSchema.builder().build();
705+
}
706+
702707
var oneOfVariants = new ArrayList<Document>();
703708

704709
for (var memberDef : oneOfTrait.getMembers()) {
@@ -711,6 +716,7 @@ private JsonOneOfSchema createJsonOneOfSchema(
711716
oneOfVariants.add(createUnionVariant(memberName, memberSchema));
712717
}
713718

719+
visited.remove(targetId);
714720
return JsonOneOfSchema.builder()
715721
.oneOf(oneOfVariants)
716722
.description(memberDescription(documentMember))

0 commit comments

Comments
 (0)