Skip to content

Commit 42bc783

Browse files
Copilotstephentoub
andcommitted
Fix Extensions not being copied to ServerCapabilities and add tests
In McpServerImpl.ConfigureExperimental, the Extensions property from the options was not being copied to the server's ServerCapabilities. This caused Extensions to be missing from the initialize response. Also added: - Test for Extensions in initialize response - Test for Experimental in initialize response - Reflection-based test to catch future missing capability copies Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent bbf96f6 commit 42bc783

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

src/ModelContextProtocol.Core/Server/McpServerImpl.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ private void ConfigureCompletion(McpServerOptions options)
265265
private void ConfigureExperimental(McpServerOptions options)
266266
{
267267
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
268+
ServerCapabilities.ExtensionsCore = options.Capabilities?.ExtensionsCore;
268269
}
269270

270271
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+
#pragma warning disable MCPEXP001
294+
[Fact]
295+
public async Task Initialize_IncludesExtensionsInResponse()
296+
{
297+
await Can_Handle_Requests(
298+
serverCapabilities: new ServerCapabilities
299+
{
300+
Extensions = new Dictionary<string, object> { ["io.myext"] = new JsonObject { ["required"] = true } },
301+
},
302+
method: RequestMethods.Initialize,
303+
configureOptions: null,
304+
assertResult: (_, response) =>
305+
{
306+
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
307+
Assert.NotNull(result);
308+
Assert.NotNull(result.Capabilities.Extensions);
309+
Assert.True(result.Capabilities.Extensions.ContainsKey("io.myext"));
310+
});
311+
}
312+
313+
[Fact]
314+
public async Task Initialize_IncludesExperimentalInResponse()
315+
{
316+
await Can_Handle_Requests(
317+
serverCapabilities: new ServerCapabilities
318+
{
319+
Experimental = new Dictionary<string, object> { ["customFeature"] = new JsonObject { ["enabled"] = true } },
320+
},
321+
method: RequestMethods.Initialize,
322+
configureOptions: null,
323+
assertResult: (_, response) =>
324+
{
325+
var result = JsonSerializer.Deserialize<InitializeResult>(response, McpJsonUtilities.DefaultOptions);
326+
Assert.NotNull(result);
327+
Assert.NotNull(result.Capabilities.Experimental);
328+
Assert.True(result.Capabilities.Experimental.ContainsKey("customFeature"));
329+
});
330+
}
331+
332+
[Fact]
333+
public async Task Initialize_CopiesAllCapabilityProperties()
334+
{
335+
// Set every public property on ServerCapabilities to a non-null value.
336+
// If a new property is added to ServerCapabilities in the future but the
337+
// server fails to copy it, this test will fail as a reminder.
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)