Resource subscribe should be true if handler provided (#676)

Co-authored-by: Stephen Halter <halter73@gmail.com>
Co-authored-by: Mike Kistler <mikekistler@microsoft.com>
This commit is contained in:
Peder Holdgaard Pedersen 2025-12-12 05:41:43 +01:00 • committed by GitHub
commit 9bbae84c9b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 18 additions and 42 deletions

View file

@ -97,14 +97,15 @@ internal sealed class McpServerOptionsSetup(
optionsHandlers.ListResourceTemplatesHandler = handlers.ListResourceTemplatesHandler ?? optionsHandlers.ListResourceTemplatesHandler; optionsHandlers.ListResourceTemplatesHandler = handlers.ListResourceTemplatesHandler ?? optionsHandlers.ListResourceTemplatesHandler;
optionsHandlers.ListResourcesHandler = handlers.ListResourcesHandler ?? optionsHandlers.ListResourcesHandler; optionsHandlers.ListResourcesHandler = handlers.ListResourcesHandler ?? optionsHandlers.ListResourcesHandler;
optionsHandlers.ReadResourceHandler = handlers.ReadResourceHandler ?? optionsHandlers.ReadResourceHandler; optionsHandlers.ReadResourceHandler = handlers.ReadResourceHandler ?? optionsHandlers.ReadResourceHandler;
}
if (handlers.SubscribeToResourcesHandler is not null || handlers.UnsubscribeFromResourcesHandler is not null) if (handlers.SubscribeToResourcesHandler is not null || handlers.UnsubscribeFromResourcesHandler is not null)
{ {
resourcesCapability ??= new();
optionsHandlers.SubscribeToResourcesHandler = handlers.SubscribeToResourcesHandler ?? optionsHandlers.SubscribeToResourcesHandler; optionsHandlers.SubscribeToResourcesHandler = handlers.SubscribeToResourcesHandler ?? optionsHandlers.SubscribeToResourcesHandler;
optionsHandlers.UnsubscribeFromResourcesHandler = handlers.UnsubscribeFromResourcesHandler ?? optionsHandlers.UnsubscribeFromResourcesHandler; optionsHandlers.UnsubscribeFromResourcesHandler = handlers.UnsubscribeFromResourcesHandler ?? optionsHandlers.UnsubscribeFromResourcesHandler;
resourcesCapability.Subscribe = true; resourcesCapability.Subscribe = true;
} }
}
ToolsCapability? toolsCapability = options.Capabilities?.Tools; ToolsCapability? toolsCapability = options.Capabilities?.Tools;
if (handlers.ListToolsHandler is not null || handlers.CallToolHandler is not null) if (handlers.ListToolsHandler is not null || handlers.CallToolHandler is not null)

View file

@ -110,21 +110,6 @@ public class ServerConformanceTests : IAsyncLifetime
private void StartConformanceServer() private void StartConformanceServer()
{ {
// The ConformanceServer binary is in a parallel directory to the test binary
// Test binary is in: artifacts/bin/ModelContextProtocol.ConformanceTests/Debug/{tfm}/
// ConformanceServer binary is in: artifacts/bin/ModelContextProtocol.ConformanceServer/Debug/{tfm}/
var testBinaryDir = AppContext.BaseDirectory; // e.g., .../net10.0/
var configuration = Path.GetFileName(Path.GetDirectoryName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar))!);
var targetFramework = Path.GetFileName(testBinaryDir.TrimEnd(Path.DirectorySeparatorChar));
var conformanceServerDir = Path.GetFullPath(
Path.Combine(testBinaryDir, "..", "..", "..", "ModelContextProtocol.ConformanceServer", configuration, targetFramework));
if (!Directory.Exists(conformanceServerDir))
{
throw new DirectoryNotFoundException(
$"ConformanceServer directory not found at: {conformanceServerDir}");
}
// Start the server in a background task // Start the server in a background task
_serverCts = new CancellationTokenSource(); _serverCts = new CancellationTokenSource();
_serverTask = Task.Run(() => ConformanceServer.Program.MainAsync(["--urls", _serverUrl], new XunitLoggerProvider(_output), cancellationToken: _serverCts.Token)); _serverTask = Task.Run(() => ConformanceServer.Program.MainAsync(["--urls", _serverUrl], new XunitLoggerProvider(_output), cancellationToken: _serverCts.Token));

View file

@ -1,12 +1,9 @@
using ConformanceServer.Prompts; using ConformanceServer.Prompts;
using ConformanceServer.Resources; using ConformanceServer.Resources;
using ConformanceServer.Tools; using ConformanceServer.Tools;
using Microsoft.Extensions.AI;
using ModelContextProtocol.Protocol; using ModelContextProtocol.Protocol;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace ModelContextProtocol.ConformanceServer; namespace ModelContextProtocol.ConformanceServer;
@ -41,18 +38,8 @@ public class Program
} }
if (ctx.Params?.Uri is { } uri) if (ctx.Params?.Uri is { } uri)
{ {
subscriptions[ctx.Server.SessionId].TryAdd(uri, 0); var sessionSubscriptions = subscriptions.GetOrAdd(ctx.Server.SessionId, _ => new());
sessionSubscriptions.TryAdd(uri, 0);
await ctx.Server.SampleAsync([
new ChatMessage(ChatRole.System, "You are a helpful test server"),
new ChatMessage(ChatRole.User, $"Resource {uri}, context: A new subscription was started"),
],
chatOptions: new ChatOptions
{
MaxOutputTokens = 100,
Temperature = 0.7f,
},
cancellationToken: ct);
} }
return new EmptyResult(); return new EmptyResult();
@ -67,6 +54,7 @@ public class Program
{ {
subscriptions[ctx.Server.SessionId].TryRemove(uri, out _); subscriptions[ctx.Server.SessionId].TryRemove(uri, out _);
} }
return new EmptyResult(); return new EmptyResult();
}) })
.WithCompleteHandler(async (ctx, ct) => .WithCompleteHandler(async (ctx, ct) =>

View file

@ -108,7 +108,7 @@ public class McpServerOptionsSetupTests
} }
[Fact] [Fact]
public void Configure_WithSubscribeToResourcesHandler_WithoutOtherResourcesHandler_DoesNotCreateResourcesCapability() public void Configure_WithSubscribeToResourcesHandler_WithoutOtherResourcesHandler_DoesCreateResourcesCapability()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddMcpServer() services.AddMcpServer()
@ -116,12 +116,13 @@ public class McpServerOptionsSetupTests
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value; var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Null(options.Handlers.SubscribeToResourcesHandler); Assert.NotNull(options.Handlers.SubscribeToResourcesHandler);
Assert.Null(options.Capabilities?.Resources); Assert.NotNull(options.Capabilities?.Resources);
Assert.True(options.Capabilities.Resources.Subscribe);
} }
[Fact] [Fact]
public void Configure_WithUnsubscribeFromResourcesHandler_WithoutOtherResourcesHandler_DoesNotCreateResourcesCapability() public void Configure_WithUnsubscribeFromResourcesHandler_WithoutOtherResourcesHandler_DoesCreateResourcesCapability()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddMcpServer() services.AddMcpServer()
@ -129,8 +130,9 @@ public class McpServerOptionsSetupTests
var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value; var options = services.BuildServiceProvider().GetRequiredService<IOptions<McpServerOptions>>().Value;
Assert.Null(options.Handlers.UnsubscribeFromResourcesHandler); Assert.NotNull(options.Handlers.UnsubscribeFromResourcesHandler);
Assert.Null(options.Capabilities?.Resources); Assert.NotNull(options.Capabilities?.Resources);
Assert.True(options.Capabilities.Resources.Subscribe);
} }
[Fact] [Fact]