Recursive AI

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.

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

Terminal
dotnet add package RecursiveAI.OneModelContext

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
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:

C#
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, 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

C#
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:

C#
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

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 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 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 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.

All onemodelcontext recipes