csharp-sdk/tests/ModelContextProtocol.Tests/Server/McpServerTasksNoStoreTests.cs
Jeff Handley 2ca96f548b
Extract Tasks into the ModelContextProtocol.Extensions.Tasks extension package (#1693)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Tarek Mahmoud Sayed <tarekms@ntdev.microsoft.com>
2026-07-14 18:53:58 -04:00

71 lines
3 KiB
C#

using ModelContextProtocol.Extensions.Tasks;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Runtime.InteropServices;
#pragma warning disable MCPEXP001
namespace ModelContextProtocol.Tests.Server;
/// <summary>
/// Pins the behavior when a client signals the SEP-2575 tasks opt-in via <c>_meta</c> but the server
/// has neither <see cref="McpTasksBuilderExtensions.WithTasks(IMcpServerBuilder, IMcpTaskStore)"/> nor a <see cref="McpServerHandlers.CallToolWithAlternateHandler"/>
/// configured. The expected behavior is a silent synchronous fallback: the server returns the normal
/// <see cref="CallToolResult"/> with no <c>Task</c> envelope and no exception.
/// </summary>
public class McpServerTasksNoStoreTests : ClientServerTestBase
{
public McpServerTasksNoStoreTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
{
#if !NET
Assert.SkipWhen(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), "https://github.com/modelcontextprotocol/csharp-sdk/issues/587");
#endif
}
protected override void ConfigureServices(ServiceCollection services, IMcpServerBuilder mcpServerBuilder)
{
// Intentionally do NOT configure TaskStore or CallToolWithAlternateHandler.
mcpServerBuilder.WithTools<NoStoreTools>();
}
[Fact]
public async Task ClientOptIn_NoTaskStore_FallsBackToSyncResult()
{
await using var client = await CreateMcpClientForServer();
var ct = TestContext.Current.CancellationToken;
// CallToolAsTaskAsync always writes the SEP-2575 tasks opt-in into _meta.
var augmented = await client.CallToolAsTaskAsync(
new CallToolRequestParams { Name = "sync-tool" }, ct);
// With no task store configured, the server must complete synchronously and return
// the standard CallToolResult — not a task envelope.
Assert.False(augmented.IsTask);
Assert.NotNull(augmented.Result);
Assert.Equal("sync result", Assert.IsType<TextContentBlock>(augmented.Result!.Content[0]).Text);
}
[Fact]
public async Task ClientOptIn_NoTaskStore_CallToolAsync_StillReturnsResult()
{
// CallToolAsync (the higher-level convenience) must also work in the no-store case,
// delegating to the underlying sync path without throwing.
await using var client = await CreateMcpClientForServer();
var ct = TestContext.Current.CancellationToken;
var result = await client.CallToolWithPollingAsync(
new CallToolRequestParams { Name = "sync-tool" }, cancellationToken: ct);
Assert.NotNull(result);
Assert.Equal("sync result", Assert.IsType<TextContentBlock>(result.Content[0]).Text);
}
[McpServerToolType]
private sealed class NoStoreTools
{
[McpServerTool(Name = "sync-tool"), System.ComponentModel.Description("A plain sync tool")]
public static string SyncTool() => "sync result";
}
}