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
13 changes: 4 additions & 9 deletions src/ModelContextProtocol.Core/Client/McpHttpClient.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
using ModelContextProtocol.Protocol;
using System.Diagnostics;
using System.Net.Http.Headers;

#if NET
using System.Net.Http.Json;
#else
using System.Text.Json;
#endif

namespace ModelContextProtocol.Client;

Expand All @@ -31,13 +26,13 @@ internal virtual async Task<HttpResponseMessage> SendAsync(HttpRequestMessage re
return null;
}

#if NET
return JsonContent.Create(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage, s_applicationJsonContentType);
#else
// Buffer the serialized message so the request carries a Content-Length rather than
// being streamed with Transfer-Encoding: chunked. JsonContent serializes lazily and
// cannot report a length, so HttpClient falls back to chunked encoding, which some
// hosts reject outright (for example the local Azure Functions Python worker).
var bytes = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
var content = new ByteArrayContent(bytes);
content.Headers.ContentType = s_applicationJsonContentType;
return content;
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,69 @@ public async Task DisposeAsync_Should_Dispose_Resources()
Assert.False(transportBase.IsConnected);
}

[Fact]
public async Task SendMessageAsync_PostsBodyWithContentLength_InsteadOfChunkedEncoding()
{
// Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/932
// JsonContent serializes lazily and cannot report a length, so HttpClient falls back to
// Transfer-Encoding: chunked. Hosts reject that outright -- the local Azure Functions
// Python worker answers 400 "'Transfer-Encoding: chunked' header can not be used when
// content object is not specified" -- so the body must be buffered and sent with a
// Content-Length.
var options = new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost:8080/mcp"),
TransportMode = HttpTransportMode.StreamableHttp,
};

using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new HttpClientTransport(options, httpClient, LoggerFactory);

HttpRequestMessage? postRequest = null;
long? declaredLength = null;
bool? transferEncodingChunked = null;
byte[]? bodyBytes = null;

// Observed from the handler, which is where the request is still live: McpHttpClient
// disposes the content once SendAsync returns, so the values have to be captured here.
mockHttpHandler.RequestHandler = async request =>
{
Assert.Equal(HttpMethod.Post, request.Method);
Assert.NotNull(request.Content);

postRequest = request;
declaredLength = request.Content.Headers.ContentLength;
transferEncodingChunked = request.Headers.TransferEncodingChunked;
bodyBytes = await request.Content.ReadAsByteArrayAsync(TestContext.Current.CancellationToken);

return new HttpResponseMessage
{
StatusCode = HttpStatusCode.Accepted,
Content = new StringContent("", Encoding.UTF8, "application/json"),
};
};

await using var session = await transport.ConnectAsync(TestContext.Current.CancellationToken);
await session.SendMessageAsync(
new JsonRpcNotification { Method = "notifications/initialized" },
TestContext.Current.CancellationToken);

Assert.NotNull(postRequest);

// A known length is what keeps HttpClient on the Content-Length path. With JsonContent
// this is null and the body goes out chunked instead.
Assert.True(
declaredLength.HasValue && declaredLength.Value > 0,
$"the POST body must declare a Content-Length; got {declaredLength?.ToString() ?? "null"}");
Assert.NotEqual(true, transferEncodingChunked);

// The declared length has to describe the bytes actually sent.
Assert.NotNull(bodyBytes);
Assert.Equal(declaredLength.GetValueOrDefault(), (long)bodyBytes.Length);
Assert.Contains("notifications/initialized", Encoding.UTF8.GetString(bodyBytes));
}

// Strict server mock used in Content-Type tests below.
// Returns 200 only for bare "application/json", otherwise 415.
private static Func<HttpRequestMessage, Task<HttpResponseMessage>> StrictJsonContentTypeHandler =>
Expand Down