Skip to content

Commit 1380fa5

Browse files
authored
Merge branch 'main' into copilot/implement-allowed-values-completion
2 parents b61ed6c + 94aa695 commit 1380fa5

File tree

2 files changed

+93
-3
lines changed

2 files changed

+93
-3
lines changed

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public McpServerImpl(ITransport transport, McpServerOptions options, ILoggerFact
9090
ConfigureTasks(options);
9191
ConfigureLogging(options);
9292
ConfigureCompletion(options);
93-
ConfigureExperimental(options);
93+
ConfigureExperimentalAndExtensions(options);
9494

9595
// Register any notification handlers that were provided.
9696
if (options.Handlers.NotificationHandlers is { } notificationHandlers)
@@ -138,7 +138,7 @@ void Register<TPrimitive>(McpServerPrimitiveCollection<TPrimitive>? collection,
138138
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
139139

140140
/// <inheritdoc/>
141-
public ServerCapabilities ServerCapabilities { get; } = new();
141+
public ServerCapabilities ServerCapabilities { get; }
142142

143143
/// <inheritdoc />
144144
public override ClientCapabilities? ClientCapabilities => _clientCapabilities;
@@ -382,9 +382,10 @@ private void ConfigureCompletion(McpServerOptions options)
382382
return result;
383383
}
384384

385-
private void ConfigureExperimental(McpServerOptions options)
385+
private void ConfigureExperimentalAndExtensions(McpServerOptions options)
386386
{
387387
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
388+
ServerCapabilities.Extensions = options.Capabilities?.Extensions;
388389
}
389390

390391
private void ConfigureResources(McpServerOptions options)

tests/ModelContextProtocol.Tests/Server/McpServerTests.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,95 @@ await Can_Handle_Requests(
290290
});
291291
}
292292

293+
[Fact]
294+
public async Task Initialize_IncludesExtensionsInResponse()
295+
{
296+
await Can_Handle_Requests(
297+
serverCapabilities: new ServerCapabilities
298+
{
299+
Extensions = new Dictionary<string, object> { ["io.myext"] = new JsonObject { ["required"] = true } },
300+
},
301+
method: RequestMethods.Initialize,
302+
configureOptions: null,
303+
assertResult: (_, response) =>
304+
{
305+
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
306+
Assert.NotNull(result);
307+
Assert.NotNull(result.Capabilities.Extensions);
308+
Assert.True(result.Capabilities.Extensions.ContainsKey("io.myext"));
309+
});
310+
}
311+
312+
[Fact]
313+
public async Task Initialize_IncludesExperimentalInResponse()
314+
{
315+
await Can_Handle_Requests(
316+
serverCapabilities: new ServerCapabilities
317+
{
318+
Experimental = new Dictionary<string, object> { ["customFeature"] = new JsonObject { ["enabled"] = true } },
319+
},
320+
method: RequestMethods.Initialize,
321+
configureOptions: null,
322+
assertResult: (_, response) =>
323+
{
324+
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
325+
Assert.NotNull(result);
326+
Assert.NotNull(result.Capabilities.Experimental);
327+
Assert.True(result.Capabilities.Experimental.ContainsKey("customFeature"));
328+
});
329+
}
330+
331+
[Fact]
332+
public async Task Initialize_CopiesAllCapabilityProperties()
333+
{
334+
// Set every public property on ServerCapabilities to a non-null value.
335+
// If a new property is added to ServerCapabilities in the future but the
336+
// server fails to copy it, this reflection-based test will automatically
337+
// detect the missing property and fail.
338+
var inputCapabilities = new ServerCapabilities
339+
{
340+
Experimental = new Dictionary<string, object> { ["test"] = new JsonObject() },
341+
Logging = new LoggingCapability(),
342+
Prompts = new PromptsCapability(),
343+
Resources = new ResourcesCapability(),
344+
Tools = new ToolsCapability(),
345+
Completions = new CompletionsCapability(),
346+
Tasks = new McpTasksCapability(),
347+
Extensions = new Dictionary<string, object> { ["io.test"] = new JsonObject() },
348+
};
349+
350+
await Can_Handle_Requests(
351+
serverCapabilities: inputCapabilities,
352+
method: RequestMethods.Initialize,
353+
configureOptions: options =>
354+
{
355+
// Tasks capability requires a TaskStore
356+
options.TaskStore = new InMemoryMcpTaskStore();
357+
},
358+
assertResult: (_, response) =>
359+
{
360+
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
361+
Assert.NotNull(result);
362+
363+
// Use reflection to verify every public property on ServerCapabilities is non-null.
364+
// This catches cases where new capability properties are added but not copied
365+
// from options in McpServerImpl.
366+
foreach (var property in typeof(ServerCapabilities).GetProperties(BindingFlags.Public | BindingFlags.Instance))
367+
{
368+
if (!property.CanRead)
369+
{
370+
continue;
371+
}
372+
373+
Assert.True(
374+
property.GetValue(result.Capabilities) is not null,
375+
$"ServerCapabilities.{property.Name} was set on options but is null in the initialize response. " +
376+
$"Ensure the property is copied in McpServerImpl's Configure* methods.");
377+
}
378+
});
379+
}
380+
#pragma warning restore MCPEXP001
381+
293382
[Fact]
294383
public async Task Can_Handle_Completion_Requests()
295384
{

0 commit comments

Comments
 (0)