diff --git a/src/All.slnx b/src/All.slnx
index 2ccf0322499..b9fe1830ee1 100644
--- a/src/All.slnx
+++ b/src/All.slnx
@@ -213,6 +213,13 @@
+
+
+
+
+
+
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 02bcd753860..2c646edd019 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -13,6 +13,8 @@
+
+
@@ -20,6 +22,7 @@
+
diff --git a/src/HotChocolate/Core/src/Types/Types/Scalars/TimeSpanType.cs b/src/HotChocolate/Core/src/Types/Types/Scalars/TimeSpanType.cs
index 298896dcaa0..11b6728885a 100644
--- a/src/HotChocolate/Core/src/Types/Types/Scalars/TimeSpanType.cs
+++ b/src/HotChocolate/Core/src/Types/Types/Scalars/TimeSpanType.cs
@@ -11,7 +11,7 @@ namespace HotChocolate.Types;
public class TimeSpanType
: ScalarType
{
- private readonly TimeSpanFormat _format;
+ public TimeSpanFormat Format { get; }
public TimeSpanType(
TimeSpanFormat format = TimeSpanFormat.Iso8601,
@@ -27,7 +27,7 @@ public TimeSpanType(
BindingBehavior bind = BindingBehavior.Explicit)
: base(name, bind)
{
- _format = format;
+ Format = format;
Description = description;
}
@@ -39,7 +39,7 @@ public TimeSpanType()
protected override TimeSpan ParseLiteral(StringValueNode valueSyntax)
{
- if (TryDeserializeFromString(valueSyntax.Value, _format, out var value)
+ if (TryDeserializeFromString(valueSyntax.Value, Format, out var value)
&& value != null)
{
return value.Value;
@@ -52,7 +52,7 @@ protected override TimeSpan ParseLiteral(StringValueNode valueSyntax)
protected override StringValueNode ParseValue(TimeSpan runtimeValue)
{
- return _format == TimeSpanFormat.Iso8601
+ return Format == TimeSpanFormat.Iso8601
? new StringValueNode(XmlConvert.ToString(runtimeValue))
: new StringValueNode(runtimeValue.ToString("c"));
}
@@ -65,7 +65,7 @@ public override IValueNode ParseResult(object? resultValue)
}
if (resultValue is string s
- && TryDeserializeFromString(s, _format, out var timeSpan))
+ && TryDeserializeFromString(s, Format, out var timeSpan))
{
return ParseValue(timeSpan);
}
@@ -90,7 +90,7 @@ public override bool TrySerialize(object? runtimeValue, out object? resultValue)
if (runtimeValue is TimeSpan timeSpan)
{
- if (_format == TimeSpanFormat.Iso8601)
+ if (Format == TimeSpanFormat.Iso8601)
{
resultValue = XmlConvert.ToString(timeSpan);
return true;
@@ -113,7 +113,7 @@ public override bool TryDeserialize(object? resultValue, out object? runtimeValu
}
if (resultValue is string s
- && TryDeserializeFromString(s, _format, out var timeSpan))
+ && TryDeserializeFromString(s, Format, out var timeSpan))
{
runtimeValue = timeSpan;
return true;
diff --git a/src/HotChocolate/ModelContextProtocol/Directory.Build.props b/src/HotChocolate/ModelContextProtocol/Directory.Build.props
new file mode 100644
index 00000000000..106b12775b2
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/Directory.Build.props
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/HotChocolate/ModelContextProtocol/HotChocolate.ModelContextProtocol.slnx b/src/HotChocolate/ModelContextProtocol/HotChocolate.ModelContextProtocol.slnx
new file mode 100644
index 00000000000..a88c405cce1
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/HotChocolate.ModelContextProtocol.slnx
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/HotChocolate/ModelContextProtocol/src/Directory.Build.props b/src/HotChocolate/ModelContextProtocol/src/Directory.Build.props
new file mode 100644
index 00000000000..db3f6f4adb2
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/Directory.Build.props
@@ -0,0 +1,12 @@
+
+
+
+
+ $(NoWarn);CA1812
+
+
+
+ true
+
+
+
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Attributes/McpToolAnnotationsAttribute.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Attributes/McpToolAnnotationsAttribute.cs
new file mode 100644
index 00000000000..baa7e0d9279
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Attributes/McpToolAnnotationsAttribute.cs
@@ -0,0 +1,65 @@
+using System.Reflection;
+using HotChocolate.ModelContextProtocol.Directives;
+using HotChocolate.Types;
+using HotChocolate.Types.Descriptors;
+
+namespace HotChocolate.ModelContextProtocol.Attributes;
+
+///
+/// Additional properties describing a Tool to clients.
+///
+[AttributeUsage(AttributeTargets.Method)]
+public sealed class McpToolAnnotationsAttribute : DescriptorAttribute
+{
+ private readonly bool? _destructiveHint;
+ private readonly bool? _idempotentHint;
+ private readonly bool? _openWorldHint;
+
+ ///
+ /// If true, the tool may perform destructive updates to its environment. If
+ /// false, the tool performs only additive updates.
+ ///
+ public bool DestructiveHint
+ {
+ get => _destructiveHint ?? throw new InvalidOperationException();
+ init => _destructiveHint = value;
+ }
+
+ ///
+ /// If true, calling the tool repeatedly with the same arguments will have no additional
+ /// effect on its environment.
+ ///
+ public bool IdempotentHint
+ {
+ get => _idempotentHint ?? throw new InvalidOperationException();
+ init => _idempotentHint = value;
+ }
+
+ ///
+ /// If true, this tool may interact with an “open world” of external entities. If
+ /// false, the tool’s domain of interaction is closed. For example, the world of a web
+ /// search tool is open, whereas that of a memory tool is not.
+ ///
+ public bool OpenWorldHint
+ {
+ get => _openWorldHint ?? throw new InvalidOperationException();
+ init => _openWorldHint = value;
+ }
+
+ protected override void TryConfigure(
+ IDescriptorContext context,
+ IDescriptor descriptor,
+ ICustomAttributeProvider element)
+ {
+ if (descriptor is IObjectFieldDescriptor objectFieldDescriptor)
+ {
+ objectFieldDescriptor.Directive(
+ new McpToolAnnotationsDirective
+ {
+ DestructiveHint = _destructiveHint,
+ IdempotentHint = _idempotentHint,
+ OpenWorldHint = _openWorldHint
+ });
+ }
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolAnnotationsDirective.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolAnnotationsDirective.cs
new file mode 100644
index 00000000000..219036c8992
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolAnnotationsDirective.cs
@@ -0,0 +1,26 @@
+namespace HotChocolate.ModelContextProtocol.Directives;
+
+///
+/// Additional properties describing a Tool to clients.
+///
+public sealed class McpToolAnnotationsDirective
+{
+ ///
+ /// If true, the tool may perform destructive updates to its environment. If
+ /// false, the tool performs only additive updates.
+ ///
+ public bool? DestructiveHint { get; init; }
+
+ ///
+ /// If true, calling the tool repeatedly with the same arguments will have no additional
+ /// effect on its environment.
+ ///
+ public bool? IdempotentHint { get; init; }
+
+ ///
+ /// If true, this tool may interact with an “open world” of external entities. If
+ /// false, the tool’s domain of interaction is closed. For example, the world of a web
+ /// search tool is open, whereas that of a memory tool is not.
+ ///
+ public bool? OpenWorldHint { get; init; }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolDirective.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolDirective.cs
new file mode 100644
index 00000000000..ec66a6a4829
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolDirective.cs
@@ -0,0 +1,25 @@
+namespace HotChocolate.ModelContextProtocol.Directives;
+
+internal sealed class McpToolDirective
+{
+ public string? Title { get; init; }
+
+ ///
+ /// If true, the tool may perform destructive updates to its environment. If
+ /// false, the tool performs only additive updates.
+ ///
+ public bool? DestructiveHint { get; init; }
+
+ ///
+ /// If true, calling the tool repeatedly with the same arguments will have no additional
+ /// effect on its environment.
+ ///
+ public bool? IdempotentHint { get; init; }
+
+ ///
+ /// If true, this tool may interact with an “open world” of external entities. If
+ /// false, the tool’s domain of interaction is closed. For example, the world of a web
+ /// search tool is open, whereas that of a memory tool is not.
+ ///
+ public bool? OpenWorldHint { get; init; }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolDirectiveParser.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolDirectiveParser.cs
new file mode 100644
index 00000000000..2e8be88d8e3
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Directives/McpToolDirectiveParser.cs
@@ -0,0 +1,67 @@
+using HotChocolate.Language;
+using static HotChocolate.ModelContextProtocol.Properties.ModelContextProtocolResources;
+
+namespace HotChocolate.ModelContextProtocol.Directives;
+
+internal static class McpToolDirectiveParser
+{
+ public static McpToolDirective Parse(DirectiveNode directive)
+ {
+ string? title = null;
+ bool? destructiveHint = null;
+ bool? idempotentHint = null;
+ bool? openWorldHint = null;
+
+ foreach (var argument in directive.Arguments)
+ {
+ switch (argument.Name.Value)
+ {
+ case WellKnownArgumentNames.Title:
+ if (argument.Value is StringValueNode titleString)
+ {
+ title = titleString.Value;
+ }
+
+ break;
+
+ case WellKnownArgumentNames.DestructiveHint:
+ if (argument.Value is BooleanValueNode destructiveHintBoolean)
+ {
+ destructiveHint = destructiveHintBoolean.Value;
+ }
+
+ break;
+
+ case WellKnownArgumentNames.IdempotentHint:
+ if (argument.Value is BooleanValueNode idempotentHintBoolean)
+ {
+ idempotentHint = idempotentHintBoolean.Value;
+ }
+
+ break;
+
+ case WellKnownArgumentNames.OpenWorldHint:
+ if (argument.Value is BooleanValueNode openWorldHintBoolean)
+ {
+ openWorldHint = openWorldHintBoolean.Value;
+ }
+
+ break;
+
+ default:
+ throw new Exception(
+ string.Format(
+ McpToolDirectiveParser_ArgumentNotSupportedOnMcpToolDirective,
+ argument.Name.Value));
+ }
+ }
+
+ return new McpToolDirective()
+ {
+ Title = title,
+ DestructiveHint = destructiveHint,
+ IdempotentHint = idempotentHint,
+ OpenWorldHint = openWorldHint
+ };
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/EndpointRouteBuilderExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/EndpointRouteBuilderExtensions.cs
new file mode 100644
index 00000000000..4e18826189d
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/EndpointRouteBuilderExtensions.cs
@@ -0,0 +1,88 @@
+using System.Diagnostics.CodeAnalysis;
+using HotChocolate.ModelContextProtocol.Proxies;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Metadata;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Protocol;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+public static class EndpointRouteBuilderExtensions
+{
+ public static IEndpointConventionBuilder MapGraphQLMcp(
+ this IEndpointRouteBuilder endpoints,
+ [StringSyntax("Route")] string pattern = "/graphql/mcp",
+ string? schemaName = null)
+ {
+ schemaName ??= ISchemaDefinition.DefaultName;
+
+ var streamableHttpHandler =
+ endpoints.ServiceProvider.GetKeyedService(schemaName)
+ ?? throw new InvalidOperationException(
+ "You must call AddMcp(). Unable to find required services. Call "
+ + "builder.Services.AddGraphQL().AddMcp() in application startup code.");
+
+ var mcpGroup = endpoints.MapGroup(pattern);
+
+ var streamableHttpGroup =
+ mcpGroup
+ .MapGroup("")
+ .WithDisplayName(b => $"GraphQL MCP Streamable HTTP | {b.DisplayName}")
+ .WithMetadata(
+ new ProducesResponseTypeMetadata(
+ StatusCodes.Status404NotFound,
+ typeof(JsonRpcError),
+ contentTypes: ["application/json"]));
+
+ streamableHttpGroup
+ .MapPost("", streamableHttpHandler.HandlePostRequestAsync)
+ .WithMetadata(new AcceptsMetadata(["application/json"]))
+ .WithMetadata(
+ new ProducesResponseTypeMetadata(
+ StatusCodes.Status200OK,
+ contentTypes: ["text/event-stream"]))
+ .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted));
+
+ if (!streamableHttpHandler.HttpServerTransportOptions.Stateless)
+ {
+ // The GET and DELETE endpoints are not mapped in Stateless mode since there's no way to
+ // send unsolicited messages for the GET to handle, and there is no server-side state
+ // for the DELETE to clean up.
+ streamableHttpGroup
+ .MapGet("", streamableHttpHandler.HandleGetRequestAsync)
+ .WithMetadata(
+ new ProducesResponseTypeMetadata(
+ StatusCodes.Status200OK,
+ contentTypes: ["text/event-stream"]));
+
+ streamableHttpGroup.MapDelete("", streamableHttpHandler.HandleDeleteRequestAsync);
+
+ // Map legacy HTTP with SSE endpoints only if not in Stateless mode, because we cannot
+ // guarantee the /message requests will be handled by the same process as the /sse
+ // request.
+ var sseHandler =
+ endpoints.ServiceProvider.GetRequiredKeyedService(schemaName);
+
+ var sseGroup =
+ mcpGroup
+ .MapGroup("")
+ .WithDisplayName(b => $"GraphQL MCP HTTP with SSE | {b.DisplayName}");
+
+ sseGroup
+ .MapGet("/sse", sseHandler.HandleSseRequestAsync)
+ .WithMetadata(
+ new ProducesResponseTypeMetadata(
+ StatusCodes.Status200OK,
+ contentTypes: ["text/event-stream"]));
+
+ sseGroup
+ .MapPost("/message", sseHandler.HandleMessageRequestAsync)
+ .WithMetadata(new AcceptsMetadata(["application/json"]))
+ .WithMetadata(new ProducesResponseTypeMetadata(StatusCodes.Status202Accepted));
+ }
+
+ return mcpGroup;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/InputFieldExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/InputFieldExtensions.cs
new file mode 100644
index 00000000000..67624eea051
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/InputFieldExtensions.cs
@@ -0,0 +1,28 @@
+using HotChocolate.Types;
+using Json.Schema;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+internal static class InputFieldExtensions
+{
+ public static JsonSchema ToJsonSchema(this InputField inputField)
+ {
+ var graphQLType = inputField.Type;
+ var schemaBuilder =
+ graphQLType.ToJsonSchemaBuilder(isOneOf: inputField.DeclaringType.IsOneOf);
+
+ // Description.
+ if (inputField.Description is not null)
+ {
+ schemaBuilder.Description(inputField.Description);
+ }
+
+ // Default value.
+ if (inputField.DefaultValue is not null)
+ {
+ schemaBuilder.Default(inputField.DefaultValue.ToJsonNode(graphQLType));
+ }
+
+ return schemaBuilder.Build();
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/JsonSchemaExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/JsonSchemaExtensions.cs
new file mode 100644
index 00000000000..9d45a4507c0
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/JsonSchemaExtensions.cs
@@ -0,0 +1,18 @@
+using System.Text.Json;
+using HotChocolate.ModelContextProtocol.JsonSerializerContexts;
+using Json.Schema;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+internal static class JsonSchemaExtensions
+{
+ public static JsonElement ToJsonElement(this JsonSchema jsonSchema)
+ {
+ var json =
+ JsonSerializer.Serialize(
+ jsonSchema,
+ JsonSchemaJsonSerializerContext.Default.JsonSchema);
+
+ return JsonDocument.Parse(json).RootElement;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/ObjectFieldDescriptorExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/ObjectFieldDescriptorExtensions.cs
new file mode 100644
index 00000000000..50c66df031e
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/ObjectFieldDescriptorExtensions.cs
@@ -0,0 +1,30 @@
+using HotChocolate.ModelContextProtocol.Directives;
+using HotChocolate.Types;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+///
+/// Provides extension methods to .
+///
+public static class ObjectFieldDescriptorExtensions
+{
+ ///
+ /// Additional properties describing a Tool to clients.
+ ///
+ public static IObjectFieldDescriptor McpToolAnnotations(
+ this IObjectFieldDescriptor descriptor,
+ bool? destructiveHint = null,
+ bool? idempotentHint = null,
+ bool? openWorldHint = null)
+ {
+ ArgumentNullException.ThrowIfNull(descriptor);
+
+ return descriptor.Directive(
+ new McpToolAnnotationsDirective
+ {
+ DestructiveHint = destructiveHint,
+ IdempotentHint = idempotentHint,
+ OpenWorldHint = openWorldHint
+ });
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/OperationDefinitionNodeExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/OperationDefinitionNodeExtensions.cs
new file mode 100644
index 00000000000..d2dedbfa51e
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/OperationDefinitionNodeExtensions.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+using HotChocolate.Language;
+using HotChocolate.ModelContextProtocol.Directives;
+using static HotChocolate.ModelContextProtocol.WellKnownDirectiveNames;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+internal static class OperationDefinitionNodeExtensions
+{
+ public static McpToolDirective? GetMcpToolDirective(this OperationDefinitionNode operationNode)
+ {
+ var mcpToolDirectiveNode =
+ operationNode.Directives.SingleOrDefault(d => d.Name.Value == McpTool);
+
+ return mcpToolDirectiveNode is null
+ ? null
+ : McpToolDirectiveParser.Parse(mcpToolDirectiveNode);
+ }
+
+ public static bool TryGetMcpToolDirective(
+ this OperationDefinitionNode operationNode,
+ [NotNullWhen(true)] out McpToolDirective? mcpToolDirective)
+ {
+ mcpToolDirective = operationNode.GetMcpToolDirective();
+
+ return mcpToolDirective is not null;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/RequestExecutorBuilderExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/RequestExecutorBuilderExtensions.cs
new file mode 100644
index 00000000000..93e09e695df
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/RequestExecutorBuilderExtensions.cs
@@ -0,0 +1,96 @@
+using HotChocolate.Execution;
+using HotChocolate.Execution.Configuration;
+using HotChocolate.ModelContextProtocol.Factories;
+using HotChocolate.ModelContextProtocol.Handlers;
+using HotChocolate.ModelContextProtocol.Proxies;
+using HotChocolate.ModelContextProtocol.Registries;
+using HotChocolate.ModelContextProtocol.Storage;
+using HotChocolate.ModelContextProtocol.Types;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+public static class RequestExecutorBuilderExtensions
+{
+ public static IRequestExecutorBuilder AddMcp(this IRequestExecutorBuilder builder)
+ {
+ builder.Services
+ .AddKeyedSingleton(
+ builder.Name,
+ static (sp, name) => new McpRequestExecutorProxy(
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ (string)name))
+ .AddKeyedSingleton(
+ builder.Name,
+ static (sp, name) => new StreamableHttpHandlerProxy(
+ sp.GetRequiredKeyedService(name)))
+ .AddKeyedSingleton(
+ builder.Name,
+ static (sp, name) => new SseHandlerProxy(
+ sp.GetRequiredKeyedService(name)));
+
+ builder.ConfigureSchemaServices(
+ services =>
+ {
+ services
+ .TryAddSingleton
+ ();
+
+ services
+ .AddSingleton(
+ static sp => sp
+ .GetRootServiceProvider()
+ .GetRequiredService())
+ .AddSingleton(
+ static sp => sp
+ .GetRootServiceProvider().GetRequiredService())
+ .AddSingleton();
+
+ services
+ .AddMcpServer()
+ .WithHttpTransport()
+ .WithListToolsHandler(
+ (context, _) => ValueTask.FromResult(ListToolsHandler.Handle(context)))
+ .WithCallToolHandler(
+ async (context, cancellationToken)
+ => await CallToolHandler
+ .HandleAsync(context, cancellationToken)
+ .ConfigureAwait(false));
+ });
+
+ builder.AddDirectiveType();
+
+ builder.ConfigureOnRequestExecutorCreatedAsync(
+ async (executor, cancellationToken) =>
+ {
+ var schema = executor.Schema;
+ var storage = schema.Services.GetRequiredService();
+ var registry = schema.Services.GetRequiredService();
+ var factory = new GraphQLMcpToolFactory(schema);
+
+ var toolDocuments =
+ await storage.GetToolDocumentsAsync(cancellationToken).ConfigureAwait(false);
+
+ registry.Clear();
+
+ foreach (var (name, document) in toolDocuments)
+ {
+ registry.Add(factory.CreateTool(name, document));
+ }
+ });
+
+ return builder;
+ }
+
+ public static IRequestExecutorBuilder AddMcpOperationDocumentStorage(
+ this IRequestExecutorBuilder builder,
+ IMcpOperationDocumentStorage storage)
+ {
+ builder.ConfigureSchemaServices(s => s.AddSingleton(storage));
+ return builder;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/SchemaBuilderExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/SchemaBuilderExtensions.cs
new file mode 100644
index 00000000000..302d98054a5
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/SchemaBuilderExtensions.cs
@@ -0,0 +1,13 @@
+using HotChocolate.ModelContextProtocol.Types;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+public static class SchemaBuilderExtensions
+{
+ public static ISchemaBuilder AddMcp(this ISchemaBuilder builder)
+ {
+ builder.AddDirectiveType();
+
+ return builder;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/SelectionExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/SelectionExtensions.cs
new file mode 100644
index 00000000000..b716fbc1ffe
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/SelectionExtensions.cs
@@ -0,0 +1,41 @@
+using HotChocolate.Execution.Processing;
+using HotChocolate.Language;
+using HotChocolate.Types;
+using static HotChocolate.ModelContextProtocol.WellKnownDirectiveNames;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+internal static class SelectionExtensions
+{
+ public static SelectionState GetSelectionState(this ISelection selection)
+ {
+ var skip =
+ selection.SyntaxNode.Directives.FirstOrDefault(d => d.Name.Value == Skip)?
+ .GetArgumentValue(WellKnownArgumentNames.If);
+
+ var include =
+ selection.SyntaxNode.Directives.FirstOrDefault(d => d.Name.Value == Include)?
+ .GetArgumentValue(WellKnownArgumentNames.If);
+
+ if (skip is null or BooleanValueNode { Value: false }
+ && include is null or BooleanValueNode { Value: true })
+ {
+ return SelectionState.Included;
+ }
+
+ if (skip is BooleanValueNode { Value: true }
+ || include is BooleanValueNode { Value: false })
+ {
+ return SelectionState.Excluded;
+ }
+
+ return SelectionState.Conditional;
+ }
+}
+
+internal enum SelectionState
+{
+ Included,
+ Excluded,
+ Conditional
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/TypeExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/TypeExtensions.cs
new file mode 100644
index 00000000000..71dd3d29d60
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/TypeExtensions.cs
@@ -0,0 +1,292 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Nodes;
+using HotChocolate.Types;
+using Json.Schema;
+using static HotChocolate.ModelContextProtocol.Properties.ModelContextProtocolResources;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+internal static class TypeExtensions
+{
+ public static JsonSchemaBuilder ToJsonSchemaBuilder(
+ this IType graphQLType,
+ bool isOneOf = false)
+ {
+ var schemaBuilder = new JsonSchemaBuilder();
+
+ // Type.
+ var type = graphQLType.GetJsonSchemaValueType();
+
+ if (!graphQLType.IsNonNullType() && !isOneOf)
+ {
+ // Nullability.
+ type |= SchemaValueType.Null;
+ }
+
+ schemaBuilder.Type(type);
+
+ // Format.
+ if (graphQLType.TryGetJsonSchemaFormat(out var format))
+ {
+ schemaBuilder.Format(format);
+ }
+
+ // Pattern.
+ if (graphQLType.TryGetJsonSchemaPattern(out var pattern))
+ {
+ schemaBuilder.Pattern(pattern);
+ }
+
+ // Minimum.
+ if (graphQLType.TryGetJsonSchemaMinimum(out var minimum))
+ {
+ schemaBuilder.Minimum(minimum);
+ }
+
+ // Maximum.
+ if (graphQLType.TryGetJsonSchemaMaximum(out var maximum))
+ {
+ schemaBuilder.Maximum(maximum);
+ }
+
+ switch (graphQLType.NullableType())
+ {
+ case EnumType enumType:
+ // Enum values.
+ List enumValues = [];
+
+ foreach (var enumValue in enumType.Values)
+ {
+ enumValues.Add(JsonValue.Create(enumValue.Name));
+ }
+
+ if (graphQLType.IsNullableType())
+ {
+ enumValues.Add(null);
+ }
+
+ schemaBuilder.Enum(enumValues);
+ break;
+
+ case InputObjectType inputObjectType:
+ // Object properties.
+ var objectProperties = new Dictionary();
+ var requiredObjectProperties = new List();
+
+ foreach (var field in inputObjectType.Fields)
+ {
+ var fieldSchema = field.ToJsonSchema();
+
+ 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);
+ }
+
+ break;
+
+ case ListType listType:
+ // Array items.
+ schemaBuilder.Items(listType.ElementType().ToJsonSchemaBuilder());
+ break;
+ }
+
+ return schemaBuilder;
+ }
+
+ private static SchemaValueType GetJsonSchemaValueType(this IType graphQLType)
+ {
+ return graphQLType switch
+ {
+ EnumType => SchemaValueType.String,
+ InputObjectType => SchemaValueType.Object,
+ InterfaceType => SchemaValueType.Object,
+ ListType => SchemaValueType.Array,
+ NonNullType => GetJsonSchemaValueType(graphQLType.NullableType()),
+ ObjectType => SchemaValueType.Object,
+ ScalarType => graphQLType switch
+ {
+ AnyType or JsonType =>
+ SchemaValueType.Object
+ | SchemaValueType.Array
+ | SchemaValueType.Boolean
+ | SchemaValueType.String
+ | SchemaValueType.Number
+ | SchemaValueType.Integer,
+ BooleanType => SchemaValueType.Boolean,
+ ByteType => SchemaValueType.Integer,
+ DecimalType => SchemaValueType.Number,
+ FloatType => SchemaValueType.Number,
+ IdType => SchemaValueType.String,
+ IntType => SchemaValueType.Integer,
+ LongType => SchemaValueType.Integer,
+ ShortType => SchemaValueType.Integer,
+ StringType => SchemaValueType.String,
+ // The following types are serialized as strings:
+ // URL, UUID, ByteArray, DateTime, Date, TimeSpan, LocalDate, LocalDateTime,
+ // LocalTime.
+ // FIXME: Treating all unknown scalar types as strings is a temporary solution.
+ _ => SchemaValueType.String
+ },
+ UnionType => SchemaValueType.Object,
+ _ =>
+ throw new NotSupportedException(
+ string.Format(
+ TypeExtensions_UnableToDetermineJsonSchemaValueType,
+ graphQLType.GetType().Name))
+ };
+ }
+
+ private static bool TryGetJsonSchemaFormat(
+ this IType graphQLType,
+ [NotNullWhen(true)] out Format? format)
+ {
+ format = graphQLType.NullableType() switch
+ {
+ DateTimeType => Formats.DateTime, // Further constrained by pattern.
+ DateType => Formats.Date,
+ LocalDateTimeType => Formats.DateTime, // Further constrained by pattern.
+ LocalDateType => Formats.Date,
+ LocalTimeType => Formats.Time, // Further constrained by pattern.
+ UrlType => Formats.UriReference,
+ UuidType => Formats.Uuid,
+ _ => null
+ };
+
+ return format is not null;
+ }
+
+ private static bool TryGetJsonSchemaPattern(
+ this IType graphQLType,
+ [NotNullWhen(true)] out string? pattern)
+ {
+ pattern = graphQLType.NullableType() switch
+ {
+ ByteArrayType
+ // e.g. dmFsdWU= (Base64-encoded string)
+ => @"^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$",
+ DateTimeType
+ // e.g. 2011-08-30T13:22:53.108Z (https://www.graphql-scalars.com/date-time/)
+ =>
+ @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}"
+ + @"(?:\.\d{1,7})?(?:[Zz]|[+-]\d{2}:\d{2})$",
+ LocalDateTimeType
+ // e.g. 2011-08-30T13:22:53
+ => @"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$",
+ LocalTimeType
+ // e.g. 13:22:53
+ => @"^\d{2}:\d{2}:\d{2}$",
+ TimeSpanType timeSpanType
+ => timeSpanType.Format switch
+ {
+ // e.g. PT5M
+ TimeSpanFormat.Iso8601
+ =>
+ @"^-?P(?:\d+W|(?=\d|T(?:\d|$))(?:\d+Y)?(?:\d+M)?(?:\d+D)?"
+ + @"(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?)$",
+ // e.g. 00:05:00
+ TimeSpanFormat.DotNet
+ =>
+ @"^-?(?:(?:\d{1,8})\.)?(?:[0-1]?\d|2[0-3]):(?:[0-5]?\d):(?:[0-5]?\d)"
+ + @"(?:\.(?:\d{1,7}))?$",
+ _ => throw new InvalidOperationException()
+ },
+ _ => null
+ };
+
+ return pattern is not null;
+ }
+
+ private static bool TryGetJsonSchemaMinimum(this IType graphQLType, out decimal minimum)
+ {
+ switch (graphQLType.NullableType())
+ {
+ case ByteType byteType when byteType.MinValue != byte.MinValue:
+ minimum = byteType.MinValue;
+ return true;
+
+ case DecimalType decimalType when decimalType.MinValue != decimal.MinValue:
+ minimum = decimalType.MinValue;
+ return true;
+
+ case FloatType { MinValue: >= (double)decimal.MinValue } floatType:
+ minimum = (decimal)floatType.MinValue;
+ return true;
+
+ case IntType intType when intType.MinValue != int.MinValue:
+ minimum = intType.MinValue;
+ return true;
+
+ case LongType longType when longType.MinValue != long.MinValue:
+ minimum = longType.MinValue;
+ return true;
+
+ case ShortType shortType when shortType.MinValue != short.MinValue:
+ minimum = shortType.MinValue;
+ return true;
+ }
+
+ minimum = 0;
+ return false;
+ }
+
+ private static bool TryGetJsonSchemaMaximum(this IType graphQLType, out decimal maximum)
+ {
+ switch (graphQLType.NullableType())
+ {
+ case ByteType byteType when byteType.MaxValue != byte.MaxValue:
+ maximum = byteType.MaxValue;
+ return true;
+
+ case DecimalType decimalType when decimalType.MaxValue != decimal.MaxValue:
+ maximum = decimalType.MaxValue;
+ return true;
+
+ case FloatType { MaxValue: <= (double)decimal.MaxValue } floatType:
+ maximum = (decimal)floatType.MaxValue;
+ return true;
+
+ case IntType intType when intType.MaxValue != int.MaxValue:
+ maximum = intType.MaxValue;
+ return true;
+
+ case LongType longType when longType.MaxValue != long.MaxValue:
+ maximum = longType.MaxValue;
+ return true;
+
+ case ShortType shortType when shortType.MaxValue != short.MaxValue:
+ maximum = shortType.MaxValue;
+ return true;
+ }
+
+ maximum = 0;
+ return false;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/TypeNodeExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/TypeNodeExtensions.cs
new file mode 100644
index 00000000000..8588f51c132
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/TypeNodeExtensions.cs
@@ -0,0 +1,54 @@
+using System.Collections.Frozen;
+using HotChocolate.Language;
+using HotChocolate.Types;
+using static HotChocolate.ModelContextProtocol.Properties.ModelContextProtocolResources;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+internal static class TypeNodeExtensions
+{
+ public static IType GetGraphQLType(
+ this ITypeNode graphQLTypeNode,
+ ISchemaDefinition graphQLSchema)
+ {
+ var typeName = graphQLTypeNode.NamedType().Name.Value;
+
+ if (graphQLSchema.Types.TryGetType(typeName, out var typeDefinition))
+ {
+ return graphQLTypeNode.RewriteToType(typeDefinition);
+ }
+
+ if (s_typeMap.TryGetValue(typeName, out var type))
+ {
+ return graphQLTypeNode.RewriteToType(type.AsTypeDefinition());
+ }
+
+ throw new NotSupportedException(
+ string.Format(TypeNodeExtensions_UnableToFindGraphQLTypeInSchemaOrTypeMap, typeName));
+ }
+
+ private static readonly FrozenDictionary s_typeMap =
+ new Dictionary
+ {
+ { ScalarNames.Any, new AnyType() },
+ { ScalarNames.Boolean, new BooleanType() },
+ { ScalarNames.Byte, new ByteType() },
+ { ScalarNames.ByteArray, new ByteArrayType() },
+ { ScalarNames.Date, new DateType() },
+ { ScalarNames.DateTime, new DateTimeType() },
+ { ScalarNames.Decimal, new DecimalType() },
+ { ScalarNames.Float, new FloatType() },
+ { ScalarNames.ID, new IdType() },
+ { ScalarNames.Int, new IntType() },
+ { ScalarNames.JSON, new JsonType() },
+ { ScalarNames.LocalDate, new LocalDateType() },
+ { ScalarNames.LocalDateTime, new LocalDateTimeType() },
+ { ScalarNames.LocalTime, new LocalTimeType() },
+ { ScalarNames.Long, new LongType() },
+ { ScalarNames.Short, new ShortType() },
+ { ScalarNames.String, new StringType() },
+ { ScalarNames.TimeSpan, new TimeSpanType() },
+ { ScalarNames.URL, new UrlType() },
+ { ScalarNames.UUID, new UuidType() }
+ }.ToFrozenDictionary();
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/ValueNodeExtensions.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/ValueNodeExtensions.cs
new file mode 100644
index 00000000000..467e4a354ba
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Extensions/ValueNodeExtensions.cs
@@ -0,0 +1,75 @@
+using System.Text.Json.Nodes;
+using HotChocolate.Language;
+using HotChocolate.Types;
+using static HotChocolate.ModelContextProtocol.Properties.ModelContextProtocolResources;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+internal static class ValueNodeExtensions
+{
+ public static JsonNode? ToJsonNode(this IValueNode valueNode, IType graphQLType)
+ {
+ var nullableType = graphQLType.NullableType();
+
+ return valueNode switch
+ {
+ BooleanValueNode booleanValueNode => JsonValue.Create(booleanValueNode.Value),
+ EnumValueNode enumValueNode => JsonValue.Create(enumValueNode.Value),
+ FloatValueNode floatValueNode => nullableType switch
+ {
+ DecimalType => JsonValue.Create(floatValueNode.ToDecimal()),
+ FloatType => JsonValue.Create(floatValueNode.ToDouble()),
+ // FIXME: Treating all unknown scalar types as strings is a temporary solution.
+ _ => JsonValue.Create(floatValueNode.Value)
+ },
+ IntValueNode intValueNode => nullableType switch
+ {
+ ByteType => JsonValue.Create(intValueNode.ToByte()),
+ DecimalType => JsonValue.Create(intValueNode.ToDecimal()),
+ FloatType => JsonValue.Create(intValueNode.ToDouble()),
+ IntType => JsonValue.Create(intValueNode.ToInt32()),
+ LongType => JsonValue.Create(intValueNode.ToInt64()),
+ ShortType => JsonValue.Create(intValueNode.ToInt16()),
+ // FIXME: Treating all unknown scalar types as strings is a temporary solution.
+ _ => JsonValue.Create(intValueNode.Value)
+ },
+ ListValueNode listValueNode => listValueNode.ToJsonNode(nullableType),
+ NullValueNode => null,
+ ObjectValueNode objectValueNode => objectValueNode.ToJsonNode(nullableType),
+ StringValueNode stringValueNode => JsonValue.Create(stringValueNode.Value),
+ _ =>
+ throw new NotSupportedException(
+ string.Format(
+ ValueNodeExtensions_UnableToConvertValueNodeToJsonNode,
+ valueNode.GetType().Name))
+ };
+ }
+
+ private static JsonArray ToJsonNode(this ListValueNode valueNode, IType listType)
+ {
+ var jsonArray = new JsonArray();
+
+ foreach (var item in valueNode.Items)
+ {
+ jsonArray.Add(item.ToJsonNode(listType.ElementType()));
+ }
+
+ return jsonArray;
+ }
+
+ private static JsonObject ToJsonNode(this ObjectValueNode valueNode, IType objectType)
+ {
+ var jsonObject = new JsonObject();
+
+ foreach (var field in valueNode.Fields)
+ {
+ var graphQLFieldType = objectType is InputObjectType inputObjectType
+ ? inputObjectType.Fields[field.Name.Value].Type
+ : new AnyType(); // Types like JsonType or AnyType have no schema.
+
+ jsonObject.Add(field.Name.Value, field.Value.ToJsonNode(graphQLFieldType));
+ }
+
+ return jsonObject;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Factories/GraphQLMcpToolFactory.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Factories/GraphQLMcpToolFactory.cs
new file mode 100644
index 00000000000..d9204fdfde1
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Factories/GraphQLMcpToolFactory.cs
@@ -0,0 +1,364 @@
+using CaseConverter;
+using HotChocolate.Execution.Processing;
+using HotChocolate.Language;
+using HotChocolate.ModelContextProtocol.Extensions;
+using HotChocolate.Types;
+using Json.Schema;
+using ModelContextProtocol.Protocol;
+using static HotChocolate.ModelContextProtocol.WellKnownArgumentNames;
+using static HotChocolate.ModelContextProtocol.WellKnownDirectiveNames;
+
+namespace HotChocolate.ModelContextProtocol.Factories;
+
+internal sealed class GraphQLMcpToolFactory(ISchemaDefinition graphQLSchema)
+{
+ public GraphQLMcpTool CreateTool(string name, DocumentNode document)
+ {
+ var operationNode = document.Definitions.OfType().Single();
+ var mcpToolDirective = operationNode.GetMcpToolDirective();
+ var operationCompiler = new OperationCompiler(new InputParser());
+ var operation =
+ operationCompiler.Compile(
+ new OperationCompilerRequest(
+ operationNode.Name!.Value,
+ document,
+ operationNode,
+ (ObjectType)graphQLSchema.GetOperationType(operationNode.Operation),
+ graphQLSchema));
+ var inputSchema = CreateInputSchema(operationNode);
+ var outputSchema = CreateOutputSchema(operation);
+
+ var tool = new Tool
+ {
+ Name = name,
+ Title = mcpToolDirective?.Title ?? operation.Name!.InsertSpaceBeforeUpperCase(),
+ Description = operationNode.Description?.Value,
+ InputSchema = inputSchema.ToJsonElement(),
+ OutputSchema = outputSchema.ToJsonElement(),
+ Annotations = new ToolAnnotations
+ {
+ DestructiveHint = GetDestructiveHint(operation),
+ IdempotentHint = GetIdempotentHint(operation),
+ OpenWorldHint = GetOpenWorldHint(operation),
+ ReadOnlyHint = operationNode.Operation is not OperationType.Mutation
+ }
+ };
+
+ return new GraphQLMcpTool(operation, tool);
+ }
+
+ private JsonSchema CreateInputSchema(OperationDefinitionNode operation)
+ {
+ var properties = new Dictionary();
+ var requiredProperties = new List();
+
+ foreach (var variableNode in operation.VariableDefinitions)
+ {
+ var graphQLType = variableNode.Type.GetGraphQLType(graphQLSchema);
+ var propertyBuilder = graphQLType.ToJsonSchemaBuilder();
+ var variableName = variableNode.Variable.Name.Value;
+
+ // Description.
+ if (variableNode.Description is not null)
+ {
+ propertyBuilder.Description(variableNode.Description.Value);
+ }
+
+ // Default value.
+ if (variableNode.DefaultValue is not null)
+ {
+ propertyBuilder.Default(variableNode.DefaultValue.ToJsonNode(graphQLType));
+ }
+
+ // Required.
+ if (graphQLType.IsNonNullType() && variableNode.DefaultValue is null)
+ {
+ requiredProperties.Add(variableName);
+ }
+
+ properties.Add(variableName, propertyBuilder);
+ }
+
+ return
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Object)
+ .Properties(properties)
+ .Required(requiredProperties)
+ .Build();
+ }
+
+ private static JsonSchema CreateOutputSchema(IOperation operation)
+ {
+ return
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Object)
+ .Properties(
+ (WellKnownFieldNames.Data, CreateDataSchema(operation)),
+ (WellKnownFieldNames.Errors, s_errorSchema))
+ .AdditionalProperties(false)
+ .Build();
+ }
+
+ private static JsonSchema CreateDataSchema(IOperation operation)
+ {
+ var properties = new Dictionary();
+ var requiredProperties = new List();
+
+ foreach (var rootSelection in operation.RootSelectionSet.Selections)
+ {
+ var selectionState = rootSelection.GetSelectionState();
+
+ if (selectionState is SelectionState.Excluded)
+ {
+ continue;
+ }
+
+ properties.Add(
+ rootSelection.ResponseName,
+ CreateOutputSchema(rootSelection, operation));
+
+ if (selectionState is SelectionState.Included)
+ {
+ requiredProperties.Add(rootSelection.ResponseName);
+ }
+ }
+
+ return
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Object | SchemaValueType.Null)
+ .Properties(properties)
+ .AdditionalProperties(false)
+ .Required(requiredProperties)
+ .Build();
+ }
+
+ private static JsonSchema CreateOutputSchema(ISelection selection, IOperation operation)
+ {
+ var schemaBuilder = selection.Field.Type.ToJsonSchemaBuilder();
+
+ if (selection.SelectionSet is not null)
+ {
+ var properties = new Dictionary();
+ var requiredProperties = new List();
+
+ foreach (var type in operation.GetPossibleTypes(selection))
+ {
+ var selectionSet = operation.GetSelectionSet(selection, type);
+
+ foreach (var subSelection in selectionSet.Selections)
+ {
+ var selectionState = subSelection.GetSelectionState();
+
+ if (selectionState is SelectionState.Excluded)
+ {
+ continue;
+ }
+
+ var propertyAdded =
+ properties.TryAdd(
+ subSelection.ResponseName,
+ CreateOutputSchema(subSelection, operation));
+
+ if (propertyAdded && selectionState is SelectionState.Included)
+ {
+ requiredProperties.Add(subSelection.ResponseName);
+ }
+ }
+ }
+
+ if (selection.Field.Type.NullableType() is ListType listType)
+ {
+ var itemType = SchemaValueType.Object;
+
+ if (listType.ElementType.IsNullableType())
+ {
+ itemType |= SchemaValueType.Null;
+ }
+
+ var arrayItemSchemaBuilder
+ = new JsonSchemaBuilder()
+ .Type(itemType)
+ .Properties(properties)
+ .Required(requiredProperties)
+ .AdditionalProperties(false);
+
+ schemaBuilder.Items(arrayItemSchemaBuilder);
+ }
+ else
+ {
+ schemaBuilder
+ .Properties(properties)
+ .Required(requiredProperties)
+ .AdditionalProperties(false);
+ }
+ }
+
+ // Description.
+ if (selection.Field.Description is not null)
+ {
+ schemaBuilder.Description(selection.Field.Description);
+ }
+
+ return schemaBuilder.Build();
+ }
+
+ private static bool GetDestructiveHint(IOperation operation)
+ {
+ // @mcpTool operation directive.
+ if (operation.Definition.TryGetMcpToolDirective(out var mcpToolDirective)
+ && mcpToolDirective.DestructiveHint is { } destructiveHint)
+ {
+ return destructiveHint;
+ }
+
+ // @mcpToolAnnotations field directive.
+ var destructiveHints =
+ operation.RootSelectionSet.Selections
+ .Select(
+ s => s
+ .Field.Directives[McpToolAnnotations]
+ .SingleOrDefault()?
+ .GetArgumentValue(DestructiveHint)
+ // Default to `true` for mutations.
+ ?? operation.Type is OperationType.Mutation)
+ .ToList();
+
+ // Return `true` if any of the destructive hints are `true`.
+ return destructiveHints.Any(d => d);
+ }
+
+ private static bool GetIdempotentHint(IOperation operation)
+ {
+ // @mcpTool operation directive.
+ if (operation.Definition.TryGetMcpToolDirective(out var mcpToolDirective)
+ && mcpToolDirective.IdempotentHint is { } idempotentHint)
+ {
+ return idempotentHint;
+ }
+
+ // @mcpToolAnnotations field directive.
+ var idempotentHints =
+ operation.RootSelectionSet.Selections
+ .Select(
+ s => s
+ .Field.Directives[McpToolAnnotations]
+ .SingleOrDefault()?
+ .GetArgumentValue(IdempotentHint)
+ // Default to `true` for queries and subscriptions.
+ ?? operation.Type is not OperationType.Mutation)
+ .ToList();
+
+ // Return `true` if all the idempotent hints are `true`.
+ return idempotentHints.All(i => i);
+ }
+
+ private static bool GetOpenWorldHint(IOperation operation)
+ {
+ // @mcpTool operation directive.
+ if (operation.Definition.TryGetMcpToolDirective(out var mcpToolDirective)
+ && mcpToolDirective.OpenWorldHint is { } openWorldHint)
+ {
+ return openWorldHint;
+ }
+
+ // @mcpToolAnnotations field directive.
+ List openWorldHints = [];
+ foreach (var rootSelection in operation.RootSelectionSet.Selections)
+ {
+ var rootOpenWorldHint = GetOpenWorldHint(rootSelection, operation);
+
+ // Default to `true`.
+ openWorldHints.Add(rootOpenWorldHint ?? true);
+ }
+
+ // Return `true` if any of the open world hints are `true`.
+ return openWorldHints.Any(i => i);
+ }
+
+ private static bool? GetOpenWorldHint(
+ ISelection selection,
+ IOperation operation,
+ bool? parentOpenWorldHint = null)
+ {
+ var openWorldHint =
+ selection.Field.Directives[McpToolAnnotations]
+ .SingleOrDefault()?
+ .GetArgumentValue(OpenWorldHint) ?? parentOpenWorldHint;
+
+ // Return early if the open world hint is explicitly set to `true`.
+ if (openWorldHint == true)
+ {
+ return openWorldHint;
+ }
+
+ List openWorldHints = [openWorldHint];
+
+ if (selection.SelectionSet is not null)
+ {
+ foreach (var type in operation.GetPossibleTypes(selection))
+ {
+ var selectionSet = operation.GetSelectionSet(selection, type);
+
+ foreach (var subSelection in selectionSet.Selections)
+ {
+ openWorldHints.Add(
+ GetOpenWorldHint(
+ subSelection,
+ operation,
+ parentOpenWorldHint: openWorldHint));
+ }
+ }
+ }
+
+ return openWorldHints.All(o => o is null)
+ ? null
+ : openWorldHints.Any(o => o == true);
+ }
+
+ private static readonly JsonSchema s_integerSchema =
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Integer)
+ .Build();
+
+ private static readonly JsonSchema s_errorSchema =
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Array)
+ .Items(
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Object)
+ .Properties(
+ (
+ WellKnownFieldNames.Message,
+ new JsonSchemaBuilder().Type(SchemaValueType.String)
+ ),
+ (
+ WellKnownFieldNames.Locations,
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Array | SchemaValueType.Null)
+ .Items(
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Object)
+ .Properties(
+ (WellKnownFieldNames.Line, s_integerSchema),
+ (WellKnownFieldNames.Column, s_integerSchema))
+ .AdditionalProperties(false))
+ ),
+ (
+ WellKnownFieldNames.Path,
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Array | SchemaValueType.Null)
+ .Items(
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.String | SchemaValueType.Integer))
+ ),
+ (
+ WellKnownFieldNames.Extensions,
+ new JsonSchemaBuilder()
+ .Type(SchemaValueType.Object | SchemaValueType.Null)
+ .AdditionalProperties(true)
+ ))
+ .Required(WellKnownFieldNames.Message)
+ .AdditionalProperties(false)
+ .Build())
+ .Build();
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/GraphQLMcpTool.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/GraphQLMcpTool.cs
new file mode 100644
index 00000000000..52353cb44a7
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/GraphQLMcpTool.cs
@@ -0,0 +1,9 @@
+using HotChocolate.Execution.Processing;
+using ModelContextProtocol.Protocol;
+
+namespace HotChocolate.ModelContextProtocol;
+
+internal sealed record GraphQLMcpTool(IOperation Operation, Tool McpTool)
+{
+ public string Name => McpTool.Name;
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Handlers/CallToolHandler.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Handlers/CallToolHandler.cs
new file mode 100644
index 00000000000..a187aa004fc
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Handlers/CallToolHandler.cs
@@ -0,0 +1,71 @@
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using HotChocolate.Buffers;
+using HotChocolate.Execution;
+using HotChocolate.Language;
+using HotChocolate.ModelContextProtocol.Registries;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using static HotChocolate.ModelContextProtocol.Properties.ModelContextProtocolResources;
+
+namespace HotChocolate.ModelContextProtocol.Handlers;
+
+internal static class CallToolHandler
+{
+ public static async ValueTask HandleAsync(
+ RequestContext context,
+ CancellationToken cancellationToken)
+ {
+ var registry = context.Services!.GetRequiredService();
+
+ if (!registry.TryGetTool(context.Params!.Name, out var graphQLMcpTool))
+ {
+ return new CallToolResult
+ {
+ Content =
+ [
+ new TextContentBlock
+ {
+ Text = string.Format(CallToolHandler_ToolNotFound, context.Params.Name)
+ }
+ ],
+ IsError = true
+ };
+ }
+
+ var requestExecutor = context.Services!.GetRequiredService();
+ var arguments =
+ context.Params?.Arguments ?? Enumerable.Empty>();
+
+ Dictionary variableValues = [];
+ using var buffer = new PooledArrayWriter();
+ var jsonValueParser = new JsonValueParser(buffer: buffer);
+
+ foreach (var (name, value) in arguments)
+ {
+ variableValues.Add(name, jsonValueParser.Parse(value));
+ }
+
+ var result =
+ await requestExecutor.ExecuteAsync(
+ b => b
+ .SetDocument(graphQLMcpTool.Operation.Document)
+ .SetVariableValues(variableValues),
+ cancellationToken)
+ .ConfigureAwait(false);
+
+ var operationResult = result.ExpectOperationResult();
+
+ return new CallToolResult
+ {
+ // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
+ // For backwards compatibility, a tool that returns structured content SHOULD
+ // also return functionally equivalent unstructured content. (For example,
+ // serialized JSON can be returned in a TextContent block.)
+ Content = [new TextContentBlock { Text = operationResult.ToJson() }],
+ StructuredContent = JsonNode.Parse(operationResult.ToJson()),
+ IsError = operationResult.Errors?.Any()
+ };
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Handlers/ListToolsHandler.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Handlers/ListToolsHandler.cs
new file mode 100644
index 00000000000..256d76cbc19
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Handlers/ListToolsHandler.cs
@@ -0,0 +1,19 @@
+using HotChocolate.ModelContextProtocol.Registries;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+
+namespace HotChocolate.ModelContextProtocol.Handlers;
+
+internal static class ListToolsHandler
+{
+ public static ListToolsResult Handle(RequestContext context)
+ {
+ var registry = context.Services!.GetRequiredService();
+
+ return new ListToolsResult
+ {
+ Tools = registry.GetTools().Values.Select(t => t.McpTool).ToList()
+ };
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/HotChocolate.ModelContextProtocol.csproj b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/HotChocolate.ModelContextProtocol.csproj
new file mode 100644
index 00000000000..a51113af55e
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/HotChocolate.ModelContextProtocol.csproj
@@ -0,0 +1,38 @@
+
+
+
+ HotChocolate.ModelContextProtocol
+ HotChocolate.ModelContextProtocol
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ResXFileCodeGenerator
+ ModelContextProtocolResources.Designer.cs
+
+
+
+
+
+ True
+ True
+ ModelContextProtocolResources.resx
+
+
+
+
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/JsonSerializerContexts/JsonSchemaJsonSerializerContext.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/JsonSerializerContexts/JsonSchemaJsonSerializerContext.cs
new file mode 100644
index 00000000000..53e8870529a
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/JsonSerializerContexts/JsonSchemaJsonSerializerContext.cs
@@ -0,0 +1,8 @@
+using System.Text.Json.Serialization;
+using Json.Schema;
+
+namespace HotChocolate.ModelContextProtocol.JsonSerializerContexts;
+
+[JsonSerializable(typeof(JsonSchema))]
+[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Serialization)]
+internal partial class JsonSchemaJsonSerializerContext : JsonSerializerContext;
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Properties/ModelContextProtocolResources.Designer.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Properties/ModelContextProtocolResources.Designer.cs
new file mode 100644
index 00000000000..849b2d99db8
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Properties/ModelContextProtocolResources.Designer.cs
@@ -0,0 +1,134 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace HotChocolate.ModelContextProtocol.Properties {
+ using System;
+
+
+ ///
+ /// A strongly-typed resource class, for looking up localized strings, etc.
+ ///
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class ModelContextProtocolResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal ModelContextProtocolResources() {
+ }
+
+ ///
+ /// Returns the cached ResourceManager instance used by this class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HotChocolate.ModelContextProtocol.Properties.ModelContextProtocolResources", typeof(ModelContextProtocolResources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The tool '{0}' was not found..
+ ///
+ internal static string CallToolHandler_ToolNotFound {
+ get {
+ return ResourceManager.GetString("CallToolHandler_ToolNotFound", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A tool document must contain a single operation definition..
+ ///
+ internal static string InMemoryMcpOperationDocumentStorage_ToolDocumentMustContainSingleOperation {
+ get {
+ return ResourceManager.GetString("InMemoryMcpOperationDocumentStorage_ToolDocumentMustContainSingleOperation", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A tool document operation with the same name already exists..
+ ///
+ internal static string InMemoryMcpOperationDocumentStorage_ToolDocumentOperationAlreadyExists {
+ get {
+ return ResourceManager.GetString("InMemoryMcpOperationDocumentStorage_ToolDocumentOperationAlreadyExists", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to A tool document operation must be named..
+ ///
+ internal static string InMemoryMcpOperationDocumentStorage_ToolDocumentOperationMustBeNamed {
+ get {
+ return ResourceManager.GetString("InMemoryMcpOperationDocumentStorage_ToolDocumentOperationMustBeNamed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The argument `{0}` is not supported on @mcpTool..
+ ///
+ internal static string McpToolDirectiveParser_ArgumentNotSupportedOnMcpToolDirective {
+ get {
+ return ResourceManager.GetString("McpToolDirectiveParser_ArgumentNotSupportedOnMcpToolDirective", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unable to determine the JSON schema value type for the GraphQL type '{0}'..
+ ///
+ internal static string TypeExtensions_UnableToDetermineJsonSchemaValueType {
+ get {
+ return ResourceManager.GetString("TypeExtensions_UnableToDetermineJsonSchemaValueType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unable to find GraphQL type '{0}' in the schema or type map..
+ ///
+ internal static string TypeNodeExtensions_UnableToFindGraphQLTypeInSchemaOrTypeMap {
+ get {
+ return ResourceManager.GetString("TypeNodeExtensions_UnableToFindGraphQLTypeInSchemaOrTypeMap", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Unable to convert value node of type '{0}' to JSON node..
+ ///
+ internal static string ValueNodeExtensions_UnableToConvertValueNodeToJsonNode {
+ get {
+ return ResourceManager.GetString("ValueNodeExtensions_UnableToConvertValueNodeToJsonNode", resourceCulture);
+ }
+ }
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Properties/ModelContextProtocolResources.resx b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Properties/ModelContextProtocolResources.resx
new file mode 100644
index 00000000000..3484ef6556d
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Properties/ModelContextProtocolResources.resx
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ The tool '{0}' was not found.
+
+
+ A tool document must contain a single operation definition.
+
+
+ A tool document operation with the same name already exists.
+
+
+ A tool document operation must be named.
+
+
+ The argument `{0}` is not supported on @mcpTool.
+
+
+ Unable to determine the JSON schema value type for the GraphQL type '{0}'.
+
+
+ Unable to find GraphQL type '{0}' in the schema or type map.
+
+
+ Unable to convert value node of type '{0}' to JSON node.
+
+
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/McpRequestExecutorProxy.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/McpRequestExecutorProxy.cs
new file mode 100644
index 00000000000..35ac24880aa
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/McpRequestExecutorProxy.cs
@@ -0,0 +1,55 @@
+using HotChocolate.Execution;
+using HotChocolate.Features;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.AspNetCore;
+
+namespace HotChocolate.ModelContextProtocol.Proxies;
+
+internal sealed class McpRequestExecutorProxy(
+ IRequestExecutorProvider executorProvider,
+ IRequestExecutorEvents executorEvents,
+ string schemaName)
+ : RequestExecutorProxy(executorProvider, executorEvents, schemaName)
+{
+ private McpExecutorSession? _session;
+
+ public McpExecutorSession GetOrCreateSession()
+ {
+ return _session
+ ?? Task.Factory
+ .StartNew(async () => await GetOrCreateSessionAsync(CancellationToken.None))
+ .Unwrap()
+ .GetAwaiter()
+ .GetResult();
+ }
+
+ public async ValueTask GetOrCreateSessionAsync(
+ CancellationToken cancellationToken)
+ {
+ if (_session is not null)
+ {
+ return _session;
+ }
+
+ var executor = await GetExecutorAsync(cancellationToken).ConfigureAwait(false);
+ return executor.Features.GetRequired();
+ }
+
+ protected override void OnRequestExecutorUpdated(IRequestExecutor? executor)
+ {
+ if (executor is not null)
+ {
+ var session =
+ new McpExecutorSession(
+ executor.Schema.Services.GetRequiredService(),
+ executor.Schema.Services.GetRequiredService());
+
+ executor.Features.Set(session);
+ _session = session;
+ }
+ }
+}
+
+internal sealed record McpExecutorSession(
+ StreamableHttpHandler StreamableHttpHandler,
+ SseHandler SseHandler);
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/SseHandlerProxy.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/SseHandlerProxy.cs
new file mode 100644
index 00000000000..f0491b85b42
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/SseHandlerProxy.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.Http;
+
+namespace HotChocolate.ModelContextProtocol.Proxies;
+
+internal sealed class SseHandlerProxy
+{
+ private readonly McpRequestExecutorProxy _mcpRequestExecutor;
+
+ public SseHandlerProxy(McpRequestExecutorProxy mcpRequestExecutor)
+ {
+ ArgumentNullException.ThrowIfNull(mcpRequestExecutor);
+ _mcpRequestExecutor = mcpRequestExecutor;
+ }
+
+ public async Task HandleSseRequestAsync(HttpContext context)
+ {
+ var session = await _mcpRequestExecutor.GetOrCreateSessionAsync(context.RequestAborted);
+ await session.SseHandler.HandleSseRequestAsync(context);
+ }
+
+ public async Task HandleMessageRequestAsync(HttpContext context)
+ {
+ var session = await _mcpRequestExecutor.GetOrCreateSessionAsync(context.RequestAborted);
+ await session.SseHandler.HandleMessageRequestAsync(context);
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/StreamableHttpHandlerProxy.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/StreamableHttpHandlerProxy.cs
new file mode 100644
index 00000000000..13592faa2f1
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Proxies/StreamableHttpHandlerProxy.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Http;
+using ModelContextProtocol.AspNetCore;
+
+namespace HotChocolate.ModelContextProtocol.Proxies;
+
+internal sealed class StreamableHttpHandlerProxy
+{
+ private readonly McpRequestExecutorProxy _mcpRequestExecutor;
+
+ public StreamableHttpHandlerProxy(McpRequestExecutorProxy mcpRequestExecutor)
+ {
+ ArgumentNullException.ThrowIfNull(mcpRequestExecutor);
+ _mcpRequestExecutor = mcpRequestExecutor;
+ }
+
+ public HttpServerTransportOptions HttpServerTransportOptions
+ => _mcpRequestExecutor.GetOrCreateSession().StreamableHttpHandler.HttpServerTransportOptions;
+
+ public async Task HandlePostRequestAsync(HttpContext context)
+ {
+ var session = await _mcpRequestExecutor.GetOrCreateSessionAsync(context.RequestAborted);
+ await session.StreamableHttpHandler.HandlePostRequestAsync(context);
+ }
+
+ public async Task HandleGetRequestAsync(HttpContext context)
+ {
+ var session = await _mcpRequestExecutor.GetOrCreateSessionAsync(context.RequestAborted);
+ await session.StreamableHttpHandler.HandleGetRequestAsync(context);
+ }
+
+ public async Task HandleDeleteRequestAsync(HttpContext context)
+ {
+ var session = await _mcpRequestExecutor.GetOrCreateSessionAsync(context.RequestAborted);
+ await session.StreamableHttpHandler.HandleDeleteRequestAsync(context);
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Registries/GraphQLMcpToolRegistry.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Registries/GraphQLMcpToolRegistry.cs
new file mode 100644
index 00000000000..505e754e77b
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Registries/GraphQLMcpToolRegistry.cs
@@ -0,0 +1,28 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace HotChocolate.ModelContextProtocol.Registries;
+
+internal sealed class GraphQLMcpToolRegistry
+{
+ private readonly Dictionary _tools = [];
+
+ public void Add(GraphQLMcpTool tool)
+ {
+ _tools[tool.Name] = tool;
+ }
+
+ public Dictionary GetTools()
+ {
+ return _tools;
+ }
+
+ public bool TryGetTool(string name, [NotNullWhen(true)] out GraphQLMcpTool? tool)
+ {
+ return _tools.TryGetValue(name, out tool);
+ }
+
+ public void Clear()
+ {
+ _tools.Clear();
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Storage/IMcpOperationDocumentStorage.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Storage/IMcpOperationDocumentStorage.cs
new file mode 100644
index 00000000000..0062dd674d6
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Storage/IMcpOperationDocumentStorage.cs
@@ -0,0 +1,13 @@
+using HotChocolate.Language;
+
+namespace HotChocolate.ModelContextProtocol.Storage;
+
+public interface IMcpOperationDocumentStorage
+{
+ ValueTask> GetToolDocumentsAsync(
+ CancellationToken cancellationToken = default);
+
+ ValueTask SaveToolDocumentAsync(
+ DocumentNode document,
+ CancellationToken cancellationToken = default);
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Storage/InMemoryMcpOperationDocumentStorage.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Storage/InMemoryMcpOperationDocumentStorage.cs
new file mode 100644
index 00000000000..93fc629e0d1
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Storage/InMemoryMcpOperationDocumentStorage.cs
@@ -0,0 +1,45 @@
+using CaseConverter;
+using HotChocolate.Language;
+using static HotChocolate.ModelContextProtocol.Properties.ModelContextProtocolResources;
+
+namespace HotChocolate.ModelContextProtocol.Storage;
+
+public sealed class InMemoryMcpOperationDocumentStorage : IMcpOperationDocumentStorage
+{
+ private readonly Dictionary _tools = [];
+
+ public ValueTask> GetToolDocumentsAsync(
+ CancellationToken cancellationToken = default)
+ {
+ return ValueTask.FromResult(_tools);
+ }
+
+ public ValueTask SaveToolDocumentAsync(
+ DocumentNode document,
+ CancellationToken cancellationToken = default)
+ {
+ var operationDefinitions = document.Definitions
+ .OfType()
+ .ToList();
+
+ if (operationDefinitions.Count != 1)
+ {
+ throw new Exception(
+ InMemoryMcpOperationDocumentStorage_ToolDocumentMustContainSingleOperation);
+ }
+
+ if (operationDefinitions[0].Name is not { } nameNode)
+ {
+ throw new Exception(
+ InMemoryMcpOperationDocumentStorage_ToolDocumentOperationMustBeNamed);
+ }
+
+ if (!_tools.TryAdd(nameNode.Value.ToSnakeCase(), document))
+ {
+ throw new Exception(
+ InMemoryMcpOperationDocumentStorage_ToolDocumentOperationAlreadyExists);
+ }
+
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Types/McpToolAnnotationsDirectiveType.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Types/McpToolAnnotationsDirectiveType.cs
new file mode 100644
index 00000000000..54b60d58339
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/Types/McpToolAnnotationsDirectiveType.cs
@@ -0,0 +1,43 @@
+using HotChocolate.ModelContextProtocol.Directives;
+using HotChocolate.Types;
+
+namespace HotChocolate.ModelContextProtocol.Types;
+
+///
+/// Additional properties describing a Tool to clients.
+///
+public sealed class McpToolAnnotationsDirectiveType : DirectiveType
+{
+ private const string DirectiveName = "mcpToolAnnotations";
+
+ protected override void Configure(
+ IDirectiveTypeDescriptor descriptor)
+ {
+ descriptor
+ .Name(DirectiveName)
+ .Description("Additional properties describing a Tool to clients.")
+ .Location(DirectiveLocation.FieldDefinition);
+
+ descriptor
+ .Argument(d => d.DestructiveHint)
+ .Type()
+ .Description(
+ "If `true`, the tool may perform destructive updates to its environment. If "
+ + "`false`, the tool performs only additive updates.");
+
+ descriptor
+ .Argument(d => d.IdempotentHint)
+ .Type()
+ .Description(
+ "If `true`, calling the tool repeatedly with the same arguments will have no "
+ + "additional effect on its environment.");
+
+ descriptor
+ .Argument(d => d.OpenWorldHint)
+ .Type()
+ .Description(
+ "If `true`, this tool may interact with an “open world” of external entities. If "
+ + "`false`, the tool’s domain of interaction is closed. For example, the world of "
+ + "a web search tool is open, whereas that of a memory tool is not.");
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownArgumentNames.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownArgumentNames.cs
new file mode 100644
index 00000000000..5563cf0ef12
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownArgumentNames.cs
@@ -0,0 +1,10 @@
+namespace HotChocolate.ModelContextProtocol;
+
+internal static class WellKnownArgumentNames
+{
+ public const string If = "if";
+ public const string Title = "title";
+ public const string DestructiveHint = "destructiveHint";
+ public const string IdempotentHint = "idempotentHint";
+ public const string OpenWorldHint = "openWorldHint";
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownDirectiveNames.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownDirectiveNames.cs
new file mode 100644
index 00000000000..eedb0beca7b
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownDirectiveNames.cs
@@ -0,0 +1,9 @@
+namespace HotChocolate.ModelContextProtocol;
+
+internal static class WellKnownDirectiveNames
+{
+ public const string Include = "include";
+ public const string McpTool = "mcpTool";
+ public const string McpToolAnnotations = "mcpToolAnnotations";
+ public const string Skip = "skip";
+}
diff --git a/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownFieldNames.cs b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownFieldNames.cs
new file mode 100644
index 00000000000..39bd254c68c
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/src/HotChocolate.ModelContextProtocol/WellKnownFieldNames.cs
@@ -0,0 +1,13 @@
+namespace HotChocolate.ModelContextProtocol;
+
+internal static class WellKnownFieldNames
+{
+ public const string Column = "column";
+ public const string Data = "data";
+ public const string Errors = "errors";
+ public const string Extensions = "extensions";
+ public const string Line = "line";
+ public const string Locations = "locations";
+ public const string Message = "message";
+ public const string Path = "path";
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/Directory.Build.props b/src/HotChocolate/ModelContextProtocol/test/Directory.Build.props
new file mode 100644
index 00000000000..d1285228361
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/Directory.Build.props
@@ -0,0 +1,33 @@
+
+
+
+
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Extensions/TypeExtensionsTests.cs b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Extensions/TypeExtensionsTests.cs
new file mode 100644
index 00000000000..e8b29884b49
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Extensions/TypeExtensionsTests.cs
@@ -0,0 +1,105 @@
+using System.Text.RegularExpressions;
+using HotChocolate.Types;
+using Json.Schema;
+
+namespace HotChocolate.ModelContextProtocol.Extensions;
+
+public sealed class TypeExtensionsTests
+{
+ [Theory]
+ [InlineData(typeof(ByteArrayType), "dmFsdWU=")]
+ // A DateTime with UTC offset (+00:00).
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.108Z")]
+ // A DateTime with +00:00 which is the same as UTC.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.108+00:00")]
+ // The z and t may be lower case.
+ [InlineData(typeof(DateTimeType), "2011-08-30t13:22:53.108z")]
+ // A DateTime with -3h offset.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.108-03:00")]
+ // A DateTime with +3h 30min offset.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.108+03:30")]
+ // A DateTime with 7 fractional digits.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.1230000+03:30")]
+ // A DateTime with no fractional seconds.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53+03:30")]
+ [InlineData(typeof(LocalDateTimeType), "2011-08-30T13:22:53")]
+ [InlineData(typeof(LocalTimeType), "13:22:53")]
+ [InlineData(typeof(TimeSpanType), "P18M")]
+ [InlineData(typeof(TimeSpanType), "PT1H30M")]
+ [InlineData(typeof(TimeSpanType), "P1Y2M3DT4H5M6S")]
+ [InlineData(typeof(TimeSpanType), "PT4.567S")]
+ public void ToJsonSchemaBuilder_ValidValues_MatchPattern(Type type, string value)
+ {
+ // arrange
+ var instance = (IType)Activator.CreateInstance(type)!;
+
+ // act
+ var jsonSchema = instance.ToJsonSchemaBuilder().Build();
+ var regex = new Regex(jsonSchema.GetPatternValue()!);
+
+ // assert
+ Assert.Matches(regex, value);
+ }
+
+ [Theory]
+ [InlineData(typeof(ByteArrayType), "invalidBase64")]
+ // The minutes of the offset are missing.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.108-03")]
+ // No offset provided.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.108")]
+ // No time provided.
+ [InlineData(typeof(DateTimeType), "2011-08-30")]
+ // Seconds are not allowed for the offset.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.108+03:30:15")]
+ // A DateTime with 8 fractional digits.
+ [InlineData(typeof(DateTimeType), "2011-08-30T13:22:53.12345678+03:30")]
+ [InlineData(typeof(LocalDateTimeType), "2018/06/11T08:46:14 pm")]
+ [InlineData(typeof(LocalDateTimeType), "abc")]
+ [InlineData(typeof(LocalTimeType), "08:46:14 pm")]
+ [InlineData(typeof(LocalTimeType), "abc")]
+ [InlineData(typeof(TimeSpanType), "bad")]
+ public void ToJsonSchemaBuilder_InvalidValues_DoNotMatchPattern(Type type, string value)
+ {
+ // arrange
+ var instance = (IType)Activator.CreateInstance(type)!;
+
+ // act
+ var jsonSchema = instance.ToJsonSchemaBuilder().Build();
+ var regex = new Regex(jsonSchema.GetPatternValue()!);
+
+ // assert
+ Assert.DoesNotMatch(regex, value);
+ }
+
+ [Theory]
+ [InlineData("-10675199.02:48:05.4775808")] // TimeSpan.MinValue.
+ [InlineData("10675199.02:48:05.4775807")] // TimeSpan.MaxValue.
+ public void ToJsonSchemaBuilder_TimeSpanTypeDotNetValidValues_MatchPattern(string value)
+ {
+ // arrange
+ var timeSpanType = new TimeSpanType(TimeSpanFormat.DotNet);
+
+ // act
+ var jsonSchema = timeSpanType.ToJsonSchemaBuilder().Build();
+ var regex = new Regex(jsonSchema.GetPatternValue()!);
+
+ // assert
+ Assert.Matches(regex, value);
+ }
+
+ [Theory]
+ [InlineData("bad")] // Invalid format.
+ [InlineData("+01:30:00")] // A leading plus sign is not allowed.
+ public void ToJsonSchemaBuilder_TimeSpanTypeDotNetInvalidValues_DoNotMatchPattern(string value)
+ {
+ // arrange
+ var timeSpanType = new TimeSpanType(TimeSpanFormat.DotNet);
+
+ // act
+ var jsonSchema = timeSpanType.ToJsonSchemaBuilder().Build();
+ var regex = new Regex(jsonSchema.GetPatternValue()!);
+
+ // assert
+ Assert.DoesNotMatch(regex, value);
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/GraphQLMcpToolFactoryTests.cs b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/GraphQLMcpToolFactoryTests.cs
new file mode 100644
index 00000000000..3bc59c9b819
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/GraphQLMcpToolFactoryTests.cs
@@ -0,0 +1,770 @@
+using CookieCrumble;
+using HotChocolate.Language;
+using HotChocolate.ModelContextProtocol.Extensions;
+using HotChocolate.Types;
+
+namespace HotChocolate.ModelContextProtocol.Factories;
+
+public sealed class GraphQLMcpToolFactoryTests
+{
+ [Fact]
+ public void CreateTool_DocumentWithNoOperations_ThrowsException()
+ {
+ // arrange & act
+ static GraphQLMcpTool Action()
+ {
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse("fragment Fragment on Type { field }");
+
+ return new GraphQLMcpToolFactory(schema).CreateTool("", document);
+ }
+
+ // assert
+ Assert.Throws(Action);
+ }
+
+ [Fact]
+ public void CreateTool_DocumentWithMultipleOperations_ThrowsException()
+ {
+ // arrange & act
+ static GraphQLMcpTool Action()
+ {
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ query Operation1 {
+ query {
+ field
+ }
+ }
+
+ query Operation2 {
+ query {
+ field
+ }
+ }
+ """);
+
+ return new GraphQLMcpToolFactory(schema).CreateTool("", document);
+ }
+
+ // assert
+ Assert.Throws(Action);
+ }
+
+ [Fact]
+ public void CreateTool_ValidQueryDocument_ReturnsTool()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ "Get books"
+ query GetBooks {
+ books {
+ title
+ }
+ }
+ """);
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("get_books", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ Assert.Equal("get_books", mcpTool.Name);
+ Assert.Equal("Get Books", mcpTool.Title);
+ Assert.Equal("Get books", mcpTool.Description);
+ Assert.Equal(false, mcpTool.Annotations?.DestructiveHint);
+ Assert.Equal(true, mcpTool.Annotations?.IdempotentHint);
+ Assert.Equal(true, mcpTool.Annotations?.OpenWorldHint);
+ Assert.Equal(true, mcpTool.Annotations?.ReadOnlyHint);
+ }
+
+ [Fact]
+ public void CreateTool_ValidMutationDocument_ReturnsTool()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ "Add book"
+ mutation AddBook {
+ addBook {
+ title
+ }
+ }
+ """);
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("add_book", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ Assert.Equal("add_book", mcpTool.Name);
+ Assert.Equal("Add Book", mcpTool.Title);
+ Assert.Equal("Add book", mcpTool.Description);
+ Assert.Equal(true, mcpTool.Annotations?.DestructiveHint);
+ Assert.Equal(false, mcpTool.Annotations?.IdempotentHint);
+ Assert.Equal(true, mcpTool.Annotations?.OpenWorldHint);
+ Assert.Equal(false, mcpTool.Annotations?.ReadOnlyHint);
+ }
+
+ [Fact]
+ public void CreateTool_ValidSubscriptionDocument_ReturnsTool()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ "Book added"
+ subscription BookAdded {
+ bookAdded {
+ title
+ }
+ }
+ """);
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("book_added", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ Assert.Equal("book_added", mcpTool.Name);
+ Assert.Equal("Book Added", mcpTool.Title);
+ Assert.Equal("Book added", mcpTool.Description);
+ Assert.Equal(false, mcpTool.Annotations?.DestructiveHint);
+ Assert.Equal(true, mcpTool.Annotations?.IdempotentHint);
+ Assert.Equal(true, mcpTool.Annotations?.OpenWorldHint);
+ Assert.Equal(true, mcpTool.Annotations?.ReadOnlyHint);
+ }
+
+ [Fact]
+ public void CreateTool_SetTitle_ReturnsTool()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ query GetBooks @mcpTool(title: "Custom Title") {
+ books {
+ title
+ }
+ }
+ """);
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("get_books", document);
+
+ // assert
+ Assert.Equal("Custom Title", tool.McpTool.Title);
+ }
+
+ [Fact]
+ public void CreateTool_SetAnnotations_ReturnsTool()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ mutation AddBook
+ @mcpTool(destructiveHint: false, idempotentHint: true, openWorldHint: false) {
+ addBook {
+ title
+ }
+ }
+ """);
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("add_book", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ Assert.Equal(false, mcpTool.Annotations?.DestructiveHint);
+ Assert.Equal(true, mcpTool.Annotations?.IdempotentHint);
+ Assert.Equal(false, mcpTool.Annotations?.OpenWorldHint);
+ }
+
+ [Fact]
+ public void CreateTool_WithNullableVariables_CreatesCorrectSchema()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ File.ReadAllText("__resources__/GetWithNullableVariables.graphql"));
+
+ // act
+ var tool =
+ new GraphQLMcpToolFactory(schema).CreateTool("get_with_nullable_variables", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ mcpTool.InputSchema.MatchSnapshot(postFix: "Input", extension: ".json");
+ mcpTool.OutputSchema.MatchSnapshot(postFix: "Output", extension: ".json");
+ }
+
+ [Fact]
+ public void CreateTool_WithNonNullableVariables_CreatesCorrectSchema()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ File.ReadAllText("__resources__/GetWithNonNullableVariables.graphql"));
+
+ // act
+ var tool =
+ new GraphQLMcpToolFactory(schema)
+ .CreateTool("get_with_non_nullable_variables", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ mcpTool.InputSchema.MatchSnapshot(postFix: "Input", extension: ".json");
+ mcpTool.OutputSchema.MatchSnapshot(postFix: "Output", extension: ".json");
+ }
+
+ [Fact]
+ public void CreateTool_WithDefaultedVariables_CreatesCorrectSchema()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ File.ReadAllText("__resources__/GetWithDefaultedVariables.graphql"));
+
+ // act
+ var tool =
+ new GraphQLMcpToolFactory(schema).CreateTool("get_with_defaulted_variables", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ mcpTool.InputSchema.MatchSnapshot(postFix: "Input", extension: ".json");
+ mcpTool.OutputSchema.MatchSnapshot(postFix: "Output", extension: ".json");
+ }
+
+ [Fact]
+ public void CreateTool_WithComplexVariables_CreatesCorrectSchema()
+ {
+ // arrange
+ var schema = CreateSchema(s => s.AddType(new TimeSpanType(TimeSpanFormat.DotNet)));
+ var document = Utf8GraphQLParser.Parse(
+ File.ReadAllText("__resources__/GetWithComplexVariables.graphql"));
+
+ // act
+ var tool =
+ new GraphQLMcpToolFactory(schema).CreateTool("get_with_complex_variables", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ mcpTool.InputSchema.MatchSnapshot(postFix: "Input", extension: ".json");
+ mcpTool.OutputSchema.MatchSnapshot(postFix: "Output", extension: ".json");
+ }
+
+ [Fact]
+ public void CreateTool_WithVariableMinMaxValues_CreatesCorrectInputSchema()
+ {
+ // arrange
+ var schema =
+ CreateSchema(
+ s => s
+ .AddType(new ByteType(min: 1, max: 10))
+ .AddType(new DecimalType(min: 1, max: 10))
+ .AddType(new FloatType(min: 1.0, max: 10.0))
+ .AddType(new IntType(min: 1, max: 10))
+ .AddType(new LongType(min: 1, max: 10))
+ .AddType(new ShortType(min: 1, max: 10)));
+ var document = Utf8GraphQLParser.Parse(
+ File.ReadAllText("__resources__/GetWithVariableMinMaxValues.graphql"));
+
+ // act
+ var tool =
+ new GraphQLMcpToolFactory(schema)
+ .CreateTool("get_with_variable_min_max_values", document);
+
+ // assert
+ tool.McpTool.InputSchema.MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public void CreateTool_WithInterfaceType_CreatesCorrectOutputSchema()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ query GetWithInterfaceType {
+ withInterfaceType {
+ __typename
+ name
+ ... on Cat {
+ isPurring
+ }
+ ... on Dog {
+ isBarking
+ }
+ }
+ }
+ """);
+
+ // act
+ var tool =
+ new GraphQLMcpToolFactory(schema).CreateTool("get_with_interface_type", document);
+
+ // assert
+ tool.McpTool.OutputSchema.MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public void CreateTool_WithUnionType_CreatesCorrectOutputSchema()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ query GetWithUnionType {
+ withUnionType {
+ __typename
+ ... on Cat {
+ isPurring
+ }
+ ... on Dog {
+ isBarking
+ }
+ }
+ }
+ """);
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("get_with_union_type", document);
+
+ // assert
+ tool.McpTool.OutputSchema.MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public void CreateTool_WithSkipAndInclude_CreatesCorrectOutputSchema()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ query GetWithSkipAndInclude($skip: Boolean!, $include: Boolean!) {
+ withDefaultedVariables {
+ # Skip
+ skipped: int @skip(if: true)
+ notSkipped: int @skip(if: false)
+ possiblySkipped: int @skip(if: $skip)
+ # Include
+ included: int @include(if: true)
+ notIncluded: int @include(if: false)
+ possiblyIncluded: int @include(if: $include)
+ # Skip and Include (excluded)
+ skippedAndIncluded: int @skip(if: true) @include(if: true)
+ skippedAndNotIncluded: int @skip(if: true) @include(if: false)
+ skippedAndPossiblyIncluded: int @skip(if: true) @include(if: $include)
+ notSkippedAndNotIncluded: int @skip(if: false) @include(if: false)
+ possiblySkippedAndNotIncluded: int @skip(if: $skip) @include(if: false)
+ # Skip and Include (included)
+ notSkippedAndIncluded: int @skip(if: false) @include(if: true)
+ notSkippedAndPossiblyIncluded: int @skip(if: false) @include(if: $include)
+ possiblySkippedAndIncluded: int @skip(if: $skip) @include(if: true)
+ possiblySkippedAndPossiblyIncluded: int
+ @skip(if: $skip)
+ @include(if: $include)
+ # Object field (nested fields are still required)
+ objectFieldPossiblySkipped: object @skip(if: $skip) {
+ field1A { field1B { field1C } }
+ }
+ # Fragment spread
+ ...Fragment @skip(if: $skip)
+ # Inline fragment
+ ... @skip(if: $skip) {
+ inlineFragmentFieldPossiblySkipped: int
+ }
+ }
+ }
+
+ fragment Fragment on Object1Defaulted {
+ field1A { field1B { field1C } }
+ }
+ """);
+
+ // act
+ var tool =
+ new GraphQLMcpToolFactory(schema).CreateTool("get_with_skip_and_include", document);
+
+ // assert
+ tool.McpTool.OutputSchema.MatchSnapshot(extension: ".json");
+ }
+
+ [Theory]
+ [InlineData("ImplicitDestructiveTool.graphql", true)]
+ [InlineData("ExplicitDestructiveTool.graphql", true)]
+ [InlineData("ExplicitNonDestructiveTool.graphql", false)]
+ public void CreateTool_McpToolAnnotationsDestructiveHintImplementationFirst_SetsCorrectHint(
+ string fileName,
+ bool destructiveHint)
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(destructiveHint, tool.McpTool.Annotations?.DestructiveHint);
+ }
+
+ [Theory]
+ [InlineData("ImplicitDestructiveTool.graphql", true)]
+ [InlineData("ExplicitDestructiveTool.graphql", true)]
+ [InlineData("ExplicitNonDestructiveTool.graphql", false)]
+ public void CreateTool_McpToolAnnotationsCodeFirst_SetsCorrectHint(
+ string fileName,
+ bool destructiveHint)
+ {
+ // arrange
+ var schema =
+ SchemaBuilder
+ .New()
+ .AddMcp()
+ .AddMutationType(
+ descriptor =>
+ {
+ descriptor.Name(OperationTypeNames.Mutation);
+
+ descriptor
+ .Field("implicitDestructiveMutation")
+ .Type();
+
+ descriptor
+ .Field("explicitDestructiveMutation")
+ .Type()
+ .McpToolAnnotations(destructiveHint: true);
+
+ descriptor
+ .Field("explicitNonDestructiveMutation")
+ .Type()
+ .McpToolAnnotations(destructiveHint: false);
+ })
+ .Use(next => next)
+ .ModifyOptions(o => o.StrictValidation = false)
+ .Create();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(destructiveHint, tool.McpTool.Annotations?.DestructiveHint);
+ }
+
+ [Theory]
+ [InlineData("ImplicitDestructiveTool.graphql", true)]
+ [InlineData("ExplicitDestructiveTool.graphql", true)]
+ [InlineData("ExplicitNonDestructiveTool.graphql", false)]
+ public void CreateTool_McpToolAnnotationsDestructiveHintSchemaFirst_SetsCorrectHint(
+ string fileName,
+ bool destructiveHint)
+ {
+ // arrange
+ var schema =
+ SchemaBuilder
+ .New()
+ .AddMcp()
+ .AddDocumentFromString(
+ """
+ type Mutation {
+ implicitDestructiveMutation: Int
+ explicitDestructiveMutation: Int
+ @mcpToolAnnotations(destructiveHint: true)
+ explicitNonDestructiveMutation: Int
+ @mcpToolAnnotations(destructiveHint: false)
+ }
+ """)
+ .Use(next => next)
+ .ModifyOptions(o => o.StrictValidation = false)
+ .Create();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(destructiveHint, tool.McpTool.Annotations?.DestructiveHint);
+ }
+
+ [Theory]
+ [InlineData("ImplicitNonIdempotentTool.graphql", false)]
+ [InlineData("ExplicitNonIdempotentTool.graphql", false)]
+ [InlineData("ExplicitIdempotentTool.graphql", true)]
+ public void CreateTool_McpToolAnnotationsIdempotentHintImplementationFirst_SetsCorrectHint(
+ string fileName,
+ bool idempotentHint)
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(idempotentHint, tool.McpTool.Annotations?.IdempotentHint);
+ }
+
+ [Theory]
+ [InlineData("ImplicitNonIdempotentTool.graphql", false)]
+ [InlineData("ExplicitNonIdempotentTool.graphql", false)]
+ [InlineData("ExplicitIdempotentTool.graphql", true)]
+ public void CreateTool_McpToolAnnotationsIdempotentHintCodeFirst_SetsCorrectHint(
+ string fileName,
+ bool idempotentHint)
+ {
+ // arrange
+ var schema =
+ SchemaBuilder
+ .New()
+ .AddMcp()
+ .AddMutationType(
+ descriptor =>
+ {
+ descriptor.Name(OperationTypeNames.Mutation);
+
+ descriptor
+ .Field("implicitNonIdempotentMutation")
+ .Type();
+
+ descriptor
+ .Field("explicitNonIdempotentMutation")
+ .Type()
+ .McpToolAnnotations(idempotentHint: false);
+
+ descriptor
+ .Field("explicitIdempotentMutation")
+ .Type()
+ .McpToolAnnotations(idempotentHint: true);
+ })
+ .Use(next => next)
+ .ModifyOptions(o => o.StrictValidation = false)
+ .Create();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(idempotentHint, tool.McpTool.Annotations?.IdempotentHint);
+ }
+
+ [Theory]
+ [InlineData("ImplicitNonIdempotentTool.graphql", false)]
+ [InlineData("ExplicitNonIdempotentTool.graphql", false)]
+ [InlineData("ExplicitIdempotentTool.graphql", true)]
+ public void CreateTool_McpToolAnnotationsIdempotentHintSchemaFirst_SetsCorrectHint(
+ string fileName,
+ bool idempotentHint)
+ {
+ // arrange
+ var schema =
+ SchemaBuilder
+ .New()
+ .AddMcp()
+ .AddDocumentFromString(
+ """
+ type Mutation {
+ implicitNonIdempotentMutation: Int
+ explicitNonIdempotentMutation: Int
+ @mcpToolAnnotations(idempotentHint: false)
+ explicitIdempotentMutation: Int
+ @mcpToolAnnotations(idempotentHint: true)
+ }
+ """)
+ .Use(next => next)
+ .ModifyOptions(o => o.StrictValidation = false)
+ .Create();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(idempotentHint, tool.McpTool.Annotations?.IdempotentHint);
+ }
+
+ [Theory]
+ [InlineData("ImplicitOpenWorldTool.graphql", true)]
+ [InlineData("ExplicitOpenWorldTool.graphql", true)]
+ [InlineData("ExplicitClosedWorldTool.graphql", false)]
+ [InlineData("ExplicitOpenWorldSubfieldTool.graphql", true)]
+ [InlineData("ImplicitClosedWorldSubfieldTool.graphql", false)]
+ [InlineData("ExplicitClosedWorldSubfieldTool.graphql", false)]
+ public void CreateTool_McpToolAnnotationsOpenWorldHintImplementationFirst_SetsCorrectHint(
+ string fileName,
+ bool openWorldHint)
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(openWorldHint, tool.McpTool.Annotations?.OpenWorldHint);
+ }
+
+ [Theory]
+ [InlineData("ImplicitOpenWorldTool.graphql", true)]
+ [InlineData("ExplicitOpenWorldTool.graphql", true)]
+ [InlineData("ExplicitClosedWorldTool.graphql", false)]
+ [InlineData("ExplicitOpenWorldSubfieldTool.graphql", true)]
+ [InlineData("ImplicitClosedWorldSubfieldTool.graphql", false)]
+ [InlineData("ExplicitClosedWorldSubfieldTool.graphql", false)]
+ public void CreateTool_McpToolAnnotationsOpenWorldHintCodeFirst_SetsCorrectHint(
+ string fileName,
+ bool openWorldHint)
+ {
+ // arrange
+ var schema =
+ SchemaBuilder
+ .New()
+ .AddMcp()
+ .AddQueryType(
+ descriptor =>
+ {
+ descriptor.Name(OperationTypeNames.Query);
+
+ descriptor
+ .Field("implicitOpenWorldQuery")
+ .Type();
+
+ descriptor
+ .Field("explicitOpenWorldQuery")
+ .Type()
+ .McpToolAnnotations(openWorldHint: true);
+
+ descriptor
+ .Field("explicitClosedWorldQuery")
+ .Type()
+ .McpToolAnnotations(openWorldHint: false);
+
+ descriptor
+ .Field("explicitOpenWorldSubfieldQuery")
+ .Type(typeof(TestSchema.ExplicitOpenWorld))
+ .McpToolAnnotations(openWorldHint: false);
+
+ descriptor
+ .Field("implicitClosedWorldSubfieldQuery")
+ .Type(typeof(TestSchema.ImplicitClosedWorld))
+ .McpToolAnnotations(openWorldHint: false);
+
+ descriptor
+ .Field("explicitClosedWorldSubfieldQuery")
+ .Type(typeof(TestSchema.ExplicitClosedWorld))
+ .McpToolAnnotations(openWorldHint: false);
+ })
+ .Use(next => next)
+ .Create();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(openWorldHint, tool.McpTool.Annotations?.OpenWorldHint);
+ }
+
+ [Theory]
+ [InlineData("ImplicitOpenWorldTool.graphql", true)]
+ [InlineData("ExplicitOpenWorldTool.graphql", true)]
+ [InlineData("ExplicitClosedWorldTool.graphql", false)]
+ [InlineData("ExplicitOpenWorldSubfieldTool.graphql", true)]
+ [InlineData("ImplicitClosedWorldSubfieldTool.graphql", false)]
+ [InlineData("ExplicitClosedWorldSubfieldTool.graphql", false)]
+ public void CreateTool_McpToolAnnotationsOpenWorldHintSchemaFirst_SetsCorrectHint(
+ string fileName,
+ bool openWorldHint)
+ {
+ // arrange
+ var schema =
+ SchemaBuilder
+ .New()
+ .AddMcp()
+ .AddDocumentFromString(
+ """
+ type Query {
+ implicitOpenWorldQuery: Int
+ explicitOpenWorldQuery: Int
+ @mcpToolAnnotations(openWorldHint: true)
+ explicitClosedWorldQuery: Int
+ @mcpToolAnnotations(openWorldHint: false)
+ explicitOpenWorldSubfieldQuery: ExplicitOpenWorld
+ @mcpToolAnnotations(openWorldHint: false)
+ implicitClosedWorldSubfieldQuery: ImplicitClosedWorld
+ @mcpToolAnnotations(openWorldHint: false)
+ explicitClosedWorldSubfieldQuery: ExplicitClosedWorld
+ @mcpToolAnnotations(openWorldHint: false)
+ }
+
+ type ExplicitOpenWorld {
+ explicitOpenWorldField: Int @mcpToolAnnotations(openWorldHint: true)
+ }
+
+ type ImplicitClosedWorld {
+ implicitClosedWorldField: Int
+ }
+
+ type ExplicitClosedWorld {
+ explicitClosedWorldField: Int @mcpToolAnnotations(openWorldHint: false)
+ }
+ """)
+ .Use(next => next)
+ .Create();
+ var document = Utf8GraphQLParser.Parse(File.ReadAllText($"__resources__/{fileName}"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+
+ // assert
+ Assert.Equal(openWorldHint, tool.McpTool.Annotations?.OpenWorldHint);
+ }
+
+ [Fact]
+ public void CreateTool_McpToolAnnotationsWithFragment_SetsCorrectHints()
+ {
+ // arrange
+ var schema = CreateSchema();
+ var document =
+ Utf8GraphQLParser.Parse(
+ File.ReadAllText("__resources__/AnnotationsWithFragment.graphql"));
+
+ // act
+ var tool = new GraphQLMcpToolFactory(schema).CreateTool("", document);
+ var mcpTool = tool.McpTool;
+
+ // assert
+ Assert.Equal(true, mcpTool.Annotations?.DestructiveHint);
+ Assert.Equal(false, mcpTool.Annotations?.IdempotentHint);
+ Assert.Equal(true, mcpTool.Annotations?.OpenWorldHint);
+ }
+
+ private static Schema CreateSchema(Action? configure = null)
+ {
+ var schemaBuilder =
+ SchemaBuilder
+ .New()
+ .ModifyOptions(o => o.StripLeadingIFromInterface = true)
+ .AddMcp()
+ .AddQueryType()
+ .AddMutationType()
+ .AddSubscriptionType()
+ .AddInterfaceType()
+ .AddUnionType()
+ .AddObjectType()
+ .AddObjectType();
+
+ configure?.Invoke(schemaBuilder);
+
+ return schemaBuilder.Create();
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Input.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Input.json
new file mode 100644
index 00000000000..8a878a65984
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Input.json
@@ -0,0 +1,328 @@
+{
+ "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"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "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"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "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"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "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",
+ "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",
+ "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",
+ "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
+ }
+ }
+ },
+ "timeSpanDotNet": {
+ "type": "string",
+ "pattern": "^-?(?:(?:\\d{1,8})\\.)?(?:[0-1]?\\d|2[0-3]):(?:[0-5]?\\d):(?:[0-5]?\\d)(?:\\.(?:\\d{1,7}))?$",
+ "description": "TimeSpan with DotNet format",
+ "default": "00:05:00"
+ }
+ },
+ "required": []
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Output.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Output.json
new file mode 100644
index 00000000000..fd00ba452bc
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithComplexVariables_CreatesCorrectSchema_Output.json
@@ -0,0 +1,323 @@
+{
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "withComplexVariables": {
+ "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"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ],
+ "additionalProperties": false
+ },
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ],
+ "additionalProperties": false,
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "object": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ],
+ "additionalProperties": false
+ },
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ],
+ "additionalProperties": false,
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ],
+ "additionalProperties": false
+ },
+ "nullDefault": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "listWithNullDefault": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "objectWithNullDefault": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ],
+ "additionalProperties": false
+ },
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ],
+ "additionalProperties": false,
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ],
+ "additionalProperties": false
+ },
+ "oneOf": {
+ "type": "object",
+ "properties": {
+ "field1": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "field1 description"
+ },
+ "field2": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "field2 description"
+ }
+ },
+ "required": [
+ "field1",
+ "field2"
+ ],
+ "additionalProperties": false
+ },
+ "oneOfList": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "field1": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "field1 description"
+ },
+ "field2": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "field2 description"
+ }
+ },
+ "required": [
+ "field1",
+ "field2"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "objectWithOneOfField": {
+ "type": "object",
+ "properties": {
+ "field": {
+ "type": "object",
+ "properties": {
+ "field1": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "field1 description"
+ },
+ "field2": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "field2 description"
+ }
+ },
+ "required": [
+ "field1",
+ "field2"
+ ],
+ "additionalProperties": false,
+ "description": "field description"
+ }
+ },
+ "required": [
+ "field"
+ ],
+ "additionalProperties": false
+ },
+ "timeSpanDotNet": {
+ "type": "string",
+ "pattern": "^-?(?:(?:\\d{1,8})\\.)?(?:[0-1]?\\d|2[0-3]):(?:[0-5]?\\d):(?:[0-5]?\\d)(?:\\.(?:\\d{1,7}))?$"
+ }
+ },
+ "required": [
+ "list",
+ "object",
+ "nullDefault",
+ "listWithNullDefault",
+ "objectWithNullDefault",
+ "oneOf",
+ "oneOfList",
+ "objectWithOneOfField",
+ "timeSpanDotNet"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "withComplexVariables"
+ ]
+ },
+ "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/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Input.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Input.json
new file mode 100644
index 00000000000..0f205d77d9b
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Input.json
@@ -0,0 +1,203 @@
+{
+ "type": "object",
+ "properties": {
+ "any": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer"
+ ],
+ "description": "Any description",
+ "default": {
+ "key": "value"
+ }
+ },
+ "boolean": {
+ "type": "boolean",
+ "description": "Boolean description",
+ "default": true
+ },
+ "byte": {
+ "type": "integer",
+ "description": "Byte description",
+ "default": 1
+ },
+ "byteArray": {
+ "type": "string",
+ "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$",
+ "description": "ByteArray description",
+ "default": "ZGVmYXVsdA=="
+ },
+ "date": {
+ "type": "string",
+ "format": "date",
+ "description": "Date description",
+ "default": "2000-01-01"
+ },
+ "dateTime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,7})?(?:[Zz]|[+-]\\d{2}:\\d{2})$",
+ "description": "DateTime description",
+ "default": "2000-01-01T12:00:00Z"
+ },
+ "decimal": {
+ "type": "number",
+ "description": "Decimal description",
+ "default": 79228162514264337593543950335
+ },
+ "enum": {
+ "type": "string",
+ "enum": [
+ "VALUE1",
+ "VALUE2"
+ ],
+ "description": "Enum description",
+ "default": "VALUE1"
+ },
+ "float": {
+ "type": "number",
+ "description": "Float description",
+ "default": 1.5
+ },
+ "id": {
+ "type": "string",
+ "description": "ID description",
+ "default": "default"
+ },
+ "int": {
+ "type": "integer",
+ "description": "Int description",
+ "default": 1
+ },
+ "json": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer"
+ ],
+ "description": "JSON description",
+ "default": {
+ "key": "value"
+ }
+ },
+ "list": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List description",
+ "default": [
+ "default"
+ ]
+ },
+ "localDate": {
+ "type": "string",
+ "format": "date",
+ "description": "LocalDate description",
+ "default": "2000-01-01"
+ },
+ "localDateTime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "description": "LocalDateTime description",
+ "default": "2000-01-01T12:00:00"
+ },
+ "localTime": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "LocalTime description",
+ "default": "12:00:00"
+ },
+ "long": {
+ "type": "integer",
+ "description": "Long description",
+ "default": 9223372036854775807
+ },
+ "object": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "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 description",
+ "default": {
+ "field1A": {
+ "field1B": {
+ "field1C": "12:00:00"
+ }
+ }
+ }
+ },
+ "short": {
+ "type": "integer",
+ "description": "Short description",
+ "default": 1
+ },
+ "string": {
+ "type": "string",
+ "description": "String description",
+ "default": "default"
+ },
+ "timeSpan": {
+ "type": "string",
+ "pattern": "^-?P(?:\\d+W|(?=\\d|T(?:\\d|$))(?:\\d+Y)?(?:\\d+M)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?)$",
+ "description": "TimeSpan description",
+ "default": "PT5M"
+ },
+ "unknown": {
+ "type": "string",
+ "description": "Unknown description",
+ "default": "default"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri-reference",
+ "description": "URL description",
+ "default": "https://example.com"
+ },
+ "uuid": {
+ "type": "string",
+ "format": "uuid",
+ "description": "UUID description",
+ "default": "00000000-0000-0000-0000-000000000000"
+ }
+ },
+ "required": []
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Output.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Output.json
new file mode 100644
index 00000000000..4e160088c8f
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithDefaultedVariables_CreatesCorrectSchema_Output.json
@@ -0,0 +1,239 @@
+{
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "withDefaultedVariables": {
+ "type": "object",
+ "properties": {
+ "any": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer"
+ ]
+ },
+ "boolean": {
+ "type": "boolean"
+ },
+ "byte": {
+ "type": "integer"
+ },
+ "byteArray": {
+ "type": "string",
+ "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$"
+ },
+ "date": {
+ "type": "string",
+ "format": "date"
+ },
+ "dateTime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,7})?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
+ },
+ "decimal": {
+ "type": "number"
+ },
+ "enum": {
+ "type": "string",
+ "enum": [
+ "VALUE1",
+ "VALUE2"
+ ]
+ },
+ "float": {
+ "type": "number"
+ },
+ "id": {
+ "type": "string"
+ },
+ "int": {
+ "type": "integer"
+ },
+ "json": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer"
+ ]
+ },
+ "list": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "localDate": {
+ "type": "string",
+ "format": "date"
+ },
+ "localDateTime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$"
+ },
+ "localTime": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$"
+ },
+ "long": {
+ "type": "integer"
+ },
+ "object": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ],
+ "additionalProperties": false,
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ],
+ "additionalProperties": false,
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ],
+ "additionalProperties": false
+ },
+ "short": {
+ "type": "integer"
+ },
+ "string": {
+ "type": "string"
+ },
+ "timeSpan": {
+ "type": "string",
+ "pattern": "^-?P(?:\\d+W|(?=\\d|T(?:\\d|$))(?:\\d+Y)?(?:\\d+M)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?)$"
+ },
+ "unknown": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "uuid": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "any",
+ "boolean",
+ "byte",
+ "byteArray",
+ "date",
+ "dateTime",
+ "decimal",
+ "enum",
+ "float",
+ "id",
+ "int",
+ "json",
+ "list",
+ "localDate",
+ "localDateTime",
+ "localTime",
+ "long",
+ "object",
+ "short",
+ "string",
+ "timeSpan",
+ "unknown",
+ "url",
+ "uuid"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "withDefaultedVariables"
+ ]
+ },
+ "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/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithInterfaceType_CreatesCorrectOutputSchema.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithInterfaceType_CreatesCorrectOutputSchema.json
new file mode 100644
index 00000000000..456be7196d4
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithInterfaceType_CreatesCorrectOutputSchema.json
@@ -0,0 +1,95 @@
+{
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "withInterfaceType": {
+ "type": "object",
+ "properties": {
+ "__typename": {
+ "type": "string",
+ "description": "The name of the current Object type at runtime."
+ },
+ "name": {
+ "type": "string"
+ },
+ "isBarking": {
+ "type": "boolean"
+ },
+ "isPurring": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "__typename",
+ "name",
+ "isBarking",
+ "isPurring"
+ ],
+ "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/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Input.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Input.json
new file mode 100644
index 00000000000..a604c727ecd
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Input.json
@@ -0,0 +1,189 @@
+{
+ "type": "object",
+ "properties": {
+ "any": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer"
+ ],
+ "description": "Any description"
+ },
+ "boolean": {
+ "type": "boolean",
+ "description": "Boolean description"
+ },
+ "byte": {
+ "type": "integer",
+ "description": "Byte description"
+ },
+ "byteArray": {
+ "type": "string",
+ "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$",
+ "description": "ByteArray description"
+ },
+ "date": {
+ "type": "string",
+ "format": "date",
+ "description": "Date description"
+ },
+ "dateTime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,7})?(?:[Zz]|[+-]\\d{2}:\\d{2})$",
+ "description": "DateTime description"
+ },
+ "decimal": {
+ "type": "number",
+ "description": "Decimal description"
+ },
+ "enum": {
+ "type": "string",
+ "enum": [
+ "VALUE1",
+ "VALUE2"
+ ],
+ "description": "Enum description"
+ },
+ "float": {
+ "type": "number",
+ "description": "Float description"
+ },
+ "id": {
+ "type": "string",
+ "description": "ID description"
+ },
+ "int": {
+ "type": "integer",
+ "description": "Int description"
+ },
+ "json": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer"
+ ],
+ "description": "JSON description"
+ },
+ "list": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List description"
+ },
+ "localDate": {
+ "type": "string",
+ "format": "date",
+ "description": "LocalDate description"
+ },
+ "localDateTime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "description": "LocalDateTime description"
+ },
+ "localTime": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "LocalTime description"
+ },
+ "long": {
+ "type": "integer",
+ "description": "Long description"
+ },
+ "object": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ],
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ],
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ],
+ "description": "Object description"
+ },
+ "short": {
+ "type": "integer",
+ "description": "Short description"
+ },
+ "string": {
+ "type": "string",
+ "description": "String description"
+ },
+ "timeSpan": {
+ "type": "string",
+ "pattern": "^-?P(?:\\d+W|(?=\\d|T(?:\\d|$))(?:\\d+Y)?(?:\\d+M)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?)$",
+ "description": "TimeSpan description"
+ },
+ "unknown": {
+ "type": "string",
+ "description": "Unknown description"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri-reference",
+ "description": "URL description"
+ },
+ "uuid": {
+ "type": "string",
+ "format": "uuid",
+ "description": "UUID description"
+ }
+ },
+ "required": [
+ "any",
+ "boolean",
+ "byte",
+ "byteArray",
+ "date",
+ "dateTime",
+ "decimal",
+ "enum",
+ "float",
+ "id",
+ "int",
+ "json",
+ "list",
+ "localDate",
+ "localDateTime",
+ "localTime",
+ "long",
+ "object",
+ "short",
+ "string",
+ "timeSpan",
+ "unknown",
+ "url",
+ "uuid"
+ ]
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Output.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Output.json
new file mode 100644
index 00000000000..cd2aaab579e
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNonNullableVariables_CreatesCorrectSchema_Output.json
@@ -0,0 +1,239 @@
+{
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "withNonNullableVariables": {
+ "type": "object",
+ "properties": {
+ "any": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer"
+ ]
+ },
+ "boolean": {
+ "type": "boolean"
+ },
+ "byte": {
+ "type": "integer"
+ },
+ "byteArray": {
+ "type": "string",
+ "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$"
+ },
+ "date": {
+ "type": "string",
+ "format": "date"
+ },
+ "dateTime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,7})?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
+ },
+ "decimal": {
+ "type": "number"
+ },
+ "enum": {
+ "type": "string",
+ "enum": [
+ "VALUE1",
+ "VALUE2"
+ ]
+ },
+ "float": {
+ "type": "number"
+ },
+ "id": {
+ "type": "string"
+ },
+ "int": {
+ "type": "integer"
+ },
+ "json": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer"
+ ]
+ },
+ "list": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "localDate": {
+ "type": "string",
+ "format": "date"
+ },
+ "localDateTime": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$"
+ },
+ "localTime": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$"
+ },
+ "long": {
+ "type": "integer"
+ },
+ "object": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ],
+ "additionalProperties": false,
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ],
+ "additionalProperties": false,
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ],
+ "additionalProperties": false
+ },
+ "short": {
+ "type": "integer"
+ },
+ "string": {
+ "type": "string"
+ },
+ "timeSpan": {
+ "type": "string",
+ "pattern": "^-?P(?:\\d+W|(?=\\d|T(?:\\d|$))(?:\\d+Y)?(?:\\d+M)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?)$"
+ },
+ "unknown": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri-reference"
+ },
+ "uuid": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "any",
+ "boolean",
+ "byte",
+ "byteArray",
+ "date",
+ "dateTime",
+ "decimal",
+ "enum",
+ "float",
+ "id",
+ "int",
+ "json",
+ "list",
+ "localDate",
+ "localDateTime",
+ "localTime",
+ "long",
+ "object",
+ "short",
+ "string",
+ "timeSpan",
+ "unknown",
+ "url",
+ "uuid"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "withNonNullableVariables"
+ ]
+ },
+ "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/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Input.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Input.json
new file mode 100644
index 00000000000..403fe020c8f
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Input.json
@@ -0,0 +1,239 @@
+{
+ "type": "object",
+ "properties": {
+ "any": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer",
+ "null"
+ ],
+ "description": "Any description"
+ },
+ "boolean": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "Boolean description"
+ },
+ "byte": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Byte description"
+ },
+ "byteArray": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$",
+ "description": "ByteArray description"
+ },
+ "date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date",
+ "description": "Date description"
+ },
+ "dateTime": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,7})?(?:[Zz]|[+-]\\d{2}:\\d{2})$",
+ "description": "DateTime description"
+ },
+ "decimal": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "description": "Decimal description"
+ },
+ "enum": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "VALUE1",
+ "VALUE2",
+ null
+ ],
+ "description": "Enum description"
+ },
+ "float": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "description": "Float description"
+ },
+ "id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "ID description"
+ },
+ "int": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Int description"
+ },
+ "json": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer",
+ "null"
+ ],
+ "description": "JSON description"
+ },
+ "list": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": "List description"
+ },
+ "localDate": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date",
+ "description": "LocalDate description"
+ },
+ "localDateTime": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$",
+ "description": "LocalDateTime description"
+ },
+ "localTime": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "LocalTime description"
+ },
+ "long": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Long description"
+ },
+ "object": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field1A": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field1B": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [],
+ "description": "field1B description"
+ }
+ },
+ "required": [],
+ "description": "field1A description"
+ }
+ },
+ "required": [],
+ "description": "Object description"
+ },
+ "short": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Short description"
+ },
+ "string": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "String description"
+ },
+ "timeSpan": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^-?P(?:\\d+W|(?=\\d|T(?:\\d|$))(?:\\d+Y)?(?:\\d+M)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?)$",
+ "description": "TimeSpan description"
+ },
+ "unknown": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Unknown description"
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uri-reference",
+ "description": "URL description"
+ },
+ "uuid": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uuid",
+ "description": "UUID description"
+ }
+ },
+ "required": []
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Output.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Output.json
new file mode 100644
index 00000000000..fe9067d8276
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithNullableVariables_CreatesCorrectSchema_Output.json
@@ -0,0 +1,320 @@
+{
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "withNullableVariables": {
+ "type": "object",
+ "properties": {
+ "any": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer",
+ "null"
+ ]
+ },
+ "boolean": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ },
+ "byte": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "byteArray": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=)?$"
+ },
+ "date": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date"
+ },
+ "dateTime": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}[Tt]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,7})?(?:[Zz]|[+-]\\d{2}:\\d{2})$"
+ },
+ "decimal": {
+ "type": [
+ "number",
+ "null"
+ ]
+ },
+ "enum": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "enum": [
+ "VALUE1",
+ "VALUE2",
+ null
+ ]
+ },
+ "float": {
+ "type": [
+ "number",
+ "null"
+ ]
+ },
+ "id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "int": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "json": {
+ "type": [
+ "object",
+ "array",
+ "boolean",
+ "string",
+ "number",
+ "integer",
+ "null"
+ ]
+ },
+ "list": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "localDate": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date"
+ },
+ "localDateTime": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "date-time",
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$"
+ },
+ "localTime": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$"
+ },
+ "long": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "object": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field1A": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field1B": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "field1C": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ],
+ "additionalProperties": false,
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ],
+ "additionalProperties": false,
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ],
+ "additionalProperties": false
+ },
+ "short": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "string": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timeSpan": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^-?P(?:\\d+W|(?=\\d|T(?:\\d|$))(?:\\d+Y)?(?:\\d+M)?(?:\\d+D)?(?:T(?:\\d+H)?(?:\\d+M)?(?:\\d+(?:\\.\\d+)?S)?)?)$"
+ },
+ "unknown": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uri-reference"
+ },
+ "uuid": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "format": "uuid"
+ }
+ },
+ "required": [
+ "any",
+ "boolean",
+ "byte",
+ "byteArray",
+ "date",
+ "dateTime",
+ "decimal",
+ "enum",
+ "float",
+ "id",
+ "int",
+ "json",
+ "list",
+ "localDate",
+ "localDateTime",
+ "localTime",
+ "long",
+ "object",
+ "short",
+ "string",
+ "timeSpan",
+ "unknown",
+ "url",
+ "uuid"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "withNullableVariables"
+ ]
+ },
+ "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/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithSkipAndInclude_CreatesCorrectOutputSchema.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithSkipAndInclude_CreatesCorrectOutputSchema.json
new file mode 100644
index 00000000000..dafcd671e21
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithSkipAndInclude_CreatesCorrectOutputSchema.json
@@ -0,0 +1,144 @@
+{
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "withDefaultedVariables": {
+ "type": "object",
+ "properties": {
+ "notSkipped": {
+ "type": "integer"
+ },
+ "possiblySkipped": {
+ "type": "integer"
+ },
+ "included": {
+ "type": "integer"
+ },
+ "possiblyIncluded": {
+ "type": "integer"
+ },
+ "notSkippedAndIncluded": {
+ "type": "integer"
+ },
+ "notSkippedAndPossiblyIncluded": {
+ "type": "integer"
+ },
+ "possiblySkippedAndIncluded": {
+ "type": "integer"
+ },
+ "possiblySkippedAndPossiblyIncluded": {
+ "type": "integer"
+ },
+ "objectFieldPossiblySkipped": {
+ "type": "object",
+ "properties": {
+ "field1A": {
+ "type": "object",
+ "properties": {
+ "field1B": {
+ "type": "object",
+ "properties": {
+ "field1C": {
+ "type": "string",
+ "format": "time",
+ "pattern": "^\\d{2}:\\d{2}:\\d{2}$",
+ "description": "field1C description"
+ }
+ },
+ "required": [
+ "field1C"
+ ],
+ "additionalProperties": false,
+ "description": "field1B description"
+ }
+ },
+ "required": [
+ "field1B"
+ ],
+ "additionalProperties": false,
+ "description": "field1A description"
+ }
+ },
+ "required": [
+ "field1A"
+ ],
+ "additionalProperties": false
+ },
+ "inlineFragmentFieldPossiblySkipped": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "notSkipped",
+ "included",
+ "notSkippedAndIncluded",
+ "inlineFragmentFieldPossiblySkipped"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "withDefaultedVariables"
+ ]
+ },
+ "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/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithUnionType_CreatesCorrectOutputSchema.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithUnionType_CreatesCorrectOutputSchema.json
new file mode 100644
index 00000000000..912c920edce
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithUnionType_CreatesCorrectOutputSchema.json
@@ -0,0 +1,91 @@
+{
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "properties": {
+ "withUnionType": {
+ "type": "object",
+ "properties": {
+ "__typename": {
+ "type": "string",
+ "description": "The name of the current Object type at runtime."
+ },
+ "isBarking": {
+ "type": "boolean"
+ },
+ "isPurring": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "__typename",
+ "isBarking",
+ "isPurring"
+ ],
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "required": [
+ "withUnionType"
+ ]
+ },
+ "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/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithVariableMinMaxValues_CreatesCorrectInputSchema.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithVariableMinMaxValues_CreatesCorrectInputSchema.json
new file mode 100644
index 00000000000..e3992362eb2
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Factories/__snapshots__/GraphQLMcpToolFactoryTests.CreateTool_WithVariableMinMaxValues_CreatesCorrectInputSchema.json
@@ -0,0 +1,43 @@
+{
+ "type": "object",
+ "properties": {
+ "byte": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "decimal": {
+ "type": "number",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "float": {
+ "type": "number",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "int": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "long": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ },
+ "short": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 10
+ }
+ },
+ "required": [
+ "byte",
+ "decimal",
+ "float",
+ "int",
+ "long",
+ "short"
+ ]
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Handlers/CallToolHandlerTests.cs b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Handlers/CallToolHandlerTests.cs
new file mode 100644
index 00000000000..1cf2df36760
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Handlers/CallToolHandlerTests.cs
@@ -0,0 +1,59 @@
+using HotChocolate.Execution;
+using HotChocolate.Language;
+using HotChocolate.ModelContextProtocol.Extensions;
+using HotChocolate.ModelContextProtocol.Storage;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using Moq;
+
+namespace HotChocolate.ModelContextProtocol.Handlers;
+
+public sealed class CallToolHandlerTests
+{
+ [Fact]
+ public async Task HandleAsync_MissingTool_ReturnsCallToolResultWithError()
+ {
+ // arrange
+ var context = await CreateRequestContextAsync("unknown");
+
+ // act
+ var result = await CallToolHandler.HandleAsync(context, CancellationToken.None);
+
+ // assert
+ Assert.True(result.IsError);
+ var textContentBlock = Assert.IsType(result.Content[0]);
+ Assert.Equal("The tool 'unknown' was not found.", textContentBlock.Text);
+ }
+
+ private static async Task> CreateRequestContextAsync(
+ string toolName)
+ {
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ await storage.SaveToolDocumentAsync(
+ Utf8GraphQLParser.Parse(
+ await File.ReadAllTextAsync("__resources__/GetWithNullableVariables.graphql")));
+ var services = new ServiceCollection().AddSingleton(storage);
+ services
+ .AddGraphQL()
+ .AddMcp()
+ .AddQueryType()
+ .AddInterfaceType()
+ .AddUnionType()
+ .AddObjectType()
+ .AddObjectType();
+ var serviceProvider = services.BuildServiceProvider();
+ var executorProvider = serviceProvider.GetRequiredService();
+ var executor = await executorProvider.GetExecutorAsync();
+ Mock mockServer = new();
+ mockServer.SetupGet(s => s.Services).Returns(executor.Schema.Services);
+
+ return new RequestContext(mockServer.Object)
+ {
+ Params = new CallToolRequestParams
+ {
+ Name = toolName
+ }
+ };
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/HotChocolate.ModelContextProtocol.Tests.csproj b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/HotChocolate.ModelContextProtocol.Tests.csproj
new file mode 100644
index 00000000000..738ffb1ef84
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/HotChocolate.ModelContextProtocol.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ HotChocolate.ModelContextProtocol.Tests
+ HotChocolate.ModelContextProtocol
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/IntegrationTests.cs b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/IntegrationTests.cs
new file mode 100644
index 00000000000..daaed8be050
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/IntegrationTests.cs
@@ -0,0 +1,293 @@
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using CookieCrumble;
+using HotChocolate.Execution.Configuration;
+using HotChocolate.Language;
+using HotChocolate.ModelContextProtocol.Extensions;
+using HotChocolate.ModelContextProtocol.Storage;
+using HotChocolate.Types;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using ModelContextProtocol.Client;
+
+namespace HotChocolate.ModelContextProtocol;
+
+public sealed class IntegrationTests
+{
+ [Fact]
+ public async Task ListTools_Valid_ReturnsTools()
+ {
+ // arrange
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ await storage.SaveToolDocumentAsync(
+ Utf8GraphQLParser.Parse(
+ await File.ReadAllTextAsync("__resources__/GetWithNullableVariables.graphql")));
+ await storage.SaveToolDocumentAsync(
+ Utf8GraphQLParser.Parse(
+ await File.ReadAllTextAsync("__resources__/GetWithNonNullableVariables.graphql")));
+ var server = CreateTestServer(b => b.AddMcpOperationDocumentStorage(storage));
+ var mcpClient = await CreateMcpClient(server.CreateClient());
+
+ // act
+ var tools = await mcpClient.ListToolsAsync();
+
+ // assert
+ JsonSerializer.Serialize(
+ tools.Select(
+ t =>
+ new
+ {
+ t.Name,
+ t.Title,
+ t.Description
+ }),
+ s_jsonSerializerOptions)
+ .ReplaceLineEndings("\n")
+ .MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public async Task CallTool_GetWithNullableVariables_ReturnsExpectedResult()
+ {
+ // arrange
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ await storage.SaveToolDocumentAsync(
+ Utf8GraphQLParser.Parse(
+ await File.ReadAllTextAsync("__resources__/GetWithNullableVariables.graphql")));
+ var server = CreateTestServer(b => b.AddMcpOperationDocumentStorage(storage));
+ var mcpClient = await CreateMcpClient(server.CreateClient());
+
+ // act
+ var result = await mcpClient.CallToolAsync(
+ "get_with_nullable_variables",
+ new Dictionary
+ {
+ { "any", null },
+ { "boolean", null },
+ { "byte", null },
+ { "byteArray", null },
+ { "date", null },
+ { "dateTime", null },
+ { "decimal", null },
+ { "enum", null },
+ { "float", null },
+ { "id", null },
+ { "int", null },
+ { "json", null },
+ { "list", null },
+ { "localDate", null },
+ { "localDateTime", null },
+ { "localTime", null },
+ { "long", null },
+ { "object", null },
+ { "short", null },
+ { "string", null },
+ { "timeSpan", null },
+ { "unknown", null },
+ { "url", null },
+ { "uuid", null }
+ },
+ serializerOptions: JsonSerializerOptions.Default);
+
+ // assert
+ result.StructuredContent!
+ .ToString()
+ .ReplaceLineEndings("\n")
+ .MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public async Task CallTool_GetWithNonNullableVariables_ReturnsExpectedResult()
+ {
+ // arrange
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ await storage.SaveToolDocumentAsync(
+ Utf8GraphQLParser.Parse(
+ await File.ReadAllTextAsync("__resources__/GetWithNonNullableVariables.graphql")));
+ var server = CreateTestServer(b => b.AddMcpOperationDocumentStorage(storage));
+ var mcpClient = await CreateMcpClient(server.CreateClient());
+
+ // act
+ var result = await mcpClient.CallToolAsync(
+ "get_with_non_nullable_variables",
+ // JSON values.
+ new Dictionary
+ {
+ { "any", new { key = "value" } },
+ { "boolean", true },
+ { "byte", 1 },
+ { "byteArray", "dGVzdA==" },
+ { "date", "2000-01-01" },
+ { "dateTime", "2000-01-01T12:00:00Z" },
+ { "decimal", 79228162514264337593543950335m },
+ { "enum", "VALUE1" },
+ { "float", 1.5 },
+ { "id", "test" },
+ { "int", 1 },
+ { "json", new { key = "value" } },
+ { "list", s_list },
+ { "localDate", "2000-01-01" },
+ { "localDateTime", "2000-01-01T12:00:00" },
+ { "localTime", "12:00:00" },
+ { "long", 9223372036854775807 },
+ { "object", new { field1A = new { field1B = new { field1C = "12:00:00" } } } },
+ { "short", 1 },
+ { "string", "test" },
+ { "timeSpan", "PT5M" },
+ { "unknown", "test" },
+ { "url", "https://example.com" },
+ { "uuid", "00000000-0000-0000-0000-000000000000" }
+ },
+ serializerOptions: JsonSerializerOptions.Default);
+
+ // assert
+ result.StructuredContent!
+ .ToString()
+ .ReplaceLineEndings("\n")
+ .MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public async Task CallTool_GetWithDefaultedVariables_ReturnsExpectedResult()
+ {
+ // arrange
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ await storage.SaveToolDocumentAsync(
+ Utf8GraphQLParser.Parse(
+ await File.ReadAllTextAsync("__resources__/GetWithDefaultedVariables.graphql")));
+ var server = CreateTestServer(b => b.AddMcpOperationDocumentStorage(storage));
+ var mcpClient = await CreateMcpClient(server.CreateClient());
+
+ // act
+ var result = await mcpClient.CallToolAsync("get_with_defaulted_variables");
+
+ // assert
+ result.StructuredContent!
+ .ToString()
+ .ReplaceLineEndings("\n")
+ .MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public async Task CallTool_GetWithComplexVariables_ReturnsExpectedResult()
+ {
+ // arrange
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ await storage.SaveToolDocumentAsync(
+ Utf8GraphQLParser.Parse(
+ await File.ReadAllTextAsync("__resources__/GetWithComplexVariables.graphql")));
+ var server =
+ CreateTestServer(
+ b => b
+ .AddMcpOperationDocumentStorage(storage)
+ .AddType(new TimeSpanType(TimeSpanFormat.DotNet)));
+ var mcpClient = await CreateMcpClient(server.CreateClient());
+
+ // act
+ var result = await mcpClient.CallToolAsync(
+ "get_with_complex_variables",
+ // JSON values.
+ new Dictionary
+ {
+ {
+ "list",
+ new[]
+ {
+ new { field1A = new { field1B = new[] { new { field1C = "12:00:00" } } } }
+ }
+ },
+ {
+ "object",
+ new { field1A = new { field1B = new[] { new { field1C = "12:00:00" } } } }
+ },
+ { "nullDefault", null },
+ { "listWithNullDefault", new string?[] { null } },
+ {
+ "objectWithNullDefault",
+ new { field1A = new { field1B = new[] { new { field1C = (string?)null } } } }
+ },
+ { "oneOf", new { field1 = 1 } },
+ { "oneOfList", new object[] { new { field1 = 1 }, new { field2 = "test" } } },
+ { "objectWithOneOfField", new { field = new { field1 = 1 } } },
+ { "timeSpanDotNet", "00:05:00" }
+ },
+ serializerOptions: JsonSerializerOptions.Default);
+
+ // assert
+ result.StructuredContent!
+ .ToString()
+ .ReplaceLineEndings("\n")
+ .MatchSnapshot(extension: ".json");
+ }
+
+ [Fact]
+ public async Task CallTool_GetWithErrors_ReturnsExpectedResult()
+ {
+ // arrange
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ await storage.SaveToolDocumentAsync(
+ Utf8GraphQLParser.Parse("query GetWithErrors { withErrors }"));
+ var server = CreateTestServer(b => b.AddMcpOperationDocumentStorage(storage));
+ var mcpClient = await CreateMcpClient(server.CreateClient());
+
+ // act
+ var result = await mcpClient.CallToolAsync("get_with_errors");
+
+ // assert
+ result.StructuredContent!
+ .ToString()
+ .ReplaceLineEndings("\n")
+ .MatchSnapshot(extension: ".json");
+ }
+
+ private static readonly JsonSerializerOptions s_jsonSerializerOptions =
+ new()
+ {
+ Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+ WriteIndented = true
+ };
+
+ private static readonly string[] s_list = ["test"];
+
+ private static TestServer CreateTestServer(
+ Action? configureRequestExecutor = null)
+ {
+ var builder = new WebHostBuilder()
+ .ConfigureServices(
+ services =>
+ {
+ var executor =
+ services
+ .AddRouting()
+ .AddGraphQL()
+ .AddMcp()
+ .AddQueryType()
+ .AddInterfaceType()
+ .AddUnionType()
+ .AddObjectType()
+ .AddObjectType();
+
+ configureRequestExecutor?.Invoke(executor);
+ })
+ .Configure(
+ app => app
+ .UseRouting()
+ .UseEndpoints(endpoints => endpoints.MapGraphQLMcp()));
+
+ return new TestServer(builder);
+ }
+
+ private static async Task CreateMcpClient(HttpClient httpClient)
+ {
+ return
+ await McpClientFactory.CreateAsync(
+ new SseClientTransport(
+ new SseClientTransportOptions
+ {
+ Endpoint = new Uri(httpClient.BaseAddress!, "/graphql/mcp")
+ },
+ httpClient));
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Storage/InMemoryMcpOperationDocumentStorageTests.cs b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Storage/InMemoryMcpOperationDocumentStorageTests.cs
new file mode 100644
index 00000000000..bc9afbc2530
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/Storage/InMemoryMcpOperationDocumentStorageTests.cs
@@ -0,0 +1,84 @@
+using HotChocolate.Language;
+
+namespace HotChocolate.ModelContextProtocol.Storage;
+
+public sealed class InMemoryMcpOperationDocumentStorageTests
+{
+ [Fact]
+ public async Task SaveToolDocumentAsync_DocumentWithNoOperations_ThrowsException()
+ {
+ // arrange & act
+ static async Task Action()
+ {
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ var document = Utf8GraphQLParser.Parse("fragment Fragment on Type { field }");
+
+ await storage.SaveToolDocumentAsync(document);
+ }
+
+ // assert
+ Assert.Equal(
+ "A tool document must contain a single operation definition.",
+ (await Assert.ThrowsAsync(Action)).Message);
+ }
+
+ [Fact]
+ public async Task SaveToolDocumentAsync_DocumentWithMultipleOperations_ThrowsException()
+ {
+ // arrange & act
+ static async Task Action()
+ {
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ var document = Utf8GraphQLParser.Parse(
+ """
+ query Operation1 { query { field } }
+ query Operation2 { query { field } }
+ """);
+
+ await storage.SaveToolDocumentAsync(document);
+ }
+
+ // assert
+ Assert.Equal(
+ "A tool document must contain a single operation definition.",
+ (await Assert.ThrowsAsync(Action)).Message);
+ }
+
+ [Fact]
+ public async Task SaveToolDocumentAsync_DocumentWithUnnamedOperation_ThrowsException()
+ {
+ // arrange & act
+ static async Task Action()
+ {
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ var document = Utf8GraphQLParser.Parse("query { query { field } }");
+
+ await storage.SaveToolDocumentAsync(document);
+ }
+
+ // assert
+ Assert.Equal(
+ "A tool document operation must be named.",
+ (await Assert.ThrowsAsync(Action)).Message);
+ }
+
+ [Fact]
+ public async Task SaveToolDocumentAsync_DocumentWithNonUniqueOperationName_ThrowsException()
+ {
+ // arrange & act
+ static async Task Action()
+ {
+ var storage = new InMemoryMcpOperationDocumentStorage();
+ var document1 = Utf8GraphQLParser.Parse("query Operation1 { query1 { field } }");
+ var document2 = Utf8GraphQLParser.Parse("query Operation1 { query2 { field } }");
+
+ await storage.SaveToolDocumentAsync(document1);
+ await storage.SaveToolDocumentAsync(document2);
+ }
+
+ // assert
+ Assert.Equal(
+ "A tool document operation with the same name already exists.",
+ (await Assert.ThrowsAsync(Action)).Message);
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/TestSchema.cs b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/TestSchema.cs
new file mode 100644
index 00000000000..a5754584277
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/TestSchema.cs
@@ -0,0 +1,459 @@
+// ReSharper disable NotAccessedPositionalProperty.Global
+// ReSharper disable UnusedMember.Global
+using System.Text.Json;
+using HotChocolate.Language;
+using HotChocolate.ModelContextProtocol.Attributes;
+using HotChocolate.Types;
+
+namespace HotChocolate.ModelContextProtocol;
+
+public sealed class TestSchema
+{
+ public sealed class Query
+ {
+ public Book[] GetBooks() => [new("Title")];
+
+ public ResultNullable GetWithNullableVariables(
+ [GraphQLType] object? any,
+ bool? boolean,
+ byte? @byte,
+ [GraphQLType] byte[]? byteArray,
+ [GraphQLType] DateOnly? date,
+ DateTimeOffset? dateTime,
+ decimal? @decimal,
+ TestEnum? @enum,
+ float? @float,
+ [GraphQLType] string? id,
+ int? @int,
+ JsonElement? json,
+ string?[]? list,
+ DateOnly? localDate,
+ [GraphQLType] DateTime? localDateTime,
+ TimeOnly? localTime,
+ long? @long,
+ Object1Nullable? @object,
+ short? @short,
+ string? @string,
+ TimeSpan? timeSpan,
+ [GraphQLType] string? unknown,
+ Uri? url,
+ Guid? uuid)
+ =>
+ new(
+ any,
+ boolean,
+ @byte,
+ byteArray,
+ date,
+ dateTime,
+ @decimal,
+ @enum,
+ @float,
+ id,
+ @int,
+ json,
+ list,
+ localDate,
+ localDateTime,
+ localTime,
+ @long,
+ @object,
+ @short,
+ @string,
+ timeSpan,
+ unknown,
+ url,
+ uuid);
+
+ public ResultNonNullable GetWithNonNullableVariables(
+ [GraphQLType>] object any,
+ bool boolean,
+ byte @byte,
+ [GraphQLType>] byte[] byteArray,
+ [GraphQLType>] DateOnly date,
+ DateTimeOffset dateTime,
+ decimal @decimal,
+ TestEnum @enum,
+ float @float,
+ [GraphQLType>] string id,
+ int @int,
+ JsonElement json,
+ string[] list,
+ DateOnly localDate,
+ [GraphQLType>] DateTime localDateTime,
+ TimeOnly localTime,
+ long @long,
+ Object1NonNullable @object,
+ short @short,
+ string @string,
+ TimeSpan timeSpan,
+ [GraphQLType>] string unknown,
+ Uri url,
+ Guid uuid)
+ =>
+ new(
+ any,
+ boolean,
+ @byte,
+ byteArray,
+ date,
+ dateTime,
+ @decimal,
+ @enum,
+ @float,
+ id,
+ @int,
+ json,
+ list,
+ localDate,
+ localDateTime,
+ localTime,
+ @long,
+ @object,
+ @short,
+ @string,
+ timeSpan,
+ unknown,
+ url,
+ uuid);
+
+ public ResultDefaulted GetWithDefaultedVariables(
+ [GraphQLType>] object any,
+ bool boolean,
+ byte @byte,
+ [GraphQLType>] byte[] byteArray,
+ [GraphQLType>] DateOnly date,
+ DateTimeOffset dateTime,
+ decimal @decimal,
+ TestEnum @enum,
+ float @float,
+ [GraphQLType>] string id,
+ int @int,
+ JsonElement json,
+ string[] list,
+ DateOnly localDate,
+ [GraphQLType>] DateTime localDateTime,
+ TimeOnly localTime,
+ long @long,
+ Object1Defaulted @object,
+ short @short,
+ string @string,
+ TimeSpan timeSpan,
+ [GraphQLType>] string unknown,
+ Uri url,
+ Guid uuid)
+ =>
+ new(
+ any,
+ boolean,
+ @byte,
+ byteArray,
+ date,
+ dateTime,
+ @decimal,
+ @enum,
+ @float,
+ id,
+ @int,
+ json,
+ list,
+ localDate,
+ localDateTime,
+ localTime,
+ @long,
+ @object,
+ @short,
+ @string,
+ timeSpan,
+ unknown,
+ url,
+ uuid);
+
+ public ResultComplex GetWithComplexVariables(
+ Object1Complex[] list,
+ Object1Complex @object,
+ string? nullDefault,
+ string?[]? listWithNullDefault,
+ Object1Complex? objectWithNullDefault,
+ OneOf oneOf,
+ OneOf[] oneOfList,
+ ObjectWithOneOfField objectWithOneOfField,
+ TimeSpan timeSpanDotNet)
+ =>
+ new(
+ list,
+ @object,
+ nullDefault,
+ listWithNullDefault,
+ objectWithNullDefault,
+ oneOf,
+ oneOfList,
+ objectWithOneOfField,
+ timeSpanDotNet);
+
+ public int GetWithVariableMinMaxValues() => 1;
+
+ public IPet GetWithInterfaceType() => new Cat(Name: "Whiskers", IsPurring: true);
+
+ [GraphQLType("PetUnion!")]
+ public IPet GetWithUnionType() => new Cat(Name: "Whiskers", IsPurring: true);
+
+ // Implicitly open-world by default, unless annotated otherwise.
+ public int ImplicitOpenWorldQuery() => 1;
+
+ [McpToolAnnotations(OpenWorldHint = true)]
+ public int ExplicitOpenWorldQuery() => 1;
+
+ [McpToolAnnotations(OpenWorldHint = false)]
+ public int ExplicitClosedWorldQuery() => 1;
+
+ // The query field is closed-world, but the subfield is (explicitly) open-world.
+ [McpToolAnnotations(OpenWorldHint = false)]
+ public ExplicitOpenWorld ExplicitOpenWorldSubfieldQuery() => new();
+
+ // The query field is closed-world, and the subfield is also (implicitly) closed-world.
+ [McpToolAnnotations(OpenWorldHint = false)]
+ public ImplicitClosedWorld ImplicitClosedWorldSubfieldQuery() => new();
+
+ // The query field is closed-world, and the subfield is also (explicitly) closed-world.
+ [McpToolAnnotations(OpenWorldHint = false)]
+ public ExplicitClosedWorld ExplicitClosedWorldSubfieldQuery() => new();
+
+ public int GetWithErrors()
+ {
+ throw new GraphQLException(
+ ErrorBuilder
+ .New()
+ .SetMessage("Error 1")
+ .SetCode("Code 1")
+ .SetException(new Exception("Exception 1"))
+ .Build(),
+ ErrorBuilder
+ .New()
+ .SetMessage("Error 2")
+ .SetCode("Code 2")
+ .SetException(new Exception("Exception 2"))
+ .Build());
+ }
+ }
+
+ public sealed class Mutation
+ {
+ public Book AddBook() => new("Title");
+
+ // Destructive by default, unless annotated otherwise.
+ public int ImplicitDestructiveMutation() => 1;
+
+ [McpToolAnnotations(DestructiveHint = true)]
+ public int ExplicitDestructiveMutation() => 1;
+
+ [McpToolAnnotations(DestructiveHint = false)]
+ public int ExplicitNonDestructiveMutation() => 1;
+
+ // Non-idempotent by default, unless annotated otherwise.
+ public int ImplicitNonIdempotentMutation() => 1;
+
+ [McpToolAnnotations(IdempotentHint = false)]
+ public int ExplicitNonIdempotentMutation() => 1;
+
+ [McpToolAnnotations(IdempotentHint = true)]
+ public int ExplicitIdempotentMutation() => 1;
+
+ [McpToolAnnotations(OpenWorldHint = true)]
+ public int ExplicitOpenWorldMutation() => 1;
+
+ [McpToolAnnotations(OpenWorldHint = false)]
+ public int ExplicitClosedWorldMutation() => 1;
+ }
+
+ public sealed class Subscription
+ {
+ public Book BookAdded() => new("Title");
+ }
+
+ public sealed record Book(string Title);
+
+ public enum TestEnum
+ {
+ Value1,
+ Value2
+ }
+
+ public sealed record Object1Nullable(
+ [property: GraphQLDescription("field1A description")] Object2Nullable? Field1A);
+ public sealed record Object2Nullable(
+ [property: GraphQLDescription("field1B description")] Object3Nullable? Field1B);
+ public sealed record Object3Nullable(
+ [property: GraphQLDescription("field1C description")] TimeOnly? Field1C);
+
+ public sealed record Object1NonNullable(
+ [property: GraphQLDescription("field1A description")] Object2NonNullable Field1A);
+ public sealed record Object2NonNullable(
+ [property: GraphQLDescription("field1B description")] Object3NonNullable Field1B);
+ public sealed record Object3NonNullable(
+ [property: GraphQLDescription("field1C description")] TimeOnly Field1C);
+
+ public sealed record Object1Defaulted(
+ [property: GraphQLDescription("field1A description")]
+ [property: DefaultValueSyntax("""{ field1B: { field1C: "12:00:00" } }""")]
+ Object2Defaulted Field1A);
+ public sealed record Object2Defaulted(
+ [property: GraphQLDescription("field1B description")]
+ [property: DefaultValueSyntax("""{ field1C: "12:00:00" }""")]
+ Object3Defaulted Field1B);
+ public sealed record Object3Defaulted(
+ [property: GraphQLDescription("field1C description")]
+ [property: DefaultValueSyntax("\"12:00:00\"")]
+ TimeOnly Field1C);
+
+ public sealed record Object1Complex(
+ [property: GraphQLDescription("field1A description")]
+ [property: DefaultValueSyntax("""{ field1B: [{ field1C: "12:00:00" }] }""")]
+ Object2Complex Field1A);
+ public sealed record Object2Complex(
+ [property: GraphQLDescription("field1B description")]
+ [property: DefaultValueSyntax("""[{ field1C: "12:00:00" }]""")]
+ Object3Complex[] Field1B);
+ public sealed record Object3Complex(
+ [property: GraphQLDescription("field1C description")]
+ [property: DefaultValueSyntax("\"12:00:00\"")]
+ TimeOnly? Field1C);
+
+ [OneOf]
+ public sealed record OneOf(
+ [property: GraphQLDescription("field1 description")] int? Field1,
+ [property: GraphQLDescription("field2 description")] string? Field2);
+
+ public sealed record ObjectWithOneOfField(
+ [property: GraphQLDescription("field description")]
+ [property: DefaultValueSyntax("{ field1: 1 }")]
+ OneOf Field);
+
+ public sealed record ResultNullable(
+ [property: GraphQLType] object? Any,
+ bool? Boolean,
+ byte? Byte,
+ [property: GraphQLType] byte[]? ByteArray,
+ [property: GraphQLType] DateOnly? Date,
+ DateTimeOffset? DateTime,
+ decimal? Decimal,
+ TestEnum? Enum,
+ float? Float,
+ [property: GraphQLType] string? Id,
+ int? Int,
+ JsonElement? Json,
+ string?[]? List,
+ DateOnly? LocalDate,
+ [property: GraphQLType] DateTime? LocalDateTime,
+ TimeOnly? LocalTime,
+ long? Long,
+ Object1Nullable? Object,
+ short? Short,
+ string? String,
+ TimeSpan? TimeSpan,
+ [property: GraphQLType] string? Unknown,
+ Uri? Url,
+ Guid? Uuid);
+
+ public sealed record ResultNonNullable(
+ [property: GraphQLType>] object Any,
+ bool Boolean,
+ byte Byte,
+ [property: GraphQLType>] byte[] ByteArray,
+ [property: GraphQLType>] DateOnly Date,
+ DateTimeOffset DateTime,
+ decimal Decimal,
+ TestEnum Enum,
+ float Float,
+ [property: GraphQLType>] string Id,
+ int Int,
+ JsonElement Json,
+ string[] List,
+ DateOnly LocalDate,
+ [property: GraphQLType>] DateTime LocalDateTime,
+ TimeOnly LocalTime,
+ long Long,
+ Object1NonNullable Object,
+ short Short,
+ string String,
+ TimeSpan TimeSpan,
+ [property: GraphQLType>] string Unknown,
+ Uri Url,
+ Guid Uuid);
+
+ public sealed record ResultDefaulted(
+ [property: GraphQLType>] object Any,
+ bool Boolean,
+ byte Byte,
+ [property: GraphQLType>] byte[] ByteArray,
+ [property: GraphQLType>] DateOnly Date,
+ DateTimeOffset DateTime,
+ decimal Decimal,
+ TestEnum Enum,
+ float Float,
+ [property: GraphQLType>] string Id,
+ int Int,
+ JsonElement Json,
+ string[] List,
+ DateOnly LocalDate,
+ [property: GraphQLType>] DateTime LocalDateTime,
+ TimeOnly LocalTime,
+ long Long,
+ Object1Defaulted Object,
+ short Short,
+ string String,
+ TimeSpan TimeSpan,
+ [property: GraphQLType>] string Unknown,
+ Uri Url,
+ Guid Uuid);
+
+ public sealed record ResultComplex(
+ Object1Complex[] List,
+ Object1Complex Object,
+ string? NullDefault,
+ string?[]? ListWithNullDefault,
+ Object1Complex? ObjectWithNullDefault,
+ OneOf OneOf,
+ OneOf[] OneOfList,
+ ObjectWithOneOfField ObjectWithOneOfField,
+ TimeSpan TimeSpanDotNet);
+
+ [UnionType(name: "PetUnion")]
+ public interface IPet
+ {
+ string Name { get; }
+ }
+
+ public sealed record Cat(string Name, bool IsPurring) : IPet;
+ public sealed record Dog(string Name, bool IsBarking) : IPet;
+
+ private sealed class UnknownType() : ScalarType("Unknown")
+ {
+ public override IValueNode ParseResult(object? resultValue)
+ => throw new NotImplementedException();
+
+ protected override string ParseLiteral(StringValueNode valueSyntax)
+ => valueSyntax.Value;
+
+ protected override StringValueNode ParseValue(string runtimeValue)
+ => throw new NotImplementedException();
+ }
+
+ public sealed class ExplicitOpenWorld
+ {
+ [McpToolAnnotations(OpenWorldHint = true)]
+ public int ExplicitOpenWorldField() => 1;
+ }
+
+ public sealed class ImplicitClosedWorld
+ {
+ // Defaults to closed-world, because the parent (query) field is closed-world.
+ public int ImplicitClosedWorldField() => 1;
+ }
+
+ public sealed class ExplicitClosedWorld
+ {
+ [McpToolAnnotations(OpenWorldHint = false)]
+ public int ExplicitClosedWorldField() => 1;
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/AnnotationsWithFragment.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/AnnotationsWithFragment.graphql
new file mode 100644
index 00000000000..37684e3e4a4
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/AnnotationsWithFragment.graphql
@@ -0,0 +1,12 @@
+mutation AnnotationsWithFragment {
+ explicitNonDestructiveMutation
+ explicitIdempotentMutation
+ explicitClosedWorldMutation
+ ...Fragment
+}
+
+fragment Fragment on Mutation {
+ explicitDestructiveMutation
+ explicitNonIdempotentMutation
+ explicitOpenWorldMutation
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitClosedWorldSubfieldTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitClosedWorldSubfieldTool.graphql
new file mode 100644
index 00000000000..041a040d9ba
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitClosedWorldSubfieldTool.graphql
@@ -0,0 +1,5 @@
+query ExplicitClosedWorldSubfieldTool {
+ explicitClosedWorldSubfieldQuery {
+ explicitClosedWorldField
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitClosedWorldTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitClosedWorldTool.graphql
new file mode 100644
index 00000000000..4a3de3dc2d8
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitClosedWorldTool.graphql
@@ -0,0 +1,4 @@
+query ExplicitClosedWorldTool {
+ explicitClosedWorldQuery
+ another: explicitClosedWorldQuery
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitDestructiveTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitDestructiveTool.graphql
new file mode 100644
index 00000000000..8bd43ba1e94
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitDestructiveTool.graphql
@@ -0,0 +1,4 @@
+mutation ExplicitDestructiveTool {
+ explicitNonDestructiveMutation
+ explicitDestructiveMutation
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitIdempotentTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitIdempotentTool.graphql
new file mode 100644
index 00000000000..7a2c66728a7
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitIdempotentTool.graphql
@@ -0,0 +1,4 @@
+mutation ExplicitIdempotentTool {
+ explicitIdempotentMutation
+ another: explicitIdempotentMutation
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitNonDestructiveTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitNonDestructiveTool.graphql
new file mode 100644
index 00000000000..7b994c943d4
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitNonDestructiveTool.graphql
@@ -0,0 +1,4 @@
+mutation ExplicitNonDestructiveTool {
+ explicitNonDestructiveMutation
+ another: explicitNonDestructiveMutation
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitNonIdempotentTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitNonIdempotentTool.graphql
new file mode 100644
index 00000000000..ee632965f77
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitNonIdempotentTool.graphql
@@ -0,0 +1,4 @@
+mutation ExplicitNonIdempotentTool {
+ explicitIdempotentMutation
+ explicitNonIdempotentMutation
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitOpenWorldSubfieldTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitOpenWorldSubfieldTool.graphql
new file mode 100644
index 00000000000..c412f569334
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitOpenWorldSubfieldTool.graphql
@@ -0,0 +1,5 @@
+query ExplicitOpenWorldSubfieldTool {
+ explicitOpenWorldSubfieldQuery {
+ explicitOpenWorldField
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitOpenWorldTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitOpenWorldTool.graphql
new file mode 100644
index 00000000000..325e3a8df3b
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ExplicitOpenWorldTool.graphql
@@ -0,0 +1,4 @@
+query ExplicitOpenWorldTool {
+ explicitClosedWorldQuery
+ explicitOpenWorldQuery
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithComplexVariables.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithComplexVariables.graphql
new file mode 100644
index 00000000000..af79f2301bf
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithComplexVariables.graphql
@@ -0,0 +1,72 @@
+"GetWithComplexVariables description"
+query GetWithComplexVariables(
+ "Complex list"
+ $list: [Object1ComplexInput!]! = [{ field1A: { field1B: [{ field1C: "12:00:00" }] } }]
+ "Complex object"
+ $object: Object1ComplexInput! = { field1A: { field1B: [{ field1C: "12:00:00" }] } }
+ "Null default"
+ $nullDefault: String = null
+ "List with null default"
+ $listWithNullDefault: [String] = [null]
+ "Object with null default"
+ $objectWithNullDefault: Object1ComplexInput = { field1A: { field1B: [{ field1C: null }] } }
+ "OneOf"
+ $oneOf: OneOfInput! = { field1: 1 }
+ "OneOf list"
+ $oneOfList: [OneOfInput!]! = [{ field1: 1 }, { field2: "default" }]
+ "Object with OneOf field"
+ $objectWithOneOfField: ObjectWithOneOfFieldInput! = { field: { field1: 1 } }
+ "TimeSpan with DotNet format"
+ $timeSpanDotNet: TimeSpan! = "00:05:00"
+) {
+ withComplexVariables(
+ list: $list
+ object: $object
+ nullDefault: $nullDefault
+ listWithNullDefault: $listWithNullDefault
+ objectWithNullDefault: $objectWithNullDefault
+ oneOf: $oneOf
+ oneOfList: $oneOfList
+ objectWithOneOfField: $objectWithOneOfField
+ timeSpanDotNet: $timeSpanDotNet
+ ) {
+ list {
+ field1A {
+ field1B {
+ field1C
+ }
+ }
+ }
+ object {
+ field1A {
+ field1B {
+ field1C
+ }
+ }
+ }
+ nullDefault
+ listWithNullDefault
+ objectWithNullDefault {
+ field1A {
+ field1B {
+ field1C
+ }
+ }
+ }
+ oneOf {
+ field1
+ field2
+ }
+ oneOfList {
+ field1
+ field2
+ }
+ objectWithOneOfField {
+ field {
+ field1
+ field2
+ }
+ }
+ timeSpanDotNet
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithDefaultedVariables.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithDefaultedVariables.graphql
new file mode 100644
index 00000000000..fea12117b67
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithDefaultedVariables.graphql
@@ -0,0 +1,109 @@
+"GetWithDefaultedVariables description"
+query GetWithDefaultedVariables(
+ "Any description"
+ $any: Any! = { key: "value" }
+ "Boolean description"
+ $boolean: Boolean! = true
+ "Byte description"
+ $byte: Byte! = 1
+ "ByteArray description"
+ $byteArray: ByteArray! = "ZGVmYXVsdA=="
+ "Date description"
+ $date: Date! = "2000-01-01"
+ "DateTime description"
+ $dateTime: DateTime! = "2000-01-01T12:00:00Z"
+ "Decimal description"
+ $decimal: Decimal! = 79228162514264337593543950335
+ "Enum description"
+ $enum: TestEnum! = VALUE1
+ "Float description"
+ $float: Float! = 1.5
+ "ID description"
+ $id: ID! = "default"
+ "Int description"
+ $int: Int! = 1
+ "JSON description"
+ $json: JSON! = { key: "value" }
+ "List description"
+ $list: [String!]! = ["default"]
+ "LocalDate description"
+ $localDate: LocalDate! = "2000-01-01"
+ "LocalDateTime description"
+ $localDateTime: LocalDateTime! = "2000-01-01T12:00:00"
+ "LocalTime description"
+ $localTime: LocalTime! = "12:00:00"
+ "Long description"
+ $long: Long! = 9223372036854775807
+ "Object description"
+ $object: Object1DefaultedInput! = { field1A: { field1B: { field1C: "12:00:00" } } }
+ "Short description"
+ $short: Short! = 1
+ "String description"
+ $string: String! = "default"
+ "TimeSpan description"
+ $timeSpan: TimeSpan! = "PT5M"
+ "Unknown description"
+ $unknown: Unknown! = "default"
+ "URL description"
+ $url: URL! = "https://example.com"
+ "UUID description"
+ $uuid: UUID! = "00000000-0000-0000-0000-000000000000"
+) {
+ withDefaultedVariables(
+ any: $any
+ boolean: $boolean
+ byte: $byte
+ byteArray: $byteArray
+ date: $date
+ dateTime: $dateTime
+ decimal: $decimal
+ enum: $enum
+ float: $float
+ id: $id
+ int: $int
+ json: $json
+ list: $list
+ localDate: $localDate
+ localDateTime: $localDateTime
+ localTime: $localTime
+ long: $long
+ object: $object
+ short: $short
+ string: $string
+ timeSpan: $timeSpan
+ unknown: $unknown
+ url: $url
+ uuid: $uuid
+ ) {
+ any
+ boolean
+ byte
+ byteArray
+ date
+ dateTime
+ decimal
+ enum
+ float
+ id
+ int
+ json
+ list
+ localDate
+ localDateTime
+ localTime
+ long
+ object {
+ field1A {
+ field1B {
+ field1C
+ }
+ }
+ }
+ short
+ string
+ timeSpan
+ unknown
+ url
+ uuid
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithNonNullableVariables.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithNonNullableVariables.graphql
new file mode 100644
index 00000000000..3288de3c94d
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithNonNullableVariables.graphql
@@ -0,0 +1,85 @@
+"GetWithNonNullableVariables description"
+query GetWithNonNullableVariables(
+ "Any description" $any: Any!
+ "Boolean description" $boolean: Boolean!
+ "Byte description" $byte: Byte!
+ "ByteArray description" $byteArray: ByteArray!
+ "Date description" $date: Date!
+ "DateTime description" $dateTime: DateTime!
+ "Decimal description" $decimal: Decimal!
+ "Enum description" $enum: TestEnum!
+ "Float description" $float: Float!
+ "ID description" $id: ID!
+ "Int description" $int: Int!
+ "JSON description" $json: JSON!
+ "List description" $list: [String!]!
+ "LocalDate description" $localDate: LocalDate!
+ "LocalDateTime description" $localDateTime: LocalDateTime!
+ "LocalTime description" $localTime: LocalTime!
+ "Long description" $long: Long!
+ "Object description" $object: Object1NonNullableInput!
+ "Short description" $short: Short!
+ "String description" $string: String!
+ "TimeSpan description" $timeSpan: TimeSpan!
+ "Unknown description" $unknown: Unknown!
+ "URL description" $url: URL!
+ "UUID description" $uuid: UUID!
+) {
+ withNonNullableVariables(
+ any: $any
+ boolean: $boolean
+ byte: $byte
+ byteArray: $byteArray
+ date: $date
+ dateTime: $dateTime
+ decimal: $decimal
+ enum: $enum
+ float: $float
+ id: $id
+ int: $int
+ json: $json
+ list: $list
+ localDate: $localDate
+ localDateTime: $localDateTime
+ localTime: $localTime
+ long: $long
+ object: $object
+ short: $short
+ string: $string
+ timeSpan: $timeSpan
+ unknown: $unknown
+ url: $url
+ uuid: $uuid
+ ) {
+ any
+ boolean
+ byte
+ byteArray
+ date
+ dateTime
+ decimal
+ enum
+ float
+ id
+ int
+ json
+ list
+ localDate
+ localDateTime
+ localTime
+ long
+ object {
+ field1A {
+ field1B {
+ field1C
+ }
+ }
+ }
+ short
+ string
+ timeSpan
+ unknown
+ url
+ uuid
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithNullableVariables.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithNullableVariables.graphql
new file mode 100644
index 00000000000..4fcd98e4d0e
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithNullableVariables.graphql
@@ -0,0 +1,85 @@
+"GetWithNullableVariables description"
+query GetWithNullableVariables(
+ "Any description" $any: Any
+ "Boolean description" $boolean: Boolean
+ "Byte description" $byte: Byte
+ "ByteArray description" $byteArray: ByteArray
+ "Date description" $date: Date
+ "DateTime description" $dateTime: DateTime
+ "Decimal description" $decimal: Decimal
+ "Enum description" $enum: TestEnum
+ "Float description" $float: Float
+ "ID description" $id: ID
+ "Int description" $int: Int
+ "JSON description" $json: JSON
+ "List description" $list: [String]
+ "LocalDate description" $localDate: LocalDate
+ "LocalDateTime description" $localDateTime: LocalDateTime
+ "LocalTime description" $localTime: LocalTime
+ "Long description" $long: Long
+ "Object description" $object: Object1NullableInput
+ "Short description" $short: Short
+ "String description" $string: String
+ "TimeSpan description" $timeSpan: TimeSpan
+ "Unknown description" $unknown: Unknown
+ "URL description" $url: URL
+ "UUID description" $uuid: UUID
+) {
+ withNullableVariables(
+ any: $any
+ boolean: $boolean
+ byte: $byte
+ byteArray: $byteArray
+ date: $date
+ dateTime: $dateTime
+ decimal: $decimal
+ enum: $enum
+ float: $float
+ id: $id
+ int: $int
+ json: $json
+ list: $list
+ localDate: $localDate
+ localDateTime: $localDateTime
+ localTime: $localTime
+ long: $long
+ object: $object
+ short: $short
+ string: $string
+ timeSpan: $timeSpan
+ unknown: $unknown
+ url: $url
+ uuid: $uuid
+ ) {
+ any
+ boolean
+ byte
+ byteArray
+ date
+ dateTime
+ decimal
+ enum
+ float
+ id
+ int
+ json
+ list
+ localDate
+ localDateTime
+ localTime
+ long
+ object {
+ field1A {
+ field1B {
+ field1C
+ }
+ }
+ }
+ short
+ string
+ timeSpan
+ unknown
+ url
+ uuid
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithVariableMinMaxValues.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithVariableMinMaxValues.graphql
new file mode 100644
index 00000000000..0f4e70d84be
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/GetWithVariableMinMaxValues.graphql
@@ -0,0 +1,11 @@
+"GetWithVariableMinMaxValues description"
+query GetWithVariableMinMaxValues(
+ $byte: Byte!
+ $decimal: Decimal!
+ $float: Float!
+ $int: Int!
+ $long: Long!
+ $short: Short!
+) {
+ withVariableMinMaxValues
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitClosedWorldSubfieldTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitClosedWorldSubfieldTool.graphql
new file mode 100644
index 00000000000..1b26467885e
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitClosedWorldSubfieldTool.graphql
@@ -0,0 +1,5 @@
+query ImplicitClosedWorldSubfieldTool {
+ implicitClosedWorldSubfieldQuery {
+ implicitClosedWorldField
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitDestructiveTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitDestructiveTool.graphql
new file mode 100644
index 00000000000..0accb393237
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitDestructiveTool.graphql
@@ -0,0 +1,4 @@
+mutation ImplicitDestructiveTool {
+ explicitNonDestructiveMutation
+ implicitDestructiveMutation
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitNonIdempotentTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitNonIdempotentTool.graphql
new file mode 100644
index 00000000000..5f62f0eb5a9
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitNonIdempotentTool.graphql
@@ -0,0 +1,4 @@
+mutation ImplicitNonIdempotentTool {
+ explicitIdempotentMutation
+ implicitNonIdempotentMutation
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitOpenWorldTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitOpenWorldTool.graphql
new file mode 100644
index 00000000000..9265519775b
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/ImplicitOpenWorldTool.graphql
@@ -0,0 +1,4 @@
+query ImplicitOpenWorldTool {
+ explicitClosedWorldQuery
+ implicitOpenWorldQuery
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/NonDestructiveFragmentTool.graphql b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/NonDestructiveFragmentTool.graphql
new file mode 100644
index 00000000000..d73d66ad0a0
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__resources__/NonDestructiveFragmentTool.graphql
@@ -0,0 +1,8 @@
+mutation NonDestructiveFragmentTool {
+ nonDestructiveMutation
+ ...NonDestructiveFragment
+}
+
+fragment NonDestructiveFragment on Mutation {
+ another: nonDestructiveMutation
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithComplexVariables_ReturnsExpectedResult.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithComplexVariables_ReturnsExpectedResult.json
new file mode 100644
index 00000000000..b63918668e6
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithComplexVariables_ReturnsExpectedResult.json
@@ -0,0 +1,60 @@
+{
+ "data": {
+ "withComplexVariables": {
+ "list": [
+ {
+ "field1A": {
+ "field1B": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ }
+ ],
+ "object": {
+ "field1A": {
+ "field1B": [
+ {
+ "field1C": "12:00:00"
+ }
+ ]
+ }
+ },
+ "nullDefault": null,
+ "listWithNullDefault": [
+ null
+ ],
+ "objectWithNullDefault": {
+ "field1A": {
+ "field1B": [
+ {
+ "field1C": null
+ }
+ ]
+ }
+ },
+ "oneOf": {
+ "field1": 1,
+ "field2": null
+ },
+ "oneOfList": [
+ {
+ "field1": 1,
+ "field2": null
+ },
+ {
+ "field1": null,
+ "field2": "test"
+ }
+ ],
+ "objectWithOneOfField": {
+ "field": {
+ "field1": 1,
+ "field2": null
+ }
+ },
+ "timeSpanDotNet": "00:05:00"
+ }
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithDefaultedVariables_ReturnsExpectedResult.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithDefaultedVariables_ReturnsExpectedResult.json
new file mode 100644
index 00000000000..5cea0549974
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithDefaultedVariables_ReturnsExpectedResult.json
@@ -0,0 +1,42 @@
+{
+ "data": {
+ "withDefaultedVariables": {
+ "any": {
+ "key": "value"
+ },
+ "boolean": true,
+ "byte": 1,
+ "byteArray": "ZGVmYXVsdA==",
+ "date": "2000-01-01",
+ "dateTime": "2000-01-01T12:00:00.000Z",
+ "decimal": 79228162514264337593543950335,
+ "enum": "VALUE1",
+ "float": 1.5,
+ "id": "default",
+ "int": 1,
+ "json": {
+ "key": "value"
+ },
+ "list": [
+ "default"
+ ],
+ "localDate": "2000-01-01",
+ "localDateTime": "2000-01-01T12:00:00",
+ "localTime": "12:00:00",
+ "long": 9223372036854775807,
+ "object": {
+ "field1A": {
+ "field1B": {
+ "field1C": "12:00:00"
+ }
+ }
+ },
+ "short": 1,
+ "string": "default",
+ "timeSpan": "PT5M",
+ "unknown": "default",
+ "url": "https://example.com/",
+ "uuid": "00000000-0000-0000-0000-000000000000"
+ }
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithErrors_ReturnsExpectedResult.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithErrors_ReturnsExpectedResult.json
new file mode 100644
index 00000000000..17b675208a0
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithErrors_ReturnsExpectedResult.json
@@ -0,0 +1,35 @@
+{
+ "errors": [
+ {
+ "message": "Error 1",
+ "locations": [
+ {
+ "line": 1,
+ "column": 23
+ }
+ ],
+ "path": [
+ "withErrors"
+ ],
+ "extensions": {
+ "code": "Code 1"
+ }
+ },
+ {
+ "message": "Error 2",
+ "locations": [
+ {
+ "line": 1,
+ "column": 23
+ }
+ ],
+ "path": [
+ "withErrors"
+ ],
+ "extensions": {
+ "code": "Code 2"
+ }
+ }
+ ],
+ "data": null
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithNonNullableVariables_ReturnsExpectedResult.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithNonNullableVariables_ReturnsExpectedResult.json
new file mode 100644
index 00000000000..a8a85ba6340
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithNonNullableVariables_ReturnsExpectedResult.json
@@ -0,0 +1,42 @@
+{
+ "data": {
+ "withNonNullableVariables": {
+ "any": {
+ "key": "value"
+ },
+ "boolean": true,
+ "byte": 1,
+ "byteArray": "dGVzdA==",
+ "date": "2000-01-01",
+ "dateTime": "2000-01-01T12:00:00.000Z",
+ "decimal": 79228162514264337593543950335,
+ "enum": "VALUE1",
+ "float": 1.5,
+ "id": "test",
+ "int": 1,
+ "json": {
+ "key": "value"
+ },
+ "list": [
+ "test"
+ ],
+ "localDate": "2000-01-01",
+ "localDateTime": "2000-01-01T12:00:00",
+ "localTime": "12:00:00",
+ "long": 9223372036854775807,
+ "object": {
+ "field1A": {
+ "field1B": {
+ "field1C": "12:00:00"
+ }
+ }
+ },
+ "short": 1,
+ "string": "test",
+ "timeSpan": "PT5M",
+ "unknown": "test",
+ "url": "https://example.com/",
+ "uuid": "00000000-0000-0000-0000-000000000000"
+ }
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithNullableVariables_ReturnsExpectedResult.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithNullableVariables_ReturnsExpectedResult.json
new file mode 100644
index 00000000000..a91766e774b
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.CallTool_GetWithNullableVariables_ReturnsExpectedResult.json
@@ -0,0 +1,30 @@
+{
+ "data": {
+ "withNullableVariables": {
+ "any": null,
+ "boolean": null,
+ "byte": null,
+ "byteArray": null,
+ "date": null,
+ "dateTime": null,
+ "decimal": null,
+ "enum": null,
+ "float": null,
+ "id": null,
+ "int": null,
+ "json": null,
+ "list": null,
+ "localDate": null,
+ "localDateTime": null,
+ "localTime": null,
+ "long": null,
+ "object": null,
+ "short": null,
+ "string": null,
+ "timeSpan": null,
+ "unknown": null,
+ "url": null,
+ "uuid": null
+ }
+ }
+}
diff --git a/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.ListTools_Valid_ReturnsTools.json b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.ListTools_Valid_ReturnsTools.json
new file mode 100644
index 00000000000..49180fb6314
--- /dev/null
+++ b/src/HotChocolate/ModelContextProtocol/test/HotChocolate.ModelContextProtocol.Tests/__snapshots__/IntegrationTests.ListTools_Valid_ReturnsTools.json
@@ -0,0 +1,12 @@
+[
+ {
+ "Name": "get_with_nullable_variables",
+ "Title": "Get With Nullable Variables",
+ "Description": "GetWithNullableVariables description"
+ },
+ {
+ "Name": "get_with_non_nullable_variables",
+ "Title": "Get With Non Nullable Variables",
+ "Description": "GetWithNonNullableVariables description"
+ }
+]