Skip to content

Commit d23f442

Browse files
authored
Allow more accept headers (#429)
1 parent 8498097 commit d23f442

File tree

3 files changed

+42
-9
lines changed

3 files changed

+42
-9
lines changed

src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.AspNetCore.WebUtilities;
55
using Microsoft.Extensions.Logging;
66
using Microsoft.Extensions.Options;
7+
using Microsoft.Extensions.Primitives;
78
using Microsoft.Net.Http.Headers;
89
using ModelContextProtocol.AspNetCore.Stateless;
910
using ModelContextProtocol.Protocol;
@@ -28,9 +29,6 @@ internal sealed class StreamableHttpHandler(
2829
{
2930
private static readonly JsonTypeInfo<JsonRpcError> s_errorTypeInfo = GetRequiredJsonTypeInfo<JsonRpcError>();
3031

31-
private static readonly MediaTypeHeaderValue s_applicationJsonMediaType = new("application/json");
32-
private static readonly MediaTypeHeaderValue s_textEventStreamMediaType = new("text/event-stream");
33-
3432
public ConcurrentDictionary<string, HttpMcpSession<StreamableHttpServerTransport>> Sessions { get; } = new(StringComparer.Ordinal);
3533

3634
public HttpServerTransportOptions HttpServerTransportOptions => httpServerTransportOptions.Value;
@@ -43,8 +41,8 @@ public async Task HandlePostRequestAsync(HttpContext context)
4341
// ASP.NET Core Minimal APIs mostly try to stay out of the business of response content negotiation,
4442
// so we have to do this manually. The spec doesn't mandate that servers MUST reject these requests,
4543
// but it's probably good to at least start out trying to be strict.
46-
var acceptHeaders = context.Request.GetTypedHeaders().Accept;
47-
if (!acceptHeaders.Contains(s_applicationJsonMediaType) || !acceptHeaders.Contains(s_textEventStreamMediaType))
44+
var typedHeaders = context.Request.GetTypedHeaders();
45+
if (!typedHeaders.Accept.Any(MatchesApplicationJsonMediaType) || !typedHeaders.Accept.Any(MatchesTextEventStreamMediaType))
4846
{
4947
await WriteJsonRpcErrorAsync(context,
5048
"Not Acceptable: Client must accept both application/json and text/event-stream",
@@ -85,8 +83,7 @@ await WriteJsonRpcErrorAsync(context,
8583

8684
public async Task HandleGetRequestAsync(HttpContext context)
8785
{
88-
var acceptHeaders = context.Request.GetTypedHeaders().Accept;
89-
if (!acceptHeaders.Contains(s_textEventStreamMediaType))
86+
if (!context.Request.GetTypedHeaders().Accept.Any(MatchesTextEventStreamMediaType))
9087
{
9188
await WriteJsonRpcErrorAsync(context,
9289
"Not Acceptable: Client must accept text/event-stream",
@@ -331,6 +328,12 @@ internal static Task RunSessionAsync(HttpContext httpContext, IMcpServer session
331328

332329
private static JsonTypeInfo<T> GetRequiredJsonTypeInfo<T>() => (JsonTypeInfo<T>)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(T));
333330

331+
private static bool MatchesApplicationJsonMediaType(MediaTypeHeaderValue acceptHeaderValue)
332+
=> acceptHeaderValue.MatchesMediaType("application/json");
333+
334+
private static bool MatchesTextEventStreamMediaType(MediaTypeHeaderValue acceptHeaderValue)
335+
=> acceptHeaderValue.MatchesMediaType("text/event-stream");
336+
334337
private sealed class HttpDuplexPipe(HttpContext context) : IDuplexPipe
335338
{
336339
public PipeReader Input => context.Request.BodyReader;

src/ModelContextProtocol/Client/StreamableHttpClientSessionTransport.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public override async Task SendMessageAsync(
5959
using var content = new StringContent(
6060
JsonSerializer.Serialize(message, McpJsonUtilities.JsonContext.Default.JsonRpcMessage),
6161
Encoding.UTF8,
62-
"application/json"
62+
"application/json; charset=utf-8"
6363
);
6464
#endif
6565

tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public async Task PostRequest_IsUnsupportedMediaType_WithoutJsonContentType()
105105
[InlineData("text/event-stream")]
106106
[InlineData("application/json")]
107107
[InlineData("application/json-text/event-stream")]
108-
public async Task PostRequest_IsNotAcceptable_WithSingleAcceptHeader(string singleAcceptValue)
108+
public async Task PostRequest_IsNotAcceptable_WithSingleSpecificAcceptHeader(string singleAcceptValue)
109109
{
110110
await StartAsync();
111111

@@ -116,6 +116,20 @@ public async Task PostRequest_IsNotAcceptable_WithSingleAcceptHeader(string sing
116116
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
117117
}
118118

119+
[Theory]
120+
[InlineData("*/*")]
121+
[InlineData("text/event-stream, application/json;q=0.9")]
122+
public async Task PostRequest_IsAcceptable_WithWildcardOrAddedQualityInAcceptHeader(string acceptHeaderValue)
123+
{
124+
await StartAsync();
125+
126+
HttpClient.DefaultRequestHeaders.Accept.Clear();
127+
HttpClient.DefaultRequestHeaders.TryAddWithoutValidation(HeaderNames.Accept, acceptHeaderValue);
128+
129+
using var response = await HttpClient.PostAsync("", JsonContent(InitializeRequest), TestContext.Current.CancellationToken);
130+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
131+
}
132+
119133
[Fact]
120134
public async Task GetRequest_IsNotAcceptable_WithoutTextEventStreamAcceptHeader()
121135
{
@@ -128,6 +142,22 @@ public async Task GetRequest_IsNotAcceptable_WithoutTextEventStreamAcceptHeader(
128142
Assert.Equal(HttpStatusCode.NotAcceptable, response.StatusCode);
129143
}
130144

145+
[Theory]
146+
[InlineData("*/*")]
147+
[InlineData("application/json, text/event-stream;q=0.9")]
148+
public async Task GetRequest_IsAcceptable_WithWildcardOrAddedQualityInAcceptHeader(string acceptHeaderValue)
149+
{
150+
await StartAsync();
151+
152+
HttpClient.DefaultRequestHeaders.Accept.Clear();
153+
HttpClient.DefaultRequestHeaders.TryAddWithoutValidation(HeaderNames.Accept, acceptHeaderValue);
154+
155+
await CallInitializeAndValidateAsync();
156+
157+
using var response = await HttpClient.GetAsync("", HttpCompletionOption.ResponseHeadersRead, TestContext.Current.CancellationToken);
158+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
159+
}
160+
131161
[Fact]
132162
public async Task PostRequest_IsNotFound_WithUnrecognizedSessionId()
133163
{

0 commit comments

Comments
 (0)