diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/FieldNodeExtensions.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/FieldNodeExtensions.cs index 1018bafe8b5..7fa02dd4f1b 100644 --- a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/FieldNodeExtensions.cs +++ b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/FieldNodeExtensions.cs @@ -8,7 +8,8 @@ internal static class FieldNodeExtensions public static SelectionState GetSelectionState( this FieldNode fieldNode, ISyntaxNode? declaringNode, - ITypeDefinition? parentType) + ITypeDefinition? parentType, + NamedTypeNode? fragmentTypeCondition = null) { var skipField = fieldNode.GetSkipIfValue(); var includeField = fieldNode.GetIncludeIfValue(); @@ -23,6 +24,12 @@ public static SelectionState GetSelectionState( skipFragment = fragmentSpread.GetSkipIfValue(); includeFragment = fragmentSpread.GetIncludeIfValue(); + if (fragmentTypeCondition is not null + && fragmentTypeCondition.Name.Value != parentType?.Name) + { + typeConditional = true; + } + break; case InlineFragmentNode inlineFragment: diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs index 224e9e69908..229bc02500e 100644 --- a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs +++ b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs @@ -437,12 +437,22 @@ protected override ISyntaxVisitorAction Enter( var parentType = context.PendingFields.Count == 0 - ? null + ? context.Schema.GetOperationType(context.OperationType) : context.PendingFields.Peek().Field.Type.NamedType(); + var declaringNode = context.Nodes.ElementAtOrDefault(2); + + NamedTypeNode? fragmentTypeCondition = null; + if (declaringNode is FragmentSpreadNode spreadNode + && context.Fragments.TryGetValue(spreadNode.Name.Value, out var fragmentNode)) + { + fragmentTypeCondition = fragmentNode.TypeCondition; + } + var selectionState = fieldNode.GetSelectionState( - declaringNode: context.Nodes.ElementAtOrDefault(2), - parentType); + declaringNode, + parentType, + fragmentTypeCondition); if (selectionState is SelectionState.Excluded) { @@ -635,7 +645,14 @@ protected override ISyntaxVisitorAction Enter( { if (context.Fragments.TryGetValue(fragmentSpreadNode.Name.Value, out var fragmentNode)) { + // Narrow the type for the duration of this fragment. + var parent = context.Frames.Peek(); + var typeCondition = fragmentNode.TypeCondition.Name.Value; + var narrowed = (IOutputTypeDefinition)context.Schema.Types[typeCondition]; + + context.Frames.Push(parent with { Type = narrowed }); Visit(fragmentNode.SelectionSet, context); + context.Frames.Pop(); } return Skip; diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/OperationToolFactoryTests.cs b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/OperationToolFactoryTests.cs index 6a36a364012..7667311fa36 100644 --- a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/OperationToolFactoryTests.cs +++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/OperationToolFactoryTests.cs @@ -566,6 +566,109 @@ fragment DogFields on Dog { tool.Tool.OutputSchema.MatchSnapshot(extension: ".json"); } + [Fact] + public void CreateTool_FragmentSpreadSelectsImplementationDeclaredField_NarrowsToTypeCondition() + { + // arrange + var schema = CreateSchema(); + var document = Utf8GraphQLParser.Parse( + """ + query GetWithInterfaceType { + withInterfaceType { + name + ...CatFields + } + } + + fragment CatFields on Cat { + isPurring + } + """); + var toolDefinition = new OperationToolDefinition(document); + + // act + var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition); + + // assert + tool.Tool.OutputSchema.MatchSnapshot(extension: ".json"); + } + + [Fact] + public void CreateTool_FragmentSpreadOnParentType_KeepsFieldsRequired() + { + // arrange + var schema = CreateSchema(); + var document = Utf8GraphQLParser.Parse( + """ + query GetWithInterfaceType { + withInterfaceType { + ...PetFields + } + } + + fragment PetFields on Pet { + name + } + """); + var toolDefinition = new OperationToolDefinition(document); + + // act + var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition); + + // assert + tool.Tool.OutputSchema.MatchSnapshot(extension: ".json"); + } + + [Fact] + public void CreateTool_FragmentSpreadOnOperationType_KeepsFieldsRequired() + { + // arrange + var schema = CreateSchema(); + var document = Utf8GraphQLParser.Parse( + """ + query GetBooks { + ...QueryFields + } + + fragment QueryFields on Query { + books { + title + } + } + """); + var toolDefinition = new OperationToolDefinition(document); + + // act + var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition); + + // assert + tool.Tool.OutputSchema.MatchSnapshot(extension: ".json"); + } + + [Fact] + public void CreateTool_InlineFragmentOnOperationType_KeepsFieldsRequired() + { + // arrange + var schema = CreateSchema(); + var document = Utf8GraphQLParser.Parse( + """ + query GetBooks { + ... on Query { + books { + title + } + } + } + """); + var toolDefinition = new OperationToolDefinition(document); + + // act + var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition); + + // assert + tool.Tool.OutputSchema.MatchSnapshot(extension: ".json"); + } + [Fact] public void CreateTool_DuplicateFieldSelection_MergesIntoSingleProperty() { diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadOnOperationType_KeepsFieldsRequired.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadOnOperationType_KeepsFieldsRequired.json new file mode 100644 index 00000000000..d4444282e76 --- /dev/null +++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadOnOperationType_KeepsFieldsRequired.json @@ -0,0 +1,85 @@ +{ + "type": "object", + "properties": { + "data": { + "type": [ + "object", + "null" + ], + "properties": { + "books": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "required": [ + "books" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "locations": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "line": { + "type": "integer" + }, + "column": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "path": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "integer" + ] + } + }, + "extensions": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadOnParentType_KeepsFieldsRequired.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadOnParentType_KeepsFieldsRequired.json new file mode 100644 index 00000000000..f2ea6d39c15 --- /dev/null +++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadOnParentType_KeepsFieldsRequired.json @@ -0,0 +1,82 @@ +{ + "type": "object", + "properties": { + "data": { + "type": [ + "object", + "null" + ], + "properties": { + "withInterfaceType": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": [ + "withInterfaceType" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "locations": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "line": { + "type": "integer" + }, + "column": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "path": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "integer" + ] + } + }, + "extensions": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadSelectsImplementationDeclaredField_NarrowsToTypeCondition.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadSelectsImplementationDeclaredField_NarrowsToTypeCondition.json new file mode 100644 index 00000000000..7d121a1ce20 --- /dev/null +++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadSelectsImplementationDeclaredField_NarrowsToTypeCondition.json @@ -0,0 +1,85 @@ +{ + "type": "object", + "properties": { + "data": { + "type": [ + "object", + "null" + ], + "properties": { + "withInterfaceType": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "isPurring": { + "type": "boolean" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": [ + "withInterfaceType" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "locations": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "line": { + "type": "integer" + }, + "column": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "path": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "integer" + ] + } + }, + "extensions": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadsSelectSameField_MergesIntoSingleProperty.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadsSelectSameField_MergesIntoSingleProperty.json index f2ea6d39c15..4afd6a07fcf 100644 --- a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadsSelectSameField_MergesIntoSingleProperty.json +++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_FragmentSpreadsSelectSameField_MergesIntoSingleProperty.json @@ -14,9 +14,7 @@ "type": "string" } }, - "required": [ - "name" - ], + "required": [], "additionalProperties": false } }, diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_InlineFragmentOnOperationType_KeepsFieldsRequired.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_InlineFragmentOnOperationType_KeepsFieldsRequired.json new file mode 100644 index 00000000000..d4444282e76 --- /dev/null +++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_InlineFragmentOnOperationType_KeepsFieldsRequired.json @@ -0,0 +1,85 @@ +{ + "type": "object", + "properties": { + "data": { + "type": [ + "object", + "null" + ], + "properties": { + "books": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + } + }, + "required": [ + "title" + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false, + "required": [ + "books" + ] + }, + "errors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "locations": { + "type": [ + "array", + "null" + ], + "items": { + "type": "object", + "properties": { + "line": { + "type": "integer" + }, + "column": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "path": { + "type": [ + "array", + "null" + ], + "items": { + "type": [ + "string", + "integer" + ] + } + }, + "extensions": { + "type": [ + "object", + "null" + ], + "additionalProperties": true + } + }, + "required": [ + "message" + ], + "additionalProperties": false + } + } + }, + "additionalProperties": false +}