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);
+ }
+}