Skip to content

Use Options pattern for openapi and mcp#9598

Open
PascalSenn wants to merge 9 commits intomainfrom
feat/mcp-openapi-setup-pattern
Open

Use Options pattern for openapi and mcp#9598
PascalSenn wants to merge 9 commits intomainfrom
feat/mcp-openapi-setup-pattern

Conversation

@PascalSenn
Copy link
Copy Markdown
Member

@PascalSenn PascalSenn commented Apr 25, 2026

No description provided.

…roduce IMcpProvider and IOpenApiProvider interfaces
…method, refactor OpenApiManager and McpManager to support new transport setup, and introduce tests for MCP setup composition.
…p-pattern

# Conflicts:
#	src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/EndpointRouteBuilderExtensions.cs
#	src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/OpenApiOptionsExtensions.cs
@PascalSenn PascalSenn marked this pull request as ready for review April 25, 2026 19:39
Copilot AI review requested due to automatic review settings April 25, 2026 19:39
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the OpenAPI and MCP adapters to use an options-based per-schema setup model (via IOptionsMonitor<T>), replacing prior keyed-service registrations with manager-based lookups.

Changes:

  • Introduces OpenApiSetup/McpSetup options and OpenApiManager/McpManager registries for per-schema composition and resolution.
  • Updates OpenAPI/MCP builder and endpoint extensions to resolve per-schema infrastructure through the managers instead of keyed DI services.
  • Adds tests to validate setup composition and schema-name discovery for both adapters.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/Extensions/OpenApiSetupTests.cs Adds tests asserting OpenAPI per-schema setup registration and name discovery.
src/HotChocolate/Adapters/test/Adapters.Mcp.Tests/Extensions/McpSetupCompositionTests.cs Adds tests asserting MCP setup composition across multiple AddMcp calls and name discovery.
src/HotChocolate/Adapters/src/Fusion.Adapters.OpenApi/Extensions/OpenApiFusionGatewayBuilderExtensions.cs Switches OpenAPI storage registration to options-based setup; warmup uses OpenApiManager.
src/HotChocolate/Adapters/src/Fusion.Adapters.Mcp/Extensions/FusionGatewayBuilderExtensions.cs Switches MCP registration to options-based setup; updates warmup task wiring.
src/HotChocolate/Adapters/src/Adapters.OpenApi/Extensions/OpenApiRequestExecutorBuilderExtensions.cs Switches OpenAPI storage registration to options-based setup; warmup uses OpenApiManager.
src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/OpenApiRegistration.cs Adds internal container for OpenAPI per-schema runtime objects.
src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/OpenApiManager.cs Adds manager responsible for building/caching per-schema OpenAPI runtime objects from options.
src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/IOpenApiProvider.cs Adds internal provider abstraction for schema name inference.
src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Extensions/InternalServiceCollectionExtensions.cs Registers OpenApiManager and options infrastructure (replacing keyed services).
src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Execution/DynamicEndpointMiddleware.cs Resolves executor proxy through OpenApiManager instead of keyed DI.
src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Configuration/OpenApiTransportSetup.cs Adds internal transport setup options for endpoint/document transformer factories.
src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Configuration/OpenApiSetup.cs Adds public per-schema OpenAPI setup options (storage factory).
src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/OpenApiOptionsExtensions.cs Resolves OpenAPI document transformer via OpenApiManager for ASP.NET OpenAPI generation.
src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/InternalServiceCollectionExtensions.cs Uses PostConfigureAll to supply default OpenAPI transport factories.
src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/EndpointRouteBuilderExtensions.cs Resolves endpoint data source via OpenApiManager for mapping endpoints.
src/HotChocolate/Adapters/src/Adapters.Mcp/Extensions/RequestExecutorBuilderExtensions.cs Switches MCP registration to options-based setup; updates warmup task wiring.
src/HotChocolate/Adapters/src/Adapters.Mcp.Core/McpStorageWarmupTask.cs Adds warmup task wrapper for starting MCP storage observer.
src/HotChocolate/Adapters/src/Adapters.Mcp.Core/McpRegistration.cs Adds internal container for MCP per-schema runtime objects.
src/HotChocolate/Adapters/src/Adapters.Mcp.Core/McpManager.cs Adds manager responsible for caching per-schema MCP runtime objects.
src/HotChocolate/Adapters/src/Adapters.Mcp.Core/IMcpProvider.cs Adds internal provider abstraction for schema name inference.
src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/ServiceCollectionExtensions.cs Registers McpManager/options and adds per-schema setup composition logic.
src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Extensions/EndpointRouteBuilderExtensions.cs Resolves MCP handler proxy via McpManager for endpoint mapping.
src/HotChocolate/Adapters/src/Adapters.Mcp.Core/Configuration/McpSetup.cs Adds public per-schema MCP setup options (modifier lists).
Comments suppressed due to low confidence (1)

src/HotChocolate/Adapters/src/Adapters.OpenApi.AspNetCore/Extensions/EndpointRouteBuilderExtensions.cs:22

  • MapOpenApiEndpoints downcasts the registration's EndpointDataSource to DynamicEndpointDataSource. If the endpoint data source is ever swapped via transport setup, this will fail at runtime. Consider changing the registration/transport setup to expose an EndpointDataSource (base type) directly, or otherwise avoid relying on this concrete cast here.
        var dataSource = (DynamicEndpointDataSource)manager.Get(schemaName).EndpointDataSource;

        if (!endpoints.DataSources.Contains(dataSource))
        {
            endpoints.DataSources.Add(dataSource);
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +74 to +86
var storageFactory =
setup.StorageFactory
?? throw new InvalidOperationException(
$"No OpenAPI definition storage is registered for schema '{name}'. "
+ "Call AddOpenApiDefinitionStorage(...) when configuring the GraphQL server.");

var storage = storageFactory(_applicationServices);
var endpointDataSource = transportSetup.EndpointDataSourceFactory!();
var documentTransformer = transportSetup.DocumentTransformerFactory!();
var registry = new OpenApiDefinitionRegistry(storage, documentTransformer, endpointDataSource);
var executorProxy = HttpRequestExecutorProxy.Create(_applicationServices, name);

return new OpenApiRegistration(registry, executorProxy, endpointDataSource, documentTransformer);
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenApiManager creates and caches several disposable objects (OpenApiDefinitionRegistry is IAsyncDisposable; HttpRequestExecutorProxy ultimately implements IDisposable; DynamicEndpointDataSource implements IDisposable) but nothing disposes them when the application ServiceProvider is disposed. This is a regression from the previous DI-registered singletons and can leak event subscriptions/CTS resources. Consider creating a per-schema IServiceScope and storing it in the registration, or make OpenApiManager implement IAsyncDisposable/IDisposable and dispose the created registration components when the container shuts down.

Copilot uses AI. Check for mistakes.
Comment on lines +80 to +82
var storage = storageFactory(_applicationServices);
var endpointDataSource = transportSetup.EndpointDataSourceFactory!();
var documentTransformer = transportSetup.DocumentTransformerFactory!();
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CreateRegistration dereferences transportSetup.EndpointDataSourceFactory and DocumentTransformerFactory with null-forgiving operators. If AddOpenApiAspNetCoreServices() was not registered (or registration order changes), this will throw a NullReferenceException without a clear error. Prefer validating these factories and throwing an InvalidOperationException with guidance on the missing registration.

Suggested change
var storage = storageFactory(_applicationServices);
var endpointDataSource = transportSetup.EndpointDataSourceFactory!();
var documentTransformer = transportSetup.DocumentTransformerFactory!();
var endpointDataSourceFactory =
transportSetup.EndpointDataSourceFactory
?? throw new InvalidOperationException(
$"No OpenAPI endpoint data source factory is registered for schema '{name}'. "
+ "Call AddOpenApiAspNetCoreServices() when configuring the GraphQL server.");
var documentTransformerFactory =
transportSetup.DocumentTransformerFactory
?? throw new InvalidOperationException(
$"No OpenAPI document transformer factory is registered for schema '{name}'. "
+ "Call AddOpenApiAspNetCoreServices() when configuring the GraphQL server.");
var storage = storageFactory(_applicationServices);
var endpointDataSource = endpointDataSourceFactory();
var documentTransformer = documentTransformerFactory();

Copilot uses AI. Check for mistakes.
Comment on lines +18 to 22
var transformer = (DynamicOpenApiDocumentTransformer)manager
.Get(resolvedSchemaName)
.DocumentTransformer;

return transformer.TransformAsync(document, context, ct);
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AddGraphQLTransformer casts the registration's DocumentTransformer to DynamicOpenApiDocumentTransformer. Since the registration exposes it as IDynamicOpenApiDocumentTransformer, this cast is a runtime hazard if the transport setup ever changes (InvalidCastException). Prefer storing/exposing the transformer as IOpenApiDocumentTransformer (or a combined interface) so this code can call TransformAsync without a concrete cast.

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +63
private McpRegistration CreateRegistration(string name)
{
var executorProxy = new McpRequestExecutorProxy(
_applicationServices.GetRequiredService<IRequestExecutorProvider>(),
_applicationServices.GetRequiredService<IRequestExecutorEvents>(),
name);

var handlerProxy = new StreamableHttpHandlerProxy(executorProxy);

return new McpRegistration(executorProxy, handlerProxy);
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

McpManager creates McpRequestExecutorProxy (inherits RequestExecutorProxy and implements IDisposable) but never disposes it. This can leak the executor event subscription and CTS held by RequestExecutorProxy. Consider storing registrations as container-managed singletons (so DI disposes them), or make McpManager implement IDisposable and dispose created proxies when the ServiceProvider is disposed.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +26
var setup = services
.BuildServiceProvider()
.GetRequiredService<IOptionsMonitor<OpenApiSetup>>()
.Get(ISchemaDefinition.DefaultName);

// assert
Assert.NotNull(setup.StorageFactory);
Assert.Same(storage, setup.StorageFactory(null!));
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test invokes StorageFactory with null even though the delegate is typed as Func<IServiceProvider, ...>. Passing a real IServiceProvider here (e.g., the one built in this test) would better match runtime usage and avoid masking future StorageFactory implementations that rely on the provider.

Suggested change
var setup = services
.BuildServiceProvider()
.GetRequiredService<IOptionsMonitor<OpenApiSetup>>()
.Get(ISchemaDefinition.DefaultName);
// assert
Assert.NotNull(setup.StorageFactory);
Assert.Same(storage, setup.StorageFactory(null!));
var serviceProvider = services.BuildServiceProvider();
var setup = serviceProvider
.GetRequiredService<IOptionsMonitor<OpenApiSetup>>()
.Get(ISchemaDefinition.DefaultName);
// assert
Assert.NotNull(setup.StorageFactory);
Assert.Same(storage, setup.StorageFactory(serviceProvider));

Copilot uses AI. Check for mistakes.
Comment on lines +41 to +44
foreach (var modifier in setup.ServerModifiers)
{
modifier(null!);
}
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test calls each ServerModifier with null (modifier(null!)). This works for the current lambdas but can hide issues if a modifier starts using the IMcpServerBuilder parameter. Consider invoking modifiers with a minimal stub/mock IMcpServerBuilder instead so the test reflects real usage more closely.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants