---
title: "Trace server tool calls in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/trace-tool-calls/
description: "Observe tool calls through .NET activities, the spans used by OpenTelemetry. No collector is needed for this local example."
version: "1.0.1"
---

# Trace server tool calls 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/trace-tool-calls/trace-tool-calls.zip)

Observe tool calls through .NET activities, the spans used by OpenTelemetry. No collector is needed for this local example.

```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 System.Diagnostics;

using var listener = new ActivityListener
{
    ShouldListenTo = source => source.Name ==
        "RecursiveAI.OneModelContext",
    Sample = (ref ActivityCreationOptions<
        ActivityContext> _) =>
        ActivitySamplingResult.AllData,
    ActivityStopped = activity =>
    {
        if (activity.Kind == ActivityKind.Server &&
            activity.DisplayName
                .StartsWith("tools/call"))
            Console.WriteLine(
                activity.DisplayName);
    }
};
ActivitySource.AddActivityListener(listener);
```

Registers the listener. Keep it alive in your server process while tool calls run; the full example below makes a call.

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

## The full program

The program creates a server with a `hello` tool and calls it through an in-memory client. The listener prints only completed server tool-call spans, followed by the client printing the result.

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

using var listener = new ActivityListener
{
    ShouldListenTo = source =>
        source.Name == "RecursiveAI.OneModelContext",
    Sample = (ref ActivityCreationOptions<ActivityContext> _) =>
        ActivitySamplingResult.AllData,
    ActivityStopped = activity =>
    {
        if (activity.Kind == ActivityKind.Server &&
            activity.DisplayName.StartsWith("tools/call"))
            Console.WriteLine(activity.DisplayName);
    }
};
ActivitySource.AddActivityListener(listener);
var server = new ContextServer("Traces", "1.0");
using var schema = JsonDocument.Parse("""{"type":"object"}""");
server.Tools.Add(new ToolDefinition("hello", "Greet", schema),
    (_, _) => ValueTask.FromResult(CallToolResult.FromText("Hello")));

var toServer = new System.IO.Pipelines.Pipe();
var toClient = new System.IO.Pipelines.Pipe();
var serving = server.RunAsync(ContextTransport.Stream(
    toServer.Reader.AsStream(), toClient.Writer.AsStream()));
await using (var client = await ContextClient.ConnectAsync(
    ContextTransport.Stream(toClient.Reader.AsStream(), toServer.Writer.AsStream())))
{
    Console.WriteLine((await client.CallToolAsync("hello", new { })).Text);
}
await serving;
```

`dotnet run` prints:

```text
tools/call hello
Hello
```

Without a matching listener, the server does not start these activities; registering one after the call does not recover that call.

## Variations

To export spans through OpenTelemetry, install:

```shell
dotnet add package \
  OpenTelemetry.Extensions.Hosting
dotnet add package \
  OpenTelemetry.Exporter.Console
dotnet add package \
  Microsoft.Extensions.Hosting
```

The zip pins OpenTelemetry packages to 1.21.0 and Hosting to 10.0.0. Replace the full program’s `ActivityListener` setup with this code, keeping the `using` directives at the top:

```csharp
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Trace;

var builder = Host.CreateApplicationBuilder();
builder.Logging.ClearProviders();
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddSource("RecursiveAI.OneModelContext")
        .AddConsoleExporter());
using var host = builder.Build();
await host.StartAsync();
```

Add this after `await serving;`:

```csharp
await host.StopAsync();
```

This was run with the same in-memory server and client. The console exporter printed discovery, server tool-call and client tool-call spans; this excerpt is from the actual output:

```text
Activity.DisplayName:        tools/call hello
Activity.Kind:               Server
```

It exports locally to the console, not to an external collector. Trace IDs and timestamps vary between runs.

## Related recipes

- [Build a protocol server in C#](https://recursiveai.org/onemodelcontext/cookbook/build-a-server-in-csharp/)
- [Serve over HTTP without ASP.NET Core](https://recursiveai.org/onemodelcontext/cookbook/serve-over-http-without-aspnet/)

---

## The short answer

Listen to the `RecursiveAI.OneModelContext` activity source in the process running your server. Register the listener before handling calls and retain it until those calls finish. The server names a tool-call activity `tools/call` followed by the tool name, so the local example prints `tools/call hello`.

The snippet at the top installs observation; it does not make a call. The full program supplies the server, handler, client and invocation. It uses a local .NET `ActivityListener`, not an exporter, and requires no collector endpoint or service credentials.

## How can I use OpenTelemetry in C#?

OneModelContext exposes an `ActivitySource` and a `Meter`, both named `RecursiveAI.OneModelContext`, according to its [pinned README](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/README.md). For tracing, configure your telemetry pipeline to subscribe to that activity source. For local inspection without additional packages, use the listener shown above.

The source name is an exact match. A listener subscribed only to an HTTP instrumentation source does not subscribe to the library’s protocol activities. Likewise, enabling a meter is not the same as collecting spans: it subscribes to measurements rather than call activities.

The example chooses `AllData` so the activity is created and its completion callback runs. It filters on both `ActivityKind.Server` and the `tools/call` name prefix. That distinction matters when a process hosts both a client and a server, as this sample does. Without filtering, a single invocation can produce client and server observations, plus unrelated protocol operations.

## What is the difference between a span and a trace in OpenTelemetry?

A span describes an operation; a trace connects related operations. The [OpenTelemetry traces documentation](https://opentelemetry.io/docs/concepts/signals/traces/) explains spans, parent relationships and span kinds. In .NET, activities are the instrumentation objects observed here.

The server’s [activity creation code](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/src/RecursiveAI.OneModelContext/ContextServer.cs) uses `ActivityKind.Server` and checks for incoming trace context. When it can read a parent, it starts the activity with that parent. It does not make every observed call part of a single global trace merely because the source names match.

## How do I visualize OpenTelemetry traces?

For a local check, the completion callback is enough to inspect the activity name and kind. For a timeline across services, use an OpenTelemetry collection and export pipeline that subscribes to the library’s source, then inspect the exported trace in your chosen backend. The variation configures and tests a console exporter, not an external backend.

Keep filtering and sampling separate. Filtering the output callback changes what this console prints; sampling controls which activities are created or recorded. The OpenTelemetry variation replaces the manual listener with a host-managed tracing pipeline. Dispose the listener when the server is shutting down rather than while requests are still running.
