---
title: "Report server tool progress in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/report-tool-progress/
description: "Report server tool progress in C# with OneModelContext. Run a complete .NET server and client example with no external service."
version: "1.0.1"
---

# Report server tool progress 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/report-tool-progress/report-tool-progress.zip)

Progress notifications let a client show work completed while a tool call is still running.

```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<string> Work(RequestContext context)
    {
        await context.ReportProgressAsync(1, 1);
        return "Finished";
    }
}
```

Without a progress token from the caller, `ReportProgressAsync` does nothing. A client must request progress to see any updates; this handler sends `1` of `1`, then returns `Finished`.

Unzip [the example](https://recursiveai.org/onemodelcontext/cookbook/report-tool-progress/report-tool-progress.zip), then `dotnet run`.

The download runs a client and child server over standard input and output. Its handler reports three completed work units; its client supplies an `IProgress<ProgressNotificationParams>` receiver. The package’s generator supplies tool registration.

## 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 arguments = JsonDocument.Parse("{}");
var progress = new InlineProgress();
var result = await client.CallToolAsync("work", arguments.RootElement, progress);
Console.WriteLine(result.Text);

public partial class Demo
{
    [Tool("work")]
    public async Task<string> Work(RequestContext context)
    {
        for (int done = 1; done <= 3; done++)
        {
            await Task.Delay(50, context.CancellationToken);
            await context.ReportProgressAsync(done, 3, "Working");
        }
        return "Finished";
    }
}

public sealed class InlineProgress : IProgress<ProgressNotificationParams>
{
    public void Report(ProgressNotificationParams update) =>
        Console.WriteLine($"{update.Progress}/{update.Total}: {update.Message}");
}
```

`dotnet run` prints:

```text
1/3: Working
2/3: Working
3/3: Working
Finished
```

## Variations

Report completed units without a known total. Replace the progress call inside the full program’s loop with:

```csharp
await context.ReportProgressAsync(
    done, message: "Working");
```

The client prints:

```text
1/: Working
2/: Working
3/: Working
Finished
```

## 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/)
- [Cancel a long-running server tool call in C#](https://recursiveai.org/onemodelcontext/cookbook/cancel-long-running-tools/)

---

## The short answer

Inject `RequestContext` into a generated tool method and await `ReportProgressAsync(progress, total, message)`. On the client, call the overload of `CallToolAsync` that accepts `IProgress<ProgressNotificationParams>`. OneModelContext adds a progress token to that request and routes matching notifications to the receiver.

Both sides matter. A handler can report progress correctly and still produce no visible updates when the caller did not ask for them. The full program includes a receiver as well as a handler, so it demonstrates the complete path rather than only sending notifications into an unobserved connection.

## How do I report server tool progress in C#?

Measure completed work in units that mean something for the operation: processed documents, completed steps or rows examined. The example uses three short simulated steps. Each delay accepts the request’s cancellation token, and each completed step sends an increasing value with the same total.

`RequestContext` is filled in by the generated invoker; it is not a model-supplied argument in the tool’s JSON Schema. The [tool signature rules](https://recursiveai.org/onemodelcontext/docs/tool-schemas/#a-tool) also allow an injected `CancellationToken`. Keep progress reporting inside the lifetime of the request. It describes that call, not a background task that continues after a result is returned.

The final `Finished` line comes from the tool result. It is separate from the progress messages. Do not treat a progress value equal to the total as the returned result: the caller still needs to await `CallToolAsync` and handle its success or failure.

## How does a client receive progress notifications?

The complete program passes a `JsonElement` containing an empty argument object and an `InlineProgress` receiver to the progress-aware overload. Its receiver writes each update directly. This keeps the demonstration’s console output in notification order without posting callbacks onto another synchronization context.

A graphical application should marshal updates to its own UI thread as needed. The receiver interface does not itself draw a progress bar. Store the latest completed value, optional total and message in whatever display model your application uses.

The [protocol progress specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress) ties notifications to a token supplied in request metadata. OneModelContext creates the token when a non-null progress receiver is supplied. In the pinned implementation, `RequestContext.ReportProgressAsync` returns immediately without sending anything if no token is present. That is why adding only server code may appear to have no effect.

## Can I report progress without a total?

Yes. The total is nullable, and the variation omits it. Report an increasing completed-work value rather than guessing a percentage. The [specification](https://modelcontextprotocol.io/specification/2025-03-26/basic/utilities/progress#progress-flow) requires progress to increase with each notification and allows the total to be unknown.

The example’s simple console formatter leaves the total blank, producing `1/: Working`. A real display can instead show “1 completed” or an indeterminate indicator. Do not divide by a missing total or assume that a value of `1` always means 100 percent.

Notifications are optional status updates, not a durable job record. Send them at useful milestones rather than once per tiny loop iteration, and stop reporting when the operation ends. Use the eventual result or exception to decide the outcome; missing updates alone do not establish that a call failed.
