Fix: Remove charset=utf-8 from Content-Type: application/json in HTTP transport (#1528)
This commit is contained in:
parent
af6fcff595
commit
a04c3e4f50
2 changed files with 53 additions and 2 deletions
|
|
@ -12,7 +12,7 @@ namespace ModelContextProtocol.Client;
|
||||||
|
|
||||||
internal class McpHttpClient(HttpClient httpClient)
|
internal class McpHttpClient(HttpClient httpClient)
|
||||||
{
|
{
|
||||||
internal static readonly MediaTypeHeaderValue s_applicationJsonContentType = new("application/json") { CharSet = "utf-8" };
|
internal static readonly MediaTypeHeaderValue s_applicationJsonContentType = new("application/json");
|
||||||
|
|
||||||
internal virtual async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, JsonRpcMessage? message, CancellationToken cancellationToken)
|
internal virtual async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, JsonRpcMessage? message, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
|
@ -32,7 +32,7 @@ internal class McpHttpClient(HttpClient httpClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NET
|
#if NET
|
||||||
return JsonContent.Create(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
|
return JsonContent.Create(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage, s_applicationJsonContentType);
|
||||||
#else
|
#else
|
||||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
|
var bytes = JsonSerializer.SerializeToUtf8Bytes(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage);
|
||||||
var content = new ByteArrayContent(bytes);
|
var content = new ByteArrayContent(bytes);
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,57 @@ public class HttpClientTransportTests : LoggedTest
|
||||||
Assert.False(transportBase.IsConnected);
|
Assert.False(transportBase.IsConnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 =>
|
||||||
|
(request) =>
|
||||||
|
{
|
||||||
|
if (request.Method == HttpMethod.Post)
|
||||||
|
{
|
||||||
|
var contentType = request.Content?.Headers.ContentType;
|
||||||
|
if (contentType?.CharSet is not null)
|
||||||
|
{
|
||||||
|
return Task.FromResult(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.UnsupportedMediaType,
|
||||||
|
Content = new StringContent("Content-Type must be 'application/json'"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(new HttpResponseMessage
|
||||||
|
{
|
||||||
|
StatusCode = HttpStatusCode.OK,
|
||||||
|
Content = new StringContent(
|
||||||
|
"""{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-03-26","capabilities":{},"serverInfo":{"name":"Test","version":"1.0"}}}""",
|
||||||
|
Encoding.UTF8,
|
||||||
|
"application/json"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException("Abort");
|
||||||
|
};
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SendMessageAsync_StrictServer_Returns200_WhenContentTypeIsApplicationJson()
|
||||||
|
{
|
||||||
|
// Regression test for https://github.com/modelcontextprotocol/csharp-sdk/issues/1527
|
||||||
|
// SDK must send bare "application/json" — no charset parameter.
|
||||||
|
var options = new HttpClientTransportOptions
|
||||||
|
{
|
||||||
|
Endpoint = new Uri("http://localhost:8080"),
|
||||||
|
TransportMode = HttpTransportMode.StreamableHttp,
|
||||||
|
};
|
||||||
|
|
||||||
|
using var mockHttpHandler = new MockHttpHandler();
|
||||||
|
using var httpClient = new HttpClient(mockHttpHandler);
|
||||||
|
await using var transport = new HttpClientTransport(options, httpClient, LoggerFactory);
|
||||||
|
mockHttpHandler.RequestHandler = StrictJsonContentTypeHandler;
|
||||||
|
|
||||||
|
// Succeeds only if the SDK sends Content-Type: application/json (no charset)
|
||||||
|
await using var session = await transport.ConnectAsync(TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(session);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task StreamableHttp_InitialGetSseConnection_DoesNotCountAgainstMaxReconnectionAttempts()
|
public async Task StreamableHttp_InitialGetSseConnection_DoesNotCountAgainstMaxReconnectionAttempts()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue