diff --git a/ModelContextProtocol.sln b/ModelContextProtocol.sln index 064dc40d..1ceb3a23 100644 --- a/ModelContextProtocol.sln +++ b/ModelContextProtocol.sln @@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickstartWeatherServer", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickstartClient", "samples\QuickstartClient\QuickstartClient.csproj", "{0D1552DC-E6ED-4AAC-5562-12F8352F46AA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EverythingServer", "samples\EverythingServer\EverythingServer.csproj", "{17B8453F-AB72-99C5-E5EA-D0B065A6AE65}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModelContextProtocol.AspNetCore", "src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj", "{37B6A5E0-9995-497D-8B43-3BC6870CC716}" EndProject Global @@ -94,6 +96,10 @@ Global {0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Debug|Any CPU.Build.0 = Debug|Any CPU {0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Release|Any CPU.ActiveCfg = Release|Any CPU {0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Release|Any CPU.Build.0 = Release|Any CPU + {17B8453F-AB72-99C5-E5EA-D0B065A6AE65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17B8453F-AB72-99C5-E5EA-D0B065A6AE65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17B8453F-AB72-99C5-E5EA-D0B065A6AE65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17B8453F-AB72-99C5-E5EA-D0B065A6AE65}.Release|Any CPU.Build.0 = Release|Any CPU {37B6A5E0-9995-497D-8B43-3BC6870CC716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {37B6A5E0-9995-497D-8B43-3BC6870CC716}.Debug|Any CPU.Build.0 = Debug|Any CPU {37B6A5E0-9995-497D-8B43-3BC6870CC716}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -113,6 +119,7 @@ Global {0C6D0512-D26D-63D3-5019-C5F7A657B28C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {0D1552DC-E6ED-4AAC-5562-12F8352F46AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {17B8453F-AB72-99C5-E5EA-D0B065A6AE65} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {37B6A5E0-9995-497D-8B43-3BC6870CC716} = {A2F1F52A-9107-4BF8-8C3F-2F6670E7D0AD} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/samples/EverythingServer/EverythingServer.csproj b/samples/EverythingServer/EverythingServer.csproj new file mode 100644 index 00000000..3aee2bc2 --- /dev/null +++ b/samples/EverythingServer/EverythingServer.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + Exe + + + + + + + + + + + diff --git a/samples/EverythingServer/LoggingUpdateMessageSender.cs b/samples/EverythingServer/LoggingUpdateMessageSender.cs new file mode 100644 index 00000000..7b64aa2c --- /dev/null +++ b/samples/EverythingServer/LoggingUpdateMessageSender.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ModelContextProtocol; +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; + +namespace EverythingServer; + +public class LoggingUpdateMessageSender(IMcpServer server, Func getMinLevel) : BackgroundService +{ + readonly Dictionary _loggingLevelMap = new() + { + { LoggingLevel.Debug, "Debug-level message" }, + { LoggingLevel.Info, "Info-level message" }, + { LoggingLevel.Notice, "Notice-level message" }, + { LoggingLevel.Warning, "Warning-level message" }, + { LoggingLevel.Error, "Error-level message" }, + { LoggingLevel.Critical, "Critical-level message" }, + { LoggingLevel.Alert, "Alert-level message" }, + { LoggingLevel.Emergency, "Emergency-level message" } + }; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + var newLevel = (LoggingLevel)Random.Shared.Next(_loggingLevelMap.Count); + + var message = new + { + Level = newLevel.ToString().ToLower(), + Data = _loggingLevelMap[newLevel], + }; + + if (newLevel > getMinLevel()) + { + await server.SendNotificationAsync("notifications/message", message, cancellationToken: stoppingToken); + } + + await Task.Delay(15000, stoppingToken); + } + } +} \ No newline at end of file diff --git a/samples/EverythingServer/Program.cs b/samples/EverythingServer/Program.cs new file mode 100644 index 00000000..17ee0753 --- /dev/null +++ b/samples/EverythingServer/Program.cs @@ -0,0 +1,194 @@ +using EverythingServer; +using EverythingServer.Prompts; +using EverythingServer.Tools; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using ModelContextProtocol; +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; + +var builder = Host.CreateApplicationBuilder(args); +builder.Logging.AddConsole(consoleLogOptions => +{ + // Configure all logs to go to stderr + consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; +}); + +HashSet subscriptions = []; +var _minimumLoggingLevel = LoggingLevel.Debug; + +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithTools() + .WithPrompts() + .WithPrompts() + .WithListResourceTemplatesHandler((ctx, ct) => + { + return Task.FromResult(new ListResourceTemplatesResult + { + ResourceTemplates = + [ + new ResourceTemplate { Name = "Static Resource", Description = "A static resource with a numeric ID", UriTemplate = "test://static/resource/{id}" } + ] + }); + }) + .WithReadResourceHandler((ctx, ct) => + { + var uri = ctx.Params?.Uri; + + if (uri is null || !uri.StartsWith("test://static/resource/")) + { + throw new NotSupportedException($"Unknown resource: {uri}"); + } + + int index = int.Parse(uri["test://static/resource/".Length..]) - 1; + + if (index < 0 || index >= ResourceGenerator.Resources.Count) + { + throw new NotSupportedException($"Unknown resource: {uri}"); + } + + var resource = ResourceGenerator.Resources[index]; + + if (resource.MimeType == "text/plain") + { + return Task.FromResult(new ReadResourceResult + { + Contents = [new TextResourceContents + { + Text = resource.Description!, + MimeType = resource.MimeType, + Uri = resource.Uri, + }] + }); + } + else + { + return Task.FromResult(new ReadResourceResult + { + Contents = [new BlobResourceContents + { + Blob = resource.Description!, + MimeType = resource.MimeType, + Uri = resource.Uri, + }] + }); + } + }) + .WithSubscribeToResourcesHandler(async (ctx, ct) => + { + var uri = ctx.Params?.Uri; + + if (uri is not null) + { + subscriptions.Add(uri); + + await ctx.Server.RequestSamplingAsync([ + new ChatMessage(ChatRole.System, "You are a helpful test server"), + new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"), + ], + options: new ChatOptions + { + MaxOutputTokens = 100, + Temperature = 0.7f, + }, + cancellationToken: ct); + } + + return new EmptyResult(); + }) + .WithUnsubscribeFromResourcesHandler((ctx, ct) => + { + var uri = ctx.Params?.Uri; + if (uri is not null) + { + subscriptions.Remove(uri); + } + return Task.FromResult(new EmptyResult()); + }) + .WithGetCompletionHandler((ctx, ct) => + { + var exampleCompletions = new Dictionary> + { + { "style", ["casual", "formal", "technical", "friendly"] }, + { "temperature", ["0", "0.5", "0.7", "1.0"] }, + { "resourceId", ["1", "2", "3", "4", "5"] } + }; + + if (ctx.Params is not { } @params) + { + throw new NotSupportedException($"Params are required."); + } + + var @ref = @params.Ref; + var argument = @params.Argument; + + if (@ref.Type == "ref/resource") + { + var resourceId = @ref.Uri?.Split("/").Last(); + + if (resourceId is null) + { + return Task.FromResult(new CompleteResult()); + } + + var values = exampleCompletions["resourceId"].Where(id => id.StartsWith(argument.Value)); + + return Task.FromResult(new CompleteResult + { + Completion = new Completion { Values = [..values], HasMore = false, Total = values.Count() } + }); + } + + if (@ref.Type == "ref/prompt") + { + if (!exampleCompletions.TryGetValue(argument.Name, out IEnumerable? value)) + { + throw new NotSupportedException($"Unknown argument name: {argument.Name}"); + } + + var values = value.Where(value => value.StartsWith(argument.Value)); + return Task.FromResult(new CompleteResult + { + Completion = new Completion { Values = [..values], HasMore = false, Total = values.Count() } + }); + } + + throw new NotSupportedException($"Unknown reference type: {@ref.Type}"); + }) + .WithSetLoggingLevelHandler(async (ctx, ct) => + { + if (ctx.Params?.Level is null) + { + throw new McpException("Missing required argument 'level'"); + } + + _minimumLoggingLevel = ctx.Params.Level; + + await ctx.Server.SendNotificationAsync("notifications/message", new + { + Level = "debug", + Logger = "test-server", + Data = $"Logging level set to {_minimumLoggingLevel}", + }, cancellationToken: ct); + + return new EmptyResult(); + }) + ; + +builder.Services.AddSingleton(subscriptions); +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); + +builder.Services.AddSingleton>(_ => () => _minimumLoggingLevel); + +await builder.Build().RunAsync(); diff --git a/samples/EverythingServer/Prompts/ComplexPromptType.cs b/samples/EverythingServer/Prompts/ComplexPromptType.cs new file mode 100644 index 00000000..8b47a07e --- /dev/null +++ b/samples/EverythingServer/Prompts/ComplexPromptType.cs @@ -0,0 +1,22 @@ +using EverythingServer.Tools; +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace EverythingServer.Prompts; + +[McpServerPromptType] +public class ComplexPromptType +{ + [McpServerPrompt(Name = "complex_prompt"), Description("A prompt with arguments")] + public static IEnumerable ComplexPrompt( + [Description("Temperature setting")] int temperature, + [Description("Output style")] string? style = null) + { + return [ + new ChatMessage(ChatRole.User,$"This is a complex prompt with arguments: temperature={temperature}, style={style}"), + new ChatMessage(ChatRole.Assistant, "I understand. You've provided a complex prompt with temperature and style arguments. How would you like me to proceed?"), + new ChatMessage(ChatRole.User, [new DataContent(TinyImageTool.MCP_TINY_IMAGE)]) + ]; + } +} diff --git a/samples/EverythingServer/Prompts/SimplePromptType.cs b/samples/EverythingServer/Prompts/SimplePromptType.cs new file mode 100644 index 00000000..d6ba51a3 --- /dev/null +++ b/samples/EverythingServer/Prompts/SimplePromptType.cs @@ -0,0 +1,11 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace EverythingServer.Prompts; + +[McpServerPromptType] +public class SimplePromptType +{ + [McpServerPrompt(Name = "simple_prompt"), Description("A prompt without arguments")] + public static string SimplePrompt() => "This is a simple prompt without arguments"; +} diff --git a/samples/EverythingServer/ResourceGenerator.cs b/samples/EverythingServer/ResourceGenerator.cs new file mode 100644 index 00000000..54764b8c --- /dev/null +++ b/samples/EverythingServer/ResourceGenerator.cs @@ -0,0 +1,37 @@ +using ModelContextProtocol.Protocol.Types; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EverythingServer; + +static class ResourceGenerator +{ + private static readonly List _resources = Enumerable.Range(1, 100).Select(i => + { + var uri = $"test://static/resource/{i}"; + if (i % 2 != 0) + { + return new Resource + { + Uri = uri, + Name = $"Resource {i}", + MimeType = "text/plain", + Description = $"Resource {i}: This is a plaintext resource" + }; + } + else + { + var buffer = System.Text.Encoding.UTF8.GetBytes($"Resource {i}: This is a base64 blob"); + return new Resource + { + Uri = uri, + Name = $"Resource {i}", + MimeType = "application/octet-stream", + Description = Convert.ToBase64String(buffer) + }; + } + }).ToList(); + + public static IReadOnlyList Resources => _resources; +} \ No newline at end of file diff --git a/samples/EverythingServer/SubscriptionMessageSender.cs b/samples/EverythingServer/SubscriptionMessageSender.cs new file mode 100644 index 00000000..774d9852 --- /dev/null +++ b/samples/EverythingServer/SubscriptionMessageSender.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Hosting; +using ModelContextProtocol; +using ModelContextProtocol.Server; + +internal class SubscriptionMessageSender(IMcpServer server, HashSet subscriptions) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + foreach (var uri in subscriptions) + { + await server.SendNotificationAsync("notifications/resource/updated", + new + { + Uri = uri, + }, cancellationToken: stoppingToken); + } + + await Task.Delay(5000, stoppingToken); + } + } +} diff --git a/samples/EverythingServer/Tools/AddTool.cs b/samples/EverythingServer/Tools/AddTool.cs new file mode 100644 index 00000000..ccaa306d --- /dev/null +++ b/samples/EverythingServer/Tools/AddTool.cs @@ -0,0 +1,11 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace EverythingServer.Tools; + +[McpServerToolType] +public class AddTool +{ + [McpServerTool(Name = "add"), Description("Adds two numbers.")] + public static string Add(int a, int b) => $"The sum of {a} and {b} is {a + b}"; +} diff --git a/samples/EverythingServer/Tools/AnnotatedMessageTool.cs b/samples/EverythingServer/Tools/AnnotatedMessageTool.cs new file mode 100644 index 00000000..25027faf --- /dev/null +++ b/samples/EverythingServer/Tools/AnnotatedMessageTool.cs @@ -0,0 +1,56 @@ +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace EverythingServer.Tools; + +[McpServerToolType] +public class AnnotatedMessageTool +{ + public enum MessageType + { + Error, + Success, + Debug, + } + + [McpServerTool(Name = "annotatedMessage"), Description("Generates an annotated message")] + public static IEnumerable AnnotatedMessage(MessageType messageType, bool includeImage = true) + { + List contents = messageType switch + { + MessageType.Error => [new() + { + Type = "text", + Text = "Error: Operation failed", + Annotations = new() { Audience = [Role.User, Role.Assistant], Priority = 1.0f } + }], + MessageType.Success => [new() + { + Type = "text", + Text = "Operation completed successfully", + Annotations = new() { Audience = [Role.User], Priority = 0.7f } + }], + MessageType.Debug => [new() + { + Type = "text", + Text = "Debug: Cache hit ratio 0.95, latency 150ms", + Annotations = new() { Audience = [Role.Assistant], Priority = 0.3f } + }], + _ => throw new ArgumentOutOfRangeException(nameof(messageType), messageType, null) + }; + + if (includeImage) + { + contents.Add(new() + { + Type = "image", + Data = TinyImageTool.MCP_TINY_IMAGE.Split(",").Last(), + MimeType = "image/png", + Annotations = new() { Audience = [Role.User], Priority = 0.5f } + }); + } + + return contents; + } +} diff --git a/samples/EverythingServer/Tools/EchoTool.cs b/samples/EverythingServer/Tools/EchoTool.cs new file mode 100644 index 00000000..6abd6d36 --- /dev/null +++ b/samples/EverythingServer/Tools/EchoTool.cs @@ -0,0 +1,11 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace EverythingServer.Tools; + +[McpServerToolType] +public class EchoTool +{ + [McpServerTool(Name = "echo"), Description("Echoes the message back to the client.")] + public static string Echo(string message) => $"Echo: {message}"; +} diff --git a/samples/EverythingServer/Tools/LongRunningTool.cs b/samples/EverythingServer/Tools/LongRunningTool.cs new file mode 100644 index 00000000..86acc84d --- /dev/null +++ b/samples/EverythingServer/Tools/LongRunningTool.cs @@ -0,0 +1,39 @@ +using ModelContextProtocol; +using ModelContextProtocol.Protocol.Messages; +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace EverythingServer.Tools; + +[McpServerToolType] +public class LongRunningTool +{ + [McpServerTool(Name = "longRunningOperation"), Description("Demonstrates a long running operation with progress updates")] + public static async Task LongRunningOperation( + IMcpServer server, + RequestContext context, + int duration = 10, + int steps = 5) + { + var progressToken = context.Params?.Meta?.ProgressToken; + var stepDuration = duration / steps; + + for (int i = 1; i <= steps + 1; i++) + { + await Task.Delay(stepDuration * 1000); + + if (progressToken is not null) + { + await server.SendNotificationAsync("notifications/progress", new + { + Progress = i, + Total = steps, + progressToken + }); + } + } + + return $"Long running operation completed. Duration: {duration} seconds. Steps: {steps}."; + } +} diff --git a/samples/EverythingServer/Tools/PrintEnvTool.cs b/samples/EverythingServer/Tools/PrintEnvTool.cs new file mode 100644 index 00000000..ca289b5f --- /dev/null +++ b/samples/EverythingServer/Tools/PrintEnvTool.cs @@ -0,0 +1,18 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Text.Json; + +namespace EverythingServer.Tools; + +[McpServerToolType] +public class PrintEnvTool +{ + private static readonly JsonSerializerOptions options = new() + { + WriteIndented = true + }; + + [McpServerTool(Name = "printEnv"), Description("Prints all environment variables, helpful for debugging MCP server configuration")] + public static string PrintEnv() => + JsonSerializer.Serialize(Environment.GetEnvironmentVariables(), options); +} diff --git a/samples/EverythingServer/Tools/SampleLlmTool.cs b/samples/EverythingServer/Tools/SampleLlmTool.cs new file mode 100644 index 00000000..53ebfff2 --- /dev/null +++ b/samples/EverythingServer/Tools/SampleLlmTool.cs @@ -0,0 +1,42 @@ +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace EverythingServer.Tools; + +[McpServerToolType] +public class SampleLlmTool +{ + [McpServerTool(Name = "sampleLLM"), Description("Samples from an LLM using MCP's sampling feature")] + public static async Task SampleLLM( + IMcpServer server, + [Description("The prompt to send to the LLM")] string prompt, + [Description("Maximum number of tokens to generate")] int maxTokens, + CancellationToken cancellationToken) + { + var samplingParams = CreateRequestSamplingParams(prompt ?? string.Empty, "sampleLLM", maxTokens); + var sampleResult = await server.RequestSamplingAsync(samplingParams, cancellationToken); + + return $"LLM sampling result: {sampleResult.Content.Text}"; + } + + private static CreateMessageRequestParams CreateRequestSamplingParams(string context, string uri, int maxTokens = 100) + { + return new CreateMessageRequestParams() + { + Messages = [new SamplingMessage() + { + Role = Role.User, + Content = new Content() + { + Type = "text", + Text = $"Resource {uri} context: {context}" + } + }], + SystemPrompt = "You are a helpful test server.", + MaxTokens = maxTokens, + Temperature = 0.7f, + IncludeContext = ContextInclusion.ThisServer + }; + } +} diff --git a/samples/EverythingServer/Tools/TinyImageTool.cs b/samples/EverythingServer/Tools/TinyImageTool.cs new file mode 100644 index 00000000..bd88ce98 --- /dev/null +++ b/samples/EverythingServer/Tools/TinyImageTool.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.AI; +using ModelContextProtocol.Server; +using System.ComponentModel; + +namespace EverythingServer.Tools; + +[McpServerToolType] +public class TinyImageTool +{ + [McpServerTool(Name = "getTinyImage"), Description("Get a tiny image from the server")] + public static IEnumerable GetTinyImage() => [ + new TextContent("This is a tiny image:"), + new DataContent(MCP_TINY_IMAGE), + new TextContent("The image above is the MCP tiny image.") + ]; + + internal const string MCP_TINY_IMAGE = + "data:image/png;base64,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"; +} diff --git a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs index a59de8ce..a564fd39 100644 --- a/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs +++ b/src/ModelContextProtocol/Configuration/McpServerBuilderExtensions.cs @@ -314,7 +314,7 @@ public static IMcpServerBuilder WithSubscribeToResourcesHandler(this IMcpServerB } /// - /// Sets or sets the handler for subscribe to resources messages. + /// Sets the handler for subscribe to resources messages. /// /// The builder instance. /// The handler. @@ -325,6 +325,18 @@ public static IMcpServerBuilder WithUnsubscribeFromResourcesHandler(this IMcpSer builder.Services.Configure(s => s.UnsubscribeFromResourcesHandler = handler); return builder; } + + /// + /// Sets the handler for setting the logging level. + /// + /// The builder instance. + /// The handler. + public static IMcpServerBuilder WithSetLoggingLevelHandler(this IMcpServerBuilder builder, Func, CancellationToken, Task> handler) + { + Throw.IfNull(builder); + builder.Services.Configure(s => s.SetLoggingLevelHandler = handler); + return builder; + } #endregion #region Transports diff --git a/src/ModelContextProtocol/Server/McpServerHandlers.cs b/src/ModelContextProtocol/Server/McpServerHandlers.cs index e472a7ae..b591182d 100644 --- a/src/ModelContextProtocol/Server/McpServerHandlers.cs +++ b/src/ModelContextProtocol/Server/McpServerHandlers.cs @@ -113,6 +113,7 @@ internal void OverwriteWithSetHandlers(McpServerOptions options) options.Capabilities.Prompts = promptsCapability; options.Capabilities.Resources = resourcesCapability; options.Capabilities.Tools = toolsCapability; + options.Capabilities.Logging = loggingCapability; options.GetCompletionHandler = GetCompletionHandler ?? options.GetCompletionHandler; } diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs new file mode 100644 index 00000000..a97fe6f3 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Server/McpServerLoggingLevelTests.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Protocol.Types; +using ModelContextProtocol.Server; + +namespace ModelContextProtocol.Tests.Server; +public class McpServerLoggingLevelTests +{ + [Fact] + public void CanCreateServerWithLoggingLevelHandler() + { + var services = new ServiceCollection(); + + services.AddMcpServer() + .WithStdioServerTransport() + .WithSetLoggingLevelHandler((ctx, ct) => + { + return Task.FromResult(new EmptyResult()); + }); + + var provider = services.BuildServiceProvider(); + + provider.GetRequiredService(); + } + + [Fact] + public void AddingLoggingLevelHandlerSetsLoggingCapability() + { + var services = new ServiceCollection(); + + services.AddMcpServer() + .WithStdioServerTransport() + .WithSetLoggingLevelHandler((ctx, ct) => + { + return Task.FromResult(new EmptyResult()); + }); + + var provider = services.BuildServiceProvider(); + + var server = provider.GetRequiredService(); + + Assert.NotNull(server.ServerOptions.Capabilities?.Logging); + Assert.NotNull(server.ServerOptions.Capabilities.Logging.SetLoggingLevelHandler); + } + + [Fact] + public void ServerWithoutCallingLoggingLevelHandlerDoesNotSetLoggingCapability() + { + var services = new ServiceCollection(); + services.AddMcpServer() + .WithStdioServerTransport(); + var provider = services.BuildServiceProvider(); + var server = provider.GetRequiredService(); + Assert.Null(server.ServerOptions.Capabilities?.Logging); + } +}