---
title: "Cancel a long-running server tool call in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/cancel-long-running-tools/
description: "Cancel a long-running server tool call in C# with OneModelContext. Run a complete .NET server and client example with no external service."
version: "1.0.1"
---

# Cancel a long-running server tool call in C#

**OneModelContext** is a free, open-source .NET SDK for the Model Context Protocol® from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneModelContext) · [GitHub](https://github.com/recursive-ai-oss/onemodelcontext-csharp) · [Docs](https://recursiveai.org/onemodelcontext/docs/) · [Download example (.zip)](https://recursiveai.org/onemodelcontext/cookbook/cancel-long-running-tools/cancel-long-running-tools.zip)

Cancellation lets a caller ask a running tool to stop; the handler must observe its cancellation token.

```shell
dotnet add package RecursiveAI.OneModelContext
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneModelContext;
await ModelContext.ServeAsync(new Tools());

public partial class Tools
{
    [Tool("work")]
    public async Task Work(CancellationToken token) =>
        await Task.Delay(Timeout.Infinite, token);
}
```

Serves a waiting tool that exits when its request token is canceled; it prints no application text.

In a client with a connected `ContextClient` named `client`:

```csharp
using var cancellation = new CancellationTokenSource();
var call = client.CallToolAsync("work", cancellationToken: cancellation.Token);
cancellation.Cancel();
```

Await `call` and catch `OperationCanceledException`, as the full program does.

Unzip [the example](https://recursiveai.org/onemodelcontext/cookbook/cancel-long-running-tools/cancel-long-running-tools.zip), then `dotnet run`.

The download starts a client and child server. The client cancels only after receiving a “Started” progress notification, catches `OperationCanceledException`, then calls `ping` on the same server. The package’s generator injects the handler’s token. Cancellation is cooperative: code that ignores the token is not forcibly terminated.

## The full program

```csharp
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Protocol;
using System.Reflection;
using System.Text.Json;

if (args.Contains("--stdio"))
{
    await ModelContext.ServeAsync(new Demo());
    return;
}
var assembly = Assembly.GetExecutingAssembly().Location;
await using var client = await ModelContext.ConnectAsync($"dotnet \"{assembly}\" --stdio");
using var cancellation = new CancellationTokenSource();
using var arguments = JsonDocument.Parse("{}");
var progress = new CancelOnStart(cancellation);
try
{
    await client.CallToolAsync("work", arguments.RootElement, progress,
        cancellation.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Client: call canceled");
}
Console.WriteLine((await client.CallToolAsync("ping")).Text);

public partial class Demo
{
    [Tool("work")]
    public async Task<string> Work(RequestContext context, CancellationToken token)
    {
        await context.ReportProgressAsync(0, message: "Started");
        await Task.Delay(Timeout.Infinite, token);
        return "Finished";
    }

    [Tool("ping")]
    public string Ping() => "Server: still available";
}

public sealed class CancelOnStart(CancellationTokenSource cancellation)
    : IProgress<ProgressNotificationParams>
{
    public void Report(ProgressNotificationParams update)
    {
        Console.WriteLine("Client: cancel requested after " + update.Message);
        cancellation.Cancel();
    }
}
```

`dotnet run` prints:

```text
Client: cancel requested after Started
Client: call canceled
Server: still available
```

## Variations

Cancel after a deadline instead of waiting for the start notification. Replace the `progress` declaration and the whole `try`/`catch` block with:

```csharp
cancellation.CancelAfter(TimeSpan.FromMilliseconds(100));
try
{
    await client.CallToolAsync("work",
        arguments.RootElement,
        cancellationToken: cancellation.Token);
}
catch (OperationCanceledException)
{
    Console.WriteLine("Client: call canceled");
}
```

The remaining `ping` call still runs. Output:

```text
Client: call canceled
Server: still available
```

## Related recipes

- [Serve resource URI templates in C#](https://recursiveai.org/onemodelcontext/cookbook/serve-resource-templates/)
- [Serve prompt templates in C#](https://recursiveai.org/onemodelcontext/cookbook/serve-prompt-templates/)
- [Report server tool progress in C#](https://recursiveai.org/onemodelcontext/cookbook/report-tool-progress/)

---

## The short answer

Pass a `CancellationToken` to `ContextClient.CallToolAsync` and accept an injected token in the server’s `[Tool]` method. Cancel the caller’s token source when the user requests a stop. The server must pass its token into cancellable operations or check it during its own work.

OneModelContext maps request cancellation to the handler’s request token. This stops cooperative work; it is not thread termination, process termination or an automatic rollback. The full example catches the caller’s `OperationCanceledException` and then makes another call to show that canceling one request does not require shutting down the server.

## How do I cancel a server tool call in C#?

Create a token source for the call and pass its token through the `cancellationToken` parameter. Keep the source alive until the call finishes. In the example the client waits for a progress notification before calling `Cancel`, so the stop request follows the handler’s start rather than racing the initial connection.

The generated handler accepts both `RequestContext` and `CancellationToken`. Neither is exposed as a model-supplied tool argument, as the [signature mapping](https://recursiveai.org/onemodelcontext/docs/tool-schemas/#a-tool) explains. The context reports that work started; the token is passed into an indefinite delay. That delay represents a cancellable operation, not a polling loop consuming a CPU core.

The deadline variation uses `CancelAfter` to cancel the caller’s token. It is a deadline for this request, not a promise that all remote side effects have ended at that instant. The caller stops awaiting the call; any confirmation of business-level cleanup needs to come from your application’s own state.

## Can I cancel a task without a CancellationToken?

Not through this cooperative handler pattern. If a tool calls an API that ignores cancellation, canceling the request does not interrupt that API. Pass the injected token to asynchronous operations that accept it. For CPU work, check the token at reasonable boundaries and exit when cancellation is requested.

Microsoft’s [managed cancellation documentation](https://learn.microsoft.com/en-us/dotnet/standard/threading/cancellation-in-managed-threads) describes this as cooperation between the requester and listener. Decide where your handler can stop safely. If it performs several writes, cancellation can arrive after some have already succeeded. A token does not undo those writes or turn them into a transaction.

Avoid swallowing `OperationCanceledException` and continuing expensive work. Use normal resource cleanup, such as `using` or `finally`, around resources the handler owns. Cleanup should release those resources without claiming that previously committed external changes were reversed.

## Does canceling a tool call stop the server?

The example cancels one request and then receives `Server: still available` from `ping`. A per-call token and a server lifetime token have different jobs. Do not use the server shutdown source for every individual call if the intended behavior is only to stop one operation.

The [protocol cancellation specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/utilities/cancellation) identifies the request being canceled and describes races with completion. A cancellation notification may arrive after an operation has already finished. Treat that as a possible outcome, not as evidence that cancellation was ignored.

Cancellation is also different from a tool’s error result. A validation problem or `ToolException` can produce an `isError` result; caller cancellation is handled by the exception path shown here. Catch cancellation specifically rather than catching every exception and labeling every failed request “canceled.” That preserves the distinction between a user’s stop request, a protocol problem and an operation that completed with an error.
