diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp.Abstractions/Configuration/McpToolOptions.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp.Abstractions/Configuration/McpToolOptions.cs
new file mode 100644
index 00000000000..70147bdc2b7
--- /dev/null
+++ b/src/HotChocolate/Adapters/src/Adapters.Mcp.Abstractions/Configuration/McpToolOptions.cs
@@ -0,0 +1,25 @@
+namespace HotChocolate.Adapters.Mcp.Configuration;
+
+///
+/// Holds the per-schema options that control how MCP tools are generated from operations.
+///
+public sealed class McpToolOptions
+{
+ ///
+ /// Gets or sets a value indicating whether named input object types are emitted in a tool's
+ /// input schema as JSON Schema definitions ($defs) referenced through $ref.
+ ///
+ /// When true (the default), each named input object type is emitted once under
+ /// $defs and referenced by $ref. This is the only way to represent recursive
+ /// input types (such as filter inputs that reference themselves through and/or)
+ /// in a finite schema.
+ ///
+ ///
+ /// When false, input object types are inlined. This suits MCP clients that do not
+ /// support JSON Schema references. Where a type refers to itself, that point is collapsed to
+ /// a generic object ({ "type": "object" }) and the rest of the schema keeps its full
+ /// structure.
+ ///
+ ///
+ public bool UseJsonSchemaReferences { get; set; } = true;
+}
diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/InputValueDefinitionExtensions.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/InputValueDefinitionExtensions.cs
index cdf48b208ac..2fb34226f4e 100644
--- a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/InputValueDefinitionExtensions.cs
+++ b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/InputValueDefinitionExtensions.cs
@@ -5,7 +5,9 @@ namespace HotChocolate.Adapters.Mcp.Extensions;
internal static class InputValueDefinitionExtensions
{
- public static JsonSchema ToJsonSchema(this IInputValueDefinition inputField)
+ public static JsonSchema ToJsonSchema(
+ this IInputValueDefinition inputField,
+ JsonSchemaContext? context = null)
{
var type = inputField.Type;
var schemaBuilder =
@@ -13,7 +15,8 @@ public static JsonSchema ToJsonSchema(this IInputValueDefinition inputField)
isOneOf: inputField.DeclaringMember is IInputObjectTypeDefinition
{
IsOneOf: true
- });
+ },
+ context);
// Description.
if (inputField.Description is not null)
diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/JsonSchemaContext.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/JsonSchemaContext.cs
new file mode 100644
index 00000000000..54b108195bd
--- /dev/null
+++ b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/JsonSchemaContext.cs
@@ -0,0 +1,29 @@
+using Json.Schema;
+
+namespace HotChocolate.Adapters.Mcp.Extensions;
+
+///
+/// Carries the state shared across a single JSON schema generation walk: the accumulated
+/// definitions, the set of input object types currently being expanded (for cycle detection),
+/// and whether references are used.
+///
+internal sealed class JsonSchemaContext
+{
+ ///
+ /// Gets the named input object type definitions discovered during the walk, keyed by type
+ /// name. These are emitted at the root of the schema under $defs.
+ ///
+ public Dictionary Defs { get; } = new(StringComparer.Ordinal);
+
+ ///
+ /// Gets the names of the input object types currently being expanded on the active path.
+ /// Used to detect cycles so the walk terminates on self-referencing types.
+ ///
+ public HashSet Building { get; } = new(StringComparer.Ordinal);
+
+ ///
+ /// Gets a value indicating whether named input object types are emitted as $defs
+ /// referenced through $ref (true) or inlined (false).
+ ///
+ public bool UseReferences { get; init; } = true;
+}
diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/ServiceCollectionExtensions.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/ServiceCollectionExtensions.cs
index 417e80d075e..3a4d26917c5 100644
--- a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/ServiceCollectionExtensions.cs
+++ b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/ServiceCollectionExtensions.cs
@@ -58,10 +58,25 @@ public static void AddMcpSchemaServices(
var mcpManager = applicationServices.GetRequiredService();
var setup = mcpManager.GetSetup(schemaName);
- services
- .AddLogging()
- .TryAddSingleton(
- static sp => new OperationToolFactory(sp.GetRequiredService()));
+ services.AddLogging();
+
+ services.TryAddSingleton(
+ static sp =>
+ {
+ var toolOptions = new McpToolOptions();
+
+ foreach (var configure in sp.GetServices>())
+ {
+ configure(toolOptions);
+ }
+
+ return toolOptions;
+ });
+
+ services.TryAddSingleton(
+ static sp => new OperationToolFactory(
+ sp.GetRequiredService(),
+ sp.GetRequiredService()));
services.TryAddSingleton(sp =>
{
diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/TypeExtensions.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/TypeExtensions.cs
index 481a1e3e219..edda51e62ee 100644
--- a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/TypeExtensions.cs
+++ b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/TypeExtensions.cs
@@ -8,14 +8,30 @@ namespace HotChocolate.Adapters.Mcp.Extensions;
internal static class TypeExtensions
{
- public static JsonSchemaBuilder ToJsonSchemaBuilder(this IType type, bool isOneOf = false)
+ public static JsonSchemaBuilder ToJsonSchemaBuilder(
+ this IType type,
+ bool isOneOf = false,
+ JsonSchemaContext? context = null)
{
+ var namedType = type.NullableType();
+ var isNullable = !type.IsNonNullType() && !isOneOf;
+
+ // Named input object types are emitted once under $defs and referenced through $ref.
+ // This is the only way to represent a self-referencing input type (such as a filter
+ // input) in a finite schema.
+ if (context is { UseReferences: true }
+ && namedType is IInputObjectTypeDefinition inputObjectReference)
+ {
+ RegisterInputObjectDef(inputObjectReference, context);
+ return RefSchema(inputObjectReference.Name, isNullable);
+ }
+
var schemaBuilder = new JsonSchemaBuilder();
// Type.
var jsonType = type.GetJsonSchemaValueType();
- if (!type.IsNonNullType() && !isOneOf)
+ if (isNullable)
{
// Nullability.
jsonType |= SchemaValueType.Null;
@@ -47,7 +63,7 @@ public static JsonSchemaBuilder ToJsonSchemaBuilder(this IType type, bool isOneO
schemaBuilder.Pattern(pattern);
}
- switch (type.NullableType())
+ switch (namedType)
{
case IEnumTypeDefinition enumType:
// Enum values.
@@ -67,58 +83,109 @@ public static JsonSchemaBuilder ToJsonSchemaBuilder(this IType type, bool isOneO
break;
case IInputObjectTypeDefinition inputObjectType:
- // Object properties.
- var objectProperties = new Dictionary();
- var requiredObjectProperties = new List();
-
- foreach (var field in inputObjectType.Fields)
+ // References are disabled, so the type is inlined. The cycle guard keeps the walk
+ // finite by collapsing a self-reference to the generic object schema already set.
+ // A false result means the type is already being expanded on the current path.
+ if (context?.Building.Add(inputObjectType.Name) == false)
{
- var fieldSchema = field.ToJsonSchema();
-
- objectProperties.Add(field.Name, fieldSchema);
-
- if (field.Type.IsNonNullType() && field.DefaultValue is null)
- {
- requiredObjectProperties.Add(field.Name);
- }
+ break;
}
- // OneOf.
- if (inputObjectType.IsOneOf)
- {
- List oneOfSchemas = [];
-
- foreach (var (propertyName, propertySchema) in objectProperties)
- {
- var oneOfSchema = new JsonSchemaBuilder();
-
- oneOfSchema
- .Type(SchemaValueType.Object)
- .Properties((propertyName, propertySchema))
- .Required(propertyName);
-
- oneOfSchemas.Add(oneOfSchema.Build());
- }
-
- schemaBuilder.OneOf(oneOfSchemas);
- }
- else
- {
- schemaBuilder.Properties(objectProperties);
- schemaBuilder.Required(requiredObjectProperties);
- }
+ PopulateInputObjectMembers(schemaBuilder, inputObjectType, context);
+ context?.Building.Remove(inputObjectType.Name);
break;
case ListType listType:
// Array items.
- schemaBuilder.Items(listType.ElementType().ToJsonSchemaBuilder());
+ schemaBuilder.Items(listType.ElementType().ToJsonSchemaBuilder(context: context));
break;
}
return schemaBuilder;
}
+ private static void RegisterInputObjectDef(
+ IInputObjectTypeDefinition inputObjectType,
+ JsonSchemaContext context)
+ {
+ var name = inputObjectType.Name;
+
+ // The definition name is reserved before its members are expanded so that a field
+ // referencing the same type resolves to a $ref instead of recursing.
+ if (context.Defs.ContainsKey(name) || !context.Building.Add(name))
+ {
+ return;
+ }
+
+ var defBuilder = new JsonSchemaBuilder().Type(SchemaValueType.Object);
+ PopulateInputObjectMembers(defBuilder, inputObjectType, context);
+
+ context.Defs[name] = defBuilder;
+ context.Building.Remove(name);
+ }
+
+ private static JsonSchemaBuilder RefSchema(string typeName, bool isNullable)
+ {
+ var reference = new JsonSchemaBuilder().Ref("#/$defs/" + typeName);
+
+ if (!isNullable)
+ {
+ return reference;
+ }
+
+ // A bare $ref cannot also permit null, so a nullable reference is expressed as an anyOf.
+ return new JsonSchemaBuilder()
+ .AnyOf(reference, new JsonSchemaBuilder().Type(SchemaValueType.Null));
+ }
+
+ private static void PopulateInputObjectMembers(
+ JsonSchemaBuilder schemaBuilder,
+ IInputObjectTypeDefinition inputObjectType,
+ JsonSchemaContext? context)
+ {
+ // Object properties.
+ var objectProperties = new Dictionary();
+ var requiredObjectProperties = new List();
+
+ foreach (var field in inputObjectType.Fields)
+ {
+ var fieldSchema = field.ToJsonSchema(context);
+
+ objectProperties.Add(field.Name, fieldSchema);
+
+ if (field.Type.IsNonNullType() && field.DefaultValue is null)
+ {
+ requiredObjectProperties.Add(field.Name);
+ }
+ }
+
+ // OneOf.
+ if (inputObjectType.IsOneOf)
+ {
+ List oneOfSchemas = [];
+
+ foreach (var (propertyName, propertySchema) in objectProperties)
+ {
+ var oneOfSchema = new JsonSchemaBuilder();
+
+ oneOfSchema
+ .Type(SchemaValueType.Object)
+ .Properties((propertyName, propertySchema))
+ .Required(propertyName);
+
+ oneOfSchemas.Add(oneOfSchema.Build());
+ }
+
+ schemaBuilder.OneOf(oneOfSchemas);
+ }
+ else
+ {
+ schemaBuilder.Properties(objectProperties);
+ schemaBuilder.Required(requiredObjectProperties);
+ }
+ }
+
private static SchemaValueType GetJsonSchemaValueType(this IType type)
{
return type switch
diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs
index bd3ff587796..ac5b5e541ae 100644
--- a/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs
+++ b/src/HotChocolate/Adapters/src/Adapters.Mcp.Core/OperationToolFactory.cs
@@ -1,6 +1,7 @@
using System.Collections.Frozen;
using System.Text;
using System.Text.Json.Nodes;
+using HotChocolate.Adapters.Mcp.Configuration;
using HotChocolate.Adapters.Mcp.Extensions;
using HotChocolate.Adapters.Mcp.Storage;
using HotChocolate.Language;
@@ -13,7 +14,7 @@
namespace HotChocolate.Adapters.Mcp;
-internal sealed class OperationToolFactory(ISchemaDefinition schema)
+internal sealed class OperationToolFactory(ISchemaDefinition schema, McpToolOptions options)
{
private static readonly Walker s_walker = new();
@@ -137,11 +138,12 @@ private JsonSchema CreateInputSchema(OperationDefinitionNode operation)
{
var properties = new Dictionary();
var requiredProperties = new List();
+ var context = new JsonSchemaContext { UseReferences = options.UseJsonSchemaReferences };
foreach (var variableNode in operation.VariableDefinitions)
{
var type = variableNode.Type.ToType(schema);
- var propertyBuilder = type.ToJsonSchemaBuilder();
+ var propertyBuilder = type.ToJsonSchemaBuilder(context: context);
var variableName = variableNode.Variable.Name.Value;
// Description.
@@ -165,12 +167,22 @@ private JsonSchema CreateInputSchema(OperationDefinitionNode operation)
properties.Add(variableName, propertyBuilder);
}
- return
+ var schemaBuilder =
new JsonSchemaBuilder()
.Type(SchemaValueType.Object)
.Properties(properties)
- .Required(requiredProperties)
- .Build();
+ .Required(requiredProperties);
+
+ // Definitions, emitted in a stable order so the schema is deterministic.
+ if (context.Defs.Count > 0)
+ {
+ schemaBuilder.Defs(
+ context.Defs
+ .OrderBy(definition => definition.Key, StringComparer.Ordinal)
+ .ToDictionary());
+ }
+
+ return schemaBuilder.Build();
}
private static JsonSchema CreateOutputSchema(JsonSchema dataSchema)
diff --git a/src/HotChocolate/Adapters/src/Adapters.Mcp/Extensions/McpRequestExecutorBuilderExtensions.cs b/src/HotChocolate/Adapters/src/Adapters.Mcp/Extensions/McpRequestExecutorBuilderExtensions.cs
index f2a7f050a9d..ab143704b8d 100644
--- a/src/HotChocolate/Adapters/src/Adapters.Mcp/Extensions/McpRequestExecutorBuilderExtensions.cs
+++ b/src/HotChocolate/Adapters/src/Adapters.Mcp/Extensions/McpRequestExecutorBuilderExtensions.cs
@@ -42,6 +42,36 @@ public static IRequestExecutorBuilder AddMcp(
return builder;
}
+ ///
+ /// Modifies the options that control how MCP tools are generated from operations.
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// A delegate to modify the .
+ ///
+ ///
+ /// Returns the so that configuration can be chained.
+ ///
+ ///
+ /// The is null.
+ ///
+ ///
+ /// The is null.
+ ///
+ public static IRequestExecutorBuilder ModifyMcpToolOptions(
+ this IRequestExecutorBuilder builder,
+ Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configure);
+
+ builder.ConfigureSchemaServices(services => services.AddSingleton(configure));
+
+ return builder;
+ }
+
///
/// Adds an MCP storage to the schema.
///
diff --git a/src/HotChocolate/Adapters/src/Fusion.Adapters.Mcp/Extensions/FusionGatewayBuilderExtensions.cs b/src/HotChocolate/Adapters/src/Fusion.Adapters.Mcp/Extensions/FusionGatewayBuilderExtensions.cs
index 3725e573b30..fda96ecf28d 100644
--- a/src/HotChocolate/Adapters/src/Fusion.Adapters.Mcp/Extensions/FusionGatewayBuilderExtensions.cs
+++ b/src/HotChocolate/Adapters/src/Fusion.Adapters.Mcp/Extensions/FusionGatewayBuilderExtensions.cs
@@ -39,6 +39,36 @@ public static IFusionGatewayBuilder AddMcp(
return builder;
}
+ ///
+ /// Modifies the options that control how MCP tools are generated from operations.
+ ///
+ ///
+ /// The .
+ ///
+ ///
+ /// A delegate to modify the .
+ ///
+ ///
+ /// Returns the so that configuration can be chained.
+ ///
+ ///
+ /// The is null.
+ ///
+ ///
+ /// The is null.
+ ///
+ public static IFusionGatewayBuilder ModifyMcpToolOptions(
+ this IFusionGatewayBuilder builder,
+ Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentNullException.ThrowIfNull(configure);
+
+ builder.ConfigureSchemaServices((_, services) => services.AddSingleton(configure));
+
+ return builder;
+ }
+
///
/// Adds an MCP storage to the gateway.
///
diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/OperationToolFactoryTests.cs b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/OperationToolFactoryTests.cs
index 20e40942a82..910eb206021 100644
--- a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/OperationToolFactoryTests.cs
+++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/OperationToolFactoryTests.cs
@@ -1,3 +1,4 @@
+using HotChocolate.Adapters.Mcp.Configuration;
using HotChocolate.Adapters.Mcp.Storage;
using HotChocolate.Language;
using HotChocolate.Types;
@@ -16,7 +17,7 @@ static OperationTool Action()
var document = Utf8GraphQLParser.Parse("fragment Fragment on Type { field }");
var toolDefinition = new OperationToolDefinition(document);
- return new OperationToolFactory(schema).CreateTool(toolDefinition);
+ return new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
}
// assert
@@ -48,7 +49,7 @@ query Operation2 {
""");
var toolDefinition = new OperationToolDefinition(document);
- return new OperationToolFactory(schema).CreateTool(toolDefinition);
+ return new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
}
// assert
@@ -74,7 +75,7 @@ query GetBooks {
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -104,7 +105,7 @@ mutation AddBook {
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -134,7 +135,7 @@ subscription BookAdded {
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -167,7 +168,7 @@ query GetBooks {
};
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal("Custom Title", tool.Tool.Title);
@@ -195,7 +196,7 @@ mutation AddBook {
};
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -232,7 +233,7 @@ mutation AddBook {
};
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -252,7 +253,7 @@ public void CreateTool_WithNullableVariables_CreatesCorrectSchema()
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -270,7 +271,7 @@ public void CreateTool_WithNonNullableVariables_CreatesCorrectSchema()
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -288,7 +289,7 @@ public void CreateTool_WithDefaultedVariables_CreatesCorrectSchema()
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -306,7 +307,7 @@ public void CreateTool_WithComplexVariables_CreatesCorrectSchema()
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -314,6 +315,72 @@ public void CreateTool_WithComplexVariables_CreatesCorrectSchema()
mcpTool.OutputSchema.MatchSnapshot(postFix: "Output", extension: ".json");
}
+ // The same non-recursive fixture, with references off, must inline every nested input
+ // object and emit no $ref/$defs.
+ [Fact]
+ public void CreateTool_WithComplexVariables_ReferencesDisabled_InlinesWithoutReferences()
+ {
+ // arrange
+ var schema = CreateSchema(s => s.AddType(new DurationType(DurationFormat.DotNet)));
+ var document = Utf8GraphQLParser.Parse(
+ File.ReadAllText("__resources__/GetWithComplexVariables.graphql"));
+ var toolDefinition = new OperationToolDefinition(document);
+
+ // act
+ var tool =
+ new OperationToolFactory(schema, new McpToolOptions { UseJsonSchemaReferences = false })
+ .CreateTool(toolDefinition);
+
+ // assert
+ tool.Tool.InputSchema.MatchSnapshot(extension: ".json");
+ }
+
+ // An input object that references itself (as filter inputs do via and/or) is finite
+ // only when emitted through $defs/$ref.
+ [Fact]
+ public void CreateTool_SelfReferencingInputVariable_UsesReferences()
+ {
+ // arrange
+ var schema = CreateRecursiveFilterSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ query GetWithRecursiveFilter($filter: RecursiveFilterInput) {
+ withRecursiveFilter(filter: $filter)
+ }
+ """);
+ var toolDefinition = new OperationToolDefinition(document);
+
+ // act
+ var tool =
+ new OperationToolFactory(schema, new McpToolOptions())
+ .CreateTool(toolDefinition);
+
+ // assert
+ tool.Tool.InputSchema.MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public void CreateTool_SelfReferencingInputVariable_ReferencesDisabled_Inlines()
+ {
+ // arrange
+ var schema = CreateRecursiveFilterSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ query GetWithRecursiveFilter($filter: RecursiveFilterInput) {
+ withRecursiveFilter(filter: $filter)
+ }
+ """);
+ var toolDefinition = new OperationToolDefinition(document);
+
+ // act
+ var tool =
+ new OperationToolFactory(schema, new McpToolOptions { UseJsonSchemaReferences = false })
+ .CreateTool(toolDefinition);
+
+ // assert
+ tool.Tool.InputSchema.MatchSnapshot(extension: ".json");
+ }
+
[Fact]
public void CreateTool_WithInterfaceType_CreatesCorrectOutputSchema()
{
@@ -337,7 +404,7 @@ ... on Dog {
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
tool.Tool.OutputSchema.MatchSnapshot(extension: ".json");
@@ -365,7 +432,7 @@ ... on Dog {
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
tool.Tool.OutputSchema.MatchSnapshot(extension: ".json");
@@ -381,7 +448,7 @@ public void CreateTool_WithSkipAndInclude_CreatesCorrectOutputSchema()
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
tool.Tool.OutputSchema.MatchSnapshot(extension: ".json");
@@ -401,7 +468,7 @@ public void CreateTool_McpToolAnnotationsDestructiveHintImplementationFirst_Sets
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(destructiveHint, tool.Tool.Annotations?.DestructiveHint);
@@ -446,7 +513,7 @@ public void CreateTool_McpToolAnnotationsCodeFirst_SetsCorrectHint(
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(destructiveHint, tool.Tool.Annotations?.DestructiveHint);
@@ -482,7 +549,7 @@ type Mutation {
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(destructiveHint, tool.Tool.Annotations?.DestructiveHint);
@@ -502,7 +569,7 @@ public void CreateTool_McpToolAnnotationsIdempotentHintImplementationFirst_SetsC
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(idempotentHint, tool.Tool.Annotations?.IdempotentHint);
@@ -547,7 +614,7 @@ public void CreateTool_McpToolAnnotationsIdempotentHintCodeFirst_SetsCorrectHint
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(idempotentHint, tool.Tool.Annotations?.IdempotentHint);
@@ -583,7 +650,7 @@ type Mutation {
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(idempotentHint, tool.Tool.Annotations?.IdempotentHint);
@@ -605,7 +672,7 @@ public void CreateTool_McpToolAnnotationsOpenWorldHintImplementationFirst_SetsCo
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(openWorldHint, tool.Tool.Annotations?.OpenWorldHint);
@@ -661,7 +728,7 @@ public void CreateTool_McpToolAnnotationsOpenWorldHintCodeFirst_SetsCorrectHint(
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(openWorldHint, tool.Tool.Annotations?.OpenWorldHint);
@@ -710,7 +777,7 @@ type ExplicitClosedWorld {
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
// assert
Assert.Equal(openWorldHint, tool.Tool.Annotations?.OpenWorldHint);
@@ -727,7 +794,7 @@ public void CreateTool_McpToolAnnotationsWithFragment_SetsCorrectHints()
var toolDefinition = new OperationToolDefinition(document);
// act
- var tool = new OperationToolFactory(schema).CreateTool(toolDefinition);
+ var tool = new OperationToolFactory(schema, new McpToolOptions()).CreateTool(toolDefinition);
var mcpTool = tool.Tool;
// assert
@@ -756,4 +823,28 @@ private static Schema CreateSchema(Action? configure = null)
return schemaBuilder.Create();
}
+
+ private static Schema CreateRecursiveFilterSchema()
+ {
+ return SchemaBuilder
+ .New()
+ .AddMcp()
+ .AddQueryType()
+ .ModifyOptions(o => o.StrictValidation = false)
+ .Create();
+ }
+
+ public sealed class RecursiveFilterQuery
+ {
+ public int GetWithRecursiveFilter(RecursiveFilter? filter) => filter is null ? 0 : 1;
+ }
+
+ public sealed class RecursiveFilter
+ {
+ public RecursiveFilter[]? And { get; set; }
+
+ public RecursiveFilter[]? Or { get; set; }
+
+ public string? Name { get; set; }
+ }
}
diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_SelfReferencingInputVariable_ReferencesDisabled_Inlines.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_SelfReferencingInputVariable_ReferencesDisabled_Inlines.json
new file mode 100644
index 00000000000..8bf18c4f7e5
--- /dev/null
+++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_SelfReferencingInputVariable_ReferencesDisabled_Inlines.json
@@ -0,0 +1,39 @@
+{
+ "type": "object",
+ "properties": {
+ "filter": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "and": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "object"
+ }
+ },
+ "or": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "object"
+ }
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": []
+ }
+ },
+ "required": []
+}
diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_SelfReferencingInputVariable_UsesReferences.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_SelfReferencingInputVariable_UsesReferences.json
new file mode 100644
index 00000000000..49e0c6c9d52
--- /dev/null
+++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_SelfReferencingInputVariable_UsesReferences.json
@@ -0,0 +1,48 @@
+{
+ "type": "object",
+ "properties": {
+ "filter": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/RecursiveFilterInput"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": [],
+ "$defs": {
+ "RecursiveFilterInput": {
+ "type": "object",
+ "properties": {
+ "and": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/RecursiveFilterInput"
+ }
+ },
+ "or": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/RecursiveFilterInput"
+ }
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "required": []
+ }
+ }
+}
diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Input.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Input.json
index 6b2f2f3a596..f5898c1c400 100644
--- a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Input.json
+++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Input.json
@@ -4,48 +4,7 @@
"list": {
"type": "array",
"items": {
- "type": "object",
- "properties": {
- "field1A": {
- "type": "object",
- "properties": {
- "field1B": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "field1C": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
- "description": "field1C description",
- "default": "12:00:00"
- }
- },
- "required": []
- },
- "description": "field1B description",
- "default": [
- {
- "field1C": "12:00:00"
- }
- ]
- }
- },
- "required": [],
- "description": "field1A description",
- "default": {
- "field1B": [
- {
- "field1C": "12:00:00"
- }
- ]
- }
- }
- },
- "required": []
+ "$ref": "#/$defs/Object1ComplexInput"
},
"description": "Complex list",
"default": [
@@ -61,48 +20,7 @@
]
},
"object": {
- "type": "object",
- "properties": {
- "field1A": {
- "type": "object",
- "properties": {
- "field1B": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "field1C": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
- "description": "field1C description",
- "default": "12:00:00"
- }
- },
- "required": []
- },
- "description": "field1B description",
- "default": [
- {
- "field1C": "12:00:00"
- }
- ]
- }
- },
- "required": [],
- "description": "field1A description",
- "default": {
- "field1B": [
- {
- "field1C": "12:00:00"
- }
- ]
- }
- }
- },
- "required": [],
+ "$ref": "#/$defs/Object1ComplexInput",
"description": "Complex object",
"default": {
"field1A": {
@@ -139,40 +57,70 @@
]
},
"objectWithNullDefault": {
- "type": [
- "object",
- "null"
+ "anyOf": [
+ {
+ "$ref": "#/$defs/Object1ComplexInput"
+ },
+ {
+ "type": "null"
+ }
],
- "properties": {
+ "description": "Object with null default",
+ "default": {
"field1A": {
- "type": "object",
- "properties": {
- "field1B": {
- "type": "array",
- "items": {
- "type": "object",
- "properties": {
- "field1C": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
- "description": "field1C description",
- "default": "12:00:00"
- }
- },
- "required": []
- },
- "description": "field1B description",
- "default": [
- {
- "field1C": "12:00:00"
- }
- ]
+ "field1B": [
+ {
+ "field1C": null
}
- },
- "required": [],
+ ]
+ }
+ }
+ },
+ "oneOf": {
+ "$ref": "#/$defs/OneOfInput",
+ "description": "OneOf",
+ "default": {
+ "field1": 1
+ }
+ },
+ "oneOfList": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/OneOfInput"
+ },
+ "description": "OneOf list",
+ "default": [
+ {
+ "field1": 1
+ },
+ {
+ "field2": "default"
+ }
+ ]
+ },
+ "objectWithOneOfField": {
+ "$ref": "#/$defs/ObjectWithOneOfFieldInput",
+ "description": "Object with OneOf field",
+ "default": {
+ "field": {
+ "field1": 1
+ }
+ }
+ },
+ "durationDotNet": {
+ "type": "string",
+ "pattern": "^-?(?:(?:\\d{1,8})\\.)?(?:[0-1]?\\d|2[0-3]):(?:[0-5]?\\d):(?:[0-5]?\\d)(?:\\.(?:\\d{1,7}))?$",
+ "description": "Duration with DotNet format",
+ "default": "00:05:00"
+ }
+ },
+ "required": [],
+ "$defs": {
+ "Object1ComplexInput": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "$ref": "#/$defs/Object2ComplexInput",
"description": "field1A description",
"default": {
"field1B": [
@@ -183,19 +131,55 @@
}
}
},
- "required": [],
- "description": "Object with null default",
- "default": {
- "field1A": {
- "field1B": [
+ "required": []
+ },
+ "Object2ComplexInput": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/Object3ComplexInput"
+ },
+ "description": "field1B description",
+ "default": [
{
- "field1C": null
+ "field1C": "12:00:00"
}
]
}
- }
+ },
+ "required": []
},
- "oneOf": {
+ "Object3ComplexInput": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
+ "description": "field1C description",
+ "default": "12:00:00"
+ }
+ },
+ "required": []
+ },
+ "ObjectWithOneOfFieldInput": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "$ref": "#/$defs/OneOfInput",
+ "description": "field description",
+ "default": {
+ "field1": 1
+ }
+ }
+ },
+ "required": []
+ },
+ "OneOfInput": {
"type": "object",
"oneOf": [
{
@@ -224,108 +208,7 @@
"field2"
]
}
- ],
- "description": "OneOf",
- "default": {
- "field1": 1
- }
- },
- "oneOfList": {
- "type": "array",
- "items": {
- "type": "object",
- "oneOf": [
- {
- "type": "object",
- "properties": {
- "field1": {
- "type": "integer",
- "minimum": -2147483648,
- "maximum": 2147483647,
- "description": "field1 description"
- }
- },
- "required": [
- "field1"
- ]
- },
- {
- "type": "object",
- "properties": {
- "field2": {
- "type": "string",
- "description": "field2 description"
- }
- },
- "required": [
- "field2"
- ]
- }
- ]
- },
- "description": "OneOf list",
- "default": [
- {
- "field1": 1
- },
- {
- "field2": "default"
- }
]
- },
- "objectWithOneOfField": {
- "type": "object",
- "properties": {
- "field": {
- "type": "object",
- "oneOf": [
- {
- "type": "object",
- "properties": {
- "field1": {
- "type": "integer",
- "minimum": -2147483648,
- "maximum": 2147483647,
- "description": "field1 description"
- }
- },
- "required": [
- "field1"
- ]
- },
- {
- "type": "object",
- "properties": {
- "field2": {
- "type": "string",
- "description": "field2 description"
- }
- },
- "required": [
- "field2"
- ]
- }
- ],
- "description": "field description",
- "default": {
- "field1": 1
- }
- }
- },
- "required": [],
- "description": "Object with OneOf field",
- "default": {
- "field": {
- "field1": 1
- }
- }
- },
- "durationDotNet": {
- "type": "string",
- "pattern": "^-?(?:(?:\\d{1,8})\\.)?(?:[0-1]?\\d|2[0-3]):(?:[0-5]?\\d):(?:[0-5]?\\d)(?:\\.(?:\\d{1,7}))?$",
- "description": "Duration with DotNet format",
- "default": "00:05:00"
}
- },
- "required": []
+ }
}
diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithComplexVariables_ReferencesDisabled_InlinesWithoutReferences.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithComplexVariables_ReferencesDisabled_InlinesWithoutReferences.json
new file mode 100644
index 00000000000..6b2f2f3a596
--- /dev/null
+++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithComplexVariables_ReferencesDisabled_InlinesWithoutReferences.json
@@ -0,0 +1,331 @@
+{
+ "type": "object",
+ "properties": {
+ "list": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
+ "description": "field1C description",
+ "default": "12:00:00"
+ }
+ },
+ "required": []
+ },
+ "description": "field1B description",
+ "default": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ },
+ "required": [],
+ "description": "field1A description",
+ "default": {
+ "field1B": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ }
+ },
+ "required": []
+ },
+ "description": "Complex list",
+ "default": [
+ {
+ "field1A": {
+ "field1B": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "object": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
+ "description": "field1C description",
+ "default": "12:00:00"
+ }
+ },
+ "required": []
+ },
+ "description": "field1B description",
+ "default": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ },
+ "required": [],
+ "description": "field1A description",
+ "default": {
+ "field1B": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ }
+ },
+ "required": [],
+ "description": "Complex object",
+ "default": {
+ "field1A": {
+ "field1B": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ }
+ },
+ "nullDefault": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Null default",
+ "default": null
+ },
+ "listWithNullDefault": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": "List with null default",
+ "default": [
+ null
+ ]
+ },
+ "objectWithNullDefault": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
+ "description": "field1C description",
+ "default": "12:00:00"
+ }
+ },
+ "required": []
+ },
+ "description": "field1B description",
+ "default": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ },
+ "required": [],
+ "description": "field1A description",
+ "default": {
+ "field1B": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ }
+ },
+ "required": [],
+ "description": "Object with null default",
+ "default": {
+ "field1A": {
+ "field1B": [
+ {
+ "field1C": null
+ }
+ ]
+ }
+ }
+ },
+ "oneOf": {
+ "type": "object",
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "field1": {
+ "type": "integer",
+ "minimum": -2147483648,
+ "maximum": 2147483647,
+ "description": "field1 description"
+ }
+ },
+ "required": [
+ "field1"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "field2": {
+ "type": "string",
+ "description": "field2 description"
+ }
+ },
+ "required": [
+ "field2"
+ ]
+ }
+ ],
+ "description": "OneOf",
+ "default": {
+ "field1": 1
+ }
+ },
+ "oneOfList": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "field1": {
+ "type": "integer",
+ "minimum": -2147483648,
+ "maximum": 2147483647,
+ "description": "field1 description"
+ }
+ },
+ "required": [
+ "field1"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "field2": {
+ "type": "string",
+ "description": "field2 description"
+ }
+ },
+ "required": [
+ "field2"
+ ]
+ }
+ ]
+ },
+ "description": "OneOf list",
+ "default": [
+ {
+ "field1": 1
+ },
+ {
+ "field2": "default"
+ }
+ ]
+ },
+ "objectWithOneOfField": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "object",
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "field1": {
+ "type": "integer",
+ "minimum": -2147483648,
+ "maximum": 2147483647,
+ "description": "field1 description"
+ }
+ },
+ "required": [
+ "field1"
+ ]
+ },
+ {
+ "type": "object",
+ "properties": {
+ "field2": {
+ "type": "string",
+ "description": "field2 description"
+ }
+ },
+ "required": [
+ "field2"
+ ]
+ }
+ ],
+ "description": "field description",
+ "default": {
+ "field1": 1
+ }
+ }
+ },
+ "required": [],
+ "description": "Object with OneOf field",
+ "default": {
+ "field": {
+ "field1": 1
+ }
+ }
+ },
+ "durationDotNet": {
+ "type": "string",
+ "pattern": "^-?(?:(?:\\d{1,8})\\.)?(?:[0-1]?\\d|2[0-3]):(?:[0-5]?\\d):(?:[0-5]?\\d)(?:\\.(?:\\d{1,7}))?$",
+ "description": "Duration with DotNet format",
+ "default": "00:05:00"
+ }
+ },
+ "required": []
+}
diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Input.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Input.json
index 258fe6022e9..80bf85098be 100644
--- a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Input.json
+++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Input.json
@@ -127,38 +127,7 @@
"default": 9223372036854775807
},
"object": {
- "type": "object",
- "properties": {
- "field1A": {
- "type": "object",
- "properties": {
- "field1B": {
- "type": "object",
- "properties": {
- "field1C": {
- "type": "string",
- "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
- "description": "field1C description",
- "default": "12:00:00"
- }
- },
- "required": [],
- "description": "field1B description",
- "default": {
- "field1C": "12:00:00"
- }
- }
- },
- "required": [],
- "description": "field1A description",
- "default": {
- "field1B": {
- "field1C": "12:00:00"
- }
- }
- }
- },
- "required": [],
+ "$ref": "#/$defs/Object1DefaultedInput",
"description": "Object description",
"default": {
"field1A": {
@@ -233,5 +202,47 @@
"default": "00000000-0000-0000-0000-000000000000"
}
},
- "required": []
+ "required": [],
+ "$defs": {
+ "Object1DefaultedInput": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "$ref": "#/$defs/Object2DefaultedInput",
+ "description": "field1A description",
+ "default": {
+ "field1B": {
+ "field1C": "12:00:00"
+ }
+ }
+ }
+ },
+ "required": []
+ },
+ "Object2DefaultedInput": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "$ref": "#/$defs/Object3DefaultedInput",
+ "description": "field1B description",
+ "default": {
+ "field1C": "12:00:00"
+ }
+ }
+ },
+ "required": []
+ },
+ "Object3DefaultedInput": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": "string",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
+ "description": "field1C description",
+ "default": "12:00:00"
+ }
+ },
+ "required": []
+ }
+ }
}
diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Input.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Input.json
index fbbda38f80a..851e981761f 100644
--- a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Input.json
+++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Input.json
@@ -106,35 +106,7 @@
"description": "Long description"
},
"object": {
- "type": "object",
- "properties": {
- "field1A": {
- "type": "object",
- "properties": {
- "field1B": {
- "type": "object",
- "properties": {
- "field1C": {
- "type": "string",
- "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
- "description": "field1C description"
- }
- },
- "required": [
- "field1C"
- ],
- "description": "field1B description"
- }
- },
- "required": [
- "field1B"
- ],
- "description": "field1A description"
- }
- },
- "required": [
- "field1A"
- ],
+ "$ref": "#/$defs/Object1NonNullableInput",
"description": "Object description"
},
"short": {
@@ -221,5 +193,44 @@
"uri",
"url",
"uuid"
- ]
+ ],
+ "$defs": {
+ "Object1NonNullableInput": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "$ref": "#/$defs/Object2NonNullableInput",
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ]
+ },
+ "Object2NonNullableInput": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "$ref": "#/$defs/Object3NonNullableInput",
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ]
+ },
+ "Object3NonNullableInput": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": "string",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ]
+ }
+ }
}
diff --git a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Input.json b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Input.json
index f3e257bbd5d..d34d90cfda3 100644
--- a/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Input.json
+++ b/src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/__snapshots__/OperationToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Input.json
@@ -157,41 +157,14 @@
"description": "Long description"
},
"object": {
- "type": [
- "object",
- "null"
- ],
- "properties": {
- "field1A": {
- "type": [
- "object",
- "null"
- ],
- "properties": {
- "field1B": {
- "type": [
- "object",
- "null"
- ],
- "properties": {
- "field1C": {
- "type": [
- "string",
- "null"
- ],
- "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
- "description": "field1C description"
- }
- },
- "required": [],
- "description": "field1B description"
- }
- },
- "required": [],
- "description": "field1A description"
+ "anyOf": [
+ {
+ "$ref": "#/$defs/Object1NullableInput"
+ },
+ {
+ "type": "null"
}
- },
- "required": [],
+ ],
"description": "Object description"
},
"short": {
@@ -279,5 +252,55 @@
"description": "UUID description"
}
},
- "required": []
+ "required": [],
+ "$defs": {
+ "Object1NullableInput": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/Object2NullableInput"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "field1A description"
+ }
+ },
+ "required": []
+ },
+ "Object2NullableInput": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/Object3NullableInput"
+ },
+ {
+ "type": "null"
+ }
+ ],
+ "description": "field1B description"
+ }
+ },
+ "required": []
+ },
+ "Object3NullableInput": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,9})?$",
+ "description": "field1C description"
+ }
+ },
+ "required": []
+ }
+ }
}
diff --git a/website/src/docs/fusion/v16/adapters/mcp.md b/website/src/docs/fusion/v16/adapters/mcp.md
index b2306efc0df..1cdb78ab532 100644
--- a/website/src/docs/fusion/v16/adapters/mcp.md
+++ b/website/src/docs/fusion/v16/adapters/mcp.md
@@ -81,6 +81,24 @@ builder
Tools registered through `configureServer` appear alongside the GraphQL-derived tools, so you can mix native MCP tools with operation tools in the same gateway.
+## Input Schema References
+
+An operation's variables become a tool's input schema (JSON Schema). When a variable's type is an input object, the adapter emits that type once under `$defs` and references it with `$ref`. References are how a self-referencing input type is represented in a finite schema, for example a filter input whose `and` and `or` fields are lists of the same type.
+
+References are the default and are understood by current MCP clients and agents. Some clients have limited support for JSON Schema references (recursive `$ref` in particular). For those, turn references off with `ModifyMcpToolOptions()`:
+
+```csharp
+builder
+ .AddGraphQLGateway()
+ .AddMcp()
+ .ModifyMcpToolOptions(options =>
+ {
+ options.UseJsonSchemaReferences = false;
+ });
+```
+
+With references disabled, input object types are inlined. Where a type refers to itself, that point is collapsed to a generic object (`{ "type": "object" }`) and the rest of the schema keeps its full structure. Leave references enabled unless a target client cannot resolve `$ref`.
+
## Mapping the MCP Endpoint
`MapGraphQLMcp()` accepts two optional arguments:
@@ -185,6 +203,10 @@ Storage is registered but returned no definitions. With Nitro, ensure a publishe
`NitroServiceOptions` is missing one or more of `ApiId`, `ApiKey`, or `Stage`. Set them through the `AddNitro()` configuration delegate or via the `NITRO_API_ID`, `NITRO_API_KEY`, and `NITRO_STAGE` environment variables.
+### MCP client rejects or cannot render a tool's input schema
+
+The client may not support JSON Schema references (`$ref`/`$defs`), which the adapter uses for input object types by default. Disable them with `.ModifyMcpToolOptions(options => options.UseJsonSchemaReferences = false)` to inline the schema instead. See [Input Schema References](#input-schema-references).
+
## Next Steps
- Author tools and prompts and publish them to a stage in the [Nitro MCP](/docs/nitro/adapters/mcp) section.
diff --git a/website/src/docs/hotchocolate/v16/build/adapters/mcp.md b/website/src/docs/hotchocolate/v16/build/adapters/mcp.md
index 33bc72a5d36..c80b54092e4 100644
--- a/website/src/docs/hotchocolate/v16/build/adapters/mcp.md
+++ b/website/src/docs/hotchocolate/v16/build/adapters/mcp.md
@@ -81,6 +81,24 @@ builder
Tools registered through `configureServer` appear alongside the GraphQL-derived tools, so you can mix native MCP tools with operation tools in the same server.
+## Input Schema References
+
+An operation's variables become a tool's input schema (JSON Schema). When a variable's type is an input object, the adapter emits that type once under `$defs` and references it with `$ref`. References are how a self-referencing input type is represented in a finite schema, for example a filter input whose `and` and `or` fields are lists of the same type.
+
+References are the default and are understood by current MCP clients and agents. Some clients have limited support for JSON Schema references (recursive `$ref` in particular). For those, turn references off with `ModifyMcpToolOptions()`:
+
+```csharp
+builder
+ .AddGraphQL()
+ .AddMcp()
+ .ModifyMcpToolOptions(options =>
+ {
+ options.UseJsonSchemaReferences = false;
+ });
+```
+
+With references disabled, input object types are inlined. Where a type refers to itself, that point is collapsed to a generic object (`{ "type": "object" }`) and the rest of the schema keeps its full structure. Leave references enabled unless a target client cannot resolve `$ref`.
+
## Mapping the MCP Endpoint
`MapGraphQLMcp()` accepts two optional arguments:
@@ -185,6 +203,10 @@ Storage is registered but returned no definitions. With Nitro, ensure a publishe
`NitroServiceOptions` is missing one or more of `ApiId`, `ApiKey`, or `Stage`. Set them through the `AddNitro()` configuration delegate or via the `NITRO_API_ID`, `NITRO_API_KEY`, and `NITRO_STAGE` environment variables.
+### MCP client rejects or cannot render a tool's input schema
+
+The client may not support JSON Schema references (`$ref`/`$defs`), which the adapter uses for input object types by default. Disable them with `.ModifyMcpToolOptions(options => options.UseJsonSchemaReferences = false)` to inline the schema instead. See [Input Schema References](#input-schema-references).
+
## Next Steps
- Author tools and prompts and publish them with Nitro in the [Nitro MCP](/docs/nitro/adapters/mcp) section.