Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/modelcontextprotocol/csharp-sdk/llms.txt

Use this file to discover all available pages before exploring further.

MCP supports cancellation of in-flight requests. Either the client or server can cancel a previously issued request using standard .NET CancellationToken parameters.

How Cancellation Works

The MCP C# SDK automatically maps CancellationToken cancellation to MCP notifications/cancelled protocol messages:
  1. Client cancels: When a client cancels a CancellationToken passed to a method like CallToolAsync, the SDK sends a notifications/cancelled message to the server
  2. Server receives: The server’s CancellationToken parameter is triggered, allowing the handler to stop work gracefully
  3. Bidirectional: This works in both directions - clients can cancel server requests, and servers can cancel client requests
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

try
{
    var result = await client.CallToolAsync(
        "longOperation",
        cancellationToken: cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation was cancelled");
}

Server-Side Cancellation

Basic Pattern

Server tools receive a CancellationToken that triggers when the client sends a cancellation notification:
[McpServerTool]
[Description("Processes a large dataset")]
public static async Task<string> ProcessDataset(
    [Description("Number of records")] int recordCount,
    CancellationToken cancellationToken)
{
    for (int i = 0; i < recordCount; i++)
    {
        // Check for cancellation
        cancellationToken.ThrowIfCancellationRequested();

        // Process record
        await ProcessRecordAsync(i, cancellationToken);
    }

    return $"Processed {recordCount} records";
}

Passing Tokens to Dependencies

Always pass the CancellationToken to async methods and operations:
[McpServerTool]
public static async Task<string> FetchAndProcess(
    string url,
    CancellationToken cancellationToken)
{
    using var httpClient = new HttpClient();

    // Pass token to HTTP request
    var response = await httpClient.GetAsync(url, cancellationToken);
    var data = await response.Content.ReadAsStringAsync(cancellationToken);

    // Pass token to database operation
    await SaveToDatabase(data, cancellationToken);

    return "Processing complete";
}

Cleanup on Cancellation

Use try-finally or await using to ensure cleanup happens even when cancelled:
[McpServerTool]
public static async Task<string> ProcessWithResources(
    string filePath,
    CancellationToken cancellationToken)
{
    await using var stream = File.OpenRead(filePath);
    try
    {
        // Process file
        await ProcessStreamAsync(stream, cancellationToken);
        return "Success";
    }
    catch (OperationCanceledException)
    {
        // Log cancellation
        Console.WriteLine("Processing was cancelled");
        throw; // Re-throw to propagate cancellation
    }
    // Stream is automatically disposed even if cancelled
}

Client-Side Cancellation

Timeout Pattern

Set a timeout for long-running operations:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));

try
{
    var result = await client.CallToolAsync(
        "complexAnalysis",
        new Dictionary<string, object> { { "dataset", "large.csv" } },
        cancellationToken: cts.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation timed out after 30 seconds");
}

User-Initiated Cancellation

Allow users to cancel operations:
var cts = new CancellationTokenSource();

Console.WriteLine("Press 'C' to cancel...");

var task = client.CallToolAsync(
    "longOperation",
    cancellationToken: cts.Token);

var keyTask = Task.Run(() =>
{
    if (Console.ReadKey(true).Key == ConsoleKey.C)
    {
        cts.Cancel();
        Console.WriteLine("\nCancelling operation...");
    }
});

try
{
    await task;
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation was cancelled by user");
}

Combining Multiple Tokens

Combine app lifetime, timeout, and user cancellation:
var appLifetime = GetApplicationLifetimeToken();
var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(5));
var userCancel = new CancellationTokenSource();

using var combined = CancellationTokenSource.CreateLinkedTokenSource(
    appLifetime,
    timeout.Token,
    userCancel.Token);

try
{
    var result = await client.CallToolAsync(
        "operation",
        cancellationToken: combined.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operation cancelled");
}

Observing Cancellation Notifications

You can register a handler to observe cancellation notifications:
mcpClient.RegisterNotificationHandler(
    NotificationMethods.CancelledNotification,
    (notification, ct) =>
    {
        var cancelled = notification.Params?.Deserialize<CancelledNotificationParams>(
            McpJsonUtilities.DefaultOptions);
        if (cancelled is not null)
        {
            Console.WriteLine($"Request {cancelled.RequestId} cancelled: {cancelled.Reason}");
        }
        return default;
    });

Cancellation Notification Details

The notifications/cancelled notification includes:
PropertyTypeDescription
RequestIdstringThe ID of the request to cancel
Reasonstring?Optional human-readable cancellation reason

Best Practices

1

Always accept CancellationToken

Add a CancellationToken parameter to all async tools, even if they complete quickly:
public static async Task<string> MyTool(
    string param,
    CancellationToken cancellationToken) // Always include
2

Pass tokens to all async operations

Ensure cancellation propagates through your entire call chain:
await httpClient.GetAsync(url, cancellationToken);
await dbContext.SaveChangesAsync(cancellationToken);
await Task.Delay(1000, cancellationToken);
3

Check cancellation in loops

For CPU-bound work, periodically check for cancellation:
for (int i = 0; i < largeNumber; i++)
{
    cancellationToken.ThrowIfCancellationRequested();
    // Do work
}
4

Handle OperationCanceledException

Catch cancellation exceptions appropriately:
try
{
    await operation(cancellationToken);
}
catch (OperationCanceledException)
{
    // Clean up and log
    throw; // Usually re-throw
}
When a CancellationToken is cancelled, the OperationCanceledException propagates back to the client as a cancellation response in the MCP protocol.

API Reference