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
115 changed files with 5169 additions and 3786 deletions

View file

@ -37,8 +37,8 @@ dotnet add package ModelContextProtocol --prerelease
## Getting Started (Client) ## Getting Started (Client)
To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `IMcpClient` To get started writing a client, the `McpClient.CreateAsync` method is used to instantiate and connect an `McpClient`
to a server. Once you have an `IMcpClient`, you can interact with it, such as to enumerate all available tools and invoke tools. to a server. Once you have an `McpClient`, you can interact with it, such as to enumerate all available tools and invoke tools.
```csharp ```csharp
var clientTransport = new StdioClientTransport(new StdioClientTransportOptions var clientTransport = new StdioClientTransport(new StdioClientTransportOptions
@ -48,7 +48,7 @@ var clientTransport = new StdioClientTransport(new StdioClientTransportOptions
Arguments = ["-y", "@modelcontextprotocol/server-everything"], Arguments = ["-y", "@modelcontextprotocol/server-everything"],
}); });
var client = await McpClientFactory.CreateAsync(clientTransport); var client = await McpClient.CreateAsync(clientTransport);
// Print the list of tools available from the server. // Print the list of tools available from the server.
foreach (var tool in await client.ListToolsAsync()) foreach (var tool in await client.ListToolsAsync())
@ -122,14 +122,14 @@ public static class EchoTool
} }
``` ```
Tools can have the `IMcpServer` representing the server injected via a parameter to the method, and can use that for interaction with Tools can have the `McpServer` representing the server injected via a parameter to the method, and can use that for interaction with
the connected client. Similarly, arguments may be injected via dependency injection. For example, this tool will use the supplied the connected client. Similarly, arguments may be injected via dependency injection. For example, this tool will use the supplied
`IMcpServer` to make sampling requests back to the client in order to summarize content it downloads from the specified url via `McpServer` to make sampling requests back to the client in order to summarize content it downloads from the specified url via
an `HttpClient` injected via dependency injection. an `HttpClient` injected via dependency injection.
```csharp ```csharp
[McpServerTool(Name = "SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")] [McpServerTool(Name = "SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")]
public static async Task<string> SummarizeDownloadedContent( public static async Task<string> SummarizeDownloadedContent(
IMcpServer thisServer, McpServer thisServer,
HttpClient httpClient, HttpClient httpClient,
[Description("The url from which to download the content to summarize")] string url, [Description("The url from which to download the content to summarize")] string url,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@ -224,7 +224,7 @@ McpServerOptions options = new()
}, },
}; };
await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("MyServer"), options); await using McpServer server = McpServer.Create(new StdioServerTransport("MyServer"), options);
await server.RunAsync(); await server.RunAsync();
``` ```

View file

@ -4,7 +4,7 @@ using ModelContextProtocol.Protocol;
var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001"; var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001";
var clientTransport = new SseClientTransport(new() var clientTransport = new HttpClientTransport(new()
{ {
Endpoint = new Uri(endpoint), Endpoint = new Uri(endpoint),
TransportMode = HttpTransportMode.StreamableHttp, TransportMode = HttpTransportMode.StreamableHttp,
@ -27,7 +27,7 @@ McpClientOptions options = new()
} }
}; };
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport, options); await using var mcpClient = await McpClient.CreateAsync(clientTransport, options);
// </snippet_McpInitialize> // </snippet_McpInitialize>
var tools = await mcpClient.ListToolsAsync(); var tools = await mcpClient.ListToolsAsync();

View file

@ -13,7 +13,7 @@ public sealed class InteractiveTools
// <snippet_GuessTheNumber> // <snippet_GuessTheNumber>
[McpServerTool, Description("A simple game where the user has to guess a number between 1 and 10.")] [McpServerTool, Description("A simple game where the user has to guess a number between 1 and 10.")]
public async Task<string> GuessTheNumber( public async Task<string> GuessTheNumber(
IMcpServer server, // Get the McpServer from DI container McpServer server, // Get the McpServer from DI container
CancellationToken token CancellationToken token
) )
{ {

View file

@ -4,13 +4,13 @@ using System.Text.Json;
var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001"; var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001";
var clientTransport = new SseClientTransport(new() var clientTransport = new HttpClientTransport(new()
{ {
Endpoint = new Uri(endpoint), Endpoint = new Uri(endpoint),
TransportMode = HttpTransportMode.StreamableHttp, TransportMode = HttpTransportMode.StreamableHttp,
}); });
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport); await using var mcpClient = await McpClient.CreateAsync(clientTransport);
// <snippet_LoggingCapabilities> // <snippet_LoggingCapabilities>
// Verify that the server supports logging // Verify that the server supports logging

View file

@ -5,7 +5,7 @@ using ModelContextProtocol.Protocol;
var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001"; var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001";
var clientTransport = new SseClientTransport(new() var clientTransport = new HttpClientTransport(new()
{ {
Endpoint = new Uri(endpoint), Endpoint = new Uri(endpoint),
TransportMode = HttpTransportMode.StreamableHttp, TransportMode = HttpTransportMode.StreamableHttp,
@ -20,7 +20,7 @@ McpClientOptions options = new()
} }
}; };
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport, options); await using var mcpClient = await McpClient.CreateAsync(clientTransport, options);
var tools = await mcpClient.ListToolsAsync(); var tools = await mcpClient.ListToolsAsync();
foreach (var tool in tools) foreach (var tool in tools)

View file

@ -10,7 +10,7 @@ public class LongRunningTools
{ {
[McpServerTool, Description("Demonstrates a long running tool with progress updates")] [McpServerTool, Description("Demonstrates a long running tool with progress updates")]
public static async Task<string> LongRunningTool( public static async Task<string> LongRunningTool(
IMcpServer server, McpServer server,
RequestContext<CallToolRequestParams> context, RequestContext<CallToolRequestParams> context,
int duration = 10, int duration = 10,
int steps = 5) int steps = 5)

View file

@ -12,7 +12,7 @@ public sealed class SampleLlmTool
{ {
[McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
public static async Task<string> SampleLLM( public static async Task<string> SampleLLM(
IMcpServer thisServer, McpServer thisServer,
[Description("The prompt to send to the LLM")] string prompt, [Description("The prompt to send to the LLM")] string prompt,
[Description("Maximum number of tokens to generate")] int maxTokens, [Description("Maximum number of tokens to generate")] int maxTokens,
CancellationToken cancellationToken) CancellationToken cancellationToken)

View file

@ -32,7 +32,7 @@ using IChatClient samplingClient = openAIClient.AsIChatClient()
.UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true) .UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true)
.Build(); .Build();
var mcpClient = await McpClientFactory.CreateAsync( var mcpClient = await McpClient.CreateAsync(
new StdioClientTransport(new() new StdioClientTransport(new()
{ {
Command = "npx", Command = "npx",

View file

@ -5,7 +5,7 @@ using ModelContextProtocol.Server;
namespace EverythingServer; namespace EverythingServer;
public class LoggingUpdateMessageSender(IMcpServer server, Func<LoggingLevel> getMinLevel) : BackgroundService public class LoggingUpdateMessageSender(McpServer server, Func<LoggingLevel> getMinLevel) : BackgroundService
{ {
readonly Dictionary<LoggingLevel, string> _loggingLevelMap = new() readonly Dictionary<LoggingLevel, string> _loggingLevelMap = new()
{ {

View file

@ -2,7 +2,7 @@
using ModelContextProtocol; using ModelContextProtocol;
using ModelContextProtocol.Server; using ModelContextProtocol.Server;
internal class SubscriptionMessageSender(IMcpServer server, HashSet<string> subscriptions) : BackgroundService internal class SubscriptionMessageSender(McpServer server, HashSet<string> subscriptions) : BackgroundService
{ {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{ {

View file

@ -10,7 +10,7 @@ public class LongRunningTool
{ {
[McpServerTool(Name = "longRunningOperation"), Description("Demonstrates a long running operation with progress updates")] [McpServerTool(Name = "longRunningOperation"), Description("Demonstrates a long running operation with progress updates")]
public static async Task<string> LongRunningOperation( public static async Task<string> LongRunningOperation(
IMcpServer server, McpServer server,
RequestContext<CallToolRequestParams> context, RequestContext<CallToolRequestParams> context,
int duration = 10, int duration = 10,
int steps = 5) int steps = 5)

View file

@ -9,7 +9,7 @@ public class SampleLlmTool
{ {
[McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
public static async Task<string> SampleLLM( public static async Task<string> SampleLLM(
IMcpServer server, McpServer server,
[Description("The prompt to send to the LLM")] string prompt, [Description("The prompt to send to the LLM")] string prompt,
[Description("Maximum number of tokens to generate")] int maxTokens, [Description("Maximum number of tokens to generate")] int maxTokens,
CancellationToken cancellationToken) CancellationToken cancellationToken)

View file

@ -6,7 +6,7 @@ using System.IO.Pipelines;
Pipe clientToServerPipe = new(), serverToClientPipe = new(); Pipe clientToServerPipe = new(), serverToClientPipe = new();
// Create a server using a stream-based transport over an in-memory pipe. // Create a server using a stream-based transport over an in-memory pipe.
await using IMcpServer server = McpServerFactory.Create( await using McpServer server = McpServer.Create(
new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()), new StreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()),
new McpServerOptions() new McpServerOptions()
{ {
@ -21,7 +21,7 @@ await using IMcpServer server = McpServerFactory.Create(
_ = server.RunAsync(); _ = server.RunAsync();
// Connect a client using a stream-based transport over the same in-memory pipe. // Connect a client using a stream-based transport over the same in-memory pipe.
await using IMcpClient client = await McpClientFactory.CreateAsync( await using McpClient client = await McpClient.CreateAsync(
new StreamClientTransport(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream())); new StreamClientTransport(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream()));
// List all tools. // List all tools.

View file

@ -25,7 +25,7 @@ var consoleLoggerFactory = LoggerFactory.Create(builder =>
builder.AddConsole(); builder.AddConsole();
}); });
var transport = new SseClientTransport(new() var transport = new HttpClientTransport(new()
{ {
Endpoint = new Uri(serverUrl), Endpoint = new Uri(serverUrl),
Name = "Secure Weather Client", Name = "Secure Weather Client",
@ -40,7 +40,7 @@ var transport = new SseClientTransport(new()
} }
}, httpClient, consoleLoggerFactory); }, httpClient, consoleLoggerFactory);
var client = await McpClientFactory.CreateAsync(transport, loggerFactory: consoleLoggerFactory); var client = await McpClient.CreateAsync(transport, loggerFactory: consoleLoggerFactory);
var tools = await client.ListToolsAsync(); var tools = await client.ListToolsAsync();
if (tools.Count == 0) if (tools.Count == 0)

View file

@ -19,7 +19,7 @@ var (command, arguments) = GetCommandAndArguments(args);
if (command == "http") if (command == "http")
{ {
// make sure AspNetCoreMcpServer is running // make sure AspNetCoreMcpServer is running
clientTransport = new SseClientTransport(new() clientTransport = new HttpClientTransport(new()
{ {
Endpoint = new Uri("http://localhost:3001") Endpoint = new Uri("http://localhost:3001")
}); });
@ -33,7 +33,7 @@ else
Arguments = arguments, Arguments = arguments,
}); });
} }
await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport!); await using var mcpClient = await McpClient.CreateAsync(clientTransport!);
var tools = await mcpClient.ListToolsAsync(); var tools = await mcpClient.ListToolsAsync();
foreach (var tool in tools) foreach (var tool in tools)
@ -62,7 +62,7 @@ var messages = new List<ChatMessage>();
var sb = new StringBuilder(); var sb = new StringBuilder();
PromptForInput(); PromptForInput();
while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase)) while (Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase))
{ {
if (string.IsNullOrWhiteSpace(query)) if (string.IsNullOrWhiteSpace(query))
{ {

View file

@ -12,7 +12,7 @@ public sealed class SampleLlmTool
{ {
[McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")]
public static async Task<string> SampleLLM( public static async Task<string> SampleLLM(
IMcpServer thisServer, McpServer thisServer,
[Description("The prompt to send to the LLM")] string prompt, [Description("The prompt to send to the LLM")] string prompt,
[Description("Maximum number of tokens to generate")] int maxTokens, [Description("Maximum number of tokens to generate")] int maxTokens,
CancellationToken cancellationToken) CancellationToken cancellationToken)

View file

@ -292,7 +292,7 @@ internal sealed class AuthorizationFilterSetup(IAuthorizationPolicyProvider? pol
if (requestServices is null) if (requestServices is null)
{ {
// The IAuthorizationPolicyProvider service must be non-null to get to this line, so it's very unexpected for RequestContext.Services to not be set. // The IAuthorizationPolicyProvider service must be non-null to get to this line, so it's very unexpected for RequestContext.Services to not be set.
throw new InvalidOperationException("RequestContext.Services is not set! The IMcpServer must be initialized with a non-null IServiceProvider."); throw new InvalidOperationException("RequestContext.Services is not set! The McpServer must be initialized with a non-null IServiceProvider.");
} }
// ASP.NET Core's AuthorizationMiddleware resolves the IAuthorizationService from scoped request services, so we do the same. // ASP.NET Core's AuthorizationMiddleware resolves the IAuthorizationService from scoped request services, so we do the same.

View file

@ -20,7 +20,7 @@ public class HttpServerTransportOptions
/// Gets or sets an optional asynchronous callback for running new MCP sessions manually. /// Gets or sets an optional asynchronous callback for running new MCP sessions manually.
/// This is useful for running logic before a sessions starts and after it completes. /// This is useful for running logic before a sessions starts and after it completes.
/// </summary> /// </summary>
public Func<HttpContext, IMcpServer, CancellationToken, Task>? RunSessionHandler { get; set; } public Func<HttpContext, McpServer, CancellationToken, Task>? RunSessionHandler { get; set; }
/// <summary> /// <summary>
/// Gets or sets whether the server should run in a stateless mode that does not require all requests for a given session /// Gets or sets whether the server should run in a stateless mode that does not require all requests for a given session

View file

@ -53,7 +53,7 @@ internal sealed class SseHandler(
try try
{ {
await using var mcpServer = McpServerFactory.Create(transport, mcpServerOptions, loggerFactory, context.RequestServices); await using var mcpServer = McpServer.Create(transport, mcpServerOptions, loggerFactory, context.RequestServices);
context.Features.Set(mcpServer); context.Features.Set(mcpServer);
var runSessionAsync = httpMcpServerOptions.Value.RunSessionHandler ?? StreamableHttpHandler.RunSessionAsync; var runSessionAsync = httpMcpServerOptions.Value.RunSessionHandler ?? StreamableHttpHandler.RunSessionAsync;

View file

@ -232,7 +232,7 @@ internal sealed class StreamableHttpHandler(
} }
} }
var server = McpServerFactory.Create(transport, mcpServerOptions, loggerFactory, mcpServerServices); var server = McpServer.Create(transport, mcpServerOptions, loggerFactory, mcpServerServices);
context.Features.Set(server); context.Features.Set(server);
var userIdClaim = statelessId?.UserIdClaim ?? GetUserIdClaim(context.User); var userIdClaim = statelessId?.UserIdClaim ?? GetUserIdClaim(context.User);
@ -307,7 +307,7 @@ internal sealed class StreamableHttpHandler(
}; };
} }
internal static Task RunSessionAsync(HttpContext httpContext, IMcpServer session, CancellationToken requestAborted) internal static Task RunSessionAsync(HttpContext httpContext, McpServer session, CancellationToken requestAborted)
=> session.RunAsync(requestAborted); => session.RunAsync(requestAborted);
// SignalR only checks for ClaimTypes.NameIdentifier in HttpConnectionDispatcher, but AspNetCore.Antiforgery checks that plus the sub and UPN claims. // SignalR only checks for ClaimTypes.NameIdentifier in HttpConnectionDispatcher, but AspNetCore.Antiforgery checks that plus the sub and UPN claims.

Some files were not shown because too many files have changed in this diff Show more