-
Notifications
You must be signed in to change notification settings - Fork 676
Expand file tree
/
Copy pathHttpClientTransportAutoDetectTests.cs
More file actions
162 lines (136 loc) · 6.29 KB
/
HttpClientTransportAutoDetectTests.cs
File metadata and controls
162 lines (136 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
using ModelContextProtocol.Client;
using ModelContextProtocol.Tests.Utils;
using System.Net;
namespace ModelContextProtocol.Tests.Transport;
public class HttpClientTransportAutoDetectTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper)
{
[Fact]
public async Task AutoDetectMode_UsesStreamableHttp_WhenServerSupportsIt()
{
var options = new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost"),
TransportMode = HttpTransportMode.AutoDetect,
Name = "AutoDetect test client"
};
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new HttpClientTransport(options, httpClient, LoggerFactory);
// Simulate successful Streamable HTTP response for initialize
mockHttpHandler.RequestHandler = (request) =>
{
if (request.Method == HttpMethod.Post)
{
return Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("{\"jsonrpc\":\"2.0\",\"id\":\"init-id\",\"result\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{\"tools\":{}}}}"),
Headers =
{
{ "Content-Type", "application/json" },
{ "mcp-session-id", "test-session" }
}
});
}
// Shouldn't reach here for successful Streamable HTTP
throw new InvalidOperationException("Unexpected request");
};
await using var session = await transport.ConnectAsync(TestContext.Current.CancellationToken);
// The auto-detecting transport should be returned
Assert.NotNull(session);
}
[Theory]
[InlineData(HttpStatusCode.Unauthorized)]
[InlineData(HttpStatusCode.Forbidden)]
public async Task AutoDetectMode_DoesNotFallBackToSse_OnAuthError(HttpStatusCode authStatusCode)
{
// Auth errors (401, 403) are not transport-related — the server understood the
// request but rejected the credentials. The SDK should propagate the error
// immediately instead of falling back to SSE, which would mask the real cause.
var options = new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost"),
TransportMode = HttpTransportMode.AutoDetect,
Name = "AutoDetect test client"
};
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new HttpClientTransport(options, httpClient, LoggerFactory);
var requestMethods = new List<HttpMethod>();
mockHttpHandler.RequestHandler = (request) =>
{
requestMethods.Add(request.Method);
if (request.Method == HttpMethod.Post)
{
// Streamable HTTP POST returns auth error
return Task.FromResult(new HttpResponseMessage
{
StatusCode = authStatusCode,
Content = new StringContent($"{{\"error\": \"{authStatusCode}\"}}")
});
}
// SSE GET should never be reached
throw new InvalidOperationException("Should not fall back to SSE on auth error");
};
// ConnectAsync for AutoDetect mode just creates the transport without sending
// any HTTP request. The auto-detection is triggered lazily by the first
// SendMessageAsync call, which happens inside McpClient.CreateAsync when it
// sends the JSON-RPC "initialize" message.
var ex = await Assert.ThrowsAsync<HttpRequestException>(
() => McpClient.CreateAsync(transport, cancellationToken: TestContext.Current.CancellationToken));
Assert.Equal(authStatusCode, ex.StatusCode);
// Verify only POST was sent — no GET fallback
Assert.Single(requestMethods);
Assert.Equal(HttpMethod.Post, requestMethods[0]);
}
[Fact]
public async Task AutoDetectMode_FallsBackToSse_WhenStreamableHttpFails()
{
var options = new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost"),
TransportMode = HttpTransportMode.AutoDetect,
Name = "AutoDetect test client"
};
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new HttpClientTransport(options, httpClient, LoggerFactory);
var requestCount = 0;
mockHttpHandler.RequestHandler = (request) =>
{
requestCount++;
if (request.Method == HttpMethod.Post && requestCount == 1)
{
// First POST (Streamable HTTP) fails
return Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.NotFound,
Content = new StringContent("Streamable HTTP not supported")
});
}
if (request.Method == HttpMethod.Get)
{
// SSE connection request
return Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("event: endpoint\r\ndata: /sse-endpoint\r\n\r\n"),
Headers = { { "Content-Type", "text/event-stream" } }
});
}
if (request.Method == HttpMethod.Post && requestCount > 1)
{
// Subsequent POST to SSE endpoint succeeds
return Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent("accepted")
});
}
throw new InvalidOperationException($"Unexpected request: {request.Method}, count: {requestCount}");
};
await using var session = await transport.ConnectAsync(TestContext.Current.CancellationToken);
// The auto-detecting transport should be returned
Assert.NotNull(session);
}
}