Fix Extensions not being copied to ServerCapabilities during initialization (#1399)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
This commit is contained in:
parent
1578e940dd
commit
94aa6952eb
2 changed files with 93 additions and 3 deletions
|
|
@ -90,7 +90,7 @@ internal sealed partial class McpServerImpl : McpServer
|
||||||
ConfigureTasks(options);
|
ConfigureTasks(options);
|
||||||
ConfigureLogging(options);
|
ConfigureLogging(options);
|
||||||
ConfigureCompletion(options);
|
ConfigureCompletion(options);
|
||||||
ConfigureExperimental(options);
|
ConfigureExperimentalAndExtensions(options);
|
||||||
|
|
||||||
// Register any notification handlers that were provided.
|
// Register any notification handlers that were provided.
|
||||||
if (options.Handlers.NotificationHandlers is { } notificationHandlers)
|
if (options.Handlers.NotificationHandlers is { } notificationHandlers)
|
||||||
|
|
@ -138,7 +138,7 @@ internal sealed partial class McpServerImpl : McpServer
|
||||||
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
|
public override string? NegotiatedProtocolVersion => _negotiatedProtocolVersion;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ServerCapabilities ServerCapabilities { get; } = new();
|
public ServerCapabilities ServerCapabilities { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override ClientCapabilities? ClientCapabilities => _clientCapabilities;
|
public override ClientCapabilities? ClientCapabilities => _clientCapabilities;
|
||||||
|
|
@ -262,9 +262,10 @@ internal sealed partial class McpServerImpl : McpServer
|
||||||
McpJsonUtilities.JsonContext.Default.CompleteResult);
|
McpJsonUtilities.JsonContext.Default.CompleteResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureExperimental(McpServerOptions options)
|
private void ConfigureExperimentalAndExtensions(McpServerOptions options)
|
||||||
{
|
{
|
||||||
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
|
ServerCapabilities.Experimental = options.Capabilities?.Experimental;
|
||||||
|
ServerCapabilities.Extensions = options.Capabilities?.Extensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ConfigureResources(McpServerOptions options)
|
private void ConfigureResources(McpServerOptions options)
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,95 @@ public class McpServerTests : LoggedTest
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Initialize_IncludesExtensionsInResponse()
|
||||||
|
{
|
||||||
|
await Can_Handle_Requests(
|
||||||
|
serverCapabilities: new ServerCapabilities
|
||||||
|
{
|
||||||
|
Extensions = new Dictionary<string, object> { ["io.myext"] = new JsonObject { ["required"] = true } },
|
||||||
|
},
|
||||||
|
method: RequestMethods.Initialize,
|
||||||
|
configureOptions: null,
|
||||||
|
assertResult: (_, response) =>
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<InitializeResult>(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<string, object> { ["customFeature"] = new JsonObject { ["enabled"] = true } },
|
||||||
|
},
|
||||||
|
method: RequestMethods.Initialize,
|
||||||
|
configureOptions: null,
|
||||||
|
assertResult: (_, response) =>
|
||||||
|
{
|
||||||
|
var result = JsonSerializer.Deserialize<InitializeResult>(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<string, object> { ["test"] = new JsonObject() },
|
||||||
|
Logging = new LoggingCapability(),
|
||||||
|
Prompts = new PromptsCapability(),
|
||||||
|
Resources = new ResourcesCapability(),
|
||||||
|
Tools = new ToolsCapability(),
|
||||||
|
Completions = new CompletionsCapability(),
|
||||||
|
Tasks = new McpTasksCapability(),
|
||||||
|
Extensions = new Dictionary<string, object> { ["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<InitializeResult>(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]
|
[Fact]
|
||||||
public async Task Can_Handle_Completion_Requests()
|
public async Task Can_Handle_Completion_Requests()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue