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:
- Client cancels: When a client cancels a
CancellationToken passed to a method like CallToolAsync, the SDK sends a notifications/cancelled message to the server
- Server receives: The server’s
CancellationToken parameter is triggered, allowing the handler to stop work gracefully
- 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:
| Property | Type | Description |
|---|
RequestId | string | The ID of the request to cancel |
Reason | string? | Optional human-readable cancellation reason |
Best Practices
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
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);
Check cancellation in loops
For CPU-bound work, periodically check for cancellation:for (int i = 0; i < largeNumber; i++)
{
cancellationToken.ThrowIfCancellationRequested();
// Do work
}
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