Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,98 @@ public void Configure_WithUnsubscribeFromResourcesHandler_WithoutOtherResourcesH
Assert.Null(options.Handlers.UnsubscribeFromResourcesHandler);
Assert.Null(options.Capabilities?.Resources);
}

[Fact]
public void Configure_WithManualResourceSubscribeCapability_AndWithResources_PreservesCapabilityAndExposesResources()
{
var services = new ServiceCollection();
services.AddMcpServer(options =>
{
// User manually declares support for sending resource subscription notifications
options.Capabilities = new()
{
Resources = new()
{
Subscribe = true,
}
};
})
.WithResources<SimpleResourceType>();

var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;

// The manually set capability should be preserved
Assert.NotNull(options.Capabilities?.Resources);
Assert.True(options.Capabilities.Resources.Subscribe, "User's manually set Subscribe capability should be preserved");

// Resources should still be exposed
Assert.NotNull(options.ResourceCollection);
Assert.NotEmpty(options.ResourceCollection);
}

[Fact]
public async Task ServerCapabilities_WithManualResourceSubscribeCapability_AndWithResources_ExposesSubscribeCapability()
{
// This test would require a full client-server setup, so we'll test via options validation instead
var services = new ServiceCollection();
services.AddMcpServer(options =>
{
// User manually declares support for sending resource subscription notifications
options.Capabilities = new()
{
Resources = new()
{
Subscribe = true,
ListChanged = false, // explicitly set to false to test preservation
}
};
})
.WithResources<SimpleResourceType>()
.WithStdioServerTransport();

var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;

// The options should preserve the user's manually set capabilities
Assert.NotNull(options.Capabilities?.Resources);
Assert.True(options.Capabilities.Resources.Subscribe, "User's manually set Subscribe capability should be preserved in options");

// ListChanged should be false as manually set (not overridden to true by resource collection logic in McpServerOptionsSetup)
Assert.False(options.Capabilities.Resources.ListChanged, "User's manually set ListChanged capability should be preserved in options");
}

[Fact]
public void Configure_WithManualResourceSubscribeCapability_WithoutWithResources_PreservesCapability()
{
var services = new ServiceCollection();
services.AddMcpServer(options =>
{
// User manually declares support for sending resource subscription notifications
options.Capabilities = new()
{
Resources = new()
{
Subscribe = true,
ListChanged = true,
}
};
});

var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;

// The manually set capability should be preserved
Assert.NotNull(options.Capabilities?.Resources);
Assert.True(options.Capabilities.Resources.Subscribe);
Assert.True(options.Capabilities.Resources.ListChanged);
}
#endregion

[McpServerResourceType]
public sealed class SimpleResourceType
{
[McpServerResource]
public static string TestResource() => "Test content";
}

#region Tool Handler Tests
[Fact]
public void Configure_WithListToolsHandler_CreatesToolsCapability()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using ModelContextProtocol.Client;
using ModelContextProtocol.Server;
using ModelContextProtocol.Protocol;

namespace ModelContextProtocol.Tests.Configuration;

// Integration test with full client-server setup
public class McpServerResourceCapabilityIntegrationTests : ClientServerTestBase
{
public McpServerResourceCapabilityIntegrationTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}

protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
// User manually declares support for sending resource subscription notifications
services.Configure<McpServerOptions>(options =>
{
options.Capabilities = new()
{
Resources = new()
{
Subscribe = true,
}
};
});

mcpServerBuilder.WithResources<SimpleResourceType>();
}

[Fact]
public async Task Client_CanListResources_WhenSubscribeCapabilityIsManuallySet()
{
await using McpClient client = await CreateMcpClientForServer();

// The server should advertise Subscribe capability
Assert.NotNull(client.ServerCapabilities.Resources);
Assert.True(client.ServerCapabilities.Resources.Subscribe, "Server should advertise Subscribe capability when manually set");

// The resources should be exposed and listable
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
Assert.NotEmpty(resources);
Assert.Contains(resources, r => r.Name == "test_resource");
}

[Fact]
public async Task Client_CanListResources_WhenCapabilitySetViaAddMcpServerCallback()
{
// This is a separate test using a different configuration approach
await using McpClient client = await CreateMcpClientForServer();

// The server should advertise Subscribe capability
Assert.NotNull(client.ServerCapabilities.Resources);

// The resources should be exposed and listable
var resources = await client.ListResourcesAsync(TestContext.Current.CancellationToken);
Assert.NotEmpty(resources);
}

[McpServerResourceType]
public sealed class SimpleResourceType
{
[McpServerResource]
public static string TestResource() => "Test content";
}
}

// Test that exactly matches the issue scenario
public class McpServerResourceCapabilityIssueReproTests : ClientServerTestBase
{
public McpServerResourceCapabilityIssueReproTests(ITestOutputHelper testOutputHelper)
: base(testOutputHelper)
{
}

protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
// This test uses the exact pattern from the issue:
// AddMcpServer with options callback that sets Capabilities.Resources.Subscribe = true
// followed by WithResources
// NO call to services.Configure after AddMcpServer
}

[Fact]
public async Task Resources_AreExposed_WhenSubscribeCapabilitySetInAddMcpServerOptions()
{
// Create a fresh service collection to test the exact scenario from the issue
var services = new ServiceCollection();
services.AddLogging();
services.AddSingleton(XunitLoggerProvider);

// This matches the issue: setting capabilities in AddMcpServer callback
var builder = services.AddMcpServer(
options =>
{
// Declare support for sending resource subscription notifications
options.Capabilities = new()
{
Resources = new()
{
Subscribe = true,
}
};
})
.WithResources<LiveResources>()
.WithStdioServerTransport();

var serviceProvider = services.BuildServiceProvider();
var mcpOptions = serviceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;

// Verify capabilities are preserved
Assert.NotNull(mcpOptions.Capabilities?.Resources);
Assert.True(mcpOptions.Capabilities.Resources.Subscribe, "Subscribe capability should be preserved");

// Verify resources are registered
Assert.NotNull(mcpOptions.ResourceCollection);
Assert.NotEmpty(mcpOptions.ResourceCollection);
Assert.Contains(mcpOptions.ResourceCollection, r => r.ProtocolResource?.Name == "live_resource");
}

[Fact]
public void ResourcesCapability_IsCreated_WhenOnlyResourcesAreProvided()
{
// Test that ResourcesCapability is created even without handlers or manual setting
var services = new ServiceCollection();
var builder = services.AddMcpServer()
.WithResources<LiveResources>()
.WithStdioServerTransport();

var serviceProvider = services.BuildServiceProvider();
var mcpOptions = serviceProvider.GetRequiredService<IOptions<McpServerOptions>>().Value;

// Resources are registered
Assert.NotNull(mcpOptions.ResourceCollection);
Assert.NotEmpty(mcpOptions.ResourceCollection);

// But ResourcesCapability should NOT be created just because resources exist!
// The capability is only created when resources are actually used by the server
// This is correct behavior - the capability is set up during server initialization
// in McpServerImpl.ConfigureResources
}

[McpServerResourceType]
public sealed class LiveResources
{
[McpServerResource]
public static string LiveResource() => "Live content";
}
}