Skip to content
Open
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 @@ -167,14 +167,22 @@ private void ProcessPayload(JsonElement root)
{
foreach (var element in incremental.EnumerateArray())
{
if (!element.TryGetProperty(IdProp, out var idElement))
{
continue;
}
JsonElement basePath;

var id = idElement.GetString()!;
if (element.TryGetProperty(IdProp, out var idElement))
{
var id = idElement.GetString()!;

if (!_pendingPaths.TryGetValue(id, out var basePath))
if (!_pendingPaths.TryGetValue(id, out basePath))
{
continue;
}
}
else if (element.TryGetProperty(PathProp, out basePath))
{
// legacy format uses "path" directly without "id"/"pending"
}
else
{
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,78 @@ internal sealed class GraphQLHttpResponseFormatter : SnapshotValueFormatter<Grap
protected override void Format(IBufferWriter<byte> snapshot, GraphQLHttpResponse value)
{
var contentType = value.ContentHeaders.ContentType;
var mediaType = contentType?.MediaType;

if (string.Equals(contentType?.MediaType, "multipart/mixed", StringComparison.Ordinal))
if (string.Equals(mediaType, "multipart/mixed", StringComparison.Ordinal))
{
var boundary = contentType!.Parameters.First(
t => string.Equals(t.Name, "boundary", StringComparison.Ordinal));
FormatStreamAsync(snapshot, boundary, value.HttpResponseMessage.Content.ReadAsStream()).Wait();
}
else if (string.Equals(mediaType, "application/jsonl", StringComparison.Ordinal)
|| string.Equals(mediaType, "application/graphql-response+jsonl", StringComparison.Ordinal)
|| string.Equals(mediaType, "text/event-stream", StringComparison.Ordinal))
{
FormatResultStreamAsync(snapshot, value).Wait();
}
}

private static async Task FormatResultStreamAsync(
IBufferWriter<byte> snapshot,
GraphQLHttpResponse response)
{
var content = await response.HttpResponseMessage.Content.ReadAsStringAsync();
var docs = new List<JsonDocument>();
var patcher = new JsonResultPatcher();
var first = true;

try
{
foreach (var line in content.Split('\n'))
{
var trimmed = line.Trim();

// skip empty lines, SSE field names, and keep-alive comments
if (trimmed.Length == 0
|| trimmed.StartsWith("event:", StringComparison.Ordinal)
|| trimmed.StartsWith(":", StringComparison.Ordinal))
{
continue;
}

// strip SSE "data: " prefix if present
var json = trimmed.StartsWith("data:", StringComparison.Ordinal)
? trimmed["data:".Length..].Trim()
: trimmed;

if (json.Length == 0 || json[0] != '{')
{
continue;
}

var doc = JsonDocument.Parse(json);
docs.Add(doc);

if (first)
{
patcher.SetResponse(doc);
first = false;
}
else
{
patcher.ApplyPatch(doc);
}
}

patcher.WriteResponse(snapshot);
}
finally
{
foreach (var doc in docs)
{
doc.Dispose();
}
}
}

private static async Task FormatStreamAsync(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
using HotChocolate.Types.Descriptors;
using HotChocolate.Types.Descriptors.Configurations;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
Expand All @@ -30,19 +30,18 @@ await storage.AddOrUpdateToolAsync(
Utf8GraphQLParser.Parse(
await File.ReadAllTextAsync("__resources__/GetSingleField.graphql"))));
var typeModule = new TestTypeModule();
var builder = new WebHostBuilder()
.ConfigureServices(
services => services
.AddRouting()
.AddGraphQL()
.AddTypeModule(_ => typeModule)
.AddMcp()
.AddMcpStorage(storage))
.Configure(
app => app
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapGraphQLMcp()));
var server = new TestServer(builder);
var webAppBuilder = WebApplication.CreateSlimBuilder();
webAppBuilder.WebHost.UseTestServer();
webAppBuilder.Services
.AddRouting()
.AddGraphQL()
.AddTypeModule(_ => typeModule)
.AddMcp()
.AddMcpStorage(storage);
var webApp = webAppBuilder.Build();
webApp.MapGraphQLMcp();
webApp.Start();
var server = webApp.GetTestServer();
var mcpClient1 = await CreateMcpClientAsync(server.CreateClient());
var mcpClient2 = await CreateMcpClientAsync(server.CreateClient());
var listChangedResetEvent1 = new ManualResetEventSlim(false);
Expand Down Expand Up @@ -101,53 +100,51 @@ protected override Task<TestServer> CreateTestServerAsync(
Action<McpServerOptions>? configureMcpServerOptions = null,
Action<IMcpServerBuilder>? configureMcpServer = null)
{
var builder = new WebHostBuilder()
.ConfigureServices(
services =>
{
services
.AddLogging()
.AddRouting()
.AddAuthentication()
.AddJwtBearer(
o => o.TokenValidationParameters =
new TokenValidationParameters
{
ValidIssuer = TokenIssuer,
ValidAudience = TokenAudience,
IssuerSigningKey = TokenKey
});

var builder =
services
.AddGraphQL()
.AddAuthorization()
.AddMcp(configureMcpServerOptions, configureMcpServer)
.AddMcpStorage(storage)
.AddQueryType<TestSchema.Query>()
.AddMutationType<TestSchema.Mutation>()
.AddInterfaceType<TestSchema.IPet>()
.AddUnionType<TestSchema.IPet>()
.AddObjectType<TestSchema.Cat>()
.AddObjectType<TestSchema.Dog>();

if (additionalTypes is not null)
var webAppBuilder = WebApplication.CreateSlimBuilder();
webAppBuilder.WebHost.UseTestServer();
webAppBuilder.Services
.AddLogging()
.AddRouting()
.AddAuthentication()
.AddJwtBearer(
o => o.TokenValidationParameters =
new TokenValidationParameters
{
builder.AddTypes(additionalTypes);
}
ValidIssuer = TokenIssuer,
ValidAudience = TokenAudience,
IssuerSigningKey = TokenKey
});

var graphqlBuilder =
webAppBuilder.Services
.AddGraphQL()
.AddAuthorization()
.AddMcp(configureMcpServerOptions, configureMcpServer)
.AddMcpStorage(storage)
.AddQueryType<TestSchema.Query>()
.AddMutationType<TestSchema.Mutation>()
.AddInterfaceType<TestSchema.IPet>()
.AddUnionType<TestSchema.IPet>()
.AddObjectType<TestSchema.Cat>()
.AddObjectType<TestSchema.Dog>();

if (additionalTypes is not null)
{
graphqlBuilder.AddTypes(additionalTypes);
}

if (diagnosticEventListener is not null)
{
builder.AddDiagnosticEventListener(_ => diagnosticEventListener);
}
})
.Configure(
app => app
.UseRouting()
.UseAuthentication()
.UseEndpoints(endpoints => endpoints.MapGraphQLMcp()));

return Task.FromResult(new TestServer(builder));
if (diagnosticEventListener is not null)
{
graphqlBuilder.AddDiagnosticEventListener(_ => diagnosticEventListener);
}

var app = webAppBuilder.Build();
app.UseRouting();
app.UseAuthentication();
app.MapGraphQLMcp();
app.Start();

return Task.FromResult(app.GetTestServer());
}

private sealed class TestTypeModule : TypeModule
Expand Down
Loading
Loading