Restructure IMcpEndpoint, IMcpClient, and IMcpServer (#723)

* Remove `McpEndpoint` & `IMcpClient`

* Revise naming, add abstract classes

* Fix missing renames

* Move extension methods to instance methods

* Add back old API, but obsoleted

* Add back obsoleted factory classes

* Move deprecation attributes

* `SseClientTransport` -> `HttpClientTransport`

* Update src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs

Co-authored-by: Stephen Halter <halter73@gmail.com>

* PR feedback

* PR feedback

* Add back dispose lock

* Fix docs projects

* PR feedback

* Undo `docs/*.md` changes

---------

Co-authored-by: Stephen Halter <halter73@gmail.com>
This commit is contained in:
Mackinnon Buck 2025-09-16 13:22:10 -04:00 • committed by GitHub
commit 38b4a26998
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 5169 additions and 3786 deletions

View file

@ -106,7 +106,7 @@ public class AuthEventTests : KestrelInMemoryTest, IAsyncDisposable
await app.StartAsync(TestContext.Current.CancellationToken);
await using var transport = new SseClientTransport(
await using var transport = new HttpClientTransport(
new()
{
Endpoint = new(McpServerUrl),
@ -122,7 +122,7 @@ public class AuthEventTests : KestrelInMemoryTest, IAsyncDisposable
LoggerFactory
);
await using var client = await McpClientFactory.CreateAsync(
await using var client = await McpClient.CreateAsync(
transport,
loggerFactory: LoggerFactory,
cancellationToken: TestContext.Current.CancellationToken
@ -142,7 +142,7 @@ public class AuthEventTests : KestrelInMemoryTest, IAsyncDisposable
DynamicClientRegistrationResponse? dcrResponse = null;
await using var transport = new SseClientTransport(
await using var transport = new HttpClientTransport(
new()
{
Endpoint = new(McpServerUrl),
@ -167,7 +167,7 @@ public class AuthEventTests : KestrelInMemoryTest, IAsyncDisposable
LoggerFactory
);
await using var client = await McpClientFactory.CreateAsync(
await using var client = await McpClient.CreateAsync(
transport,
loggerFactory: LoggerFactory,
cancellationToken: TestContext.Current.CancellationToken

View file

@ -97,7 +97,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
await app.StartAsync(TestContext.Current.CancellationToken);
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new(McpServerUrl),
OAuth = new()
@ -109,7 +109,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
},
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(
await using var client = await McpClient.CreateAsync(
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
}
@ -124,12 +124,12 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
await app.StartAsync(TestContext.Current.CancellationToken);
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new(McpServerUrl),
}, HttpClient, LoggerFactory);
var httpEx = await Assert.ThrowsAsync<HttpRequestException>(async () => await McpClientFactory.CreateAsync(
var httpEx = await Assert.ThrowsAsync<HttpRequestException>(async () => await McpClient.CreateAsync(
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
Assert.Equal(HttpStatusCode.Unauthorized, httpEx.StatusCode);
@ -146,7 +146,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
await app.StartAsync(TestContext.Current.CancellationToken);
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new(McpServerUrl),
OAuth = new()
@ -159,7 +159,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
}, HttpClient, LoggerFactory);
// The EqualException is thrown by HandleAuthorizationUrlAsync when the /authorize request gets a 400
var equalEx = await Assert.ThrowsAsync<EqualException>(async () => await McpClientFactory.CreateAsync(
var equalEx = await Assert.ThrowsAsync<EqualException>(async () => await McpClient.CreateAsync(
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
}
@ -174,7 +174,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
await app.StartAsync(TestContext.Current.CancellationToken);
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new(McpServerUrl),
OAuth = new ClientOAuthOptions()
@ -190,7 +190,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
},
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(
await using var client = await McpClient.CreateAsync(
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
}
@ -205,7 +205,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
await app.StartAsync(TestContext.Current.CancellationToken);
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new(McpServerUrl),
OAuth = new()
@ -219,7 +219,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
// The test-refresh-client should get an expired token first,
// then automatically refresh it to get a working token
await using var client = await McpClientFactory.CreateAsync(
await using var client = await McpClient.CreateAsync(
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
Assert.True(_testOAuthServer.HasIssuedRefreshToken);
@ -236,7 +236,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
await app.StartAsync(TestContext.Current.CancellationToken);
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new(McpServerUrl),
OAuth = new()
@ -252,7 +252,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
},
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(
await using var client = await McpClient.CreateAsync(
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
Assert.NotNull(_lastAuthorizationUri?.Query);
@ -270,7 +270,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
await app.StartAsync(TestContext.Current.CancellationToken);
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new(McpServerUrl),
OAuth = new()
@ -286,7 +286,7 @@ public class AuthTests : KestrelInMemoryTest, IAsyncDisposable
},
}, HttpClient, LoggerFactory);
await Assert.ThrowsAsync<ArgumentException>(() => McpClientFactory.CreateAsync(
await Assert.ThrowsAsync<ArgumentException>(() => McpClient.CreateAsync(
transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken));
}

View file

@ -19,14 +19,14 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
{
private readonly MockLoggerProvider _mockLoggerProvider = new();
private async Task<IMcpClient> ConnectAsync()
private async Task<McpClient> ConnectAsync()
{
await using var transport = new SseClientTransport(new SseClientTransportOptions
await using var transport = new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new("http://localhost:5000"),
}, HttpClient, LoggerFactory);
return await McpClientFactory.CreateAsync(transport, cancellationToken: TestContext.Current.CancellationToken, loggerFactory: LoggerFactory);
return await McpClient.CreateAsync(transport, cancellationToken: TestContext.Current.CancellationToken, loggerFactory: LoggerFactory);
}
[Fact]
@ -447,7 +447,7 @@ public class AuthorizeAttributeTests(ITestOutputHelper testOutputHelper) : Kestr
private ClaimsPrincipal CreateUser(string name, params string[] roles)
=> new ClaimsPrincipal(new ClaimsIdentity(
[new Claim("name", name), new Claim(ClaimTypes.NameIdentifier, name), ..roles.Select(role => new Claim("role", role))],
[new Claim("name", name), new Claim(ClaimTypes.NameIdentifier, name), .. roles.Select(role => new Claim("role", role))],
"TestAuthType", "name", "role"));
[McpServerToolType]

View file

@ -21,9 +21,9 @@ public abstract class HttpServerIntegrationTests : LoggedTest, IClassFixture<Sse
base.Dispose();
}
protected abstract SseClientTransportOptions ClientTransportOptions { get; }
protected abstract HttpClientTransportOptions ClientTransportOptions { get; }
private Task<IMcpClient> GetClientAsync(McpClientOptions? options = null)
private Task<McpClient> GetClientAsync(McpClientOptions? options = null)
{
return _fixture.ConnectMcpClientAsync(options, LoggerFactory);
}

View file

@ -23,21 +23,21 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
options.Stateless = Stateless;
}
protected async Task<IMcpClient> ConnectAsync(
protected async Task<McpClient> ConnectAsync(
string? path = null,
SseClientTransportOptions? transportOptions = null,
HttpClientTransportOptions? transportOptions = null,
McpClientOptions? clientOptions = null)
{
// Default behavior when no options are provided
path ??= UseStreamableHttp ? "/" : "/sse";
await using var transport = new SseClientTransport(transportOptions ?? new SseClientTransportOptions
await using var transport = new HttpClientTransport(transportOptions ?? new HttpClientTransportOptions
{
Endpoint = new Uri($"http://localhost:5000{path}"),
TransportMode = UseStreamableHttp ? HttpTransportMode.StreamableHttp : HttpTransportMode.Sse,
}, HttpClient, LoggerFactory);
return await McpClientFactory.CreateAsync(transport, clientOptions, LoggerFactory, TestContext.Current.CancellationToken);
return await McpClient.CreateAsync(transport, clientOptions, LoggerFactory, TestContext.Current.CancellationToken);
}
[Fact]
@ -244,7 +244,7 @@ public abstract class MapMcpTests(ITestOutputHelper testOutputHelper) : KestrelI
private class SamplingRegressionTools
{
[McpServerTool(Name = "sampling-tool")]
public static async Task<string> SamplingToolAsync(IMcpServer server, string prompt, CancellationToken cancellationToken)
public static async Task<string> SamplingToolAsync(McpServer server, string prompt, CancellationToken cancellationToken)
{
// This tool reproduces the scenario described in https://github.com/modelcontextprotocol/csharp-sdk/issues/464
// 1. The client calls tool with request ID 2, because it's the first request after the initialize request.

View file

@ -15,15 +15,15 @@ namespace ModelContextProtocol.AspNetCore.Tests;
public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : KestrelInMemoryTest(outputHelper)
{
private readonly SseClientTransportOptions DefaultTransportOptions = new()
private readonly HttpClientTransportOptions DefaultTransportOptions = new()
{
Endpoint = new("http://localhost:5000/sse"),
Name = "In-memory SSE Client",
};
private Task<IMcpClient> ConnectMcpClientAsync(HttpClient? httpClient = null, SseClientTransportOptions? transportOptions = null)
=> McpClientFactory.CreateAsync(
new SseClientTransport(transportOptions ?? DefaultTransportOptions, httpClient ?? HttpClient, LoggerFactory),
private Task<McpClient> ConnectMcpClientAsync(HttpClient? httpClient = null, HttpClientTransportOptions? transportOptions = null)
=> McpClient.CreateAsync(
new HttpClientTransport(transportOptions ?? DefaultTransportOptions, httpClient ?? HttpClient, LoggerFactory),
loggerFactory: LoggerFactory,
cancellationToken: TestContext.Current.CancellationToken);
@ -195,7 +195,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
app.MapMcp();
await app.StartAsync(TestContext.Current.CancellationToken);
var sseOptions = new SseClientTransportOptions
var sseOptions = new HttpClientTransportOptions
{
Endpoint = new("http://localhost:5000/sse"),
Name = "In-memory SSE Client",
@ -222,7 +222,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
app.MapMcp();
await app.StartAsync(TestContext.Current.CancellationToken);
var sseOptions = new SseClientTransportOptions
var sseOptions = new HttpClientTransportOptions
{
Endpoint = new("http://localhost:5000/sse"),
Name = "In-memory SSE Client",
@ -257,7 +257,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr
try
{
var transportTask = transport.RunAsync(cancellationToken: requestAborted);
await using var server = McpServerFactory.Create(transport, optionsSnapshot.Value, loggerFactory, endpoints.ServiceProvider);
await using var server = McpServer.Create(transport, optionsSnapshot.Value, loggerFactory, endpoints.ServiceProvider);
try
{

View file

@ -18,7 +18,7 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
// multiple tests, so this dispatches the output to the current test.
private readonly DelegatingTestOutputHelper _delegatingTestOutputHelper = new();
private SseClientTransportOptions DefaultTransportOptions { get; set; } = new()
private HttpClientTransportOptions DefaultTransportOptions { get; set; } = new()
{
Endpoint = new("http://localhost:5000/"),
};
@ -44,16 +44,16 @@ public class SseServerIntegrationTestFixture : IAsyncDisposable
public HttpClient HttpClient { get; }
public Task<IMcpClient> ConnectMcpClientAsync(McpClientOptions? options, ILoggerFactory loggerFactory)
public Task<McpClient> ConnectMcpClientAsync(McpClientOptions? options, ILoggerFactory loggerFactory)
{
return McpClientFactory.CreateAsync(
new SseClientTransport(DefaultTransportOptions, HttpClient, loggerFactory),
return McpClient.CreateAsync(
new HttpClientTransport(DefaultTransportOptions, HttpClient, loggerFactory),
options,
loggerFactory,
TestContext.Current.CancellationToken);
}
public void Initialize(ITestOutputHelper output, SseClientTransportOptions clientTransportOptions)
public void Initialize(ITestOutputHelper output, HttpClientTransportOptions clientTransportOptions)
{
_delegatingTestOutputHelper.CurrentTestOutputHelper = output;
DefaultTransportOptions = clientTransportOptions;

View file

@ -8,7 +8,7 @@ public class SseServerIntegrationTests(SseServerIntegrationTestFixture fixture,
: HttpServerIntegrationTests(fixture, testOutputHelper)
{
protected override SseClientTransportOptions ClientTransportOptions => new()
protected override HttpClientTransportOptions ClientTransportOptions => new()
{
Endpoint = new("http://localhost:5000/sse"),
Name = "In-memory SSE Client",

View file

@ -5,7 +5,7 @@ namespace ModelContextProtocol.AspNetCore.Tests;
public class StatelessServerIntegrationTests(SseServerIntegrationTestFixture fixture, ITestOutputHelper testOutputHelper)
: StreamableHttpServerIntegrationTests(fixture, testOutputHelper)
{
protected override SseClientTransportOptions ClientTransportOptions => new()
protected override HttpClientTransportOptions ClientTransportOptions => new()
{
Endpoint = new("http://localhost:5000/stateless"),
Name = "In-memory Streamable HTTP Client",

View file

@ -14,7 +14,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
{
private WebApplication? _app;
private readonly SseClientTransportOptions DefaultTransportOptions = new()
private readonly HttpClientTransportOptions DefaultTransportOptions = new()
{
Endpoint = new("http://localhost:5000/"),
Name = "In-memory Streamable HTTP Client",
@ -58,9 +58,9 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
HttpClient.DefaultRequestHeaders.Accept.Add(new("text/event-stream"));
}
private Task<IMcpClient> ConnectMcpClientAsync(McpClientOptions? clientOptions = null)
=> McpClientFactory.CreateAsync(
new SseClientTransport(DefaultTransportOptions, HttpClient, LoggerFactory),
private Task<McpClient> ConnectMcpClientAsync(McpClientOptions? clientOptions = null)
=> McpClient.CreateAsync(
new HttpClientTransport(DefaultTransportOptions, HttpClient, LoggerFactory),
clientOptions, LoggerFactory, TestContext.Current.CancellationToken);
public async ValueTask DisposeAsync()
@ -194,7 +194,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
}
[McpServerTool(Name = "testSamplingErrors")]
public static async Task<string> TestSamplingErrors(IMcpServer server)
public static async Task<string> TestSamplingErrors(McpServer server)
{
const string expectedSamplingErrorMessage = "Sampling is not supported in stateless mode.";
@ -212,7 +212,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
}
[McpServerTool(Name = "testRootsErrors")]
public static async Task<string> TestRootsErrors(IMcpServer server)
public static async Task<string> TestRootsErrors(McpServer server)
{
const string expectedRootsErrorMessage = "Roots are not supported in stateless mode.";
@ -227,7 +227,7 @@ public class StatelessServerTests(ITestOutputHelper outputHelper) : KestrelInMem
}
[McpServerTool(Name = "testElicitationErrors")]
public static async Task<string> TestElicitationErrors(IMcpServer server)
public static async Task<string> TestElicitationErrors(McpServer server)
{
const string expectedElicitationErrorMessage = "Elicitation is not supported in stateless mode.";

View file

@ -112,13 +112,13 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
{
await StartAsync();
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new("http://localhost:5000/mcp"),
TransportMode = HttpTransportMode.StreamableHttp,
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
await using var client = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
var echoTool = Assert.Single(tools);
@ -132,13 +132,13 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
{
await StartAsync();
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new("http://localhost:5000/mcp"),
TransportMode = HttpTransportMode.StreamableHttp,
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
await using var client = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken);
var echoTool = Assert.Single(tools);
@ -158,13 +158,13 @@ public class StreamableHttpClientConformanceTests(ITestOutputHelper outputHelper
{
await StartAsync(enableDelete: true);
await using var transport = new SseClientTransport(new()
await using var transport = new HttpClientTransport(new()
{
Endpoint = new("http://localhost:5000/mcp"),
TransportMode = HttpTransportMode.StreamableHttp,
}, HttpClient, LoggerFactory);
await using var client = await McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
await using var client = await McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken);
// Dispose should trigger DELETE request
await client.DisposeAsync();

View file

@ -252,7 +252,7 @@ public class StreamableHttpServerConformanceTests(ITestOutputHelper outputHelper
[Fact]
public async Task GetRequest_Receives_UnsolicitedNotifications()
{
IMcpServer? server = null;
McpServer? server = null;
Builder.Services.AddMcpServer()
.WithHttpTransport(options =>

View file

@ -11,7 +11,7 @@ public class StreamableHttpServerIntegrationTests(SseServerIntegrationTestFixtur
{"jsonrpc":"2.0","id":"1","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"IntegrationTestClient","version":"1.0.0"}}}
""";
protected override SseClientTransportOptions ClientTransportOptions => new()
protected override HttpClientTransportOptions ClientTransportOptions => new()
{
Endpoint = new("http://localhost:5000/"),
Name = "In-memory Streamable HTTP Client",