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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Security.Claims;
using System.Text;
using System.Text.Json;
using HotChocolate.AspNetCore.Instrumentation;
using HotChocolate.Buffers;
using HotChocolate.Execution;
using HotChocolate.Transport.Formatters;
Expand Down Expand Up @@ -61,6 +62,7 @@ public static async ValueTask<CallToolResult> HandleAsync(
var requestExecutor = services.GetRequiredService<IRequestExecutor>();
var rootServiceProvider = services.GetRequiredService<IRootServiceProviderAccessor>().ServiceProvider;
var httpContext = rootServiceProvider.GetRequiredService<IHttpContextAccessor>().HttpContext!;
var diagnosticEvents = requestExecutor.Schema.Services.GetRequiredService<IServerDiagnosticEvents>();
var requestBuilder = CreateRequestBuilder(httpContext);

if (context.Params?.Arguments is { Count: > 0 } arguments)
Expand All @@ -69,24 +71,28 @@ public static async ValueTask<CallToolResult> HandleAsync(
}

var request = requestBuilder.SetDocument(tool.DocumentNode).Build();
var result = await requestExecutor.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
var operationResult = result.ExpectOperationResult();

using var writer = new PooledArrayWriter();
using (diagnosticEvents.ExecuteHttpRequest(httpContext, HttpRequestKind.HttpPost))
{
var result = await requestExecutor.ExecuteAsync(request, cancellationToken).ConfigureAwait(false);
var operationResult = result.ExpectOperationResult();

JsonResultFormatter.Indented.Format(operationResult, writer);
var jsonOperationResult = Encoding.UTF8.GetString(writer.WrittenSpan);
using var writer = new PooledArrayWriter();

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 = jsonOperationResult }],
StructuredContent = JsonElement.Parse(jsonOperationResult),
IsError = !operationResult.Errors.IsEmpty
};
JsonResultFormatter.Indented.Format(operationResult, writer);
var jsonOperationResult = Encoding.UTF8.GetString(writer.WrittenSpan);

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 = jsonOperationResult }],
StructuredContent = JsonElement.Parse(jsonOperationResult),
IsError = !operationResult.Errors.IsEmpty
};
}
}

#if !NET9_0_OR_GREATER
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

<ItemGroup>
<ProjectReference Include="..\Adapters.Mcp.Abstractions\HotChocolate.Adapters.Mcp.Abstractions.csproj" />
<ProjectReference Include="..\..\..\AspNetCore\src\AspNetCore.Pipeline\HotChocolate.AspNetCore.Pipeline.csproj" />
<ProjectReference Include="..\..\..\AspNetCore\src\Transport.Formatters\HotChocolate.Transport.Formatters.csproj" />
<ProjectReference Include="..\..\..\Core\src\Execution.Abstractions\HotChocolate.Execution.Abstractions.csproj" />
<ProjectReference Include="..\..\..\Core\src\Types.Abstractions\HotChocolate.Types.Abstractions.csproj" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using HotChocolate.AspNetCore.Instrumentation;
using HotChocolate.Buffers;
using HotChocolate.Execution;
using HotChocolate.Language;
Expand Down Expand Up @@ -61,75 +62,81 @@ await Results.Problem(
.Get(schemaName)
.ExecutorProxy;
var session = await proxy.GetOrCreateSessionAsync(context.RequestAborted);
var requestKind = HttpMethods.IsGet(context.Request.Method)
? HttpRequestKind.HttpGet
: HttpRequestKind.HttpPost;

using var variableBuffer = new PooledArrayWriter();
using var variables = await BuildVariablesAsync(
endpointDescriptor,
context,
variableBuffer,
cancellationToken);
using (session.DiagnosticEvents.ExecuteHttpRequest(context, requestKind))
{
using var variableBuffer = new PooledArrayWriter();
using var variables = await BuildVariablesAsync(
endpointDescriptor,
context,
variableBuffer,
cancellationToken);

var requestBuilder = OperationRequestBuilder.New()
.SetDocument(endpointDescriptor.Document)
.SetErrorHandlingMode(ErrorHandlingMode.Propagate)
.SetVariableValues(variables);
var requestBuilder = OperationRequestBuilder.New()
.SetDocument(endpointDescriptor.Document)
.SetErrorHandlingMode(ErrorHandlingMode.Propagate)
.SetVariableValues(variables);

await session.OnCreateAsync(context, requestBuilder, cancellationToken);
await session.OnCreateAsync(context, requestBuilder, cancellationToken);

var executionResult = await session.ExecuteAsync(
requestBuilder.Build(),
cancellationToken).ConfigureAwait(false);
var executionResult = await session.ExecuteAsync(
requestBuilder.Build(),
cancellationToken).ConfigureAwait(false);

// If the request was canceled, we do not attempt to write a response.
if (cancellationToken.IsCancellationRequested)
{
return;
}
// If the request was canceled, we do not attempt to write a response.
if (cancellationToken.IsCancellationRequested)
{
return;
}

// If we do not have an operation result, something went wrong, and we return HTTP 500.
if (executionResult is not OperationResult operationResult)
{
// If we do not have an operation result, something went wrong, and we return HTTP 500.
if (executionResult is not OperationResult operationResult)
{
#if NET9_0_OR_GREATER
await Results.InternalServerError().ExecuteAsync(context);
await Results.InternalServerError().ExecuteAsync(context);
#else
await Results.StatusCode(500).ExecuteAsync(context);
await Results.StatusCode(500).ExecuteAsync(context);
#endif
return;
}

// If the request had validation errors or execution didn't start, we return HTTP 400.
if (operationResult.ContextData.ContainsKey(ExecutionContextData.ValidationErrors)
|| !operationResult.Data.HasValue)
{
var firstErrorMessage = operationResult.Errors.FirstOrDefault()?.Message;
return;
}

if (!string.IsNullOrEmpty(firstErrorMessage))
// If the request had validation errors or execution didn't start, we return HTTP 400.
if (operationResult.ContextData.ContainsKey(ExecutionContextData.ValidationErrors)
|| !operationResult.Data.HasValue)
{
await Results.Problem(
detail: firstErrorMessage,
statusCode: StatusCodes.Status400BadRequest).ExecuteAsync(context);
var firstErrorMessage = operationResult.Errors.FirstOrDefault()?.Message;

if (!string.IsNullOrEmpty(firstErrorMessage))
{
await Results.Problem(
detail: firstErrorMessage,
statusCode: StatusCodes.Status400BadRequest).ExecuteAsync(context);
}
else
{
await Results.BadRequest().ExecuteAsync(context);
}

return;
}
else

// If execution started, and we produced GraphQL errors,
// we return HTTP 500 or 401/403 for authorization errors.
if (!operationResult.Errors.IsEmpty)
{
await Results.BadRequest().ExecuteAsync(context);
}
var result = GetResultFromErrors(operationResult.Errors);

return;
}
await result.ExecuteAsync(context);
return;
}

// If execution started, and we produced GraphQL errors,
// we return HTTP 500 or 401/403 for authorization errors.
if (!operationResult.Errors.IsEmpty)
{
var result = GetResultFromErrors(operationResult.Errors);
var formatter = session.Schema.Services.GetRequiredService<IOpenApiResultFormatter>();

await result.ExecuteAsync(context);
return;
await formatter.FormatResultAsync(operationResult, context, endpointDescriptor, cancellationToken);
}

var formatter = session.Schema.Services.GetRequiredService<IOpenApiResultFormatter>();

await formatter.FormatResultAsync(operationResult, context, endpointDescriptor, cancellationToken);
}
catch (BadRequestException badRequestException)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
await storage.AddOrUpdateToolAsync(
new OperationToolDefinition(
Utf8GraphQLParser.Parse("query GetBooks { books { title } }")));
var builder = new WebHostBuilder()

Check warning on line 31 in src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/CoreIntegrationTests.cs

View workflow job for this annotation

GitHub Actions / Run HotChocolate.Adapters.Mcp.Tests

'WebHostBuilder' is obsolete: 'WebHostBuilder is deprecated in favor of HostBuilder and WebApplicationBuilder. For more information, visit https://aka.ms/aspnet/deprecate/004.' (https://aka.ms/aspnet/deprecate/004)
.ConfigureServices(
services => services
.AddRouting()
Expand All @@ -46,7 +46,7 @@
app => app
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapGraphQLMcp()));
var server = new TestServer(builder);

Check warning on line 49 in src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/CoreIntegrationTests.cs

View workflow job for this annotation

GitHub Actions / Run HotChocolate.Adapters.Mcp.Tests

'TestServer.TestServer(IWebHostBuilder)' is obsolete: 'IWebHost, which this method uses, is obsolete. Use one of the ctors that takes an IServiceProvider instead. For more information, visit https://aka.ms/aspnet/deprecate/008.' (https://aka.ms/aspnet/deprecate/008)
var mcpClient = await CreateMcpClientAsync(server.CreateClient());

// act
Expand All @@ -60,7 +60,7 @@
public void MapGraphQLMcp_Should_Throw_When_AddMcpNotCalled()
{
// arrange
var builder = new WebHostBuilder()

Check warning on line 63 in src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/CoreIntegrationTests.cs

View workflow job for this annotation

GitHub Actions / Run HotChocolate.Adapters.Mcp.Tests

'WebHostBuilder' is obsolete: 'WebHostBuilder is deprecated in favor of HostBuilder and WebApplicationBuilder. For more information, visit https://aka.ms/aspnet/deprecate/004.' (https://aka.ms/aspnet/deprecate/004)
.ConfigureServices(
services => services
.AddRouting()
Expand All @@ -72,7 +72,7 @@
.UseEndpoints(endpoints => endpoints.MapGraphQLMcp()));

// act
var exception = Assert.Throws<InvalidOperationException>(() => new TestServer(builder));

Check warning on line 75 in src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/CoreIntegrationTests.cs

View workflow job for this annotation

GitHub Actions / Run HotChocolate.Adapters.Mcp.Tests

'TestServer.TestServer(IWebHostBuilder)' is obsolete: 'IWebHost, which this method uses, is obsolete. Use one of the ctors that takes an IServiceProvider instead. For more information, visit https://aka.ms/aspnet/deprecate/008.' (https://aka.ms/aspnet/deprecate/008)

// assert
Assert.Equal(
Expand All @@ -90,7 +90,7 @@
Utf8GraphQLParser.Parse(
await File.ReadAllTextAsync("__resources__/GetSingleField.graphql"))));
var typeModule = new TestTypeModule();
var builder = new WebHostBuilder()

Check warning on line 93 in src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/CoreIntegrationTests.cs

View workflow job for this annotation

GitHub Actions / Run HotChocolate.Adapters.Mcp.Tests

'WebHostBuilder' is obsolete: 'WebHostBuilder is deprecated in favor of HostBuilder and WebApplicationBuilder. For more information, visit https://aka.ms/aspnet/deprecate/004.' (https://aka.ms/aspnet/deprecate/004)
.ConfigureServices(
services => services
.AddRouting()
Expand All @@ -102,7 +102,7 @@
app => app
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapGraphQLMcp()));
var server = new TestServer(builder);

Check warning on line 105 in src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/CoreIntegrationTests.cs

View workflow job for this annotation

GitHub Actions / Run HotChocolate.Adapters.Mcp.Tests

'TestServer.TestServer(IWebHostBuilder)' is obsolete: 'IWebHost, which this method uses, is obsolete. Use one of the ctors that takes an IServiceProvider instead. For more information, visit https://aka.ms/aspnet/deprecate/008.' (https://aka.ms/aspnet/deprecate/008)
var mcpClient1 = await CreateMcpClientAsync(server.CreateClient());
var mcpClient2 = await CreateMcpClientAsync(server.CreateClient());
var listChangedResetEvent1 = new ManualResetEventSlim(false);
Expand Down Expand Up @@ -180,7 +180,7 @@

var builder =
services
.AddGraphQL()
.AddGraphQLServer()
.AddAuthorization()
.AddMcp(configureMcpServerOptions, configureMcpServer)
.AddMcpStorage(storage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@
<ProjectReference Include="..\..\..\CostAnalysis\src\CostAnalysis\HotChocolate.CostAnalysis.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\..\Adapters\src\Adapters.Mcp\HotChocolate.Adapters.Mcp.csproj" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' != 'net8.0'">
<ProjectReference Include="..\..\..\Adapters\src\Adapters.OpenApi\HotChocolate.Adapters.OpenApi.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="OpenTelemetry.Exporter.InMemory" />
Expand Down
Loading
Loading