Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2182,6 +2182,24 @@ void testUnionMemberNamesInSchema() {
assertEquals(3, memberNames.size(), "Should have exactly 3 members");
}

@Test
void testRecursiveOneOfSchemaTerminatesWithoutInfiniteLoop() {
initializeLatestProtocol();
var schemas = getMcpEchoToolSchemas();
var inputSchemaNode = schemas.inputSchema().getSchemaNode();

// The recursiveTreeNode field should exist and have a oneOf array
var treeNodeSchema = inputSchemaNode.path("properties")
.path("echo")
.path("properties")
.path("recursiveTreeNode");
assertFalse(treeNodeSchema.isMissingNode(), "recursiveTreeNode should be in the schema");

var oneOf = treeNodeSchema.path("oneOf");
assertFalse(oneOf.isMissingNode(), "recursiveTreeNode should have oneOf");
assertEquals(10, oneOf.size(), "Recursive @oneOf should have 10 variants");
}

// ========== Helper Methods ==========

private void initializeWithProtocolVersion(ProtocolVersion protocolVersion) {
Expand Down
41 changes: 41 additions & 0 deletions mcp/mcp-server/src/it/resources/META-INF/smithy/main.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,21 @@ structure Echo {
// Helper to make CircleWithNested reachable for schema generation
circleWithNested: CircleWithNested

// Recursive @oneOf document (for testing cycle detection in schema generation)
recursiveTreeNode: TreeNode

// Helpers to make recursive @oneOf targets reachable for schema generation
recNodeA: RecNodeA
recNodeB: RecNodeB
recNodeC: RecNodeC
recNodeD: RecNodeD
recNodeE: RecNodeE
recNodeF: RecNodeF
recNodeG: RecNodeG
recNodeH: RecNodeH
recNodeI: RecNodeI
recNodeJ: RecNodeJ

// Required field to test required validation
@required
requiredField: String
Expand Down Expand Up @@ -255,3 +270,29 @@ map ShapeWithOneOfMap {
key: String
value: ShapeWithOneOf
}

/// A recursive @oneOf document where multiple members cycle back, causing O(N!) schema blowup
@oneOf(discriminator: "__type", members: [
{name: "nodeA", target: RecNodeA},
{name: "nodeB", target: RecNodeB},
{name: "nodeC", target: RecNodeC},
{name: "nodeD", target: RecNodeD},
{name: "nodeE", target: RecNodeE},
{name: "nodeF", target: RecNodeF},
{name: "nodeG", target: RecNodeG},
{name: "nodeH", target: RecNodeH},
{name: "nodeI", target: RecNodeI},
{name: "nodeJ", target: RecNodeJ}
])
document TreeNode

structure RecNodeA { value: String, child: TreeNode }
structure RecNodeB { value: String, child: TreeNode }
structure RecNodeC { value: String, child: TreeNode }
structure RecNodeD { value: String, child: TreeNode }
structure RecNodeE { value: String, child: TreeNode }
structure RecNodeF { value: String, child: TreeNode }
structure RecNodeG { value: String, child: TreeNode }
structure RecNodeH { value: String, child: TreeNode }
structure RecNodeI { value: String, child: TreeNode }
structure RecNodeJ { value: String, child: TreeNode }
Original file line number Diff line number Diff line change
Expand Up @@ -694,11 +694,16 @@ private SerializableShape createJsonDocumentSchema(Schema member, Set<ShapeId> v
}
}

private JsonOneOfSchema createJsonOneOfSchema(
private SerializableShape createJsonOneOfSchema(
OneOfTrait oneOfTrait,
Schema documentMember,
Set<ShapeId> visited
) {
var targetId = (documentMember.isMember() ? documentMember.memberTarget() : documentMember).id();
if (!visited.add(targetId)) {
return JsonObjectSchema.builder().build();
}

var oneOfVariants = new ArrayList<Document>();

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

visited.remove(targetId);
return JsonOneOfSchema.builder()
.oneOf(oneOfVariants)
.description(memberDescription(documentMember))
Expand Down
Loading