diff --git a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs index 49fadb5e4..d9fd0e2e2 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerImpl.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerImpl.cs @@ -90,7 +90,7 @@ public McpServerImpl(ITransport transport, McpServerOptions options, ILoggerFact ConfigureTasks(options); ConfigureLogging(options); ConfigureCompletion(options); - ConfigureExperimental(options); + ConfigureExperimentalAndExtensions(options); // Register any notification handlers that were provided. if (options.Handlers.NotificationHandlers is { } notificationHandlers) @@ -138,7 +138,7 @@ void Register(McpServerPrimitiveCollection? collection, public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion; /// - public ServerCapabilities ServerCapabilities { get; } = new(); + public ServerCapabilities ServerCapabilities { get; } /// public override ClientCapabilities? ClientCapabilities => _clientCapabilities; @@ -262,9 +262,10 @@ private void ConfigureCompletion(McpServerOptions options) McpJsonUtilities.JsonContext.Default.CompleteResult); } - private void ConfigureExperimental(McpServerOptions options) + private void ConfigureExperimentalAndExtensions(McpServerOptions options) { ServerCapabilities.Experimental = options.Capabilities?.Experimental; + ServerCapabilities.Extensions = options.Capabilities?.Extensions; } private void ConfigureResources(McpServerOptions options) diff --git a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs index 53fd50685..ea680ecf0 100644 --- a/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs +++ b/tests/ModelContextProtocol.Tests/Server/McpServerTests.cs @@ -290,6 +290,95 @@ await Can_Handle_Requests( }); } + [Fact] + public async Task Initialize_IncludesExtensionsInResponse() + { + await Can_Handle_Requests( + serverCapabilities: new ServerCapabilities + { + Extensions = new Dictionary { ["io.myext"] = new JsonObject { ["required"] = true } }, + }, + method: RequestMethods.Initialize, + configureOptions: null, + assertResult: (_, response) => + { + var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); + Assert.NotNull(result); + Assert.NotNull(result.Capabilities.Extensions); + Assert.True(result.Capabilities.Extensions.ContainsKey("io.myext")); + }); + } + + [Fact] + public async Task Initialize_IncludesExperimentalInResponse() + { + await Can_Handle_Requests( + serverCapabilities: new ServerCapabilities + { + Experimental = new Dictionary { ["customFeature"] = new JsonObject { ["enabled"] = true } }, + }, + method: RequestMethods.Initialize, + configureOptions: null, + assertResult: (_, response) => + { + var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); + Assert.NotNull(result); + Assert.NotNull(result.Capabilities.Experimental); + Assert.True(result.Capabilities.Experimental.ContainsKey("customFeature")); + }); + } + + [Fact] + public async Task Initialize_CopiesAllCapabilityProperties() + { + // Set every public property on ServerCapabilities to a non-null value. + // If a new property is added to ServerCapabilities in the future but the + // server fails to copy it, this reflection-based test will automatically + // detect the missing property and fail. + var inputCapabilities = new ServerCapabilities + { + Experimental = new Dictionary { ["test"] = new JsonObject() }, + Logging = new LoggingCapability(), + Prompts = new PromptsCapability(), + Resources = new ResourcesCapability(), + Tools = new ToolsCapability(), + Completions = new CompletionsCapability(), + Tasks = new McpTasksCapability(), + Extensions = new Dictionary { ["io.test"] = new JsonObject() }, + }; + + await Can_Handle_Requests( + serverCapabilities: inputCapabilities, + method: RequestMethods.Initialize, + configureOptions: options => + { + // Tasks capability requires a TaskStore + options.TaskStore = new InMemoryMcpTaskStore(); + }, + assertResult: (_, response) => + { + var result = JsonSerializer.Deserialize(response, McpJsonUtilities.DefaultOptions); + Assert.NotNull(result); + + // Use reflection to verify every public property on ServerCapabilities is non-null. + // This catches cases where new capability properties are added but not copied + // from options in McpServerImpl. + foreach (var property in typeof(ServerCapabilities).GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (!property.CanRead) + { + continue; + } + + Assert.True( + property.GetValue(result.Capabilities) is not null, + $"ServerCapabilities.{property.Name} was set on options but is null in the initialize response. " + + $"Ensure the property is copied in McpServerImpl's Configure* methods."); + } + }); + } +#pragma warning restore MCPEXP001 + [Fact] public async Task Can_Handle_Completion_Requests() {