From 2ef8821f0087f070aef2bbb82e9cbb63d1c8ea21 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 20 Aug 2025 16:49:45 -0400 Subject: [PATCH 01/15] Remove `McpEndpoint` & `IMcpClient` --- README.md | 4 +- samples/InMemoryTransport/Program.cs | 2 +- .../AssemblyNameHelper.cs | 9 + .../Client/IClientTransport.cs | 4 +- .../Client/IMcpClient.cs | 47 --- .../Client/McpClient.cs | 236 --------------- .../Client/McpClientExtensions.cs | 48 ++-- .../Client/McpClientFactory.cs | 28 +- .../Client/McpClientOptions.cs | 2 +- .../Client/McpClientPrompt.cs | 4 +- .../Client/McpClientResource.cs | 6 +- .../Client/McpClientResourceTemplate.cs | 4 +- .../Client/McpClientSession.cs | 272 ++++++++++++++++++ .../Client/McpClientTool.cs | 10 +- src/ModelContextProtocol.Core/IMcpEndpoint.cs | 2 +- src/ModelContextProtocol.Core/McpEndpoint.cs | 144 ---------- .../McpEndpointExtensions.cs | 4 +- .../{McpSession.cs => McpSessionHandler.cs} | 50 +++- src/ModelContextProtocol.Core/README.md | 4 +- .../Server/DestinationBoundMcpServer.cs | 3 +- .../Server/McpServerExtensions.cs | 4 +- .../Server/McpServerFactory.cs | 2 +- .../{McpServer.cs => McpServerSession.cs} | 70 +++-- .../Server/StdioServerTransport.cs | 2 +- .../TokenProgress.cs | 6 +- .../HttpServerIntegrationTests.cs | 2 +- .../MapMcpTests.cs | 2 +- .../SseIntegrationTests.cs | 2 +- .../SseServerIntegrationTestFixture.cs | 2 +- .../StatelessServerTests.cs | 2 +- .../Client/McpClientExtensionsTests.cs | 22 +- .../Client/McpClientFactoryTests.cs | 4 +- .../Client/McpClientResourceTemplateTests.cs | 2 +- .../ClientIntegrationTestFixture.cs | 2 +- .../ClientServerTestBase.cs | 2 +- .../McpServerBuilderExtensionsPromptsTests.cs | 12 +- ...cpServerBuilderExtensionsResourcesTests.cs | 12 +- .../McpServerBuilderExtensionsToolsTests.cs | 32 +-- .../Configuration/McpServerScopedTests.cs | 2 +- .../DiagnosticTests.cs | 4 +- .../Protocol/ElicitationTests.cs | 2 +- .../Protocol/NotificationHandlerTests.cs | 10 +- 42 files changed, 497 insertions(+), 586 deletions(-) create mode 100644 src/ModelContextProtocol.Core/AssemblyNameHelper.cs delete mode 100644 src/ModelContextProtocol.Core/Client/IMcpClient.cs delete mode 100644 src/ModelContextProtocol.Core/Client/McpClient.cs create mode 100644 src/ModelContextProtocol.Core/Client/McpClientSession.cs delete mode 100644 src/ModelContextProtocol.Core/McpEndpoint.cs rename src/ModelContextProtocol.Core/{McpSession.cs => McpSessionHandler.cs} (95%) rename src/ModelContextProtocol.Core/Server/{McpServer.cs => McpServerSession.cs} (90%) diff --git a/README.md b/README.md index 163d57f8a..5c9cfb428 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ dotnet add package ModelContextProtocol --prerelease ## Getting Started (Client) -To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `IMcpClient` -to a server. Once you have an `IMcpClient`, you can interact with it, such as to enumerate all available tools and invoke tools. +To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `McpClientSession` +to a server. Once you have an `McpClientSession`, you can interact with it, such as to enumerate all available tools and invoke tools. ```csharp var clientTransport = new StdioClientTransport(new StdioClientTransportOptions diff --git a/samples/InMemoryTransport/Program.cs b/samples/InMemoryTransport/Program.cs index 67e2d320c..5aa003901 100644 --- a/samples/InMemoryTransport/Program.cs +++ b/samples/InMemoryTransport/Program.cs @@ -21,7 +21,7 @@ _ = server.RunAsync(); // Connect a client using a stream-based transport over the same in-memory pipe. -await using IMcpClient client = await McpClientFactory.CreateAsync( +await using McpClientSession client = await McpClientFactory.CreateAsync( new StreamClientTransport(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream())); // List all tools. diff --git a/src/ModelContextProtocol.Core/AssemblyNameHelper.cs b/src/ModelContextProtocol.Core/AssemblyNameHelper.cs new file mode 100644 index 000000000..292ed2f96 --- /dev/null +++ b/src/ModelContextProtocol.Core/AssemblyNameHelper.cs @@ -0,0 +1,9 @@ +using System.Reflection; + +namespace ModelContextProtocol; + +internal static class AssemblyNameHelper +{ + /// Cached naming information used for MCP session name/version when none is specified. + public static AssemblyName DefaultAssemblyName { get; } = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName(); +} diff --git a/src/ModelContextProtocol.Core/Client/IClientTransport.cs b/src/ModelContextProtocol.Core/Client/IClientTransport.cs index 525178957..9cffa09dc 100644 --- a/src/ModelContextProtocol.Core/Client/IClientTransport.cs +++ b/src/ModelContextProtocol.Core/Client/IClientTransport.cs @@ -11,7 +11,7 @@ namespace ModelContextProtocol.Client; /// and servers, allowing different transport protocols to be used interchangeably. /// /// -/// When creating an , is typically used, and is +/// When creating an , is typically used, and is /// provided with the based on expected server configuration. /// /// @@ -35,7 +35,7 @@ public interface IClientTransport /// /// /// The lifetime of the returned instance is typically managed by the - /// that uses this transport. When the client is disposed, it will dispose + /// that uses this transport. When the client is disposed, it will dispose /// the transport session as well. /// /// diff --git a/src/ModelContextProtocol.Core/Client/IMcpClient.cs b/src/ModelContextProtocol.Core/Client/IMcpClient.cs deleted file mode 100644 index 68a92a2d9..000000000 --- a/src/ModelContextProtocol.Core/Client/IMcpClient.cs +++ /dev/null @@ -1,47 +0,0 @@ -using ModelContextProtocol.Protocol; - -namespace ModelContextProtocol.Client; - -/// -/// Represents an instance of a Model Context Protocol (MCP) client that connects to and communicates with an MCP server. -/// -public interface IMcpClient : IMcpEndpoint -{ - /// - /// Gets the capabilities supported by the connected server. - /// - /// The client is not connected. - ServerCapabilities ServerCapabilities { get; } - - /// - /// Gets the implementation information of the connected server. - /// - /// - /// - /// This property provides identification details about the connected server, including its name and version. - /// It is populated during the initialization handshake and is available after a successful connection. - /// - /// - /// This information can be useful for logging, debugging, compatibility checks, and displaying server - /// information to users. - /// - /// - /// The client is not connected. - Implementation ServerInfo { get; } - - /// - /// Gets any instructions describing how to use the connected server and its features. - /// - /// - /// - /// This property contains instructions provided by the server during initialization that explain - /// how to effectively use its capabilities. These instructions can include details about available - /// tools, expected input formats, limitations, or any other helpful information. - /// - /// - /// This can be used by clients to improve an LLM's understanding of available tools, prompts, and resources. - /// It can be thought of like a "hint" to the model and may be added to a system prompt. - /// - /// - string? ServerInstructions { get; } -} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs deleted file mode 100644 index dd8c7fe09..000000000 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ /dev/null @@ -1,236 +0,0 @@ -using Microsoft.Extensions.Logging; -using ModelContextProtocol.Protocol; -using System.Text.Json; - -namespace ModelContextProtocol.Client; - -/// -internal sealed partial class McpClient : McpEndpoint, IMcpClient -{ - private static Implementation DefaultImplementation { get; } = new() - { - Name = DefaultAssemblyName.Name ?? nameof(McpClient), - Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0", - }; - - private readonly IClientTransport _clientTransport; - private readonly McpClientOptions _options; - - private ITransport? _sessionTransport; - private CancellationTokenSource? _connectCts; - - private ServerCapabilities? _serverCapabilities; - private Implementation? _serverInfo; - private string? _serverInstructions; - - /// - /// Initializes a new instance of the class. - /// - /// The transport to use for communication with the server. - /// Options for the client, defining protocol version and capabilities. - /// The logger factory. - public McpClient(IClientTransport clientTransport, McpClientOptions? options, ILoggerFactory? loggerFactory) - : base(loggerFactory) - { - options ??= new(); - - _clientTransport = clientTransport; - _options = options; - - EndpointName = clientTransport.Name; - - if (options.Capabilities is { } capabilities) - { - if (capabilities.NotificationHandlers is { } notificationHandlers) - { - NotificationHandlers.RegisterRange(notificationHandlers); - } - - if (capabilities.Sampling is { } samplingCapability) - { - if (samplingCapability.SamplingHandler is not { } samplingHandler) - { - throw new InvalidOperationException("Sampling capability was set but it did not provide a handler."); - } - - RequestHandlers.Set( - RequestMethods.SamplingCreateMessage, - (request, _, cancellationToken) => samplingHandler( - request, - request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance, - cancellationToken), - McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, - McpJsonUtilities.JsonContext.Default.CreateMessageResult); - } - - if (capabilities.Roots is { } rootsCapability) - { - if (rootsCapability.RootsHandler is not { } rootsHandler) - { - throw new InvalidOperationException("Roots capability was set but it did not provide a handler."); - } - - RequestHandlers.Set( - RequestMethods.RootsList, - (request, _, cancellationToken) => rootsHandler(request, cancellationToken), - McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, - McpJsonUtilities.JsonContext.Default.ListRootsResult); - } - - if (capabilities.Elicitation is { } elicitationCapability) - { - if (elicitationCapability.ElicitationHandler is not { } elicitationHandler) - { - throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler."); - } - - RequestHandlers.Set( - RequestMethods.ElicitationCreate, - (request, _, cancellationToken) => elicitationHandler(request, cancellationToken), - McpJsonUtilities.JsonContext.Default.ElicitRequestParams, - McpJsonUtilities.JsonContext.Default.ElicitResult); - } - } - } - - /// - public string? SessionId - { - get - { - if (_sessionTransport is null) - { - throw new InvalidOperationException("Must have already initialized a session when invoking this property."); - } - - return _sessionTransport.SessionId; - } - } - - /// - public ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected."); - - /// - public Implementation ServerInfo => _serverInfo ?? throw new InvalidOperationException("The client is not connected."); - - /// - public string? ServerInstructions => _serverInstructions; - - /// - public override string EndpointName { get; } - - /// - /// Asynchronously connects to an MCP server, establishes the transport connection, and completes the initialization handshake. - /// - public async Task ConnectAsync(CancellationToken cancellationToken = default) - { - _connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - cancellationToken = _connectCts.Token; - - try - { - // Connect transport - _sessionTransport = await _clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false); - InitializeSession(_sessionTransport); - // We don't want the ConnectAsync token to cancel the session after we've successfully connected. - // The base class handles cleaning up the session in DisposeAsync without our help. - StartSession(_sessionTransport, fullSessionCancellationToken: CancellationToken.None); - - // Perform initialization sequence - using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - initializationCts.CancelAfter(_options.InitializationTimeout); - - try - { - // Send initialize request - string requestProtocol = _options.ProtocolVersion ?? McpSession.LatestProtocolVersion; - var initializeResponse = await this.SendRequestAsync( - RequestMethods.Initialize, - new InitializeRequestParams - { - ProtocolVersion = requestProtocol, - Capabilities = _options.Capabilities ?? new ClientCapabilities(), - ClientInfo = _options.ClientInfo ?? DefaultImplementation, - }, - McpJsonUtilities.JsonContext.Default.InitializeRequestParams, - McpJsonUtilities.JsonContext.Default.InitializeResult, - cancellationToken: initializationCts.Token).ConfigureAwait(false); - - // Store server information - if (_logger.IsEnabled(LogLevel.Information)) - { - LogServerCapabilitiesReceived(EndpointName, - capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities), - serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation)); - } - - _serverCapabilities = initializeResponse.Capabilities; - _serverInfo = initializeResponse.ServerInfo; - _serverInstructions = initializeResponse.Instructions; - - // Validate protocol version - bool isResponseProtocolValid = - _options.ProtocolVersion is { } optionsProtocol ? optionsProtocol == initializeResponse.ProtocolVersion : - McpSession.SupportedProtocolVersions.Contains(initializeResponse.ProtocolVersion); - if (!isResponseProtocolValid) - { - LogServerProtocolVersionMismatch(EndpointName, requestProtocol, initializeResponse.ProtocolVersion); - throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}"); - } - - // Send initialized notification - await this.SendNotificationAsync( - NotificationMethods.InitializedNotification, - new InitializedNotificationParams(), - McpJsonUtilities.JsonContext.Default.InitializedNotificationParams, - cancellationToken: initializationCts.Token).ConfigureAwait(false); - - } - catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) - { - LogClientInitializationTimeout(EndpointName); - throw new TimeoutException("Initialization timed out", oce); - } - } - catch (Exception e) - { - LogClientInitializationError(EndpointName, e); - await DisposeAsync().ConfigureAwait(false); - throw; - } - } - - /// - public override async ValueTask DisposeUnsynchronizedAsync() - { - try - { - if (_connectCts is not null) - { - await _connectCts.CancelAsync().ConfigureAwait(false); - _connectCts.Dispose(); - } - - await base.DisposeUnsynchronizedAsync().ConfigureAwait(false); - } - finally - { - if (_sessionTransport is not null) - { - await _sessionTransport.DisposeAsync().ConfigureAwait(false); - } - } - } - - [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client received server '{ServerInfo}' capabilities: '{Capabilities}'.")] - private partial void LogServerCapabilitiesReceived(string endpointName, string capabilities, string serverInfo); - - [LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization error.")] - private partial void LogClientInitializationError(string endpointName, Exception exception); - - [LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization timed out.")] - private partial void LogClientInitializationTimeout(string endpointName); - - [LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client protocol version mismatch with server. Expected '{Expected}', received '{Received}'.")] - private partial void LogServerProtocolVersionMismatch(string endpointName, string expected, string received); -} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 60a9c3a64..4e082b07a 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Client; /// -/// Provides extension methods for interacting with an . +/// Provides extension methods for interacting with an . /// /// /// @@ -38,7 +38,7 @@ public static class McpClientExtensions /// /// is . /// Thrown when the server cannot be reached or returns an error response. - public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default) + public static Task PingAsync(this McpClientSession client, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -90,7 +90,7 @@ public static Task PingAsync(this IMcpClient client, CancellationToken cancellat /// /// is . public static async ValueTask> ListToolsAsync( - this IMcpClient client, + this McpClientSession client, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) { @@ -156,7 +156,7 @@ public static async ValueTask> ListToolsAsync( /// /// is . public static async IAsyncEnumerable EnumerateToolsAsync( - this IMcpClient client, + this McpClientSession client, JsonSerializerOptions? serializerOptions = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -203,7 +203,7 @@ public static async IAsyncEnumerable EnumerateToolsAsync( /// /// is . public static async ValueTask> ListPromptsAsync( - this IMcpClient client, CancellationToken cancellationToken = default) + this McpClientSession client, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -259,7 +259,7 @@ public static async ValueTask> ListPromptsAsync( /// /// is . public static async IAsyncEnumerable EnumeratePromptsAsync( - this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) + this McpClientSession client, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -309,7 +309,7 @@ public static async IAsyncEnumerable EnumeratePromptsAsync( /// Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt. /// is . public static ValueTask GetPromptAsync( - this IMcpClient client, + this McpClientSession client, string name, IReadOnlyDictionary? arguments = null, JsonSerializerOptions? serializerOptions = null, @@ -347,7 +347,7 @@ public static ValueTask GetPromptAsync( /// /// is . public static async ValueTask> ListResourceTemplatesAsync( - this IMcpClient client, CancellationToken cancellationToken = default) + this McpClientSession client, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -404,7 +404,7 @@ public static async ValueTask> ListResourceTemp /// /// is . public static async IAsyncEnumerable EnumerateResourceTemplatesAsync( - this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) + this McpClientSession client, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -458,7 +458,7 @@ public static async IAsyncEnumerable EnumerateResourc /// /// is . public static async ValueTask> ListResourcesAsync( - this IMcpClient client, CancellationToken cancellationToken = default) + this McpClientSession client, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -515,7 +515,7 @@ public static async ValueTask> ListResourcesAsync( /// /// is . public static async IAsyncEnumerable EnumerateResourcesAsync( - this IMcpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) + this McpClientSession client, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -549,7 +549,7 @@ public static async IAsyncEnumerable EnumerateResourcesAsync( /// is . /// is empty or composed entirely of whitespace. public static ValueTask ReadResourceAsync( - this IMcpClient client, string uri, CancellationToken cancellationToken = default) + this McpClientSession client, string uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uri); @@ -571,7 +571,7 @@ public static ValueTask ReadResourceAsync( /// is . /// is . public static ValueTask ReadResourceAsync( - this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) + this McpClientSession client, Uri uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(uri); @@ -590,7 +590,7 @@ public static ValueTask ReadResourceAsync( /// is . /// is empty or composed entirely of whitespace. public static ValueTask ReadResourceAsync( - this IMcpClient client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) + this McpClientSession client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uriTemplate); @@ -633,7 +633,7 @@ public static ValueTask ReadResourceAsync( /// is . /// is empty or composed entirely of whitespace. /// The server returned an error response. - public static ValueTask CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) + public static ValueTask CompleteAsync(this McpClientSession client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(reference); @@ -676,7 +676,7 @@ public static ValueTask CompleteAsync(this IMcpClient client, Re /// is . /// is . /// is empty or composed entirely of whitespace. - public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) + public static Task SubscribeToResourceAsync(this McpClientSession client, string uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uri); @@ -713,7 +713,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, /// /// is . /// is . - public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) + public static Task SubscribeToResourceAsync(this McpClientSession client, Uri uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(uri); @@ -745,7 +745,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, Can /// is . /// is . /// is empty or composed entirely of whitespace. - public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) + public static Task UnsubscribeFromResourceAsync(this McpClientSession client, string uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uri); @@ -781,7 +781,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string u /// /// is . /// is . - public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) + public static Task UnsubscribeFromResourceAsync(this McpClientSession client, Uri uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(uri); @@ -825,7 +825,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, /// /// public static ValueTask CallToolAsync( - this IMcpClient client, + this McpClientSession client, string toolName, IReadOnlyDictionary? arguments = null, IProgress? progress = null, @@ -854,7 +854,7 @@ public static ValueTask CallToolAsync( cancellationToken: cancellationToken); static async ValueTask SendRequestWithProgressAsync( - IMcpClient client, + McpClientSession client, string toolName, IReadOnlyDictionary? arguments, IProgress progress, @@ -1035,7 +1035,7 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat /// /// /// is . - public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, CancellationToken cancellationToken = default) + public static Task SetLoggingLevel(this McpClientSession client, LoggingLevel level, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -1070,8 +1070,8 @@ public static Task SetLoggingLevel(this IMcpClient client, LoggingLevel level, C /// /// /// is . - public static Task SetLoggingLevel(this IMcpClient client, LogLevel level, CancellationToken cancellationToken = default) => - SetLoggingLevel(client, McpServer.ToLoggingLevel(level), cancellationToken); + public static Task SetLoggingLevel(this McpClientSession client, LogLevel level, CancellationToken cancellationToken = default) => + SetLoggingLevel(client, McpServerSession.ToLoggingLevel(level), cancellationToken); /// Convers a dictionary with values to a dictionary with values. private static Dictionary? ToArgumentsDictionary( diff --git a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs index 30b3a9476..57827a42e 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs @@ -6,13 +6,13 @@ namespace ModelContextProtocol.Client; /// Provides factory methods for creating Model Context Protocol (MCP) clients. /// /// -/// This factory class is the primary way to instantiate instances +/// This factory class is the primary way to instantiate instances /// that connect to MCP servers. It handles the creation and connection /// of appropriate implementations through the supplied transport. /// -public static partial class McpClientFactory +public static class McpClientFactory { - /// Creates an , connecting it to the specified server. + /// Creates an , connecting it to the specified server. /// The transport instance used to communicate with the server. /// /// A client configuration object which specifies client capabilities and protocol version. @@ -20,10 +20,10 @@ public static partial class McpClientFactory /// /// A logger factory for creating loggers for clients. /// The to monitor for cancellation requests. The default is . - /// An that's connected to the specified server. + /// An that's connected to the specified server. /// is . /// is . - public static async Task CreateAsync( + public static async Task CreateAsync( IClientTransport clientTransport, McpClientOptions? clientOptions = null, ILoggerFactory? loggerFactory = null, @@ -31,24 +31,20 @@ public static async Task CreateAsync( { Throw.IfNull(clientTransport); - McpClient client = new(clientTransport, clientOptions, loggerFactory); + var transport = await clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false); + var endpointName = clientTransport.Name; + + var clientSession = new McpClientSession(transport, endpointName, clientOptions, loggerFactory); try { - await client.ConnectAsync(cancellationToken).ConfigureAwait(false); - if (loggerFactory?.CreateLogger(typeof(McpClientFactory)) is ILogger logger) - { - logger.LogClientCreated(client.EndpointName); - } + await clientSession.ConnectAsync(cancellationToken).ConfigureAwait(false); } catch { - await client.DisposeAsync().ConfigureAwait(false); + await clientSession.DisposeAsync().ConfigureAwait(false); throw; } - return client; + return clientSession; } - - [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client created and connected.")] - private static partial void LogClientCreated(this ILogger logger, string endpointName); } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs index 76099d0d9..93145a3ac 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs @@ -3,7 +3,7 @@ namespace ModelContextProtocol.Client; /// -/// Provides configuration options for creating instances. +/// Provides configuration options for creating instances. /// /// /// These options are typically passed to when creating a client. diff --git a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs index 43fc759a0..4726b46e7 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs @@ -20,9 +20,9 @@ namespace ModelContextProtocol.Client; /// public sealed class McpClientPrompt { - private readonly IMcpClient _client; + private readonly McpClientSession _client; - internal McpClientPrompt(IMcpClient client, Prompt prompt) + internal McpClientPrompt(McpClientSession client, Prompt prompt) { _client = client; ProtocolPrompt = prompt; diff --git a/src/ModelContextProtocol.Core/Client/McpClientResource.cs b/src/ModelContextProtocol.Core/Client/McpClientResource.cs index 06f8aff67..91ebf4d44 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResource.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResource.cs @@ -15,9 +15,9 @@ namespace ModelContextProtocol.Client; /// public sealed class McpClientResource { - private readonly IMcpClient _client; + private readonly McpClientSession _client; - internal McpClientResource(IMcpClient client, Resource resource) + internal McpClientResource(McpClientSession client, Resource resource) { _client = client; ProtocolResource = resource; @@ -58,7 +58,7 @@ internal McpClientResource(IMcpClient client, Resource resource) /// A containing the resource's result with content and messages. /// /// - /// This is a convenience method that internally calls . + /// This is a convenience method that internally calls . /// /// public ValueTask ReadAsync( diff --git a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs index 4da1bd0c3..678f7ccc6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs @@ -15,9 +15,9 @@ namespace ModelContextProtocol.Client; /// public sealed class McpClientResourceTemplate { - private readonly IMcpClient _client; + private readonly McpClientSession _client; - internal McpClientResourceTemplate(IMcpClient client, ResourceTemplate resourceTemplate) + internal McpClientResourceTemplate(McpClientSession client, ResourceTemplate resourceTemplate) { _client = client; ProtocolResourceTemplate = resourceTemplate; diff --git a/src/ModelContextProtocol.Core/Client/McpClientSession.cs b/src/ModelContextProtocol.Core/Client/McpClientSession.cs new file mode 100644 index 000000000..584b1c7e9 --- /dev/null +++ b/src/ModelContextProtocol.Core/Client/McpClientSession.cs @@ -0,0 +1,272 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using ModelContextProtocol.Protocol; +using System.Text.Json; + +namespace ModelContextProtocol.Client; + +/// +/// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server. +/// +public sealed partial class McpClientSession : IMcpEndpoint +{ + private static Implementation DefaultImplementation { get; } = new() + { + Name = AssemblyNameHelper.DefaultAssemblyName.Name ?? nameof(McpClientSession), + Version = AssemblyNameHelper.DefaultAssemblyName.Version?.ToString() ?? "1.0.0", + }; + + private readonly ILogger _logger; + private readonly ITransport _transport; + private readonly string _endpointName; + private readonly McpClientOptions _options; + private readonly McpSessionHandler _sessionHandler; + + private CancellationTokenSource? _connectCts; + + private ServerCapabilities? _serverCapabilities; + private Implementation? _serverInfo; + private string? _serverInstructions; + + private int _isDisposed; + + /// + /// Initializes a new instance of the class. + /// + /// The transport to use for communication with the server. + /// The name of the endpoint for logging and debug purposes. + /// Options for the client, defining protocol version and capabilities. + /// The logger factory. + internal McpClientSession(ITransport transport, string endpointName, McpClientOptions? options, ILoggerFactory? loggerFactory) + { + options ??= new(); + + _transport = transport; + _endpointName = $"Client ({options.ClientInfo?.Name ?? DefaultImplementation.Name} {options.ClientInfo?.Version ?? DefaultImplementation.Version})"; + _options = options; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + + var notificationHandlers = new NotificationHandlers(); + var requestHandlers = new RequestHandlers(); + + if (options.Capabilities is { } capabilities) + { + RegisterHandlers(capabilities, notificationHandlers, requestHandlers); + } + + _sessionHandler = new McpSessionHandler(isServer: false, transport, endpointName, requestHandlers, notificationHandlers, _logger); + } + + private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandlers notificationHandlers, RequestHandlers requestHandlers) + { + if (capabilities.NotificationHandlers is { } notificationHandlersFromCapabilities) + { + notificationHandlers.RegisterRange(notificationHandlersFromCapabilities); + } + + if (capabilities.Sampling is { } samplingCapability) + { + if (samplingCapability.SamplingHandler is not { } samplingHandler) + { + throw new InvalidOperationException("Sampling capability was set but it did not provide a handler."); + } + + requestHandlers.Set( + RequestMethods.SamplingCreateMessage, + (request, _, cancellationToken) => samplingHandler( + request, + request?.ProgressToken is { } token ? new TokenProgress(this, token) : NullProgress.Instance, + cancellationToken), + McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, + McpJsonUtilities.JsonContext.Default.CreateMessageResult); + } + + if (capabilities.Roots is { } rootsCapability) + { + if (rootsCapability.RootsHandler is not { } rootsHandler) + { + throw new InvalidOperationException("Roots capability was set but it did not provide a handler."); + } + + requestHandlers.Set( + RequestMethods.RootsList, + (request, _, cancellationToken) => rootsHandler(request, cancellationToken), + McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, + McpJsonUtilities.JsonContext.Default.ListRootsResult); + } + + if (capabilities.Elicitation is { } elicitationCapability) + { + if (elicitationCapability.ElicitationHandler is not { } elicitationHandler) + { + throw new InvalidOperationException("Elicitation capability was set but it did not provide a handler."); + } + + requestHandlers.Set( + RequestMethods.ElicitationCreate, + (request, _, cancellationToken) => elicitationHandler(request, cancellationToken), + McpJsonUtilities.JsonContext.Default.ElicitRequestParams, + McpJsonUtilities.JsonContext.Default.ElicitResult); + } + } + + /// + public string? SessionId => _transport.SessionId; + + /// + /// Gets the capabilities supported by the connected server. + /// + /// The client is not connected. + public ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected."); + + /// + /// Gets the implementation information of the connected server. + /// + /// + /// + /// This property provides identification details about the connected server, including its name and version. + /// It is populated during the initialization handshake and is available after a successful connection. + /// + /// + /// This information can be useful for logging, debugging, compatibility checks, and displaying server + /// information to users. + /// + /// + /// The client is not connected. + public Implementation ServerInfo => _serverInfo ?? throw new InvalidOperationException("The client is not connected."); + + /// + /// Gets any instructions describing how to use the connected server and its features. + /// + /// + /// + /// This property contains instructions provided by the server during initialization that explain + /// how to effectively use its capabilities. These instructions can include details about available + /// tools, expected input formats, limitations, or any other helpful information. + /// + /// + /// This can be used by clients to improve an LLM's understanding of available tools, prompts, and resources. + /// It can be thought of like a "hint" to the model and may be added to a system prompt. + /// + /// + public string? ServerInstructions => _serverInstructions; + + /// + /// Asynchronously connects to an MCP server, establishes the transport connection, and completes the initialization handshake. + /// + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + _connectCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cancellationToken = _connectCts.Token; + + try + { + // We don't want the ConnectAsync token to cancel the message processing loop after we've successfully connected. + // The session handler handles cancelling the loop upon its disposal. + _ = _sessionHandler.ProcessMessagesAsync(CancellationToken.None); + + // Perform initialization sequence + using var initializationCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + initializationCts.CancelAfter(_options.InitializationTimeout); + + try + { + // Send initialize request + string requestProtocol = _options.ProtocolVersion ?? McpSessionHandler.LatestProtocolVersion; + var initializeResponse = await this.SendRequestAsync( + RequestMethods.Initialize, + new InitializeRequestParams + { + ProtocolVersion = requestProtocol, + Capabilities = _options.Capabilities ?? new ClientCapabilities(), + ClientInfo = _options.ClientInfo ?? DefaultImplementation, + }, + McpJsonUtilities.JsonContext.Default.InitializeRequestParams, + McpJsonUtilities.JsonContext.Default.InitializeResult, + cancellationToken: initializationCts.Token).ConfigureAwait(false); + + // Store server information + if (_logger.IsEnabled(LogLevel.Information)) + { + LogServerCapabilitiesReceived(_endpointName, + capabilities: JsonSerializer.Serialize(initializeResponse.Capabilities, McpJsonUtilities.JsonContext.Default.ServerCapabilities), + serverInfo: JsonSerializer.Serialize(initializeResponse.ServerInfo, McpJsonUtilities.JsonContext.Default.Implementation)); + } + + _serverCapabilities = initializeResponse.Capabilities; + _serverInfo = initializeResponse.ServerInfo; + _serverInstructions = initializeResponse.Instructions; + + // Validate protocol version + bool isResponseProtocolValid = + _options.ProtocolVersion is { } optionsProtocol ? optionsProtocol == initializeResponse.ProtocolVersion : + McpSessionHandler.SupportedProtocolVersions.Contains(initializeResponse.ProtocolVersion); + if (!isResponseProtocolValid) + { + LogServerProtocolVersionMismatch(_endpointName, requestProtocol, initializeResponse.ProtocolVersion); + throw new McpException($"Server protocol version mismatch. Expected {requestProtocol}, got {initializeResponse.ProtocolVersion}"); + } + + // Send initialized notification + await this.SendNotificationAsync( + NotificationMethods.InitializedNotification, + new InitializedNotificationParams(), + McpJsonUtilities.JsonContext.Default.InitializedNotificationParams, + cancellationToken: initializationCts.Token).ConfigureAwait(false); + + } + catch (OperationCanceledException oce) when (initializationCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested) + { + LogClientInitializationTimeout(_endpointName); + throw new TimeoutException("Initialization timed out", oce); + } + } + catch (Exception e) + { + LogClientInitializationError(_endpointName, e); + await DisposeAsync().ConfigureAwait(false); + throw; + } + + LogClientConnected(_endpointName); + } + + /// + public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) + => _sessionHandler.SendRequestAsync(request, cancellationToken); + + /// + public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + => _sessionHandler.SendMessageAsync(message, cancellationToken); + + /// + public IAsyncDisposable RegisterNotificationHandler(string method, Func handler) + => _sessionHandler.RegisterNotificationHandler(method, handler); + + /// + public async ValueTask DisposeAsync() + { + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) + { + return; + } + + await _sessionHandler.DisposeAsync().ConfigureAwait(false); + await _transport.DisposeAsync().ConfigureAwait(false); + } + + [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client received server '{ServerInfo}' capabilities: '{Capabilities}'.")] + private partial void LogServerCapabilitiesReceived(string endpointName, string capabilities, string serverInfo); + + [LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization error.")] + private partial void LogClientInitializationError(string endpointName, Exception exception); + + [LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client initialization timed out.")] + private partial void LogClientInitializationTimeout(string endpointName); + + [LoggerMessage(Level = LogLevel.Error, Message = "{EndpointName} client protocol version mismatch with server. Expected '{Expected}', received '{Received}'.")] + private partial void LogServerProtocolVersionMismatch(string endpointName, string expected, string received); + + [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} client created and connected.")] + private partial void LogClientConnected(string endpointName); +} diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs index 1810e9c56..d0be89297 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs @@ -6,11 +6,11 @@ namespace ModelContextProtocol.Client; /// -/// Provides an that calls a tool via an . +/// Provides an that calls a tool via an . /// /// /// -/// The class encapsulates an along with a description of +/// The class encapsulates an along with a description of /// a tool available via that client, allowing it to be invoked as an . This enables integration /// with AI models that support function calling capabilities. /// @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Client; /// /// /// Typically, you would get instances of this class by calling the -/// or extension methods on an instance. +/// or extension methods on an instance. /// /// public sealed class McpClientTool : AIFunction @@ -32,13 +32,13 @@ public sealed class McpClientTool : AIFunction ["Strict"] = false, // some MCP schemas may not meet "strict" requirements }); - private readonly IMcpClient _client; + private readonly McpClientSession _client; private readonly string _name; private readonly string _description; private readonly IProgress? _progress; internal McpClientTool( - IMcpClient client, + McpClientSession client, Tool tool, JsonSerializerOptions serializerOptions, string? name = null, diff --git a/src/ModelContextProtocol.Core/IMcpEndpoint.cs b/src/ModelContextProtocol.Core/IMcpEndpoint.cs index ea825e682..d48cc2d16 100644 --- a/src/ModelContextProtocol.Core/IMcpEndpoint.cs +++ b/src/ModelContextProtocol.Core/IMcpEndpoint.cs @@ -17,7 +17,7 @@ namespace ModelContextProtocol; /// /// /// -/// serves as the base interface for both and +/// serves as the base interface for both and /// interfaces, providing the common functionality needed for MCP protocol /// communication. Most applications will use these more specific interfaces rather than working with /// directly. diff --git a/src/ModelContextProtocol.Core/McpEndpoint.cs b/src/ModelContextProtocol.Core/McpEndpoint.cs deleted file mode 100644 index 0d0ccbb98..000000000 --- a/src/ModelContextProtocol.Core/McpEndpoint.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace ModelContextProtocol; - -/// -/// Base class for an MCP JSON-RPC endpoint. This covers both MCP clients and servers. -/// It is not supported, nor necessary, to implement both client and server functionality in the same class. -/// If an application needs to act as both a client and a server, it should use separate objects for each. -/// This is especially true as a client represents a connection to one and only one server, and vice versa. -/// Any multi-client or multi-server functionality should be implemented at a higher level of abstraction. -/// -internal abstract partial class McpEndpoint : IAsyncDisposable -{ - /// Cached naming information used for name/version when none is specified. - internal static AssemblyName DefaultAssemblyName { get; } = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetName(); - - private McpSession? _session; - private CancellationTokenSource? _sessionCts; - - private readonly SemaphoreSlim _disposeLock = new(1, 1); - private bool _disposed; - - protected readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The logger factory. - protected McpEndpoint(ILoggerFactory? loggerFactory = null) - { - _logger = loggerFactory?.CreateLogger(GetType()) ?? NullLogger.Instance; - } - - protected RequestHandlers RequestHandlers { get; } = []; - - protected NotificationHandlers NotificationHandlers { get; } = new(); - - public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) - => GetSessionOrThrow().SendRequestAsync(request, cancellationToken); - - public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) - => GetSessionOrThrow().SendMessageAsync(message, cancellationToken); - - public IAsyncDisposable RegisterNotificationHandler(string method, Func handler) => - GetSessionOrThrow().RegisterNotificationHandler(method, handler); - - /// - /// Gets the name of the endpoint for logging and debug purposes. - /// - public abstract string EndpointName { get; } - - /// - /// Task that processes incoming messages from the transport. - /// - protected Task? MessageProcessingTask { get; private set; } - - protected void InitializeSession(ITransport sessionTransport) - { - _session = new McpSession(this is IMcpServer, sessionTransport, EndpointName, RequestHandlers, NotificationHandlers, _logger); - } - - [MemberNotNull(nameof(MessageProcessingTask))] - protected void StartSession(ITransport sessionTransport, CancellationToken fullSessionCancellationToken) - { - _sessionCts = CancellationTokenSource.CreateLinkedTokenSource(fullSessionCancellationToken); - MessageProcessingTask = GetSessionOrThrow().ProcessMessagesAsync(_sessionCts.Token); - } - - protected void CancelSession() => _sessionCts?.Cancel(); - - public async ValueTask DisposeAsync() - { - using var _ = await _disposeLock.LockAsync().ConfigureAwait(false); - - if (_disposed) - { - return; - } - _disposed = true; - - await DisposeUnsynchronizedAsync().ConfigureAwait(false); - } - - /// - /// Cleans up the endpoint and releases resources. - /// - /// - public virtual async ValueTask DisposeUnsynchronizedAsync() - { - LogEndpointShuttingDown(EndpointName); - - try - { - if (_sessionCts is not null) - { - await _sessionCts.CancelAsync().ConfigureAwait(false); - } - - if (MessageProcessingTask is not null) - { - try - { - await MessageProcessingTask.ConfigureAwait(false); - } - catch (OperationCanceledException) - { - // Ignore cancellation - } - } - } - finally - { - _session?.Dispose(); - _sessionCts?.Dispose(); - } - - LogEndpointShutDown(EndpointName); - } - - protected McpSession GetSessionOrThrow() - { -#if NET - ObjectDisposedException.ThrowIf(_disposed, this); -#else - if (_disposed) - { - throw new ObjectDisposedException(GetType().Name); - } -#endif - - return _session ?? throw new InvalidOperationException($"This should be unreachable from public API! Call {nameof(InitializeSession)} before sending messages."); - } - - [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} shutting down.")] - private partial void LogEndpointShuttingDown(string endpointName); - - [LoggerMessage(Level = LogLevel.Information, Message = "{EndpointName} shut down.")] - private partial void LogEndpointShutDown(string endpointName); -} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs index 4e4abe5ce..345c10530 100644 --- a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs +++ b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs @@ -16,8 +16,8 @@ namespace ModelContextProtocol; /// simplifying JSON-RPC communication by handling serialization and deserialization of parameters and results. /// /// -/// These extension methods are designed to be used with both client () and -/// server () implementations of the interface. +/// These extension methods are designed to be used with both client () and +/// server () implementations of the interface. /// /// public static class McpEndpointExtensions diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSessionHandler.cs similarity index 95% rename from src/ModelContextProtocol.Core/McpSession.cs rename to src/ModelContextProtocol.Core/McpSessionHandler.cs index da9542055..463e796ae 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSessionHandler.cs @@ -17,7 +17,7 @@ namespace ModelContextProtocol; /// /// Class for managing an MCP JSON-RPC session. This covers both MCP clients and servers. /// -internal sealed partial class McpSession : IDisposable +internal sealed partial class McpSessionHandler : IAsyncDisposable { private static readonly Histogram s_clientSessionDuration = Diagnostics.CreateDurationHistogram( "mcp.client.session.duration", "Measures the duration of a client session.", longBuckets: true); @@ -61,8 +61,11 @@ internal sealed partial class McpSession : IDisposable private readonly string _sessionId = Guid.NewGuid().ToString("N"); private long _lastRequestId; + private CancellationTokenSource? _messageProcessingCts; + private Task? _messageProcessingTask; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// true if this is a server; false if it's a client. /// An MCP transport implementation. @@ -70,7 +73,7 @@ internal sealed partial class McpSession : IDisposable /// A collection of request handlers. /// A collection of notification handlers. /// The logger. - public McpSession( + public McpSessionHandler( bool isServer, ITransport transport, string endpointName, @@ -107,7 +110,21 @@ public McpSession( /// Starts processing messages from the transport. This method will block until the transport is disconnected. /// This is generally started in a background task or thread from the initialization logic of the derived class. /// - public async Task ProcessMessagesAsync(CancellationToken cancellationToken) + public Task ProcessMessagesAsync(CancellationToken cancellationToken) + { + if (_messageProcessingTask is not null) + { + throw new InvalidOperationException("The message processing loop has already started."); + } + + Debug.Assert(_messageProcessingCts is null); + + _messageProcessingCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + _messageProcessingTask = ProcessMessagesCoreAsync(_messageProcessingCts.Token); + return _messageProcessingTask; + } + + private async Task ProcessMessagesCoreAsync(CancellationToken cancellationToken) { try { @@ -344,7 +361,7 @@ private CancellationTokenRegistration RegisterCancellation(CancellationToken can return cancellationToken.Register(static objState => { - var state = (Tuple)objState!; + var state = (Tuple)objState!; _ = state.Item1.SendMessageAsync(new JsonRpcNotification { Method = NotificationMethods.CancelledNotification, @@ -372,6 +389,8 @@ public IAsyncDisposable RegisterNotificationHandler(string method, FuncA task containing the server's response. public async Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) { + Throw.IfNull(request); + cancellationToken.ThrowIfCancellationRequested(); Histogram durationMetric = _isServer ? s_serverOperationDuration : s_clientOperationDuration; @@ -682,7 +701,7 @@ private static void FinalizeDiagnostics( } } - public void Dispose() + public async ValueTask DisposeAsync() { Histogram durationMetric = _isServer ? s_serverSessionDuration : s_clientSessionDuration; if (durationMetric.Enabled) @@ -695,13 +714,30 @@ public void Dispose() durationMetric.Record(GetElapsed(_sessionStartingTimestamp).TotalSeconds, tags); } - // Complete all pending requests with cancellation foreach (var entry in _pendingRequests) { entry.Value.TrySetCanceled(); } _pendingRequests.Clear(); + + if (_messageProcessingCts is not null) + { + await _messageProcessingCts.CancelAsync().ConfigureAwait(false); + } + + if (_messageProcessingTask is not null) + { + try + { + await _messageProcessingTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + // Ignore cancellation + } + } + LogSessionDisposed(EndpointName, _sessionId, _transportKind); } diff --git a/src/ModelContextProtocol.Core/README.md b/src/ModelContextProtocol.Core/README.md index beb365c80..121c61841 100644 --- a/src/ModelContextProtocol.Core/README.md +++ b/src/ModelContextProtocol.Core/README.md @@ -27,8 +27,8 @@ dotnet add package ModelContextProtocol.Core --prerelease ## Getting Started (Client) -To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `IMcpClient` -to a server. Once you have an `IMcpClient`, you can interact with it, such as to enumerate all available tools and invoke tools. +To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `McpClientSession` +to a server. Once you have an `McpClientSession`, you can interact with it, such as to enumerate all available tools and invoke tools. ```csharp var clientTransport = new StdioClientTransport(new StdioClientTransportOptions diff --git a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs index d286d1ef4..e11604681 100644 --- a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs +++ b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs @@ -3,9 +3,8 @@ namespace ModelContextProtocol.Server; -internal sealed class DestinationBoundMcpServer(McpServer server, ITransport? transport) : IMcpServer +internal sealed class DestinationBoundMcpServer(McpServerSession server, ITransport? transport) : IMcpServer { - public string EndpointName => server.EndpointName; public string? SessionId => transport?.SessionId ?? server.SessionId; public ClientCapabilities? ClientCapabilities => server.ClientCapabilities; public Implementation? ClientInfo => server.ClientInfo; diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs index 277ed737b..4f19adff8 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs @@ -333,7 +333,7 @@ private sealed class ClientLogger(IMcpServer server, string categoryName) : ILog /// public bool IsEnabled(LogLevel logLevel) => server?.LoggingLevel is { } loggingLevel && - McpServer.ToLoggingLevel(logLevel) >= loggingLevel; + McpServerSession.ToLoggingLevel(logLevel) >= loggingLevel; /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -351,7 +351,7 @@ void Log(LogLevel logLevel, string message) { _ = server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams { - Level = McpServer.ToLoggingLevel(logLevel), + Level = McpServerSession.ToLoggingLevel(logLevel), Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String), Logger = categoryName, }); diff --git a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs index 50d4188b5..79e3d8c10 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs @@ -31,6 +31,6 @@ public static IMcpServer Create( Throw.IfNull(transport); Throw.IfNull(serverOptions); - return new McpServer(transport, serverOptions, loggerFactory, serviceProvider); + return new McpServerSession(transport, serverOptions, loggerFactory, serviceProvider); } } diff --git a/src/ModelContextProtocol.Core/Server/McpServer.cs b/src/ModelContextProtocol.Core/Server/McpServerSession.cs similarity index 90% rename from src/ModelContextProtocol.Core/Server/McpServer.cs rename to src/ModelContextProtocol.Core/Server/McpServerSession.cs index 6c5858f91..1614399c1 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerSession.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Protocol; using System.Runtime.CompilerServices; using System.Text.Json.Serialization.Metadata; @@ -7,22 +8,28 @@ namespace ModelContextProtocol.Server; /// -internal sealed class McpServer : McpEndpoint, IMcpServer +public sealed class McpServerSession : IMcpServer { internal static Implementation DefaultImplementation { get; } = new() { - Name = DefaultAssemblyName.Name ?? nameof(McpServer), - Version = DefaultAssemblyName.Version?.ToString() ?? "1.0.0", + Name = AssemblyNameHelper.DefaultAssemblyName.Name ?? nameof(McpServerSession), + Version = AssemblyNameHelper.DefaultAssemblyName.Version?.ToString() ?? "1.0.0", }; + private readonly ILogger _logger; private readonly ITransport _sessionTransport; private readonly bool _servicesScopePerRequest; private readonly List _disposables = []; + private readonly NotificationHandlers _notificationHandlers; + private readonly RequestHandlers _requestHandlers; + private readonly McpSessionHandler _sessionHandler; private readonly string _serverOnlyEndpointName; - private string? _endpointName; + private string _endpointName; private int _started; + private int _isDisposed; + /// Holds a boxed value for the server. /// /// Initialized to non-null the first time SetLevel is used. This is stored as a strong box @@ -31,7 +38,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer private StrongBox? _loggingLevel; /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// Transport to use for the server representing an already-established session. /// Configuration options for this server, including capabilities. @@ -39,8 +46,7 @@ internal sealed class McpServer : McpEndpoint, IMcpServer /// Logger factory to use for logging /// Optional service provider to use for dependency injection /// The server was incorrectly configured. - public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider) - : base(loggerFactory) + public McpServerSession(ITransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider) { Throw.IfNull(transport); Throw.IfNull(options); @@ -51,11 +57,16 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? ServerOptions = options; Services = serviceProvider; _serverOnlyEndpointName = $"Server ({options.ServerInfo?.Name ?? DefaultImplementation.Name} {options.ServerInfo?.Version ?? DefaultImplementation.Version})"; + _endpointName = _serverOnlyEndpointName; _servicesScopePerRequest = options.ScopeRequests; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; ClientInfo = options.KnownClientInfo; UpdateEndpointNameWithClientInfo(); + _notificationHandlers = new(); + _requestHandlers = []; + // Configure all request handlers based on the supplied options. ServerCapabilities = new(); ConfigureInitialize(options); @@ -70,7 +81,7 @@ public McpServer(ITransport transport, McpServerOptions options, ILoggerFactory? // Register any notification handlers that were provided. if (options.Capabilities?.NotificationHandlers is { } notificationHandlers) { - NotificationHandlers.RegisterRange(notificationHandlers); + _notificationHandlers.RegisterRange(notificationHandlers); } // Now that everything has been configured, subscribe to any necessary notifications. @@ -93,7 +104,7 @@ void Register(McpServerPrimitiveCollection? collection, } // And initialize the session. - InitializeSession(transport); + _sessionHandler = new McpSessionHandler(isServer: true, _sessionTransport, _endpointName!, _requestHandlers, _notificationHandlers, _logger); } /// @@ -114,9 +125,6 @@ void Register(McpServerPrimitiveCollection? collection, /// public IServiceProvider? Services { get; } - /// - public override string EndpointName => _endpointName ?? _serverOnlyEndpointName; - /// public LoggingLevel? LoggingLevel => _loggingLevel?.Value; @@ -130,8 +138,7 @@ public async Task RunAsync(CancellationToken cancellationToken = default) try { - StartSession(_sessionTransport, fullSessionCancellationToken: cancellationToken); - await MessageProcessingTask.ConfigureAwait(false); + await _sessionHandler.ProcessMessagesAsync(cancellationToken).ConfigureAwait(false); } finally { @@ -139,10 +146,29 @@ public async Task RunAsync(CancellationToken cancellationToken = default) } } - public override async ValueTask DisposeUnsynchronizedAsync() + + /// + public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) + => _sessionHandler.SendRequestAsync(request, cancellationToken); + + /// + public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + => _sessionHandler.SendMessageAsync(message, cancellationToken); + + /// + public IAsyncDisposable RegisterNotificationHandler(string method, Func handler) + => _sessionHandler.RegisterNotificationHandler(method, handler); + + /// + public async ValueTask DisposeAsync() { + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) + { + return; + } + _disposables.ForEach(d => d()); - await base.DisposeUnsynchronizedAsync().ConfigureAwait(false); + await _sessionHandler.DisposeAsync().ConfigureAwait(false); } private void ConfigurePing() @@ -155,7 +181,7 @@ private void ConfigurePing() private void ConfigureInitialize(McpServerOptions options) { - RequestHandlers.Set(RequestMethods.Initialize, + _requestHandlers.Set(RequestMethods.Initialize, async (request, _, _) => { ClientCapabilities = request?.Capabilities ?? new(); @@ -163,7 +189,7 @@ private void ConfigureInitialize(McpServerOptions options) // Use the ClientInfo to update the session EndpointName for logging. UpdateEndpointNameWithClientInfo(); - GetSessionOrThrow().EndpointName = EndpointName; + _sessionHandler.EndpointName = _endpointName; // Negotiate a protocol version. If the server options provide one, use that. // Otherwise, try to use whatever the client requested as long as it's supported. @@ -171,9 +197,9 @@ private void ConfigureInitialize(McpServerOptions options) string? protocolVersion = options.ProtocolVersion; if (protocolVersion is null) { - protocolVersion = request?.ProtocolVersion is string clientProtocolVersion && McpSession.SupportedProtocolVersions.Contains(clientProtocolVersion) ? + protocolVersion = request?.ProtocolVersion is string clientProtocolVersion && McpSessionHandler.SupportedProtocolVersions.Contains(clientProtocolVersion) ? clientProtocolVersion : - McpSession.LatestProtocolVersion; + McpSessionHandler.LatestProtocolVersion; } return new InitializeResult @@ -496,7 +522,7 @@ private void ConfigureLogging(McpServerOptions options) ServerCapabilities.Logging = new(); ServerCapabilities.Logging.SetLoggingLevelHandler = setLoggingLevelHandler; - RequestHandlers.Set( + _requestHandlers.Set( RequestMethods.LoggingSetLevel, (request, destinationTransport, cancellationToken) => { @@ -566,7 +592,7 @@ private void SetHandler( JsonTypeInfo requestTypeInfo, JsonTypeInfo responseTypeInfo) { - RequestHandlers.Set(method, + _requestHandlers.Set(method, (request, destinationTransport, cancellationToken) => InvokeHandlerAsync(handler, request, destinationTransport, cancellationToken), requestTypeInfo, responseTypeInfo); diff --git a/src/ModelContextProtocol.Core/Server/StdioServerTransport.cs b/src/ModelContextProtocol.Core/Server/StdioServerTransport.cs index 556a31159..26641cf6a 100644 --- a/src/ModelContextProtocol.Core/Server/StdioServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StdioServerTransport.cs @@ -37,7 +37,7 @@ private static string GetServerName(McpServerOptions serverOptions) { Throw.IfNull(serverOptions); - return serverOptions.ServerInfo?.Name ?? McpServer.DefaultImplementation.Name; + return serverOptions.ServerInfo?.Name ?? McpServerSession.DefaultImplementation.Name; } // Neither WindowsConsoleStream nor UnixConsoleStream respect CancellationTokens or cancel any I/O on Dispose. diff --git a/src/ModelContextProtocol.Core/TokenProgress.cs b/src/ModelContextProtocol.Core/TokenProgress.cs index f222fbf71..e05060119 100644 --- a/src/ModelContextProtocol.Core/TokenProgress.cs +++ b/src/ModelContextProtocol.Core/TokenProgress.cs @@ -4,13 +4,13 @@ namespace ModelContextProtocol; /// /// Provides an tied to a specific progress token and that will issue -/// progress notifications on the supplied endpoint. +/// progress notifications on the supplied session. /// -internal sealed class TokenProgress(IMcpEndpoint endpoint, ProgressToken progressToken) : IProgress +internal sealed class TokenProgress(IMcpEndpoint session, ProgressToken progressToken) : IProgress { /// public void Report(ProgressNotificationValue value) { - _ = endpoint.NotifyProgressAsync(progressToken, value, CancellationToken.None); + _ = session.NotifyProgressAsync(progressToken, value, CancellationToken.None); } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index 9b3c91b94..844fd7347 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -23,7 +23,7 @@ public override void Dispose() protected abstract SseClientTransportOptions ClientTransportOptions { get; } - private Task GetClientAsync(McpClientOptions? options = null) + private Task GetClientAsync(McpClientOptions? options = null) { return _fixture.ConnectMcpClientAsync(options, LoggerFactory); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index 4d0d73562..7ada1f9f2 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -23,7 +23,7 @@ protected void ConfigureStateless(HttpServerTransportOptions options) options.Stateless = Stateless; } - protected async Task ConnectAsync( + protected async Task ConnectAsync( string? path = null, SseClientTransportOptions? transportOptions = null, McpClientOptions? clientOptions = null) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs index 8191f6091..7a74eb316 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs @@ -21,7 +21,7 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr Name = "In-memory SSE Client", }; - private Task ConnectMcpClientAsync(HttpClient? httpClient = null, SseClientTransportOptions? transportOptions = null) + private Task ConnectMcpClientAsync(HttpClient? httpClient = null, SseClientTransportOptions? transportOptions = null) => McpClientFactory.CreateAsync( new SseClientTransport(transportOptions ?? DefaultTransportOptions, httpClient ?? HttpClient, LoggerFactory), loggerFactory: LoggerFactory, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs index 2aa675c84..8cdc76456 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs @@ -44,7 +44,7 @@ public SseServerIntegrationTestFixture() public HttpClient HttpClient { get; } - public Task ConnectMcpClientAsync(McpClientOptions? options, ILoggerFactory loggerFactory) + public Task ConnectMcpClientAsync(McpClientOptions? options, ILoggerFactory loggerFactory) { return McpClientFactory.CreateAsync( new SseClientTransport(DefaultTransportOptions, HttpClient, loggerFactory), diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs index b50a43edc..865aaf6e5 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs @@ -58,7 +58,7 @@ private async Task StartAsync() HttpClient.DefaultRequestHeaders.Accept.Add(new("text/event-stream")); } - private Task ConnectMcpClientAsync(McpClientOptions? clientOptions = null) + private Task ConnectMcpClientAsync(McpClientOptions? clientOptions = null) => McpClientFactory.CreateAsync( new SseClientTransport(DefaultTransportOptions, HttpClient, LoggerFactory), clientOptions, LoggerFactory, TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index e3d7ce44c..87622719d 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -197,7 +197,7 @@ public async Task CreateSamplingHandler_ShouldHandleResourceMessages() [Fact] public async Task ListToolsAsync_AllToolsReturned() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(12, tools.Count); @@ -223,7 +223,7 @@ public async Task ListToolsAsync_AllToolsReturned() [Fact] public async Task EnumerateToolsAsync_AllToolsReturned() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken)) { @@ -242,7 +242,7 @@ public async Task EnumerateToolsAsync_AllToolsReturned() public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions() { JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); bool hasTools = false; await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken)) @@ -263,7 +263,7 @@ public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions() public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions() { JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First(); await Assert.ThrowsAsync(async () => await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken)); @@ -273,7 +273,7 @@ public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions() public async Task SendRequestAsync_HonorsJsonSerializerOptions() { JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.SendRequestAsync("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); } @@ -282,7 +282,7 @@ public async Task SendRequestAsync_HonorsJsonSerializerOptions() public async Task SendNotificationAsync_HonorsJsonSerializerOptions() { JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(() => client.SendNotificationAsync("Method4", new { Value = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); } @@ -291,7 +291,7 @@ public async Task SendNotificationAsync_HonorsJsonSerializerOptions() public async Task GetPromptsAsync_HonorsJsonSerializerOptions() { JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.GetPromptAsync("Prompt", new Dictionary { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); } @@ -300,7 +300,7 @@ public async Task GetPromptsAsync_HonorsJsonSerializerOptions() public async Task WithName_ChangesToolName() { JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).First(); var originalName = tool.Name; @@ -315,7 +315,7 @@ public async Task WithName_ChangesToolName() public async Task WithDescription_ChangesToolDescription() { JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).FirstOrDefault(); var originalDescription = tool?.Description; var redescribedTool = tool?.WithDescription("ToolWithNewDescription"); @@ -344,7 +344,7 @@ public async Task WithProgress_ProgressReported() return 42; }, new() { Name = "ProgressReporter" })); - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken)).First(t => t.Name == "ProgressReporter"); @@ -372,7 +372,7 @@ private sealed class SynchronousProgress(Action callb [Fact] public async Task AsClientLoggerProvider_MessagesSentToClient() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); ILoggerProvider loggerProvider = Server.AsClientLoggerProvider(); Assert.Throws("categoryName", () => loggerProvider.CreateLogger(null!)); diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs index 7516a2186..6c59d94a0 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs @@ -85,9 +85,9 @@ public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) }; var clientTransport = (IClientTransport)Activator.CreateInstance(transportType)!; - IMcpClient? client = null; + McpClientSession? client = null; - var actionTask = McpClientFactory.CreateAsync(clientTransport, clientOptions, new Mock().Object, CancellationToken.None); + var actionTask = McpClientFactory.CreateAsync(clientTransport, clientOptions, loggerFactory: null, CancellationToken.None); // Act if (clientTransport is FailureTransport) diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs index 48c3c370d..a063e4c59 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs @@ -73,7 +73,7 @@ public static IEnumerable UriTemplate_InputsProduceExpectedOutputs_Mem public async Task UriTemplate_InputsProduceExpectedOutputs( IReadOnlyDictionary variables, string uriTemplate, object expected) { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.ReadResourceAsync(uriTemplate, variables, TestContext.Current.CancellationToken); Assert.NotNull(result); diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs index ebc7171e2..f36e26210 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs @@ -41,7 +41,7 @@ public void Initialize(ILoggerFactory loggerFactory) _loggerFactory = loggerFactory; } - public Task CreateClientAsync(string clientId, McpClientOptions? clientOptions = null) => + public Task CreateClientAsync(string clientId, McpClientOptions? clientOptions = null) => McpClientFactory.CreateAsync(new StdioClientTransport(clientId switch { "everything" => EverythingServerTransportOptions, diff --git a/tests/ModelContextProtocol.Tests/ClientServerTestBase.cs b/tests/ModelContextProtocol.Tests/ClientServerTestBase.cs index ec1c85107..8d1b8590b 100644 --- a/tests/ModelContextProtocol.Tests/ClientServerTestBase.cs +++ b/tests/ModelContextProtocol.Tests/ClientServerTestBase.cs @@ -62,7 +62,7 @@ public async ValueTask DisposeAsync() Dispose(); } - protected async Task CreateMcpClientForServer(McpClientOptions? clientOptions = null) + protected async Task CreateMcpClientForServer(McpClientOptions? clientOptions = null) { return await McpClientFactory.CreateAsync( new StreamClientTransport( diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index 3fa2ec78b..cc2107e03 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -95,7 +95,7 @@ public void Adds_Prompts_To_Server() [Fact] public async Task Can_List_And_Call_Registered_Prompts() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); Assert.Equal(6, prompts.Count); @@ -124,7 +124,7 @@ public async Task Can_List_And_Call_Registered_Prompts() [Fact] public async Task Can_Be_Notified_Of_Prompt_Changes() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); Assert.Equal(6, prompts.Count); @@ -165,7 +165,7 @@ public async Task Can_Be_Notified_Of_Prompt_Changes() [Fact] public async Task TitleAttributeProperty_PropagatedToTitle() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(prompts); @@ -179,7 +179,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() [Fact] public async Task Throws_When_Prompt_Fails() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.GetPromptAsync( nameof(SimplePrompts.ThrowsException), @@ -189,7 +189,7 @@ await Assert.ThrowsAsync(async () => await client.GetPromptAsync( [Fact] public async Task Throws_Exception_On_Unknown_Prompt() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.GetPromptAsync( "NotRegisteredPrompt", @@ -201,7 +201,7 @@ public async Task Throws_Exception_On_Unknown_Prompt() [Fact] public async Task Throws_Exception_Missing_Parameter() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.GetPromptAsync( "returns_chat_messages", diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index ed930b174..f2b90c8a4 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -122,7 +122,7 @@ public void Adds_Resources_To_Server() [Fact] public async Task Can_List_And_Call_Registered_Resources() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); Assert.NotNull(client.ServerCapabilities.Resources); @@ -141,7 +141,7 @@ public async Task Can_List_And_Call_Registered_Resources() [Fact] public async Task Can_List_And_Call_Registered_ResourceTemplates() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var resources = await client.ListResourceTemplatesAsync(TestContext.Current.CancellationToken); Assert.Equal(3, resources.Count); @@ -158,7 +158,7 @@ public async Task Can_List_And_Call_Registered_ResourceTemplates() [Fact] public async Task Can_Be_Notified_Of_Resource_Changes() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken); Assert.Equal(5, resources.Count); @@ -199,7 +199,7 @@ public async Task Can_Be_Notified_Of_Resource_Changes() [Fact] public async Task TitleAttributeProperty_PropagatedToTitle() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(resources); @@ -217,7 +217,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() [Fact] public async Task Throws_When_Resource_Fails() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( $"resource://mcp/{nameof(SimpleResources.ThrowsException)}", @@ -227,7 +227,7 @@ await Assert.ThrowsAsync(async () => await client.ReadResourceAsyn [Fact] public async Task Throws_Exception_On_Unknown_Resource() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( "test:///NotRegisteredResource", diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 35f833d50..748ee50fa 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -121,7 +121,7 @@ public void Adds_Tools_To_Server() [Fact] public async Task Can_List_Registered_Tools() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(16, tools.Count); @@ -185,7 +185,7 @@ public async Task Can_Create_Multiple_Servers_From_Options_And_List_Registered_T [Fact] public async Task Can_Be_Notified_Of_Tool_Changes() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(16, tools.Count); @@ -226,7 +226,7 @@ public async Task Can_Be_Notified_Of_Tool_Changes() [Fact] public async Task Can_Call_Registered_Tool() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "echo", @@ -245,7 +245,7 @@ public async Task Can_Call_Registered_Tool() [Fact] public async Task Can_Call_Registered_Tool_With_Array_Result() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "echo_array", @@ -268,7 +268,7 @@ public async Task Can_Call_Registered_Tool_With_Array_Result() [Fact] public async Task Can_Call_Registered_Tool_With_Null_Result() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "return_null", @@ -282,7 +282,7 @@ public async Task Can_Call_Registered_Tool_With_Null_Result() [Fact] public async Task Can_Call_Registered_Tool_With_Json_Result() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "return_json", @@ -299,7 +299,7 @@ public async Task Can_Call_Registered_Tool_With_Json_Result() [Fact] public async Task Can_Call_Registered_Tool_With_Int_Result() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "return_integer", @@ -314,7 +314,7 @@ public async Task Can_Call_Registered_Tool_With_Int_Result() [Fact] public async Task Can_Call_Registered_Tool_And_Pass_ComplexType() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "echo_complex", @@ -331,7 +331,7 @@ public async Task Can_Call_Registered_Tool_And_Pass_ComplexType() [Fact] public async Task Can_Call_Registered_Tool_With_Instance_Method() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); string[][] parts = new string[2][]; for (int i = 0; i < 2; i++) @@ -360,7 +360,7 @@ public async Task Can_Call_Registered_Tool_With_Instance_Method() [Fact] public async Task Returns_IsError_Content_When_Tool_Fails() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "throw_exception", @@ -375,7 +375,7 @@ public async Task Returns_IsError_Content_When_Tool_Fails() [Fact] public async Task Throws_Exception_On_Unknown_Tool() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.CallToolAsync( "NotRegisteredTool", @@ -387,7 +387,7 @@ public async Task Throws_Exception_On_Unknown_Tool() [Fact] public async Task Returns_IsError_Missing_Parameter() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "echo", @@ -506,7 +506,7 @@ public void WithToolsFromAssembly_Parameters_Satisfiable_From_DI(ServiceLifetime [Fact] public async Task Recognizes_Parameter_Types() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); @@ -581,7 +581,7 @@ public void Create_ExtractsToolAnnotations_SomeSet() [Fact] public async Task TitleAttributeProperty_PropagatedToTitle() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); @@ -597,7 +597,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() [Fact] public async Task HandlesIProgressParameter() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); @@ -651,7 +651,7 @@ public async Task HandlesIProgressParameter() [Fact] public async Task CancellationNotificationsPropagateToToolTokens() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs index b940c1c7c..4eec471e0 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs @@ -22,7 +22,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer [Fact] public async Task InjectScopedServiceAsArgument() { - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(McpServerScopedTestsJsonContext.Default.Options, TestContext.Current.CancellationToken); var tool = tools.First(t => t.Name == "echo_complex"); diff --git a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs index 116c62a15..fe08438e0 100644 --- a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs +++ b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs @@ -128,7 +128,7 @@ await RunConnected(async (client, server) => Assert.Equal("-32602", doesNotExistToolClient.Tags.Single(t => t.Key == "rpc.jsonrpc.error_code").Value); } - private static async Task RunConnected(Func action, List clientToServerLog) + private static async Task RunConnected(Func action, List clientToServerLog) { Pipe clientToServerPipe = new(), serverToClientPipe = new(); StreamServerTransport serverTransport = new(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()); @@ -153,7 +153,7 @@ private static async Task RunConnected(Func action { serverTask = server.RunAsync(TestContext.Current.CancellationToken); - await using (IMcpClient client = await McpClientFactory.CreateAsync( + await using (McpClientSession client = await McpClientFactory.CreateAsync( clientTransport, cancellationToken: TestContext.Current.CancellationToken)) { diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs index f44743916..2b735ed38 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs @@ -67,7 +67,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer [Fact] public async Task Can_Elicit_Information() { - await using IMcpClient client = await CreateMcpClientForServer(new McpClientOptions + await using McpClientSession client = await CreateMcpClientForServer(new McpClientOptions { Capabilities = new() { diff --git a/tests/ModelContextProtocol.Tests/Protocol/NotificationHandlerTests.cs b/tests/ModelContextProtocol.Tests/Protocol/NotificationHandlerTests.cs index 0d18667e9..d8fc4b94c 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/NotificationHandlerTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/NotificationHandlerTests.cs @@ -13,7 +13,7 @@ public NotificationHandlerTests(ITestOutputHelper testOutputHelper) public async Task RegistrationsAreRemovedWhenDisposed() { const string NotificationName = "somethingsomething"; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); const int Iterations = 10; @@ -40,7 +40,7 @@ public async Task RegistrationsAreRemovedWhenDisposed() public async Task MultipleRegistrationsResultInMultipleCallbacks() { const string NotificationName = "somethingsomething"; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); const int RegistrationCount = 10; @@ -80,7 +80,7 @@ public async Task MultipleRegistrationsResultInMultipleCallbacks() public async Task MultipleHandlersRunEvenIfOneThrows() { const string NotificationName = "somethingsomething"; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); const int RegistrationCount = 10; @@ -122,7 +122,7 @@ public async Task MultipleHandlersRunEvenIfOneThrows() public async Task DisposeAsyncDoesNotCompleteWhileNotificationHandlerRuns(int numberOfDisposals) { const string NotificationName = "somethingsomething"; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var handlerRunning = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -163,7 +163,7 @@ public async Task DisposeAsyncDoesNotCompleteWhileNotificationHandlerRuns(int nu public async Task DisposeAsyncCompletesImmediatelyWhenInvokedFromHandler(int numberOfDisposals) { const string NotificationName = "somethingsomething"; - await using IMcpClient client = await CreateMcpClientForServer(); + await using McpClientSession client = await CreateMcpClientForServer(); var handlerRunning = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); From 81bcae0e497eb2775fa705bf4480952d2b70fa41 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 28 Aug 2025 14:16:04 -0400 Subject: [PATCH 02/15] Revise naming, add abstract classes --- README.md | 8 +- .../Tools/SampleLlmTool.cs | 2 +- samples/ChatWithTools/Program.cs | 2 +- .../LoggingUpdateMessageSender.cs | 2 +- .../SubscriptionMessageSender.cs | 2 +- .../EverythingServer/Tools/LongRunningTool.cs | 2 +- .../EverythingServer/Tools/SampleLlmTool.cs | 2 +- samples/InMemoryTransport/Program.cs | 4 +- samples/ProtectedMcpClient/Program.cs | 2 +- samples/QuickstartClient/Program.cs | 2 +- .../Tools/SampleLlmTool.cs | 2 +- .../HttpMcpSession.cs | 2 +- .../HttpServerTransportOptions.cs | 2 +- .../SseHandler.cs | 2 +- .../StreamableHttpHandler.cs | 4 +- .../Client/IClientTransport.cs | 6 +- .../Client/McpClient.cs | 84 +++++++++++++++++ .../Client/McpClientExtensions.cs | 56 +++++------ .../Client/McpClientFactory.cs | 50 ---------- .../{McpClientSession.cs => McpClientImpl.cs} | 65 ++++--------- .../Client/McpClientOptions.cs | 4 +- .../Client/McpClientPrompt.cs | 4 +- .../Client/McpClientResource.cs | 6 +- .../Client/McpClientResourceTemplate.cs | 4 +- .../Client/McpClientTool.cs | 10 +- .../StreamableHttpClientSessionTransport.cs | 2 +- .../McpEndpointExtensions.cs | 18 ++-- .../{IMcpEndpoint.cs => McpSession.cs} | 35 +++---- .../Protocol/ClientCapabilities.cs | 2 +- .../Protocol/ITransport.cs | 2 +- .../Protocol/JsonRpcMessage.cs | 2 +- .../Protocol/ServerCapabilities.cs | 2 +- src/ModelContextProtocol.Core/README.md | 6 +- .../Server/AugmentedServiceProvider.cs | 4 +- .../Server/DestinationBoundMcpServer.cs | 35 ------- .../DestinationBoundMcpServerSession.cs | 35 +++++++ .../Server/{IMcpServer.cs => McpServer.cs} | 37 ++++++-- .../Server/McpServerExtensions.cs | 30 +++--- .../Server/McpServerFactory.cs | 36 -------- .../{McpServerSession.cs => McpServerImpl.cs} | 45 ++++----- .../Server/McpServerPrompt.cs | 10 +- .../Server/McpServerPromptAttribute.cs | 4 +- .../Server/McpServerResource.cs | 8 +- .../Server/McpServerResourceAttribute.cs | 4 +- .../Server/McpServerTool.cs | 10 +- .../Server/McpServerToolAttribute.cs | 4 +- .../Server/RequestContext.cs | 10 +- .../Server/StdioServerTransport.cs | 2 +- .../TokenProgress.cs | 2 +- src/ModelContextProtocol/IMcpServerBuilder.cs | 2 +- .../McpServerBuilderExtensions.cs | 6 +- .../SingleSessionMcpServerHostedService.cs | 2 +- .../AuthEventTests.cs | 4 +- .../AuthTests.cs | 14 +-- .../HttpServerIntegrationTests.cs | 2 +- .../MapMcpTests.cs | 6 +- .../SseIntegrationTests.cs | 6 +- .../SseServerIntegrationTestFixture.cs | 4 +- .../StatelessServerTests.cs | 10 +- .../StreamableHttpClientConformanceTests.cs | 6 +- .../StreamableHttpServerConformanceTests.cs | 2 +- .../Program.cs | 4 +- .../Client/McpClientExtensionsTests.cs | 22 ++--- .../Client/McpClientResourceTemplateTests.cs | 2 +- ...lientFactoryTests.cs => McpClientTests.cs} | 12 +-- .../ClientIntegrationTestFixture.cs | 4 +- .../ClientIntegrationTests.cs | 6 +- .../ClientServerTestBase.cs | 8 +- .../McpServerBuilderExtensionsPromptsTests.cs | 12 +-- ...cpServerBuilderExtensionsResourcesTests.cs | 12 +-- .../McpServerBuilderExtensionsToolsTests.cs | 36 ++++---- .../Configuration/McpServerScopedTests.cs | 2 +- .../DiagnosticTests.cs | 6 +- .../DockerEverythingServerTests.cs | 4 +- .../Protocol/ElicitationTests.cs | 2 +- .../Protocol/NotificationHandlerTests.cs | 10 +- .../Server/McpServerFactoryTests.cs | 45 --------- .../Server/McpServerLoggingLevelTests.cs | 6 +- .../Server/McpServerPromptTests.cs | 38 ++++---- .../Server/McpServerResourceTests.cs | 68 +++++++------- .../Server/McpServerTests.cs | 92 ++++++++++++------- .../Server/McpServerToolTests.cs | 60 ++++++------ .../StdioServerIntegrationTests.cs | 2 +- .../Transport/StdioClientTransportTests.cs | 4 +- 84 files changed, 586 insertions(+), 609 deletions(-) create mode 100644 src/ModelContextProtocol.Core/Client/McpClient.cs delete mode 100644 src/ModelContextProtocol.Core/Client/McpClientFactory.cs rename src/ModelContextProtocol.Core/Client/{McpClientSession.cs => McpClientImpl.cs} (77%) rename src/ModelContextProtocol.Core/{IMcpEndpoint.cs => McpSession.cs} (70%) delete mode 100644 src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs create mode 100644 src/ModelContextProtocol.Core/Server/DestinationBoundMcpServerSession.cs rename src/ModelContextProtocol.Core/Server/{IMcpServer.cs => McpServer.cs} (52%) delete mode 100644 src/ModelContextProtocol.Core/Server/McpServerFactory.cs rename src/ModelContextProtocol.Core/Server/{McpServerSession.cs => McpServerImpl.cs} (93%) rename tests/ModelContextProtocol.Tests/Client/{McpClientFactoryTests.cs => McpClientTests.cs} (91%) delete mode 100644 tests/ModelContextProtocol.Tests/Server/McpServerFactoryTests.cs diff --git a/README.md b/README.md index 5c9cfb428..e4e6cd9d4 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ dotnet add package ModelContextProtocol --prerelease ## Getting Started (Client) -To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `McpClientSession` -to a server. Once you have an `McpClientSession`, you can interact with it, such as to enumerate all available tools and invoke tools. +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 `McpClient`, you can interact with it, such as to enumerate all available tools and invoke tools. ```csharp var clientTransport = new StdioClientTransport(new StdioClientTransportOptions @@ -48,7 +48,7 @@ var clientTransport = new StdioClientTransport(new StdioClientTransportOptions 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. foreach (var tool in await client.ListToolsAsync()) @@ -224,7 +224,7 @@ McpServerOptions options = new() }, }; -await using IMcpServer server = McpServerFactory.Create(new StdioServerTransport("MyServer"), options); +await using IMcpServer server = McpServer.Create(new StdioServerTransport("MyServer"), options); await server.RunAsync(); ``` diff --git a/samples/AspNetCoreMcpServer/Tools/SampleLlmTool.cs b/samples/AspNetCoreMcpServer/Tools/SampleLlmTool.cs index 3ac7f567d..e69477452 100644 --- a/samples/AspNetCoreMcpServer/Tools/SampleLlmTool.cs +++ b/samples/AspNetCoreMcpServer/Tools/SampleLlmTool.cs @@ -12,7 +12,7 @@ public sealed class SampleLlmTool { [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] public static async Task SampleLLM( - IMcpServer thisServer, + McpServer thisServer, [Description("The prompt to send to the LLM")] string prompt, [Description("Maximum number of tokens to generate")] int maxTokens, CancellationToken cancellationToken) diff --git a/samples/ChatWithTools/Program.cs b/samples/ChatWithTools/Program.cs index ba597ae8a..a84393e15 100644 --- a/samples/ChatWithTools/Program.cs +++ b/samples/ChatWithTools/Program.cs @@ -32,7 +32,7 @@ .UseOpenTelemetry(loggerFactory: loggerFactory, configure: o => o.EnableSensitiveData = true) .Build(); -var mcpClient = await McpClientFactory.CreateAsync( +var mcpClient = await McpClient.CreateAsync( new StdioClientTransport(new() { Command = "npx", diff --git a/samples/EverythingServer/LoggingUpdateMessageSender.cs b/samples/EverythingServer/LoggingUpdateMessageSender.cs index 844aa70d8..5f524ad8a 100644 --- a/samples/EverythingServer/LoggingUpdateMessageSender.cs +++ b/samples/EverythingServer/LoggingUpdateMessageSender.cs @@ -5,7 +5,7 @@ namespace EverythingServer; -public class LoggingUpdateMessageSender(IMcpServer server, Func getMinLevel) : BackgroundService +public class LoggingUpdateMessageSender(McpServer server, Func getMinLevel) : BackgroundService { readonly Dictionary _loggingLevelMap = new() { diff --git a/samples/EverythingServer/SubscriptionMessageSender.cs b/samples/EverythingServer/SubscriptionMessageSender.cs index 774d98523..b071965dc 100644 --- a/samples/EverythingServer/SubscriptionMessageSender.cs +++ b/samples/EverythingServer/SubscriptionMessageSender.cs @@ -2,7 +2,7 @@ using ModelContextProtocol; using ModelContextProtocol.Server; -internal class SubscriptionMessageSender(IMcpServer server, HashSet subscriptions) : BackgroundService +internal class SubscriptionMessageSender(McpServer server, HashSet subscriptions) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { diff --git a/samples/EverythingServer/Tools/LongRunningTool.cs b/samples/EverythingServer/Tools/LongRunningTool.cs index 27f6ac20f..405b5e823 100644 --- a/samples/EverythingServer/Tools/LongRunningTool.cs +++ b/samples/EverythingServer/Tools/LongRunningTool.cs @@ -10,7 +10,7 @@ public class LongRunningTool { [McpServerTool(Name = "longRunningOperation"), Description("Demonstrates a long running operation with progress updates")] public static async Task LongRunningOperation( - IMcpServer server, + McpServer server, RequestContext context, int duration = 10, int steps = 5) diff --git a/samples/EverythingServer/Tools/SampleLlmTool.cs b/samples/EverythingServer/Tools/SampleLlmTool.cs index a58675c30..6bbe6e51d 100644 --- a/samples/EverythingServer/Tools/SampleLlmTool.cs +++ b/samples/EverythingServer/Tools/SampleLlmTool.cs @@ -9,7 +9,7 @@ public class SampleLlmTool { [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] public static async Task SampleLLM( - IMcpServer server, + McpServer server, [Description("The prompt to send to the LLM")] string prompt, [Description("Maximum number of tokens to generate")] int maxTokens, CancellationToken cancellationToken) diff --git a/samples/InMemoryTransport/Program.cs b/samples/InMemoryTransport/Program.cs index 5aa003901..141692fe9 100644 --- a/samples/InMemoryTransport/Program.cs +++ b/samples/InMemoryTransport/Program.cs @@ -6,7 +6,7 @@ Pipe clientToServerPipe = new(), serverToClientPipe = new(); // 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 McpServerOptions() { @@ -21,7 +21,7 @@ _ = server.RunAsync(); // Connect a client using a stream-based transport over the same in-memory pipe. -await using McpClientSession client = await McpClientFactory.CreateAsync( +await using McpClient client = await McpClient.CreateAsync( new StreamClientTransport(clientToServerPipe.Writer.AsStream(), serverToClientPipe.Reader.AsStream())); // List all tools. diff --git a/samples/ProtectedMcpClient/Program.cs b/samples/ProtectedMcpClient/Program.cs index 516227b37..df2863cd7 100644 --- a/samples/ProtectedMcpClient/Program.cs +++ b/samples/ProtectedMcpClient/Program.cs @@ -37,7 +37,7 @@ } }, httpClient, consoleLoggerFactory); -var client = await McpClientFactory.CreateAsync(transport, loggerFactory: consoleLoggerFactory); +var client = await McpClient.CreateAsync(transport, loggerFactory: consoleLoggerFactory); var tools = await client.ListToolsAsync(); if (tools.Count == 0) diff --git a/samples/QuickstartClient/Program.cs b/samples/QuickstartClient/Program.cs index 423af627f..09db291fd 100644 --- a/samples/QuickstartClient/Program.cs +++ b/samples/QuickstartClient/Program.cs @@ -21,7 +21,7 @@ Arguments = arguments, }); -await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport); +await using var mcpClient = await McpClient.CreateAsync(clientTransport); var tools = await mcpClient.ListToolsAsync(); foreach (var tool in tools) diff --git a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs index a096f9301..2c96b8c35 100644 --- a/samples/TestServerWithHosting/Tools/SampleLlmTool.cs +++ b/samples/TestServerWithHosting/Tools/SampleLlmTool.cs @@ -12,7 +12,7 @@ public sealed class SampleLlmTool { [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] public static async Task SampleLLM( - IMcpServer thisServer, + McpServer thisServer, [Description("The prompt to send to the LLM")] string prompt, [Description("Maximum number of tokens to generate")] int maxTokens, CancellationToken cancellationToken) diff --git a/src/ModelContextProtocol.AspNetCore/HttpMcpSession.cs b/src/ModelContextProtocol.AspNetCore/HttpMcpSession.cs index 1456ce565..24b0d502a 100644 --- a/src/ModelContextProtocol.AspNetCore/HttpMcpSession.cs +++ b/src/ModelContextProtocol.AspNetCore/HttpMcpSession.cs @@ -27,7 +27,7 @@ internal sealed class HttpMcpSession( private TimeProvider TimeProvider => timeProvider; - public IMcpServer? Server { get; set; } + public McpServer? Server { get; set; } public Task? ServerRunTask { get; set; } public IDisposable AcquireReference() diff --git a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs index 2a34a17a1..9893df50b 100644 --- a/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs +++ b/src/ModelContextProtocol.AspNetCore/HttpServerTransportOptions.cs @@ -20,7 +20,7 @@ public class HttpServerTransportOptions /// 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. /// - public Func? RunSessionHandler { get; set; } + public Func? RunSessionHandler { get; set; } /// /// Gets or sets whether the server should run in a stateless mode that does not require all requests for a given session diff --git a/src/ModelContextProtocol.AspNetCore/SseHandler.cs b/src/ModelContextProtocol.AspNetCore/SseHandler.cs index c5ac5a948..9097ab500 100644 --- a/src/ModelContextProtocol.AspNetCore/SseHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/SseHandler.cs @@ -54,7 +54,7 @@ public async Task HandleSseRequestAsync(HttpContext context) try { - await using var mcpServer = McpServerFactory.Create(transport, mcpServerOptions, loggerFactory, context.RequestServices); + await using var mcpServer = McpServer.Create(transport, mcpServerOptions, loggerFactory, context.RequestServices); httpMcpSession.Server = mcpServer; context.Features.Set(mcpServer); diff --git a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs index 6dac1c3e4..6dba687ef 100644 --- a/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs +++ b/src/ModelContextProtocol.AspNetCore/StreamableHttpHandler.cs @@ -244,7 +244,7 @@ private async ValueTask> CreateSes } } - var server = McpServerFactory.Create(transport, mcpServerOptions, loggerFactory, mcpServerServices); + var server = McpServer.Create(transport, mcpServerOptions, loggerFactory, mcpServerServices); context.Features.Set(server); var userIdClaim = statelessId?.UserIdClaim ?? GetUserIdClaim(context.User); @@ -306,7 +306,7 @@ private void ScheduleStatelessSessionIdWrite(HttpContext context, StreamableHttp }; } - internal static Task RunSessionAsync(HttpContext httpContext, IMcpServer session, CancellationToken requestAborted) + internal static Task RunSessionAsync(HttpContext httpContext, McpServer session, CancellationToken requestAborted) => session.RunAsync(requestAborted); // SignalR only checks for ClaimTypes.NameIdentifier in HttpConnectionDispatcher, but AspNetCore.Antiforgery checks that plus the sub and UPN claims. diff --git a/src/ModelContextProtocol.Core/Client/IClientTransport.cs b/src/ModelContextProtocol.Core/Client/IClientTransport.cs index 9cffa09dc..2201e9b4f 100644 --- a/src/ModelContextProtocol.Core/Client/IClientTransport.cs +++ b/src/ModelContextProtocol.Core/Client/IClientTransport.cs @@ -11,7 +11,7 @@ namespace ModelContextProtocol.Client; /// and servers, allowing different transport protocols to be used interchangeably. /// /// -/// When creating an , is typically used, and is +/// When creating an , is typically used, and is /// provided with the based on expected server configuration. /// /// @@ -35,11 +35,11 @@ public interface IClientTransport /// /// /// The lifetime of the returned instance is typically managed by the - /// that uses this transport. When the client is disposed, it will dispose + /// that uses this transport. When the client is disposed, it will dispose /// the transport session as well. /// /// - /// This method is used by to initialize the connection. + /// This method is used by to initialize the connection. /// /// /// The transport connection could not be established. diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs new file mode 100644 index 000000000..bda535af8 --- /dev/null +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -0,0 +1,84 @@ +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol.Client; + +/// +/// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server. +/// +public abstract class McpClient : McpSession +{ + /// + /// Gets the capabilities supported by the connected server. + /// + /// The client is not connected. + public abstract ServerCapabilities ServerCapabilities { get; } + + /// + /// Gets the implementation information of the connected server. + /// + /// + /// + /// This property provides identification details about the connected server, including its name and version. + /// It is populated during the initialization handshake and is available after a successful connection. + /// + /// + /// This information can be useful for logging, debugging, compatibility checks, and displaying server + /// information to users. + /// + /// + /// The client is not connected. + public abstract Implementation ServerInfo { get; } + + /// + /// Gets any instructions describing how to use the connected server and its features. + /// + /// + /// + /// This property contains instructions provided by the server during initialization that explain + /// how to effectively use its capabilities. These instructions can include details about available + /// tools, expected input formats, limitations, or any other helpful information. + /// + /// + /// This can be used by clients to improve an LLM's understanding of available tools, prompts, and resources. + /// It can be thought of like a "hint" to the model and may be added to a system prompt. + /// + /// + public abstract string? ServerInstructions { get; } + + /// Creates an , connecting it to the specified server. + /// The transport instance used to communicate with the server. + /// + /// A client configuration object which specifies client capabilities and protocol version. + /// If , details based on the current process will be employed. + /// + /// A logger factory for creating loggers for clients. + /// The to monitor for cancellation requests. The default is . + /// An that's connected to the specified server. + /// is . + /// is . + public static async Task CreateAsync( + IClientTransport clientTransport, + McpClientOptions? clientOptions = null, + ILoggerFactory? loggerFactory = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(clientTransport); + + var transport = await clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false); + var endpointName = clientTransport.Name; + + var clientSession = new McpClientImpl(transport, endpointName, clientOptions, loggerFactory); + try + { + await clientSession.ConnectAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + await clientSession.DisposeAsync().ConfigureAwait(false); + throw; + } + + return clientSession; + } +} diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 4e082b07a..c7f101ac9 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Client; /// -/// Provides extension methods for interacting with an . +/// Provides extension methods for interacting with an . /// /// /// @@ -38,7 +38,7 @@ public static class McpClientExtensions /// /// is . /// Thrown when the server cannot be reached or returns an error response. - public static Task PingAsync(this McpClientSession client, CancellationToken cancellationToken = default) + public static Task PingAsync(this McpClient client, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -90,7 +90,7 @@ public static Task PingAsync(this McpClientSession client, CancellationToken can /// /// is . public static async ValueTask> ListToolsAsync( - this McpClientSession client, + this McpClient client, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) { @@ -156,7 +156,7 @@ public static async ValueTask> ListToolsAsync( /// /// is . public static async IAsyncEnumerable EnumerateToolsAsync( - this McpClientSession client, + this McpClient client, JsonSerializerOptions? serializerOptions = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -203,7 +203,7 @@ public static async IAsyncEnumerable EnumerateToolsAsync( /// /// is . public static async ValueTask> ListPromptsAsync( - this McpClientSession client, CancellationToken cancellationToken = default) + this McpClient client, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -259,7 +259,7 @@ public static async ValueTask> ListPromptsAsync( /// /// is . public static async IAsyncEnumerable EnumeratePromptsAsync( - this McpClientSession client, [EnumeratorCancellation] CancellationToken cancellationToken = default) + this McpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -309,7 +309,7 @@ public static async IAsyncEnumerable EnumeratePromptsAsync( /// Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt. /// is . public static ValueTask GetPromptAsync( - this McpClientSession client, + this McpClient client, string name, IReadOnlyDictionary? arguments = null, JsonSerializerOptions? serializerOptions = null, @@ -347,7 +347,7 @@ public static ValueTask GetPromptAsync( /// /// is . public static async ValueTask> ListResourceTemplatesAsync( - this McpClientSession client, CancellationToken cancellationToken = default) + this McpClient client, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -404,7 +404,7 @@ public static async ValueTask> ListResourceTemp /// /// is . public static async IAsyncEnumerable EnumerateResourceTemplatesAsync( - this McpClientSession client, [EnumeratorCancellation] CancellationToken cancellationToken = default) + this McpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -458,7 +458,7 @@ public static async IAsyncEnumerable EnumerateResourc /// /// is . public static async ValueTask> ListResourcesAsync( - this McpClientSession client, CancellationToken cancellationToken = default) + this McpClient client, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -515,7 +515,7 @@ public static async ValueTask> ListResourcesAsync( /// /// is . public static async IAsyncEnumerable EnumerateResourcesAsync( - this McpClientSession client, [EnumeratorCancellation] CancellationToken cancellationToken = default) + this McpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -549,7 +549,7 @@ public static async IAsyncEnumerable EnumerateResourcesAsync( /// is . /// is empty or composed entirely of whitespace. public static ValueTask ReadResourceAsync( - this McpClientSession client, string uri, CancellationToken cancellationToken = default) + this McpClient client, string uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uri); @@ -571,7 +571,7 @@ public static ValueTask ReadResourceAsync( /// is . /// is . public static ValueTask ReadResourceAsync( - this McpClientSession client, Uri uri, CancellationToken cancellationToken = default) + this McpClient client, Uri uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(uri); @@ -590,7 +590,7 @@ public static ValueTask ReadResourceAsync( /// is . /// is empty or composed entirely of whitespace. public static ValueTask ReadResourceAsync( - this McpClientSession client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) + this McpClient client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uriTemplate); @@ -633,7 +633,7 @@ public static ValueTask ReadResourceAsync( /// is . /// is empty or composed entirely of whitespace. /// The server returned an error response. - public static ValueTask CompleteAsync(this McpClientSession client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) + public static ValueTask CompleteAsync(this McpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(reference); @@ -670,13 +670,13 @@ public static ValueTask CompleteAsync(this McpClientSession clie /// /// /// To handle resource change notifications, register an event handler for the appropriate notification events, - /// such as with . + /// such as with . /// /// /// is . /// is . /// is empty or composed entirely of whitespace. - public static Task SubscribeToResourceAsync(this McpClientSession client, string uri, CancellationToken cancellationToken = default) + public static Task SubscribeToResourceAsync(this McpClient client, string uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uri); @@ -708,12 +708,12 @@ public static Task SubscribeToResourceAsync(this McpClientSession client, string /// /// /// To handle resource change notifications, register an event handler for the appropriate notification events, - /// such as with . + /// such as with . /// /// /// is . /// is . - public static Task SubscribeToResourceAsync(this McpClientSession client, Uri uri, CancellationToken cancellationToken = default) + public static Task SubscribeToResourceAsync(this McpClient client, Uri uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(uri); @@ -745,7 +745,7 @@ public static Task SubscribeToResourceAsync(this McpClientSession client, Uri ur /// is . /// is . /// is empty or composed entirely of whitespace. - public static Task UnsubscribeFromResourceAsync(this McpClientSession client, string uri, CancellationToken cancellationToken = default) + public static Task UnsubscribeFromResourceAsync(this McpClient client, string uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(uri); @@ -781,7 +781,7 @@ public static Task UnsubscribeFromResourceAsync(this McpClientSession client, st /// /// is . /// is . - public static Task UnsubscribeFromResourceAsync(this McpClientSession client, Uri uri, CancellationToken cancellationToken = default) + public static Task UnsubscribeFromResourceAsync(this McpClient client, Uri uri, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNull(uri); @@ -825,7 +825,7 @@ public static Task UnsubscribeFromResourceAsync(this McpClientSession client, Ur /// /// public static ValueTask CallToolAsync( - this McpClientSession client, + this McpClient client, string toolName, IReadOnlyDictionary? arguments = null, IProgress? progress = null, @@ -854,7 +854,7 @@ public static ValueTask CallToolAsync( cancellationToken: cancellationToken); static async ValueTask SendRequestWithProgressAsync( - McpClientSession client, + McpClient client, string toolName, IReadOnlyDictionary? arguments, IProgress progress, @@ -1031,11 +1031,11 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat /// /// /// Log messages are delivered as notifications to the client and can be captured by registering - /// appropriate event handlers with the client implementation, such as with . + /// appropriate event handlers with the client implementation, such as with . /// /// /// is . - public static Task SetLoggingLevel(this McpClientSession client, LoggingLevel level, CancellationToken cancellationToken = default) + public static Task SetLoggingLevel(this McpClient client, LoggingLevel level, CancellationToken cancellationToken = default) { Throw.IfNull(client); @@ -1066,12 +1066,12 @@ public static Task SetLoggingLevel(this McpClientSession client, LoggingLevel le /// /// /// Log messages are delivered as notifications to the client and can be captured by registering - /// appropriate event handlers with the client implementation, such as with . + /// appropriate event handlers with the client implementation, such as with . /// /// /// is . - public static Task SetLoggingLevel(this McpClientSession client, LogLevel level, CancellationToken cancellationToken = default) => - SetLoggingLevel(client, McpServerSession.ToLoggingLevel(level), cancellationToken); + public static Task SetLoggingLevel(this McpClient client, LogLevel level, CancellationToken cancellationToken = default) => + SetLoggingLevel(client, McpServerImpl.ToLoggingLevel(level), cancellationToken); /// Convers a dictionary with values to a dictionary with values. private static Dictionary? ToArgumentsDictionary( diff --git a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs deleted file mode 100644 index 57827a42e..000000000 --- a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace ModelContextProtocol.Client; - -/// -/// Provides factory methods for creating Model Context Protocol (MCP) clients. -/// -/// -/// This factory class is the primary way to instantiate instances -/// that connect to MCP servers. It handles the creation and connection -/// of appropriate implementations through the supplied transport. -/// -public static class McpClientFactory -{ - /// Creates an , connecting it to the specified server. - /// The transport instance used to communicate with the server. - /// - /// A client configuration object which specifies client capabilities and protocol version. - /// If , details based on the current process will be employed. - /// - /// A logger factory for creating loggers for clients. - /// The to monitor for cancellation requests. The default is . - /// An that's connected to the specified server. - /// is . - /// is . - public static async Task CreateAsync( - IClientTransport clientTransport, - McpClientOptions? clientOptions = null, - ILoggerFactory? loggerFactory = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(clientTransport); - - var transport = await clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false); - var endpointName = clientTransport.Name; - - var clientSession = new McpClientSession(transport, endpointName, clientOptions, loggerFactory); - try - { - await clientSession.ConnectAsync(cancellationToken).ConfigureAwait(false); - } - catch - { - await clientSession.DisposeAsync().ConfigureAwait(false); - throw; - } - - return clientSession; - } -} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClientSession.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs similarity index 77% rename from src/ModelContextProtocol.Core/Client/McpClientSession.cs rename to src/ModelContextProtocol.Core/Client/McpClientImpl.cs index 584b1c7e9..4a1e13973 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientSession.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs @@ -5,14 +5,12 @@ namespace ModelContextProtocol.Client; -/// -/// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server. -/// -public sealed partial class McpClientSession : IMcpEndpoint +/// +internal sealed partial class McpClientImpl : McpClient { private static Implementation DefaultImplementation { get; } = new() { - Name = AssemblyNameHelper.DefaultAssemblyName.Name ?? nameof(McpClientSession), + Name = AssemblyNameHelper.DefaultAssemblyName.Name ?? nameof(McpClient), Version = AssemblyNameHelper.DefaultAssemblyName.Version?.ToString() ?? "1.0.0", }; @@ -31,20 +29,20 @@ public sealed partial class McpClientSession : IMcpEndpoint private int _isDisposed; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The transport to use for communication with the server. /// The name of the endpoint for logging and debug purposes. /// Options for the client, defining protocol version and capabilities. /// The logger factory. - internal McpClientSession(ITransport transport, string endpointName, McpClientOptions? options, ILoggerFactory? loggerFactory) + internal McpClientImpl(ITransport transport, string endpointName, McpClientOptions? options, ILoggerFactory? loggerFactory) { options ??= new(); _transport = transport; _endpointName = $"Client ({options.ClientInfo?.Name ?? DefaultImplementation.Name} {options.ClientInfo?.Version ?? DefaultImplementation.Version})"; _options = options; - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; var notificationHandlers = new NotificationHandlers(); var requestHandlers = new RequestHandlers(); @@ -111,45 +109,16 @@ private void RegisterHandlers(ClientCapabilities capabilities, NotificationHandl } /// - public string? SessionId => _transport.SessionId; + public override string? SessionId => _transport.SessionId; - /// - /// Gets the capabilities supported by the connected server. - /// - /// The client is not connected. - public ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected."); + /// + public override ServerCapabilities ServerCapabilities => _serverCapabilities ?? throw new InvalidOperationException("The client is not connected."); - /// - /// Gets the implementation information of the connected server. - /// - /// - /// - /// This property provides identification details about the connected server, including its name and version. - /// It is populated during the initialization handshake and is available after a successful connection. - /// - /// - /// This information can be useful for logging, debugging, compatibility checks, and displaying server - /// information to users. - /// - /// - /// The client is not connected. - public Implementation ServerInfo => _serverInfo ?? throw new InvalidOperationException("The client is not connected."); + /// + public override Implementation ServerInfo => _serverInfo ?? throw new InvalidOperationException("The client is not connected."); - /// - /// Gets any instructions describing how to use the connected server and its features. - /// - /// - /// - /// This property contains instructions provided by the server during initialization that explain - /// how to effectively use its capabilities. These instructions can include details about available - /// tools, expected input formats, limitations, or any other helpful information. - /// - /// - /// This can be used by clients to improve an LLM's understanding of available tools, prompts, and resources. - /// It can be thought of like a "hint" to the model and may be added to a system prompt. - /// - /// - public string? ServerInstructions => _serverInstructions; + /// + public override string? ServerInstructions => _serverInstructions; /// /// Asynchronously connects to an MCP server, establishes the transport connection, and completes the initialization handshake. @@ -232,19 +201,19 @@ await this.SendNotificationAsync( } /// - public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) + public override Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) => _sessionHandler.SendRequestAsync(request, cancellationToken); /// - public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) => _sessionHandler.SendMessageAsync(message, cancellationToken); /// - public IAsyncDisposable RegisterNotificationHandler(string method, Func handler) + public override IAsyncDisposable RegisterNotificationHandler(string method, Func handler) => _sessionHandler.RegisterNotificationHandler(method, handler); /// - public async ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) { diff --git a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs index 93145a3ac..d4ed41db4 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientOptions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientOptions.cs @@ -3,10 +3,10 @@ namespace ModelContextProtocol.Client; /// -/// Provides configuration options for creating instances. +/// Provides configuration options for creating instances. /// /// -/// These options are typically passed to when creating a client. +/// These options are typically passed to when creating a client. /// They define client capabilities, protocol version, and other client-specific settings. /// public sealed class McpClientOptions diff --git a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs index 4726b46e7..6cef1d778 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs @@ -20,9 +20,9 @@ namespace ModelContextProtocol.Client; /// public sealed class McpClientPrompt { - private readonly McpClientSession _client; + private readonly McpClient _client; - internal McpClientPrompt(McpClientSession client, Prompt prompt) + internal McpClientPrompt(McpClient client, Prompt prompt) { _client = client; ProtocolPrompt = prompt; diff --git a/src/ModelContextProtocol.Core/Client/McpClientResource.cs b/src/ModelContextProtocol.Core/Client/McpClientResource.cs index 91ebf4d44..7f7297866 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResource.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResource.cs @@ -15,9 +15,9 @@ namespace ModelContextProtocol.Client; /// public sealed class McpClientResource { - private readonly McpClientSession _client; + private readonly McpClient _client; - internal McpClientResource(McpClientSession client, Resource resource) + internal McpClientResource(McpClient client, Resource resource) { _client = client; ProtocolResource = resource; @@ -58,7 +58,7 @@ internal McpClientResource(McpClientSession client, Resource resource) /// A containing the resource's result with content and messages. /// /// - /// This is a convenience method that internally calls . + /// This is a convenience method that internally calls . /// /// public ValueTask ReadAsync( diff --git a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs index 678f7ccc6..7cd1fa986 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs @@ -15,9 +15,9 @@ namespace ModelContextProtocol.Client; /// public sealed class McpClientResourceTemplate { - private readonly McpClientSession _client; + private readonly McpClient _client; - internal McpClientResourceTemplate(McpClientSession client, ResourceTemplate resourceTemplate) + internal McpClientResourceTemplate(McpClient client, ResourceTemplate resourceTemplate) { _client = client; ProtocolResourceTemplate = resourceTemplate; diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs index d0be89297..b66c2368f 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs @@ -6,11 +6,11 @@ namespace ModelContextProtocol.Client; /// -/// Provides an that calls a tool via an . +/// Provides an that calls a tool via an . /// /// /// -/// The class encapsulates an along with a description of +/// The class encapsulates an along with a description of /// a tool available via that client, allowing it to be invoked as an . This enables integration /// with AI models that support function calling capabilities. /// @@ -20,7 +20,7 @@ namespace ModelContextProtocol.Client; /// /// /// Typically, you would get instances of this class by calling the -/// or extension methods on an instance. +/// or extension methods on an instance. /// /// public sealed class McpClientTool : AIFunction @@ -32,13 +32,13 @@ public sealed class McpClientTool : AIFunction ["Strict"] = false, // some MCP schemas may not meet "strict" requirements }); - private readonly McpClientSession _client; + private readonly McpClient _client; private readonly string _name; private readonly string _description; private readonly IProgress? _progress; internal McpClientTool( - McpClientSession client, + McpClient client, Tool tool, JsonSerializerOptions serializerOptions, string? name = null, diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index 190bec0b2..ebc729bfc 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -44,7 +44,7 @@ public StreamableHttpClientSessionTransport( _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; // We connect with the initialization request with the MCP transport. This means that any errors won't be observed - // until the first call to SendMessageAsync. Fortunately, that happens internally in McpClientFactory.ConnectAsync + // until the first call to SendMessageAsync. Fortunately, that happens internally in McpClient.ConnectAsync // so we still throw any connection-related Exceptions from there and never expose a pre-connected client to the user. SetConnected(); } diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs index 345c10530..cf6e57be6 100644 --- a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs +++ b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol; /// -/// Provides extension methods for interacting with an . +/// Provides extension methods for interacting with an . /// /// /// @@ -16,8 +16,8 @@ namespace ModelContextProtocol; /// simplifying JSON-RPC communication by handling serialization and deserialization of parameters and results. /// /// -/// These extension methods are designed to be used with both client () and -/// server () implementations of the interface. +/// These extension methods are designed to be used with both client () and +/// server () implementations of the interface. /// /// public static class McpEndpointExtensions @@ -35,7 +35,7 @@ public static class McpEndpointExtensions /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. The task result contains the deserialized result. public static ValueTask SendRequestAsync( - this IMcpEndpoint endpoint, + this McpSession endpoint, string method, TParameters parameters, JsonSerializerOptions? serializerOptions = null, @@ -65,7 +65,7 @@ public static ValueTask SendRequestAsync( /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. The task result contains the deserialized result. internal static async ValueTask SendRequestAsync( - this IMcpEndpoint endpoint, + this McpSession endpoint, string method, TParameters parameters, JsonTypeInfo parametersTypeInfo, @@ -104,7 +104,7 @@ internal static async ValueTask SendRequestAsync( /// changes in state. /// /// - public static Task SendNotificationAsync(this IMcpEndpoint client, string method, CancellationToken cancellationToken = default) + public static Task SendNotificationAsync(this McpSession client, string method, CancellationToken cancellationToken = default) { Throw.IfNull(client); Throw.IfNullOrWhiteSpace(method); @@ -136,7 +136,7 @@ public static Task SendNotificationAsync(this IMcpEndpoint client, string method /// /// public static Task SendNotificationAsync( - this IMcpEndpoint endpoint, + this McpSession endpoint, string method, TParameters parameters, JsonSerializerOptions? serializerOptions = null, @@ -158,7 +158,7 @@ public static Task SendNotificationAsync( /// The type information for request parameter serialization. /// The to monitor for cancellation requests. The default is . internal static Task SendNotificationAsync( - this IMcpEndpoint endpoint, + this McpSession endpoint, string method, TParameters parameters, JsonTypeInfo parametersTypeInfo, @@ -192,7 +192,7 @@ internal static Task SendNotificationAsync( /// /// public static Task NotifyProgressAsync( - this IMcpEndpoint endpoint, + this McpSession endpoint, ProgressToken progressToken, ProgressNotificationValue progress, CancellationToken cancellationToken = default) diff --git a/src/ModelContextProtocol.Core/IMcpEndpoint.cs b/src/ModelContextProtocol.Core/McpSession.cs similarity index 70% rename from src/ModelContextProtocol.Core/IMcpEndpoint.cs rename to src/ModelContextProtocol.Core/McpSession.cs index d48cc2d16..82caca12b 100644 --- a/src/ModelContextProtocol.Core/IMcpEndpoint.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -5,28 +5,28 @@ namespace ModelContextProtocol; /// -/// Represents a client or server Model Context Protocol (MCP) endpoint. +/// Represents a client or server Model Context Protocol (MCP) session. /// /// /// -/// The MCP endpoint provides the core communication functionality used by both clients and servers: +/// The MCP session provides the core communication functionality used by both clients and servers: /// /// Sending JSON-RPC requests and receiving responses. -/// Sending notifications to the connected endpoint. +/// Sending notifications to the connected session. /// Registering handlers for receiving notifications. /// /// /// -/// serves as the base interface for both and -/// interfaces, providing the common functionality needed for MCP protocol +/// serves as the base interface for both and +/// interfaces, providing the common functionality needed for MCP protocol /// communication. Most applications will use these more specific interfaces rather than working with -/// directly. +/// directly. /// /// -/// All MCP endpoints should be properly disposed after use as they implement . +/// All MCP sessions should be properly disposed after use as they implement . /// /// -public interface IMcpEndpoint : IAsyncDisposable +public abstract class McpSession : IAsyncDisposable { /// Gets an identifier associated with the current MCP session. /// @@ -34,24 +34,24 @@ public interface IMcpEndpoint : IAsyncDisposable /// Can return if the session hasn't initialized or if the transport doesn't /// support multiple sessions (as is the case with STDIO). /// - string? SessionId { get; } + public abstract string? SessionId { get; } /// - /// Sends a JSON-RPC request to the connected endpoint and waits for a response. + /// Sends a JSON-RPC request to the connected session and waits for a response. /// /// The JSON-RPC request to send. /// The to monitor for cancellation requests. The default is . - /// A task containing the endpoint's response. + /// A task containing the session's response. /// The transport is not connected, or another error occurs during request processing. /// An error occured during request processing. /// /// This method provides low-level access to send raw JSON-RPC requests. For most use cases, /// consider using the strongly-typed extension methods that provide a more convenient API. /// - Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default); + public abstract Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default); /// - /// Sends a JSON-RPC message to the connected endpoint. + /// Sends a JSON-RPC message to the connected session. /// /// /// The JSON-RPC message to send. This can be any type that implements JsonRpcMessage, such as @@ -65,18 +65,21 @@ public interface IMcpEndpoint : IAsyncDisposable /// /// This method provides low-level access to send any JSON-RPC message. For specific message types, /// consider using the higher-level methods such as or extension methods - /// like , + /// like , /// which provide a simpler API. /// /// /// The method will serialize the message and transmit it using the underlying transport mechanism. /// /// - Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default); + public abstract Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default); /// Registers a handler to be invoked when a notification for the specified method is received. /// The notification method. /// The handler to be invoked. /// An that will remove the registered handler when disposed. - IAsyncDisposable RegisterNotificationHandler(string method, Func handler); + public abstract IAsyncDisposable RegisterNotificationHandler(string method, Func handler); + + /// + public abstract ValueTask DisposeAsync(); } diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index ebe698135..1bcc25f4f 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -78,7 +78,7 @@ public sealed class ClientCapabilities /// /// /// Handlers provided via will be registered with the client for the lifetime of the client. - /// For transient handlers, may be used to register a handler that can + /// For transient handlers, may be used to register a handler that can /// then be unregistered by disposing of the returned from the method. /// /// diff --git a/src/ModelContextProtocol.Core/Protocol/ITransport.cs b/src/ModelContextProtocol.Core/Protocol/ITransport.cs index e35b3a6fb..4fac03597 100644 --- a/src/ModelContextProtocol.Core/Protocol/ITransport.cs +++ b/src/ModelContextProtocol.Core/Protocol/ITransport.cs @@ -62,7 +62,7 @@ public interface ITransport : IAsyncDisposable /// /// /// This is a core method used by higher-level abstractions in the MCP protocol implementation. - /// Most client code should use the higher-level methods provided by , + /// Most client code should use the higher-level methods provided by , /// , , or , /// rather than accessing this method directly. /// diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs index b3176937c..65003667e 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessage.cs @@ -44,7 +44,7 @@ private protected JsonRpcMessage() /// /// /// This is used to support the Streamable HTTP transport in its default stateful mode. In this mode, - /// the outlives the initial HTTP request context it was created on, and new + /// the outlives the initial HTTP request context it was created on, and new /// JSON-RPC messages can originate from future HTTP requests. This allows the transport to flow the /// context with the JSON-RPC message. This is particularly useful for enabling IHttpContextAccessor /// in tool calls. diff --git a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs index 6a4b2e62a..023a869a4 100644 --- a/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ServerCapabilities.cs @@ -77,7 +77,7 @@ public sealed class ServerCapabilities /// /// /// Handlers provided via will be registered with the server for the lifetime of the server. - /// For transient handlers, may be used to register a handler that can + /// For transient handlers, may be used to register a handler that can /// then be unregistered by disposing of the returned from the method. /// /// diff --git a/src/ModelContextProtocol.Core/README.md b/src/ModelContextProtocol.Core/README.md index 121c61841..f6cffaf68 100644 --- a/src/ModelContextProtocol.Core/README.md +++ b/src/ModelContextProtocol.Core/README.md @@ -27,8 +27,8 @@ dotnet add package ModelContextProtocol.Core --prerelease ## Getting Started (Client) -To get started writing a client, the `McpClientFactory.CreateAsync` method is used to instantiate and connect an `McpClientSession` -to a server. Once you have an `McpClientSession`, you can interact with it, such as to enumerate all available tools and invoke tools. +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 `McpClient`, you can interact with it, such as to enumerate all available tools and invoke tools. ```csharp var clientTransport = new StdioClientTransport(new StdioClientTransportOptions @@ -38,7 +38,7 @@ var clientTransport = new StdioClientTransport(new StdioClientTransportOptions 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. foreach (var tool in await client.ListToolsAsync()) diff --git a/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs b/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs index 3372072fe..df8006174 100644 --- a/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs +++ b/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs @@ -17,13 +17,13 @@ internal sealed class RequestServiceProvider( /// Gets whether the specified type is in the list of additional types this service provider wraps around the one in a provided request's services. public static bool IsAugmentedWith(Type serviceType) => serviceType == typeof(RequestContext) || - serviceType == typeof(IMcpServer) || + serviceType == typeof(McpServer) || serviceType == typeof(IProgress); /// public object? GetService(Type serviceType) => serviceType == typeof(RequestContext) ? request : - serviceType == typeof(IMcpServer) ? request.Server : + serviceType == typeof(McpServer) ? request.Server : serviceType == typeof(IProgress) ? (request.Params?.ProgressToken is { } progressToken ? new TokenProgress(request.Server, progressToken) : NullProgress.Instance) : innerServices?.GetService(serviceType); diff --git a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs deleted file mode 100644 index e11604681..000000000 --- a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs +++ /dev/null @@ -1,35 +0,0 @@ -using ModelContextProtocol.Protocol; -using System.Diagnostics; - -namespace ModelContextProtocol.Server; - -internal sealed class DestinationBoundMcpServer(McpServerSession server, ITransport? transport) : IMcpServer -{ - public string? SessionId => transport?.SessionId ?? server.SessionId; - public ClientCapabilities? ClientCapabilities => server.ClientCapabilities; - public Implementation? ClientInfo => server.ClientInfo; - public McpServerOptions ServerOptions => server.ServerOptions; - public IServiceProvider? Services => server.Services; - public LoggingLevel? LoggingLevel => server.LoggingLevel; - - public ValueTask DisposeAsync() => server.DisposeAsync(); - - public IAsyncDisposable RegisterNotificationHandler(string method, Func handler) => server.RegisterNotificationHandler(method, handler); - - // This will throw because the server must already be running for this class to be constructed, but it should give us a good Exception message. - public Task RunAsync(CancellationToken cancellationToken) => server.RunAsync(cancellationToken); - - public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) - { - Debug.Assert(message.RelatedTransport is null); - message.RelatedTransport = transport; - return server.SendMessageAsync(message, cancellationToken); - } - - public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) - { - Debug.Assert(request.RelatedTransport is null); - request.RelatedTransport = transport; - return server.SendRequestAsync(request, cancellationToken); - } -} diff --git a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServerSession.cs b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServerSession.cs new file mode 100644 index 000000000..2220ed17c --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServerSession.cs @@ -0,0 +1,35 @@ +using ModelContextProtocol.Protocol; +using System.Diagnostics; + +namespace ModelContextProtocol.Server; + +internal sealed class DestinationBoundMcpServerSession(McpServerImpl server, ITransport? transport) : McpServer +{ + public override string? SessionId => transport?.SessionId ?? server.SessionId; + public override ClientCapabilities? ClientCapabilities => server.ClientCapabilities; + public override Implementation? ClientInfo => server.ClientInfo; + public override McpServerOptions ServerOptions => server.ServerOptions; + public override IServiceProvider? Services => server.Services; + public override LoggingLevel? LoggingLevel => server.LoggingLevel; + + public override ValueTask DisposeAsync() => server.DisposeAsync(); + + public override IAsyncDisposable RegisterNotificationHandler(string method, Func handler) => server.RegisterNotificationHandler(method, handler); + + // This will throw because the server must already be running for this class to be constructed, but it should give us a good Exception message. + public override Task RunAsync(CancellationToken cancellationToken) => server.RunAsync(cancellationToken); + + public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + { + Debug.Assert(message.RelatedTransport is null); + message.RelatedTransport = transport; + return server.SendMessageAsync(message, cancellationToken); + } + + public override Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) + { + Debug.Assert(request.RelatedTransport is null); + request.RelatedTransport = transport; + return server.SendRequestAsync(request, cancellationToken); + } +} diff --git a/src/ModelContextProtocol.Core/Server/IMcpServer.cs b/src/ModelContextProtocol.Core/Server/McpServer.cs similarity index 52% rename from src/ModelContextProtocol.Core/Server/IMcpServer.cs rename to src/ModelContextProtocol.Core/Server/McpServer.cs index ec2b87ade..de5ed6add 100644 --- a/src/ModelContextProtocol.Core/Server/IMcpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; namespace ModelContextProtocol.Server; @@ -5,7 +6,7 @@ namespace ModelContextProtocol.Server; /// /// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. /// -public interface IMcpServer : IMcpEndpoint +public abstract class McpServer : McpSession { /// /// Gets the capabilities supported by the client. @@ -21,7 +22,7 @@ public interface IMcpServer : IMcpEndpoint /// are available when interacting with the client. /// /// - ClientCapabilities? ClientCapabilities { get; } + public abstract ClientCapabilities? ClientCapabilities { get; } /// /// Gets the version and implementation information of the connected client. @@ -36,7 +37,7 @@ public interface IMcpServer : IMcpEndpoint /// or implementing client-specific behaviors. /// /// - Implementation? ClientInfo { get; } + public abstract Implementation? ClientInfo { get; } /// /// Gets the options used to construct this server. @@ -45,18 +46,40 @@ public interface IMcpServer : IMcpEndpoint /// These options define the server's capabilities, protocol version, and other configuration /// settings that were used to initialize the server. /// - McpServerOptions ServerOptions { get; } + public abstract McpServerOptions ServerOptions { get; } /// /// Gets the service provider for the server. /// - IServiceProvider? Services { get; } + public abstract IServiceProvider? Services { get; } /// Gets the last logging level set by the client, or if it's never been set. - LoggingLevel? LoggingLevel { get; } + public abstract LoggingLevel? LoggingLevel { get; } /// /// Runs the server, listening for and handling client requests. /// - Task RunAsync(CancellationToken cancellationToken = default); + public abstract Task RunAsync(CancellationToken cancellationToken = default); + + /// + /// Creates a new instance of an . + /// + /// Transport to use for the server representing an already-established MCP session. + /// Configuration options for this server, including capabilities. + /// Logger factory to use for logging. If null, logging will be disabled. + /// Optional service provider to create new instances of tools and other dependencies. + /// An instance that should be disposed when no longer needed. + /// is . + /// is . + public static McpServer Create( + ITransport transport, + McpServerOptions serverOptions, + ILoggerFactory? loggerFactory = null, + IServiceProvider? serviceProvider = null) + { + Throw.IfNull(transport); + Throw.IfNull(serverOptions); + + return new McpServerImpl(transport, serverOptions, loggerFactory, serviceProvider); + } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs index 4f19adff8..73240cf90 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs @@ -8,7 +8,7 @@ namespace ModelContextProtocol.Server; /// -/// Provides extension methods for interacting with an instance. +/// Provides extension methods for interacting with an instance. /// public static class McpServerExtensions { @@ -27,7 +27,7 @@ public static class McpServerExtensions /// and token limits. /// public static ValueTask SampleAsync( - this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default) + this McpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default) { Throw.IfNull(server); ThrowIfSamplingUnsupported(server); @@ -56,7 +56,7 @@ public static ValueTask SampleAsync( /// handling different content types such as text, images, and audio. /// public static async Task SampleAsync( - this IMcpServer server, + this McpServer server, IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) { Throw.IfNull(server); @@ -161,7 +161,7 @@ public static async Task SampleAsync( /// The that can be used to issue sampling requests to the client. /// is . /// The client does not support sampling. - public static IChatClient AsSamplingChatClient(this IMcpServer server) + public static IChatClient AsSamplingChatClient(this McpServer server) { Throw.IfNull(server); ThrowIfSamplingUnsupported(server); @@ -172,7 +172,7 @@ public static IChatClient AsSamplingChatClient(this IMcpServer server) /// Gets an on which logged messages will be sent as notifications to the client. /// The server to wrap as an . /// An that can be used to log to the client.. - public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server) + public static ILoggerProvider AsClientLoggerProvider(this McpServer server) { Throw.IfNull(server); @@ -195,7 +195,7 @@ public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server) /// or other structured data sources that the client makes available through the protocol. /// public static ValueTask RequestRootsAsync( - this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default) + this McpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default) { Throw.IfNull(server); ThrowIfRootsUnsupported(server); @@ -221,7 +221,7 @@ public static ValueTask RequestRootsAsync( /// This method requires the client to support the elicitation capability. /// public static ValueTask ElicitAsync( - this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default) + this McpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default) { Throw.IfNull(server); ThrowIfElicitationUnsupported(server); @@ -234,7 +234,7 @@ public static ValueTask ElicitAsync( cancellationToken: cancellationToken); } - private static void ThrowIfSamplingUnsupported(IMcpServer server) + private static void ThrowIfSamplingUnsupported(McpServer server) { if (server.ClientCapabilities?.Sampling is null) { @@ -247,7 +247,7 @@ private static void ThrowIfSamplingUnsupported(IMcpServer server) } } - private static void ThrowIfRootsUnsupported(IMcpServer server) + private static void ThrowIfRootsUnsupported(McpServer server) { if (server.ClientCapabilities?.Roots is null) { @@ -260,7 +260,7 @@ private static void ThrowIfRootsUnsupported(IMcpServer server) } } - private static void ThrowIfElicitationUnsupported(IMcpServer server) + private static void ThrowIfElicitationUnsupported(McpServer server) { if (server.ClientCapabilities?.Elicitation is null) { @@ -274,7 +274,7 @@ private static void ThrowIfElicitationUnsupported(IMcpServer server) } /// Provides an implementation that's implemented via client sampling. - private sealed class SamplingChatClient(IMcpServer server) : IChatClient + private sealed class SamplingChatClient(McpServer server) : IChatClient { /// public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => @@ -311,7 +311,7 @@ void IDisposable.Dispose() { } // nop /// Provides an implementation for creating loggers /// that send logging message notifications to the client for logged messages. /// - private sealed class ClientLoggerProvider(IMcpServer server) : ILoggerProvider + private sealed class ClientLoggerProvider(McpServer server) : ILoggerProvider { /// public ILogger CreateLogger(string categoryName) @@ -324,7 +324,7 @@ public ILogger CreateLogger(string categoryName) /// void IDisposable.Dispose() { } - private sealed class ClientLogger(IMcpServer server, string categoryName) : ILogger + private sealed class ClientLogger(McpServer server, string categoryName) : ILogger { /// public IDisposable? BeginScope(TState state) where TState : notnull => @@ -333,7 +333,7 @@ private sealed class ClientLogger(IMcpServer server, string categoryName) : ILog /// public bool IsEnabled(LogLevel logLevel) => server?.LoggingLevel is { } loggingLevel && - McpServerSession.ToLoggingLevel(logLevel) >= loggingLevel; + McpServerImpl.ToLoggingLevel(logLevel) >= loggingLevel; /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -351,7 +351,7 @@ void Log(LogLevel logLevel, string message) { _ = server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams { - Level = McpServerSession.ToLoggingLevel(logLevel), + Level = McpServerImpl.ToLoggingLevel(logLevel), Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String), Logger = categoryName, }); diff --git a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs deleted file mode 100644 index 79e3d8c10..000000000 --- a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Microsoft.Extensions.Logging; -using ModelContextProtocol.Protocol; - -namespace ModelContextProtocol.Server; - -/// -/// Provides a factory for creating instances. -/// -/// -/// This is the recommended way to create instances. -/// The factory handles proper initialization of server instances with the required dependencies. -/// -public static class McpServerFactory -{ - /// - /// Creates a new instance of an . - /// - /// Transport to use for the server representing an already-established MCP session. - /// Configuration options for this server, including capabilities. - /// Logger factory to use for logging. If null, logging will be disabled. - /// Optional service provider to create new instances of tools and other dependencies. - /// An instance that should be disposed when no longer needed. - /// is . - /// is . - public static IMcpServer Create( - ITransport transport, - McpServerOptions serverOptions, - ILoggerFactory? loggerFactory = null, - IServiceProvider? serviceProvider = null) - { - Throw.IfNull(transport); - Throw.IfNull(serverOptions); - - return new McpServerSession(transport, serverOptions, loggerFactory, serviceProvider); - } -} diff --git a/src/ModelContextProtocol.Core/Server/McpServerSession.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs similarity index 93% rename from src/ModelContextProtocol.Core/Server/McpServerSession.cs rename to src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 1614399c1..77b47d6a3 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerSession.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -8,11 +8,11 @@ namespace ModelContextProtocol.Server; /// -public sealed class McpServerSession : IMcpServer +internal sealed class McpServerImpl : McpServer { internal static Implementation DefaultImplementation { get; } = new() { - Name = AssemblyNameHelper.DefaultAssemblyName.Name ?? nameof(McpServerSession), + Name = AssemblyNameHelper.DefaultAssemblyName.Name ?? nameof(McpServer), Version = AssemblyNameHelper.DefaultAssemblyName.Version?.ToString() ?? "1.0.0", }; @@ -24,6 +24,9 @@ public sealed class McpServerSession : IMcpServer private readonly RequestHandlers _requestHandlers; private readonly McpSessionHandler _sessionHandler; + private ClientCapabilities? _clientCapabilities; + private Implementation? _clientInfo; + private readonly string _serverOnlyEndpointName; private string _endpointName; private int _started; @@ -38,7 +41,7 @@ public sealed class McpServerSession : IMcpServer private StrongBox? _loggingLevel; /// - /// Creates a new instance of . + /// Creates a new instance of . /// /// Transport to use for the server representing an already-established session. /// Configuration options for this server, including capabilities. @@ -46,7 +49,7 @@ public sealed class McpServerSession : IMcpServer /// Logger factory to use for logging /// Optional service provider to use for dependency injection /// The server was incorrectly configured. - public McpServerSession(ITransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider) + public McpServerImpl(ITransport transport, McpServerOptions options, ILoggerFactory? loggerFactory, IServiceProvider? serviceProvider) { Throw.IfNull(transport); Throw.IfNull(options); @@ -59,9 +62,9 @@ public McpServerSession(ITransport transport, McpServerOptions options, ILoggerF _serverOnlyEndpointName = $"Server ({options.ServerInfo?.Name ?? DefaultImplementation.Name} {options.ServerInfo?.Version ?? DefaultImplementation.Version})"; _endpointName = _serverOnlyEndpointName; _servicesScopePerRequest = options.ScopeRequests; - _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; - ClientInfo = options.KnownClientInfo; + _clientInfo = options.KnownClientInfo; UpdateEndpointNameWithClientInfo(); _notificationHandlers = new(); @@ -108,28 +111,28 @@ void Register(McpServerPrimitiveCollection? collection, } /// - public string? SessionId => _sessionTransport.SessionId; + public override string? SessionId => _sessionTransport.SessionId; /// public ServerCapabilities ServerCapabilities { get; } = new(); /// - public ClientCapabilities? ClientCapabilities { get; set; } + public override ClientCapabilities? ClientCapabilities => _clientCapabilities; /// - public Implementation? ClientInfo { get; set; } + public override Implementation? ClientInfo => _clientInfo; /// - public McpServerOptions ServerOptions { get; } + public override McpServerOptions ServerOptions { get; } /// - public IServiceProvider? Services { get; } + public override IServiceProvider? Services { get; } /// - public LoggingLevel? LoggingLevel => _loggingLevel?.Value; + public override LoggingLevel? LoggingLevel => _loggingLevel?.Value; /// - public async Task RunAsync(CancellationToken cancellationToken = default) + public override async Task RunAsync(CancellationToken cancellationToken = default) { if (Interlocked.Exchange(ref _started, 1) != 0) { @@ -148,19 +151,19 @@ public async Task RunAsync(CancellationToken cancellationToken = default) /// - public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) + public override Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default) => _sessionHandler.SendRequestAsync(request, cancellationToken); /// - public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) => _sessionHandler.SendMessageAsync(message, cancellationToken); /// - public IAsyncDisposable RegisterNotificationHandler(string method, Func handler) + public override IAsyncDisposable RegisterNotificationHandler(string method, Func handler) => _sessionHandler.RegisterNotificationHandler(method, handler); /// - public async ValueTask DisposeAsync() + public override async ValueTask DisposeAsync() { if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) { @@ -184,8 +187,8 @@ private void ConfigureInitialize(McpServerOptions options) _requestHandlers.Set(RequestMethods.Initialize, async (request, _, _) => { - ClientCapabilities = request?.Capabilities ?? new(); - ClientInfo = request?.ClientInfo; + _clientCapabilities = request?.Capabilities ?? new(); + _clientInfo = request?.ClientInfo; // Use the ClientInfo to update the session EndpointName for logging. UpdateEndpointNameWithClientInfo(); @@ -558,7 +561,7 @@ private ValueTask InvokeHandlerAsync( { return _servicesScopePerRequest ? InvokeScopedAsync(handler, args, cancellationToken) : - handler(new(new DestinationBoundMcpServer(this, destinationTransport)) { Params = args }, cancellationToken); + handler(new(new DestinationBoundMcpServerSession(this, destinationTransport)) { Params = args }, cancellationToken); async ValueTask InvokeScopedAsync( Func, CancellationToken, ValueTask> handler, @@ -569,7 +572,7 @@ async ValueTask InvokeScopedAsync( try { return await handler( - new RequestContext(new DestinationBoundMcpServer(this, destinationTransport)) + new RequestContext(new DestinationBoundMcpServerSession(this, destinationTransport)) { Services = scope?.ServiceProvider ?? Services, Params = args diff --git a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs index 68874df3e..41d6b1cb0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPrompt.cs @@ -15,8 +15,8 @@ namespace ModelContextProtocol.Server; /// is an abstract base class that represents an MCP prompt for use in the server (as opposed /// to , which provides the protocol representation of a prompt, and , which /// provides a client-side representation of a prompt). Instances of can be added into a -/// to be picked up automatically when is used to create -/// an , or added into a . +/// to be picked up automatically when is used to create +/// an , or added into a . /// /// /// Most commonly, instances are created using the static methods. @@ -34,7 +34,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . /// /// @@ -45,7 +45,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// parameters are bound directly to the instance associated +/// parameters are bound directly to the instance associated /// with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// @@ -201,7 +201,7 @@ public static McpServerPrompt Create( /// is . /// /// Unlike the other overloads of Create, the created by - /// does not provide all of the special parameter handling for MCP-specific concepts, like . + /// does not provide all of the special parameter handling for MCP-specific concepts, like . /// public static McpServerPrompt Create( AIFunction function, diff --git a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs index c71e969db..ac9e247f6 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerPromptAttribute.cs @@ -25,7 +25,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . /// /// @@ -36,7 +36,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// parameters are bound directly to the instance associated +/// parameters are bound directly to the instance associated /// with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerResource.cs b/src/ModelContextProtocol.Core/Server/McpServerResource.cs index 8e42d3e1c..fe36b2843 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResource.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResource.cs @@ -13,7 +13,7 @@ namespace ModelContextProtocol.Server; /// is an abstract base class that represents an MCP resource for use in the server (as opposed /// to or , which provide the protocol representations of a resource). Instances of /// can be added into a to be picked up automatically when -/// is used to create an , or added into a . +/// is used to create an , or added into a . /// /// /// Most commonly, instances are created using the static methods. @@ -35,7 +35,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . /// /// @@ -46,7 +46,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// parameters are bound directly to the instance associated +/// parameters are bound directly to the instance associated /// with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// @@ -223,7 +223,7 @@ public static McpServerResource Create( /// is . /// /// Unlike the other overloads of Create, the created by - /// does not provide all of the special parameter handling for MCP-specific concepts, like . + /// does not provide all of the special parameter handling for MCP-specific concepts, like . /// public static McpServerResource Create( AIFunction function, diff --git a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs index bc2f138f0..66c593e47 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerResourceAttribute.cs @@ -23,7 +23,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . /// /// @@ -34,7 +34,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// parameters are bound directly to the instance associated +/// parameters are bound directly to the instance associated /// with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// diff --git a/src/ModelContextProtocol.Core/Server/McpServerTool.cs b/src/ModelContextProtocol.Core/Server/McpServerTool.cs index e3958271b..76391a506 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerTool.cs @@ -15,8 +15,8 @@ namespace ModelContextProtocol.Server; /// is an abstract base class that represents an MCP tool for use in the server (as opposed /// to , which provides the protocol representation of a tool, and , which /// provides a client-side representation of a tool). Instances of can be added into a -/// to be picked up automatically when is used to create -/// an , or added into a . +/// to be picked up automatically when is used to create +/// an , or added into a . /// /// /// Most commonly, instances are created using the static methods. @@ -35,7 +35,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . The parameter is not included in the generated JSON schema. /// /// @@ -47,7 +47,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// parameters are not included in the JSON schema and are bound directly to the +/// parameters are not included in the JSON schema and are bound directly to the /// instance associated with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// @@ -203,7 +203,7 @@ public static McpServerTool Create( /// is . /// /// Unlike the other overloads of Create, the created by - /// does not provide all of the special parameter handling for MCP-specific concepts, like . + /// does not provide all of the special parameter handling for MCP-specific concepts, like . /// public static McpServerTool Create( AIFunction function, diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs index d4ea9eb75..7d5bf488b 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolAttribute.cs @@ -26,7 +26,7 @@ namespace ModelContextProtocol.Server; /// /// /// parameters are automatically bound to a provided by the -/// and that respects any s sent by the client for this operation's +/// and that respects any s sent by the client for this operation's /// . The parameter is not included in the generated JSON schema. /// /// @@ -38,7 +38,7 @@ namespace ModelContextProtocol.Server; /// /// /// -/// parameters are not included in the JSON schema and are bound directly to the +/// parameters are not included in the JSON schema and are bound directly to the /// instance associated with this request's . Such parameters may be used to understand /// what server is being used to process the request, and to interact with the client issuing the request to that server. /// diff --git a/src/ModelContextProtocol.Core/Server/RequestContext.cs b/src/ModelContextProtocol.Core/Server/RequestContext.cs index b0ea9d993..37d24b986 100644 --- a/src/ModelContextProtocol.Core/Server/RequestContext.cs +++ b/src/ModelContextProtocol.Core/Server/RequestContext.cs @@ -12,13 +12,13 @@ namespace ModelContextProtocol.Server; public sealed class RequestContext { /// The server with which this instance is associated. - private IMcpServer _server; + private McpServer _server; /// /// Initializes a new instance of the class with the specified server. /// /// The server with which this instance is associated. - public RequestContext(IMcpServer server) + public RequestContext(McpServer server) { Throw.IfNull(server); @@ -27,7 +27,7 @@ public RequestContext(IMcpServer server) } /// Gets or sets the server with which this instance is associated. - public IMcpServer Server + public McpServer Server { get => _server; set @@ -39,10 +39,10 @@ public IMcpServer Server /// Gets or sets the services associated with this request. /// - /// This may not be the same instance stored in + /// This may not be the same instance stored in /// if was true, in which case this /// might be a scoped derived from the server's - /// . + /// . /// public IServiceProvider? Services { get; set; } diff --git a/src/ModelContextProtocol.Core/Server/StdioServerTransport.cs b/src/ModelContextProtocol.Core/Server/StdioServerTransport.cs index 26641cf6a..307c180a1 100644 --- a/src/ModelContextProtocol.Core/Server/StdioServerTransport.cs +++ b/src/ModelContextProtocol.Core/Server/StdioServerTransport.cs @@ -37,7 +37,7 @@ private static string GetServerName(McpServerOptions serverOptions) { Throw.IfNull(serverOptions); - return serverOptions.ServerInfo?.Name ?? McpServerSession.DefaultImplementation.Name; + return serverOptions.ServerInfo?.Name ?? McpServerImpl.DefaultImplementation.Name; } // Neither WindowsConsoleStream nor UnixConsoleStream respect CancellationTokens or cancel any I/O on Dispose. diff --git a/src/ModelContextProtocol.Core/TokenProgress.cs b/src/ModelContextProtocol.Core/TokenProgress.cs index e05060119..6b7a91e00 100644 --- a/src/ModelContextProtocol.Core/TokenProgress.cs +++ b/src/ModelContextProtocol.Core/TokenProgress.cs @@ -6,7 +6,7 @@ namespace ModelContextProtocol; /// Provides an tied to a specific progress token and that will issue /// progress notifications on the supplied session. /// -internal sealed class TokenProgress(IMcpEndpoint session, ProgressToken progressToken) : IProgress +internal sealed class TokenProgress(McpSession session, ProgressToken progressToken) : IProgress { /// public void Report(ProgressNotificationValue value) diff --git a/src/ModelContextProtocol/IMcpServerBuilder.cs b/src/ModelContextProtocol/IMcpServerBuilder.cs index 5ec37eba9..016e9eb3e 100644 --- a/src/ModelContextProtocol/IMcpServerBuilder.cs +++ b/src/ModelContextProtocol/IMcpServerBuilder.cs @@ -3,7 +3,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// -/// Provides a builder for configuring instances. +/// Provides a builder for configuring instances. /// /// /// diff --git a/src/ModelContextProtocol/McpServerBuilderExtensions.cs b/src/ModelContextProtocol/McpServerBuilderExtensions.cs index d925b24f6..455564b0c 100644 --- a/src/ModelContextProtocol/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/McpServerBuilderExtensions.cs @@ -693,8 +693,8 @@ public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpSer /// and may begin sending log messages at or above the specified level to the client. /// /// - /// Regardless of whether a handler is provided, an should itself handle - /// such notifications by updating its property to return the + /// Regardless of whether a handler is provided, an should itself handle + /// such notifications by updating its property to return the /// most recently set level. /// /// @@ -774,7 +774,7 @@ private static void AddSingleSessionServerDependencies(IServiceCollection servic ITransport serverTransport = services.GetRequiredService(); IOptions options = services.GetRequiredService>(); ILoggerFactory? loggerFactory = services.GetService(); - return McpServerFactory.Create(serverTransport, options.Value, loggerFactory, services); + return McpServer.Create(serverTransport, options.Value, loggerFactory, services); }); } #endregion diff --git a/src/ModelContextProtocol/SingleSessionMcpServerHostedService.cs b/src/ModelContextProtocol/SingleSessionMcpServerHostedService.cs index b50e46140..80e8216a8 100644 --- a/src/ModelContextProtocol/SingleSessionMcpServerHostedService.cs +++ b/src/ModelContextProtocol/SingleSessionMcpServerHostedService.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol; /// /// The host's application lifetime. If available, it will have termination requested when the session's run completes. /// -internal sealed class SingleSessionMcpServerHostedService(IMcpServer session, IHostApplicationLifetime? lifetime = null) : BackgroundService +internal sealed class SingleSessionMcpServerHostedService(McpServer session, IHostApplicationLifetime? lifetime = null) : BackgroundService { /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs index c993edd4c..f59c453cd 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs @@ -122,7 +122,7 @@ public async Task CanAuthenticate_WithResourceMetadataFromEvent() LoggerFactory ); - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken @@ -157,7 +157,7 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent() LoggerFactory ); - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs index 2252b1b7c..2a79cab17 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs @@ -109,7 +109,7 @@ public async Task CanAuthenticate() }, }, HttpClient, LoggerFactory); - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); } @@ -129,7 +129,7 @@ public async Task CannotAuthenticate_WithoutOAuthConfiguration() Endpoint = new(McpServerUrl), }, HttpClient, LoggerFactory); - var httpEx = await Assert.ThrowsAsync(async () => await McpClientFactory.CreateAsync( + var httpEx = await Assert.ThrowsAsync(async () => await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); Assert.Equal(HttpStatusCode.Unauthorized, httpEx.StatusCode); @@ -159,7 +159,7 @@ public async Task CannotAuthenticate_WithUnregisteredClient() }, HttpClient, LoggerFactory); // The EqualException is thrown by HandleAuthorizationUrlAsync when the /authorize request gets a 400 - var equalEx = await Assert.ThrowsAsync(async () => await McpClientFactory.CreateAsync( + var equalEx = await Assert.ThrowsAsync(async () => await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); } @@ -187,7 +187,7 @@ public async Task CanAuthenticate_WithDynamicClientRegistration() }, }, HttpClient, LoggerFactory); - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); } @@ -216,7 +216,7 @@ public async Task CanAuthenticate_WithTokenRefresh() // 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); @@ -249,7 +249,7 @@ public async Task CanAuthenticate_WithExtraParams() }, }, 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); @@ -283,7 +283,7 @@ public async Task CannotOverrideExistingParameters_WithExtraParams() }, }, HttpClient, LoggerFactory); - await Assert.ThrowsAsync(() => McpClientFactory.CreateAsync( + await Assert.ThrowsAsync(() => McpClient.CreateAsync( transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index 844fd7347..30e0f9dfa 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -23,7 +23,7 @@ public override void Dispose() protected abstract SseClientTransportOptions ClientTransportOptions { get; } - private Task GetClientAsync(McpClientOptions? options = null) + private Task GetClientAsync(McpClientOptions? options = null) { return _fixture.ConnectMcpClientAsync(options, LoggerFactory); } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index 7ada1f9f2..cce23a533 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -23,7 +23,7 @@ protected void ConfigureStateless(HttpServerTransportOptions options) options.Stateless = Stateless; } - protected async Task ConnectAsync( + protected async Task ConnectAsync( string? path = null, SseClientTransportOptions? transportOptions = null, McpClientOptions? clientOptions = null) @@ -37,7 +37,7 @@ protected async Task ConnectAsync( 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] @@ -204,7 +204,7 @@ public string EchoWithUserName(string message) private class SamplingRegressionTools { [McpServerTool(Name = "sampling-tool")] - public static async Task SamplingToolAsync(IMcpServer server, string prompt, CancellationToken cancellationToken) + public static async Task 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. diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs index 7a74eb316..c419ec695 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs @@ -21,8 +21,8 @@ public partial class SseIntegrationTests(ITestOutputHelper outputHelper) : Kestr Name = "In-memory SSE Client", }; - private Task ConnectMcpClientAsync(HttpClient? httpClient = null, SseClientTransportOptions? transportOptions = null) - => McpClientFactory.CreateAsync( + private Task ConnectMcpClientAsync(HttpClient? httpClient = null, SseClientTransportOptions? transportOptions = null) + => McpClient.CreateAsync( new SseClientTransport(transportOptions ?? DefaultTransportOptions, httpClient ?? HttpClient, LoggerFactory), loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); @@ -257,7 +257,7 @@ private static void MapAbsoluteEndpointUriMcp(IEndpointRouteBuilder endpoints, b 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 { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs index 8cdc76456..7a11bebcd 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs @@ -44,9 +44,9 @@ public SseServerIntegrationTestFixture() public HttpClient HttpClient { get; } - public Task ConnectMcpClientAsync(McpClientOptions? options, ILoggerFactory loggerFactory) + public Task ConnectMcpClientAsync(McpClientOptions? options, ILoggerFactory loggerFactory) { - return McpClientFactory.CreateAsync( + return McpClient.CreateAsync( new SseClientTransport(DefaultTransportOptions, HttpClient, loggerFactory), options, loggerFactory, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs index 865aaf6e5..8bc2130d9 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs @@ -58,8 +58,8 @@ private async Task StartAsync() HttpClient.DefaultRequestHeaders.Accept.Add(new("text/event-stream")); } - private Task ConnectMcpClientAsync(McpClientOptions? clientOptions = null) - => McpClientFactory.CreateAsync( + private Task ConnectMcpClientAsync(McpClientOptions? clientOptions = null) + => McpClient.CreateAsync( new SseClientTransport(DefaultTransportOptions, HttpClient, LoggerFactory), clientOptions, LoggerFactory, TestContext.Current.CancellationToken); @@ -194,7 +194,7 @@ public async Task ScopedServices_Resolve_FromRequestScope() } [McpServerTool(Name = "testSamplingErrors")] - public static async Task TestSamplingErrors(IMcpServer server) + public static async Task TestSamplingErrors(McpServer server) { const string expectedSamplingErrorMessage = "Sampling is not supported in stateless mode."; @@ -212,7 +212,7 @@ public static async Task TestSamplingErrors(IMcpServer server) } [McpServerTool(Name = "testRootsErrors")] - public static async Task TestRootsErrors(IMcpServer server) + public static async Task TestRootsErrors(McpServer server) { const string expectedRootsErrorMessage = "Roots are not supported in stateless mode."; @@ -227,7 +227,7 @@ public static async Task TestRootsErrors(IMcpServer server) } [McpServerTool(Name = "testElicitationErrors")] - public static async Task TestElicitationErrors(IMcpServer server) + public static async Task TestElicitationErrors(McpServer server) { const string expectedElicitationErrorMessage = "Elicitation is not supported in stateless mode."; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs index 7ce3516ef..3ca8010a7 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs @@ -118,7 +118,7 @@ public async Task CanCallToolOnSessionlessStreamableHttpServer() 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); @@ -138,7 +138,7 @@ public async Task CanCallToolConcurrently() 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); @@ -164,7 +164,7 @@ public async Task SendsDeleteRequestOnDispose() 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(); diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs index 0b3ae4c2a..8b5f3e157 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerConformanceTests.cs @@ -252,7 +252,7 @@ public async Task MultipleConcurrentJsonRpcRequests_IsHandled_InParallel() [Fact] public async Task GetRequest_Receives_UnsolicitedNotifications() { - IMcpServer? server = null; + McpServer? server = null; Builder.Services.AddMcpServer() .WithHttpTransport(options => diff --git a/tests/ModelContextProtocol.TestServer/Program.cs b/tests/ModelContextProtocol.TestServer/Program.cs index 0bc4134fa..743c858c0 100644 --- a/tests/ModelContextProtocol.TestServer/Program.cs +++ b/tests/ModelContextProtocol.TestServer/Program.cs @@ -51,7 +51,7 @@ private static async Task Main(string[] args) using var loggerFactory = CreateLoggerFactory(); await using var stdioTransport = new StdioServerTransport("TestServer", loggerFactory); - await using IMcpServer server = McpServerFactory.Create(stdioTransport, options, loggerFactory); + await using McpServer server = McpServer.Create(stdioTransport, options, loggerFactory); Log.Logger.Information("Server running..."); @@ -61,7 +61,7 @@ private static async Task Main(string[] args) await server.RunAsync(); } - private static async Task RunBackgroundLoop(IMcpServer server, CancellationToken cancellationToken = default) + private static async Task RunBackgroundLoop(McpServer server, CancellationToken cancellationToken = default) { var loggingLevels = (LoggingLevel[])Enum.GetValues(typeof(LoggingLevel)); var random = new Random(); diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs index 87622719d..46b98118b 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs @@ -197,7 +197,7 @@ public async Task CreateSamplingHandler_ShouldHandleResourceMessages() [Fact] public async Task ListToolsAsync_AllToolsReturned() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(12, tools.Count); @@ -223,7 +223,7 @@ public async Task ListToolsAsync_AllToolsReturned() [Fact] public async Task EnumerateToolsAsync_AllToolsReturned() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken)) { @@ -242,7 +242,7 @@ public async Task EnumerateToolsAsync_AllToolsReturned() public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions() { JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); bool hasTools = false; await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken)) @@ -263,7 +263,7 @@ public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions() public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions() { JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First(); await Assert.ThrowsAsync(async () => await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken)); @@ -273,7 +273,7 @@ public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions() public async Task SendRequestAsync_HonorsJsonSerializerOptions() { JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.SendRequestAsync("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); } @@ -282,7 +282,7 @@ public async Task SendRequestAsync_HonorsJsonSerializerOptions() public async Task SendNotificationAsync_HonorsJsonSerializerOptions() { JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(() => client.SendNotificationAsync("Method4", new { Value = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); } @@ -291,7 +291,7 @@ public async Task SendNotificationAsync_HonorsJsonSerializerOptions() public async Task GetPromptsAsync_HonorsJsonSerializerOptions() { JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.GetPromptAsync("Prompt", new Dictionary { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); } @@ -300,7 +300,7 @@ public async Task GetPromptsAsync_HonorsJsonSerializerOptions() public async Task WithName_ChangesToolName() { JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).First(); var originalName = tool.Name; @@ -315,7 +315,7 @@ public async Task WithName_ChangesToolName() public async Task WithDescription_ChangesToolDescription() { JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).FirstOrDefault(); var originalDescription = tool?.Description; var redescribedTool = tool?.WithDescription("ToolWithNewDescription"); @@ -344,7 +344,7 @@ public async Task WithProgress_ProgressReported() return 42; }, new() { Name = "ProgressReporter" })); - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tool = (await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken)).First(t => t.Name == "ProgressReporter"); @@ -372,7 +372,7 @@ private sealed class SynchronousProgress(Action callb [Fact] public async Task AsClientLoggerProvider_MessagesSentToClient() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); ILoggerProvider loggerProvider = Server.AsClientLoggerProvider(); Assert.Throws("categoryName", () => loggerProvider.CreateLogger(null!)); diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs index a063e4c59..2599d7485 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientResourceTemplateTests.cs @@ -73,7 +73,7 @@ public static IEnumerable UriTemplate_InputsProduceExpectedOutputs_Mem public async Task UriTemplate_InputsProduceExpectedOutputs( IReadOnlyDictionary variables, string uriTemplate, object expected) { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.ReadResourceAsync(uriTemplate, variables, TestContext.Current.CancellationToken); Assert.NotNull(result); diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs similarity index 91% rename from tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs rename to tests/ModelContextProtocol.Tests/Client/McpClientTests.cs index 6c59d94a0..2d230d243 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientFactoryTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs @@ -8,19 +8,19 @@ namespace ModelContextProtocol.Tests.Client; -public class McpClientFactoryTests +public class McpClientTests { [Fact] public async Task CreateAsync_WithInvalidArgs_Throws() { - await Assert.ThrowsAsync("clientTransport", () => McpClientFactory.CreateAsync(null!, cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync("clientTransport", () => McpClient.CreateAsync(null!, cancellationToken: TestContext.Current.CancellationToken)); } [Fact] public async Task CreateAsync_NopTransport_ReturnsClient() { // Act - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( new NopTransport(), cancellationToken: TestContext.Current.CancellationToken); @@ -39,7 +39,7 @@ public async Task Cancellation_ThrowsCancellationException(bool preCanceled) cts.Cancel(); } - Task t = McpClientFactory.CreateAsync( + Task t = McpClient.CreateAsync( new StreamClientTransport(new Pipe().Writer.AsStream(), new Pipe().Reader.AsStream()), cancellationToken: cts.Token); if (!preCanceled) @@ -85,9 +85,9 @@ public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) }; var clientTransport = (IClientTransport)Activator.CreateInstance(transportType)!; - McpClientSession? client = null; + McpClient? client = null; - var actionTask = McpClientFactory.CreateAsync(clientTransport, clientOptions, loggerFactory: null, CancellationToken.None); + var actionTask = McpClient.CreateAsync(clientTransport, clientOptions, loggerFactory: null, CancellationToken.None); // Act if (clientTransport is FailureTransport) diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs index f36e26210..6f625866a 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTestFixture.cs @@ -41,8 +41,8 @@ public void Initialize(ILoggerFactory loggerFactory) _loggerFactory = loggerFactory; } - public Task CreateClientAsync(string clientId, McpClientOptions? clientOptions = null) => - McpClientFactory.CreateAsync(new StdioClientTransport(clientId switch + public Task CreateClientAsync(string clientId, McpClientOptions? clientOptions = null) => + McpClient.CreateAsync(new StdioClientTransport(clientId switch { "everything" => EverythingServerTransportOptions, "test_server" => TestServerTransportOptions, diff --git a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs index 3e4361a57..211688419 100644 --- a/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/ClientIntegrationTests.cs @@ -471,7 +471,7 @@ public async Task CallTool_Stdio_MemoryServer() ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" } }; - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( new StdioClientTransport(stdioOptions), clientOptions, loggerFactory: LoggerFactory, @@ -495,7 +495,7 @@ public async Task CallTool_Stdio_MemoryServer() public async Task ListToolsAsync_UsingEverythingServer_ToolsAreProperlyCalled() { // Get the MCP client and tools from it. - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( new StdioClientTransport(_fixture.EverythingServerTransportOptions), cancellationToken: TestContext.Current.CancellationToken); var mappedTools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); @@ -527,7 +527,7 @@ public async Task SamplingViaChatClient_RequestResponseProperlyPropagated() var samplingHandler = new OpenAIClient(s_openAIKey).GetChatClient("gpt-4o-mini") .AsIChatClient() .CreateSamplingHandler(); - await using var client = await McpClientFactory.CreateAsync(new StdioClientTransport(_fixture.EverythingServerTransportOptions), new() + await using var client = await McpClient.CreateAsync(new StdioClientTransport(_fixture.EverythingServerTransportOptions), new() { Capabilities = new() { diff --git a/tests/ModelContextProtocol.Tests/ClientServerTestBase.cs b/tests/ModelContextProtocol.Tests/ClientServerTestBase.cs index 8d1b8590b..7dcebfd72 100644 --- a/tests/ModelContextProtocol.Tests/ClientServerTestBase.cs +++ b/tests/ModelContextProtocol.Tests/ClientServerTestBase.cs @@ -28,11 +28,11 @@ public ClientServerTestBase(ITestOutputHelper testOutputHelper) ServiceProvider = sc.BuildServiceProvider(validateScopes: true); _cts = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); - Server = ServiceProvider.GetRequiredService(); + Server = ServiceProvider.GetRequiredService(); _serverTask = Server.RunAsync(_cts.Token); } - protected IMcpServer Server { get; } + protected McpServer Server { get; } protected IServiceProvider ServiceProvider { get; } @@ -62,9 +62,9 @@ public async ValueTask DisposeAsync() Dispose(); } - protected async Task CreateMcpClientForServer(McpClientOptions? clientOptions = null) + protected async Task CreateMcpClientForServer(McpClientOptions? clientOptions = null) { - return await McpClientFactory.CreateAsync( + return await McpClient.CreateAsync( new StreamClientTransport( serverInput: _clientToServerPipe.Writer.AsStream(), _serverToClientPipe.Reader.AsStream(), diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs index cc2107e03..d3ea849fd 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsPromptsTests.cs @@ -95,7 +95,7 @@ public void Adds_Prompts_To_Server() [Fact] public async Task Can_List_And_Call_Registered_Prompts() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); Assert.Equal(6, prompts.Count); @@ -124,7 +124,7 @@ public async Task Can_List_And_Call_Registered_Prompts() [Fact] public async Task Can_Be_Notified_Of_Prompt_Changes() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var prompts = await client.ListPromptsAsync(TestContext.Current.CancellationToken); Assert.Equal(6, prompts.Count); @@ -165,7 +165,7 @@ public async Task Can_Be_Notified_Of_Prompt_Changes() [Fact] public async Task TitleAttributeProperty_PropagatedToTitle() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var prompts = await client.ListPromptsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(prompts); @@ -179,7 +179,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() [Fact] public async Task Throws_When_Prompt_Fails() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.GetPromptAsync( nameof(SimplePrompts.ThrowsException), @@ -189,7 +189,7 @@ await Assert.ThrowsAsync(async () => await client.GetPromptAsync( [Fact] public async Task Throws_Exception_On_Unknown_Prompt() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.GetPromptAsync( "NotRegisteredPrompt", @@ -201,7 +201,7 @@ public async Task Throws_Exception_On_Unknown_Prompt() [Fact] public async Task Throws_Exception_Missing_Parameter() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.GetPromptAsync( "returns_chat_messages", diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs index f2b90c8a4..34cc67c10 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsResourcesTests.cs @@ -122,7 +122,7 @@ public void Adds_Resources_To_Server() [Fact] public async Task Can_List_And_Call_Registered_Resources() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); Assert.NotNull(client.ServerCapabilities.Resources); @@ -141,7 +141,7 @@ public async Task Can_List_And_Call_Registered_Resources() [Fact] public async Task Can_List_And_Call_Registered_ResourceTemplates() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var resources = await client.ListResourceTemplatesAsync(TestContext.Current.CancellationToken); Assert.Equal(3, resources.Count); @@ -158,7 +158,7 @@ public async Task Can_List_And_Call_Registered_ResourceTemplates() [Fact] public async Task Can_Be_Notified_Of_Resource_Changes() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken); Assert.Equal(5, resources.Count); @@ -199,7 +199,7 @@ public async Task Can_Be_Notified_Of_Resource_Changes() [Fact] public async Task TitleAttributeProperty_PropagatedToTitle() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var resources = await client.ListResourcesAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(resources); @@ -217,7 +217,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() [Fact] public async Task Throws_When_Resource_Fails() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( $"resource://mcp/{nameof(SimpleResources.ThrowsException)}", @@ -227,7 +227,7 @@ await Assert.ThrowsAsync(async () => await client.ReadResourceAsyn [Fact] public async Task Throws_Exception_On_Unknown_Resource() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( "test:///NotRegisteredResource", diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs index 748ee50fa..da3571ba6 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerBuilderExtensionsToolsTests.cs @@ -121,7 +121,7 @@ public void Adds_Tools_To_Server() [Fact] public async Task Can_List_Registered_Tools() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(16, tools.Count); @@ -150,10 +150,10 @@ public async Task Can_Create_Multiple_Servers_From_Options_And_List_Registered_T var stdoutPipe = new Pipe(); await using var transport = new StreamServerTransport(stdinPipe.Reader.AsStream(), stdoutPipe.Writer.AsStream()); - await using var server = McpServerFactory.Create(transport, options, loggerFactory, ServiceProvider); + await using var server = McpServer.Create(transport, options, loggerFactory, ServiceProvider); var serverRunTask = server.RunAsync(TestContext.Current.CancellationToken); - await using (var client = await McpClientFactory.CreateAsync( + await using (var client = await McpClient.CreateAsync( new StreamClientTransport( serverInput: stdinPipe.Writer.AsStream(), serverOutput: stdoutPipe.Reader.AsStream(), @@ -185,7 +185,7 @@ public async Task Can_Create_Multiple_Servers_From_Options_And_List_Registered_T [Fact] public async Task Can_Be_Notified_Of_Tool_Changes() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.Equal(16, tools.Count); @@ -226,7 +226,7 @@ public async Task Can_Be_Notified_Of_Tool_Changes() [Fact] public async Task Can_Call_Registered_Tool() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "echo", @@ -245,7 +245,7 @@ public async Task Can_Call_Registered_Tool() [Fact] public async Task Can_Call_Registered_Tool_With_Array_Result() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "echo_array", @@ -268,7 +268,7 @@ public async Task Can_Call_Registered_Tool_With_Array_Result() [Fact] public async Task Can_Call_Registered_Tool_With_Null_Result() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "return_null", @@ -282,7 +282,7 @@ public async Task Can_Call_Registered_Tool_With_Null_Result() [Fact] public async Task Can_Call_Registered_Tool_With_Json_Result() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "return_json", @@ -299,7 +299,7 @@ public async Task Can_Call_Registered_Tool_With_Json_Result() [Fact] public async Task Can_Call_Registered_Tool_With_Int_Result() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "return_integer", @@ -314,7 +314,7 @@ public async Task Can_Call_Registered_Tool_With_Int_Result() [Fact] public async Task Can_Call_Registered_Tool_And_Pass_ComplexType() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "echo_complex", @@ -331,7 +331,7 @@ public async Task Can_Call_Registered_Tool_And_Pass_ComplexType() [Fact] public async Task Can_Call_Registered_Tool_With_Instance_Method() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); string[][] parts = new string[2][]; for (int i = 0; i < 2; i++) @@ -360,7 +360,7 @@ public async Task Can_Call_Registered_Tool_With_Instance_Method() [Fact] public async Task Returns_IsError_Content_When_Tool_Fails() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "throw_exception", @@ -375,7 +375,7 @@ public async Task Returns_IsError_Content_When_Tool_Fails() [Fact] public async Task Throws_Exception_On_Unknown_Tool() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var e = await Assert.ThrowsAsync(async () => await client.CallToolAsync( "NotRegisteredTool", @@ -387,7 +387,7 @@ public async Task Throws_Exception_On_Unknown_Tool() [Fact] public async Task Returns_IsError_Missing_Parameter() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var result = await client.CallToolAsync( "echo", @@ -506,7 +506,7 @@ public void WithToolsFromAssembly_Parameters_Satisfiable_From_DI(ServiceLifetime [Fact] public async Task Recognizes_Parameter_Types() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); @@ -581,7 +581,7 @@ public void Create_ExtractsToolAnnotations_SomeSet() [Fact] public async Task TitleAttributeProperty_PropagatedToTitle() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); @@ -597,7 +597,7 @@ public async Task TitleAttributeProperty_PropagatedToTitle() [Fact] public async Task HandlesIProgressParameter() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); @@ -651,7 +651,7 @@ public async Task HandlesIProgressParameter() [Fact] public async Task CancellationNotificationsPropagateToToolTokens() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); Assert.NotNull(tools); diff --git a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs index 4eec471e0..5ddc3c54a 100644 --- a/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs +++ b/tests/ModelContextProtocol.Tests/Configuration/McpServerScopedTests.cs @@ -22,7 +22,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer [Fact] public async Task InjectScopedServiceAsArgument() { - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var tools = await client.ListToolsAsync(McpServerScopedTestsJsonContext.Default.Options, TestContext.Current.CancellationToken); var tool = tools.First(t => t.Name == "echo_complex"); diff --git a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs index fe08438e0..5ad30d282 100644 --- a/tests/ModelContextProtocol.Tests/DiagnosticTests.cs +++ b/tests/ModelContextProtocol.Tests/DiagnosticTests.cs @@ -128,7 +128,7 @@ await RunConnected(async (client, server) => Assert.Equal("-32602", doesNotExistToolClient.Tags.Single(t => t.Key == "rpc.jsonrpc.error_code").Value); } - private static async Task RunConnected(Func action, List clientToServerLog) + private static async Task RunConnected(Func action, List clientToServerLog) { Pipe clientToServerPipe = new(), serverToClientPipe = new(); StreamServerTransport serverTransport = new(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream()); @@ -137,7 +137,7 @@ private static async Task RunConnected(Func Task serverTask; - await using (IMcpServer server = McpServerFactory.Create(serverTransport, new() + await using (McpServer server = McpServer.Create(serverTransport, new() { Capabilities = new() { @@ -153,7 +153,7 @@ private static async Task RunConnected(Func { serverTask = server.RunAsync(TestContext.Current.CancellationToken); - await using (McpClientSession client = await McpClientFactory.CreateAsync( + await using (McpClient client = await McpClient.CreateAsync( clientTransport, cancellationToken: TestContext.Current.CancellationToken)) { diff --git a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs index ffd95076f..e3faf05f4 100644 --- a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs +++ b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs @@ -43,7 +43,7 @@ public async Task ConnectAndReceiveMessage_EverythingServerWithSse() }; // Create client and run tests - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( new SseClientTransport(defaultConfig), defaultOptions, loggerFactory: LoggerFactory, @@ -90,7 +90,7 @@ public async Task Sampling_Sse_EverythingServer() }, }; - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( new SseClientTransport(defaultConfig), defaultOptions, loggerFactory: LoggerFactory, diff --git a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs index 2b735ed38..22fd69c17 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/ElicitationTests.cs @@ -67,7 +67,7 @@ protected override void ConfigureServices(ServiceCollection services, IMcpServer [Fact] public async Task Can_Elicit_Information() { - await using McpClientSession client = await CreateMcpClientForServer(new McpClientOptions + await using McpClient client = await CreateMcpClientForServer(new McpClientOptions { Capabilities = new() { diff --git a/tests/ModelContextProtocol.Tests/Protocol/NotificationHandlerTests.cs b/tests/ModelContextProtocol.Tests/Protocol/NotificationHandlerTests.cs index d8fc4b94c..25470650e 100644 --- a/tests/ModelContextProtocol.Tests/Protocol/NotificationHandlerTests.cs +++ b/tests/ModelContextProtocol.Tests/Protocol/NotificationHandlerTests.cs @@ -13,7 +13,7 @@ public NotificationHandlerTests(ITestOutputHelper testOutputHelper) public async Task RegistrationsAreRemovedWhenDisposed() { const string NotificationName = "somethingsomething"; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); const int Iterations = 10; @@ -40,7 +40,7 @@ public async Task RegistrationsAreRemovedWhenDisposed() public async Task MultipleRegistrationsResultInMultipleCallbacks() { const string NotificationName = "somethingsomething"; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); const int RegistrationCount = 10; @@ -80,7 +80,7 @@ public async Task MultipleRegistrationsResultInMultipleCallbacks() public async Task MultipleHandlersRunEvenIfOneThrows() { const string NotificationName = "somethingsomething"; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); const int RegistrationCount = 10; @@ -122,7 +122,7 @@ public async Task MultipleHandlersRunEvenIfOneThrows() public async Task DisposeAsyncDoesNotCompleteWhileNotificationHandlerRuns(int numberOfDisposals) { const string NotificationName = "somethingsomething"; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var handlerRunning = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -163,7 +163,7 @@ public async Task DisposeAsyncDoesNotCompleteWhileNotificationHandlerRuns(int nu public async Task DisposeAsyncCompletesImmediatelyWhenInvokedFromHandler(int numberOfDisposals) { const string NotificationName = "somethingsomething"; - await using McpClientSession client = await CreateMcpClientForServer(); + await using McpClient client = await CreateMcpClientForServer(); var handlerRunning = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var releaseHandler = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerFactoryTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerFactoryTests.cs deleted file mode 100644 index 034a30bd7..000000000 --- a/tests/ModelContextProtocol.Tests/Server/McpServerFactoryTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -using ModelContextProtocol.Server; -using ModelContextProtocol.Tests.Utils; - -namespace ModelContextProtocol.Tests.Server; - -public class McpServerFactoryTests : LoggedTest -{ - private readonly McpServerOptions _options; - - public McpServerFactoryTests(ITestOutputHelper testOutputHelper) - : base(testOutputHelper) - { - _options = new McpServerOptions - { - ProtocolVersion = "1.0", - InitializationTimeout = TimeSpan.FromSeconds(30) - }; - } - - [Fact] - public async Task Create_Should_Initialize_With_Valid_Parameters() - { - // Arrange & Act - await using var transport = new TestServerTransport(); - await using IMcpServer server = McpServerFactory.Create(transport, _options, LoggerFactory); - - // Assert - Assert.NotNull(server); - } - - [Fact] - public void Create_Throws_For_Null_ServerTransport() - { - // Arrange, Act & Assert - Assert.Throws("transport", () => McpServerFactory.Create(null!, _options, LoggerFactory)); - } - - [Fact] - public async Task Create_Throws_For_Null_Options() - { - // Arrange, Act & Assert - await using var transport = new TestServerTransport(); - Assert.Throws("serverOptions", () => McpServerFactory.Create(transport, null!, LoggerFactory)); - } -} diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs index b2e748730..be271a686 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs @@ -25,7 +25,7 @@ public void CanCreateServerWithLoggingLevelHandler() var provider = services.BuildServiceProvider(); - provider.GetRequiredService(); + provider.GetRequiredService(); } [Fact] @@ -39,7 +39,7 @@ public void AddingLoggingLevelHandlerSetsLoggingCapability() var provider = services.BuildServiceProvider(); - var server = provider.GetRequiredService(); + var server = provider.GetRequiredService(); Assert.NotNull(server.ServerOptions.Capabilities?.Logging); Assert.NotNull(server.ServerOptions.Capabilities.Logging.SetLoggingLevelHandler); @@ -52,7 +52,7 @@ public void ServerWithoutCallingLoggingLevelHandlerDoesNotSetLoggingCapability() services.AddMcpServer() .WithStdioServerTransport(); var provider = services.BuildServiceProvider(); - var server = provider.GetRequiredService(); + var server = provider.GetRequiredService(); Assert.Null(server.ServerOptions.Capabilities?.Logging); } } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs index 39e9b72ff..d49aff5bb 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerPromptTests.cs @@ -35,9 +35,9 @@ public void Create_InvalidArgs_Throws() [Fact] public async Task SupportsIMcpServer() { - Mock mockServer = new(); + Mock mockServer = new(); - McpServerPrompt prompt = McpServerPrompt.Create((IMcpServer server) => + McpServerPrompt prompt = McpServerPrompt.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new ChatMessage(ChatRole.User, "Hello"); @@ -63,7 +63,7 @@ public async Task SupportsCtorInjection() sc.AddSingleton(expectedMyService); IServiceProvider services = sc.BuildServiceProvider(); - Mock mockServer = new(); + Mock mockServer = new(); mockServer.SetupGet(s => s.Services).Returns(services); MethodInfo? testMethod = typeof(HasCtorWithSpecialParameters).GetMethod(nameof(HasCtorWithSpecialParameters.TestPrompt)); @@ -86,11 +86,11 @@ public async Task SupportsCtorInjection() private sealed class HasCtorWithSpecialParameters { private readonly MyService _ms; - private readonly IMcpServer _server; + private readonly McpServer _server; private readonly RequestContext _request; private readonly IProgress _progress; - public HasCtorWithSpecialParameters(MyService ms, IMcpServer server, RequestContext request, IProgress progress) + public HasCtorWithSpecialParameters(MyService ms, McpServer server, RequestContext request, IProgress progress) { Assert.NotNull(ms); Assert.NotNull(server); @@ -125,11 +125,11 @@ public async Task SupportsServiceFromDI() Assert.DoesNotContain("actualMyService", prompt.ProtocolPrompt.Arguments?.Select(a => a.Name) ?? []); await Assert.ThrowsAnyAsync(async () => await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken)); var result = await prompt.GetAsync( - new RequestContext(new Mock().Object) { Services = services }, + new RequestContext(new Mock().Object) { Services = services }, TestContext.Current.CancellationToken); Assert.Equal("Hello", Assert.IsType(result.Messages[0].Content).Text); } @@ -150,7 +150,7 @@ public async Task SupportsOptionalServiceFromDI() }, new() { Services = services }); var result = await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("Hello", Assert.IsType(result.Messages[0].Content).Text); } @@ -163,7 +163,7 @@ public async Task SupportsDisposingInstantiatedDisposableTargets() _ => new DisposablePromptType()); var result = await prompt1.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("disposals:1", Assert.IsType(result.Messages[0].Content).Text); } @@ -176,7 +176,7 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableTargets() _ => new AsyncDisposablePromptType()); var result = await prompt1.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("asyncDisposals:1", Assert.IsType(result.Messages[0].Content).Text); } @@ -189,7 +189,7 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposable _ => new AsyncDisposableAndDisposablePromptType()); var result = await prompt1.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("disposals:0, asyncDisposals:1", Assert.IsType(result.Messages[0].Content).Text); } @@ -205,7 +205,7 @@ public async Task CanReturnGetPromptResult() }); var actual = await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Same(expected, actual); @@ -222,7 +222,7 @@ public async Task CanReturnText() }); var actual = await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); @@ -248,7 +248,7 @@ public async Task CanReturnPromptMessage() }); var actual = await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); @@ -280,7 +280,7 @@ public async Task CanReturnPromptMessages() }); var actual = await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); @@ -307,7 +307,7 @@ public async Task CanReturnChatMessage() }); var actual = await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); @@ -339,7 +339,7 @@ public async Task CanReturnChatMessages() }); var actual = await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.NotNull(actual); @@ -360,7 +360,7 @@ public async Task ThrowsForNullReturn() }); await Assert.ThrowsAsync(async () => await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken)); } @@ -373,7 +373,7 @@ public async Task ThrowsForUnexpectedTypeReturn() }); await Assert.ThrowsAsync(async () => await prompt.GetAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken)); } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs index 011c4f2b6..9e688455d 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerResourceTests.cs @@ -50,7 +50,7 @@ public void CanCreateServerWithResource() var provider = services.BuildServiceProvider(); - provider.GetRequiredService(); + provider.GetRequiredService(); } @@ -86,7 +86,7 @@ public void CanCreateServerWithResourceTemplates() var provider = services.BuildServiceProvider(); - provider.GetRequiredService(); + provider.GetRequiredService(); } [Fact] @@ -109,7 +109,7 @@ public void CreatingReadHandlerWithNoListHandlerSucceeds() }); var sp = services.BuildServiceProvider(); - sp.GetRequiredService(); + sp.GetRequiredService(); } [Fact] @@ -133,7 +133,7 @@ public async Task UriTemplate_CreatedFromParameters_LotsOfTypesSupported() McpServerResource t; ReadResourceResult? result; - IMcpServer server = new Mock().Object; + McpServer server = new Mock().Object; t = McpServerResource.Create(() => "42", new() { Name = Name }); Assert.Equal("resource://mcp/Hello", t.ProtocolResourceTemplate.UriTemplate); @@ -143,7 +143,7 @@ public async Task UriTemplate_CreatedFromParameters_LotsOfTypesSupported() Assert.NotNull(result); Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text); - t = McpServerResource.Create((IMcpServer server) => "42", new() { Name = Name }); + t = McpServerResource.Create((McpServer server) => "42", new() { Name = Name }); Assert.Equal("resource://mcp/Hello", t.ProtocolResourceTemplate.UriTemplate); result = await t.ReadAsync( new RequestContext(server) { Params = new() { Uri = "resource://mcp/Hello" } }, @@ -277,7 +277,7 @@ public async Task UriTemplate_NonMatchingUri_ReturnsNull(string uri) McpServerResource t = McpServerResource.Create((string arg1) => arg1, new() { Name = "Hello" }); Assert.Equal("resource://mcp/Hello{?arg1}", t.ProtocolResourceTemplate.UriTemplate); Assert.Null(await t.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = uri } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = uri } }, TestContext.Current.CancellationToken)); } @@ -288,7 +288,7 @@ public async Task UriTemplate_IsHostCaseInsensitive(string actualUri, string que { McpServerResource t = McpServerResource.Create(() => "resource", new() { UriTemplate = actualUri }); Assert.NotNull(await t.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = queriedUri } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = queriedUri } }, TestContext.Current.CancellationToken)); } @@ -317,7 +317,7 @@ public async Task UriTemplate_MissingParameter_Throws(string uri) McpServerResource t = McpServerResource.Create((string arg1, int arg2) => arg1, new() { Name = "Hello" }); Assert.Equal("resource://mcp/Hello{?arg1,arg2}", t.ProtocolResourceTemplate.UriTemplate); await Assert.ThrowsAsync(async () => await t.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = uri } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = uri } }, TestContext.Current.CancellationToken)); } @@ -330,25 +330,25 @@ public async Task UriTemplate_MissingOptionalParameter_Succeeds() ReadResourceResult? result; result = await t.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Hello" } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Hello" } }, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal("", ((TextResourceContents)result.Contents[0]).Text); result = await t.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Hello?arg1=first" } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Hello?arg1=first" } }, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal("first", ((TextResourceContents)result.Contents[0]).Text); result = await t.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Hello?arg2=42" } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Hello?arg2=42" } }, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text); result = await t.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Hello?arg1=first&arg2=42" } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Hello?arg1=first&arg2=42" } }, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal("first42", ((TextResourceContents)result.Contents[0]).Text); @@ -357,9 +357,9 @@ public async Task UriTemplate_MissingOptionalParameter_Succeeds() [Fact] public async Task SupportsIMcpServer() { - Mock mockServer = new(); + Mock mockServer = new(); - McpServerResource resource = McpServerResource.Create((IMcpServer server) => + McpServerResource resource = McpServerResource.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return "42"; @@ -381,7 +381,7 @@ public async Task SupportsCtorInjection() sc.AddSingleton(expectedMyService); IServiceProvider services = sc.BuildServiceProvider(); - Mock mockServer = new(); + Mock mockServer = new(); mockServer.SetupGet(s => s.Services).Returns(services); MethodInfo? testMethod = typeof(HasCtorWithSpecialParameters).GetMethod(nameof(HasCtorWithSpecialParameters.TestResource)); @@ -404,11 +404,11 @@ public async Task SupportsCtorInjection() private sealed class HasCtorWithSpecialParameters { private readonly MyService _ms; - private readonly IMcpServer _server; + private readonly McpServer _server; private readonly RequestContext _request; private readonly IProgress _progress; - public HasCtorWithSpecialParameters(MyService ms, IMcpServer server, RequestContext request, IProgress progress) + public HasCtorWithSpecialParameters(MyService ms, McpServer server, RequestContext request, IProgress progress) { Assert.NotNull(ms); Assert.NotNull(server); @@ -467,7 +467,7 @@ public async Task SupportsServiceFromDI(ServiceLifetime injectedArgumentLifetime McpServerResource resource = services.GetRequiredService(); - Mock mockServer = new(); + Mock mockServer = new(); await Assert.ThrowsAnyAsync(async () => await resource.ReadAsync( new RequestContext(mockServer.Object) { Params = new() { Uri = "resource://mcp/Test" } }, @@ -496,7 +496,7 @@ public async Task SupportsOptionalServiceFromDI() }, new() { Services = services, Name = "Test" }); var result = await resource.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Test" } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = "resource://mcp/Test" } }, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal("42", ((TextResourceContents)result.Contents[0]).Text); @@ -512,7 +512,7 @@ public async Task SupportsDisposingInstantiatedDisposableTargets() _ => new DisposableResourceType()); var result = await resource1.ReadAsync( - new RequestContext(new Mock().Object) { Params = new() { Uri = "test://static/resource/instanceMethod" } }, + new RequestContext(new Mock().Object) { Params = new() { Uri = "test://static/resource/instanceMethod" } }, TestContext.Current.CancellationToken); Assert.NotNull(result); Assert.Equal("0", ((TextResourceContents)result.Contents[0]).Text); @@ -523,8 +523,8 @@ public async Task SupportsDisposingInstantiatedDisposableTargets() [Fact] public async Task CanReturnReadResult() { - Mock mockServer = new(); - McpServerResource resource = McpServerResource.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerResource resource = McpServerResource.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new ReadResourceResult { Contents = new List { new TextResourceContents { Text = "hello" } } }; @@ -540,8 +540,8 @@ public async Task CanReturnReadResult() [Fact] public async Task CanReturnResourceContents() { - Mock mockServer = new(); - McpServerResource resource = McpServerResource.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerResource resource = McpServerResource.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new TextResourceContents { Text = "hello" }; @@ -557,8 +557,8 @@ public async Task CanReturnResourceContents() [Fact] public async Task CanReturnCollectionOfResourceContents() { - Mock mockServer = new(); - McpServerResource resource = McpServerResource.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerResource resource = McpServerResource.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return (IList) @@ -579,8 +579,8 @@ public async Task CanReturnCollectionOfResourceContents() [Fact] public async Task CanReturnString() { - Mock mockServer = new(); - McpServerResource resource = McpServerResource.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerResource resource = McpServerResource.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return "42"; @@ -596,8 +596,8 @@ public async Task CanReturnString() [Fact] public async Task CanReturnCollectionOfStrings() { - Mock mockServer = new(); - McpServerResource resource = McpServerResource.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerResource resource = McpServerResource.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new List { "42", "43" }; @@ -614,8 +614,8 @@ public async Task CanReturnCollectionOfStrings() [Fact] public async Task CanReturnDataContent() { - Mock mockServer = new(); - McpServerResource resource = McpServerResource.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerResource resource = McpServerResource.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new DataContent(new byte[] { 0, 1, 2 }, "application/octet-stream"); @@ -632,8 +632,8 @@ public async Task CanReturnDataContent() [Fact] public async Task CanReturnCollectionOfAIContent() { - Mock mockServer = new(); - McpServerResource resource = McpServerResource.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerResource resource = McpServerResource.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new List diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 6750b2cad..61cda7015 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -32,12 +32,38 @@ private static McpServerOptions CreateOptions(ServerCapabilities? capabilities = }; } + [Fact] + public async Task Create_Should_Initialize_With_Valid_Parameters() + { + // Arrange & Act + await using var transport = new TestServerTransport(); + await using McpServer server = McpServer.Create(transport, _options, LoggerFactory); + + // Assert + Assert.NotNull(server); + } + + [Fact] + public void Create_Throws_For_Null_ServerTransport() + { + // Arrange, Act & Assert + Assert.Throws("transport", () => McpServer.Create(null!, _options, LoggerFactory)); + } + + [Fact] + public async Task Create_Throws_For_Null_Options() + { + // Arrange, Act & Assert + await using var transport = new TestServerTransport(); + Assert.Throws("serverOptions", () => McpServer.Create(transport, null!, LoggerFactory)); + } + [Fact] public async Task Constructor_Should_Initialize_With_Valid_Parameters() { // Arrange & Act await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); // Assert Assert.NotNull(server); @@ -47,7 +73,7 @@ public async Task Constructor_Should_Initialize_With_Valid_Parameters() public void Constructor_Throws_For_Null_Transport() { // Arrange, Act & Assert - Assert.Throws(() => McpServerFactory.Create(null!, _options, LoggerFactory)); + Assert.Throws(() => McpServer.Create(null!, _options, LoggerFactory)); } [Fact] @@ -55,7 +81,7 @@ public async Task Constructor_Throws_For_Null_Options() { // Arrange, Act & Assert await using var transport = new TestServerTransport(); - Assert.Throws(() => McpServerFactory.Create(transport, null!, LoggerFactory)); + Assert.Throws(() => McpServer.Create(transport, null!, LoggerFactory)); } [Fact] @@ -63,7 +89,7 @@ public async Task Constructor_Does_Not_Throw_For_Null_Logger() { // Arrange & Act await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, null); + await using var server = McpServer.Create(transport, _options, null); // Assert Assert.NotNull(server); @@ -74,7 +100,7 @@ public async Task Constructor_Does_Not_Throw_For_Null_ServiceProvider() { // Arrange & Act await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory, null); + await using var server = McpServer.Create(transport, _options, LoggerFactory, null); // Assert Assert.NotNull(server); @@ -85,7 +111,7 @@ public async Task RunAsync_Should_Throw_InvalidOperationException_If_Already_Run { // Arrange await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); // Act & Assert @@ -100,7 +126,7 @@ public async Task SampleAsync_Should_Throw_Exception_If_Client_Does_Not_Support_ { // Arrange await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); SetClientCapabilities(server, new ClientCapabilities()); var action = async () => await server.SampleAsync(new CreateMessageRequestParams { Messages = [] }, CancellationToken.None); @@ -114,7 +140,7 @@ public async Task SampleAsync_Should_SendRequest() { // Arrange await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); SetClientCapabilities(server, new ClientCapabilities { Sampling = new SamplingCapability() }); var runTask = server.RunAsync(TestContext.Current.CancellationToken); @@ -136,7 +162,7 @@ public async Task RequestRootsAsync_Should_Throw_Exception_If_Client_Does_Not_Su { // Arrange await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); SetClientCapabilities(server, new ClientCapabilities()); // Act & Assert @@ -148,7 +174,7 @@ public async Task RequestRootsAsync_Should_SendRequest() { // Arrange await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); SetClientCapabilities(server, new ClientCapabilities { Roots = new RootsCapability() }); var runTask = server.RunAsync(TestContext.Current.CancellationToken); @@ -170,7 +196,7 @@ public async Task ElicitAsync_Should_Throw_Exception_If_Client_Does_Not_Support_ { // Arrange await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); SetClientCapabilities(server, new ClientCapabilities()); // Act & Assert @@ -182,7 +208,7 @@ public async Task ElicitAsync_Should_SendRequest() { // Arrange await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); SetClientCapabilities(server, new ClientCapabilities { Elicitation = new ElicitationCapability() }); var runTask = server.RunAsync(TestContext.Current.CancellationToken); @@ -216,7 +242,7 @@ await Can_Handle_Requests( [Fact] public async Task Can_Handle_Initialize_Requests() { - AssemblyName expectedAssemblyName = (Assembly.GetEntryAssembly() ?? typeof(IMcpServer).Assembly).GetName(); + AssemblyName expectedAssemblyName = (Assembly.GetEntryAssembly() ?? typeof(McpServer).Assembly).GetName(); await Can_Handle_Requests( serverCapabilities: null, method: RequestMethods.Initialize, @@ -510,7 +536,7 @@ private async Task Can_Handle_Requests(ServerCapabilities? serverCapabilities, s var options = CreateOptions(serverCapabilities); configureOptions?.Invoke(options); - await using var server = McpServerFactory.Create(transport, options, LoggerFactory); + await using var server = McpServer.Create(transport, options, LoggerFactory); var runTask = server.RunAsync(TestContext.Current.CancellationToken); @@ -544,7 +570,7 @@ private async Task Succeeds_Even_If_No_Handler_Assigned(ServerCapabilities serve await using var transport = new TestServerTransport(); var options = CreateOptions(serverCapabilities); - var server = McpServerFactory.Create(transport, options, LoggerFactory); + var server = McpServer.Create(transport, options, LoggerFactory); await server.DisposeAsync(); } @@ -589,7 +615,7 @@ public async Task AsSamplingChatClient_HandlesRequestResponse() public async Task Can_SendMessage_Before_RunAsync() { await using var transport = new TestServerTransport(); - await using var server = McpServerFactory.Create(transport, _options, LoggerFactory); + await using var server = McpServer.Create(transport, _options, LoggerFactory); var logNotification = new JsonRpcNotification { @@ -605,22 +631,22 @@ public async Task Can_SendMessage_Before_RunAsync() Assert.Same(logNotification, transport.SentMessages[0]); } - private static void SetClientCapabilities(IMcpServer server, ClientCapabilities capabilities) + private static void SetClientCapabilities(McpServer server, ClientCapabilities capabilities) { - PropertyInfo? property = server.GetType().GetProperty("ClientCapabilities", BindingFlags.Public | BindingFlags.Instance); - Assert.NotNull(property); - property.SetValue(server, capabilities); + FieldInfo? field = server.GetType().GetField("_clientCapabilities", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(field); + field.SetValue(server, capabilities); } - private sealed class TestServerForIChatClient(bool supportsSampling) : IMcpServer + private sealed class TestServerForIChatClient(bool supportsSampling) : McpServer { - public ClientCapabilities? ClientCapabilities => + public override ClientCapabilities? ClientCapabilities => supportsSampling ? new ClientCapabilities { Sampling = new SamplingCapability() } : null; - public McpServerOptions ServerOptions => new(); + public override McpServerOptions ServerOptions => new(); - public Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) + public override Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken) { CreateMessageRequestParams? rp = JsonSerializer.Deserialize(request.Params, McpJsonUtilities.DefaultOptions); @@ -653,17 +679,17 @@ public Task SendRequestAsync(JsonRpcRequest request, Cancellati }); } - public ValueTask DisposeAsync() => default; + public override ValueTask DisposeAsync() => default; - public string? SessionId => throw new NotImplementedException(); - public Implementation? ClientInfo => throw new NotImplementedException(); - public IServiceProvider? Services => throw new NotImplementedException(); - public LoggingLevel? LoggingLevel => throw new NotImplementedException(); - public Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) => + public override string? SessionId => throw new NotImplementedException(); + public override Implementation? ClientInfo => throw new NotImplementedException(); + public override IServiceProvider? Services => throw new NotImplementedException(); + public override LoggingLevel? LoggingLevel => throw new NotImplementedException(); + public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task RunAsync(CancellationToken cancellationToken = default) => + public override Task RunAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public IAsyncDisposable RegisterNotificationHandler(string method, Func handler) => + public override IAsyncDisposable RegisterNotificationHandler(string method, Func handler) => throw new NotImplementedException(); } @@ -683,7 +709,7 @@ public async Task NotifyProgress_Should_Be_Handled() })], }; - var server = McpServerFactory.Create(transport, options, LoggerFactory); + var server = McpServer.Create(transport, options, LoggerFactory); Task serverTask = server.RunAsync(TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs index f961eef34..c9cee1148 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerToolTests.cs @@ -42,9 +42,9 @@ public void Create_InvalidArgs_Throws() [Fact] public async Task SupportsIMcpServer() { - Mock mockServer = new(); + Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return "42"; @@ -67,7 +67,7 @@ public async Task SupportsCtorInjection() sc.AddSingleton(expectedMyService); IServiceProvider services = sc.BuildServiceProvider(); - Mock mockServer = new(); + Mock mockServer = new(); mockServer.SetupGet(s => s.Services).Returns(services); MethodInfo? testMethod = typeof(HasCtorWithSpecialParameters).GetMethod(nameof(HasCtorWithSpecialParameters.TestTool)); @@ -90,11 +90,11 @@ public async Task SupportsCtorInjection() private sealed class HasCtorWithSpecialParameters { private readonly MyService _ms; - private readonly IMcpServer _server; + private readonly McpServer _server; private readonly RequestContext _request; private readonly IProgress _progress; - public HasCtorWithSpecialParameters(MyService ms, IMcpServer server, RequestContext request, IProgress progress) + public HasCtorWithSpecialParameters(MyService ms, McpServer server, RequestContext request, IProgress progress) { Assert.NotNull(ms); Assert.NotNull(server); @@ -154,7 +154,7 @@ public async Task SupportsServiceFromDI(ServiceLifetime injectedArgumentLifetime Assert.DoesNotContain("actualMyService", JsonSerializer.Serialize(tool.ProtocolTool.InputSchema, McpJsonUtilities.DefaultOptions)); - Mock mockServer = new(); + Mock mockServer = new(); var result = await tool.InvokeAsync( new RequestContext(mockServer.Object), @@ -183,7 +183,7 @@ public async Task SupportsOptionalServiceFromDI() }, new() { Services = services }); var result = await tool.InvokeAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("42", (result.Content[0] as TextContentBlock)?.Text); } @@ -198,7 +198,7 @@ public async Task SupportsDisposingInstantiatedDisposableTargets() options); var result = await tool1.InvokeAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("""{"disposals":1}""", (result.Content[0] as TextContentBlock)?.Text); } @@ -213,7 +213,7 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableTargets() options); var result = await tool1.InvokeAsync( - new RequestContext(new Mock().Object), + new RequestContext(new Mock().Object), TestContext.Current.CancellationToken); Assert.Equal("""{"asyncDisposals":1}""", (result.Content[0] as TextContentBlock)?.Text); } @@ -232,7 +232,7 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposable options); var result = await tool1.InvokeAsync( - new RequestContext(new Mock().Object) { Services = services }, + new RequestContext(new Mock().Object) { Services = services }, TestContext.Current.CancellationToken); Assert.Equal("""{"asyncDisposals":1,"disposals":0}""", (result.Content[0] as TextContentBlock)?.Text); } @@ -241,8 +241,8 @@ public async Task SupportsAsyncDisposingInstantiatedAsyncDisposableAndDisposable [Fact] public async Task CanReturnCollectionOfAIContent() { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new List { @@ -273,8 +273,8 @@ public async Task CanReturnCollectionOfAIContent() [InlineData("data:audio/wav;base64,1234", "audio")] public async Task CanReturnSingleAIContent(string data, string type) { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return type switch @@ -316,8 +316,8 @@ public async Task CanReturnSingleAIContent(string data, string type) [Fact] public async Task CanReturnNullAIContent() { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return (string?)null; @@ -331,8 +331,8 @@ public async Task CanReturnNullAIContent() [Fact] public async Task CanReturnString() { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return "42"; @@ -347,8 +347,8 @@ public async Task CanReturnString() [Fact] public async Task CanReturnCollectionOfStrings() { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new List { "42", "43" }; @@ -363,8 +363,8 @@ public async Task CanReturnCollectionOfStrings() [Fact] public async Task CanReturnMcpContent() { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return new TextContentBlock { Text = "42" }; @@ -380,8 +380,8 @@ public async Task CanReturnMcpContent() [Fact] public async Task CanReturnCollectionOfMcpContent() { - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return (IList) @@ -407,8 +407,8 @@ public async Task CanReturnCallToolResult() Content = new List { new TextContentBlock { Text = "text" }, new ImageContentBlock { Data = "1234", MimeType = "image/png" } } }; - Mock mockServer = new(); - McpServerTool tool = McpServerTool.Create((IMcpServer server) => + Mock mockServer = new(); + McpServerTool tool = McpServerTool.Create((McpServer server) => { Assert.Same(mockServer.Object, server); return response; @@ -465,7 +465,7 @@ public async Task ToolCallError_LogsErrorMessage() throw new InvalidOperationException(exceptionMessage); }, new() { Name = toolName, Services = serviceProvider }); - var mockServer = new Mock(); + var mockServer = new Mock(); var request = new RequestContext(mockServer.Object) { Params = new CallToolRequestParams { Name = toolName }, @@ -492,7 +492,7 @@ public async Task StructuredOutput_Enabled_ReturnsExpectedSchema(T value) { JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; McpServerTool tool = McpServerTool.Create(() => value, new() { Name = "tool", UseStructuredContent = true, SerializerOptions = options }); - var mockServer = new Mock(); + var mockServer = new Mock(); var request = new RequestContext(mockServer.Object) { Params = new CallToolRequestParams { Name = "tool" }, @@ -510,7 +510,7 @@ public async Task StructuredOutput_Enabled_ReturnsExpectedSchema(T value) public async Task StructuredOutput_Enabled_VoidReturningTools_ReturnsExpectedSchema() { McpServerTool tool = McpServerTool.Create(() => { }); - var mockServer = new Mock(); + var mockServer = new Mock(); var request = new RequestContext(mockServer.Object) { Params = new CallToolRequestParams { Name = "tool" }, @@ -550,7 +550,7 @@ public async Task StructuredOutput_Disabled_ReturnsExpectedSchema(T value) { JsonSerializerOptions options = new() { TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; McpServerTool tool = McpServerTool.Create(() => value, new() { UseStructuredContent = false, SerializerOptions = options }); - var mockServer = new Mock(); + var mockServer = new Mock(); var request = new RequestContext(mockServer.Object) { Params = new CallToolRequestParams { Name = "tool" }, diff --git a/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs b/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs index f3927be62..d14c376c1 100644 --- a/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.Tests/StdioServerIntegrationTests.cs @@ -35,7 +35,7 @@ public async Task SigInt_DisposesTestServerWithHosting_Gracefully() process.StandardInput.BaseStream, serverName: "TestServerWithHosting"); - await using var client = await McpClientFactory.CreateAsync( + await using var client = await McpClient.CreateAsync( new TestClientTransport(streamServerTransport), loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs index 93cbcec82..084908fcc 100644 --- a/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/StdioClientTransportTests.cs @@ -18,7 +18,7 @@ public async Task CreateAsync_ValidProcessInvalidServer_Throws() new(new() { Command = "cmd", Arguments = ["/C", $"echo \"{id}\" >&2"] }, LoggerFactory) : new(new() { Command = "ls", Arguments = [id] }, LoggerFactory); - IOException e = await Assert.ThrowsAsync(() => McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); + IOException e = await Assert.ThrowsAsync(() => McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); Assert.Contains(id, e.ToString()); } @@ -43,7 +43,7 @@ public async Task CreateAsync_ValidProcessInvalidServer_StdErrCallbackInvoked() new(new() { Command = "cmd", Arguments = ["/C", $"echo \"{id}\" >&2"], StandardErrorLines = stdErrCallback }, LoggerFactory) : new(new() { Command = "ls", Arguments = [id], StandardErrorLines = stdErrCallback }, LoggerFactory); - await Assert.ThrowsAsync(() => McpClientFactory.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); + await Assert.ThrowsAsync(() => McpClient.CreateAsync(transport, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken)); Assert.InRange(count, 1, int.MaxValue); Assert.Contains(id, sb.ToString()); From cb0066d51a92896b50f6ed44961ec7aeefc9de92 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 28 Aug 2025 14:27:40 -0400 Subject: [PATCH 03/15] Fix missing renames --- src/ModelContextProtocol.Core/McpSession.cs | 2 +- ...tExtensions.cs => McpSessionExtensions.cs} | 52 +++++++++---------- .../Protocol/ITransport.cs | 2 +- ...ession.cs => DestinationBoundMcpServer.cs} | 2 +- .../Server/McpServerImpl.cs | 4 +- 5 files changed, 31 insertions(+), 31 deletions(-) rename src/ModelContextProtocol.Core/{McpEndpointExtensions.cs => McpSessionExtensions.cs} (85%) rename src/ModelContextProtocol.Core/Server/{DestinationBoundMcpServerSession.cs => DestinationBoundMcpServer.cs} (93%) diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs index 82caca12b..a5d929729 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -65,7 +65,7 @@ public abstract class McpSession : IAsyncDisposable /// /// This method provides low-level access to send any JSON-RPC message. For specific message types, /// consider using the higher-level methods such as or extension methods - /// like , + /// like , /// which provide a simpler API. /// /// diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpSessionExtensions.cs similarity index 85% rename from src/ModelContextProtocol.Core/McpEndpointExtensions.cs rename to src/ModelContextProtocol.Core/McpSessionExtensions.cs index cf6e57be6..69a3b64cf 100644 --- a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs +++ b/src/ModelContextProtocol.Core/McpSessionExtensions.cs @@ -12,7 +12,7 @@ namespace ModelContextProtocol; /// /// /// -/// This class provides strongly-typed methods for working with the Model Context Protocol (MCP) endpoints, +/// This class provides strongly-typed methods for working with the Model Context Protocol (MCP) sessions, /// simplifying JSON-RPC communication by handling serialization and deserialization of parameters and results. /// /// @@ -20,14 +20,14 @@ namespace ModelContextProtocol; /// server () implementations of the interface. /// /// -public static class McpEndpointExtensions +public static class McpSessionExtensions { /// /// Sends a JSON-RPC request and attempts to deserialize the result to . /// /// The type of the request parameters to serialize from. /// The type of the result to deserialize to. - /// The MCP client or server instance. + /// The MCP client or server instance. /// The JSON-RPC method name to invoke. /// Object representing the request parameters. /// The request id for the request. @@ -35,7 +35,7 @@ public static class McpEndpointExtensions /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. The task result contains the deserialized result. public static ValueTask SendRequestAsync( - this McpSession endpoint, + this McpSession session, string method, TParameters parameters, JsonSerializerOptions? serializerOptions = null, @@ -48,7 +48,7 @@ public static ValueTask SendRequestAsync( JsonTypeInfo paramsTypeInfo = serializerOptions.GetTypeInfo(); JsonTypeInfo resultTypeInfo = serializerOptions.GetTypeInfo(); - return SendRequestAsync(endpoint, method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken); + return SendRequestAsync(session, method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken); } /// @@ -56,7 +56,7 @@ public static ValueTask SendRequestAsync( /// /// The type of the request parameters to serialize from. /// The type of the result to deserialize to. - /// The MCP client or server instance. + /// The MCP client or server instance. /// The JSON-RPC method name to invoke. /// Object representing the request parameters. /// The type information for request parameter serialization. @@ -65,7 +65,7 @@ public static ValueTask SendRequestAsync( /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. The task result contains the deserialized result. internal static async ValueTask SendRequestAsync( - this McpSession endpoint, + this McpSession session, string method, TParameters parameters, JsonTypeInfo parametersTypeInfo, @@ -74,7 +74,7 @@ internal static async ValueTask SendRequestAsync( CancellationToken cancellationToken = default) where TResult : notnull { - Throw.IfNull(endpoint); + Throw.IfNull(session); Throw.IfNullOrWhiteSpace(method); Throw.IfNull(parametersTypeInfo); Throw.IfNull(resultTypeInfo); @@ -86,12 +86,12 @@ internal static async ValueTask SendRequestAsync( Params = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo), }; - JsonRpcResponse response = await endpoint.SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false); + JsonRpcResponse response = await session.SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false); return JsonSerializer.Deserialize(response.Result, resultTypeInfo) ?? throw new JsonException("Unexpected JSON result in response."); } /// - /// Sends a parameterless notification to the connected endpoint. + /// Sends a parameterless notification to the connected session. /// /// The MCP client or server instance. /// The notification method name. @@ -112,10 +112,10 @@ public static Task SendNotificationAsync(this McpSession client, string method, } /// - /// Sends a notification with parameters to the connected endpoint. + /// Sends a notification with parameters to the connected session. /// /// The type of the notification parameters to serialize. - /// The MCP client or server instance. + /// The MCP client or server instance. /// The JSON-RPC method name for the notification. /// Object representing the notification parameters. /// The options governing parameter serialization. If null, default options are used. @@ -123,7 +123,7 @@ public static Task SendNotificationAsync(this McpSession client, string method, /// A task that represents the asynchronous send operation. /// /// - /// This method sends a notification with parameters to the connected endpoint. Notifications are one-way + /// This method sends a notification with parameters to the connected session. Notifications are one-way /// messages that don't expect a response, commonly used for events, status updates, or signaling changes. /// /// @@ -136,7 +136,7 @@ public static Task SendNotificationAsync(this McpSession client, string method, /// /// public static Task SendNotificationAsync( - this McpSession endpoint, + this McpSession session, string method, TParameters parameters, JsonSerializerOptions? serializerOptions = null, @@ -146,44 +146,44 @@ public static Task SendNotificationAsync( serializerOptions.MakeReadOnly(); JsonTypeInfo parametersTypeInfo = serializerOptions.GetTypeInfo(); - return SendNotificationAsync(endpoint, method, parameters, parametersTypeInfo, cancellationToken); + return SendNotificationAsync(session, method, parameters, parametersTypeInfo, cancellationToken); } /// /// Sends a notification to the server with parameters. /// - /// The MCP client or server instance. + /// The MCP client or server instance. /// The JSON-RPC method name to invoke. /// Object representing the request parameters. /// The type information for request parameter serialization. /// The to monitor for cancellation requests. The default is . internal static Task SendNotificationAsync( - this McpSession endpoint, + this McpSession session, string method, TParameters parameters, JsonTypeInfo parametersTypeInfo, CancellationToken cancellationToken = default) { - Throw.IfNull(endpoint); + Throw.IfNull(session); Throw.IfNullOrWhiteSpace(method); Throw.IfNull(parametersTypeInfo); JsonNode? parametersJson = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo); - return endpoint.SendMessageAsync(new JsonRpcNotification { Method = method, Params = parametersJson }, cancellationToken); + return session.SendMessageAsync(new JsonRpcNotification { Method = method, Params = parametersJson }, cancellationToken); } /// - /// Notifies the connected endpoint of progress for a long-running operation. + /// Notifies the connected session of progress for a long-running operation. /// - /// The endpoint issuing the notification. + /// The session issuing the notification. /// The identifying the operation for which progress is being reported. /// The progress update to send, containing information such as percentage complete or status message. /// The to monitor for cancellation requests. The default is . /// A task representing the completion of the notification operation (not the operation being tracked). - /// is . + /// is . /// /// - /// This method sends a progress notification to the connected endpoint using the Model Context Protocol's + /// This method sends a progress notification to the connected session using the Model Context Protocol's /// standardized progress notification format. Progress updates are identified by a /// that allows the recipient to correlate multiple updates with a specific long-running operation. /// @@ -192,14 +192,14 @@ internal static Task SendNotificationAsync( /// /// public static Task NotifyProgressAsync( - this McpSession endpoint, + this McpSession session, ProgressToken progressToken, ProgressNotificationValue progress, CancellationToken cancellationToken = default) { - Throw.IfNull(endpoint); + Throw.IfNull(session); - return endpoint.SendNotificationAsync( + return session.SendNotificationAsync( NotificationMethods.ProgressNotification, new ProgressNotificationParams { diff --git a/src/ModelContextProtocol.Core/Protocol/ITransport.cs b/src/ModelContextProtocol.Core/Protocol/ITransport.cs index 4fac03597..26b7993ec 100644 --- a/src/ModelContextProtocol.Core/Protocol/ITransport.cs +++ b/src/ModelContextProtocol.Core/Protocol/ITransport.cs @@ -63,7 +63,7 @@ public interface ITransport : IAsyncDisposable /// /// This is a core method used by higher-level abstractions in the MCP protocol implementation. /// Most client code should use the higher-level methods provided by , - /// , , or , + /// , , or , /// rather than accessing this method directly. /// /// diff --git a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServerSession.cs b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs similarity index 93% rename from src/ModelContextProtocol.Core/Server/DestinationBoundMcpServerSession.cs rename to src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs index 2220ed17c..791a47c89 100644 --- a/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServerSession.cs +++ b/src/ModelContextProtocol.Core/Server/DestinationBoundMcpServer.cs @@ -3,7 +3,7 @@ namespace ModelContextProtocol.Server; -internal sealed class DestinationBoundMcpServerSession(McpServerImpl server, ITransport? transport) : McpServer +internal sealed class DestinationBoundMcpServer(McpServerImpl server, ITransport? transport) : McpServer { public override string? SessionId => transport?.SessionId ?? server.SessionId; public override ClientCapabilities? ClientCapabilities => server.ClientCapabilities; diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 77b47d6a3..1f83fc086 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -561,7 +561,7 @@ private ValueTask InvokeHandlerAsync( { return _servicesScopePerRequest ? InvokeScopedAsync(handler, args, cancellationToken) : - handler(new(new DestinationBoundMcpServerSession(this, destinationTransport)) { Params = args }, cancellationToken); + handler(new(new DestinationBoundMcpServer(this, destinationTransport)) { Params = args }, cancellationToken); async ValueTask InvokeScopedAsync( Func, CancellationToken, ValueTask> handler, @@ -572,7 +572,7 @@ async ValueTask InvokeScopedAsync( try { return await handler( - new RequestContext(new DestinationBoundMcpServerSession(this, destinationTransport)) + new RequestContext(new DestinationBoundMcpServer(this, destinationTransport)) { Services = scope?.ServiceProvider ?? Services, Params = args From c6a019da70692ee0193f7cb4dce750e3fa80cb50 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 28 Aug 2025 15:42:25 -0400 Subject: [PATCH 04/15] Move extension methods to instance methods --- .../Client/McpClient.cs | 667 +++++++++++- .../Client/McpClientExtensions.cs | 955 ------------------ .../Client/McpClientPrompt.cs | 6 +- .../Client/McpClientResource.cs | 6 +- .../Client/McpClientResourceTemplate.cs | 4 +- .../Client/McpClientTool.cs | 4 +- src/ModelContextProtocol.Core/McpSession.cs | 182 +++- .../McpSessionExtensions.cs | 212 ---- .../Protocol/ClientCapabilities.cs | 2 +- .../Protocol/ITransport.cs | 2 +- .../Protocol/Reference.cs | 2 +- .../Protocol/SamplingCapability.cs | 2 +- .../Server/McpServer.cs | 331 ++++++ .../Server/McpServerExtensions.cs | 362 ------- .../Client/McpClientCreationTests.cs | 155 +++ .../Client/McpClientExtensionsTests.cs | 471 --------- .../Client/McpClientTests.cs | 510 ++++++++-- 17 files changed, 1756 insertions(+), 2117 deletions(-) delete mode 100644 src/ModelContextProtocol.Core/McpSessionExtensions.cs delete mode 100644 src/ModelContextProtocol.Core/Server/McpServerExtensions.cs create mode 100644 tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs delete mode 100644 tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs index bda535af8..59bacc7bd 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -1,5 +1,9 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.Runtime.CompilerServices; +using System.Text.Json; namespace ModelContextProtocol.Client; @@ -81,4 +85,665 @@ public static async Task CreateAsync( return clientSession; } + + /// + /// Sends a ping request to verify server connectivity. + /// + /// The to monitor for cancellation requests. The default is . + /// A task that completes when the ping is successful. + /// Thrown when the server cannot be reached or returns an error response. + public Task PingAsync(CancellationToken cancellationToken = default) + { + var opts = McpJsonUtilities.DefaultOptions; + opts.MakeReadOnly(); + return this.SendRequestAsync( + RequestMethods.Ping, + parameters: null, + serializerOptions: opts, + cancellationToken: cancellationToken).AsTask(); + } + + /// + /// Retrieves a list of available tools from the server. + /// + /// The serializer options governing tool parameter serialization. If null, the default options will be used. + /// The to monitor for cancellation requests. The default is . + /// A list of all available tools as instances. + public async ValueTask> ListToolsAsync( + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + List? tools = null; + string? cursor = null; + do + { + var toolResults = await SendRequestAsync( + RequestMethods.ToolsList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, + McpJsonUtilities.JsonContext.Default.ListToolsResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + tools ??= new List(toolResults.Tools.Count); + foreach (var tool in toolResults.Tools) + { + tools.Add(new McpClientTool(this, tool, serializerOptions)); + } + + cursor = toolResults.NextCursor; + } + while (cursor is not null); + + return tools; + } + + /// + /// Creates an enumerable for asynchronously enumerating all available tools from the server. + /// + /// The serializer options governing tool parameter serialization. If null, the default options will be used. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available tools as instances. + public async IAsyncEnumerable EnumerateToolsAsync( + JsonSerializerOptions? serializerOptions = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + string? cursor = null; + do + { + var toolResults = await SendRequestAsync( + RequestMethods.ToolsList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, + McpJsonUtilities.JsonContext.Default.ListToolsResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + foreach (var tool in toolResults.Tools) + { + yield return new McpClientTool(this, tool, serializerOptions); + } + + cursor = toolResults.NextCursor; + } + while (cursor is not null); + } + + /// + /// Retrieves a list of available prompts from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// A list of all available prompts as instances. + public async ValueTask> ListPromptsAsync( + CancellationToken cancellationToken = default) + { + List? prompts = null; + string? cursor = null; + do + { + var promptResults = await SendRequestAsync( + RequestMethods.PromptsList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, + McpJsonUtilities.JsonContext.Default.ListPromptsResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + prompts ??= new List(promptResults.Prompts.Count); + foreach (var prompt in promptResults.Prompts) + { + prompts.Add(new McpClientPrompt(this, prompt)); + } + + cursor = promptResults.NextCursor; + } + while (cursor is not null); + + return prompts; + } + + /// + /// Creates an enumerable for asynchronously enumerating all available prompts from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available prompts as instances. + public async IAsyncEnumerable EnumeratePromptsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + string? cursor = null; + do + { + var promptResults = await SendRequestAsync( + RequestMethods.PromptsList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, + McpJsonUtilities.JsonContext.Default.ListPromptsResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + foreach (var prompt in promptResults.Prompts) + { + yield return new(this, prompt); + } + + cursor = promptResults.NextCursor; + } + while (cursor is not null); + } + + /// + /// Retrieves a specific prompt from the MCP server. + /// + /// The name of the prompt to retrieve. + /// Optional arguments for the prompt. Keys are parameter names, and values are the argument values. + /// The serialization options governing argument serialization. + /// The to monitor for cancellation requests. The default is . + /// A task containing the prompt's result with content and messages. + public ValueTask GetPromptAsync( + string name, + IReadOnlyDictionary? arguments = null, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(name); + + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + return SendRequestAsync( + RequestMethods.PromptsGet, + new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions) }, + McpJsonUtilities.JsonContext.Default.GetPromptRequestParams, + McpJsonUtilities.JsonContext.Default.GetPromptResult, + cancellationToken: cancellationToken); + } + + /// + /// Retrieves a list of available resource templates from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// A list of all available resource templates as instances. + public async ValueTask> ListResourceTemplatesAsync( + CancellationToken cancellationToken = default) + { + List? resourceTemplates = null; + + string? cursor = null; + do + { + var templateResults = await SendRequestAsync( + RequestMethods.ResourcesTemplatesList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + resourceTemplates ??= new List(templateResults.ResourceTemplates.Count); + foreach (var template in templateResults.ResourceTemplates) + { + resourceTemplates.Add(new McpClientResourceTemplate(this, template)); + } + + cursor = templateResults.NextCursor; + } + while (cursor is not null); + + return resourceTemplates; + } + + /// + /// Creates an enumerable for asynchronously enumerating all available resource templates from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available resource templates as instances. + public async IAsyncEnumerable EnumerateResourceTemplatesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + string? cursor = null; + do + { + var templateResults = await SendRequestAsync( + RequestMethods.ResourcesTemplatesList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + foreach (var templateResult in templateResults.ResourceTemplates) + { + yield return new McpClientResourceTemplate(this, templateResult); + } + + cursor = templateResults.NextCursor; + } + while (cursor is not null); + } + + /// + /// Retrieves a list of available resources from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// A list of all available resources as instances. + public async ValueTask> ListResourcesAsync( + CancellationToken cancellationToken = default) + { + List? resources = null; + + string? cursor = null; + do + { + var resourceResults = await SendRequestAsync( + RequestMethods.ResourcesList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourcesResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + resources ??= new List(resourceResults.Resources.Count); + foreach (var resource in resourceResults.Resources) + { + resources.Add(new McpClientResource(this, resource)); + } + + cursor = resourceResults.NextCursor; + } + while (cursor is not null); + + return resources; + } + + /// + /// Creates an enumerable for asynchronously enumerating all available resources from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available resources as instances. + public async IAsyncEnumerable EnumerateResourcesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + string? cursor = null; + do + { + var resourceResults = await SendRequestAsync( + RequestMethods.ResourcesList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourcesResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + foreach (var resource in resourceResults.Resources) + { + yield return new McpClientResource(this, resource); + } + + cursor = resourceResults.NextCursor; + } + while (cursor is not null); + } + + /// + /// Reads a resource from the server. + /// + /// The uri of the resource. + /// The to monitor for cancellation requests. The default is . + public ValueTask ReadResourceAsync( + string uri, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(uri); + + return SendRequestAsync( + RequestMethods.ResourcesRead, + new() { Uri = uri }, + McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, + McpJsonUtilities.JsonContext.Default.ReadResourceResult, + cancellationToken: cancellationToken); + } + + /// + /// Reads a resource from the server. + /// + /// The uri of the resource. + /// The to monitor for cancellation requests. The default is . + public ValueTask ReadResourceAsync( + Uri uri, CancellationToken cancellationToken = default) + { + Throw.IfNull(uri); + + return ReadResourceAsync(uri.ToString(), cancellationToken); + } + + /// + /// Reads a resource from the server. + /// + /// The uri template of the resource. + /// Arguments to use to format . + /// The to monitor for cancellation requests. The default is . + public ValueTask ReadResourceAsync( + string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(uriTemplate); + Throw.IfNull(arguments); + + return SendRequestAsync( + RequestMethods.ResourcesRead, + new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) }, + McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, + McpJsonUtilities.JsonContext.Default.ReadResourceResult, + cancellationToken: cancellationToken); + } + + /// + /// Requests completion suggestions for a prompt argument or resource reference. + /// + /// The reference object specifying the type and optional URI or name. + /// The name of the argument for which completions are requested. + /// The current value of the argument, used to filter relevant completions. + /// The to monitor for cancellation requests. The default is . + /// A containing completion suggestions. + public ValueTask CompleteAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) + { + Throw.IfNull(reference); + Throw.IfNullOrWhiteSpace(argumentName); + + return SendRequestAsync( + RequestMethods.CompletionComplete, + new() + { + Ref = reference, + Argument = new Argument { Name = argumentName, Value = argumentValue } + }, + McpJsonUtilities.JsonContext.Default.CompleteRequestParams, + McpJsonUtilities.JsonContext.Default.CompleteResult, + cancellationToken: cancellationToken); + } + + /// + /// Subscribes to a resource on the server to receive notifications when it changes. + /// + /// The URI of the resource to which to subscribe. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + public Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(uri); + + return SendRequestAsync( + RequestMethods.ResourcesSubscribe, + new() { Uri = uri }, + McpJsonUtilities.JsonContext.Default.SubscribeRequestParams, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken).AsTask(); + } + + /// + /// Subscribes to a resource on the server to receive notifications when it changes. + /// + /// The URI of the resource to which to subscribe. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + public Task SubscribeToResourceAsync(Uri uri, CancellationToken cancellationToken = default) + { + Throw.IfNull(uri); + + return SubscribeToResourceAsync(uri.ToString(), cancellationToken); + } + + /// + /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. + /// + /// The URI of the resource to unsubscribe from. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + public Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(uri); + + return SendRequestAsync( + RequestMethods.ResourcesUnsubscribe, + new() { Uri = uri }, + McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken).AsTask(); + } + + /// + /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. + /// + /// The URI of the resource to unsubscribe from. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + public Task UnsubscribeFromResourceAsync(Uri uri, CancellationToken cancellationToken = default) + { + Throw.IfNull(uri); + + return UnsubscribeFromResourceAsync(uri.ToString(), cancellationToken); + } + + /// + /// Invokes a tool on the server. + /// + /// The name of the tool to call on the server.. + /// An optional dictionary of arguments to pass to the tool. + /// Optional progress reporter for server notifications. + /// JSON serializer options. + /// A cancellation token. + /// The from the tool execution. + public ValueTask CallToolAsync( + string toolName, + IReadOnlyDictionary? arguments = null, + IProgress? progress = null, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(toolName); + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + if (progress is not null) + { + return SendRequestWithProgressAsync(toolName, arguments, progress, serializerOptions, cancellationToken); + } + + return SendRequestAsync( + RequestMethods.ToolsCall, + new() + { + Name = toolName, + Arguments = ToArgumentsDictionary(arguments, serializerOptions), + }, + McpJsonUtilities.JsonContext.Default.CallToolRequestParams, + McpJsonUtilities.JsonContext.Default.CallToolResult, + cancellationToken: cancellationToken); + + async ValueTask SendRequestWithProgressAsync( + string toolName, + IReadOnlyDictionary? arguments, + IProgress progress, + JsonSerializerOptions serializerOptions, + CancellationToken cancellationToken) + { + ProgressToken progressToken = new(Guid.NewGuid().ToString("N")); + + await using var _ = RegisterNotificationHandler(NotificationMethods.ProgressNotification, + (notification, cancellationToken) => + { + if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn && + pn.ProgressToken == progressToken) + { + progress.Report(pn.Progress); + } + + return default; + }).ConfigureAwait(false); + + return await SendRequestAsync( + RequestMethods.ToolsCall, + new() + { + Name = toolName, + Arguments = ToArgumentsDictionary(arguments, serializerOptions), + ProgressToken = progressToken, + }, + McpJsonUtilities.JsonContext.Default.CallToolRequestParams, + McpJsonUtilities.JsonContext.Default.CallToolResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Converts the contents of a into a pair of + /// and instances to use + /// as inputs into a operation. + /// + /// + /// The created pair of messages and options. + /// is . + internal static (IList Messages, ChatOptions? Options) ToChatClientArguments( + CreateMessageRequestParams requestParams) + { + Throw.IfNull(requestParams); + + ChatOptions? options = null; + + if (requestParams.MaxTokens is int maxTokens) + { + (options ??= new()).MaxOutputTokens = maxTokens; + } + + if (requestParams.Temperature is float temperature) + { + (options ??= new()).Temperature = temperature; + } + + if (requestParams.StopSequences is { } stopSequences) + { + (options ??= new()).StopSequences = stopSequences.ToArray(); + } + + List messages = + (from sm in requestParams.Messages + let aiContent = sm.Content.ToAIContent() + where aiContent is not null + select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) + .ToList(); + + return (messages, options); + } + + /// Converts the contents of a into a . + /// The whose contents should be extracted. + /// The created . + /// is . + internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatResponse) + { + Throw.IfNull(chatResponse); + + // The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports + // only either a single blob of text or a single image. Heuristically, we'll use an image if there is one + // in any of the response messages, or we'll use all the text from them concatenated, otherwise. + + ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); + + ContentBlock? content = null; + if (lastMessage is not null) + { + foreach (var lmc in lastMessage.Contents) + { + if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) + { + content = dc.ToContent(); + } + } + } + + return new() + { + Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, + Model = chatResponse.ModelId ?? "unknown", + Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, + StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", + }; + } + + /// + /// Creates a sampling handler for use with that will + /// satisfy sampling requests using the specified . + /// + /// The with which to satisfy sampling requests. + /// The created handler delegate that can be assigned to . + /// is . + public static Func, CancellationToken, ValueTask> CreateSamplingHandler( + IChatClient chatClient) + { + Throw.IfNull(chatClient); + + return async (requestParams, progress, cancellationToken) => + { + Throw.IfNull(requestParams); + + var (messages, options) = ToChatClientArguments(requestParams); + var progressToken = requestParams.ProgressToken; + + List updates = []; + await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) + { + updates.Add(update); + + if (progressToken is not null) + { + progress.Report(new() + { + Progress = updates.Count, + }); + } + } + + return ToCreateMessageResult(updates.ToChatResponse()); + }; + } + + /// + /// Sets the logging level for the server to control which log messages are sent to the client. + /// + /// The minimum severity level of log messages to receive from the server. + /// The to monitor for cancellation requests. The default is . + /// A task representing the asynchronous operation. + public Task SetLoggingLevel(LoggingLevel level, CancellationToken cancellationToken = default) + { + return SendRequestAsync( + RequestMethods.LoggingSetLevel, + new() { Level = level }, + McpJsonUtilities.JsonContext.Default.SetLevelRequestParams, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken).AsTask(); + } + + /// + /// Sets the logging level for the server to control which log messages are sent to the client. + /// + /// The minimum severity level of log messages to receive from the server. + /// The to monitor for cancellation requests. The default is . + /// A task representing the asynchronous operation. + public Task SetLoggingLevel(LogLevel level, CancellationToken cancellationToken = default) => + SetLoggingLevel(McpServerImpl.ToLoggingLevel(level), cancellationToken); + + /// Convers a dictionary with values to a dictionary with values. + private static Dictionary? ToArgumentsDictionary( + IReadOnlyDictionary? arguments, JsonSerializerOptions options) + { + var typeInfo = options.GetTypeInfo(); + + Dictionary? result = null; + if (arguments is not null) + { + result = new(arguments.Count); + foreach (var kvp in arguments) + { + result.Add(kvp.Key, kvp.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(kvp.Value, typeInfo)); + } + } + + return result; + } } diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index c7f101ac9..afac2bc80 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -1,9 +1,5 @@ using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; -using System.Runtime.CompilerServices; -using System.Text.Json; namespace ModelContextProtocol.Client; @@ -19,876 +15,6 @@ namespace ModelContextProtocol.Client; /// public static class McpClientExtensions { - /// - /// Sends a ping request to verify server connectivity. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// A task that completes when the ping is successful. - /// - /// - /// This method is used to check if the MCP server is online and responding to requests. - /// It can be useful for health checking, ensuring the connection is established, or verifying - /// that the client has proper authorization to communicate with the server. - /// - /// - /// The ping operation is lightweight and does not require any parameters. A successful completion - /// of the task indicates that the server is operational and accessible. - /// - /// - /// is . - /// Thrown when the server cannot be reached or returns an error response. - public static Task PingAsync(this McpClient client, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - return client.SendRequestAsync( - RequestMethods.Ping, - parameters: null, - McpJsonUtilities.JsonContext.Default.Object!, - McpJsonUtilities.JsonContext.Default.Object, - cancellationToken: cancellationToken).AsTask(); - } - - /// - /// Retrieves a list of available tools from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The serializer options governing tool parameter serialization. If null, the default options will be used. - /// The to monitor for cancellation requests. The default is . - /// A list of all available tools as instances. - /// - /// - /// This method fetches all available tools from the MCP server and returns them as a complete list. - /// It automatically handles pagination with cursors if the server responds with only a portion per request. - /// - /// - /// For servers with a large number of tools and that responds with paginated responses, consider using - /// instead, as it streams tools as they arrive rather than loading them all at once. - /// - /// - /// The serializer options provided are flowed to each and will be used - /// when invoking tools in order to serialize any parameters. - /// - /// - /// - /// - /// // Get all tools available on the server - /// var tools = await mcpClient.ListToolsAsync(); - /// - /// // Use tools with an AI client - /// ChatOptions chatOptions = new() - /// { - /// Tools = [.. tools] - /// }; - /// - /// await foreach (var update in chatClient.GetStreamingResponseAsync(userMessage, chatOptions)) - /// { - /// Console.Write(update); - /// } - /// - /// - /// is . - public static async ValueTask> ListToolsAsync( - this McpClient client, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - List? tools = null; - string? cursor = null; - do - { - var toolResults = await client.SendRequestAsync( - RequestMethods.ToolsList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, - McpJsonUtilities.JsonContext.Default.ListToolsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - tools ??= new List(toolResults.Tools.Count); - foreach (var tool in toolResults.Tools) - { - tools.Add(new McpClientTool(client, tool, serializerOptions)); - } - - cursor = toolResults.NextCursor; - } - while (cursor is not null); - - return tools; - } - - /// - /// Creates an enumerable for asynchronously enumerating all available tools from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The serializer options governing tool parameter serialization. If null, the default options will be used. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available tools as instances. - /// - /// - /// This method uses asynchronous enumeration to retrieve tools from the server, which allows processing tools - /// as they arrive rather than waiting for all tools to be retrieved. The method automatically handles pagination - /// with cursors if the server responds with tools split across multiple responses. - /// - /// - /// The serializer options provided are flowed to each and will be used - /// when invoking tools in order to serialize any parameters. - /// - /// - /// Every iteration through the returned - /// will result in re-querying the server and yielding the sequence of available tools. - /// - /// - /// - /// - /// // Enumerate all tools available on the server - /// await foreach (var tool in client.EnumerateToolsAsync()) - /// { - /// Console.WriteLine($"Tool: {tool.Name}"); - /// } - /// - /// - /// is . - public static async IAsyncEnumerable EnumerateToolsAsync( - this McpClient client, - JsonSerializerOptions? serializerOptions = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - string? cursor = null; - do - { - var toolResults = await client.SendRequestAsync( - RequestMethods.ToolsList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, - McpJsonUtilities.JsonContext.Default.ListToolsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var tool in toolResults.Tools) - { - yield return new McpClientTool(client, tool, serializerOptions); - } - - cursor = toolResults.NextCursor; - } - while (cursor is not null); - } - - /// - /// Retrieves a list of available prompts from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// A list of all available prompts as instances. - /// - /// - /// This method fetches all available prompts from the MCP server and returns them as a complete list. - /// It automatically handles pagination with cursors if the server responds with only a portion per request. - /// - /// - /// For servers with a large number of prompts and that responds with paginated responses, consider using - /// instead, as it streams prompts as they arrive rather than loading them all at once. - /// - /// - /// is . - public static async ValueTask> ListPromptsAsync( - this McpClient client, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - List? prompts = null; - string? cursor = null; - do - { - var promptResults = await client.SendRequestAsync( - RequestMethods.PromptsList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, - McpJsonUtilities.JsonContext.Default.ListPromptsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - prompts ??= new List(promptResults.Prompts.Count); - foreach (var prompt in promptResults.Prompts) - { - prompts.Add(new McpClientPrompt(client, prompt)); - } - - cursor = promptResults.NextCursor; - } - while (cursor is not null); - - return prompts; - } - - /// - /// Creates an enumerable for asynchronously enumerating all available prompts from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available prompts as instances. - /// - /// - /// This method uses asynchronous enumeration to retrieve prompts from the server, which allows processing prompts - /// as they arrive rather than waiting for all prompts to be retrieved. The method automatically handles pagination - /// with cursors if the server responds with prompts split across multiple responses. - /// - /// - /// Every iteration through the returned - /// will result in re-querying the server and yielding the sequence of available prompts. - /// - /// - /// - /// - /// // Enumerate all prompts available on the server - /// await foreach (var prompt in client.EnumeratePromptsAsync()) - /// { - /// Console.WriteLine($"Prompt: {prompt.Name}"); - /// } - /// - /// - /// is . - public static async IAsyncEnumerable EnumeratePromptsAsync( - this McpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - string? cursor = null; - do - { - var promptResults = await client.SendRequestAsync( - RequestMethods.PromptsList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, - McpJsonUtilities.JsonContext.Default.ListPromptsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var prompt in promptResults.Prompts) - { - yield return new(client, prompt); - } - - cursor = promptResults.NextCursor; - } - while (cursor is not null); - } - - /// - /// Retrieves a specific prompt from the MCP server. - /// - /// The client instance used to communicate with the MCP server. - /// The name of the prompt to retrieve. - /// Optional arguments for the prompt. Keys are parameter names, and values are the argument values. - /// The serialization options governing argument serialization. - /// The to monitor for cancellation requests. The default is . - /// A task containing the prompt's result with content and messages. - /// - /// - /// This method sends a request to the MCP server to create the specified prompt with the provided arguments. - /// The server will process the arguments and return a prompt containing messages or other content. - /// - /// - /// Arguments are serialized into JSON and passed to the server, where they may be used to customize the - /// prompt's behavior or content. Each prompt may have different argument requirements. - /// - /// - /// The returned contains a collection of objects, - /// which can be converted to objects using the method. - /// - /// - /// Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt. - /// is . - public static ValueTask GetPromptAsync( - this McpClient client, - string name, - IReadOnlyDictionary? arguments = null, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNullOrWhiteSpace(name); - - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - return client.SendRequestAsync( - RequestMethods.PromptsGet, - new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions) }, - McpJsonUtilities.JsonContext.Default.GetPromptRequestParams, - McpJsonUtilities.JsonContext.Default.GetPromptResult, - cancellationToken: cancellationToken); - } - - /// - /// Retrieves a list of available resource templates from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// A list of all available resource templates as instances. - /// - /// - /// This method fetches all available resource templates from the MCP server and returns them as a complete list. - /// It automatically handles pagination with cursors if the server responds with only a portion per request. - /// - /// - /// For servers with a large number of resource templates and that responds with paginated responses, consider using - /// instead, as it streams templates as they arrive rather than loading them all at once. - /// - /// - /// is . - public static async ValueTask> ListResourceTemplatesAsync( - this McpClient client, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - List? resourceTemplates = null; - - string? cursor = null; - do - { - var templateResults = await client.SendRequestAsync( - RequestMethods.ResourcesTemplatesList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - resourceTemplates ??= new List(templateResults.ResourceTemplates.Count); - foreach (var template in templateResults.ResourceTemplates) - { - resourceTemplates.Add(new McpClientResourceTemplate(client, template)); - } - - cursor = templateResults.NextCursor; - } - while (cursor is not null); - - return resourceTemplates; - } - - /// - /// Creates an enumerable for asynchronously enumerating all available resource templates from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available resource templates as instances. - /// - /// - /// This method uses asynchronous enumeration to retrieve resource templates from the server, which allows processing templates - /// as they arrive rather than waiting for all templates to be retrieved. The method automatically handles pagination - /// with cursors if the server responds with templates split across multiple responses. - /// - /// - /// Every iteration through the returned - /// will result in re-querying the server and yielding the sequence of available resource templates. - /// - /// - /// - /// - /// // Enumerate all resource templates available on the server - /// await foreach (var template in client.EnumerateResourceTemplatesAsync()) - /// { - /// Console.WriteLine($"Template: {template.Name}"); - /// } - /// - /// - /// is . - public static async IAsyncEnumerable EnumerateResourceTemplatesAsync( - this McpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - string? cursor = null; - do - { - var templateResults = await client.SendRequestAsync( - RequestMethods.ResourcesTemplatesList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var templateResult in templateResults.ResourceTemplates) - { - yield return new McpClientResourceTemplate(client, templateResult); - } - - cursor = templateResults.NextCursor; - } - while (cursor is not null); - } - - /// - /// Retrieves a list of available resources from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// A list of all available resources as instances. - /// - /// - /// This method fetches all available resources from the MCP server and returns them as a complete list. - /// It automatically handles pagination with cursors if the server responds with only a portion per request. - /// - /// - /// For servers with a large number of resources and that responds with paginated responses, consider using - /// instead, as it streams resources as they arrive rather than loading them all at once. - /// - /// - /// - /// - /// // Get all resources available on the server - /// var resources = await client.ListResourcesAsync(); - /// - /// // Display information about each resource - /// foreach (var resource in resources) - /// { - /// Console.WriteLine($"Resource URI: {resource.Uri}"); - /// } - /// - /// - /// is . - public static async ValueTask> ListResourcesAsync( - this McpClient client, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - List? resources = null; - - string? cursor = null; - do - { - var resourceResults = await client.SendRequestAsync( - RequestMethods.ResourcesList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourcesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - resources ??= new List(resourceResults.Resources.Count); - foreach (var resource in resourceResults.Resources) - { - resources.Add(new McpClientResource(client, resource)); - } - - cursor = resourceResults.NextCursor; - } - while (cursor is not null); - - return resources; - } - - /// - /// Creates an enumerable for asynchronously enumerating all available resources from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available resources as instances. - /// - /// - /// This method uses asynchronous enumeration to retrieve resources from the server, which allows processing resources - /// as they arrive rather than waiting for all resources to be retrieved. The method automatically handles pagination - /// with cursors if the server responds with resources split across multiple responses. - /// - /// - /// Every iteration through the returned - /// will result in re-querying the server and yielding the sequence of available resources. - /// - /// - /// - /// - /// // Enumerate all resources available on the server - /// await foreach (var resource in client.EnumerateResourcesAsync()) - /// { - /// Console.WriteLine($"Resource URI: {resource.Uri}"); - /// } - /// - /// - /// is . - public static async IAsyncEnumerable EnumerateResourcesAsync( - this McpClient client, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - string? cursor = null; - do - { - var resourceResults = await client.SendRequestAsync( - RequestMethods.ResourcesList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourcesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var resource in resourceResults.Resources) - { - yield return new McpClientResource(client, resource); - } - - cursor = resourceResults.NextCursor; - } - while (cursor is not null); - } - - /// - /// Reads a resource from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The uri of the resource. - /// The to monitor for cancellation requests. The default is . - /// is . - /// is . - /// is empty or composed entirely of whitespace. - public static ValueTask ReadResourceAsync( - this McpClient client, string uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNullOrWhiteSpace(uri); - - return client.SendRequestAsync( - RequestMethods.ResourcesRead, - new() { Uri = uri }, - McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, - McpJsonUtilities.JsonContext.Default.ReadResourceResult, - cancellationToken: cancellationToken); - } - - /// - /// Reads a resource from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The uri of the resource. - /// The to monitor for cancellation requests. The default is . - /// is . - /// is . - public static ValueTask ReadResourceAsync( - this McpClient client, Uri uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNull(uri); - - return ReadResourceAsync(client, uri.ToString(), cancellationToken); - } - - /// - /// Reads a resource from the server. - /// - /// The client instance used to communicate with the MCP server. - /// The uri template of the resource. - /// Arguments to use to format . - /// The to monitor for cancellation requests. The default is . - /// is . - /// is . - /// is empty or composed entirely of whitespace. - public static ValueTask ReadResourceAsync( - this McpClient client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNullOrWhiteSpace(uriTemplate); - Throw.IfNull(arguments); - - return client.SendRequestAsync( - RequestMethods.ResourcesRead, - new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) }, - McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, - McpJsonUtilities.JsonContext.Default.ReadResourceResult, - cancellationToken: cancellationToken); - } - - /// - /// Requests completion suggestions for a prompt argument or resource reference. - /// - /// The client instance used to communicate with the MCP server. - /// The reference object specifying the type and optional URI or name. - /// The name of the argument for which completions are requested. - /// The current value of the argument, used to filter relevant completions. - /// The to monitor for cancellation requests. The default is . - /// A containing completion suggestions. - /// - /// - /// This method allows clients to request auto-completion suggestions for arguments in a prompt template - /// or for resource references. - /// - /// - /// When working with prompt references, the server will return suggestions for the specified argument - /// that match or begin with the current argument value. This is useful for implementing intelligent - /// auto-completion in user interfaces. - /// - /// - /// When working with resource references, the server will return suggestions relevant to the specified - /// resource URI. - /// - /// - /// is . - /// is . - /// is . - /// is empty or composed entirely of whitespace. - /// The server returned an error response. - public static ValueTask CompleteAsync(this McpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNull(reference); - Throw.IfNullOrWhiteSpace(argumentName); - - return client.SendRequestAsync( - RequestMethods.CompletionComplete, - new() - { - Ref = reference, - Argument = new Argument { Name = argumentName, Value = argumentValue } - }, - McpJsonUtilities.JsonContext.Default.CompleteRequestParams, - McpJsonUtilities.JsonContext.Default.CompleteResult, - cancellationToken: cancellationToken); - } - - /// - /// Subscribes to a resource on the server to receive notifications when it changes. - /// - /// The client instance used to communicate with the MCP server. - /// The URI of the resource to which to subscribe. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - /// - /// - /// This method allows the client to register interest in a specific resource identified by its URI. - /// When the resource changes, the server will send notifications to the client, enabling real-time - /// updates without polling. - /// - /// - /// The subscription remains active until explicitly unsubscribed using - /// or until the client disconnects from the server. - /// - /// - /// To handle resource change notifications, register an event handler for the appropriate notification events, - /// such as with . - /// - /// - /// is . - /// is . - /// is empty or composed entirely of whitespace. - public static Task SubscribeToResourceAsync(this McpClient client, string uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNullOrWhiteSpace(uri); - - return client.SendRequestAsync( - RequestMethods.ResourcesSubscribe, - new() { Uri = uri }, - McpJsonUtilities.JsonContext.Default.SubscribeRequestParams, - McpJsonUtilities.JsonContext.Default.EmptyResult, - cancellationToken: cancellationToken).AsTask(); - } - - /// - /// Subscribes to a resource on the server to receive notifications when it changes. - /// - /// The client instance used to communicate with the MCP server. - /// The URI of the resource to which to subscribe. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - /// - /// - /// This method allows the client to register interest in a specific resource identified by its URI. - /// When the resource changes, the server will send notifications to the client, enabling real-time - /// updates without polling. - /// - /// - /// The subscription remains active until explicitly unsubscribed using - /// or until the client disconnects from the server. - /// - /// - /// To handle resource change notifications, register an event handler for the appropriate notification events, - /// such as with . - /// - /// - /// is . - /// is . - public static Task SubscribeToResourceAsync(this McpClient client, Uri uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNull(uri); - - return SubscribeToResourceAsync(client, uri.ToString(), cancellationToken); - } - - /// - /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. - /// - /// The client instance used to communicate with the MCP server. - /// The URI of the resource to unsubscribe from. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - /// - /// - /// This method cancels a previous subscription to a resource, stopping the client from receiving - /// notifications when that resource changes. - /// - /// - /// The unsubscribe operation is idempotent, meaning it can be called multiple times for the same - /// resource without causing errors, even if there is no active subscription. - /// - /// - /// Due to the nature of the MCP protocol, it is possible the client may receive notifications after - /// unsubscribing if those notifications were issued by the server prior to the unsubscribe request being received. - /// - /// - /// is . - /// is . - /// is empty or composed entirely of whitespace. - public static Task UnsubscribeFromResourceAsync(this McpClient client, string uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNullOrWhiteSpace(uri); - - return client.SendRequestAsync( - RequestMethods.ResourcesUnsubscribe, - new() { Uri = uri }, - McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams, - McpJsonUtilities.JsonContext.Default.EmptyResult, - cancellationToken: cancellationToken).AsTask(); - } - - /// - /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. - /// - /// The client instance used to communicate with the MCP server. - /// The URI of the resource to unsubscribe from. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - /// - /// - /// This method cancels a previous subscription to a resource, stopping the client from receiving - /// notifications when that resource changes. - /// - /// - /// The unsubscribe operation is idempotent, meaning it can be called multiple times for the same - /// resource without causing errors, even if there is no active subscription. - /// - /// - /// Due to the nature of the MCP protocol, it is possible the client may receive notifications after - /// unsubscribing if those notifications were issued by the server prior to the unsubscribe request being received. - /// - /// - /// is . - /// is . - public static Task UnsubscribeFromResourceAsync(this McpClient client, Uri uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNull(uri); - - return UnsubscribeFromResourceAsync(client, uri.ToString(), cancellationToken); - } - - /// - /// Invokes a tool on the server. - /// - /// The client instance used to communicate with the MCP server. - /// The name of the tool to call on the server.. - /// An optional dictionary of arguments to pass to the tool. Each key represents a parameter name, - /// and its associated value represents the argument value. - /// - /// - /// An optional to have progress notifications reported to it. Setting this to a non- - /// value will result in a progress token being included in the call, and any resulting progress notifications during the operation - /// routed to this instance. - /// - /// - /// The JSON serialization options governing argument serialization. If , the default serialization options will be used. - /// - /// The to monitor for cancellation requests. The default is . - /// - /// A task containing the from the tool execution. The response includes - /// the tool's output content, which may be structured data, text, or an error message. - /// - /// is . - /// is . - /// The server could not find the requested tool, or the server encountered an error while processing the request. - /// - /// - /// // Call a simple echo tool with a string argument - /// var result = await client.CallToolAsync( - /// "echo", - /// new Dictionary<string, object?> - /// { - /// ["message"] = "Hello MCP!" - /// }); - /// - /// - public static ValueTask CallToolAsync( - this McpClient client, - string toolName, - IReadOnlyDictionary? arguments = null, - IProgress? progress = null, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNull(toolName); - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - if (progress is not null) - { - return SendRequestWithProgressAsync(client, toolName, arguments, progress, serializerOptions, cancellationToken); - } - - return client.SendRequestAsync( - RequestMethods.ToolsCall, - new() - { - Name = toolName, - Arguments = ToArgumentsDictionary(arguments, serializerOptions), - }, - McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResult, - cancellationToken: cancellationToken); - - static async ValueTask SendRequestWithProgressAsync( - McpClient client, - string toolName, - IReadOnlyDictionary? arguments, - IProgress progress, - JsonSerializerOptions serializerOptions, - CancellationToken cancellationToken) - { - ProgressToken progressToken = new(Guid.NewGuid().ToString("N")); - - await using var _ = client.RegisterNotificationHandler(NotificationMethods.ProgressNotification, - (notification, cancellationToken) => - { - if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn && - pn.ProgressToken == progressToken) - { - progress.Report(pn.Progress); - } - - return default; - }).ConfigureAwait(false); - - return await client.SendRequestAsync( - RequestMethods.ToolsCall, - new() - { - Name = toolName, - Arguments = ToArgumentsDictionary(arguments, serializerOptions), - ProgressToken = progressToken, - }, - McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - } - } - /// /// Converts the contents of a into a pair of /// and instances to use @@ -1010,85 +136,4 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat return updates.ToChatResponse().ToCreateMessageResult(); }; } - - /// - /// Sets the logging level for the server to control which log messages are sent to the client. - /// - /// The client instance used to communicate with the MCP server. - /// The minimum severity level of log messages to receive from the server. - /// The to monitor for cancellation requests. The default is . - /// A task representing the asynchronous operation. - /// - /// - /// After this request is processed, the server will send log messages at or above the specified - /// logging level as notifications to the client. For example, if is set, - /// the client will receive , , - /// , , and - /// level messages. - /// - /// - /// To receive all log messages, set the level to . - /// - /// - /// Log messages are delivered as notifications to the client and can be captured by registering - /// appropriate event handlers with the client implementation, such as with . - /// - /// - /// is . - public static Task SetLoggingLevel(this McpClient client, LoggingLevel level, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - - return client.SendRequestAsync( - RequestMethods.LoggingSetLevel, - new() { Level = level }, - McpJsonUtilities.JsonContext.Default.SetLevelRequestParams, - McpJsonUtilities.JsonContext.Default.EmptyResult, - cancellationToken: cancellationToken).AsTask(); - } - - /// - /// Sets the logging level for the server to control which log messages are sent to the client. - /// - /// The client instance used to communicate with the MCP server. - /// The minimum severity level of log messages to receive from the server. - /// The to monitor for cancellation requests. The default is . - /// A task representing the asynchronous operation. - /// - /// - /// After this request is processed, the server will send log messages at or above the specified - /// logging level as notifications to the client. For example, if is set, - /// the client will receive , , - /// and level messages. - /// - /// - /// To receive all log messages, set the level to . - /// - /// - /// Log messages are delivered as notifications to the client and can be captured by registering - /// appropriate event handlers with the client implementation, such as with . - /// - /// - /// is . - public static Task SetLoggingLevel(this McpClient client, LogLevel level, CancellationToken cancellationToken = default) => - SetLoggingLevel(client, McpServerImpl.ToLoggingLevel(level), cancellationToken); - - /// Convers a dictionary with values to a dictionary with values. - private static Dictionary? ToArgumentsDictionary( - IReadOnlyDictionary? arguments, JsonSerializerOptions options) - { - var typeInfo = options.GetTypeInfo(); - - Dictionary? result = null; - if (arguments is not null) - { - result = new(arguments.Count); - foreach (var kvp in arguments) - { - result.Add(kvp.Key, kvp.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(kvp.Value, typeInfo)); - } - } - - return result; - } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs index 6cef1d778..5a618242f 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientPrompt.cs @@ -10,8 +10,8 @@ namespace ModelContextProtocol.Client; /// /// This class provides a client-side wrapper around a prompt defined on an MCP server. It allows /// retrieving the prompt's content by sending a request to the server with optional arguments. -/// Instances of this class are typically obtained by calling -/// or . +/// Instances of this class are typically obtained by calling +/// or . /// /// /// Each prompt has a name and optionally a description, and it can be invoked with arguments @@ -63,7 +63,7 @@ internal McpClientPrompt(McpClient client, Prompt prompt) /// The server will process the request and return a result containing messages or other content. /// /// - /// This is a convenience method that internally calls + /// This is a convenience method that internally calls /// with this prompt's name and arguments. /// /// diff --git a/src/ModelContextProtocol.Core/Client/McpClientResource.cs b/src/ModelContextProtocol.Core/Client/McpClientResource.cs index 7f7297866..19f11bfdf 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResource.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResource.cs @@ -9,8 +9,8 @@ namespace ModelContextProtocol.Client; /// /// This class provides a client-side wrapper around a resource defined on an MCP server. It allows /// retrieving the resource's content by sending a request to the server with the resource's URI. -/// Instances of this class are typically obtained by calling -/// or . +/// Instances of this class are typically obtained by calling +/// or . /// /// public sealed class McpClientResource @@ -58,7 +58,7 @@ internal McpClientResource(McpClient client, Resource resource) /// A containing the resource's result with content and messages. /// /// - /// This is a convenience method that internally calls . + /// This is a convenience method that internally calls . /// /// public ValueTask ReadAsync( diff --git a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs index 7cd1fa986..033f7cf00 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientResourceTemplate.cs @@ -9,8 +9,8 @@ namespace ModelContextProtocol.Client; /// /// This class provides a client-side wrapper around a resource template defined on an MCP server. It allows /// retrieving the resource template's content by sending a request to the server with the resource's URI. -/// Instances of this class are typically obtained by calling -/// or . +/// Instances of this class are typically obtained by calling +/// or . /// /// public sealed class McpClientResourceTemplate diff --git a/src/ModelContextProtocol.Core/Client/McpClientTool.cs b/src/ModelContextProtocol.Core/Client/McpClientTool.cs index b66c2368f..c7af513ef 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientTool.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientTool.cs @@ -19,8 +19,8 @@ namespace ModelContextProtocol.Client; /// and without changing the underlying tool functionality. /// /// -/// Typically, you would get instances of this class by calling the -/// or extension methods on an instance. +/// Typically, you would get instances of this class by calling the +/// or extension methods on an instance. /// /// public sealed class McpClientTool : AIFunction diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs index a5d929729..267ec4ff5 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -1,6 +1,9 @@ using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; namespace ModelContextProtocol; @@ -46,7 +49,7 @@ public abstract class McpSession : IAsyncDisposable /// An error occured during request processing. /// /// This method provides low-level access to send raw JSON-RPC requests. For most use cases, - /// consider using the strongly-typed extension methods that provide a more convenient API. + /// consider using the strongly-typed methods that provide a more convenient API. /// public abstract Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default); @@ -64,9 +67,8 @@ public abstract class McpSession : IAsyncDisposable /// /// /// This method provides low-level access to send any JSON-RPC message. For specific message types, - /// consider using the higher-level methods such as or extension methods - /// like , - /// which provide a simpler API. + /// consider using the higher-level methods such as or methods + /// on this class that provide a simpler API. /// /// /// The method will serialize the message and transmit it using the underlying transport mechanism. @@ -82,4 +84,176 @@ public abstract class McpSession : IAsyncDisposable /// public abstract ValueTask DisposeAsync(); + + /// + /// Sends a JSON-RPC request and attempts to deserialize the result to . + /// + /// The type of the request parameters to serialize from. + /// The type of the result to deserialize to. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The request id for the request. + /// The options governing request serialization. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains the deserialized result. + public ValueTask SendRequestAsync( + string method, + TParameters parameters, + JsonSerializerOptions? serializerOptions = null, + RequestId requestId = default, + CancellationToken cancellationToken = default) + where TResult : notnull + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + JsonTypeInfo paramsTypeInfo = serializerOptions.GetTypeInfo(); + JsonTypeInfo resultTypeInfo = serializerOptions.GetTypeInfo(); + return SendRequestAsync(method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken); + } + + /// + /// Sends a JSON-RPC request and attempts to deserialize the result to . + /// + /// The type of the request parameters to serialize from. + /// The type of the result to deserialize to. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The type information for request parameter serialization. + /// The type information for request parameter deserialization. + /// The request id for the request. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains the deserialized result. + internal async ValueTask SendRequestAsync( + string method, + TParameters parameters, + JsonTypeInfo parametersTypeInfo, + JsonTypeInfo resultTypeInfo, + RequestId requestId = default, + CancellationToken cancellationToken = default) + where TResult : notnull + { + Throw.IfNullOrWhiteSpace(method); + Throw.IfNull(parametersTypeInfo); + Throw.IfNull(resultTypeInfo); + + JsonRpcRequest jsonRpcRequest = new() + { + Id = requestId, + Method = method, + Params = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo), + }; + + JsonRpcResponse response = await SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(response.Result, resultTypeInfo) ?? throw new JsonException("Unexpected JSON result in response."); + } + + /// + /// Sends a parameterless notification to the connected session. + /// + /// The notification method name. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous send operation. + /// + /// + /// This method sends a notification without any parameters. Notifications are one-way messages + /// that don't expect a response. They are commonly used for events, status updates, or to signal + /// changes in state. + /// + /// + public Task SendNotificationAsync(string method, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(method); + return SendMessageAsync(new JsonRpcNotification { Method = method }, cancellationToken); + } + + /// + /// Sends a notification with parameters to the connected session. + /// + /// The type of the notification parameters to serialize. + /// The JSON-RPC method name for the notification. + /// Object representing the notification parameters. + /// The options governing parameter serialization. If null, default options are used. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous send operation. + /// + /// + /// This method sends a notification with parameters to the connected session. Notifications are one-way + /// messages that don't expect a response, commonly used for events, status updates, or signaling changes. + /// + /// + /// The parameters object is serialized to JSON according to the provided serializer options or the default + /// options if none are specified. + /// + /// + /// The Model Context Protocol defines several standard notification methods in , + /// but custom methods can also be used for application-specific notifications. + /// + /// + public Task SendNotificationAsync( + string method, + TParameters parameters, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + JsonTypeInfo parametersTypeInfo = serializerOptions.GetTypeInfo(); + return SendNotificationAsync(method, parameters, parametersTypeInfo, cancellationToken); + } + + /// + /// Sends a notification to the server with parameters. + /// + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The type information for request parameter serialization. + /// The to monitor for cancellation requests. The default is . + internal Task SendNotificationAsync( + string method, + TParameters parameters, + JsonTypeInfo parametersTypeInfo, + CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(method); + Throw.IfNull(parametersTypeInfo); + + JsonNode? parametersJson = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo); + return SendMessageAsync(new JsonRpcNotification { Method = method, Params = parametersJson }, cancellationToken); + } + + /// + /// Notifies the connected session of progress for a long-running operation. + /// + /// The identifying the operation for which progress is being reported. + /// The progress update to send, containing information such as percentage complete or status message. + /// The to monitor for cancellation requests. The default is . + /// A task representing the completion of the notification operation (not the operation being tracked). + /// The current session instance is . + /// + /// + /// This method sends a progress notification to the connected session using the Model Context Protocol's + /// standardized progress notification format. Progress updates are identified by a + /// that allows the recipient to correlate multiple updates with a specific long-running operation. + /// + /// + /// Progress notifications are sent asynchronously and don't block the operation from continuing. + /// + /// + public Task NotifyProgressAsync( + ProgressToken progressToken, + ProgressNotificationValue progress, + CancellationToken cancellationToken = default) + { + return SendNotificationAsync( + NotificationMethods.ProgressNotification, + new ProgressNotificationParams + { + ProgressToken = progressToken, + Progress = progress, + }, + McpJsonUtilities.JsonContext.Default.ProgressNotificationParams, + cancellationToken); + } } diff --git a/src/ModelContextProtocol.Core/McpSessionExtensions.cs b/src/ModelContextProtocol.Core/McpSessionExtensions.cs deleted file mode 100644 index 69a3b64cf..000000000 --- a/src/ModelContextProtocol.Core/McpSessionExtensions.cs +++ /dev/null @@ -1,212 +0,0 @@ -using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization.Metadata; - -namespace ModelContextProtocol; - -/// -/// Provides extension methods for interacting with an . -/// -/// -/// -/// This class provides strongly-typed methods for working with the Model Context Protocol (MCP) sessions, -/// simplifying JSON-RPC communication by handling serialization and deserialization of parameters and results. -/// -/// -/// These extension methods are designed to be used with both client () and -/// server () implementations of the interface. -/// -/// -public static class McpSessionExtensions -{ - /// - /// Sends a JSON-RPC request and attempts to deserialize the result to . - /// - /// The type of the request parameters to serialize from. - /// The type of the result to deserialize to. - /// The MCP client or server instance. - /// The JSON-RPC method name to invoke. - /// Object representing the request parameters. - /// The request id for the request. - /// The options governing request serialization. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. The task result contains the deserialized result. - public static ValueTask SendRequestAsync( - this McpSession session, - string method, - TParameters parameters, - JsonSerializerOptions? serializerOptions = null, - RequestId requestId = default, - CancellationToken cancellationToken = default) - where TResult : notnull - { - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - JsonTypeInfo paramsTypeInfo = serializerOptions.GetTypeInfo(); - JsonTypeInfo resultTypeInfo = serializerOptions.GetTypeInfo(); - return SendRequestAsync(session, method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken); - } - - /// - /// Sends a JSON-RPC request and attempts to deserialize the result to . - /// - /// The type of the request parameters to serialize from. - /// The type of the result to deserialize to. - /// The MCP client or server instance. - /// The JSON-RPC method name to invoke. - /// Object representing the request parameters. - /// The type information for request parameter serialization. - /// The type information for request parameter deserialization. - /// The request id for the request. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. The task result contains the deserialized result. - internal static async ValueTask SendRequestAsync( - this McpSession session, - string method, - TParameters parameters, - JsonTypeInfo parametersTypeInfo, - JsonTypeInfo resultTypeInfo, - RequestId requestId = default, - CancellationToken cancellationToken = default) - where TResult : notnull - { - Throw.IfNull(session); - Throw.IfNullOrWhiteSpace(method); - Throw.IfNull(parametersTypeInfo); - Throw.IfNull(resultTypeInfo); - - JsonRpcRequest jsonRpcRequest = new() - { - Id = requestId, - Method = method, - Params = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo), - }; - - JsonRpcResponse response = await session.SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(response.Result, resultTypeInfo) ?? throw new JsonException("Unexpected JSON result in response."); - } - - /// - /// Sends a parameterless notification to the connected session. - /// - /// The MCP client or server instance. - /// The notification method name. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous send operation. - /// - /// - /// This method sends a notification without any parameters. Notifications are one-way messages - /// that don't expect a response. They are commonly used for events, status updates, or to signal - /// changes in state. - /// - /// - public static Task SendNotificationAsync(this McpSession client, string method, CancellationToken cancellationToken = default) - { - Throw.IfNull(client); - Throw.IfNullOrWhiteSpace(method); - return client.SendMessageAsync(new JsonRpcNotification { Method = method }, cancellationToken); - } - - /// - /// Sends a notification with parameters to the connected session. - /// - /// The type of the notification parameters to serialize. - /// The MCP client or server instance. - /// The JSON-RPC method name for the notification. - /// Object representing the notification parameters. - /// The options governing parameter serialization. If null, default options are used. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous send operation. - /// - /// - /// This method sends a notification with parameters to the connected session. Notifications are one-way - /// messages that don't expect a response, commonly used for events, status updates, or signaling changes. - /// - /// - /// The parameters object is serialized to JSON according to the provided serializer options or the default - /// options if none are specified. - /// - /// - /// The Model Context Protocol defines several standard notification methods in , - /// but custom methods can also be used for application-specific notifications. - /// - /// - public static Task SendNotificationAsync( - this McpSession session, - string method, - TParameters parameters, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - JsonTypeInfo parametersTypeInfo = serializerOptions.GetTypeInfo(); - return SendNotificationAsync(session, method, parameters, parametersTypeInfo, cancellationToken); - } - - /// - /// Sends a notification to the server with parameters. - /// - /// The MCP client or server instance. - /// The JSON-RPC method name to invoke. - /// Object representing the request parameters. - /// The type information for request parameter serialization. - /// The to monitor for cancellation requests. The default is . - internal static Task SendNotificationAsync( - this McpSession session, - string method, - TParameters parameters, - JsonTypeInfo parametersTypeInfo, - CancellationToken cancellationToken = default) - { - Throw.IfNull(session); - Throw.IfNullOrWhiteSpace(method); - Throw.IfNull(parametersTypeInfo); - - JsonNode? parametersJson = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo); - return session.SendMessageAsync(new JsonRpcNotification { Method = method, Params = parametersJson }, cancellationToken); - } - - /// - /// Notifies the connected session of progress for a long-running operation. - /// - /// The session issuing the notification. - /// The identifying the operation for which progress is being reported. - /// The progress update to send, containing information such as percentage complete or status message. - /// The to monitor for cancellation requests. The default is . - /// A task representing the completion of the notification operation (not the operation being tracked). - /// is . - /// - /// - /// This method sends a progress notification to the connected session using the Model Context Protocol's - /// standardized progress notification format. Progress updates are identified by a - /// that allows the recipient to correlate multiple updates with a specific long-running operation. - /// - /// - /// Progress notifications are sent asynchronously and don't block the operation from continuing. - /// - /// - public static Task NotifyProgressAsync( - this McpSession session, - ProgressToken progressToken, - ProgressNotificationValue progress, - CancellationToken cancellationToken = default) - { - Throw.IfNull(session); - - return session.SendNotificationAsync( - NotificationMethods.ProgressNotification, - new ProgressNotificationParams - { - ProgressToken = progressToken, - Progress = progress, - }, - McpJsonUtilities.JsonContext.Default.ProgressNotificationParams, - cancellationToken); - } -} diff --git a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs index 1bcc25f4f..c065ed6cb 100644 --- a/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs +++ b/src/ModelContextProtocol.Core/Protocol/ClientCapabilities.cs @@ -44,7 +44,7 @@ public sealed class ClientCapabilities /// server requests for listing root URIs. Root URIs serve as entry points for resource navigation in the protocol. /// /// - /// The server can use to request the list of + /// The server can use to request the list of /// available roots from the client, which will trigger the client's . /// /// diff --git a/src/ModelContextProtocol.Core/Protocol/ITransport.cs b/src/ModelContextProtocol.Core/Protocol/ITransport.cs index 26b7993ec..148472e90 100644 --- a/src/ModelContextProtocol.Core/Protocol/ITransport.cs +++ b/src/ModelContextProtocol.Core/Protocol/ITransport.cs @@ -63,7 +63,7 @@ public interface ITransport : IAsyncDisposable /// /// This is a core method used by higher-level abstractions in the MCP protocol implementation. /// Most client code should use the higher-level methods provided by , - /// , , or , + /// , or , /// rather than accessing this method directly. /// /// diff --git a/src/ModelContextProtocol.Core/Protocol/Reference.cs b/src/ModelContextProtocol.Core/Protocol/Reference.cs index a9c87fe49..af95cf330 100644 --- a/src/ModelContextProtocol.Core/Protocol/Reference.cs +++ b/src/ModelContextProtocol.Core/Protocol/Reference.cs @@ -12,7 +12,7 @@ namespace ModelContextProtocol.Protocol; /// /// /// -/// References are commonly used with to request completion suggestions for arguments, +/// References are commonly used with to request completion suggestions for arguments, /// and with other methods that need to reference resources or prompts. /// /// diff --git a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs index 6e0f1190a..7828ce290 100644 --- a/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs +++ b/src/ModelContextProtocol.Core/Protocol/SamplingCapability.cs @@ -34,7 +34,7 @@ public sealed class SamplingCapability /// generated content. /// /// - /// You can create a handler using the extension + /// You can create a handler using the extension /// method with any implementation of . /// /// diff --git a/src/ModelContextProtocol.Core/Server/McpServer.cs b/src/ModelContextProtocol.Core/Server/McpServer.cs index de5ed6add..4af249fe3 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.cs @@ -1,5 +1,9 @@ +using Microsoft.Extensions.AI; using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; namespace ModelContextProtocol.Server; @@ -82,4 +86,331 @@ public static McpServer Create( return new McpServerImpl(transport, serverOptions, loggerFactory, serviceProvider); } + + /// + /// Requests to sample an LLM via the client using the specified request parameters. + /// + /// The parameters for the sampling request. + /// The to monitor for cancellation requests. + /// A task containing the sampling result from the client. + /// The client does not support sampling. + public ValueTask SampleAsync( + CreateMessageRequestParams request, CancellationToken cancellationToken = default) + { + ThrowIfSamplingUnsupported(); + + return SendRequestAsync( + RequestMethods.SamplingCreateMessage, + request, + McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, + McpJsonUtilities.JsonContext.Default.CreateMessageResult, + cancellationToken: cancellationToken); + } + + /// + /// Requests to sample an LLM via the client using the provided chat messages and options. + /// + /// The messages to send as part of the request. + /// The options to use for the request, including model parameters and constraints. + /// The to monitor for cancellation requests. The default is . + /// A task containing the chat response from the model. + /// is . + /// The client does not support sampling. + public async Task SampleAsync( + IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) + { + Throw.IfNull(messages); + + StringBuilder? systemPrompt = null; + + if (options?.Instructions is { } instructions) + { + (systemPrompt ??= new()).Append(instructions); + } + + List samplingMessages = []; + foreach (var message in messages) + { + if (message.Role == ChatRole.System) + { + if (systemPrompt is null) + { + systemPrompt = new(); + } + else + { + systemPrompt.AppendLine(); + } + + systemPrompt.Append(message.Text); + continue; + } + + if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant) + { + Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant; + + foreach (var content in message.Contents) + { + switch (content) + { + case TextContent textContent: + samplingMessages.Add(new() + { + Role = role, + Content = new TextContentBlock { Text = textContent.Text }, + }); + break; + + case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"): + samplingMessages.Add(new() + { + Role = role, + Content = dataContent.HasTopLevelMediaType("image") ? + new ImageContentBlock + { + MimeType = dataContent.MediaType, + Data = dataContent.Base64Data.ToString(), + } : + new AudioContentBlock + { + MimeType = dataContent.MediaType, + Data = dataContent.Base64Data.ToString(), + }, + }); + break; + } + } + } + } + + ModelPreferences? modelPreferences = null; + if (options?.ModelId is { } modelId) + { + modelPreferences = new() { Hints = [new() { Name = modelId }] }; + } + + var result = await SampleAsync(new() + { + Messages = samplingMessages, + MaxTokens = options?.MaxOutputTokens, + StopSequences = options?.StopSequences?.ToArray(), + SystemPrompt = systemPrompt?.ToString(), + Temperature = options?.Temperature, + ModelPreferences = modelPreferences, + }, cancellationToken).ConfigureAwait(false); + + AIContent? responseContent = result.Content.ToAIContent(); + + return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : [])) + { + ModelId = result.Model, + FinishReason = result.StopReason switch + { + "maxTokens" => ChatFinishReason.Length, + "endTurn" or "stopSequence" or _ => ChatFinishReason.Stop, + } + }; + } + + /// + /// Creates an wrapper that can be used to send sampling requests to the client. + /// + /// The that can be used to issue sampling requests to the client. + /// The client does not support sampling. + public IChatClient AsSamplingChatClient() + { + ThrowIfSamplingUnsupported(); + return new SamplingChatClient(this); + } + + /// Gets an on which logged messages will be sent as notifications to the client. + /// An that can be used to log to the client.. + public ILoggerProvider AsClientLoggerProvider() + { + return new ClientLoggerProvider(this); + } + + /// + /// Requests the client to list the roots it exposes. + /// + /// The parameters for the list roots request. + /// The to monitor for cancellation requests. + /// A task containing the list of roots exposed by the client. + /// The client does not support roots. + public ValueTask RequestRootsAsync( + ListRootsRequestParams request, CancellationToken cancellationToken = default) + { + ThrowIfRootsUnsupported(); + + return SendRequestAsync( + RequestMethods.RootsList, + request, + McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, + McpJsonUtilities.JsonContext.Default.ListRootsResult, + cancellationToken: cancellationToken); + } + + /// + /// Requests additional information from the user via the client, allowing the server to elicit structured data. + /// + /// The parameters for the elicitation request. + /// The to monitor for cancellation requests. + /// A task containing the elicitation result. + /// The client does not support elicitation. + public ValueTask ElicitAsync( + ElicitRequestParams request, CancellationToken cancellationToken = default) + { + ThrowIfElicitationUnsupported(); + + return SendRequestAsync( + RequestMethods.ElicitationCreate, + request, + McpJsonUtilities.JsonContext.Default.ElicitRequestParams, + McpJsonUtilities.JsonContext.Default.ElicitResult, + cancellationToken: cancellationToken); + } + + private void ThrowIfSamplingUnsupported() + { + if (ClientCapabilities?.Sampling is null) + { + if (ServerOptions.KnownClientInfo is not null) + { + throw new InvalidOperationException("Sampling is not supported in stateless mode."); + } + + throw new InvalidOperationException("Client does not support sampling."); + } + } + + private void ThrowIfRootsUnsupported() + { + if (ClientCapabilities?.Roots is null) + { + if (ServerOptions.KnownClientInfo is not null) + { + throw new InvalidOperationException("Roots are not supported in stateless mode."); + } + + throw new InvalidOperationException("Client does not support roots."); + } + } + + private void ThrowIfElicitationUnsupported() + { + if (ClientCapabilities?.Elicitation is null) + { + if (ServerOptions.KnownClientInfo is not null) + { + throw new InvalidOperationException("Elicitation is not supported in stateless mode."); + } + + throw new InvalidOperationException("Client does not support elicitation requests."); + } + } + + /// Provides an implementation that's implemented via client sampling. + private sealed class SamplingChatClient : IChatClient + { + private readonly McpServer _server; + + public SamplingChatClient(McpServer server) => _server = server; + + /// + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => + _server.SampleAsync(messages, options, cancellationToken); + + /// + async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); + foreach (var update in response.ToChatResponseUpdates()) + { + yield return update; + } + } + + /// + object? IChatClient.GetService(Type serviceType, object? serviceKey) + { + Throw.IfNull(serviceType); + + return + serviceKey is not null ? null : + serviceType.IsInstanceOfType(this) ? this : + serviceType.IsInstanceOfType(_server) ? _server : + null; + } + + /// + void IDisposable.Dispose() { } // nop + } + + /// + /// Provides an implementation for creating loggers + /// that send logging message notifications to the client for logged messages. + /// + private sealed class ClientLoggerProvider : ILoggerProvider + { + private readonly McpServer _server; + + public ClientLoggerProvider(McpServer server) => _server = server; + + /// + public ILogger CreateLogger(string categoryName) + { + Throw.IfNull(categoryName); + + return new ClientLogger(_server, categoryName); + } + + /// + void IDisposable.Dispose() { } + + private sealed class ClientLogger : ILogger + { + private readonly McpServer _server; + private readonly string _categoryName; + + public ClientLogger(McpServer server, string categoryName) + { + _server = server; + _categoryName = categoryName; + } + + /// + public IDisposable? BeginScope(TState state) where TState : notnull => + null; + + /// + public bool IsEnabled(LogLevel logLevel) => + _server?.LoggingLevel is { } loggingLevel && + McpServerImpl.ToLoggingLevel(logLevel) >= loggingLevel; + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + Throw.IfNull(formatter); + + LogInternal(logLevel, formatter(state, exception)); + + void LogInternal(LogLevel level, string message) + { + _ = _server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams + { + Level = McpServerImpl.ToLoggingLevel(level), + Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String), + Logger = _categoryName, + }); + } + } + } + } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs deleted file mode 100644 index 73240cf90..000000000 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ /dev/null @@ -1,362 +0,0 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; -using ModelContextProtocol.Protocol; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; - -namespace ModelContextProtocol.Server; - -/// -/// Provides extension methods for interacting with an instance. -/// -public static class McpServerExtensions -{ - /// - /// Requests to sample an LLM via the client using the specified request parameters. - /// - /// The server instance initiating the request. - /// The parameters for the sampling request. - /// The to monitor for cancellation requests. - /// A task containing the sampling result from the client. - /// is . - /// The client does not support sampling. - /// - /// This method requires the client to support sampling capabilities. - /// It allows detailed control over sampling parameters including messages, system prompt, temperature, - /// and token limits. - /// - public static ValueTask SampleAsync( - this McpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default) - { - Throw.IfNull(server); - ThrowIfSamplingUnsupported(server); - - return server.SendRequestAsync( - RequestMethods.SamplingCreateMessage, - request, - McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, - McpJsonUtilities.JsonContext.Default.CreateMessageResult, - cancellationToken: cancellationToken); - } - - /// - /// Requests to sample an LLM via the client using the provided chat messages and options. - /// - /// The server initiating the request. - /// The messages to send as part of the request. - /// The options to use for the request, including model parameters and constraints. - /// The to monitor for cancellation requests. The default is . - /// A task containing the chat response from the model. - /// is . - /// is . - /// The client does not support sampling. - /// - /// This method converts the provided chat messages into a format suitable for the sampling API, - /// handling different content types such as text, images, and audio. - /// - public static async Task SampleAsync( - this McpServer server, - IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) - { - Throw.IfNull(server); - Throw.IfNull(messages); - - StringBuilder? systemPrompt = null; - - if (options?.Instructions is { } instructions) - { - (systemPrompt ??= new()).Append(instructions); - } - - List samplingMessages = []; - foreach (var message in messages) - { - if (message.Role == ChatRole.System) - { - if (systemPrompt is null) - { - systemPrompt = new(); - } - else - { - systemPrompt.AppendLine(); - } - - systemPrompt.Append(message.Text); - continue; - } - - if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant) - { - Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant; - - foreach (var content in message.Contents) - { - switch (content) - { - case TextContent textContent: - samplingMessages.Add(new() - { - Role = role, - Content = new TextContentBlock { Text = textContent.Text }, - }); - break; - - case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"): - samplingMessages.Add(new() - { - Role = role, - Content = dataContent.HasTopLevelMediaType("image") ? - new ImageContentBlock - { - MimeType = dataContent.MediaType, - Data = dataContent.Base64Data.ToString(), - } : - new AudioContentBlock - { - MimeType = dataContent.MediaType, - Data = dataContent.Base64Data.ToString(), - }, - }); - break; - } - } - } - } - - ModelPreferences? modelPreferences = null; - if (options?.ModelId is { } modelId) - { - modelPreferences = new() { Hints = [new() { Name = modelId }] }; - } - - var result = await server.SampleAsync(new() - { - Messages = samplingMessages, - MaxTokens = options?.MaxOutputTokens, - StopSequences = options?.StopSequences?.ToArray(), - SystemPrompt = systemPrompt?.ToString(), - Temperature = options?.Temperature, - ModelPreferences = modelPreferences, - }, cancellationToken).ConfigureAwait(false); - - AIContent? responseContent = result.Content.ToAIContent(); - - return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : [])) - { - ModelId = result.Model, - FinishReason = result.StopReason switch - { - "maxTokens" => ChatFinishReason.Length, - "endTurn" or "stopSequence" or _ => ChatFinishReason.Stop, - } - }; - } - - /// - /// Creates an wrapper that can be used to send sampling requests to the client. - /// - /// The server to be wrapped as an . - /// The that can be used to issue sampling requests to the client. - /// is . - /// The client does not support sampling. - public static IChatClient AsSamplingChatClient(this McpServer server) - { - Throw.IfNull(server); - ThrowIfSamplingUnsupported(server); - - return new SamplingChatClient(server); - } - - /// Gets an on which logged messages will be sent as notifications to the client. - /// The server to wrap as an . - /// An that can be used to log to the client.. - public static ILoggerProvider AsClientLoggerProvider(this McpServer server) - { - Throw.IfNull(server); - - return new ClientLoggerProvider(server); - } - - /// - /// Requests the client to list the roots it exposes. - /// - /// The server initiating the request. - /// The parameters for the list roots request. - /// The to monitor for cancellation requests. - /// A task containing the list of roots exposed by the client. - /// is . - /// The client does not support roots. - /// - /// This method requires the client to support the roots capability. - /// Root resources allow clients to expose a hierarchical structure of resources that can be - /// navigated and accessed by the server. These resources might include file systems, databases, - /// or other structured data sources that the client makes available through the protocol. - /// - public static ValueTask RequestRootsAsync( - this McpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default) - { - Throw.IfNull(server); - ThrowIfRootsUnsupported(server); - - return server.SendRequestAsync( - RequestMethods.RootsList, - request, - McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, - McpJsonUtilities.JsonContext.Default.ListRootsResult, - cancellationToken: cancellationToken); - } - - /// - /// Requests additional information from the user via the client, allowing the server to elicit structured data. - /// - /// The server initiating the request. - /// The parameters for the elicitation request. - /// The to monitor for cancellation requests. - /// A task containing the elicitation result. - /// is . - /// The client does not support elicitation. - /// - /// This method requires the client to support the elicitation capability. - /// - public static ValueTask ElicitAsync( - this McpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default) - { - Throw.IfNull(server); - ThrowIfElicitationUnsupported(server); - - return server.SendRequestAsync( - RequestMethods.ElicitationCreate, - request, - McpJsonUtilities.JsonContext.Default.ElicitRequestParams, - McpJsonUtilities.JsonContext.Default.ElicitResult, - cancellationToken: cancellationToken); - } - - private static void ThrowIfSamplingUnsupported(McpServer server) - { - if (server.ClientCapabilities?.Sampling is null) - { - if (server.ServerOptions.KnownClientInfo is not null) - { - throw new InvalidOperationException("Sampling is not supported in stateless mode."); - } - - throw new InvalidOperationException("Client does not support sampling."); - } - } - - private static void ThrowIfRootsUnsupported(McpServer server) - { - if (server.ClientCapabilities?.Roots is null) - { - if (server.ServerOptions.KnownClientInfo is not null) - { - throw new InvalidOperationException("Roots are not supported in stateless mode."); - } - - throw new InvalidOperationException("Client does not support roots."); - } - } - - private static void ThrowIfElicitationUnsupported(McpServer server) - { - if (server.ClientCapabilities?.Elicitation is null) - { - if (server.ServerOptions.KnownClientInfo is not null) - { - throw new InvalidOperationException("Elicitation is not supported in stateless mode."); - } - - throw new InvalidOperationException("Client does not support elicitation requests."); - } - } - - /// Provides an implementation that's implemented via client sampling. - private sealed class SamplingChatClient(McpServer server) : IChatClient - { - /// - public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => - server.SampleAsync(messages, options, cancellationToken); - - /// - async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); - foreach (var update in response.ToChatResponseUpdates()) - { - yield return update; - } - } - - /// - object? IChatClient.GetService(Type serviceType, object? serviceKey) - { - Throw.IfNull(serviceType); - - return - serviceKey is not null ? null : - serviceType.IsInstanceOfType(this) ? this : - serviceType.IsInstanceOfType(server) ? server : - null; - } - - /// - void IDisposable.Dispose() { } // nop - } - - /// - /// Provides an implementation for creating loggers - /// that send logging message notifications to the client for logged messages. - /// - private sealed class ClientLoggerProvider(McpServer server) : ILoggerProvider - { - /// - public ILogger CreateLogger(string categoryName) - { - Throw.IfNull(categoryName); - - return new ClientLogger(server, categoryName); - } - - /// - void IDisposable.Dispose() { } - - private sealed class ClientLogger(McpServer server, string categoryName) : ILogger - { - /// - public IDisposable? BeginScope(TState state) where TState : notnull => - null; - - /// - public bool IsEnabled(LogLevel logLevel) => - server?.LoggingLevel is { } loggingLevel && - McpServerImpl.ToLoggingLevel(logLevel) >= loggingLevel; - - /// - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - if (!IsEnabled(logLevel)) - { - return; - } - - Throw.IfNull(formatter); - - Log(logLevel, formatter(state, exception)); - - void Log(LogLevel logLevel, string message) - { - _ = server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams - { - Level = McpServerImpl.ToLoggingLevel(logLevel), - Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String), - Logger = categoryName, - }); - } - } - } - } -} diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs new file mode 100644 index 000000000..15127502e --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Client/McpClientCreationTests.cs @@ -0,0 +1,155 @@ +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using System.IO.Pipelines; +using System.Text.Json; +using System.Threading.Channels; + +namespace ModelContextProtocol.Tests.Client; + +public class McpClientCreationTests +{ + [Fact] + public async Task CreateAsync_WithInvalidArgs_Throws() + { + await Assert.ThrowsAsync("clientTransport", () => McpClient.CreateAsync(null!, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task CreateAsync_NopTransport_ReturnsClient() + { + // Act + await using var client = await McpClient.CreateAsync( + new NopTransport(), + cancellationToken: TestContext.Current.CancellationToken); + + Assert.NotNull(client); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task Cancellation_ThrowsCancellationException(bool preCanceled) + { + var cts = new CancellationTokenSource(); + + if (preCanceled) + { + cts.Cancel(); + } + + Task t = McpClient.CreateAsync( + new StreamClientTransport(new Pipe().Writer.AsStream(), new Pipe().Reader.AsStream()), + cancellationToken: cts.Token); + if (!preCanceled) + { + Assert.False(t.IsCompleted); + } + + if (!preCanceled) + { + cts.Cancel(); + } + + await Assert.ThrowsAnyAsync(() => t); + } + + [Theory] + [InlineData(typeof(NopTransport))] + [InlineData(typeof(FailureTransport))] + public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) + { + // Arrange + var clientOptions = new McpClientOptions + { + Capabilities = new ClientCapabilities + { + Sampling = new SamplingCapability + { + SamplingHandler = async (c, p, t) => + new CreateMessageResult + { + Content = new TextContentBlock { Text = "result" }, + Model = "test-model", + Role = Role.User, + StopReason = "endTurn" + }, + }, + Roots = new RootsCapability + { + ListChanged = true, + RootsHandler = async (t, r) => new ListRootsResult { Roots = [] }, + } + } + }; + + var clientTransport = (IClientTransport)Activator.CreateInstance(transportType)!; + McpClient? client = null; + + var actionTask = McpClient.CreateAsync(clientTransport, clientOptions, loggerFactory: null, CancellationToken.None); + + // Act + if (clientTransport is FailureTransport) + { + var exception = await Assert.ThrowsAsync(async() => await actionTask); + Assert.Equal(FailureTransport.ExpectedMessage, exception.Message); + } + else + { + client = await actionTask; + + // Assert + Assert.NotNull(client); + } + } + + private class NopTransport : ITransport, IClientTransport + { + private readonly Channel _channel = Channel.CreateUnbounded(); + + public bool IsConnected => true; + public string? SessionId => null; + + public ChannelReader MessageReader => _channel.Reader; + + public Task ConnectAsync(CancellationToken cancellationToken = default) => Task.FromResult(this); + + public ValueTask DisposeAsync() => default; + + public string Name => "Test Nop Transport"; + + public virtual Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + { + switch (message) + { + case JsonRpcRequest: + _channel.Writer.TryWrite(new JsonRpcResponse + { + Id = ((JsonRpcRequest)message).Id, + Result = JsonSerializer.SerializeToNode(new InitializeResult + { + Capabilities = new ServerCapabilities(), + ProtocolVersion = "2024-11-05", + ServerInfo = new Implementation + { + Name = "NopTransport", + Version = "1.0.0" + }, + }, McpJsonUtilities.DefaultOptions), + }); + break; + } + + return Task.CompletedTask; + } + } + + private sealed class FailureTransport : NopTransport + { + public const string ExpectedMessage = "Something failed"; + + public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + { + throw new InvalidOperationException(ExpectedMessage); + } + } +} diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs deleted file mode 100644 index 46b98118b..000000000 --- a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs +++ /dev/null @@ -1,471 +0,0 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using ModelContextProtocol.Client; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; -using Moq; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; -using System.Threading.Channels; - -namespace ModelContextProtocol.Tests.Client; - -public class McpClientExtensionsTests : ClientServerTestBase -{ - public McpClientExtensionsTests(ITestOutputHelper outputHelper) - : base(outputHelper) - { - } - - protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) - { - for (int f = 0; f < 10; f++) - { - string name = $"Method{f}"; - mcpServerBuilder.WithTools([McpServerTool.Create((int i) => $"{name} Result {i}", new() { Name = name })]); - } - mcpServerBuilder.WithTools([McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaAttr" })]); - mcpServerBuilder.WithTools([McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true })]); - } - - [Theory] - [InlineData(null, null)] - [InlineData(0.7f, 50)] - [InlineData(1.0f, 100)] - public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperature, int? maxTokens) - { - // Arrange - var mockChatClient = new Mock(); - var requestParams = new CreateMessageRequestParams - { - Messages = - [ - new SamplingMessage - { - Role = Role.User, - Content = new TextContentBlock { Text = "Hello" } - } - ], - Temperature = temperature, - MaxTokens = maxTokens, - }; - - var cancellationToken = CancellationToken.None; - var expectedResponse = new[] { - new ChatResponseUpdate - { - ModelId = "test-model", - FinishReason = ChatFinishReason.Stop, - Role = ChatRole.Assistant, - Contents = - [ - new TextContent("Hello, World!") { RawRepresentation = "Hello, World!" } - ] - } - }.ToAsyncEnumerable(); - - mockChatClient - .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) - .Returns(expectedResponse); - - var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); - - // Act - var result = await handler(requestParams, Mock.Of>(), cancellationToken); - - // Assert - Assert.NotNull(result); - Assert.Equal("Hello, World!", (result.Content as TextContentBlock)?.Text); - Assert.Equal("test-model", result.Model); - Assert.Equal(Role.Assistant, result.Role); - Assert.Equal("endTurn", result.StopReason); - } - - [Fact] - public async Task CreateSamplingHandler_ShouldHandleImageMessages() - { - // Arrange - var mockChatClient = new Mock(); - var requestParams = new CreateMessageRequestParams - { - Messages = - [ - new SamplingMessage - { - Role = Role.User, - Content = new ImageContentBlock - { - MimeType = "image/png", - Data = Convert.ToBase64String(new byte[] { 1, 2, 3 }) - } - } - ], - MaxTokens = 100 - }; - - const string expectedData = "SGVsbG8sIFdvcmxkIQ=="; - var cancellationToken = CancellationToken.None; - var expectedResponse = new[] { - new ChatResponseUpdate - { - ModelId = "test-model", - FinishReason = ChatFinishReason.Stop, - Role = ChatRole.Assistant, - Contents = - [ - new DataContent($"data:image/png;base64,{expectedData}") { RawRepresentation = "Hello, World!" } - ] - } - }.ToAsyncEnumerable(); - - mockChatClient - .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) - .Returns(expectedResponse); - - var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); - - // Act - var result = await handler(requestParams, Mock.Of>(), cancellationToken); - - // Assert - Assert.NotNull(result); - Assert.Equal(expectedData, (result.Content as ImageContentBlock)?.Data); - Assert.Equal("test-model", result.Model); - Assert.Equal(Role.Assistant, result.Role); - Assert.Equal("endTurn", result.StopReason); - } - - [Fact] - public async Task CreateSamplingHandler_ShouldHandleResourceMessages() - { - // Arrange - const string data = "SGVsbG8sIFdvcmxkIQ=="; - string content = $"data:application/octet-stream;base64,{data}"; - var mockChatClient = new Mock(); - var resource = new BlobResourceContents - { - Blob = data, - MimeType = "application/octet-stream", - Uri = "data:application/octet-stream" - }; - - var requestParams = new CreateMessageRequestParams - { - Messages = - [ - new SamplingMessage - { - Role = Role.User, - Content = new EmbeddedResourceBlock { Resource = resource }, - } - ], - MaxTokens = 100 - }; - - var cancellationToken = CancellationToken.None; - var expectedResponse = new[] { - new ChatResponseUpdate - { - ModelId = "test-model", - FinishReason = ChatFinishReason.Stop, - AuthorName = "bot", - Role = ChatRole.Assistant, - Contents = - [ - resource.ToAIContent() - ] - } - }.ToAsyncEnumerable(); - - mockChatClient - .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) - .Returns(expectedResponse); - - var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); - - // Act - var result = await handler(requestParams, Mock.Of>(), cancellationToken); - - // Assert - Assert.NotNull(result); - Assert.Equal("test-model", result.Model); - Assert.Equal(Role.Assistant, result.Role); - Assert.Equal("endTurn", result.StopReason); - } - - [Fact] - public async Task ListToolsAsync_AllToolsReturned() - { - await using McpClient client = await CreateMcpClientForServer(); - - var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); - Assert.Equal(12, tools.Count); - var echo = tools.Single(t => t.Name == "Method4"); - var result = await echo.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken); - Assert.Contains("Method4 Result 42", result?.ToString()); - - var valuesSetViaAttr = tools.Single(t => t.Name == "ValuesSetViaAttr"); - Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.Title); - Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.ReadOnlyHint); - Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.IdempotentHint); - Assert.False(valuesSetViaAttr.ProtocolTool.Annotations?.DestructiveHint); - Assert.True(valuesSetViaAttr.ProtocolTool.Annotations?.OpenWorldHint); - - var valuesSetViaOptions = tools.Single(t => t.Name == "ValuesSetViaOptions"); - Assert.Null(valuesSetViaOptions.ProtocolTool.Annotations?.Title); - Assert.True(valuesSetViaOptions.ProtocolTool.Annotations?.ReadOnlyHint); - Assert.Null(valuesSetViaOptions.ProtocolTool.Annotations?.IdempotentHint); - Assert.True(valuesSetViaOptions.ProtocolTool.Annotations?.DestructiveHint); - Assert.False(valuesSetViaOptions.ProtocolTool.Annotations?.OpenWorldHint); - } - - [Fact] - public async Task EnumerateToolsAsync_AllToolsReturned() - { - await using McpClient client = await CreateMcpClientForServer(); - - await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken)) - { - if (tool.Name == "Method4") - { - var result = await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken); - Assert.Contains("Method4 Result 42", result?.ToString()); - return; - } - } - - Assert.Fail("Couldn't find target method"); - } - - [Fact] - public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions() - { - JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using McpClient client = await CreateMcpClientForServer(); - bool hasTools = false; - - await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken)) - { - Assert.Same(options, tool.JsonSerializerOptions); - hasTools = true; - } - - foreach (var tool in await client.ListToolsAsync(options, TestContext.Current.CancellationToken)) - { - Assert.Same(options, tool.JsonSerializerOptions); - } - - Assert.True(hasTools); - } - - [Fact] - public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions() - { - JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using McpClient client = await CreateMcpClientForServer(); - - var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First(); - await Assert.ThrowsAsync(async () => await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken)); - } - - [Fact] - public async Task SendRequestAsync_HonorsJsonSerializerOptions() - { - JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using McpClient client = await CreateMcpClientForServer(); - - await Assert.ThrowsAsync(async () => await client.SendRequestAsync("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); - } - - [Fact] - public async Task SendNotificationAsync_HonorsJsonSerializerOptions() - { - JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using McpClient client = await CreateMcpClientForServer(); - - await Assert.ThrowsAsync(() => client.SendNotificationAsync("Method4", new { Value = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); - } - - [Fact] - public async Task GetPromptsAsync_HonorsJsonSerializerOptions() - { - JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; - await using McpClient client = await CreateMcpClientForServer(); - - await Assert.ThrowsAsync(async () => await client.GetPromptAsync("Prompt", new Dictionary { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); - } - - [Fact] - public async Task WithName_ChangesToolName() - { - JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using McpClient client = await CreateMcpClientForServer(); - - var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).First(); - var originalName = tool.Name; - var renamedTool = tool.WithName("RenamedTool"); - - Assert.NotNull(renamedTool); - Assert.Equal("RenamedTool", renamedTool.Name); - Assert.Equal(originalName, tool?.Name); - } - - [Fact] - public async Task WithDescription_ChangesToolDescription() - { - JsonSerializerOptions options = new(JsonSerializerOptions.Default); - await using McpClient client = await CreateMcpClientForServer(); - var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).FirstOrDefault(); - var originalDescription = tool?.Description; - var redescribedTool = tool?.WithDescription("ToolWithNewDescription"); - Assert.NotNull(redescribedTool); - Assert.Equal("ToolWithNewDescription", redescribedTool.Description); - Assert.Equal(originalDescription, tool?.Description); - } - - [Fact] - public async Task WithProgress_ProgressReported() - { - const int TotalNotifications = 3; - int remainingProgress = TotalNotifications; - TaskCompletionSource allProgressReceived = new(TaskCreationOptions.RunContinuationsAsynchronously); - - Server.ServerOptions.Capabilities?.Tools?.ToolCollection?.Add(McpServerTool.Create(async (IProgress progress) => - { - for (int i = 0; i < TotalNotifications; i++) - { - progress.Report(new ProgressNotificationValue { Progress = i * 10, Message = "making progress" }); - await Task.Delay(1); - } - - await allProgressReceived.Task; - - return 42; - }, new() { Name = "ProgressReporter" })); - - await using McpClient client = await CreateMcpClientForServer(); - - var tool = (await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken)).First(t => t.Name == "ProgressReporter"); - - IProgress progress = new SynchronousProgress(value => - { - Assert.True(value.Progress >= 0 && value.Progress <= 100); - Assert.Equal("making progress", value.Message); - if (Interlocked.Decrement(ref remainingProgress) == 0) - { - allProgressReceived.SetResult(true); - } - }); - - Assert.Throws("progress", () => tool.WithProgress(null!)); - - var result = await tool.WithProgress(progress).InvokeAsync(cancellationToken: TestContext.Current.CancellationToken); - Assert.Contains("42", result?.ToString()); - } - - private sealed class SynchronousProgress(Action callback) : IProgress - { - public void Report(ProgressNotificationValue value) => callback(value); - } - - [Fact] - public async Task AsClientLoggerProvider_MessagesSentToClient() - { - await using McpClient client = await CreateMcpClientForServer(); - - ILoggerProvider loggerProvider = Server.AsClientLoggerProvider(); - Assert.Throws("categoryName", () => loggerProvider.CreateLogger(null!)); - - ILogger logger = loggerProvider.CreateLogger("TestLogger"); - Assert.NotNull(logger); - - Assert.Null(logger.BeginScope("")); - - Assert.Null(Server.LoggingLevel); - Assert.False(logger.IsEnabled(LogLevel.Trace)); - Assert.False(logger.IsEnabled(LogLevel.Debug)); - Assert.False(logger.IsEnabled(LogLevel.Information)); - Assert.False(logger.IsEnabled(LogLevel.Warning)); - Assert.False(logger.IsEnabled(LogLevel.Error)); - Assert.False(logger.IsEnabled(LogLevel.Critical)); - - await client.SetLoggingLevel(LoggingLevel.Info, TestContext.Current.CancellationToken); - - DateTime start = DateTime.UtcNow; - while (Server.LoggingLevel is null) - { - await Task.Delay(1, TestContext.Current.CancellationToken); - Assert.True(DateTime.UtcNow - start < TimeSpan.FromSeconds(10), "Timed out waiting for logging level to be set"); - } - - Assert.Equal(LoggingLevel.Info, Server.LoggingLevel); - Assert.False(logger.IsEnabled(LogLevel.Trace)); - Assert.False(logger.IsEnabled(LogLevel.Debug)); - Assert.True(logger.IsEnabled(LogLevel.Information)); - Assert.True(logger.IsEnabled(LogLevel.Warning)); - Assert.True(logger.IsEnabled(LogLevel.Error)); - Assert.True(logger.IsEnabled(LogLevel.Critical)); - - List data = []; - var channel = Channel.CreateUnbounded(); - - await using (client.RegisterNotificationHandler(NotificationMethods.LoggingMessageNotification, - (notification, cancellationToken) => - { - Assert.True(channel.Writer.TryWrite(JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions))); - return default; - })) - { - logger.LogTrace("Trace {Message}", "message"); - logger.LogDebug("Debug {Message}", "message"); - logger.LogInformation("Information {Message}", "message"); - logger.LogWarning("Warning {Message}", "message"); - logger.LogError("Error {Message}", "message"); - logger.LogCritical("Critical {Message}", "message"); - - for (int i = 0; i < 4; i++) - { - var m = await channel.Reader.ReadAsync(TestContext.Current.CancellationToken); - Assert.NotNull(m); - Assert.NotNull(m.Data); - - Assert.Equal("TestLogger", m.Logger); - - string ? s = JsonSerializer.Deserialize(m.Data.Value, McpJsonUtilities.DefaultOptions); - Assert.NotNull(s); - - if (s.Contains("Information")) - { - Assert.Equal(LoggingLevel.Info, m.Level); - } - else if (s.Contains("Warning")) - { - Assert.Equal(LoggingLevel.Warning, m.Level); - } - else if (s.Contains("Error")) - { - Assert.Equal(LoggingLevel.Error, m.Level); - } - else if (s.Contains("Critical")) - { - Assert.Equal(LoggingLevel.Critical, m.Level); - } - - data.Add(s); - } - - channel.Writer.Complete(); - } - - Assert.False(await channel.Reader.WaitToReadAsync(TestContext.Current.CancellationToken)); - Assert.Equal( - [ - "Critical message", - "Error message", - "Information message", - "Warning message", - ], - data.OrderBy(s => s)); - } -} \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs index 2d230d243..779e31e62 100644 --- a/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs +++ b/tests/ModelContextProtocol.Tests/Client/McpClientTests.cs @@ -1,157 +1,471 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; using Moq; -using System.IO.Pipelines; using System.Text.Json; +using System.Text.Json.Serialization.Metadata; using System.Threading.Channels; namespace ModelContextProtocol.Tests.Client; -public class McpClientTests +public class McpClientTests : ClientServerTestBase { - [Fact] - public async Task CreateAsync_WithInvalidArgs_Throws() + public McpClientTests(ITestOutputHelper outputHelper) + : base(outputHelper) { - await Assert.ThrowsAsync("clientTransport", () => McpClient.CreateAsync(null!, cancellationToken: TestContext.Current.CancellationToken)); } - [Fact] - public async Task CreateAsync_NopTransport_ReturnsClient() + protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder) { - // Act - await using var client = await McpClient.CreateAsync( - new NopTransport(), - cancellationToken: TestContext.Current.CancellationToken); - - Assert.NotNull(client); + for (int f = 0; f < 10; f++) + { + string name = $"Method{f}"; + mcpServerBuilder.WithTools([McpServerTool.Create((int i) => $"{name} Result {i}", new() { Name = name })]); + } + mcpServerBuilder.WithTools([McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaAttr" })]); + mcpServerBuilder.WithTools([McpServerTool.Create([McpServerTool(Destructive = false, OpenWorld = true)] (string i) => $"{i} Result", new() { Name = "ValuesSetViaOptions", Destructive = true, OpenWorld = false, ReadOnly = true })]); } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task Cancellation_ThrowsCancellationException(bool preCanceled) + [InlineData(null, null)] + [InlineData(0.7f, 50)] + [InlineData(1.0f, 100)] + public async Task CreateSamplingHandler_ShouldHandleTextMessages(float? temperature, int? maxTokens) { - var cts = new CancellationTokenSource(); - - if (preCanceled) + // Arrange + var mockChatClient = new Mock(); + var requestParams = new CreateMessageRequestParams { - cts.Cancel(); - } + Messages = + [ + new SamplingMessage + { + Role = Role.User, + Content = new TextContentBlock { Text = "Hello" } + } + ], + Temperature = temperature, + MaxTokens = maxTokens, + }; - Task t = McpClient.CreateAsync( - new StreamClientTransport(new Pipe().Writer.AsStream(), new Pipe().Reader.AsStream()), - cancellationToken: cts.Token); - if (!preCanceled) - { - Assert.False(t.IsCompleted); - } + var cancellationToken = CancellationToken.None; + var expectedResponse = new[] { + new ChatResponseUpdate + { + ModelId = "test-model", + FinishReason = ChatFinishReason.Stop, + Role = ChatRole.Assistant, + Contents = + [ + new TextContent("Hello, World!") { RawRepresentation = "Hello, World!" } + ] + } + }.ToAsyncEnumerable(); - if (!preCanceled) - { - cts.Cancel(); - } + mockChatClient + .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) + .Returns(expectedResponse); - await Assert.ThrowsAnyAsync(() => t); + var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); + + // Act + var result = await handler(requestParams, Mock.Of>(), cancellationToken); + + // Assert + Assert.NotNull(result); + Assert.Equal("Hello, World!", (result.Content as TextContentBlock)?.Text); + Assert.Equal("test-model", result.Model); + Assert.Equal(Role.Assistant, result.Role); + Assert.Equal("endTurn", result.StopReason); } - [Theory] - [InlineData(typeof(NopTransport))] - [InlineData(typeof(FailureTransport))] - public async Task CreateAsync_WithCapabilitiesOptions(Type transportType) + [Fact] + public async Task CreateSamplingHandler_ShouldHandleImageMessages() { // Arrange - var clientOptions = new McpClientOptions + var mockChatClient = new Mock(); + var requestParams = new CreateMessageRequestParams { - Capabilities = new ClientCapabilities - { - Sampling = new SamplingCapability - { - SamplingHandler = async (c, p, t) => - new CreateMessageResult - { - Content = new TextContentBlock { Text = "result" }, - Model = "test-model", - Role = Role.User, - StopReason = "endTurn" - }, - }, - Roots = new RootsCapability + Messages = + [ + new SamplingMessage { - ListChanged = true, - RootsHandler = async (t, r) => new ListRootsResult { Roots = [] }, + Role = Role.User, + Content = new ImageContentBlock + { + MimeType = "image/png", + Data = Convert.ToBase64String(new byte[] { 1, 2, 3 }) + } } + ], + MaxTokens = 100 + }; + + const string expectedData = "SGVsbG8sIFdvcmxkIQ=="; + var cancellationToken = CancellationToken.None; + var expectedResponse = new[] { + new ChatResponseUpdate + { + ModelId = "test-model", + FinishReason = ChatFinishReason.Stop, + Role = ChatRole.Assistant, + Contents = + [ + new DataContent($"data:image/png;base64,{expectedData}") { RawRepresentation = "Hello, World!" } + ] } + }.ToAsyncEnumerable(); + + mockChatClient + .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) + .Returns(expectedResponse); + + var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); + + // Act + var result = await handler(requestParams, Mock.Of>(), cancellationToken); + + // Assert + Assert.NotNull(result); + Assert.Equal(expectedData, (result.Content as ImageContentBlock)?.Data); + Assert.Equal("test-model", result.Model); + Assert.Equal(Role.Assistant, result.Role); + Assert.Equal("endTurn", result.StopReason); + } + + [Fact] + public async Task CreateSamplingHandler_ShouldHandleResourceMessages() + { + // Arrange + const string data = "SGVsbG8sIFdvcmxkIQ=="; + string content = $"data:application/octet-stream;base64,{data}"; + var mockChatClient = new Mock(); + var resource = new BlobResourceContents + { + Blob = data, + MimeType = "application/octet-stream", + Uri = "data:application/octet-stream" + }; + + var requestParams = new CreateMessageRequestParams + { + Messages = + [ + new SamplingMessage + { + Role = Role.User, + Content = new EmbeddedResourceBlock { Resource = resource }, + } + ], + MaxTokens = 100 }; - var clientTransport = (IClientTransport)Activator.CreateInstance(transportType)!; - McpClient? client = null; + var cancellationToken = CancellationToken.None; + var expectedResponse = new[] { + new ChatResponseUpdate + { + ModelId = "test-model", + FinishReason = ChatFinishReason.Stop, + AuthorName = "bot", + Role = ChatRole.Assistant, + Contents = + [ + resource.ToAIContent() + ] + } + }.ToAsyncEnumerable(); + + mockChatClient + .Setup(client => client.GetStreamingResponseAsync(It.IsAny>(), It.IsAny(), cancellationToken)) + .Returns(expectedResponse); - var actionTask = McpClient.CreateAsync(clientTransport, clientOptions, loggerFactory: null, CancellationToken.None); + var handler = McpClientExtensions.CreateSamplingHandler(mockChatClient.Object); // Act - if (clientTransport is FailureTransport) + var result = await handler(requestParams, Mock.Of>(), cancellationToken); + + // Assert + Assert.NotNull(result); + Assert.Equal("test-model", result.Model); + Assert.Equal(Role.Assistant, result.Role); + Assert.Equal("endTurn", result.StopReason); + } + + [Fact] + public async Task ListToolsAsync_AllToolsReturned() + { + await using McpClient client = await CreateMcpClientForServer(); + + var tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(12, tools.Count); + var echo = tools.Single(t => t.Name == "Method4"); + var result = await echo.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken); + Assert.Contains("Method4 Result 42", result?.ToString()); + + var valuesSetViaAttr = tools.Single(t => t.Name == "ValuesSetViaAttr"); + Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.Title); + Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.ReadOnlyHint); + Assert.Null(valuesSetViaAttr.ProtocolTool.Annotations?.IdempotentHint); + Assert.False(valuesSetViaAttr.ProtocolTool.Annotations?.DestructiveHint); + Assert.True(valuesSetViaAttr.ProtocolTool.Annotations?.OpenWorldHint); + + var valuesSetViaOptions = tools.Single(t => t.Name == "ValuesSetViaOptions"); + Assert.Null(valuesSetViaOptions.ProtocolTool.Annotations?.Title); + Assert.True(valuesSetViaOptions.ProtocolTool.Annotations?.ReadOnlyHint); + Assert.Null(valuesSetViaOptions.ProtocolTool.Annotations?.IdempotentHint); + Assert.True(valuesSetViaOptions.ProtocolTool.Annotations?.DestructiveHint); + Assert.False(valuesSetViaOptions.ProtocolTool.Annotations?.OpenWorldHint); + } + + [Fact] + public async Task EnumerateToolsAsync_AllToolsReturned() + { + await using McpClient client = await CreateMcpClientForServer(); + + await foreach (var tool in client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken)) + { + if (tool.Name == "Method4") + { + var result = await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken); + Assert.Contains("Method4 Result 42", result?.ToString()); + return; + } + } + + Assert.Fail("Couldn't find target method"); + } + + [Fact] + public async Task EnumerateToolsAsync_FlowsJsonSerializerOptions() + { + JsonSerializerOptions options = new(JsonSerializerOptions.Default); + await using McpClient client = await CreateMcpClientForServer(); + bool hasTools = false; + + await foreach (var tool in client.EnumerateToolsAsync(options, TestContext.Current.CancellationToken)) { - var exception = await Assert.ThrowsAsync(async() => await actionTask); - Assert.Equal(FailureTransport.ExpectedMessage, exception.Message); + Assert.Same(options, tool.JsonSerializerOptions); + hasTools = true; } - else + + foreach (var tool in await client.ListToolsAsync(options, TestContext.Current.CancellationToken)) { - client = await actionTask; + Assert.Same(options, tool.JsonSerializerOptions); + } + + Assert.True(hasTools); + } + + [Fact] + public async Task EnumerateToolsAsync_HonorsJsonSerializerOptions() + { + JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; + await using McpClient client = await CreateMcpClientForServer(); + + var tool = (await client.ListToolsAsync(emptyOptions, TestContext.Current.CancellationToken)).First(); + await Assert.ThrowsAsync(async () => await tool.InvokeAsync(new() { ["i"] = 42 }, TestContext.Current.CancellationToken)); + } - // Assert - Assert.NotNull(client); - } + [Fact] + public async Task SendRequestAsync_HonorsJsonSerializerOptions() + { + JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; + await using McpClient client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync(async () => await client.SendRequestAsync("Method4", new() { Name = "tool" }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); } - private class NopTransport : ITransport, IClientTransport + [Fact] + public async Task SendNotificationAsync_HonorsJsonSerializerOptions() + { + JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; + await using McpClient client = await CreateMcpClientForServer(); + + await Assert.ThrowsAsync(() => client.SendNotificationAsync("Method4", new { Value = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task GetPromptsAsync_HonorsJsonSerializerOptions() { - private readonly Channel _channel = Channel.CreateUnbounded(); + JsonSerializerOptions emptyOptions = new() { TypeInfoResolver = JsonTypeInfoResolver.Combine() }; + await using McpClient client = await CreateMcpClientForServer(); - public bool IsConnected => true; - public string? SessionId => null; + await Assert.ThrowsAsync(async () => await client.GetPromptAsync("Prompt", new Dictionary { ["i"] = 42 }, emptyOptions, cancellationToken: TestContext.Current.CancellationToken)); + } - public ChannelReader MessageReader => _channel.Reader; + [Fact] + public async Task WithName_ChangesToolName() + { + JsonSerializerOptions options = new(JsonSerializerOptions.Default); + await using McpClient client = await CreateMcpClientForServer(); - public Task ConnectAsync(CancellationToken cancellationToken = default) => Task.FromResult(this); + var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).First(); + var originalName = tool.Name; + var renamedTool = tool.WithName("RenamedTool"); - public ValueTask DisposeAsync() => default; + Assert.NotNull(renamedTool); + Assert.Equal("RenamedTool", renamedTool.Name); + Assert.Equal(originalName, tool?.Name); + } - public string Name => "Test Nop Transport"; + [Fact] + public async Task WithDescription_ChangesToolDescription() + { + JsonSerializerOptions options = new(JsonSerializerOptions.Default); + await using McpClient client = await CreateMcpClientForServer(); + var tool = (await client.ListToolsAsync(options, TestContext.Current.CancellationToken)).FirstOrDefault(); + var originalDescription = tool?.Description; + var redescribedTool = tool?.WithDescription("ToolWithNewDescription"); + Assert.NotNull(redescribedTool); + Assert.Equal("ToolWithNewDescription", redescribedTool.Description); + Assert.Equal(originalDescription, tool?.Description); + } + + [Fact] + public async Task WithProgress_ProgressReported() + { + const int TotalNotifications = 3; + int remainingProgress = TotalNotifications; + TaskCompletionSource allProgressReceived = new(TaskCreationOptions.RunContinuationsAsynchronously); - public virtual Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + Server.ServerOptions.Capabilities?.Tools?.ToolCollection?.Add(McpServerTool.Create(async (IProgress progress) => { - switch (message) + for (int i = 0; i < TotalNotifications; i++) { - case JsonRpcRequest: - _channel.Writer.TryWrite(new JsonRpcResponse - { - Id = ((JsonRpcRequest)message).Id, - Result = JsonSerializer.SerializeToNode(new InitializeResult - { - Capabilities = new ServerCapabilities(), - ProtocolVersion = "2024-11-05", - ServerInfo = new Implementation - { - Name = "NopTransport", - Version = "1.0.0" - }, - }, McpJsonUtilities.DefaultOptions), - }); - break; + progress.Report(new ProgressNotificationValue { Progress = i * 10, Message = "making progress" }); + await Task.Delay(1); } - return Task.CompletedTask; - } + await allProgressReceived.Task; + + return 42; + }, new() { Name = "ProgressReporter" })); + + await using McpClient client = await CreateMcpClientForServer(); + + var tool = (await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken)).First(t => t.Name == "ProgressReporter"); + + IProgress progress = new SynchronousProgress(value => + { + Assert.True(value.Progress >= 0 && value.Progress <= 100); + Assert.Equal("making progress", value.Message); + if (Interlocked.Decrement(ref remainingProgress) == 0) + { + allProgressReceived.SetResult(true); + } + }); + + Assert.Throws("progress", () => tool.WithProgress(null!)); + + var result = await tool.WithProgress(progress).InvokeAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Contains("42", result?.ToString()); } - private sealed class FailureTransport : NopTransport + private sealed class SynchronousProgress(Action callback) : IProgress { - public const string ExpectedMessage = "Something failed"; + public void Report(ProgressNotificationValue value) => callback(value); + } + + [Fact] + public async Task AsClientLoggerProvider_MessagesSentToClient() + { + await using McpClient client = await CreateMcpClientForServer(); + + ILoggerProvider loggerProvider = Server.AsClientLoggerProvider(); + Assert.Throws("categoryName", () => loggerProvider.CreateLogger(null!)); + + ILogger logger = loggerProvider.CreateLogger("TestLogger"); + Assert.NotNull(logger); - public override Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default) + Assert.Null(logger.BeginScope("")); + + Assert.Null(Server.LoggingLevel); + Assert.False(logger.IsEnabled(LogLevel.Trace)); + Assert.False(logger.IsEnabled(LogLevel.Debug)); + Assert.False(logger.IsEnabled(LogLevel.Information)); + Assert.False(logger.IsEnabled(LogLevel.Warning)); + Assert.False(logger.IsEnabled(LogLevel.Error)); + Assert.False(logger.IsEnabled(LogLevel.Critical)); + + await client.SetLoggingLevel(LoggingLevel.Info, TestContext.Current.CancellationToken); + + DateTime start = DateTime.UtcNow; + while (Server.LoggingLevel is null) { - throw new InvalidOperationException(ExpectedMessage); + await Task.Delay(1, TestContext.Current.CancellationToken); + Assert.True(DateTime.UtcNow - start < TimeSpan.FromSeconds(10), "Timed out waiting for logging level to be set"); } + + Assert.Equal(LoggingLevel.Info, Server.LoggingLevel); + Assert.False(logger.IsEnabled(LogLevel.Trace)); + Assert.False(logger.IsEnabled(LogLevel.Debug)); + Assert.True(logger.IsEnabled(LogLevel.Information)); + Assert.True(logger.IsEnabled(LogLevel.Warning)); + Assert.True(logger.IsEnabled(LogLevel.Error)); + Assert.True(logger.IsEnabled(LogLevel.Critical)); + + List data = []; + var channel = Channel.CreateUnbounded(); + + await using (client.RegisterNotificationHandler(NotificationMethods.LoggingMessageNotification, + (notification, cancellationToken) => + { + Assert.True(channel.Writer.TryWrite(JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.DefaultOptions))); + return default; + })) + { + logger.LogTrace("Trace {Message}", "message"); + logger.LogDebug("Debug {Message}", "message"); + logger.LogInformation("Information {Message}", "message"); + logger.LogWarning("Warning {Message}", "message"); + logger.LogError("Error {Message}", "message"); + logger.LogCritical("Critical {Message}", "message"); + + for (int i = 0; i < 4; i++) + { + var m = await channel.Reader.ReadAsync(TestContext.Current.CancellationToken); + Assert.NotNull(m); + Assert.NotNull(m.Data); + + Assert.Equal("TestLogger", m.Logger); + + string ? s = JsonSerializer.Deserialize(m.Data.Value, McpJsonUtilities.DefaultOptions); + Assert.NotNull(s); + + if (s.Contains("Information")) + { + Assert.Equal(LoggingLevel.Info, m.Level); + } + else if (s.Contains("Warning")) + { + Assert.Equal(LoggingLevel.Warning, m.Level); + } + else if (s.Contains("Error")) + { + Assert.Equal(LoggingLevel.Error, m.Level); + } + else if (s.Contains("Critical")) + { + Assert.Equal(LoggingLevel.Critical, m.Level); + } + + data.Add(s); + } + + channel.Writer.Complete(); + } + + Assert.False(await channel.Reader.WaitToReadAsync(TestContext.Current.CancellationToken)); + Assert.Equal( + [ + "Critical message", + "Error message", + "Information message", + "Warning message", + ], + data.OrderBy(s => s)); } -} +} \ No newline at end of file From 36dd101c6651508149493364bc4d60a4d8cd157c Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 29 Aug 2025 12:57:36 -0400 Subject: [PATCH 05/15] Add back old API, but obsoleted --- .../Client/IMcpClient.cs | 48 ++ .../Client/McpClient.cs | 4 +- .../Client/McpClientExtensions.cs | 573 ++++++++++++++++++ src/ModelContextProtocol.Core/IMcpEndpoint.cs | 83 +++ .../McpEndpointExtensions.cs | 148 +++++ src/ModelContextProtocol.Core/McpSession.cs | 4 +- .../Server/IMcpServer.cs | 63 ++ .../Server/McpServer.cs | 4 +- .../Server/McpServerExtensions.cs | 130 ++++ .../Client/McpClientExtensionsTest.cs | 387 ++++++++++++ .../McpEndpointExtensionsTests.cs | 118 ++++ .../Server/McpServerExtensionsTests.cs | 195 ++++++ 12 files changed, 1754 insertions(+), 3 deletions(-) create mode 100644 src/ModelContextProtocol.Core/Client/IMcpClient.cs create mode 100644 src/ModelContextProtocol.Core/IMcpEndpoint.cs create mode 100644 src/ModelContextProtocol.Core/McpEndpointExtensions.cs create mode 100644 src/ModelContextProtocol.Core/Server/IMcpServer.cs create mode 100644 src/ModelContextProtocol.Core/Server/McpServerExtensions.cs create mode 100644 tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTest.cs create mode 100644 tests/ModelContextProtocol.Tests/McpEndpointExtensionsTests.cs create mode 100644 tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs diff --git a/src/ModelContextProtocol.Core/Client/IMcpClient.cs b/src/ModelContextProtocol.Core/Client/IMcpClient.cs new file mode 100644 index 000000000..dad8f2823 --- /dev/null +++ b/src/ModelContextProtocol.Core/Client/IMcpClient.cs @@ -0,0 +1,48 @@ +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol.Client; + +/// +/// Represents an instance of a Model Context Protocol (MCP) client that connects to and communicates with an MCP server. +/// +[Obsolete($"Use {nameof(McpClient)} instead.")] +public interface IMcpClient : IMcpEndpoint +{ + /// + /// Gets the capabilities supported by the connected server. + /// + /// The client is not connected. + ServerCapabilities ServerCapabilities { get; } + + /// + /// Gets the implementation information of the connected server. + /// + /// + /// + /// This property provides identification details about the connected server, including its name and version. + /// It is populated during the initialization handshake and is available after a successful connection. + /// + /// + /// This information can be useful for logging, debugging, compatibility checks, and displaying server + /// information to users. + /// + /// + /// The client is not connected. + Implementation ServerInfo { get; } + + /// + /// Gets any instructions describing how to use the connected server and its features. + /// + /// + /// + /// This property contains instructions provided by the server during initialization that explain + /// how to effectively use its capabilities. These instructions can include details about available + /// tools, expected input formats, limitations, or any other helpful information. + /// + /// + /// This can be used by clients to improve an LLM's understanding of available tools, prompts, and resources. + /// It can be thought of like a "hint" to the model and may be added to a system prompt. + /// + /// + string? ServerInstructions { get; } +} diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs index 59bacc7bd..c5178e333 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -10,7 +10,9 @@ namespace ModelContextProtocol.Client; /// /// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server. /// -public abstract class McpClient : McpSession +#pragma warning disable CS0618 // Type or member is obsolete +public abstract class McpClient : McpSession, IMcpClient +#pragma warning restore CS0618 // Type or member is obsolete { /// /// Gets the capabilities supported by the connected server. diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index afac2bc80..9ceea4601 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -1,5 +1,8 @@ using Microsoft.Extensions.AI; using ModelContextProtocol.Protocol; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; namespace ModelContextProtocol.Client; @@ -136,4 +139,574 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat return updates.ToChatResponse().ToCreateMessageResult(); }; } + + /// + /// Sends a ping request to verify server connectivity. + /// + /// The client instance used to communicate with the MCP server. + /// The to monitor for cancellation requests. The default is . + /// A task that completes when the ping is successful. + /// + /// + /// This method is used to check if the MCP server is online and responding to requests. + /// It can be useful for health checking, ensuring the connection is established, or verifying + /// that the client has proper authorization to communicate with the server. + /// + /// + /// The ping operation is lightweight and does not require any parameters. A successful completion + /// of the task indicates that the server is operational and accessible. + /// + /// + /// is . + /// Thrown when the server cannot be reached or returns an error response. + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.PingAsync)} instead.")] + public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).PingAsync(cancellationToken); + + /// + /// Retrieves a list of available tools from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The serializer options governing tool parameter serialization. If null, the default options will be used. + /// The to monitor for cancellation requests. The default is . + /// A list of all available tools as instances. + /// + /// + /// This method fetches all available tools from the MCP server and returns them as a complete list. + /// It automatically handles pagination with cursors if the server responds with only a portion per request. + /// + /// + /// For servers with a large number of tools and that responds with paginated responses, consider using + /// instead, as it streams tools as they arrive rather than loading them all at once. + /// + /// + /// The serializer options provided are flowed to each and will be used + /// when invoking tools in order to serialize any parameters. + /// + /// + /// + /// + /// // Get all tools available on the server + /// var tools = await mcpClient.ListToolsAsync(); + /// + /// // Use tools with an AI client + /// ChatOptions chatOptions = new() + /// { + /// Tools = [.. tools] + /// }; + /// + /// await foreach (var update in chatClient.GetStreamingResponseAsync(userMessage, chatOptions)) + /// { + /// Console.Write(update); + /// } + /// + /// + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListToolsAsync)} instead.")] + public static ValueTask> ListToolsAsync( + this IMcpClient client, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + => AsClientOrThrow(client).ListToolsAsync(serializerOptions, cancellationToken); + + /// + /// Creates an enumerable for asynchronously enumerating all available tools from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The serializer options governing tool parameter serialization. If null, the default options will be used. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available tools as instances. + /// + /// + /// This method uses asynchronous enumeration to retrieve tools from the server, which allows processing tools + /// as they arrive rather than waiting for all tools to be retrieved. The method automatically handles pagination + /// with cursors if the server responds with tools split across multiple responses. + /// + /// + /// The serializer options provided are flowed to each and will be used + /// when invoking tools in order to serialize any parameters. + /// + /// + /// Every iteration through the returned + /// will result in re-querying the server and yielding the sequence of available tools. + /// + /// + /// + /// + /// // Enumerate all tools available on the server + /// await foreach (var tool in client.EnumerateToolsAsync()) + /// { + /// Console.WriteLine($"Tool: {tool.Name}"); + /// } + /// + /// + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateToolsAsync)} instead.")] + public static IAsyncEnumerable EnumerateToolsAsync( + this IMcpClient client, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + => AsClientOrThrow(client).EnumerateToolsAsync(serializerOptions, cancellationToken); + + /// + /// Retrieves a list of available prompts from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The to monitor for cancellation requests. The default is . + /// A list of all available prompts as instances. + /// + /// + /// This method fetches all available prompts from the MCP server and returns them as a complete list. + /// It automatically handles pagination with cursors if the server responds with only a portion per request. + /// + /// + /// For servers with a large number of prompts and that responds with paginated responses, consider using + /// instead, as it streams prompts as they arrive rather than loading them all at once. + /// + /// + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListPromptsAsync)} instead.")] + public static ValueTask> ListPromptsAsync( + this IMcpClient client, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).ListPromptsAsync(cancellationToken); + + /// + /// Creates an enumerable for asynchronously enumerating all available prompts from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available prompts as instances. + /// + /// + /// This method uses asynchronous enumeration to retrieve prompts from the server, which allows processing prompts + /// as they arrive rather than waiting for all prompts to be retrieved. The method automatically handles pagination + /// with cursors if the server responds with prompts split across multiple responses. + /// + /// + /// Every iteration through the returned + /// will result in re-querying the server and yielding the sequence of available prompts. + /// + /// + /// + /// + /// // Enumerate all prompts available on the server + /// await foreach (var prompt in client.EnumeratePromptsAsync()) + /// { + /// Console.WriteLine($"Prompt: {prompt.Name}"); + /// } + /// + /// + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumeratePromptsAsync)} instead.")] + public static IAsyncEnumerable EnumeratePromptsAsync( + this IMcpClient client, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).EnumeratePromptsAsync(cancellationToken); + + /// + /// Retrieves a specific prompt from the MCP server. + /// + /// The client instance used to communicate with the MCP server. + /// The name of the prompt to retrieve. + /// Optional arguments for the prompt. Keys are parameter names, and values are the argument values. + /// The serialization options governing argument serialization. + /// The to monitor for cancellation requests. The default is . + /// A task containing the prompt's result with content and messages. + /// + /// + /// This method sends a request to the MCP server to create the specified prompt with the provided arguments. + /// The server will process the arguments and return a prompt containing messages or other content. + /// + /// + /// Arguments are serialized into JSON and passed to the server, where they may be used to customize the + /// prompt's behavior or content. Each prompt may have different argument requirements. + /// + /// + /// The returned contains a collection of objects, + /// which can be converted to objects using the method. + /// + /// + /// Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt. + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.GetPromptAsync)} instead.")] + public static ValueTask GetPromptAsync( + this IMcpClient client, + string name, + IReadOnlyDictionary? arguments = null, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + => AsClientOrThrow(client).GetPromptAsync(name, arguments, serializerOptions, cancellationToken); + + /// + /// Retrieves a list of available resource templates from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The to monitor for cancellation requests. The default is . + /// A list of all available resource templates as instances. + /// + /// + /// This method fetches all available resource templates from the MCP server and returns them as a complete list. + /// It automatically handles pagination with cursors if the server responds with only a portion per request. + /// + /// + /// For servers with a large number of resource templates and that responds with paginated responses, consider using + /// instead, as it streams templates as they arrive rather than loading them all at once. + /// + /// + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourceTemplatesAsync)} instead.")] + public static ValueTask> ListResourceTemplatesAsync( + this IMcpClient client, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).ListResourceTemplatesAsync(cancellationToken); + + /// + /// Creates an enumerable for asynchronously enumerating all available resource templates from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available resource templates as instances. + /// + /// + /// This method uses asynchronous enumeration to retrieve resource templates from the server, which allows processing templates + /// as they arrive rather than waiting for all templates to be retrieved. The method automatically handles pagination + /// with cursors if the server responds with templates split across multiple responses. + /// + /// + /// Every iteration through the returned + /// will result in re-querying the server and yielding the sequence of available resource templates. + /// + /// + /// + /// + /// // Enumerate all resource templates available on the server + /// await foreach (var template in client.EnumerateResourceTemplatesAsync()) + /// { + /// Console.WriteLine($"Template: {template.Name}"); + /// } + /// + /// + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourceTemplatesAsync)} instead.")] + public static IAsyncEnumerable EnumerateResourceTemplatesAsync( + this IMcpClient client, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).EnumerateResourceTemplatesAsync(cancellationToken); + + /// + /// Retrieves a list of available resources from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The to monitor for cancellation requests. The default is . + /// A list of all available resources as instances. + /// + /// + /// This method fetches all available resources from the MCP server and returns them as a complete list. + /// It automatically handles pagination with cursors if the server responds with only a portion per request. + /// + /// + /// For servers with a large number of resources and that responds with paginated responses, consider using + /// instead, as it streams resources as they arrive rather than loading them all at once. + /// + /// + /// + /// + /// // Get all resources available on the server + /// var resources = await client.ListResourcesAsync(); + /// + /// // Display information about each resource + /// foreach (var resource in resources) + /// { + /// Console.WriteLine($"Resource URI: {resource.Uri}"); + /// } + /// + /// + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourcesAsync)} instead.")] + public static ValueTask> ListResourcesAsync( + this IMcpClient client, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).ListResourcesAsync(cancellationToken); + + /// + /// Creates an enumerable for asynchronously enumerating all available resources from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available resources as instances. + /// + /// + /// This method uses asynchronous enumeration to retrieve resources from the server, which allows processing resources + /// as they arrive rather than waiting for all resources to be retrieved. The method automatically handles pagination + /// with cursors if the server responds with resources split across multiple responses. + /// + /// + /// Every iteration through the returned + /// will result in re-querying the server and yielding the sequence of available resources. + /// + /// + /// + /// + /// // Enumerate all resources available on the server + /// await foreach (var resource in client.EnumerateResourcesAsync()) + /// { + /// Console.WriteLine($"Resource URI: {resource.Uri}"); + /// } + /// + /// + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourcesAsync)} instead.")] + public static IAsyncEnumerable EnumerateResourcesAsync( + this IMcpClient client, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).EnumerateResourcesAsync(cancellationToken); + + /// + /// Reads a resource from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The uri of the resource. + /// The to monitor for cancellation requests. The default is . + /// is . + /// is . + /// is empty or composed entirely of whitespace. + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] + public static ValueTask ReadResourceAsync( + this IMcpClient client, string uri, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken); + + /// + /// Reads a resource from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The uri of the resource. + /// The to monitor for cancellation requests. The default is . + /// is . + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] + public static ValueTask ReadResourceAsync( + this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken); + + /// + /// Reads a resource from the server. + /// + /// The client instance used to communicate with the MCP server. + /// The uri template of the resource. + /// Arguments to use to format . + /// The to monitor for cancellation requests. The default is . + /// is . + /// is . + /// is empty or composed entirely of whitespace. + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] + public static ValueTask ReadResourceAsync( + this IMcpClient client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).ReadResourceAsync(uriTemplate, arguments, cancellationToken); + + /// + /// Requests completion suggestions for a prompt argument or resource reference. + /// + /// The client instance used to communicate with the MCP server. + /// The reference object specifying the type and optional URI or name. + /// The name of the argument for which completions are requested. + /// The current value of the argument, used to filter relevant completions. + /// The to monitor for cancellation requests. The default is . + /// A containing completion suggestions. + /// + /// + /// This method allows clients to request auto-completion suggestions for arguments in a prompt template + /// or for resource references. + /// + /// + /// When working with prompt references, the server will return suggestions for the specified argument + /// that match or begin with the current argument value. This is useful for implementing intelligent + /// auto-completion in user interfaces. + /// + /// + /// When working with resource references, the server will return suggestions relevant to the specified + /// resource URI. + /// + /// + /// is . + /// is . + /// is . + /// is empty or composed entirely of whitespace. + /// The server returned an error response. + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CompleteAsync)} instead.")] + public static ValueTask CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).CompleteAsync(reference, argumentName, argumentValue, cancellationToken); + + /// + /// Subscribes to a resource on the server to receive notifications when it changes. + /// + /// The client instance used to communicate with the MCP server. + /// The URI of the resource to which to subscribe. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + /// + /// + /// This method allows the client to register interest in a specific resource identified by its URI. + /// When the resource changes, the server will send notifications to the client, enabling real-time + /// updates without polling. + /// + /// + /// The subscription remains active until explicitly unsubscribed using + /// or until the client disconnects from the server. + /// + /// + /// To handle resource change notifications, register an event handler for the appropriate notification events, + /// such as with . + /// + /// + /// is . + /// is . + /// is empty or composed entirely of whitespace. + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] + public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken); + + /// + /// Subscribes to a resource on the server to receive notifications when it changes. + /// + /// The client instance used to communicate with the MCP server. + /// The URI of the resource to which to subscribe. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + /// + /// + /// This method allows the client to register interest in a specific resource identified by its URI. + /// When the resource changes, the server will send notifications to the client, enabling real-time + /// updates without polling. + /// + /// + /// The subscription remains active until explicitly unsubscribed using + /// or until the client disconnects from the server. + /// + /// + /// To handle resource change notifications, register an event handler for the appropriate notification events, + /// such as with . + /// + /// + /// is . + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] + public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken); + + /// + /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. + /// + /// The client instance used to communicate with the MCP server. + /// The URI of the resource to unsubscribe from. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + /// + /// + /// This method cancels a previous subscription to a resource, stopping the client from receiving + /// notifications when that resource changes. + /// + /// + /// The unsubscribe operation is idempotent, meaning it can be called multiple times for the same + /// resource without causing errors, even if there is no active subscription. + /// + /// + /// Due to the nature of the MCP protocol, it is possible the client may receive notifications after + /// unsubscribing if those notifications were issued by the server prior to the unsubscribe request being received. + /// + /// + /// is . + /// is . + /// is empty or composed entirely of whitespace. + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] + public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken); + + /// + /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. + /// + /// The client instance used to communicate with the MCP server. + /// The URI of the resource to unsubscribe from. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + /// + /// + /// This method cancels a previous subscription to a resource, stopping the client from receiving + /// notifications when that resource changes. + /// + /// + /// The unsubscribe operation is idempotent, meaning it can be called multiple times for the same + /// resource without causing errors, even if there is no active subscription. + /// + /// + /// Due to the nature of the MCP protocol, it is possible the client may receive notifications after + /// unsubscribing if those notifications were issued by the server prior to the unsubscribe request being received. + /// + /// + /// is . + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] + public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) + => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken); + + /// + /// Invokes a tool on the server. + /// + /// The client instance used to communicate with the MCP server. + /// The name of the tool to call on the server.. + /// An optional dictionary of arguments to pass to the tool. Each key represents a parameter name, + /// and its associated value represents the argument value. + /// + /// + /// An optional to have progress notifications reported to it. Setting this to a non- + /// value will result in a progress token being included in the call, and any resulting progress notifications during the operation + /// routed to this instance. + /// + /// + /// The JSON serialization options governing argument serialization. If , the default serialization options will be used. + /// + /// The to monitor for cancellation requests. The default is . + /// + /// A task containing the from the tool execution. The response includes + /// the tool's output content, which may be structured data, text, or an error message. + /// + /// is . + /// is . + /// The server could not find the requested tool, or the server encountered an error while processing the request. + /// + /// + /// // Call a simple echo tool with a string argument + /// var result = await client.CallToolAsync( + /// "echo", + /// new Dictionary<string, object?> + /// { + /// ["message"] = "Hello MCP!" + /// }); + /// + /// + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CallToolAsync)} instead.")] + public static ValueTask CallToolAsync( + this IMcpClient client, + string toolName, + IReadOnlyDictionary? arguments = null, + IProgress? progress = null, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + => AsClientOrThrow(client).CallToolAsync(toolName, arguments, progress, serializerOptions, cancellationToken); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable CS0618 // Type or member is obsolete + private static McpClient AsClientOrThrow(IMcpClient client, [CallerMemberName] string memberName = "") +#pragma warning restore CS0618 // Type or member is obsolete + { + if (client is not McpClient mcpClient) + { + ThrowInvalidEndpointType(memberName); + } + + return mcpClient; + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowInvalidEndpointType(string memberName) + => throw new InvalidOperationException( + $"Only arguments assignable to '{nameof(McpClient)}' are supported. " + + $"Prefer using '{nameof(McpClient)}.{memberName}' instead, as " + + $"'{nameof(McpClientExtensions)}.{memberName}' is obsolete and will be " + + $"removed in the future."); + } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/IMcpEndpoint.cs b/src/ModelContextProtocol.Core/IMcpEndpoint.cs new file mode 100644 index 000000000..01221ecdb --- /dev/null +++ b/src/ModelContextProtocol.Core/IMcpEndpoint.cs @@ -0,0 +1,83 @@ +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; + +namespace ModelContextProtocol; + +/// +/// Represents a client or server Model Context Protocol (MCP) endpoint. +/// +/// +/// +/// The MCP endpoint provides the core communication functionality used by both clients and servers: +/// +/// Sending JSON-RPC requests and receiving responses. +/// Sending notifications to the connected endpoint. +/// Registering handlers for receiving notifications. +/// +/// +/// +/// serves as the base interface for both and +/// interfaces, providing the common functionality needed for MCP protocol +/// communication. Most applications will use these more specific interfaces rather than working with +/// directly. +/// +/// +/// All MCP endpoints should be properly disposed after use as they implement . +/// +/// +[Obsolete($"Use {nameof(McpSession)} instead.")] +public interface IMcpEndpoint : IAsyncDisposable +{ + /// Gets an identifier associated with the current MCP session. + /// + /// Typically populated in transports supporting multiple sessions such as Streamable HTTP or SSE. + /// Can return if the session hasn't initialized or if the transport doesn't + /// support multiple sessions (as is the case with STDIO). + /// + string? SessionId { get; } + + /// + /// Sends a JSON-RPC request to the connected endpoint and waits for a response. + /// + /// The JSON-RPC request to send. + /// The to monitor for cancellation requests. The default is . + /// A task containing the endpoint's response. + /// The transport is not connected, or another error occurs during request processing. + /// An error occured during request processing. + /// + /// This method provides low-level access to send raw JSON-RPC requests. For most use cases, + /// consider using the strongly-typed extension methods that provide a more convenient API. + /// + Task SendRequestAsync(JsonRpcRequest request, CancellationToken cancellationToken = default); + + /// + /// Sends a JSON-RPC message to the connected endpoint. + /// + /// + /// The JSON-RPC message to send. This can be any type that implements JsonRpcMessage, such as + /// JsonRpcRequest, JsonRpcResponse, JsonRpcNotification, or JsonRpcError. + /// + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous send operation. + /// The transport is not connected. + /// is . + /// + /// + /// This method provides low-level access to send any JSON-RPC message. For specific message types, + /// consider using the higher-level methods such as or extension methods + /// like , + /// which provide a simpler API. + /// + /// + /// The method will serialize the message and transmit it using the underlying transport mechanism. + /// + /// + Task SendMessageAsync(JsonRpcMessage message, CancellationToken cancellationToken = default); + + /// Registers a handler to be invoked when a notification for the specified method is received. + /// The notification method. + /// The handler to be invoked. + /// An that will remove the registered handler when disposed. + IAsyncDisposable RegisterNotificationHandler(string method, Func handler); +} diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs new file mode 100644 index 000000000..0f0239fab --- /dev/null +++ b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs @@ -0,0 +1,148 @@ +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; + +namespace ModelContextProtocol; + +/// +/// Provides extension methods for interacting with an . +/// +/// +/// +/// This class provides strongly-typed methods for working with the Model Context Protocol (MCP) endpoints, +/// simplifying JSON-RPC communication by handling serialization and deserialization of parameters and results. +/// +/// +/// These extension methods are designed to be used with both client () and +/// server () implementations of the interface. +/// +/// +public static class McpEndpointExtensions +{ + /// + /// Sends a JSON-RPC request and attempts to deserialize the result to . + /// + /// The type of the request parameters to serialize from. + /// The type of the result to deserialize to. + /// The MCP client or server instance. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The request id for the request. + /// The options governing request serialization. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains the deserialized result. + [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendRequestAsync)} instead.")] + public static ValueTask SendRequestAsync( + this IMcpEndpoint endpoint, + string method, + TParameters parameters, + JsonSerializerOptions? serializerOptions = null, + RequestId requestId = default, + CancellationToken cancellationToken = default) + where TResult : notnull + => AsSessionOrThrow(endpoint).SendRequestAsync(method, parameters, serializerOptions, requestId, cancellationToken); + + /// + /// Sends a parameterless notification to the connected endpoint. + /// + /// The MCP client or server instance. + /// The notification method name. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous send operation. + /// + /// + /// This method sends a notification without any parameters. Notifications are one-way messages + /// that don't expect a response. They are commonly used for events, status updates, or to signal + /// changes in state. + /// + /// + [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendNotificationAsync)} instead.")] + public static Task SendNotificationAsync(this IMcpEndpoint client, string method, CancellationToken cancellationToken = default) + => AsSessionOrThrow(client).SendNotificationAsync(method, cancellationToken); + + /// + /// Sends a notification with parameters to the connected endpoint. + /// + /// The type of the notification parameters to serialize. + /// The MCP client or server instance. + /// The JSON-RPC method name for the notification. + /// Object representing the notification parameters. + /// The options governing parameter serialization. If null, default options are used. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous send operation. + /// + /// + /// This method sends a notification with parameters to the connected endpoint. Notifications are one-way + /// messages that don't expect a response, commonly used for events, status updates, or signaling changes. + /// + /// + /// The parameters object is serialized to JSON according to the provided serializer options or the default + /// options if none are specified. + /// + /// + /// The Model Context Protocol defines several standard notification methods in , + /// but custom methods can also be used for application-specific notifications. + /// + /// + [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendNotificationAsync)} instead.")] + public static Task SendNotificationAsync( + this IMcpEndpoint endpoint, + string method, + TParameters parameters, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + => AsSessionOrThrow(endpoint).SendNotificationAsync(method, parameters, serializerOptions, cancellationToken); + + /// + /// Notifies the connected endpoint of progress for a long-running operation. + /// + /// The endpoint issuing the notification. + /// The identifying the operation for which progress is being reported. + /// The progress update to send, containing information such as percentage complete or status message. + /// The to monitor for cancellation requests. The default is . + /// A task representing the completion of the notification operation (not the operation being tracked). + /// is . + /// + /// + /// This method sends a progress notification to the connected endpoint using the Model Context Protocol's + /// standardized progress notification format. Progress updates are identified by a + /// that allows the recipient to correlate multiple updates with a specific long-running operation. + /// + /// + /// Progress notifications are sent asynchronously and don't block the operation from continuing. + /// + /// + [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.NotifyProgressAsync)} instead.")] + public static Task NotifyProgressAsync( + this IMcpEndpoint endpoint, + ProgressToken progressToken, + ProgressNotificationValue progress, + CancellationToken cancellationToken = default) + => AsSessionOrThrow(endpoint).NotifyProgressAsync(progressToken, progress, cancellationToken); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable CS0618 // Type or member is obsolete + private static McpSession AsSessionOrThrow(IMcpEndpoint endpoint, [CallerMemberName] string memberName = "") +#pragma warning restore CS0618 // Type or member is obsolete + { + if (endpoint is not McpSession session) + { + ThrowInvalidEndpointType(memberName); + } + + return session; + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowInvalidEndpointType(string memberName) + => throw new InvalidOperationException( + $"Only arguments assignable to '{nameof(McpSession)}' are supported. " + + $"Prefer using '{nameof(McpServer)}.{memberName}' instead, as " + + $"'{nameof(McpEndpointExtensions)}.{memberName}' is obsolete and will be " + + $"removed in the future."); + } +} diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs index 267ec4ff5..8fea9f2d9 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -29,7 +29,9 @@ namespace ModelContextProtocol; /// All MCP sessions should be properly disposed after use as they implement . /// /// -public abstract class McpSession : IAsyncDisposable +#pragma warning disable CS0618 // Type or member is obsolete +public abstract class McpSession : IMcpEndpoint, IAsyncDisposable +#pragma warning restore CS0618 // Type or member is obsolete { /// Gets an identifier associated with the current MCP session. /// diff --git a/src/ModelContextProtocol.Core/Server/IMcpServer.cs b/src/ModelContextProtocol.Core/Server/IMcpServer.cs new file mode 100644 index 000000000..31131f81f --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/IMcpServer.cs @@ -0,0 +1,63 @@ +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol.Server; + +/// +/// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. +/// +[Obsolete($"Use {nameof(McpServer)} instead.")] +public interface IMcpServer : IMcpEndpoint +{ + /// + /// Gets the capabilities supported by the client. + /// + /// + /// + /// These capabilities are established during the initialization handshake and indicate + /// which features the client supports, such as sampling, roots, and other + /// protocol-specific functionality. + /// + /// + /// Server implementations can check these capabilities to determine which features + /// are available when interacting with the client. + /// + /// + ClientCapabilities? ClientCapabilities { get; } + + /// + /// Gets the version and implementation information of the connected client. + /// + /// + /// + /// This property contains identification information about the client that has connected to this server, + /// including its name and version. This information is provided by the client during initialization. + /// + /// + /// Server implementations can use this information for logging, tracking client versions, + /// or implementing client-specific behaviors. + /// + /// + Implementation? ClientInfo { get; } + + /// + /// Gets the options used to construct this server. + /// + /// + /// These options define the server's capabilities, protocol version, and other configuration + /// settings that were used to initialize the server. + /// + McpServerOptions ServerOptions { get; } + + /// + /// Gets the service provider for the server. + /// + IServiceProvider? Services { get; } + + /// Gets the last logging level set by the client, or if it's never been set. + LoggingLevel? LoggingLevel { get; } + + /// + /// Runs the server, listening for and handling client requests. + /// + Task RunAsync(CancellationToken cancellationToken = default); +} diff --git a/src/ModelContextProtocol.Core/Server/McpServer.cs b/src/ModelContextProtocol.Core/Server/McpServer.cs index 4af249fe3..87921b389 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.cs @@ -10,7 +10,9 @@ namespace ModelContextProtocol.Server; /// /// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. /// -public abstract class McpServer : McpSession +#pragma warning disable CS0618 // Type or member is obsolete +public abstract class McpServer : McpSession, IMcpServer +#pragma warning restore CS0618 // Type or member is obsolete { /// /// Gets the capabilities supported by the client. diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs new file mode 100644 index 000000000..98edd3fc2 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs @@ -0,0 +1,130 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Protocol; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace ModelContextProtocol.Server; + +/// +/// Provides extension methods for interacting with an instance. +/// +public static class McpServerExtensions +{ + /// + /// Requests to sample an LLM via the client using the specified request parameters. + /// + /// The server instance initiating the request. + /// The parameters for the sampling request. + /// The to monitor for cancellation requests. + /// A task containing the sampling result from the client. + /// is . + /// The client does not support sampling. + /// + /// This method requires the client to support sampling capabilities. + /// It allows detailed control over sampling parameters including messages, system prompt, temperature, + /// and token limits. + /// + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead.")] + public static ValueTask SampleAsync( + this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default) + => AsServerOrThrow(server).SampleAsync(request, cancellationToken); + + /// + /// Requests to sample an LLM via the client using the provided chat messages and options. + /// + /// The server initiating the request. + /// The messages to send as part of the request. + /// The options to use for the request, including model parameters and constraints. + /// The to monitor for cancellation requests. The default is . + /// A task containing the chat response from the model. + /// is . + /// is . + /// The client does not support sampling. + /// + /// This method converts the provided chat messages into a format suitable for the sampling API, + /// handling different content types such as text, images, and audio. + /// + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead.")] + public static Task SampleAsync( + this IMcpServer server, + IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) + => AsServerOrThrow(server).SampleAsync(messages, options, cancellationToken); + + /// + /// Creates an wrapper that can be used to send sampling requests to the client. + /// + /// The server to be wrapped as an . + /// The that can be used to issue sampling requests to the client. + /// is . + /// The client does not support sampling. + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead.")] + public static IChatClient AsSamplingChatClient(this IMcpServer server) + => AsServerOrThrow(server).AsSamplingChatClient(); + + /// Gets an on which logged messages will be sent as notifications to the client. + /// The server to wrap as an . + /// An that can be used to log to the client.. + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead.")] + public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server) + => AsServerOrThrow(server).AsClientLoggerProvider(); + + /// + /// Requests the client to list the roots it exposes. + /// + /// The server initiating the request. + /// The parameters for the list roots request. + /// The to monitor for cancellation requests. + /// A task containing the list of roots exposed by the client. + /// is . + /// The client does not support roots. + /// + /// This method requires the client to support the roots capability. + /// Root resources allow clients to expose a hierarchical structure of resources that can be + /// navigated and accessed by the server. These resources might include file systems, databases, + /// or other structured data sources that the client makes available through the protocol. + /// + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.RequestRootsAsync)} instead.")] + public static ValueTask RequestRootsAsync( + this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default) + => AsServerOrThrow(server).RequestRootsAsync(request, cancellationToken); + + /// + /// Requests additional information from the user via the client, allowing the server to elicit structured data. + /// + /// The server initiating the request. + /// The parameters for the elicitation request. + /// The to monitor for cancellation requests. + /// A task containing the elicitation result. + /// is . + /// The client does not support elicitation. + /// + /// This method requires the client to support the elicitation capability. + /// + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.ElicitAsync)} instead.")] + public static ValueTask ElicitAsync( + this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default) + => AsServerOrThrow(server).ElicitAsync(request, cancellationToken); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] +#pragma warning disable CS0618 // Type or member is obsolete + private static McpServer AsServerOrThrow(IMcpServer server, [CallerMemberName] string memberName = "") +#pragma warning restore CS0618 // Type or member is obsolete + { + if (server is not McpServer mcpServer) + { + ThrowInvalidEndpointType(memberName); + } + + return mcpServer; + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowInvalidEndpointType(string memberName) + => throw new InvalidOperationException( + $"Only arguments assignable to '{nameof(McpServer)}' are supported. " + + $"Prefer using '{nameof(McpServer)}.{memberName}' instead, as " + + $"'{nameof(McpServerExtensions)}.{memberName}' is obsolete and will be " + + $"removed in the future."); + } +} diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTest.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTest.cs new file mode 100644 index 000000000..f4e6062de --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTest.cs @@ -0,0 +1,387 @@ +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; +using Moq; +using System.Text.Json; + +namespace ModelContextProtocol.Tests; + +#pragma warning disable CS0618 // Type or member is obsolete + +public class McpClientExtensionsTests +{ + [Fact] + public async Task PingAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.PingAsync(TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.PingAsync' instead", ex.Message); + } + + [Fact] + public async Task GetPromptAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.GetPromptAsync( + "name", cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.GetPromptAsync' instead", ex.Message); + } + + [Fact] + public async Task CallToolAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.CallToolAsync( + "tool", cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.CallToolAsync' instead", ex.Message); + } + + [Fact] + public async Task ListResourcesAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.ListResourcesAsync( + cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.ListResourcesAsync' instead", ex.Message); + } + + [Fact] + public void EnumerateResourcesAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = Assert.Throws(() => client.EnumerateResourcesAsync(cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.EnumerateResourcesAsync' instead", ex.Message); + } + + [Fact] + public async Task SubscribeToResourceAsync_String_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.SubscribeToResourceAsync( + "mcp://resource/1", TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.SubscribeToResourceAsync' instead", ex.Message); + } + + [Fact] + public async Task SubscribeToResourceAsync_Uri_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.SubscribeToResourceAsync( + new Uri("mcp://resource/1"), TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.SubscribeToResourceAsync' instead", ex.Message); + } + + [Fact] + public async Task UnsubscribeFromResourceAsync_String_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.UnsubscribeFromResourceAsync( + "mcp://resource/1", TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.UnsubscribeFromResourceAsync' instead", ex.Message); + } + + [Fact] + public async Task UnsubscribeFromResourceAsync_Uri_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.UnsubscribeFromResourceAsync( + new Uri("mcp://resource/1"), TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.UnsubscribeFromResourceAsync' instead", ex.Message); + } + + [Fact] + public async Task ReadResourceAsync_String_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( + "mcp://resource/1", TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.ReadResourceAsync' instead", ex.Message); + } + + [Fact] + public async Task ReadResourceAsync_Uri_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( + new Uri("mcp://resource/1"), TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.ReadResourceAsync' instead", ex.Message); + } + + [Fact] + public async Task ReadResourceAsync_Template_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.ReadResourceAsync( + "mcp://resource/{id}", new Dictionary { ["id"] = 1 }, TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.ReadResourceAsync' instead", ex.Message); + } + + [Fact] + public async Task CompleteAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + var reference = new PromptReference { Name = "prompt" }; + + var ex = await Assert.ThrowsAsync(async () => await client.CompleteAsync( + reference, "arg", "val", TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.CompleteAsync' instead", ex.Message); + } + + [Fact] + public async Task ListToolsAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.ListToolsAsync( + cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.ListToolsAsync' instead", ex.Message); + } + + [Fact] + public void EnumerateToolsAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = Assert.Throws(() => client.EnumerateToolsAsync(cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.EnumerateToolsAsync' instead", ex.Message); + } + + [Fact] + public async Task ListPromptsAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.ListPromptsAsync( + cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.ListPromptsAsync' instead", ex.Message); + } + + [Fact] + public void EnumeratePromptsAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = Assert.Throws(() => client.EnumeratePromptsAsync(cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.EnumeratePromptsAsync' instead", ex.Message); + } + + [Fact] + public async Task ListResourceTemplatesAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await client.ListResourceTemplatesAsync( + cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.ListResourceTemplatesAsync' instead", ex.Message); + } + + [Fact] + public void EnumerateResourceTemplatesAsync_Throws_When_Not_McpClient() + { + var client = new Mock(MockBehavior.Strict).Object; + + var ex = Assert.Throws(() => client.EnumerateResourceTemplatesAsync(cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpClient.EnumerateResourceTemplatesAsync' instead", ex.Message); + } + + [Fact] + public async Task PingAsync_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(new object(), McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + await client.PingAsync(TestContext.Current.CancellationToken); + + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task GetPromptAsync_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + var resultPayload = new GetPromptResult { Messages = [new PromptMessage { Role = Role.User, Content = new TextContentBlock { Text = "hi" } }] }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + var result = await client.GetPromptAsync("name", cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("hi", Assert.IsType(result.Messages[0].Content).Text); + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task CallToolAsync_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + var callResult = new CallToolResult { Content = [new TextContentBlock { Text = "ok" }] }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(callResult, McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + var result = await client.CallToolAsync("tool", cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("ok", Assert.IsType(result.Content[0]).Text); + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task SubscribeToResourceAsync_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(new EmptyResult(), McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + await client.SubscribeToResourceAsync("mcp://resource/1", TestContext.Current.CancellationToken); + + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task UnsubscribeFromResourceAsync_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(new EmptyResult(), McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + await client.UnsubscribeFromResourceAsync("mcp://resource/1", TestContext.Current.CancellationToken); + + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task CompleteAsync_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + var completion = new Completion { Values = ["one", "two"] }; + var resultPayload = new CompleteResult { Completion = completion }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + var result = await client.CompleteAsync(new PromptReference { Name = "p" }, "arg", "val", TestContext.Current.CancellationToken); + + Assert.Contains("one", result.Completion.Values); + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ReadResourceAsync_String_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + var resultPayload = new ReadResourceResult { Contents = [] }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + var result = await client.ReadResourceAsync("mcp://resource/1", TestContext.Current.CancellationToken); + + Assert.NotNull(result); + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ReadResourceAsync_Uri_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + var resultPayload = new ReadResourceResult { Contents = [] }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + var result = await client.ReadResourceAsync(new Uri("mcp://resource/1"), TestContext.Current.CancellationToken); + + Assert.NotNull(result); + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ReadResourceAsync_Template_Forwards_To_McpClient_SendRequestAsync() + { + var mockClient = new Mock { CallBase = true }; + + var resultPayload = new ReadResourceResult { Contents = [] }; + + mockClient + .Setup(c => c.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpClient client = mockClient.Object; + + var result = await client.ReadResourceAsync("mcp://resource/{id}", new Dictionary { ["id"] = 1 }, TestContext.Current.CancellationToken); + + Assert.NotNull(result); + mockClient.Verify(c => c.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} diff --git a/tests/ModelContextProtocol.Tests/McpEndpointExtensionsTests.cs b/tests/ModelContextProtocol.Tests/McpEndpointExtensionsTests.cs new file mode 100644 index 000000000..613c703c3 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/McpEndpointExtensionsTests.cs @@ -0,0 +1,118 @@ +using ModelContextProtocol.Protocol; +using Moq; +using System.Text.Json; + +namespace ModelContextProtocol.Tests; + +#pragma warning disable CS0618 // Type or member is obsolete + +public class McpEndpointExtensionsTests +{ + [Fact] + public async Task SendRequestAsync_Generic_Throws_When_Not_McpSession() + { + var endpoint = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await McpEndpointExtensions.SendRequestAsync( + endpoint, "method", "param", cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpServer.SendRequestAsync' instead", ex.Message); + } + + [Fact] + public async Task SendNotificationAsync_Parameterless_Throws_When_Not_McpSession() + { + var endpoint = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await McpEndpointExtensions.SendNotificationAsync( + endpoint, "notify", cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpServer.SendNotificationAsync' instead", ex.Message); + } + + [Fact] + public async Task SendNotificationAsync_Generic_Throws_When_Not_McpSession() + { + var endpoint = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await McpEndpointExtensions.SendNotificationAsync( + endpoint, "notify", "payload", cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpServer.SendNotificationAsync' instead", ex.Message); + } + + [Fact] + public async Task NotifyProgressAsync_Throws_When_Not_McpSession() + { + var endpoint = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await McpEndpointExtensions.NotifyProgressAsync( + endpoint, new ProgressToken("t1"), new ProgressNotificationValue { Progress = 0.5f }, cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpServer.NotifyProgressAsync' instead", ex.Message); + } + + [Fact] + public async Task SendRequestAsync_Generic_Forwards_To_McpSession_SendRequestAsync() + { + var mockSession = new Mock { CallBase = true }; + + mockSession + .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(42, McpJsonUtilities.DefaultOptions), + }); + + IMcpEndpoint endpoint = mockSession.Object; + + var result = await endpoint.SendRequestAsync("method", "param", cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal(42, result); + mockSession.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task SendNotificationAsync_Parameterless_Forwards_To_McpSession_SendMessageAsync() + { + var mockSession = new Mock { CallBase = true }; + + mockSession + .Setup(s => s.SendMessageAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + IMcpEndpoint endpoint = mockSession.Object; + + await endpoint.SendNotificationAsync("notify", cancellationToken: TestContext.Current.CancellationToken); + + mockSession.Verify(s => s.SendMessageAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task SendNotificationAsync_Generic_Forwards_To_McpSession_SendMessageAsync() + { + var mockSession = new Mock { CallBase = true }; + + mockSession + .Setup(s => s.SendMessageAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + IMcpEndpoint endpoint = mockSession.Object; + + await endpoint.SendNotificationAsync("notify", "payload", cancellationToken: TestContext.Current.CancellationToken); + + mockSession.Verify(s => s.SendMessageAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task NotifyProgressAsync_Forwards_To_McpSession_SendMessageAsync() + { + var mockSession = new Mock { CallBase = true }; + + mockSession + .Setup(s => s.SendMessageAsync(It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + IMcpEndpoint endpoint = mockSession.Object; + + await endpoint.NotifyProgressAsync(new ProgressToken("progress-token"), new ProgressNotificationValue { Progress = 1 }, cancellationToken: TestContext.Current.CancellationToken); + + mockSession.Verify(s => s.SendMessageAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} \ No newline at end of file diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs new file mode 100644 index 000000000..5569f993c --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Server/McpServerExtensionsTests.cs @@ -0,0 +1,195 @@ +using Microsoft.Extensions.AI; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using Moq; +using System.Text.Json; + +namespace ModelContextProtocol.Tests.Server; + +#pragma warning disable CS0618 // Type or member is obsolete + +public class McpServerExtensionsTests +{ + [Fact] + public async Task SampleAsync_Request_Throws_When_Not_McpServer() + { + var server = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await server.SampleAsync( + new CreateMessageRequestParams { Messages = [new SamplingMessage { Role = Role.User, Content = new TextContentBlock { Text = "hi" } }] }, + TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpServer.SampleAsync' instead", ex.Message); + } + + [Fact] + public async Task SampleAsync_Messages_Throws_When_Not_McpServer() + { + var server = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await server.SampleAsync( + [new ChatMessage(ChatRole.User, "hi")], cancellationToken: TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpServer.SampleAsync' instead", ex.Message); + } + + [Fact] + public void AsSamplingChatClient_Throws_When_Not_McpServer() + { + var server = new Mock(MockBehavior.Strict).Object; + + var ex = Assert.Throws(server.AsSamplingChatClient); + Assert.Contains("Prefer using 'McpServer.AsSamplingChatClient' instead", ex.Message); + } + + [Fact] + public void AsClientLoggerProvider_Throws_When_Not_McpServer() + { + var server = new Mock(MockBehavior.Strict).Object; + + var ex = Assert.Throws(server.AsClientLoggerProvider); + Assert.Contains("Prefer using 'McpServer.AsClientLoggerProvider' instead", ex.Message); + } + + [Fact] + public async Task RequestRootsAsync_Throws_When_Not_McpServer() + { + var server = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await server.RequestRootsAsync( + new ListRootsRequestParams(), TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpServer.RequestRootsAsync' instead", ex.Message); + } + + [Fact] + public async Task ElicitAsync_Throws_When_Not_McpServer() + { + var server = new Mock(MockBehavior.Strict).Object; + + var ex = await Assert.ThrowsAsync(async () => await server.ElicitAsync( + new ElicitRequestParams { Message = "hello" }, TestContext.Current.CancellationToken)); + Assert.Contains("Prefer using 'McpServer.ElicitAsync' instead", ex.Message); + } + + [Fact] + public async Task SampleAsync_Request_Forwards_To_McpServer_SendRequestAsync() + { + var mockServer = new Mock { CallBase = true }; + + var resultPayload = new CreateMessageResult + { + Content = new TextContentBlock { Text = "resp" }, + Model = "test-model", + Role = Role.Assistant, + StopReason = "endTurn", + }; + + mockServer + .Setup(s => s.ClientCapabilities) + .Returns(new ClientCapabilities() { Sampling = new() }); + + mockServer + .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpServer server = mockServer.Object; + + var result = await server.SampleAsync(new CreateMessageRequestParams + { + Messages = [new SamplingMessage { Role = Role.User, Content = new TextContentBlock { Text = "hi" } }] + }, TestContext.Current.CancellationToken); + + Assert.Equal("test-model", result.Model); + Assert.Equal(Role.Assistant, result.Role); + Assert.Equal("resp", Assert.IsType(result.Content).Text); + mockServer.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task SampleAsync_Messages_Forwards_To_McpServer_SendRequestAsync() + { + var mockServer = new Mock { CallBase = true }; + + var resultPayload = new CreateMessageResult + { + Content = new TextContentBlock { Text = "resp" }, + Model = "test-model", + Role = Role.Assistant, + StopReason = "endTurn", + }; + + mockServer + .Setup(s => s.ClientCapabilities) + .Returns(new ClientCapabilities() { Sampling = new() }); + + mockServer + .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpServer server = mockServer.Object; + + var chatResponse = await server.SampleAsync([new ChatMessage(ChatRole.User, "hi")], cancellationToken: TestContext.Current.CancellationToken); + + Assert.Equal("test-model", chatResponse.ModelId); + var last = chatResponse.Messages.Last(); + Assert.Equal(ChatRole.Assistant, last.Role); + Assert.Equal("resp", last.Text); + mockServer.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task RequestRootsAsync_Forwards_To_McpServer_SendRequestAsync() + { + var mockServer = new Mock { CallBase = true }; + + var resultPayload = new ListRootsResult { Roots = [new Root { Uri = "root://a" }] }; + + mockServer + .Setup(s => s.ClientCapabilities) + .Returns(new ClientCapabilities() { Roots = new() }); + + mockServer + .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpServer server = mockServer.Object; + + var result = await server.RequestRootsAsync(new ListRootsRequestParams(), TestContext.Current.CancellationToken); + + Assert.Equal("root://a", result.Roots[0].Uri); + mockServer.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async Task ElicitAsync_Forwards_To_McpServer_SendRequestAsync() + { + var mockServer = new Mock { CallBase = true }; + + var resultPayload = new ElicitResult { Action = "accept" }; + + mockServer + .Setup(s => s.ClientCapabilities) + .Returns(new ClientCapabilities() { Elicitation = new() }); + + mockServer + .Setup(s => s.SendRequestAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new JsonRpcResponse + { + Result = JsonSerializer.SerializeToNode(resultPayload, McpJsonUtilities.DefaultOptions), + }); + + IMcpServer server = mockServer.Object; + + var result = await server.ElicitAsync(new ElicitRequestParams { Message = "hi" }, TestContext.Current.CancellationToken); + + Assert.Equal("accept", result.Action); + mockServer.Verify(s => s.SendRequestAsync(It.IsAny(), It.IsAny()), Times.Once); + } +} From 32a777ab2cc74da939100a45d378153e93418c20 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 29 Aug 2025 13:06:03 -0400 Subject: [PATCH 06/15] Add back obsoleted factory classes --- .../Client/McpClientFactory.cs | 33 +++++++++++++++++++ .../Server/McpServerFactory.cs | 32 ++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/ModelContextProtocol.Core/Client/McpClientFactory.cs create mode 100644 src/ModelContextProtocol.Core/Server/McpServerFactory.cs diff --git a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs new file mode 100644 index 000000000..297099521 --- /dev/null +++ b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Logging; + +namespace ModelContextProtocol.Client; + +/// +/// Provides factory methods for creating Model Context Protocol (MCP) clients. +/// +/// +/// This factory class is the primary way to instantiate instances +/// that connect to MCP servers. It handles the creation and connection +/// of appropriate implementations through the supplied transport. +/// +public static partial class McpClientFactory +{ + /// Creates an , connecting it to the specified server. + /// The transport instance used to communicate with the server. + /// + /// A client configuration object which specifies client capabilities and protocol version. + /// If , details based on the current process will be employed. + /// + /// A logger factory for creating loggers for clients. + /// The to monitor for cancellation requests. The default is . + /// An that's connected to the specified server. + /// is . + /// is . + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead.")] + public static async Task CreateAsync( + IClientTransport clientTransport, + McpClientOptions? clientOptions = null, + ILoggerFactory? loggerFactory = null, + CancellationToken cancellationToken = default) + => await McpClient.CreateAsync(clientTransport, clientOptions, loggerFactory, cancellationToken); +} \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs new file mode 100644 index 000000000..4060a57d7 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Protocol; + +namespace ModelContextProtocol.Server; + +/// +/// Provides a factory for creating instances. +/// +/// +/// This is the recommended way to create instances. +/// The factory handles proper initialization of server instances with the required dependencies. +/// +public static class McpServerFactory +{ + /// + /// Creates a new instance of an . + /// + /// Transport to use for the server representing an already-established MCP session. + /// Configuration options for this server, including capabilities. + /// Logger factory to use for logging. If null, logging will be disabled. + /// Optional service provider to create new instances of tools and other dependencies. + /// An instance that should be disposed when no longer needed. + /// is . + /// is . + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.Create)} instead.")] + public static IMcpServer Create( + ITransport transport, + McpServerOptions serverOptions, + ILoggerFactory? loggerFactory = null, + IServiceProvider? serviceProvider = null) + => McpServer.Create(transport, serverOptions, loggerFactory, serviceProvider); +} From ef3372f61aa653d6a77eec0704786894dda38818 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Fri, 29 Aug 2025 13:08:05 -0400 Subject: [PATCH 07/15] Move deprecation attributes --- src/ModelContextProtocol.Core/Client/McpClientFactory.cs | 2 +- src/ModelContextProtocol.Core/Server/McpServerFactory.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs index 297099521..756281a13 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs @@ -10,6 +10,7 @@ namespace ModelContextProtocol.Client; /// that connect to MCP servers. It handles the creation and connection /// of appropriate implementations through the supplied transport. /// +[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead.")] public static partial class McpClientFactory { /// Creates an , connecting it to the specified server. @@ -23,7 +24,6 @@ public static partial class McpClientFactory /// An that's connected to the specified server. /// is . /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead.")] public static async Task CreateAsync( IClientTransport clientTransport, McpClientOptions? clientOptions = null, diff --git a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs index 4060a57d7..79384b7c3 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs @@ -10,6 +10,7 @@ namespace ModelContextProtocol.Server; /// This is the recommended way to create instances. /// The factory handles proper initialization of server instances with the required dependencies. /// +[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.Create)} instead.")] public static class McpServerFactory { /// @@ -22,7 +23,6 @@ public static class McpServerFactory /// An instance that should be disposed when no longer needed. /// is . /// is . - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.Create)} instead.")] public static IMcpServer Create( ITransport transport, McpServerOptions serverOptions, From 4d426d07617148b068ac2919c6d7c57a89541ee3 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Wed, 3 Sep 2025 09:53:46 -0500 Subject: [PATCH 08/15] `SseClientTransport` -> `HttpClientTransport` --- samples/ProtectedMcpClient/Program.cs | 2 +- samples/QuickstartClient/Program.cs | 2 +- .../AutoDetectingClientSessionTransport.cs | 4 ++-- ...entTransport.cs => HttpClientTransport.cs} | 12 +++++----- ...tions.cs => HttpClientTransportOptions.cs} | 4 ++-- .../Client/SseClientSessionTransport.cs | 6 ++--- .../StreamableHttpClientSessionTransport.cs | 8 +++---- .../AuthEventTests.cs | 4 ++-- .../AuthTests.cs | 14 ++++++------ .../HttpServerIntegrationTests.cs | 2 +- .../MapMcpTests.cs | 4 ++-- .../SseIntegrationTests.cs | 10 ++++----- .../SseServerIntegrationTestFixture.cs | 6 ++--- .../SseServerIntegrationTests.cs | 2 +- .../StatelessServerIntegrationTests.cs | 2 +- .../StatelessServerTests.cs | 4 ++-- .../StreamableHttpClientConformanceTests.cs | 6 ++--- .../StreamableHttpServerIntegrationTests.cs | 2 +- .../DockerEverythingServerTests.cs | 8 +++---- ... => HttpClientTransportAutoDetectTests.cs} | 10 ++++----- ...rtTests.cs => HttpClientTransportTests.cs} | 22 +++++++++---------- 21 files changed, 67 insertions(+), 67 deletions(-) rename src/ModelContextProtocol.Core/Client/{SseClientTransport.cs => HttpClientTransport.cs} (86%) rename src/ModelContextProtocol.Core/Client/{SseClientTransportOptions.cs => HttpClientTransportOptions.cs} (96%) rename tests/ModelContextProtocol.Tests/Transport/{SseClientTransportAutoDetectTests.cs => HttpClientTransportAutoDetectTests.cs} (89%) rename tests/ModelContextProtocol.Tests/Transport/{SseClientTransportTests.cs => HttpClientTransportTests.cs} (86%) diff --git a/samples/ProtectedMcpClient/Program.cs b/samples/ProtectedMcpClient/Program.cs index f9c8d4d71..9dc2410ea 100644 --- a/samples/ProtectedMcpClient/Program.cs +++ b/samples/ProtectedMcpClient/Program.cs @@ -25,7 +25,7 @@ builder.AddConsole(); }); -var transport = new SseClientTransport(new() +var transport = new HttpClientTransport(new() { Endpoint = new Uri(serverUrl), Name = "Secure Weather Client", diff --git a/samples/QuickstartClient/Program.cs b/samples/QuickstartClient/Program.cs index 9b8d9edfa..cd1c4c60a 100644 --- a/samples/QuickstartClient/Program.cs +++ b/samples/QuickstartClient/Program.cs @@ -19,7 +19,7 @@ if (command == "http") { // make sure AspNetCoreMcpServer is running - clientTransport = new SseClientTransport(new() + clientTransport = new HttpClientTransport(new() { Endpoint = new Uri("http://localhost:3001") }); diff --git a/src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs index 06f2e0bfb..2e49babcf 100644 --- a/src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/AutoDetectingClientSessionTransport.cs @@ -12,14 +12,14 @@ namespace ModelContextProtocol.Client; /// internal sealed partial class AutoDetectingClientSessionTransport : ITransport { - private readonly SseClientTransportOptions _options; + private readonly HttpClientTransportOptions _options; private readonly McpHttpClient _httpClient; private readonly ILoggerFactory? _loggerFactory; private readonly ILogger _logger; private readonly string _name; private readonly Channel _messageChannel; - public AutoDetectingClientSessionTransport(string endpointName, SseClientTransportOptions transportOptions, McpHttpClient httpClient, ILoggerFactory? loggerFactory) + public AutoDetectingClientSessionTransport(string endpointName, HttpClientTransportOptions transportOptions, McpHttpClient httpClient, ILoggerFactory? loggerFactory) { Throw.IfNull(transportOptions); Throw.IfNull(httpClient); diff --git a/src/ModelContextProtocol.Core/Client/SseClientTransport.cs b/src/ModelContextProtocol.Core/Client/HttpClientTransport.cs similarity index 86% rename from src/ModelContextProtocol.Core/Client/SseClientTransport.cs rename to src/ModelContextProtocol.Core/Client/HttpClientTransport.cs index b31c3479b..322b9175e 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientTransport.cs +++ b/src/ModelContextProtocol.Core/Client/HttpClientTransport.cs @@ -13,26 +13,26 @@ namespace ModelContextProtocol.Client; /// Unlike the , this transport connects to an existing server /// rather than launching a new process. /// -public sealed class SseClientTransport : IClientTransport, IAsyncDisposable +public sealed class HttpClientTransport : IClientTransport, IAsyncDisposable { - private readonly SseClientTransportOptions _options; + private readonly HttpClientTransportOptions _options; private readonly McpHttpClient _mcpHttpClient; private readonly ILoggerFactory? _loggerFactory; private readonly HttpClient? _ownedHttpClient; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Configuration options for the transport. /// Logger factory for creating loggers used for diagnostic output during transport operations. - public SseClientTransport(SseClientTransportOptions transportOptions, ILoggerFactory? loggerFactory = null) + public HttpClientTransport(HttpClientTransportOptions transportOptions, ILoggerFactory? loggerFactory = null) : this(transportOptions, new HttpClient(), loggerFactory, ownsHttpClient: true) { } /// - /// Initializes a new instance of the class with a provided HTTP client. + /// Initializes a new instance of the class with a provided HTTP client. /// /// Configuration options for the transport. /// The HTTP client instance used for requests. @@ -41,7 +41,7 @@ public SseClientTransport(SseClientTransportOptions transportOptions, ILoggerFac /// to dispose of when the transport is disposed; /// if the caller is retaining ownership of the 's lifetime. /// - public SseClientTransport(SseClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory = null, bool ownsHttpClient = false) + public HttpClientTransport(HttpClientTransportOptions transportOptions, HttpClient httpClient, ILoggerFactory? loggerFactory = null, bool ownsHttpClient = false) { Throw.IfNull(transportOptions); Throw.IfNull(httpClient); diff --git a/src/ModelContextProtocol.Core/Client/SseClientTransportOptions.cs b/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs similarity index 96% rename from src/ModelContextProtocol.Core/Client/SseClientTransportOptions.cs rename to src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs index 4097844cf..94b95eecb 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientTransportOptions.cs +++ b/src/ModelContextProtocol.Core/Client/HttpClientTransportOptions.cs @@ -3,9 +3,9 @@ namespace ModelContextProtocol.Client; /// -/// Provides options for configuring instances. +/// Provides options for configuring instances. /// -public sealed class SseClientTransportOptions +public sealed class HttpClientTransportOptions { /// /// Gets or sets the base address of the server for SSE connections. diff --git a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs index 479a76279..60950dfa5 100644 --- a/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs @@ -15,7 +15,7 @@ namespace ModelContextProtocol.Client; internal sealed partial class SseClientSessionTransport : TransportBase { private readonly McpHttpClient _httpClient; - private readonly SseClientTransportOptions _options; + private readonly HttpClientTransportOptions _options; private readonly Uri _sseEndpoint; private Uri? _messageEndpoint; private readonly CancellationTokenSource _connectionCts; @@ -29,7 +29,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase /// public SseClientSessionTransport( string endpointName, - SseClientTransportOptions transportOptions, + HttpClientTransportOptions transportOptions, McpHttpClient httpClient, Channel? messageChannel, ILoggerFactory? loggerFactory) @@ -42,7 +42,7 @@ public SseClientSessionTransport( _sseEndpoint = transportOptions.Endpoint; _httpClient = httpClient; _connectionCts = new CancellationTokenSource(); - _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; _connectionEstablished = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); } diff --git a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs index 823e266f0..f2fd55f16 100644 --- a/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs +++ b/src/ModelContextProtocol.Core/Client/StreamableHttpClientSessionTransport.cs @@ -17,7 +17,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa private static readonly MediaTypeWithQualityHeaderValue s_textEventStreamMediaType = new("text/event-stream"); private readonly McpHttpClient _httpClient; - private readonly SseClientTransportOptions _options; + private readonly HttpClientTransportOptions _options; private readonly CancellationTokenSource _connectionCts; private readonly ILogger _logger; @@ -29,7 +29,7 @@ internal sealed partial class StreamableHttpClientSessionTransport : TransportBa public StreamableHttpClientSessionTransport( string endpointName, - SseClientTransportOptions transportOptions, + HttpClientTransportOptions transportOptions, McpHttpClient httpClient, Channel? messageChannel, ILoggerFactory? loggerFactory) @@ -41,7 +41,7 @@ public StreamableHttpClientSessionTransport( _options = transportOptions; _httpClient = httpClient; _connectionCts = new CancellationTokenSource(); - _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; + _logger = (ILogger?)loggerFactory?.CreateLogger() ?? NullLogger.Instance; // We connect with the initialization request with the MCP transport. This means that any errors won't be observed // until the first call to SendMessageAsync. Fortunately, that happens internally in McpClient.ConnectAsync @@ -291,7 +291,7 @@ internal static void CopyAdditionalHeaders( { if (!headers.TryAddWithoutValidation(header.Key, header.Value)) { - throw new InvalidOperationException($"Failed to add header '{header.Key}' with value '{header.Value}' from {nameof(SseClientTransportOptions.AdditionalHeaders)}."); + throw new InvalidOperationException($"Failed to add header '{header.Key}' with value '{header.Value}' from {nameof(HttpClientTransportOptions.AdditionalHeaders)}."); } } } diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs index 09e14ccbd..9144121e8 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthEventTests.cs @@ -106,7 +106,7 @@ public async Task CanAuthenticate_WithResourceMetadataFromEvent() await app.StartAsync(TestContext.Current.CancellationToken); - await using var transport = new SseClientTransport( + await using var transport = new HttpClientTransport( new() { Endpoint = new(McpServerUrl), @@ -142,7 +142,7 @@ public async Task CanAuthenticate_WithDynamicClientRegistration_FromEvent() DynamicClientRegistrationResponse? dcrResponse = null; - await using var transport = new SseClientTransport( + await using var transport = new HttpClientTransport( new() { Endpoint = new(McpServerUrl), diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs index db82f6737..fff7d6d42 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/AuthTests.cs @@ -97,7 +97,7 @@ public async Task CanAuthenticate() 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() @@ -124,7 +124,7 @@ public async Task CannotAuthenticate_WithoutOAuthConfiguration() 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); @@ -146,7 +146,7 @@ public async Task CannotAuthenticate_WithUnregisteredClient() 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() @@ -174,7 +174,7 @@ public async Task CanAuthenticate_WithDynamicClientRegistration() 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() @@ -205,7 +205,7 @@ public async Task CanAuthenticate_WithTokenRefresh() 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() @@ -236,7 +236,7 @@ public async Task CanAuthenticate_WithExtraParams() 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() @@ -270,7 +270,7 @@ public async Task CannotOverrideExistingParameters_WithExtraParams() 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() diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs index 30e0f9dfa..f9aa5a5e9 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/HttpServerIntegrationTests.cs @@ -21,7 +21,7 @@ public override void Dispose() base.Dispose(); } - protected abstract SseClientTransportOptions ClientTransportOptions { get; } + protected abstract HttpClientTransportOptions ClientTransportOptions { get; } private Task GetClientAsync(McpClientOptions? options = null) { diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs index cce23a533..06076a5e4 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/MapMcpTests.cs @@ -25,13 +25,13 @@ protected void ConfigureStateless(HttpServerTransportOptions options) protected async Task 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, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs index c419ec695..ffec1a4be 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseIntegrationTests.cs @@ -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 ConnectMcpClientAsync(HttpClient? httpClient = null, SseClientTransportOptions? transportOptions = null) + private Task ConnectMcpClientAsync(HttpClient? httpClient = null, HttpClientTransportOptions? transportOptions = null) => McpClient.CreateAsync( - new SseClientTransport(transportOptions ?? DefaultTransportOptions, httpClient ?? HttpClient, LoggerFactory), + new HttpClientTransport(transportOptions ?? DefaultTransportOptions, httpClient ?? HttpClient, LoggerFactory), loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); @@ -195,7 +195,7 @@ public async Task AdditionalHeaders_AreSent_InGetAndPostRequests() 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 async Task EmptyAdditionalHeadersKey_Throws_InvalidOperationException() 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", diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs index 7a11bebcd..c382c4385 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTestFixture.cs @@ -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/"), }; @@ -47,13 +47,13 @@ public SseServerIntegrationTestFixture() public Task ConnectMcpClientAsync(McpClientOptions? options, ILoggerFactory loggerFactory) { return McpClient.CreateAsync( - new SseClientTransport(DefaultTransportOptions, HttpClient, loggerFactory), + 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; diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTests.cs index 2d4a78685..eb7db0110 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/SseServerIntegrationTests.cs @@ -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", diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerIntegrationTests.cs index d16e510cc..2ce63a1bc 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerIntegrationTests.cs @@ -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", diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs index 8bc2130d9..3c200bb61 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StatelessServerTests.cs @@ -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", @@ -60,7 +60,7 @@ private async Task StartAsync() private Task ConnectMcpClientAsync(McpClientOptions? clientOptions = null) => McpClient.CreateAsync( - new SseClientTransport(DefaultTransportOptions, HttpClient, LoggerFactory), + new HttpClientTransport(DefaultTransportOptions, HttpClient, LoggerFactory), clientOptions, LoggerFactory, TestContext.Current.CancellationToken); public async ValueTask DisposeAsync() diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs index 3ca8010a7..f1cd458f9 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpClientConformanceTests.cs @@ -112,7 +112,7 @@ public async Task CanCallToolOnSessionlessStreamableHttpServer() { 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, @@ -132,7 +132,7 @@ public async Task CanCallToolConcurrently() { 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, @@ -158,7 +158,7 @@ public async Task SendsDeleteRequestOnDispose() { 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, diff --git a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerIntegrationTests.cs b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerIntegrationTests.cs index 3524c60a4..b2b0b5499 100644 --- a/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerIntegrationTests.cs +++ b/tests/ModelContextProtocol.AspNetCore.Tests/StreamableHttpServerIntegrationTests.cs @@ -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", diff --git a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs index e3faf05f4..842371f88 100644 --- a/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs +++ b/tests/ModelContextProtocol.Tests/DockerEverythingServerTests.cs @@ -36,7 +36,7 @@ public async Task ConnectAndReceiveMessage_EverythingServerWithSse() ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" } }; - var defaultConfig = new SseClientTransportOptions + var defaultConfig = new HttpClientTransportOptions { Endpoint = new Uri($"http://localhost:{port}/sse"), Name = "Everything", @@ -44,7 +44,7 @@ public async Task ConnectAndReceiveMessage_EverythingServerWithSse() // Create client and run tests await using var client = await McpClient.CreateAsync( - new SseClientTransport(defaultConfig), + new HttpClientTransport(defaultConfig), defaultOptions, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); @@ -63,7 +63,7 @@ public async Task Sampling_Sse_EverythingServer() await using var fixture = new EverythingSseServerFixture(port); await fixture.StartAsync(); - var defaultConfig = new SseClientTransportOptions + var defaultConfig = new HttpClientTransportOptions { Endpoint = new Uri($"http://localhost:{port}/sse"), Name = "Everything", @@ -91,7 +91,7 @@ public async Task Sampling_Sse_EverythingServer() }; await using var client = await McpClient.CreateAsync( - new SseClientTransport(defaultConfig), + new HttpClientTransport(defaultConfig), defaultOptions, loggerFactory: LoggerFactory, cancellationToken: TestContext.Current.CancellationToken); diff --git a/tests/ModelContextProtocol.Tests/Transport/SseClientTransportAutoDetectTests.cs b/tests/ModelContextProtocol.Tests/Transport/HttpClientTransportAutoDetectTests.cs similarity index 89% rename from tests/ModelContextProtocol.Tests/Transport/SseClientTransportAutoDetectTests.cs rename to tests/ModelContextProtocol.Tests/Transport/HttpClientTransportAutoDetectTests.cs index 8f6fbff2c..768ebf7ea 100644 --- a/tests/ModelContextProtocol.Tests/Transport/SseClientTransportAutoDetectTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/HttpClientTransportAutoDetectTests.cs @@ -4,12 +4,12 @@ namespace ModelContextProtocol.Tests.Transport; -public class SseClientTransportAutoDetectTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) +public class HttpClientTransportAutoDetectTests(ITestOutputHelper testOutputHelper) : LoggedTest(testOutputHelper) { [Fact] public async Task AutoDetectMode_UsesStreamableHttp_WhenServerSupportsIt() { - var options = new SseClientTransportOptions + var options = new HttpClientTransportOptions { Endpoint = new Uri("http://localhost"), TransportMode = HttpTransportMode.AutoDetect, @@ -18,7 +18,7 @@ public async Task AutoDetectMode_UsesStreamableHttp_WhenServerSupportsIt() using var mockHttpHandler = new MockHttpHandler(); using var httpClient = new HttpClient(mockHttpHandler); - await using var transport = new SseClientTransport(options, httpClient, LoggerFactory); + await using var transport = new HttpClientTransport(options, httpClient, LoggerFactory); // Simulate successful Streamable HTTP response for initialize mockHttpHandler.RequestHandler = (request) => @@ -50,7 +50,7 @@ public async Task AutoDetectMode_UsesStreamableHttp_WhenServerSupportsIt() [Fact] public async Task AutoDetectMode_FallsBackToSse_WhenStreamableHttpFails() { - var options = new SseClientTransportOptions + var options = new HttpClientTransportOptions { Endpoint = new Uri("http://localhost"), TransportMode = HttpTransportMode.AutoDetect, @@ -59,7 +59,7 @@ public async Task AutoDetectMode_FallsBackToSse_WhenStreamableHttpFails() using var mockHttpHandler = new MockHttpHandler(); using var httpClient = new HttpClient(mockHttpHandler); - await using var transport = new SseClientTransport(options, httpClient, LoggerFactory); + await using var transport = new HttpClientTransport(options, httpClient, LoggerFactory); var requestCount = 0; diff --git a/tests/ModelContextProtocol.Tests/Transport/SseClientTransportTests.cs b/tests/ModelContextProtocol.Tests/Transport/HttpClientTransportTests.cs similarity index 86% rename from tests/ModelContextProtocol.Tests/Transport/SseClientTransportTests.cs rename to tests/ModelContextProtocol.Tests/Transport/HttpClientTransportTests.cs index 3ff504304..fc1ac2d88 100644 --- a/tests/ModelContextProtocol.Tests/Transport/SseClientTransportTests.cs +++ b/tests/ModelContextProtocol.Tests/Transport/HttpClientTransportTests.cs @@ -5,14 +5,14 @@ namespace ModelContextProtocol.Tests.Transport; -public class SseClientTransportTests : LoggedTest +public class HttpClientTransportTests : LoggedTest { - private readonly SseClientTransportOptions _transportOptions; + private readonly HttpClientTransportOptions _transportOptions; - public SseClientTransportTests(ITestOutputHelper testOutputHelper) + public HttpClientTransportTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) { - _transportOptions = new SseClientTransportOptions + _transportOptions = new HttpClientTransportOptions { Endpoint = new Uri("http://localhost:8080"), ConnectionTimeout = TimeSpan.FromSeconds(2), @@ -28,14 +28,14 @@ public SseClientTransportTests(ITestOutputHelper testOutputHelper) [Fact] public void Constructor_Throws_For_Null_Options() { - var exception = Assert.Throws(() => new SseClientTransport(null!, LoggerFactory)); + var exception = Assert.Throws(() => new HttpClientTransport(null!, LoggerFactory)); Assert.Equal("transportOptions", exception.ParamName); } [Fact] public void Constructor_Throws_For_Null_HttpClient() { - var exception = Assert.Throws(() => new SseClientTransport(_transportOptions, httpClient: null!, LoggerFactory)); + var exception = Assert.Throws(() => new HttpClientTransport(_transportOptions, httpClient: null!, LoggerFactory)); Assert.Equal("httpClient", exception.ParamName); } @@ -44,7 +44,7 @@ public async Task ConnectAsync_Should_Connect_Successfully() { using var mockHttpHandler = new MockHttpHandler(); using var httpClient = new HttpClient(mockHttpHandler); - await using var transport = new SseClientTransport(_transportOptions, httpClient, LoggerFactory); + await using var transport = new HttpClientTransport(_transportOptions, httpClient, LoggerFactory); bool firstCall = true; @@ -68,7 +68,7 @@ public async Task ConnectAsync_Throws_Exception_On_Failure() { using var mockHttpHandler = new MockHttpHandler(); using var httpClient = new HttpClient(mockHttpHandler); - await using var transport = new SseClientTransport(_transportOptions, httpClient, LoggerFactory); + await using var transport = new HttpClientTransport(_transportOptions, httpClient, LoggerFactory); var retries = 0; mockHttpHandler.RequestHandler = (request) => @@ -87,7 +87,7 @@ public async Task SendMessageAsync_Handles_Accepted_Response() { using var mockHttpHandler = new MockHttpHandler(); using var httpClient = new HttpClient(mockHttpHandler); - await using var transport = new SseClientTransport(_transportOptions, httpClient, LoggerFactory); + await using var transport = new HttpClientTransport(_transportOptions, httpClient, LoggerFactory); var firstCall = true; mockHttpHandler.RequestHandler = (request) => @@ -125,7 +125,7 @@ public async Task ReceiveMessagesAsync_Handles_Messages() { using var mockHttpHandler = new MockHttpHandler(); using var httpClient = new HttpClient(mockHttpHandler); - await using var transport = new SseClientTransport(_transportOptions, httpClient, LoggerFactory); + await using var transport = new HttpClientTransport(_transportOptions, httpClient, LoggerFactory); var callIndex = 0; mockHttpHandler.RequestHandler = (request) => @@ -165,7 +165,7 @@ public async Task DisposeAsync_Should_Dispose_Resources() }); }; - await using var transport = new SseClientTransport(_transportOptions, httpClient, LoggerFactory); + await using var transport = new HttpClientTransport(_transportOptions, httpClient, LoggerFactory); await using var session = await transport.ConnectAsync(TestContext.Current.CancellationToken); await session.DisposeAsync(); From 0121957e1286239389561858b550e7ce0b3367d2 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 4 Sep 2025 09:08:53 -0700 Subject: [PATCH 09/15] Update src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs Co-authored-by: Stephen Halter --- .../Server/AugmentedServiceProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs b/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs index df8006174..bc06952ca 100644 --- a/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs +++ b/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs @@ -18,6 +18,9 @@ internal sealed class RequestServiceProvider( public static bool IsAugmentedWith(Type serviceType) => serviceType == typeof(RequestContext) || serviceType == typeof(McpServer) || +#pragma warning disable CS0618 // Type or member is obsolete + serviceType == typeof(IMcpServer) || +#pragma warning restore CS0618 // Type or member is obsolete serviceType == typeof(IProgress); /// From c524acc37b75d871b077a635f7bf86789e7d9a05 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 4 Sep 2025 09:10:43 -0700 Subject: [PATCH 10/15] PR feedback --- .../Server/AugmentedServiceProvider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs b/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs index bc06952ca..0af6b4729 100644 --- a/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs +++ b/src/ModelContextProtocol.Core/Server/AugmentedServiceProvider.cs @@ -26,7 +26,9 @@ public static bool IsAugmentedWith(Type serviceType) => /// public object? GetService(Type serviceType) => serviceType == typeof(RequestContext) ? request : - serviceType == typeof(McpServer) ? request.Server : +#pragma warning disable CS0618 // Type or member is obsolete + serviceType == typeof(McpServer) || serviceType == typeof(IMcpServer) ? request.Server : +#pragma warning restore CS0618 // Type or member is obsolete serviceType == typeof(IProgress) ? (request.Params?.ProgressToken is { } progressToken ? new TokenProgress(request.Server, progressToken) : NullProgress.Instance) : innerServices?.GetService(serviceType); From 464d3f6bbae995d307e713b9cab24e563db8625f Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 4 Sep 2025 09:11:57 -0700 Subject: [PATCH 11/15] PR feedback --- src/ModelContextProtocol.Core/Client/McpClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs index c5178e333..a93cd5bec 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -98,7 +98,7 @@ public Task PingAsync(CancellationToken cancellationToken = default) { var opts = McpJsonUtilities.DefaultOptions; opts.MakeReadOnly(); - return this.SendRequestAsync( + return SendRequestAsync( RequestMethods.Ping, parameters: null, serializerOptions: opts, From 5546645d273e62b778b02183e0b86ef0b34ce8e2 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Thu, 4 Sep 2025 09:50:23 -0700 Subject: [PATCH 12/15] Add back dispose lock --- src/ModelContextProtocol.Core/Client/McpClientImpl.cs | 10 ++++++++-- src/ModelContextProtocol.Core/Server/McpServerImpl.cs | 9 +++++++-- ...ntExtensionsTest.cs => McpClientExtensionsTests.cs} | 0 3 files changed, 15 insertions(+), 4 deletions(-) rename tests/ModelContextProtocol.Tests/Client/{McpClientExtensionsTest.cs => McpClientExtensionsTests.cs} (100%) diff --git a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs index 4a1e13973..a861de1d9 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Protocol; +using System.Runtime.InteropServices; using System.Text.Json; namespace ModelContextProtocol.Client; @@ -19,6 +20,7 @@ internal sealed partial class McpClientImpl : McpClient private readonly string _endpointName; private readonly McpClientOptions _options; private readonly McpSessionHandler _sessionHandler; + private readonly SemaphoreSlim _disposeLock = new(1, 1); private CancellationTokenSource? _connectCts; @@ -26,7 +28,7 @@ internal sealed partial class McpClientImpl : McpClient private Implementation? _serverInfo; private string? _serverInstructions; - private int _isDisposed; + private bool _disposed; /// /// Initializes a new instance of the class. @@ -215,11 +217,15 @@ public override IAsyncDisposable RegisterNotificationHandler(string method, Func /// public override async ValueTask DisposeAsync() { - if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) + using var _ = await _disposeLock.LockAsync().ConfigureAwait(false); + + if (_disposed) { return; } + _disposed = true; + await _sessionHandler.DisposeAsync().ConfigureAwait(false); await _transport.DisposeAsync().ConfigureAwait(false); } diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 1f83fc086..2d23b30ee 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -23,6 +23,7 @@ internal sealed class McpServerImpl : McpServer private readonly NotificationHandlers _notificationHandlers; private readonly RequestHandlers _requestHandlers; private readonly McpSessionHandler _sessionHandler; + private readonly SemaphoreSlim _disposeLock = new(1, 1); private ClientCapabilities? _clientCapabilities; private Implementation? _clientInfo; @@ -31,7 +32,7 @@ internal sealed class McpServerImpl : McpServer private string _endpointName; private int _started; - private int _isDisposed; + private bool _disposed; /// Holds a boxed value for the server. /// @@ -165,11 +166,15 @@ public override IAsyncDisposable RegisterNotificationHandler(string method, Func /// public override async ValueTask DisposeAsync() { - if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) + using var _ = await _disposeLock.LockAsync().ConfigureAwait(false); + + if (_disposed) { return; } + _disposed = true; + _disposables.ForEach(d => d()); await _sessionHandler.DisposeAsync().ConfigureAwait(false); } diff --git a/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTest.cs b/tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs similarity index 100% rename from tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTest.cs rename to tests/ModelContextProtocol.Tests/Client/McpClientExtensionsTests.cs From 206b3001ed41e4f7b67ea98a71e2f8712fe80016 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 8 Sep 2025 11:17:29 -0700 Subject: [PATCH 13/15] Fix docs projects --- docs/concepts/elicitation/elicitation.md | 10 +++++----- docs/concepts/elicitation/samples/client/Program.cs | 4 ++-- .../samples/server/Tools/InteractiveTools.cs | 2 +- docs/concepts/logging/logging.md | 10 +++++----- docs/concepts/logging/samples/client/Program.cs | 4 ++-- docs/concepts/progress/progress.md | 12 ++++++------ docs/concepts/progress/samples/client/Program.cs | 4 ++-- .../samples/server/Tools/LongRunningTools.cs | 2 +- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index ebda0979a..de5dd37a3 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -11,12 +11,12 @@ The **elicitation** feature allows servers to request additional information fro ### Server Support for Elicitation -Servers request structured data from users with the [ElicitAsync] extension method on [IMcpServer]. -The C# SDK registers an instance of [IMcpServer] with the dependency injection container, -so tools can simply add a parameter of type [IMcpServer] to their method signature to access it. +Servers request structured data from users with the [ElicitAsync] extension method on [McpServer]. +The C# SDK registers an instance of [McpServer] with the dependency injection container, +so tools can simply add a parameter of type [McpServer] to their method signature to access it. -[ElicitAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServerExtensions.html#ModelContextProtocol_Server_McpServerExtensions_ElicitAsync_ModelContextProtocol_Server_IMcpServer_ModelContextProtocol_Protocol_ElicitRequestParams_System_Threading_CancellationToken_ -[IMcpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html +[ElicitAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServerExtensions.html#ModelContextProtocol_Server_McpServerExtensions_ElicitAsync_ModelContextProtocol_Server_McpServer_ModelContextProtocol_Protocol_ElicitRequestParams_System_Threading_CancellationToken_ +[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html The MCP Server must specify the schema of each input value it is requesting from the user. Only primitive types (string, number, boolean) are supported for elicitation requests. diff --git a/docs/concepts/elicitation/samples/client/Program.cs b/docs/concepts/elicitation/samples/client/Program.cs index 5405a61ba..7ffc01212 100644 --- a/docs/concepts/elicitation/samples/client/Program.cs +++ b/docs/concepts/elicitation/samples/client/Program.cs @@ -4,7 +4,7 @@ var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001"; -var clientTransport = new SseClientTransport(new() +var clientTransport = new HttpClientTransport(new() { Endpoint = new Uri(endpoint), TransportMode = HttpTransportMode.StreamableHttp, @@ -27,7 +27,7 @@ } }; -await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport, options); +await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); // var tools = await mcpClient.ListToolsAsync(); diff --git a/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs b/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs index b6a75e005..b907a805d 100644 --- a/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs +++ b/docs/concepts/elicitation/samples/server/Tools/InteractiveTools.cs @@ -13,7 +13,7 @@ public sealed class InteractiveTools // [McpServerTool, Description("A simple game where the user has to guess a number between 1 and 10.")] public async Task GuessTheNumber( - IMcpServer server, // Get the McpServer from DI container + McpServer server, // Get the McpServer from DI container CancellationToken token ) { diff --git a/docs/concepts/logging/logging.md b/docs/concepts/logging/logging.md index 411a61b1c..3dd18d627 100644 --- a/docs/concepts/logging/logging.md +++ b/docs/concepts/logging/logging.md @@ -51,15 +51,15 @@ messages as there may not be an open connection to the client on which the log m The C# SDK provides an extension method [WithSetLoggingLevelHandler] on [IMcpServerBuilder] to allow the server to perform any special logic it wants to perform when a client sets the logging level. However, the -SDK already takes care of setting the [LoggingLevel] in the [IMcpServer], so most servers will not need to +SDK already takes care of setting the [LoggingLevel] in the [McpServer], so most servers will not need to implement this. -[IMcpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html +[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html [IMcpServerBuilder]: https://modelcontextprotocol.github.io/csharp-sdk/api/Microsoft.Extensions.DependencyInjection.IMcpServerBuilder.html [WithSetLoggingLevelHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/Microsoft.Extensions.DependencyInjection.McpServerBuilderExtensions.html#Microsoft_Extensions_DependencyInjection_McpServerBuilderExtensions_WithSetLoggingLevelHandler_Microsoft_Extensions_DependencyInjection_IMcpServerBuilder_System_Func_ModelContextProtocol_Server_RequestContext_ModelContextProtocol_Protocol_SetLevelRequestParams__System_Threading_CancellationToken_System_Threading_Tasks_ValueTask_ModelContextProtocol_Protocol_EmptyResult___ [LoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html#ModelContextProtocol_Server_IMcpServer_LoggingLevel -MCP Servers using the MCP C# SDK can obtain an [ILoggerProvider] from the IMcpServer [AsClientLoggerProvider] extension method, +MCP Servers using the MCP C# SDK can obtain an [ILoggerProvider] from the McpServer [AsClientLoggerProvider] extension method, and from that can create an [ILogger] instance for logging messages that should be sent to the MCP client. [!code-csharp[](samples/server/Tools/LoggingTools.cs?name=snippet_LoggingConfiguration)] @@ -75,14 +75,14 @@ the logging level to specify which messages the server should send to the client Clients should check if the server supports logging by checking the [Logging] property of the [ServerCapabilities] field of [IMcpClient]. -[IMcpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html +[McpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html [ServerCapabilities]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html#ModelContextProtocol_Client_IMcpClient_ServerCapabilities [Logging]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Protocol.ServerCapabilities.html#ModelContextProtocol_Protocol_ServerCapabilities_Logging [!code-csharp[](samples/client/Program.cs?name=snippet_LoggingCapabilities)] If the server supports logging, the client should set the level of log messages it wishes to receive with -the [SetLoggingLevel] method on [IMcpClient]. If the client does not set a logging level, the server might choose +the [SetLoggingLevel] method on [McpClient]. If the client does not set a logging level, the server might choose to send all log messages or none -- this is not specified in the protocol -- so it is important that the client sets a logging level to ensure it receives the desired log messages and only those messages. diff --git a/docs/concepts/logging/samples/client/Program.cs b/docs/concepts/logging/samples/client/Program.cs index b30ca0881..29a15726a 100644 --- a/docs/concepts/logging/samples/client/Program.cs +++ b/docs/concepts/logging/samples/client/Program.cs @@ -4,13 +4,13 @@ var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001"; -var clientTransport = new SseClientTransport(new() +var clientTransport = new HttpClientTransport(new() { Endpoint = new Uri(endpoint), TransportMode = HttpTransportMode.StreamableHttp, }); -await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport); +await using var mcpClient = await McpClient.CreateAsync(clientTransport); // // Verify that the server supports logging diff --git a/docs/concepts/progress/progress.md b/docs/concepts/progress/progress.md index ccdf9f19c..b99e703cb 100644 --- a/docs/concepts/progress/progress.md +++ b/docs/concepts/progress/progress.md @@ -17,14 +17,14 @@ This project illustrates the common case of a server tool that performs a long-r ### Server Implementation -When processing a request, the server can use the [sendNotificationAsync] extension method of [IMcpServer] to send progress updates, +When processing a request, the server can use the [sendNotificationAsync] extension method of [McpServer] to send progress updates, specifying `"notifications/progress"` as the notification method name. -The C# SDK registers an instance of [IMcpServer] with the dependency injection container, -so tools can simply add a parameter of type [IMcpServer] to their method signature to access it. +The C# SDK registers an instance of [McpServer] with the dependency injection container, +so tools can simply add a parameter of type [McpServer] to their method signature to access it. The parameters passed to [sendNotificationAsync] should be an instance of [ProgressNotificationParams], which includes the current progress, total steps, and an optional message. [sendNotificationAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpEndpointExtensions.html#ModelContextProtocol_McpEndpointExtensions_SendNotificationAsync_ModelContextProtocol_IMcpEndpoint_System_String_System_Threading_CancellationToken_ -[IMcpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html +[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html [ProgressNotificationParams]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Protocol.ProgressNotificationParams.html The server must verify that the caller provided a `progressToken` in the request and include it in the call to [sendNotificationAsync]. The following example demonstrates how a server can send a progress notification: @@ -38,9 +38,9 @@ Note that servers are not required to support progress tracking, so clients shou In the MCP C# SDK, clients can specify a `progressToken` in the request parameters when calling a tool method. The client should also provide a notification handler to process "notifications/progress" notifications. -There are two way to do this. The first is to register a notification handler using the [RegisterNotificationHandler] method on the [IMcpClient] instance. A handler registered this way will receive all progress notifications sent by the server. +There are two way to do this. The first is to register a notification handler using the [RegisterNotificationHandler] method on the [McpClient] instance. A handler registered this way will receive all progress notifications sent by the server. -[IMcpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html +[McpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html [RegisterNotificationHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.IMcpEndpoint.html#ModelContextProtocol_IMcpEndpoint_RegisterNotificationHandler_System_String_System_Func_ModelContextProtocol_Protocol_JsonRpcNotification_System_Threading_CancellationToken_System_Threading_Tasks_ValueTask__ ```csharp diff --git a/docs/concepts/progress/samples/client/Program.cs b/docs/concepts/progress/samples/client/Program.cs index 6dde5de9f..2a5f589de 100644 --- a/docs/concepts/progress/samples/client/Program.cs +++ b/docs/concepts/progress/samples/client/Program.cs @@ -5,7 +5,7 @@ var endpoint = Environment.GetEnvironmentVariable("ENDPOINT") ?? "http://localhost:3001"; -var clientTransport = new SseClientTransport(new() +var clientTransport = new HttpClientTransport(new() { Endpoint = new Uri(endpoint), TransportMode = HttpTransportMode.StreamableHttp, @@ -20,7 +20,7 @@ } }; -await using var mcpClient = await McpClientFactory.CreateAsync(clientTransport, options); +await using var mcpClient = await McpClient.CreateAsync(clientTransport, options); var tools = await mcpClient.ListToolsAsync(); foreach (var tool in tools) diff --git a/docs/concepts/progress/samples/server/Tools/LongRunningTools.cs b/docs/concepts/progress/samples/server/Tools/LongRunningTools.cs index ca2a87663..7fcd1244a 100644 --- a/docs/concepts/progress/samples/server/Tools/LongRunningTools.cs +++ b/docs/concepts/progress/samples/server/Tools/LongRunningTools.cs @@ -10,7 +10,7 @@ public class LongRunningTools { [McpServerTool, Description("Demonstrates a long running tool with progress updates")] public static async Task LongRunningTool( - IMcpServer server, + McpServer server, RequestContext context, int duration = 10, int steps = 5) From f5223d48976d523c2b71848a4804177a0912067e Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Mon, 15 Sep 2025 14:17:24 -0700 Subject: [PATCH 14/15] PR feedback --- README.md | 8 +- docs/concepts/elicitation/elicitation.md | 2 +- docs/concepts/logging/logging.md | 12 +- docs/concepts/progress/progress.md | 8 +- .../AuthorizationFilterSetup.cs | 2 +- .../Client/IMcpClient.cs | 2 +- .../Client/McpClient.Methods.cs | 713 ++++++++++++++++++ .../Client/McpClient.cs | 706 +---------------- .../Client/McpClientExtensions.cs | 188 ++--- .../Client/McpClientFactory.cs | 2 +- .../Client/McpClientImpl.cs | 1 - src/ModelContextProtocol.Core/IMcpEndpoint.cs | 2 +- .../McpEndpointExtensions.cs | 9 +- .../McpSession.Methods.cs | 183 +++++ src/ModelContextProtocol.Core/McpSession.cs | 177 +---- .../Protocol/JsonRpcMessageContext.cs | 2 +- .../Server/IMcpServer.cs | 2 +- .../Server/McpServer.Methods.cs | 365 +++++++++ .../Server/McpServer.cs | 356 +-------- .../Server/McpServerExtensions.cs | 12 +- .../Server/McpServerFactory.cs | 2 +- 21 files changed, 1391 insertions(+), 1363 deletions(-) create mode 100644 src/ModelContextProtocol.Core/Client/McpClient.Methods.cs create mode 100644 src/ModelContextProtocol.Core/McpSession.Methods.cs create mode 100644 src/ModelContextProtocol.Core/Server/McpServer.Methods.cs diff --git a/README.md b/README.md index e4e6cd9d4..550199676 100644 --- a/README.md +++ b/README.md @@ -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 -`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. ```csharp [McpServerTool(Name = "SummarizeContentFromUrl"), Description("Summarizes content downloaded from a specific URI")] public static async Task SummarizeDownloadedContent( - IMcpServer thisServer, + McpServer thisServer, HttpClient httpClient, [Description("The url from which to download the content to summarize")] string url, CancellationToken cancellationToken) @@ -224,7 +224,7 @@ McpServerOptions options = new() }, }; -await using IMcpServer server = McpServer.Create(new StdioServerTransport("MyServer"), options); +await using McpServer server = McpServer.Create(new StdioServerTransport("MyServer"), options); await server.RunAsync(); ``` diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index 96038c740..522cfc7b5 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -16,7 +16,7 @@ The C# SDK registers an instance of [McpServer] with the dependency injection co so tools can simply add a parameter of type [McpServer] to their method signature to access it. [ElicitAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServerExtensions.html#ModelContextProtocol_Server_McpServerExtensions_ElicitAsync_ModelContextProtocol_Server_IMcpServer_ModelContextProtocol_Protocol_ElicitRequestParams_System_Threading_CancellationToken_ -[IMcpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html +[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html The MCP Server must specify the schema of each input value it is requesting from the user. Only primitive types (string, number, boolean) are supported for elicitation requests. diff --git a/docs/concepts/logging/logging.md b/docs/concepts/logging/logging.md index 3dd18d627..34ed436d9 100644 --- a/docs/concepts/logging/logging.md +++ b/docs/concepts/logging/logging.md @@ -54,10 +54,10 @@ server to perform any special logic it wants to perform when a client sets the l SDK already takes care of setting the [LoggingLevel] in the [McpServer], so most servers will not need to implement this. -[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html +[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html [IMcpServerBuilder]: https://modelcontextprotocol.github.io/csharp-sdk/api/Microsoft.Extensions.DependencyInjection.IMcpServerBuilder.html [WithSetLoggingLevelHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/Microsoft.Extensions.DependencyInjection.McpServerBuilderExtensions.html#Microsoft_Extensions_DependencyInjection_McpServerBuilderExtensions_WithSetLoggingLevelHandler_Microsoft_Extensions_DependencyInjection_IMcpServerBuilder_System_Func_ModelContextProtocol_Server_RequestContext_ModelContextProtocol_Protocol_SetLevelRequestParams__System_Threading_CancellationToken_System_Threading_Tasks_ValueTask_ModelContextProtocol_Protocol_EmptyResult___ -[LoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html#ModelContextProtocol_Server_IMcpServer_LoggingLevel +[LoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html#ModelContextProtocol_Server_IMcpServer_LoggingLevel MCP Servers using the MCP C# SDK can obtain an [ILoggerProvider] from the McpServer [AsClientLoggerProvider] extension method, and from that can create an [ILogger] instance for logging messages that should be sent to the MCP client. @@ -73,10 +73,10 @@ and from that can create an [ILogger] instance for logging messages that should When the server indicates that it supports logging, clients should configure the logging level to specify which messages the server should send to the client. -Clients should check if the server supports logging by checking the [Logging] property of the [ServerCapabilities] field of [IMcpClient]. +Clients should check if the server supports logging by checking the [Logging] property of the [ServerCapabilities] field of [McpClient]. -[McpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html -[ServerCapabilities]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html#ModelContextProtocol_Client_IMcpClient_ServerCapabilities +[McpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClient.html +[ServerCapabilities]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClient.html#ModelContextProtocol_Client_McpClient_ServerCapabilities [Logging]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Protocol.ServerCapabilities.html#ModelContextProtocol_Protocol_ServerCapabilities_Logging [!code-csharp[](samples/client/Program.cs?name=snippet_LoggingCapabilities)] @@ -89,7 +89,7 @@ sets a logging level to ensure it receives the desired log messages and only tho The `loggingLevel` set by the client is an MCP logging level. See the [Logging Levels](#logging-levels) section above for the mapping between MCP and .NET logging levels. -[SetLoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClientExtensions.html#ModelContextProtocol_Client_McpClientExtensions_SetLoggingLevel_ModelContextProtocol_Client_IMcpClient_Microsoft_Extensions_Logging_LogLevel_System_Threading_CancellationToken_ +[SetLoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClientExtensions.html#ModelContextProtocol_Client_McpClientExtensions_SetLoggingLevel_ModelContextProtocol_Client_McpClient_Microsoft_Extensions_Logging_LogLevel_System_Threading_CancellationToken_ [!code-csharp[](samples/client/Program.cs?name=snippet_LoggingLevel)] diff --git a/docs/concepts/progress/progress.md b/docs/concepts/progress/progress.md index b99e703cb..223146bc0 100644 --- a/docs/concepts/progress/progress.md +++ b/docs/concepts/progress/progress.md @@ -23,8 +23,8 @@ The C# SDK registers an instance of [McpServer] with the dependency injection co so tools can simply add a parameter of type [McpServer] to their method signature to access it. The parameters passed to [sendNotificationAsync] should be an instance of [ProgressNotificationParams], which includes the current progress, total steps, and an optional message. -[sendNotificationAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpEndpointExtensions.html#ModelContextProtocol_McpEndpointExtensions_SendNotificationAsync_ModelContextProtocol_IMcpEndpoint_System_String_System_Threading_CancellationToken_ -[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html +[sendNotificationAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpEndpointExtensions.html#ModelContextProtocol_McpEndpointExtensions_SendNotificationAsync_ModelContextProtocol_McpSession_System_String_System_Threading_CancellationToken_ +[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html [ProgressNotificationParams]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Protocol.ProgressNotificationParams.html The server must verify that the caller provided a `progressToken` in the request and include it in the call to [sendNotificationAsync]. The following example demonstrates how a server can send a progress notification: @@ -40,8 +40,8 @@ In the MCP C# SDK, clients can specify a `progressToken` in the request paramete The client should also provide a notification handler to process "notifications/progress" notifications. There are two way to do this. The first is to register a notification handler using the [RegisterNotificationHandler] method on the [McpClient] instance. A handler registered this way will receive all progress notifications sent by the server. -[McpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html -[RegisterNotificationHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.IMcpEndpoint.html#ModelContextProtocol_IMcpEndpoint_RegisterNotificationHandler_System_String_System_Func_ModelContextProtocol_Protocol_JsonRpcNotification_System_Threading_CancellationToken_System_Threading_Tasks_ValueTask__ +[McpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClient.html +[RegisterNotificationHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpSession.html#ModelContextProtocol_McpSession_RegisterNotificationHandler_System_String_System_Func_ModelContextProtocol_Protocol_JsonRpcNotification_System_Threading_CancellationToken_System_Threading_Tasks_ValueTask__ ```csharp mcpClient.RegisterNotificationHandler(NotificationMethods.ProgressNotification, diff --git a/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs b/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs index bd0ceabeb..2cfb74d09 100644 --- a/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs +++ b/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs @@ -292,7 +292,7 @@ private async ValueTask GetAuthorizationResultAsync( 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. - 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. diff --git a/src/ModelContextProtocol.Core/Client/IMcpClient.cs b/src/ModelContextProtocol.Core/Client/IMcpClient.cs index dad8f2823..43930c030 100644 --- a/src/ModelContextProtocol.Core/Client/IMcpClient.cs +++ b/src/ModelContextProtocol.Core/Client/IMcpClient.cs @@ -5,7 +5,7 @@ namespace ModelContextProtocol.Client; /// /// Represents an instance of a Model Context Protocol (MCP) client that connects to and communicates with an MCP server. /// -[Obsolete($"Use {nameof(McpClient)} instead.")] +[Obsolete($"Use {nameof(McpClient)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public interface IMcpClient : IMcpEndpoint { /// diff --git a/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs new file mode 100644 index 000000000..560ce31dc --- /dev/null +++ b/src/ModelContextProtocol.Core/Client/McpClient.Methods.cs @@ -0,0 +1,713 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace ModelContextProtocol.Client; + +/// +/// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server. +/// +#pragma warning disable CS0618 // Type or member is obsolete +public abstract partial class McpClient : McpSession, IMcpClient +#pragma warning restore CS0618 // Type or member is obsolete +{ + /// Creates an , connecting it to the specified server. + /// The transport instance used to communicate with the server. + /// + /// A client configuration object which specifies client capabilities and protocol version. + /// If , details based on the current process will be employed. + /// + /// A logger factory for creating loggers for clients. + /// The to monitor for cancellation requests. The default is . + /// An that's connected to the specified server. + /// is . + /// is . + public static async Task CreateAsync( + IClientTransport clientTransport, + McpClientOptions? clientOptions = null, + ILoggerFactory? loggerFactory = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(clientTransport); + + var transport = await clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false); + var endpointName = clientTransport.Name; + + var clientSession = new McpClientImpl(transport, endpointName, clientOptions, loggerFactory); + try + { + await clientSession.ConnectAsync(cancellationToken).ConfigureAwait(false); + } + catch + { + await clientSession.DisposeAsync().ConfigureAwait(false); + throw; + } + + return clientSession; + } + + /// + /// Sends a ping request to verify server connectivity. + /// + /// The to monitor for cancellation requests. The default is . + /// A task that completes when the ping is successful. + /// Thrown when the server cannot be reached or returns an error response. + public Task PingAsync(CancellationToken cancellationToken = default) + { + var opts = McpJsonUtilities.DefaultOptions; + opts.MakeReadOnly(); + return SendRequestAsync( + RequestMethods.Ping, + parameters: null, + serializerOptions: opts, + cancellationToken: cancellationToken).AsTask(); + } + + /// + /// Retrieves a list of available tools from the server. + /// + /// The serializer options governing tool parameter serialization. If null, the default options will be used. + /// The to monitor for cancellation requests. The default is . + /// A list of all available tools as instances. + public async ValueTask> ListToolsAsync( + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + List? tools = null; + string? cursor = null; + do + { + var toolResults = await SendRequestAsync( + RequestMethods.ToolsList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, + McpJsonUtilities.JsonContext.Default.ListToolsResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + tools ??= new List(toolResults.Tools.Count); + foreach (var tool in toolResults.Tools) + { + tools.Add(new McpClientTool(this, tool, serializerOptions)); + } + + cursor = toolResults.NextCursor; + } + while (cursor is not null); + + return tools; + } + + /// + /// Creates an enumerable for asynchronously enumerating all available tools from the server. + /// + /// The serializer options governing tool parameter serialization. If null, the default options will be used. + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available tools as instances. + public async IAsyncEnumerable EnumerateToolsAsync( + JsonSerializerOptions? serializerOptions = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + string? cursor = null; + do + { + var toolResults = await SendRequestAsync( + RequestMethods.ToolsList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, + McpJsonUtilities.JsonContext.Default.ListToolsResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + foreach (var tool in toolResults.Tools) + { + yield return new McpClientTool(this, tool, serializerOptions); + } + + cursor = toolResults.NextCursor; + } + while (cursor is not null); + } + + /// + /// Retrieves a list of available prompts from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// A list of all available prompts as instances. + public async ValueTask> ListPromptsAsync( + CancellationToken cancellationToken = default) + { + List? prompts = null; + string? cursor = null; + do + { + var promptResults = await SendRequestAsync( + RequestMethods.PromptsList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, + McpJsonUtilities.JsonContext.Default.ListPromptsResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + prompts ??= new List(promptResults.Prompts.Count); + foreach (var prompt in promptResults.Prompts) + { + prompts.Add(new McpClientPrompt(this, prompt)); + } + + cursor = promptResults.NextCursor; + } + while (cursor is not null); + + return prompts; + } + + /// + /// Creates an enumerable for asynchronously enumerating all available prompts from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available prompts as instances. + public async IAsyncEnumerable EnumeratePromptsAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + string? cursor = null; + do + { + var promptResults = await SendRequestAsync( + RequestMethods.PromptsList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, + McpJsonUtilities.JsonContext.Default.ListPromptsResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + foreach (var prompt in promptResults.Prompts) + { + yield return new(this, prompt); + } + + cursor = promptResults.NextCursor; + } + while (cursor is not null); + } + + /// + /// Retrieves a specific prompt from the MCP server. + /// + /// The name of the prompt to retrieve. + /// Optional arguments for the prompt. Keys are parameter names, and values are the argument values. + /// The serialization options governing argument serialization. + /// The to monitor for cancellation requests. The default is . + /// A task containing the prompt's result with content and messages. + public ValueTask GetPromptAsync( + string name, + IReadOnlyDictionary? arguments = null, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(name); + + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + return SendRequestAsync( + RequestMethods.PromptsGet, + new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions) }, + McpJsonUtilities.JsonContext.Default.GetPromptRequestParams, + McpJsonUtilities.JsonContext.Default.GetPromptResult, + cancellationToken: cancellationToken); + } + + /// + /// Retrieves a list of available resource templates from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// A list of all available resource templates as instances. + public async ValueTask> ListResourceTemplatesAsync( + CancellationToken cancellationToken = default) + { + List? resourceTemplates = null; + + string? cursor = null; + do + { + var templateResults = await SendRequestAsync( + RequestMethods.ResourcesTemplatesList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + resourceTemplates ??= new List(templateResults.ResourceTemplates.Count); + foreach (var template in templateResults.ResourceTemplates) + { + resourceTemplates.Add(new McpClientResourceTemplate(this, template)); + } + + cursor = templateResults.NextCursor; + } + while (cursor is not null); + + return resourceTemplates; + } + + /// + /// Creates an enumerable for asynchronously enumerating all available resource templates from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available resource templates as instances. + public async IAsyncEnumerable EnumerateResourceTemplatesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + string? cursor = null; + do + { + var templateResults = await SendRequestAsync( + RequestMethods.ResourcesTemplatesList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + foreach (var templateResult in templateResults.ResourceTemplates) + { + yield return new McpClientResourceTemplate(this, templateResult); + } + + cursor = templateResults.NextCursor; + } + while (cursor is not null); + } + + /// + /// Retrieves a list of available resources from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// A list of all available resources as instances. + public async ValueTask> ListResourcesAsync( + CancellationToken cancellationToken = default) + { + List? resources = null; + + string? cursor = null; + do + { + var resourceResults = await SendRequestAsync( + RequestMethods.ResourcesList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourcesResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + resources ??= new List(resourceResults.Resources.Count); + foreach (var resource in resourceResults.Resources) + { + resources.Add(new McpClientResource(this, resource)); + } + + cursor = resourceResults.NextCursor; + } + while (cursor is not null); + + return resources; + } + + /// + /// Creates an enumerable for asynchronously enumerating all available resources from the server. + /// + /// The to monitor for cancellation requests. The default is . + /// An asynchronous sequence of all available resources as instances. + public async IAsyncEnumerable EnumerateResourcesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + string? cursor = null; + do + { + var resourceResults = await SendRequestAsync( + RequestMethods.ResourcesList, + new() { Cursor = cursor }, + McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, + McpJsonUtilities.JsonContext.Default.ListResourcesResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + + foreach (var resource in resourceResults.Resources) + { + yield return new McpClientResource(this, resource); + } + + cursor = resourceResults.NextCursor; + } + while (cursor is not null); + } + + /// + /// Reads a resource from the server. + /// + /// The uri of the resource. + /// The to monitor for cancellation requests. The default is . + public ValueTask ReadResourceAsync( + string uri, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(uri); + + return SendRequestAsync( + RequestMethods.ResourcesRead, + new() { Uri = uri }, + McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, + McpJsonUtilities.JsonContext.Default.ReadResourceResult, + cancellationToken: cancellationToken); + } + + /// + /// Reads a resource from the server. + /// + /// The uri of the resource. + /// The to monitor for cancellation requests. The default is . + public ValueTask ReadResourceAsync( + Uri uri, CancellationToken cancellationToken = default) + { + Throw.IfNull(uri); + + return ReadResourceAsync(uri.ToString(), cancellationToken); + } + + /// + /// Reads a resource from the server. + /// + /// The uri template of the resource. + /// Arguments to use to format . + /// The to monitor for cancellation requests. The default is . + public ValueTask ReadResourceAsync( + string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(uriTemplate); + Throw.IfNull(arguments); + + return SendRequestAsync( + RequestMethods.ResourcesRead, + new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) }, + McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, + McpJsonUtilities.JsonContext.Default.ReadResourceResult, + cancellationToken: cancellationToken); + } + + /// + /// Requests completion suggestions for a prompt argument or resource reference. + /// + /// The reference object specifying the type and optional URI or name. + /// The name of the argument for which completions are requested. + /// The current value of the argument, used to filter relevant completions. + /// The to monitor for cancellation requests. The default is . + /// A containing completion suggestions. + public ValueTask CompleteAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) + { + Throw.IfNull(reference); + Throw.IfNullOrWhiteSpace(argumentName); + + return SendRequestAsync( + RequestMethods.CompletionComplete, + new() + { + Ref = reference, + Argument = new Argument { Name = argumentName, Value = argumentValue } + }, + McpJsonUtilities.JsonContext.Default.CompleteRequestParams, + McpJsonUtilities.JsonContext.Default.CompleteResult, + cancellationToken: cancellationToken); + } + + /// + /// Subscribes to a resource on the server to receive notifications when it changes. + /// + /// The URI of the resource to which to subscribe. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + public Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(uri); + + return SendRequestAsync( + RequestMethods.ResourcesSubscribe, + new() { Uri = uri }, + McpJsonUtilities.JsonContext.Default.SubscribeRequestParams, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken).AsTask(); + } + + /// + /// Subscribes to a resource on the server to receive notifications when it changes. + /// + /// The URI of the resource to which to subscribe. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + public Task SubscribeToResourceAsync(Uri uri, CancellationToken cancellationToken = default) + { + Throw.IfNull(uri); + + return SubscribeToResourceAsync(uri.ToString(), cancellationToken); + } + + /// + /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. + /// + /// The URI of the resource to unsubscribe from. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + public Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(uri); + + return SendRequestAsync( + RequestMethods.ResourcesUnsubscribe, + new() { Uri = uri }, + McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken).AsTask(); + } + + /// + /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. + /// + /// The URI of the resource to unsubscribe from. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. + public Task UnsubscribeFromResourceAsync(Uri uri, CancellationToken cancellationToken = default) + { + Throw.IfNull(uri); + + return UnsubscribeFromResourceAsync(uri.ToString(), cancellationToken); + } + + /// + /// Invokes a tool on the server. + /// + /// The name of the tool to call on the server.. + /// An optional dictionary of arguments to pass to the tool. + /// Optional progress reporter for server notifications. + /// JSON serializer options. + /// A cancellation token. + /// The from the tool execution. + public ValueTask CallToolAsync( + string toolName, + IReadOnlyDictionary? arguments = null, + IProgress? progress = null, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + Throw.IfNull(toolName); + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + if (progress is not null) + { + return SendRequestWithProgressAsync(toolName, arguments, progress, serializerOptions, cancellationToken); + } + + return SendRequestAsync( + RequestMethods.ToolsCall, + new() + { + Name = toolName, + Arguments = ToArgumentsDictionary(arguments, serializerOptions), + }, + McpJsonUtilities.JsonContext.Default.CallToolRequestParams, + McpJsonUtilities.JsonContext.Default.CallToolResult, + cancellationToken: cancellationToken); + + async ValueTask SendRequestWithProgressAsync( + string toolName, + IReadOnlyDictionary? arguments, + IProgress progress, + JsonSerializerOptions serializerOptions, + CancellationToken cancellationToken) + { + ProgressToken progressToken = new(Guid.NewGuid().ToString("N")); + + await using var _ = RegisterNotificationHandler(NotificationMethods.ProgressNotification, + (notification, cancellationToken) => + { + if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn && + pn.ProgressToken == progressToken) + { + progress.Report(pn.Progress); + } + + return default; + }).ConfigureAwait(false); + + return await SendRequestAsync( + RequestMethods.ToolsCall, + new() + { + Name = toolName, + Arguments = ToArgumentsDictionary(arguments, serializerOptions), + ProgressToken = progressToken, + }, + McpJsonUtilities.JsonContext.Default.CallToolRequestParams, + McpJsonUtilities.JsonContext.Default.CallToolResult, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Converts the contents of a into a pair of + /// and instances to use + /// as inputs into a operation. + /// + /// + /// The created pair of messages and options. + /// is . + internal static (IList Messages, ChatOptions? Options) ToChatClientArguments( + CreateMessageRequestParams requestParams) + { + Throw.IfNull(requestParams); + + ChatOptions? options = null; + + if (requestParams.MaxTokens is int maxTokens) + { + (options ??= new()).MaxOutputTokens = maxTokens; + } + + if (requestParams.Temperature is float temperature) + { + (options ??= new()).Temperature = temperature; + } + + if (requestParams.StopSequences is { } stopSequences) + { + (options ??= new()).StopSequences = stopSequences.ToArray(); + } + + List messages = + (from sm in requestParams.Messages + let aiContent = sm.Content.ToAIContent() + where aiContent is not null + select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) + .ToList(); + + return (messages, options); + } + + /// Converts the contents of a into a . + /// The whose contents should be extracted. + /// The created . + /// is . + internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatResponse) + { + Throw.IfNull(chatResponse); + + // The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports + // only either a single blob of text or a single image. Heuristically, we'll use an image if there is one + // in any of the response messages, or we'll use all the text from them concatenated, otherwise. + + ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); + + ContentBlock? content = null; + if (lastMessage is not null) + { + foreach (var lmc in lastMessage.Contents) + { + if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) + { + content = dc.ToContent(); + } + } + } + + return new() + { + Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, + Model = chatResponse.ModelId ?? "unknown", + Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, + StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", + }; + } + + /// + /// Creates a sampling handler for use with that will + /// satisfy sampling requests using the specified . + /// + /// The with which to satisfy sampling requests. + /// The created handler delegate that can be assigned to . + /// is . + public static Func, CancellationToken, ValueTask> CreateSamplingHandler( + IChatClient chatClient) + { + Throw.IfNull(chatClient); + + return async (requestParams, progress, cancellationToken) => + { + Throw.IfNull(requestParams); + + var (messages, options) = ToChatClientArguments(requestParams); + var progressToken = requestParams.ProgressToken; + + List updates = []; + await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) + { + updates.Add(update); + + if (progressToken is not null) + { + progress.Report(new() + { + Progress = updates.Count, + }); + } + } + + return ToCreateMessageResult(updates.ToChatResponse()); + }; + } + + /// + /// Sets the logging level for the server to control which log messages are sent to the client. + /// + /// The minimum severity level of log messages to receive from the server. + /// The to monitor for cancellation requests. The default is . + /// A task representing the asynchronous operation. + public Task SetLoggingLevel(LoggingLevel level, CancellationToken cancellationToken = default) + { + return SendRequestAsync( + RequestMethods.LoggingSetLevel, + new() { Level = level }, + McpJsonUtilities.JsonContext.Default.SetLevelRequestParams, + McpJsonUtilities.JsonContext.Default.EmptyResult, + cancellationToken: cancellationToken).AsTask(); + } + + /// + /// Sets the logging level for the server to control which log messages are sent to the client. + /// + /// The minimum severity level of log messages to receive from the server. + /// The to monitor for cancellation requests. The default is . + /// A task representing the asynchronous operation. + public Task SetLoggingLevel(LogLevel level, CancellationToken cancellationToken = default) => + SetLoggingLevel(McpServerImpl.ToLoggingLevel(level), cancellationToken); + + /// Convers a dictionary with values to a dictionary with values. + private static Dictionary? ToArgumentsDictionary( + IReadOnlyDictionary? arguments, JsonSerializerOptions options) + { + var typeInfo = options.GetTypeInfo(); + + Dictionary? result = null; + if (arguments is not null) + { + result = new(arguments.Count); + foreach (var kvp in arguments) + { + result.Add(kvp.Key, kvp.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(kvp.Value, typeInfo)); + } + } + + return result; + } +} diff --git a/src/ModelContextProtocol.Core/Client/McpClient.cs b/src/ModelContextProtocol.Core/Client/McpClient.cs index a93cd5bec..c4abe33b7 100644 --- a/src/ModelContextProtocol.Core/Client/McpClient.cs +++ b/src/ModelContextProtocol.Core/Client/McpClient.cs @@ -1,9 +1,4 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; -using ModelContextProtocol.Protocol; -using ModelContextProtocol.Server; -using System.Runtime.CompilerServices; -using System.Text.Json; +using ModelContextProtocol.Protocol; namespace ModelContextProtocol.Client; @@ -11,7 +6,7 @@ namespace ModelContextProtocol.Client; /// Represents an instance of a Model Context Protocol (MCP) client session that connects to and communicates with an MCP server. /// #pragma warning disable CS0618 // Type or member is obsolete -public abstract class McpClient : McpSession, IMcpClient +public abstract partial class McpClient : McpSession, IMcpClient #pragma warning restore CS0618 // Type or member is obsolete { /// @@ -51,701 +46,4 @@ public abstract class McpClient : McpSession, IMcpClient /// /// public abstract string? ServerInstructions { get; } - - /// Creates an , connecting it to the specified server. - /// The transport instance used to communicate with the server. - /// - /// A client configuration object which specifies client capabilities and protocol version. - /// If , details based on the current process will be employed. - /// - /// A logger factory for creating loggers for clients. - /// The to monitor for cancellation requests. The default is . - /// An that's connected to the specified server. - /// is . - /// is . - public static async Task CreateAsync( - IClientTransport clientTransport, - McpClientOptions? clientOptions = null, - ILoggerFactory? loggerFactory = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(clientTransport); - - var transport = await clientTransport.ConnectAsync(cancellationToken).ConfigureAwait(false); - var endpointName = clientTransport.Name; - - var clientSession = new McpClientImpl(transport, endpointName, clientOptions, loggerFactory); - try - { - await clientSession.ConnectAsync(cancellationToken).ConfigureAwait(false); - } - catch - { - await clientSession.DisposeAsync().ConfigureAwait(false); - throw; - } - - return clientSession; - } - - /// - /// Sends a ping request to verify server connectivity. - /// - /// The to monitor for cancellation requests. The default is . - /// A task that completes when the ping is successful. - /// Thrown when the server cannot be reached or returns an error response. - public Task PingAsync(CancellationToken cancellationToken = default) - { - var opts = McpJsonUtilities.DefaultOptions; - opts.MakeReadOnly(); - return SendRequestAsync( - RequestMethods.Ping, - parameters: null, - serializerOptions: opts, - cancellationToken: cancellationToken).AsTask(); - } - - /// - /// Retrieves a list of available tools from the server. - /// - /// The serializer options governing tool parameter serialization. If null, the default options will be used. - /// The to monitor for cancellation requests. The default is . - /// A list of all available tools as instances. - public async ValueTask> ListToolsAsync( - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - List? tools = null; - string? cursor = null; - do - { - var toolResults = await SendRequestAsync( - RequestMethods.ToolsList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, - McpJsonUtilities.JsonContext.Default.ListToolsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - tools ??= new List(toolResults.Tools.Count); - foreach (var tool in toolResults.Tools) - { - tools.Add(new McpClientTool(this, tool, serializerOptions)); - } - - cursor = toolResults.NextCursor; - } - while (cursor is not null); - - return tools; - } - - /// - /// Creates an enumerable for asynchronously enumerating all available tools from the server. - /// - /// The serializer options governing tool parameter serialization. If null, the default options will be used. - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available tools as instances. - public async IAsyncEnumerable EnumerateToolsAsync( - JsonSerializerOptions? serializerOptions = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - string? cursor = null; - do - { - var toolResults = await SendRequestAsync( - RequestMethods.ToolsList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListToolsRequestParams, - McpJsonUtilities.JsonContext.Default.ListToolsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var tool in toolResults.Tools) - { - yield return new McpClientTool(this, tool, serializerOptions); - } - - cursor = toolResults.NextCursor; - } - while (cursor is not null); - } - - /// - /// Retrieves a list of available prompts from the server. - /// - /// The to monitor for cancellation requests. The default is . - /// A list of all available prompts as instances. - public async ValueTask> ListPromptsAsync( - CancellationToken cancellationToken = default) - { - List? prompts = null; - string? cursor = null; - do - { - var promptResults = await SendRequestAsync( - RequestMethods.PromptsList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, - McpJsonUtilities.JsonContext.Default.ListPromptsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - prompts ??= new List(promptResults.Prompts.Count); - foreach (var prompt in promptResults.Prompts) - { - prompts.Add(new McpClientPrompt(this, prompt)); - } - - cursor = promptResults.NextCursor; - } - while (cursor is not null); - - return prompts; - } - - /// - /// Creates an enumerable for asynchronously enumerating all available prompts from the server. - /// - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available prompts as instances. - public async IAsyncEnumerable EnumeratePromptsAsync( - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - string? cursor = null; - do - { - var promptResults = await SendRequestAsync( - RequestMethods.PromptsList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListPromptsRequestParams, - McpJsonUtilities.JsonContext.Default.ListPromptsResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var prompt in promptResults.Prompts) - { - yield return new(this, prompt); - } - - cursor = promptResults.NextCursor; - } - while (cursor is not null); - } - - /// - /// Retrieves a specific prompt from the MCP server. - /// - /// The name of the prompt to retrieve. - /// Optional arguments for the prompt. Keys are parameter names, and values are the argument values. - /// The serialization options governing argument serialization. - /// The to monitor for cancellation requests. The default is . - /// A task containing the prompt's result with content and messages. - public ValueTask GetPromptAsync( - string name, - IReadOnlyDictionary? arguments = null, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNullOrWhiteSpace(name); - - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - return SendRequestAsync( - RequestMethods.PromptsGet, - new() { Name = name, Arguments = ToArgumentsDictionary(arguments, serializerOptions) }, - McpJsonUtilities.JsonContext.Default.GetPromptRequestParams, - McpJsonUtilities.JsonContext.Default.GetPromptResult, - cancellationToken: cancellationToken); - } - - /// - /// Retrieves a list of available resource templates from the server. - /// - /// The to monitor for cancellation requests. The default is . - /// A list of all available resource templates as instances. - public async ValueTask> ListResourceTemplatesAsync( - CancellationToken cancellationToken = default) - { - List? resourceTemplates = null; - - string? cursor = null; - do - { - var templateResults = await SendRequestAsync( - RequestMethods.ResourcesTemplatesList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - resourceTemplates ??= new List(templateResults.ResourceTemplates.Count); - foreach (var template in templateResults.ResourceTemplates) - { - resourceTemplates.Add(new McpClientResourceTemplate(this, template)); - } - - cursor = templateResults.NextCursor; - } - while (cursor is not null); - - return resourceTemplates; - } - - /// - /// Creates an enumerable for asynchronously enumerating all available resource templates from the server. - /// - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available resource templates as instances. - public async IAsyncEnumerable EnumerateResourceTemplatesAsync( - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - string? cursor = null; - do - { - var templateResults = await SendRequestAsync( - RequestMethods.ResourcesTemplatesList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourceTemplatesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var templateResult in templateResults.ResourceTemplates) - { - yield return new McpClientResourceTemplate(this, templateResult); - } - - cursor = templateResults.NextCursor; - } - while (cursor is not null); - } - - /// - /// Retrieves a list of available resources from the server. - /// - /// The to monitor for cancellation requests. The default is . - /// A list of all available resources as instances. - public async ValueTask> ListResourcesAsync( - CancellationToken cancellationToken = default) - { - List? resources = null; - - string? cursor = null; - do - { - var resourceResults = await SendRequestAsync( - RequestMethods.ResourcesList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourcesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - resources ??= new List(resourceResults.Resources.Count); - foreach (var resource in resourceResults.Resources) - { - resources.Add(new McpClientResource(this, resource)); - } - - cursor = resourceResults.NextCursor; - } - while (cursor is not null); - - return resources; - } - - /// - /// Creates an enumerable for asynchronously enumerating all available resources from the server. - /// - /// The to monitor for cancellation requests. The default is . - /// An asynchronous sequence of all available resources as instances. - public async IAsyncEnumerable EnumerateResourcesAsync( - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - string? cursor = null; - do - { - var resourceResults = await SendRequestAsync( - RequestMethods.ResourcesList, - new() { Cursor = cursor }, - McpJsonUtilities.JsonContext.Default.ListResourcesRequestParams, - McpJsonUtilities.JsonContext.Default.ListResourcesResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - - foreach (var resource in resourceResults.Resources) - { - yield return new McpClientResource(this, resource); - } - - cursor = resourceResults.NextCursor; - } - while (cursor is not null); - } - - /// - /// Reads a resource from the server. - /// - /// The uri of the resource. - /// The to monitor for cancellation requests. The default is . - public ValueTask ReadResourceAsync( - string uri, CancellationToken cancellationToken = default) - { - Throw.IfNullOrWhiteSpace(uri); - - return SendRequestAsync( - RequestMethods.ResourcesRead, - new() { Uri = uri }, - McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, - McpJsonUtilities.JsonContext.Default.ReadResourceResult, - cancellationToken: cancellationToken); - } - - /// - /// Reads a resource from the server. - /// - /// The uri of the resource. - /// The to monitor for cancellation requests. The default is . - public ValueTask ReadResourceAsync( - Uri uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(uri); - - return ReadResourceAsync(uri.ToString(), cancellationToken); - } - - /// - /// Reads a resource from the server. - /// - /// The uri template of the resource. - /// Arguments to use to format . - /// The to monitor for cancellation requests. The default is . - public ValueTask ReadResourceAsync( - string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) - { - Throw.IfNullOrWhiteSpace(uriTemplate); - Throw.IfNull(arguments); - - return SendRequestAsync( - RequestMethods.ResourcesRead, - new() { Uri = UriTemplate.FormatUri(uriTemplate, arguments) }, - McpJsonUtilities.JsonContext.Default.ReadResourceRequestParams, - McpJsonUtilities.JsonContext.Default.ReadResourceResult, - cancellationToken: cancellationToken); - } - - /// - /// Requests completion suggestions for a prompt argument or resource reference. - /// - /// The reference object specifying the type and optional URI or name. - /// The name of the argument for which completions are requested. - /// The current value of the argument, used to filter relevant completions. - /// The to monitor for cancellation requests. The default is . - /// A containing completion suggestions. - public ValueTask CompleteAsync(Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) - { - Throw.IfNull(reference); - Throw.IfNullOrWhiteSpace(argumentName); - - return SendRequestAsync( - RequestMethods.CompletionComplete, - new() - { - Ref = reference, - Argument = new Argument { Name = argumentName, Value = argumentValue } - }, - McpJsonUtilities.JsonContext.Default.CompleteRequestParams, - McpJsonUtilities.JsonContext.Default.CompleteResult, - cancellationToken: cancellationToken); - } - - /// - /// Subscribes to a resource on the server to receive notifications when it changes. - /// - /// The URI of the resource to which to subscribe. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - public Task SubscribeToResourceAsync(string uri, CancellationToken cancellationToken = default) - { - Throw.IfNullOrWhiteSpace(uri); - - return SendRequestAsync( - RequestMethods.ResourcesSubscribe, - new() { Uri = uri }, - McpJsonUtilities.JsonContext.Default.SubscribeRequestParams, - McpJsonUtilities.JsonContext.Default.EmptyResult, - cancellationToken: cancellationToken).AsTask(); - } - - /// - /// Subscribes to a resource on the server to receive notifications when it changes. - /// - /// The URI of the resource to which to subscribe. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - public Task SubscribeToResourceAsync(Uri uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(uri); - - return SubscribeToResourceAsync(uri.ToString(), cancellationToken); - } - - /// - /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. - /// - /// The URI of the resource to unsubscribe from. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - public Task UnsubscribeFromResourceAsync(string uri, CancellationToken cancellationToken = default) - { - Throw.IfNullOrWhiteSpace(uri); - - return SendRequestAsync( - RequestMethods.ResourcesUnsubscribe, - new() { Uri = uri }, - McpJsonUtilities.JsonContext.Default.UnsubscribeRequestParams, - McpJsonUtilities.JsonContext.Default.EmptyResult, - cancellationToken: cancellationToken).AsTask(); - } - - /// - /// Unsubscribes from a resource on the server to stop receiving notifications about its changes. - /// - /// The URI of the resource to unsubscribe from. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. - public Task UnsubscribeFromResourceAsync(Uri uri, CancellationToken cancellationToken = default) - { - Throw.IfNull(uri); - - return UnsubscribeFromResourceAsync(uri.ToString(), cancellationToken); - } - - /// - /// Invokes a tool on the server. - /// - /// The name of the tool to call on the server.. - /// An optional dictionary of arguments to pass to the tool. - /// Optional progress reporter for server notifications. - /// JSON serializer options. - /// A cancellation token. - /// The from the tool execution. - public ValueTask CallToolAsync( - string toolName, - IReadOnlyDictionary? arguments = null, - IProgress? progress = null, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - Throw.IfNull(toolName); - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - if (progress is not null) - { - return SendRequestWithProgressAsync(toolName, arguments, progress, serializerOptions, cancellationToken); - } - - return SendRequestAsync( - RequestMethods.ToolsCall, - new() - { - Name = toolName, - Arguments = ToArgumentsDictionary(arguments, serializerOptions), - }, - McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResult, - cancellationToken: cancellationToken); - - async ValueTask SendRequestWithProgressAsync( - string toolName, - IReadOnlyDictionary? arguments, - IProgress progress, - JsonSerializerOptions serializerOptions, - CancellationToken cancellationToken) - { - ProgressToken progressToken = new(Guid.NewGuid().ToString("N")); - - await using var _ = RegisterNotificationHandler(NotificationMethods.ProgressNotification, - (notification, cancellationToken) => - { - if (JsonSerializer.Deserialize(notification.Params, McpJsonUtilities.JsonContext.Default.ProgressNotificationParams) is { } pn && - pn.ProgressToken == progressToken) - { - progress.Report(pn.Progress); - } - - return default; - }).ConfigureAwait(false); - - return await SendRequestAsync( - RequestMethods.ToolsCall, - new() - { - Name = toolName, - Arguments = ToArgumentsDictionary(arguments, serializerOptions), - ProgressToken = progressToken, - }, - McpJsonUtilities.JsonContext.Default.CallToolRequestParams, - McpJsonUtilities.JsonContext.Default.CallToolResult, - cancellationToken: cancellationToken).ConfigureAwait(false); - } - } - - /// - /// Converts the contents of a into a pair of - /// and instances to use - /// as inputs into a operation. - /// - /// - /// The created pair of messages and options. - /// is . - internal static (IList Messages, ChatOptions? Options) ToChatClientArguments( - CreateMessageRequestParams requestParams) - { - Throw.IfNull(requestParams); - - ChatOptions? options = null; - - if (requestParams.MaxTokens is int maxTokens) - { - (options ??= new()).MaxOutputTokens = maxTokens; - } - - if (requestParams.Temperature is float temperature) - { - (options ??= new()).Temperature = temperature; - } - - if (requestParams.StopSequences is { } stopSequences) - { - (options ??= new()).StopSequences = stopSequences.ToArray(); - } - - List messages = - (from sm in requestParams.Messages - let aiContent = sm.Content.ToAIContent() - where aiContent is not null - select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) - .ToList(); - - return (messages, options); - } - - /// Converts the contents of a into a . - /// The whose contents should be extracted. - /// The created . - /// is . - internal static CreateMessageResult ToCreateMessageResult(ChatResponse chatResponse) - { - Throw.IfNull(chatResponse); - - // The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports - // only either a single blob of text or a single image. Heuristically, we'll use an image if there is one - // in any of the response messages, or we'll use all the text from them concatenated, otherwise. - - ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); - - ContentBlock? content = null; - if (lastMessage is not null) - { - foreach (var lmc in lastMessage.Contents) - { - if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) - { - content = dc.ToContent(); - } - } - } - - return new() - { - Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, - Model = chatResponse.ModelId ?? "unknown", - Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, - StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", - }; - } - - /// - /// Creates a sampling handler for use with that will - /// satisfy sampling requests using the specified . - /// - /// The with which to satisfy sampling requests. - /// The created handler delegate that can be assigned to . - /// is . - public static Func, CancellationToken, ValueTask> CreateSamplingHandler( - IChatClient chatClient) - { - Throw.IfNull(chatClient); - - return async (requestParams, progress, cancellationToken) => - { - Throw.IfNull(requestParams); - - var (messages, options) = ToChatClientArguments(requestParams); - var progressToken = requestParams.ProgressToken; - - List updates = []; - await foreach (var update in chatClient.GetStreamingResponseAsync(messages, options, cancellationToken).ConfigureAwait(false)) - { - updates.Add(update); - - if (progressToken is not null) - { - progress.Report(new() - { - Progress = updates.Count, - }); - } - } - - return ToCreateMessageResult(updates.ToChatResponse()); - }; - } - - /// - /// Sets the logging level for the server to control which log messages are sent to the client. - /// - /// The minimum severity level of log messages to receive from the server. - /// The to monitor for cancellation requests. The default is . - /// A task representing the asynchronous operation. - public Task SetLoggingLevel(LoggingLevel level, CancellationToken cancellationToken = default) - { - return SendRequestAsync( - RequestMethods.LoggingSetLevel, - new() { Level = level }, - McpJsonUtilities.JsonContext.Default.SetLevelRequestParams, - McpJsonUtilities.JsonContext.Default.EmptyResult, - cancellationToken: cancellationToken).AsTask(); - } - - /// - /// Sets the logging level for the server to control which log messages are sent to the client. - /// - /// The minimum severity level of log messages to receive from the server. - /// The to monitor for cancellation requests. The default is . - /// A task representing the asynchronous operation. - public Task SetLoggingLevel(LogLevel level, CancellationToken cancellationToken = default) => - SetLoggingLevel(McpServerImpl.ToLoggingLevel(level), cancellationToken); - - /// Convers a dictionary with values to a dictionary with values. - private static Dictionary? ToArgumentsDictionary( - IReadOnlyDictionary? arguments, JsonSerializerOptions options) - { - var typeInfo = options.GetTypeInfo(); - - Dictionary? result = null; - if (arguments is not null) - { - result = new(arguments.Count); - foreach (var kvp in arguments) - { - result.Add(kvp.Key, kvp.Value is JsonElement je ? je : JsonSerializer.SerializeToElement(kvp.Value, typeInfo)); - } - } - - return result; - } } diff --git a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs index 9ceea4601..e987f30f6 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientExtensions.cs @@ -18,81 +18,6 @@ namespace ModelContextProtocol.Client; /// public static class McpClientExtensions { - /// - /// Converts the contents of a into a pair of - /// and instances to use - /// as inputs into a operation. - /// - /// - /// The created pair of messages and options. - /// is . - internal static (IList Messages, ChatOptions? Options) ToChatClientArguments( - this CreateMessageRequestParams requestParams) - { - Throw.IfNull(requestParams); - - ChatOptions? options = null; - - if (requestParams.MaxTokens is int maxTokens) - { - (options ??= new()).MaxOutputTokens = maxTokens; - } - - if (requestParams.Temperature is float temperature) - { - (options ??= new()).Temperature = temperature; - } - - if (requestParams.StopSequences is { } stopSequences) - { - (options ??= new()).StopSequences = stopSequences.ToArray(); - } - - List messages = - (from sm in requestParams.Messages - let aiContent = sm.Content.ToAIContent() - where aiContent is not null - select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) - .ToList(); - - return (messages, options); - } - - /// Converts the contents of a into a . - /// The whose contents should be extracted. - /// The created . - /// is . - internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chatResponse) - { - Throw.IfNull(chatResponse); - - // The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports - // only either a single blob of text or a single image. Heuristically, we'll use an image if there is one - // in any of the response messages, or we'll use all the text from them concatenated, otherwise. - - ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); - - ContentBlock? content = null; - if (lastMessage is not null) - { - foreach (var lmc in lastMessage.Contents) - { - if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) - { - content = dc.ToContent(); - } - } - } - - return new() - { - Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, - Model = chatResponse.ModelId ?? "unknown", - Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, - StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", - }; - } - /// /// Creates a sampling handler for use with that will /// satisfy sampling requests using the specified . @@ -159,7 +84,7 @@ internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chat /// /// is . /// Thrown when the server cannot be reached or returns an error response. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.PingAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.PingAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static Task PingAsync(this IMcpClient client, CancellationToken cancellationToken = default) => AsClientOrThrow(client).PingAsync(cancellationToken); @@ -202,7 +127,7 @@ public static Task PingAsync(this IMcpClient client, CancellationToken cancellat /// /// /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListToolsAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListToolsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask> ListToolsAsync( this IMcpClient client, JsonSerializerOptions? serializerOptions = null, @@ -241,7 +166,7 @@ public static ValueTask> ListToolsAsync( /// /// /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateToolsAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateToolsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static IAsyncEnumerable EnumerateToolsAsync( this IMcpClient client, JsonSerializerOptions? serializerOptions = null, @@ -265,7 +190,7 @@ public static IAsyncEnumerable EnumerateToolsAsync( /// /// /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListPromptsAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListPromptsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask> ListPromptsAsync( this IMcpClient client, CancellationToken cancellationToken = default) => AsClientOrThrow(client).ListPromptsAsync(cancellationToken); @@ -297,7 +222,7 @@ public static ValueTask> ListPromptsAsync( /// /// /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumeratePromptsAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumeratePromptsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static IAsyncEnumerable EnumeratePromptsAsync( this IMcpClient client, CancellationToken cancellationToken = default) => AsClientOrThrow(client).EnumeratePromptsAsync(cancellationToken); @@ -327,7 +252,7 @@ public static IAsyncEnumerable EnumeratePromptsAsync( /// /// Thrown when the prompt does not exist, when required arguments are missing, or when the server encounters an error processing the prompt. /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.GetPromptAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.GetPromptAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask GetPromptAsync( this IMcpClient client, string name, @@ -353,7 +278,7 @@ public static ValueTask GetPromptAsync( /// /// /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourceTemplatesAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourceTemplatesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask> ListResourceTemplatesAsync( this IMcpClient client, CancellationToken cancellationToken = default) => AsClientOrThrow(client).ListResourceTemplatesAsync(cancellationToken); @@ -385,7 +310,7 @@ public static ValueTask> ListResourceTemplatesA /// /// /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourceTemplatesAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourceTemplatesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static IAsyncEnumerable EnumerateResourceTemplatesAsync( this IMcpClient client, CancellationToken cancellationToken = default) => AsClientOrThrow(client).EnumerateResourceTemplatesAsync(cancellationToken); @@ -419,7 +344,7 @@ public static IAsyncEnumerable EnumerateResourceTempl /// /// /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourcesAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ListResourcesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask> ListResourcesAsync( this IMcpClient client, CancellationToken cancellationToken = default) => AsClientOrThrow(client).ListResourcesAsync(cancellationToken); @@ -451,7 +376,7 @@ public static ValueTask> ListResourcesAsync( /// /// /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourcesAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.EnumerateResourcesAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static IAsyncEnumerable EnumerateResourcesAsync( this IMcpClient client, CancellationToken cancellationToken = default) => AsClientOrThrow(client).EnumerateResourcesAsync(cancellationToken); @@ -465,7 +390,7 @@ public static IAsyncEnumerable EnumerateResourcesAsync( /// is . /// is . /// is empty or composed entirely of whitespace. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask ReadResourceAsync( this IMcpClient client, string uri, CancellationToken cancellationToken = default) => AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken); @@ -478,7 +403,7 @@ public static ValueTask ReadResourceAsync( /// The to monitor for cancellation requests. The default is . /// is . /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask ReadResourceAsync( this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) => AsClientOrThrow(client).ReadResourceAsync(uri, cancellationToken); @@ -493,7 +418,7 @@ public static ValueTask ReadResourceAsync( /// is . /// is . /// is empty or composed entirely of whitespace. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.ReadResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask ReadResourceAsync( this IMcpClient client, string uriTemplate, IReadOnlyDictionary arguments, CancellationToken cancellationToken = default) => AsClientOrThrow(client).ReadResourceAsync(uriTemplate, arguments, cancellationToken); @@ -527,7 +452,7 @@ public static ValueTask ReadResourceAsync( /// is . /// is empty or composed entirely of whitespace. /// The server returned an error response. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CompleteAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CompleteAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask CompleteAsync(this IMcpClient client, Reference reference, string argumentName, string argumentValue, CancellationToken cancellationToken = default) => AsClientOrThrow(client).CompleteAsync(reference, argumentName, argumentValue, cancellationToken); @@ -556,7 +481,7 @@ public static ValueTask CompleteAsync(this IMcpClient client, Re /// is . /// is . /// is empty or composed entirely of whitespace. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) => AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken); @@ -584,7 +509,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, string uri, /// /// is . /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.SubscribeToResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) => AsClientOrThrow(client).SubscribeToResourceAsync(uri, cancellationToken); @@ -612,7 +537,7 @@ public static Task SubscribeToResourceAsync(this IMcpClient client, Uri uri, Can /// is . /// is . /// is empty or composed entirely of whitespace. - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string uri, CancellationToken cancellationToken = default) => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken); @@ -639,7 +564,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, string u /// /// is . /// is . - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.UnsubscribeFromResourceAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, CancellationToken cancellationToken = default) => AsClientOrThrow(client).UnsubscribeFromResourceAsync(uri, cancellationToken); @@ -678,7 +603,7 @@ public static Task UnsubscribeFromResourceAsync(this IMcpClient client, Uri uri, /// }); /// /// - [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CallToolAsync)} instead.")] + [Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CallToolAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask CallToolAsync( this IMcpClient client, string toolName, @@ -709,4 +634,79 @@ static void ThrowInvalidEndpointType(string memberName) $"'{nameof(McpClientExtensions)}.{memberName}' is obsolete and will be " + $"removed in the future."); } + + /// + /// Converts the contents of a into a pair of + /// and instances to use + /// as inputs into a operation. + /// + /// + /// The created pair of messages and options. + /// is . + internal static (IList Messages, ChatOptions? Options) ToChatClientArguments( + this CreateMessageRequestParams requestParams) + { + Throw.IfNull(requestParams); + + ChatOptions? options = null; + + if (requestParams.MaxTokens is int maxTokens) + { + (options ??= new()).MaxOutputTokens = maxTokens; + } + + if (requestParams.Temperature is float temperature) + { + (options ??= new()).Temperature = temperature; + } + + if (requestParams.StopSequences is { } stopSequences) + { + (options ??= new()).StopSequences = stopSequences.ToArray(); + } + + List messages = + (from sm in requestParams.Messages + let aiContent = sm.Content.ToAIContent() + where aiContent is not null + select new ChatMessage(sm.Role == Role.Assistant ? ChatRole.Assistant : ChatRole.User, [aiContent])) + .ToList(); + + return (messages, options); + } + + /// Converts the contents of a into a . + /// The whose contents should be extracted. + /// The created . + /// is . + internal static CreateMessageResult ToCreateMessageResult(this ChatResponse chatResponse) + { + Throw.IfNull(chatResponse); + + // The ChatResponse can include multiple messages, of varying modalities, but CreateMessageResult supports + // only either a single blob of text or a single image. Heuristically, we'll use an image if there is one + // in any of the response messages, or we'll use all the text from them concatenated, otherwise. + + ChatMessage? lastMessage = chatResponse.Messages.LastOrDefault(); + + ContentBlock? content = null; + if (lastMessage is not null) + { + foreach (var lmc in lastMessage.Contents) + { + if (lmc is DataContent dc && (dc.HasTopLevelMediaType("image") || dc.HasTopLevelMediaType("audio"))) + { + content = dc.ToContent(); + } + } + } + + return new() + { + Content = content ?? new TextContentBlock { Text = lastMessage?.Text ?? string.Empty }, + Model = chatResponse.ModelId ?? "unknown", + Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, + StopReason = chatResponse.FinishReason == ChatFinishReason.Length ? "maxTokens" : "endTurn", + }; + } } \ No newline at end of file diff --git a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs index 756281a13..6934eb2b5 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientFactory.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientFactory.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Client; /// that connect to MCP servers. It handles the creation and connection /// of appropriate implementations through the supplied transport. /// -[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead.")] +[Obsolete($"Use {nameof(McpClient)}.{nameof(McpClient.CreateAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static partial class McpClientFactory { /// Creates an , connecting it to the specified server. diff --git a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs index a861de1d9..639718885 100644 --- a/src/ModelContextProtocol.Core/Client/McpClientImpl.cs +++ b/src/ModelContextProtocol.Core/Client/McpClientImpl.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using ModelContextProtocol.Protocol; -using System.Runtime.InteropServices; using System.Text.Json; namespace ModelContextProtocol.Client; diff --git a/src/ModelContextProtocol.Core/IMcpEndpoint.cs b/src/ModelContextProtocol.Core/IMcpEndpoint.cs index 01221ecdb..beb96521f 100644 --- a/src/ModelContextProtocol.Core/IMcpEndpoint.cs +++ b/src/ModelContextProtocol.Core/IMcpEndpoint.cs @@ -26,7 +26,7 @@ namespace ModelContextProtocol; /// All MCP endpoints should be properly disposed after use as they implement . /// /// -[Obsolete($"Use {nameof(McpSession)} instead.")] +[Obsolete($"Use {nameof(McpSession)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public interface IMcpEndpoint : IAsyncDisposable { /// Gets an identifier associated with the current MCP session. diff --git a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs index 0f0239fab..f51289ac2 100644 --- a/src/ModelContextProtocol.Core/McpEndpointExtensions.cs +++ b/src/ModelContextProtocol.Core/McpEndpointExtensions.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.Json; -using System.Text.Json.Serialization.Metadata; namespace ModelContextProtocol; @@ -35,7 +34,7 @@ public static class McpEndpointExtensions /// The options governing request serialization. /// The to monitor for cancellation requests. The default is . /// A task that represents the asynchronous operation. The task result contains the deserialized result. - [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendRequestAsync)} instead.")] + [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendRequestAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask SendRequestAsync( this IMcpEndpoint endpoint, string method, @@ -60,7 +59,7 @@ public static ValueTask SendRequestAsync( /// changes in state. /// /// - [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendNotificationAsync)} instead.")] + [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendNotificationAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static Task SendNotificationAsync(this IMcpEndpoint client, string method, CancellationToken cancellationToken = default) => AsSessionOrThrow(client).SendNotificationAsync(method, cancellationToken); @@ -88,7 +87,7 @@ public static Task SendNotificationAsync(this IMcpEndpoint client, string method /// but custom methods can also be used for application-specific notifications. /// /// - [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendNotificationAsync)} instead.")] + [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.SendNotificationAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static Task SendNotificationAsync( this IMcpEndpoint endpoint, string method, @@ -116,7 +115,7 @@ public static Task SendNotificationAsync( /// Progress notifications are sent asynchronously and don't block the operation from continuing. /// /// - [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.NotifyProgressAsync)} instead.")] + [Obsolete($"Use {nameof(McpSession)}.{nameof(McpSession.NotifyProgressAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static Task NotifyProgressAsync( this IMcpEndpoint endpoint, ProgressToken progressToken, diff --git a/src/ModelContextProtocol.Core/McpSession.Methods.cs b/src/ModelContextProtocol.Core/McpSession.Methods.cs new file mode 100644 index 000000000..c537732f1 --- /dev/null +++ b/src/ModelContextProtocol.Core/McpSession.Methods.cs @@ -0,0 +1,183 @@ +using ModelContextProtocol.Protocol; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization.Metadata; + +namespace ModelContextProtocol; + +#pragma warning disable CS0618 // Type or member is obsolete +public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable +#pragma warning restore CS0618 // Type or member is obsolete +{ + /// + /// Sends a JSON-RPC request and attempts to deserialize the result to . + /// + /// The type of the request parameters to serialize from. + /// The type of the result to deserialize to. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The request id for the request. + /// The options governing request serialization. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains the deserialized result. + public ValueTask SendRequestAsync( + string method, + TParameters parameters, + JsonSerializerOptions? serializerOptions = null, + RequestId requestId = default, + CancellationToken cancellationToken = default) + where TResult : notnull + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + JsonTypeInfo paramsTypeInfo = serializerOptions.GetTypeInfo(); + JsonTypeInfo resultTypeInfo = serializerOptions.GetTypeInfo(); + return SendRequestAsync(method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken); + } + + /// + /// Sends a JSON-RPC request and attempts to deserialize the result to . + /// + /// The type of the request parameters to serialize from. + /// The type of the result to deserialize to. + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The type information for request parameter serialization. + /// The type information for request parameter deserialization. + /// The request id for the request. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous operation. The task result contains the deserialized result. + internal async ValueTask SendRequestAsync( + string method, + TParameters parameters, + JsonTypeInfo parametersTypeInfo, + JsonTypeInfo resultTypeInfo, + RequestId requestId = default, + CancellationToken cancellationToken = default) + where TResult : notnull + { + Throw.IfNullOrWhiteSpace(method); + Throw.IfNull(parametersTypeInfo); + Throw.IfNull(resultTypeInfo); + + JsonRpcRequest jsonRpcRequest = new() + { + Id = requestId, + Method = method, + Params = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo), + }; + + JsonRpcResponse response = await SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false); + return JsonSerializer.Deserialize(response.Result, resultTypeInfo) ?? throw new JsonException("Unexpected JSON result in response."); + } + + /// + /// Sends a parameterless notification to the connected session. + /// + /// The notification method name. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous send operation. + /// + /// + /// This method sends a notification without any parameters. Notifications are one-way messages + /// that don't expect a response. They are commonly used for events, status updates, or to signal + /// changes in state. + /// + /// + public Task SendNotificationAsync(string method, CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(method); + return SendMessageAsync(new JsonRpcNotification { Method = method }, cancellationToken); + } + + /// + /// Sends a notification with parameters to the connected session. + /// + /// The type of the notification parameters to serialize. + /// The JSON-RPC method name for the notification. + /// Object representing the notification parameters. + /// The options governing parameter serialization. If null, default options are used. + /// The to monitor for cancellation requests. The default is . + /// A task that represents the asynchronous send operation. + /// + /// + /// This method sends a notification with parameters to the connected session. Notifications are one-way + /// messages that don't expect a response, commonly used for events, status updates, or signaling changes. + /// + /// + /// The parameters object is serialized to JSON according to the provided serializer options or the default + /// options if none are specified. + /// + /// + /// The Model Context Protocol defines several standard notification methods in , + /// but custom methods can also be used for application-specific notifications. + /// + /// + public Task SendNotificationAsync( + string method, + TParameters parameters, + JsonSerializerOptions? serializerOptions = null, + CancellationToken cancellationToken = default) + { + serializerOptions ??= McpJsonUtilities.DefaultOptions; + serializerOptions.MakeReadOnly(); + + JsonTypeInfo parametersTypeInfo = serializerOptions.GetTypeInfo(); + return SendNotificationAsync(method, parameters, parametersTypeInfo, cancellationToken); + } + + /// + /// Sends a notification to the server with parameters. + /// + /// The JSON-RPC method name to invoke. + /// Object representing the request parameters. + /// The type information for request parameter serialization. + /// The to monitor for cancellation requests. The default is . + internal Task SendNotificationAsync( + string method, + TParameters parameters, + JsonTypeInfo parametersTypeInfo, + CancellationToken cancellationToken = default) + { + Throw.IfNullOrWhiteSpace(method); + Throw.IfNull(parametersTypeInfo); + + JsonNode? parametersJson = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo); + return SendMessageAsync(new JsonRpcNotification { Method = method, Params = parametersJson }, cancellationToken); + } + + /// + /// Notifies the connected session of progress for a long-running operation. + /// + /// The identifying the operation for which progress is being reported. + /// The progress update to send, containing information such as percentage complete or status message. + /// The to monitor for cancellation requests. The default is . + /// A task representing the completion of the notification operation (not the operation being tracked). + /// The current session instance is . + /// + /// + /// This method sends a progress notification to the connected session using the Model Context Protocol's + /// standardized progress notification format. Progress updates are identified by a + /// that allows the recipient to correlate multiple updates with a specific long-running operation. + /// + /// + /// Progress notifications are sent asynchronously and don't block the operation from continuing. + /// + /// + public Task NotifyProgressAsync( + ProgressToken progressToken, + ProgressNotificationValue progress, + CancellationToken cancellationToken = default) + { + return SendNotificationAsync( + NotificationMethods.ProgressNotification, + new ProgressNotificationParams + { + ProgressToken = progressToken, + Progress = progress, + }, + McpJsonUtilities.JsonContext.Default.ProgressNotificationParams, + cancellationToken); + } +} diff --git a/src/ModelContextProtocol.Core/McpSession.cs b/src/ModelContextProtocol.Core/McpSession.cs index c83b8c27c..241c36d4c 100644 --- a/src/ModelContextProtocol.Core/McpSession.cs +++ b/src/ModelContextProtocol.Core/McpSession.cs @@ -1,9 +1,6 @@ using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization.Metadata; namespace ModelContextProtocol; @@ -30,7 +27,7 @@ namespace ModelContextProtocol; /// /// #pragma warning disable CS0618 // Type or member is obsolete -public abstract class McpSession : IMcpEndpoint, IAsyncDisposable +public abstract partial class McpSession : IMcpEndpoint, IAsyncDisposable #pragma warning restore CS0618 // Type or member is obsolete { /// Gets an identifier associated with the current MCP session. @@ -86,176 +83,4 @@ public abstract class McpSession : IMcpEndpoint, IAsyncDisposable /// public abstract ValueTask DisposeAsync(); - - /// - /// Sends a JSON-RPC request and attempts to deserialize the result to . - /// - /// The type of the request parameters to serialize from. - /// The type of the result to deserialize to. - /// The JSON-RPC method name to invoke. - /// Object representing the request parameters. - /// The request id for the request. - /// The options governing request serialization. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. The task result contains the deserialized result. - public ValueTask SendRequestAsync( - string method, - TParameters parameters, - JsonSerializerOptions? serializerOptions = null, - RequestId requestId = default, - CancellationToken cancellationToken = default) - where TResult : notnull - { - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - JsonTypeInfo paramsTypeInfo = serializerOptions.GetTypeInfo(); - JsonTypeInfo resultTypeInfo = serializerOptions.GetTypeInfo(); - return SendRequestAsync(method, parameters, paramsTypeInfo, resultTypeInfo, requestId, cancellationToken); - } - - /// - /// Sends a JSON-RPC request and attempts to deserialize the result to . - /// - /// The type of the request parameters to serialize from. - /// The type of the result to deserialize to. - /// The JSON-RPC method name to invoke. - /// Object representing the request parameters. - /// The type information for request parameter serialization. - /// The type information for request parameter deserialization. - /// The request id for the request. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous operation. The task result contains the deserialized result. - internal async ValueTask SendRequestAsync( - string method, - TParameters parameters, - JsonTypeInfo parametersTypeInfo, - JsonTypeInfo resultTypeInfo, - RequestId requestId = default, - CancellationToken cancellationToken = default) - where TResult : notnull - { - Throw.IfNullOrWhiteSpace(method); - Throw.IfNull(parametersTypeInfo); - Throw.IfNull(resultTypeInfo); - - JsonRpcRequest jsonRpcRequest = new() - { - Id = requestId, - Method = method, - Params = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo), - }; - - JsonRpcResponse response = await SendRequestAsync(jsonRpcRequest, cancellationToken).ConfigureAwait(false); - return JsonSerializer.Deserialize(response.Result, resultTypeInfo) ?? throw new JsonException("Unexpected JSON result in response."); - } - - /// - /// Sends a parameterless notification to the connected session. - /// - /// The notification method name. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous send operation. - /// - /// - /// This method sends a notification without any parameters. Notifications are one-way messages - /// that don't expect a response. They are commonly used for events, status updates, or to signal - /// changes in state. - /// - /// - public Task SendNotificationAsync(string method, CancellationToken cancellationToken = default) - { - Throw.IfNullOrWhiteSpace(method); - return SendMessageAsync(new JsonRpcNotification { Method = method }, cancellationToken); - } - - /// - /// Sends a notification with parameters to the connected session. - /// - /// The type of the notification parameters to serialize. - /// The JSON-RPC method name for the notification. - /// Object representing the notification parameters. - /// The options governing parameter serialization. If null, default options are used. - /// The to monitor for cancellation requests. The default is . - /// A task that represents the asynchronous send operation. - /// - /// - /// This method sends a notification with parameters to the connected session. Notifications are one-way - /// messages that don't expect a response, commonly used for events, status updates, or signaling changes. - /// - /// - /// The parameters object is serialized to JSON according to the provided serializer options or the default - /// options if none are specified. - /// - /// - /// The Model Context Protocol defines several standard notification methods in , - /// but custom methods can also be used for application-specific notifications. - /// - /// - public Task SendNotificationAsync( - string method, - TParameters parameters, - JsonSerializerOptions? serializerOptions = null, - CancellationToken cancellationToken = default) - { - serializerOptions ??= McpJsonUtilities.DefaultOptions; - serializerOptions.MakeReadOnly(); - - JsonTypeInfo parametersTypeInfo = serializerOptions.GetTypeInfo(); - return SendNotificationAsync(method, parameters, parametersTypeInfo, cancellationToken); - } - - /// - /// Sends a notification to the server with parameters. - /// - /// The JSON-RPC method name to invoke. - /// Object representing the request parameters. - /// The type information for request parameter serialization. - /// The to monitor for cancellation requests. The default is . - internal Task SendNotificationAsync( - string method, - TParameters parameters, - JsonTypeInfo parametersTypeInfo, - CancellationToken cancellationToken = default) - { - Throw.IfNullOrWhiteSpace(method); - Throw.IfNull(parametersTypeInfo); - - JsonNode? parametersJson = JsonSerializer.SerializeToNode(parameters, parametersTypeInfo); - return SendMessageAsync(new JsonRpcNotification { Method = method, Params = parametersJson }, cancellationToken); - } - - /// - /// Notifies the connected session of progress for a long-running operation. - /// - /// The identifying the operation for which progress is being reported. - /// The progress update to send, containing information such as percentage complete or status message. - /// The to monitor for cancellation requests. The default is . - /// A task representing the completion of the notification operation (not the operation being tracked). - /// The current session instance is . - /// - /// - /// This method sends a progress notification to the connected session using the Model Context Protocol's - /// standardized progress notification format. Progress updates are identified by a - /// that allows the recipient to correlate multiple updates with a specific long-running operation. - /// - /// - /// Progress notifications are sent asynchronously and don't block the operation from continuing. - /// - /// - public Task NotifyProgressAsync( - ProgressToken progressToken, - ProgressNotificationValue progress, - CancellationToken cancellationToken = default) - { - return SendNotificationAsync( - NotificationMethods.ProgressNotification, - new ProgressNotificationParams - { - ProgressToken = progressToken, - Progress = progress, - }, - McpJsonUtilities.JsonContext.Default.ProgressNotificationParams, - cancellationToken); - } } diff --git a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs index 30b6745a9..261796b5f 100644 --- a/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs +++ b/src/ModelContextProtocol.Core/Protocol/JsonRpcMessageContext.cs @@ -29,7 +29,7 @@ public class JsonRpcMessageContext /// /// /// This is used to support the Streamable HTTP transport in its default stateful mode. In this mode, - /// the outlives the initial HTTP request context it was created on, and new + /// the outlives the initial HTTP request context it was created on, and new /// JSON-RPC messages can originate from future HTTP requests. This allows the transport to flow the /// context with the JSON-RPC message. This is particularly useful for enabling IHttpContextAccessor /// in tool calls. diff --git a/src/ModelContextProtocol.Core/Server/IMcpServer.cs b/src/ModelContextProtocol.Core/Server/IMcpServer.cs index 31131f81f..016ad90b3 100644 --- a/src/ModelContextProtocol.Core/Server/IMcpServer.cs +++ b/src/ModelContextProtocol.Core/Server/IMcpServer.cs @@ -5,7 +5,7 @@ namespace ModelContextProtocol.Server; /// /// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. /// -[Obsolete($"Use {nameof(McpServer)} instead.")] +[Obsolete($"Use {nameof(McpServer)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public interface IMcpServer : IMcpEndpoint { /// diff --git a/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs new file mode 100644 index 000000000..de1b9d8b2 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpServer.Methods.cs @@ -0,0 +1,365 @@ +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Protocol; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; + +namespace ModelContextProtocol.Server; + +/// +/// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. +/// +#pragma warning disable CS0618 // Type or member is obsolete +public abstract partial class McpServer : McpSession, IMcpServer +#pragma warning restore CS0618 // Type or member is obsolete +{ + /// + /// Creates a new instance of an . + /// + /// Transport to use for the server representing an already-established MCP session. + /// Configuration options for this server, including capabilities. + /// Logger factory to use for logging. If null, logging will be disabled. + /// Optional service provider to create new instances of tools and other dependencies. + /// An instance that should be disposed when no longer needed. + /// is . + /// is . + public static McpServer Create( + ITransport transport, + McpServerOptions serverOptions, + ILoggerFactory? loggerFactory = null, + IServiceProvider? serviceProvider = null) + { + Throw.IfNull(transport); + Throw.IfNull(serverOptions); + + return new McpServerImpl(transport, serverOptions, loggerFactory, serviceProvider); + } + + /// + /// Requests to sample an LLM via the client using the specified request parameters. + /// + /// The parameters for the sampling request. + /// The to monitor for cancellation requests. + /// A task containing the sampling result from the client. + /// The client does not support sampling. + public ValueTask SampleAsync( + CreateMessageRequestParams request, CancellationToken cancellationToken = default) + { + ThrowIfSamplingUnsupported(); + + return SendRequestAsync( + RequestMethods.SamplingCreateMessage, + request, + McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, + McpJsonUtilities.JsonContext.Default.CreateMessageResult, + cancellationToken: cancellationToken); + } + + /// + /// Requests to sample an LLM via the client using the provided chat messages and options. + /// + /// The messages to send as part of the request. + /// The options to use for the request, including model parameters and constraints. + /// The to monitor for cancellation requests. The default is . + /// A task containing the chat response from the model. + /// is . + /// The client does not support sampling. + public async Task SampleAsync( + IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) + { + Throw.IfNull(messages); + + StringBuilder? systemPrompt = null; + + if (options?.Instructions is { } instructions) + { + (systemPrompt ??= new()).Append(instructions); + } + + List samplingMessages = []; + foreach (var message in messages) + { + if (message.Role == ChatRole.System) + { + if (systemPrompt is null) + { + systemPrompt = new(); + } + else + { + systemPrompt.AppendLine(); + } + + systemPrompt.Append(message.Text); + continue; + } + + if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant) + { + Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant; + + foreach (var content in message.Contents) + { + switch (content) + { + case TextContent textContent: + samplingMessages.Add(new() + { + Role = role, + Content = new TextContentBlock { Text = textContent.Text }, + }); + break; + + case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"): + samplingMessages.Add(new() + { + Role = role, + Content = dataContent.HasTopLevelMediaType("image") ? + new ImageContentBlock + { + MimeType = dataContent.MediaType, + Data = dataContent.Base64Data.ToString(), + } : + new AudioContentBlock + { + MimeType = dataContent.MediaType, + Data = dataContent.Base64Data.ToString(), + }, + }); + break; + } + } + } + } + + ModelPreferences? modelPreferences = null; + if (options?.ModelId is { } modelId) + { + modelPreferences = new() { Hints = [new() { Name = modelId }] }; + } + + var result = await SampleAsync(new() + { + Messages = samplingMessages, + MaxTokens = options?.MaxOutputTokens, + StopSequences = options?.StopSequences?.ToArray(), + SystemPrompt = systemPrompt?.ToString(), + Temperature = options?.Temperature, + ModelPreferences = modelPreferences, + }, cancellationToken).ConfigureAwait(false); + + AIContent? responseContent = result.Content.ToAIContent(); + + return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : [])) + { + ModelId = result.Model, + FinishReason = result.StopReason switch + { + "maxTokens" => ChatFinishReason.Length, + "endTurn" or "stopSequence" or _ => ChatFinishReason.Stop, + } + }; + } + + /// + /// Creates an wrapper that can be used to send sampling requests to the client. + /// + /// The that can be used to issue sampling requests to the client. + /// The client does not support sampling. + public IChatClient AsSamplingChatClient() + { + ThrowIfSamplingUnsupported(); + return new SamplingChatClient(this); + } + + /// Gets an on which logged messages will be sent as notifications to the client. + /// An that can be used to log to the client.. + public ILoggerProvider AsClientLoggerProvider() + { + return new ClientLoggerProvider(this); + } + + /// + /// Requests the client to list the roots it exposes. + /// + /// The parameters for the list roots request. + /// The to monitor for cancellation requests. + /// A task containing the list of roots exposed by the client. + /// The client does not support roots. + public ValueTask RequestRootsAsync( + ListRootsRequestParams request, CancellationToken cancellationToken = default) + { + ThrowIfRootsUnsupported(); + + return SendRequestAsync( + RequestMethods.RootsList, + request, + McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, + McpJsonUtilities.JsonContext.Default.ListRootsResult, + cancellationToken: cancellationToken); + } + + /// + /// Requests additional information from the user via the client, allowing the server to elicit structured data. + /// + /// The parameters for the elicitation request. + /// The to monitor for cancellation requests. + /// A task containing the elicitation result. + /// The client does not support elicitation. + public ValueTask ElicitAsync( + ElicitRequestParams request, CancellationToken cancellationToken = default) + { + ThrowIfElicitationUnsupported(); + + return SendRequestAsync( + RequestMethods.ElicitationCreate, + request, + McpJsonUtilities.JsonContext.Default.ElicitRequestParams, + McpJsonUtilities.JsonContext.Default.ElicitResult, + cancellationToken: cancellationToken); + } + + private void ThrowIfSamplingUnsupported() + { + if (ClientCapabilities?.Sampling is null) + { + if (ServerOptions.KnownClientInfo is not null) + { + throw new InvalidOperationException("Sampling is not supported in stateless mode."); + } + + throw new InvalidOperationException("Client does not support sampling."); + } + } + + private void ThrowIfRootsUnsupported() + { + if (ClientCapabilities?.Roots is null) + { + if (ServerOptions.KnownClientInfo is not null) + { + throw new InvalidOperationException("Roots are not supported in stateless mode."); + } + + throw new InvalidOperationException("Client does not support roots."); + } + } + + private void ThrowIfElicitationUnsupported() + { + if (ClientCapabilities?.Elicitation is null) + { + if (ServerOptions.KnownClientInfo is not null) + { + throw new InvalidOperationException("Elicitation is not supported in stateless mode."); + } + + throw new InvalidOperationException("Client does not support elicitation requests."); + } + } + + /// Provides an implementation that's implemented via client sampling. + private sealed class SamplingChatClient : IChatClient + { + private readonly McpServer _server; + + public SamplingChatClient(McpServer server) => _server = server; + + /// + public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => + _server.SampleAsync(messages, options, cancellationToken); + + /// + async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( + IEnumerable messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); + foreach (var update in response.ToChatResponseUpdates()) + { + yield return update; + } + } + + /// + object? IChatClient.GetService(Type serviceType, object? serviceKey) + { + Throw.IfNull(serviceType); + + return + serviceKey is not null ? null : + serviceType.IsInstanceOfType(this) ? this : + serviceType.IsInstanceOfType(_server) ? _server : + null; + } + + /// + void IDisposable.Dispose() { } // nop + } + + /// + /// Provides an implementation for creating loggers + /// that send logging message notifications to the client for logged messages. + /// + private sealed class ClientLoggerProvider : ILoggerProvider + { + private readonly McpServer _server; + + public ClientLoggerProvider(McpServer server) => _server = server; + + /// + public ILogger CreateLogger(string categoryName) + { + Throw.IfNull(categoryName); + + return new ClientLogger(_server, categoryName); + } + + /// + void IDisposable.Dispose() { } + + private sealed class ClientLogger : ILogger + { + private readonly McpServer _server; + private readonly string _categoryName; + + public ClientLogger(McpServer server, string categoryName) + { + _server = server; + _categoryName = categoryName; + } + + /// + public IDisposable? BeginScope(TState state) where TState : notnull => + null; + + /// + public bool IsEnabled(LogLevel logLevel) => + _server?.LoggingLevel is { } loggingLevel && + McpServerImpl.ToLoggingLevel(logLevel) >= loggingLevel; + + /// + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + Throw.IfNull(formatter); + + LogInternal(logLevel, formatter(state, exception)); + + void LogInternal(LogLevel level, string message) + { + _ = _server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams + { + Level = McpServerImpl.ToLoggingLevel(level), + Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String), + Logger = _categoryName, + }); + } + } + } + } +} diff --git a/src/ModelContextProtocol.Core/Server/McpServer.cs b/src/ModelContextProtocol.Core/Server/McpServer.cs index 00fe6da1c..02c17de1a 100644 --- a/src/ModelContextProtocol.Core/Server/McpServer.cs +++ b/src/ModelContextProtocol.Core/Server/McpServer.cs @@ -1,9 +1,4 @@ -using Microsoft.Extensions.AI; -using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; namespace ModelContextProtocol.Server; @@ -11,7 +6,7 @@ namespace ModelContextProtocol.Server; /// Represents an instance of a Model Context Protocol (MCP) server that connects to and communicates with an MCP client. /// #pragma warning disable CS0618 // Type or member is obsolete -public abstract class McpServer : McpSession, IMcpServer +public abstract partial class McpServer : McpSession, IMcpServer #pragma warning restore CS0618 // Type or member is obsolete { /// @@ -66,353 +61,4 @@ public abstract class McpServer : McpSession, IMcpServer /// Runs the server, listening for and handling client requests. /// public abstract Task RunAsync(CancellationToken cancellationToken = default); - - /// - /// Creates a new instance of an . - /// - /// Transport to use for the server representing an already-established MCP session. - /// Configuration options for this server, including capabilities. - /// Logger factory to use for logging. If null, logging will be disabled. - /// Optional service provider to create new instances of tools and other dependencies. - /// An instance that should be disposed when no longer needed. - /// is . - /// is . - public static McpServer Create( - ITransport transport, - McpServerOptions serverOptions, - ILoggerFactory? loggerFactory = null, - IServiceProvider? serviceProvider = null) - { - Throw.IfNull(transport); - Throw.IfNull(serverOptions); - - return new McpServerImpl(transport, serverOptions, loggerFactory, serviceProvider); - } - - /// - /// Requests to sample an LLM via the client using the specified request parameters. - /// - /// The parameters for the sampling request. - /// The to monitor for cancellation requests. - /// A task containing the sampling result from the client. - /// The client does not support sampling. - public ValueTask SampleAsync( - CreateMessageRequestParams request, CancellationToken cancellationToken = default) - { - ThrowIfSamplingUnsupported(); - - return SendRequestAsync( - RequestMethods.SamplingCreateMessage, - request, - McpJsonUtilities.JsonContext.Default.CreateMessageRequestParams, - McpJsonUtilities.JsonContext.Default.CreateMessageResult, - cancellationToken: cancellationToken); - } - - /// - /// Requests to sample an LLM via the client using the provided chat messages and options. - /// - /// The messages to send as part of the request. - /// The options to use for the request, including model parameters and constraints. - /// The to monitor for cancellation requests. The default is . - /// A task containing the chat response from the model. - /// is . - /// The client does not support sampling. - public async Task SampleAsync( - IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) - { - Throw.IfNull(messages); - - StringBuilder? systemPrompt = null; - - if (options?.Instructions is { } instructions) - { - (systemPrompt ??= new()).Append(instructions); - } - - List samplingMessages = []; - foreach (var message in messages) - { - if (message.Role == ChatRole.System) - { - if (systemPrompt is null) - { - systemPrompt = new(); - } - else - { - systemPrompt.AppendLine(); - } - - systemPrompt.Append(message.Text); - continue; - } - - if (message.Role == ChatRole.User || message.Role == ChatRole.Assistant) - { - Role role = message.Role == ChatRole.User ? Role.User : Role.Assistant; - - foreach (var content in message.Contents) - { - switch (content) - { - case TextContent textContent: - samplingMessages.Add(new() - { - Role = role, - Content = new TextContentBlock { Text = textContent.Text }, - }); - break; - - case DataContent dataContent when dataContent.HasTopLevelMediaType("image") || dataContent.HasTopLevelMediaType("audio"): - samplingMessages.Add(new() - { - Role = role, - Content = dataContent.HasTopLevelMediaType("image") ? - new ImageContentBlock - { - MimeType = dataContent.MediaType, - Data = dataContent.Base64Data.ToString(), - } : - new AudioContentBlock - { - MimeType = dataContent.MediaType, - Data = dataContent.Base64Data.ToString(), - }, - }); - break; - } - } - } - } - - ModelPreferences? modelPreferences = null; - if (options?.ModelId is { } modelId) - { - modelPreferences = new() { Hints = [new() { Name = modelId }] }; - } - - var result = await SampleAsync(new() - { - Messages = samplingMessages, - MaxTokens = options?.MaxOutputTokens, - StopSequences = options?.StopSequences?.ToArray(), - SystemPrompt = systemPrompt?.ToString(), - Temperature = options?.Temperature, - ModelPreferences = modelPreferences, - }, cancellationToken).ConfigureAwait(false); - - AIContent? responseContent = result.Content.ToAIContent(); - - return new(new ChatMessage(result.Role is Role.User ? ChatRole.User : ChatRole.Assistant, responseContent is not null ? [responseContent] : [])) - { - ModelId = result.Model, - FinishReason = result.StopReason switch - { - "maxTokens" => ChatFinishReason.Length, - "endTurn" or "stopSequence" or _ => ChatFinishReason.Stop, - } - }; - } - - /// - /// Creates an wrapper that can be used to send sampling requests to the client. - /// - /// The that can be used to issue sampling requests to the client. - /// The client does not support sampling. - public IChatClient AsSamplingChatClient() - { - ThrowIfSamplingUnsupported(); - return new SamplingChatClient(this); - } - - /// Gets an on which logged messages will be sent as notifications to the client. - /// An that can be used to log to the client.. - public ILoggerProvider AsClientLoggerProvider() - { - return new ClientLoggerProvider(this); - } - - /// - /// Requests the client to list the roots it exposes. - /// - /// The parameters for the list roots request. - /// The to monitor for cancellation requests. - /// A task containing the list of roots exposed by the client. - /// The client does not support roots. - public ValueTask RequestRootsAsync( - ListRootsRequestParams request, CancellationToken cancellationToken = default) - { - ThrowIfRootsUnsupported(); - - return SendRequestAsync( - RequestMethods.RootsList, - request, - McpJsonUtilities.JsonContext.Default.ListRootsRequestParams, - McpJsonUtilities.JsonContext.Default.ListRootsResult, - cancellationToken: cancellationToken); - } - - /// - /// Requests additional information from the user via the client, allowing the server to elicit structured data. - /// - /// The parameters for the elicitation request. - /// The to monitor for cancellation requests. - /// A task containing the elicitation result. - /// The client does not support elicitation. - public ValueTask ElicitAsync( - ElicitRequestParams request, CancellationToken cancellationToken = default) - { - ThrowIfElicitationUnsupported(); - - return SendRequestAsync( - RequestMethods.ElicitationCreate, - request, - McpJsonUtilities.JsonContext.Default.ElicitRequestParams, - McpJsonUtilities.JsonContext.Default.ElicitResult, - cancellationToken: cancellationToken); - } - - private void ThrowIfSamplingUnsupported() - { - if (ClientCapabilities?.Sampling is null) - { - if (ServerOptions.KnownClientInfo is not null) - { - throw new InvalidOperationException("Sampling is not supported in stateless mode."); - } - - throw new InvalidOperationException("Client does not support sampling."); - } - } - - private void ThrowIfRootsUnsupported() - { - if (ClientCapabilities?.Roots is null) - { - if (ServerOptions.KnownClientInfo is not null) - { - throw new InvalidOperationException("Roots are not supported in stateless mode."); - } - - throw new InvalidOperationException("Client does not support roots."); - } - } - - private void ThrowIfElicitationUnsupported() - { - if (ClientCapabilities?.Elicitation is null) - { - if (ServerOptions.KnownClientInfo is not null) - { - throw new InvalidOperationException("Elicitation is not supported in stateless mode."); - } - - throw new InvalidOperationException("Client does not support elicitation requests."); - } - } - - /// Provides an implementation that's implemented via client sampling. - private sealed class SamplingChatClient : IChatClient - { - private readonly McpServer _server; - - public SamplingChatClient(McpServer server) => _server = server; - - /// - public Task GetResponseAsync(IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => - _server.SampleAsync(messages, options, cancellationToken); - - /// - async IAsyncEnumerable IChatClient.GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken) - { - var response = await GetResponseAsync(messages, options, cancellationToken).ConfigureAwait(false); - foreach (var update in response.ToChatResponseUpdates()) - { - yield return update; - } - } - - /// - object? IChatClient.GetService(Type serviceType, object? serviceKey) - { - Throw.IfNull(serviceType); - - return - serviceKey is not null ? null : - serviceType.IsInstanceOfType(this) ? this : - serviceType.IsInstanceOfType(_server) ? _server : - null; - } - - /// - void IDisposable.Dispose() { } // nop - } - - /// - /// Provides an implementation for creating loggers - /// that send logging message notifications to the client for logged messages. - /// - private sealed class ClientLoggerProvider : ILoggerProvider - { - private readonly McpServer _server; - - public ClientLoggerProvider(McpServer server) => _server = server; - - /// - public ILogger CreateLogger(string categoryName) - { - Throw.IfNull(categoryName); - - return new ClientLogger(_server, categoryName); - } - - /// - void IDisposable.Dispose() { } - - private sealed class ClientLogger : ILogger - { - private readonly McpServer _server; - private readonly string _categoryName; - - public ClientLogger(McpServer server, string categoryName) - { - _server = server; - _categoryName = categoryName; - } - - /// - public IDisposable? BeginScope(TState state) where TState : notnull => - null; - - /// - public bool IsEnabled(LogLevel logLevel) => - _server?.LoggingLevel is { } loggingLevel && - McpServerImpl.ToLoggingLevel(logLevel) >= loggingLevel; - - /// - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) - { - if (!IsEnabled(logLevel)) - { - return; - } - - Throw.IfNull(formatter); - - LogInternal(logLevel, formatter(state, exception)); - - void LogInternal(LogLevel level, string message) - { - _ = _server.SendNotificationAsync(NotificationMethods.LoggingMessageNotification, new LoggingMessageNotificationParams - { - Level = McpServerImpl.ToLoggingLevel(level), - Data = JsonSerializer.SerializeToElement(message, McpJsonUtilities.JsonContext.Default.String), - Logger = _categoryName, - }); - } - } - } - } } diff --git a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs index 98edd3fc2..170da17e0 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerExtensions.cs @@ -25,7 +25,7 @@ public static class McpServerExtensions /// It allows detailed control over sampling parameters including messages, system prompt, temperature, /// and token limits. /// - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead.")] + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask SampleAsync( this IMcpServer server, CreateMessageRequestParams request, CancellationToken cancellationToken = default) => AsServerOrThrow(server).SampleAsync(request, cancellationToken); @@ -45,7 +45,7 @@ public static ValueTask SampleAsync( /// This method converts the provided chat messages into a format suitable for the sampling API, /// handling different content types such as text, images, and audio. /// - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead.")] + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.SampleAsync)} instead.") // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774] public static Task SampleAsync( this IMcpServer server, IEnumerable messages, ChatOptions? options = default, CancellationToken cancellationToken = default) @@ -58,14 +58,14 @@ public static Task SampleAsync( /// The that can be used to issue sampling requests to the client. /// is . /// The client does not support sampling. - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead.")] + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static IChatClient AsSamplingChatClient(this IMcpServer server) => AsServerOrThrow(server).AsSamplingChatClient(); /// Gets an on which logged messages will be sent as notifications to the client. /// The server to wrap as an . /// An that can be used to log to the client.. - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead.")] + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.AsSamplingChatClient)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server) => AsServerOrThrow(server).AsClientLoggerProvider(); @@ -84,7 +84,7 @@ public static ILoggerProvider AsClientLoggerProvider(this IMcpServer server) /// navigated and accessed by the server. These resources might include file systems, databases, /// or other structured data sources that the client makes available through the protocol. /// - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.RequestRootsAsync)} instead.")] + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.RequestRootsAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask RequestRootsAsync( this IMcpServer server, ListRootsRequestParams request, CancellationToken cancellationToken = default) => AsServerOrThrow(server).RequestRootsAsync(request, cancellationToken); @@ -101,7 +101,7 @@ public static ValueTask RequestRootsAsync( /// /// This method requires the client to support the elicitation capability. /// - [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.ElicitAsync)} instead.")] + [Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.ElicitAsync)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static ValueTask ElicitAsync( this IMcpServer server, ElicitRequestParams request, CancellationToken cancellationToken = default) => AsServerOrThrow(server).ElicitAsync(request, cancellationToken); diff --git a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs index 79384b7c3..00ecd8b13 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerFactory.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerFactory.cs @@ -10,7 +10,7 @@ namespace ModelContextProtocol.Server; /// This is the recommended way to create instances. /// The factory handles proper initialization of server instances with the required dependencies. /// -[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.Create)} instead.")] +[Obsolete($"Use {nameof(McpServer)}.{nameof(McpServer.Create)} instead.")] // See: https://github.com/modelcontextprotocol/csharp-sdk/issues/774 public static class McpServerFactory { /// From be2fee8af2bd9ff1c67a30da8a5337b909087200 Mon Sep 17 00:00:00 2001 From: Mackinnon Buck Date: Tue, 16 Sep 2025 09:43:37 -0700 Subject: [PATCH 15/15] Undo `docs/*.md` changes --- docs/concepts/elicitation/elicitation.md | 8 ++++---- docs/concepts/logging/logging.md | 18 +++++++++--------- docs/concepts/progress/progress.md | 16 ++++++++-------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/concepts/elicitation/elicitation.md b/docs/concepts/elicitation/elicitation.md index 522cfc7b5..ebda0979a 100644 --- a/docs/concepts/elicitation/elicitation.md +++ b/docs/concepts/elicitation/elicitation.md @@ -11,12 +11,12 @@ The **elicitation** feature allows servers to request additional information fro ### Server Support for Elicitation -Servers request structured data from users with the [ElicitAsync] extension method on [McpServer]. -The C# SDK registers an instance of [McpServer] with the dependency injection container, -so tools can simply add a parameter of type [McpServer] to their method signature to access it. +Servers request structured data from users with the [ElicitAsync] extension method on [IMcpServer]. +The C# SDK registers an instance of [IMcpServer] with the dependency injection container, +so tools can simply add a parameter of type [IMcpServer] to their method signature to access it. [ElicitAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServerExtensions.html#ModelContextProtocol_Server_McpServerExtensions_ElicitAsync_ModelContextProtocol_Server_IMcpServer_ModelContextProtocol_Protocol_ElicitRequestParams_System_Threading_CancellationToken_ -[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html +[IMcpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html The MCP Server must specify the schema of each input value it is requesting from the user. Only primitive types (string, number, boolean) are supported for elicitation requests. diff --git a/docs/concepts/logging/logging.md b/docs/concepts/logging/logging.md index 34ed436d9..411a61b1c 100644 --- a/docs/concepts/logging/logging.md +++ b/docs/concepts/logging/logging.md @@ -51,15 +51,15 @@ messages as there may not be an open connection to the client on which the log m The C# SDK provides an extension method [WithSetLoggingLevelHandler] on [IMcpServerBuilder] to allow the server to perform any special logic it wants to perform when a client sets the logging level. However, the -SDK already takes care of setting the [LoggingLevel] in the [McpServer], so most servers will not need to +SDK already takes care of setting the [LoggingLevel] in the [IMcpServer], so most servers will not need to implement this. -[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html +[IMcpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html [IMcpServerBuilder]: https://modelcontextprotocol.github.io/csharp-sdk/api/Microsoft.Extensions.DependencyInjection.IMcpServerBuilder.html [WithSetLoggingLevelHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/Microsoft.Extensions.DependencyInjection.McpServerBuilderExtensions.html#Microsoft_Extensions_DependencyInjection_McpServerBuilderExtensions_WithSetLoggingLevelHandler_Microsoft_Extensions_DependencyInjection_IMcpServerBuilder_System_Func_ModelContextProtocol_Server_RequestContext_ModelContextProtocol_Protocol_SetLevelRequestParams__System_Threading_CancellationToken_System_Threading_Tasks_ValueTask_ModelContextProtocol_Protocol_EmptyResult___ -[LoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html#ModelContextProtocol_Server_IMcpServer_LoggingLevel +[LoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html#ModelContextProtocol_Server_IMcpServer_LoggingLevel -MCP Servers using the MCP C# SDK can obtain an [ILoggerProvider] from the McpServer [AsClientLoggerProvider] extension method, +MCP Servers using the MCP C# SDK can obtain an [ILoggerProvider] from the IMcpServer [AsClientLoggerProvider] extension method, and from that can create an [ILogger] instance for logging messages that should be sent to the MCP client. [!code-csharp[](samples/server/Tools/LoggingTools.cs?name=snippet_LoggingConfiguration)] @@ -73,23 +73,23 @@ and from that can create an [ILogger] instance for logging messages that should When the server indicates that it supports logging, clients should configure the logging level to specify which messages the server should send to the client. -Clients should check if the server supports logging by checking the [Logging] property of the [ServerCapabilities] field of [McpClient]. +Clients should check if the server supports logging by checking the [Logging] property of the [ServerCapabilities] field of [IMcpClient]. -[McpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClient.html -[ServerCapabilities]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClient.html#ModelContextProtocol_Client_McpClient_ServerCapabilities +[IMcpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html +[ServerCapabilities]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html#ModelContextProtocol_Client_IMcpClient_ServerCapabilities [Logging]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Protocol.ServerCapabilities.html#ModelContextProtocol_Protocol_ServerCapabilities_Logging [!code-csharp[](samples/client/Program.cs?name=snippet_LoggingCapabilities)] If the server supports logging, the client should set the level of log messages it wishes to receive with -the [SetLoggingLevel] method on [McpClient]. If the client does not set a logging level, the server might choose +the [SetLoggingLevel] method on [IMcpClient]. If the client does not set a logging level, the server might choose to send all log messages or none -- this is not specified in the protocol -- so it is important that the client sets a logging level to ensure it receives the desired log messages and only those messages. The `loggingLevel` set by the client is an MCP logging level. See the [Logging Levels](#logging-levels) section above for the mapping between MCP and .NET logging levels. -[SetLoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClientExtensions.html#ModelContextProtocol_Client_McpClientExtensions_SetLoggingLevel_ModelContextProtocol_Client_McpClient_Microsoft_Extensions_Logging_LogLevel_System_Threading_CancellationToken_ +[SetLoggingLevel]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClientExtensions.html#ModelContextProtocol_Client_McpClientExtensions_SetLoggingLevel_ModelContextProtocol_Client_IMcpClient_Microsoft_Extensions_Logging_LogLevel_System_Threading_CancellationToken_ [!code-csharp[](samples/client/Program.cs?name=snippet_LoggingLevel)] diff --git a/docs/concepts/progress/progress.md b/docs/concepts/progress/progress.md index 223146bc0..ccdf9f19c 100644 --- a/docs/concepts/progress/progress.md +++ b/docs/concepts/progress/progress.md @@ -17,14 +17,14 @@ This project illustrates the common case of a server tool that performs a long-r ### Server Implementation -When processing a request, the server can use the [sendNotificationAsync] extension method of [McpServer] to send progress updates, +When processing a request, the server can use the [sendNotificationAsync] extension method of [IMcpServer] to send progress updates, specifying `"notifications/progress"` as the notification method name. -The C# SDK registers an instance of [McpServer] with the dependency injection container, -so tools can simply add a parameter of type [McpServer] to their method signature to access it. +The C# SDK registers an instance of [IMcpServer] with the dependency injection container, +so tools can simply add a parameter of type [IMcpServer] to their method signature to access it. The parameters passed to [sendNotificationAsync] should be an instance of [ProgressNotificationParams], which includes the current progress, total steps, and an optional message. -[sendNotificationAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpEndpointExtensions.html#ModelContextProtocol_McpEndpointExtensions_SendNotificationAsync_ModelContextProtocol_McpSession_System_String_System_Threading_CancellationToken_ -[McpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.McpServer.html +[sendNotificationAsync]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpEndpointExtensions.html#ModelContextProtocol_McpEndpointExtensions_SendNotificationAsync_ModelContextProtocol_IMcpEndpoint_System_String_System_Threading_CancellationToken_ +[IMcpServer]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Server.IMcpServer.html [ProgressNotificationParams]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Protocol.ProgressNotificationParams.html The server must verify that the caller provided a `progressToken` in the request and include it in the call to [sendNotificationAsync]. The following example demonstrates how a server can send a progress notification: @@ -38,10 +38,10 @@ Note that servers are not required to support progress tracking, so clients shou In the MCP C# SDK, clients can specify a `progressToken` in the request parameters when calling a tool method. The client should also provide a notification handler to process "notifications/progress" notifications. -There are two way to do this. The first is to register a notification handler using the [RegisterNotificationHandler] method on the [McpClient] instance. A handler registered this way will receive all progress notifications sent by the server. +There are two way to do this. The first is to register a notification handler using the [RegisterNotificationHandler] method on the [IMcpClient] instance. A handler registered this way will receive all progress notifications sent by the server. -[McpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.McpClient.html -[RegisterNotificationHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.McpSession.html#ModelContextProtocol_McpSession_RegisterNotificationHandler_System_String_System_Func_ModelContextProtocol_Protocol_JsonRpcNotification_System_Threading_CancellationToken_System_Threading_Tasks_ValueTask__ +[IMcpClient]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.Client.IMcpClient.html +[RegisterNotificationHandler]: https://modelcontextprotocol.github.io/csharp-sdk/api/ModelContextProtocol.IMcpEndpoint.html#ModelContextProtocol_IMcpEndpoint_RegisterNotificationHandler_System_String_System_Func_ModelContextProtocol_Protocol_JsonRpcNotification_System_Threading_CancellationToken_System_Threading_Tasks_ValueTask__ ```csharp mcpClient.RegisterNotificationHandler(NotificationMethods.ProgressNotification,