> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcat.com/llms.txt
> Use this file to discover all available pages before exploring further.

# C#: ASP.NET Core and the generic host

> Track a C# MCP server that AddMcpServer() builds: stateless and stateful HTTP, stdio on the generic host, request headers, IAgentCat, custom events, shutdown and the options.

This page covers servers that dependency injection builds with `AddMcpServer()` from the official C# MCP SDK, [`ModelContextProtocol`](https://github.com/modelcontextprotocol/csharp-sdk): HTTP servers on ASP.NET Core, stateless or stateful, and stdio servers on the .NET generic host. For a server that you create yourself with `McpServer.Create`, see [McpServer.Create](https://docs.agentcat.com/sdk/csharp/mcp-server-create.md).

## Requirements

* `ModelContextProtocol` 1.0.0 or later, on the 1.x or 2.x line. HTTP servers also use `ModelContextProtocol.AspNetCore`.
* .NET 8 or later. The package also targets .NET Standard 2.0, for stdio servers on .NET Framework 4.7.2 or later.

```bash theme={null}
dotnet add package AgentCat
```

## Add `WithAgentCat()` to the server builder

`WithAgentCat()` is an extension method of the `IMcpServerBuilder` that `AddMcpServer()` returns, in the `Microsoft.Extensions.DependencyInjection` namespace like the MCP SDK's own builder methods:

```csharp theme={null}
builder.Services.AddMcpServer()
    .WithHttpTransport(o => o.Stateless = true)
    .WithTools<MyTools>()
    .WithAgentCat("proj_YOUR_PROJECT_ID", options =>
    {
        options.EnableAgentTracking = true;
    });
```

* **Anywhere in the chain.** AgentCat installs after every `Configure` step of the server options, so it sees the tools and handlers that `WithTools`, `WithCallToolHandler` and the other builder methods add, before or after it.
* **The project ID.** `null`, or no argument, reads it from the `AGENTCAT_PROJECT_ID` environment variable. An empty string, or no project ID at all, tracks the server but sends no events to AgentCat: your [exporters](https://docs.agentcat.com/integrations.md) still get them.
* **Options.** The second argument sets the properties of an `AgentCatOptions` object (see [Options](#options)). AgentCat calls it once, when the first server options are built, and reads the options then: later changes have no effect.
* **It never throws**, except `ArgumentNullException` for a `null` builder. When AgentCat cannot track the server, the server runs untracked.
* **Once per server.** A second `WithAgentCat()` call on the same server installs nothing, and the first call's project ID and options stay. Only its `ApiBaseUrl` applies, because all servers of a process share one event queue.
* **`IAgentCat`.** `WithAgentCat()` also registers `IAgentCat` as a singleton, unless your app registered one already (see [IAgentCat and custom events](#iagentcat-and-custom-events)).

## Stateless HTTP

Stateless HTTP is the recommended transport of the MCP specification, and the primary path of the C# SDK. A complete server:

```csharp theme={null}
using System.ComponentModel;
using ModelContextProtocol.Server;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddMcpServer()
    .WithHttpTransport(o => o.Stateless = true)
    .WithTools<MyTools>()
    .WithAgentCat("proj_YOUR_PROJECT_ID");

var app = builder.Build();
app.MapMcp();
app.Run();

[McpServerToolType]
public class MyTools
{
    [McpServerTool, Description("Echoes the message back.")]
    public static string Echo(string message) => $"Echo: {message}";
}
```

* **A new server for every request.** In stateless mode the MCP SDK builds new server options and a new server for every POST. AgentCat installs on each of them, and they all share one tracker per `WithAgentCat()` call, so what AgentCat learns from a `tools/list` serves the `tools/call` of a later request.
* **A fresh process.** When a `tools/call` reaches a process that has not listed its tools yet, for example a new instance behind a load balancer, AgentCat lists the tools itself (your list handler, then `ToolCollection`), with a 5-second limit, to learn which parameters it added.
* **Sessions.** AgentCat adds a `session_id` parameter to each tool: the agent sends `start` on its first call, gets a `ses_` ID in the result, and sends that ID on its later calls. See [Stateless Servers](https://docs.agentcat.com/sdk/stateless-servers.md).
* **The default transport.** On `ModelContextProtocol` 2.x, `WithHttpTransport()` without options is already stateless. On 1.x it is stateful.

## Stateful HTTP

A stateful server needs the same call:

```csharp theme={null}
builder.Services.AddMcpServer()
    .WithHttpTransport(o => o.Stateless = false)
    .WithTools<MyTools>()
    .WithAgentCat("proj_YOUR_PROJECT_ID");
```

AgentCat groups the calls of a task by its `session_id` parameter here too, not by the transport's `Mcp-Session-Id`. Hooks can read that one as `call.TransportSessionId`.

## stdio on the generic host

A stdio server on the generic host adds `WithAgentCat()` in the same way:

```csharp theme={null}
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;

var builder = Host.CreateApplicationBuilder(args);

// stdout carries the MCP messages: write the console logs to stderr
builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace);

builder.Services.AddMcpServer()
    .WithStdioServerTransport()
    .WithTools<MyTools>()
    .WithAgentCat("proj_YOUR_PROJECT_ID");

await builder.Build().RunAsync();

[McpServerToolType]
public class MyTools
{
    [McpServerTool, Description("Echoes the message back.")]
    public static string Echo(string message) => $"Echo: {message}";
}
```

AgentCat writes nothing to stdout or stderr. When the client closes the input, the host stops, and AgentCat sends the queued events (see [Shutdown](#shutdown)).

## Request headers and URL

On an ASP.NET Core server, AgentCat copies the HTTP request of each tool call into its event, under `parameters.extra.requestInfo`: `headers` holds the request headers, and `url` the full URL of the request. Your hooks get the same headers in `call.Headers`.

* **Every header, as received.** AgentCat filters no header: `authorization` and `cookie` go out too, and so does `x-forwarded-for`, like any other header. AgentCat records no IP address of its own. Header names are lower-case, and a header sent several times has one value, joined with `, ` (`; ` for `cookie`).
* **The URL keeps its query string**, which can carry an API key or other credentials.
* **How AgentCat reads the request.** `WithAgentCat()` registers ASP.NET Core's `HttpContextAccessor` when your app has none. `AgentCatSdk.Track()` registers nothing, so it reads the request only when your app calls `AddHttpContextAccessor()`.
* **Legacy SSE.** On the legacy SSE transport, the request is the session's `GET /sse`, not the `POST` that carried the call, so the headers and the URL belong to the session.
* **No request.** On stdio servers, on .NET Framework, with `PerSessionExecutionContext = true`, and for a tool call whose client opts in to MCP 2.x tasks, AgentCat has no request to read. The event then has no `requestInfo`, and `call.Headers` is empty.

To keep credentials out of your events, remove them in a `RedactEvent` hook. The hook also gets custom events, whose `parameters` can have any shape, and an exception in the hook drops the event, so test each level by type. This hook removes the `authorization` and `cookie` headers and the query string of the URL:

```csharp theme={null}
using System.Text.Json.Nodes;
using AgentCat;

builder.Services.AddMcpServer()
    .WithHttpTransport(o => o.Stateless = true)
    .WithTools<MyTools>()
    .WithAgentCat("proj_YOUR_PROJECT_ID", options =>
    {
        options.RedactEvent = (e, _) =>
        {
            if (e.Parameters is JsonObject p && p["extra"] is JsonObject x && x["requestInfo"] is JsonObject info)
            {
                if (info["headers"] is JsonObject headers)
                {
                    headers.Remove("authorization");
                    headers.Remove("cookie");
                }

                if (info["url"] is JsonValue url && url.TryGetValue(out string? text) && text.IndexOf('?') is var query and >= 0)
                {
                    info["url"] = text.Substring(0, query);
                }
            }

            return new ValueTask<AgentCatEvent?>(e);
        };
    });
```

## `IAgentCat` and custom events

`IAgentCat` has the methods of the static `AgentCatSdk` class except `Track()` and `Fake()`: the two forms of `PublishCustomEventAsync`, `FlushAsync` and `ShutdownAsync`. Inject it where you publish [custom events](https://docs.agentcat.com/sdk/custom-events.md). `WithAgentCat()` registers it unless your app registered an `IAgentCat` already. To test what your server tracks, use [`AgentCatSdk.Fake()`](https://docs.agentcat.com/sdk/csharp/operations#testing): the fake records the tool-call events and the custom events.

### Custom events from a tool

A tool publishes a custom event for its server with the `McpServer` that it receives:

```csharp theme={null}
using System.ComponentModel;
using System.Text.Json.Nodes;
using AgentCat;
using ModelContextProtocol.Server;

[McpServerToolType]
public class BillingTools(IAgentCat agentCat)
{
    [McpServerTool, Description("Upgrades the subscription.")]
    public async Task<string> Upgrade(McpServer server, string plan)
    {
        // ... upgrade the subscription

        await agentCat.PublishCustomEventAsync(server, "proj_YOUR_PROJECT_ID", new CustomEventData
        {
            ResourceName = "subscription_upgraded",
            Parameters = new JsonObject { ["plan"] = plan },
        });

        return $"Upgraded to {plan}";
    }
}
```

* **The server.** In stateless HTTP, the `McpServer` is a new object for every request. AgentCat finds the tracked server through its options, so any of them works.
* **The project ID** must not be empty, but the event goes to the project that the server was tracked with.
* **The session.** An event without a `SessionId` in its `CustomEventData` has no session. The tool cannot read AgentCat's `session_id`: AgentCat removes it from the arguments before your tool runs.
* **The task** completes when AgentCat has queued the event, not when it has sent it. It faults with an `ArgumentException` for an empty project ID or a server that AgentCat does not track, unless AgentCat was off when a server was set up in this process: it then logs a warning instead. Other failures never reach your code.

## Shutdown

AgentCat sends events from a process-wide queue in the background, and you call nothing at shutdown. When the host stops, AgentCat waits until every hosted service has stopped, the MCP stdio session and Kestrel included. Then it sends the queued events for at most 5 seconds, and its last diagnostics batch for at most 1 second. It also sends the queued events at process exit.

* **Allow 6 seconds.** AgentCat ignores the shutdown token of the host, so a stop can take up to 6 seconds longer than `HostOptions.ShutdownTimeout`. Set the grace period of your container above that.
* **`Microsoft.Extensions.Hosting` 8 or later** runs the flush after the host stops. Older hosts and the legacy `WebHost` rely on the process-exit handler.
* **A service provider without a host** runs no flush. If you build and dispose a service provider yourself, call `FlushAsync` or `ShutdownAsync` before you dispose it.
* **`DisableShutdownHandlers = true`** on the first install of the process turns off the process-exit handler. The flush after the host stops stays on.

[Shutdown](https://docs.agentcat.com/sdk/csharp/operations.md#shutdown) has the details: the stop budget that all stop paths share, `FlushAsync` and `ShutdownAsync`, and the cases where nothing runs, such as a stdio server that the official C# client kills.

## Options

`WithAgentCat()` and `AgentCatSdk.Track()` take the same `AgentCatOptions`. The four tool-call hooks (`ResolveSessionId`, `Identify`, `EventTags` and `EventProperties`) are `Func<AgentCatToolCall, CancellationToken, ValueTask<T>>`, and the Type column names their `T`.

| Property | Type | Default | What it does |
| - | - | - | - |
| `EnableReportMissing` | `bool` | `true` | Adds the `get_more_tools` tool ([Agent Feedback](https://docs.agentcat.com/sdk/report-missing-tools.md)). |
| `EnableTracing` | `bool` | `true` | Records tool-call events and adds the `session_id` parameter. |
| `EnableToolCallContext` | `bool` | `true` | Adds the `context` parameter ([Tool Call Context](https://docs.agentcat.com/sdk/tool-call-context.md)). |
| `CustomContextDescription` | `string?` | `null`: the default description | Replaces the description of the `context` parameter. |
| `EnableAgentTracking` | `bool` | `false` | Adds an `agent_id` parameter, with which each agent names itself. |
| `ResolveSessionId` | hook: `string?` | `null` | Returns your own session key for a tool call. AgentCat derives the session from it and adds no `session_id` parameter. A `null` result gives the call a new session. |
| `Identify` | hook: `UserIdentity?` | `null` | Names the user behind a tool call ([Identifying Users](https://docs.agentcat.com/sdk/identifying-users.md)). |
| `EventTags` | hook: `IReadOnlyDictionary<string, string>?` | `null` | Adds tags to the event ([Event Tags & Properties](https://docs.agentcat.com/sdk/event-tags-properties.md)). |
| `EventProperties` | hook: `JsonObject?` | `null` | Adds properties to the event ([Event Tags & Properties](https://docs.agentcat.com/sdk/event-tags-properties.md)). |
| `RedactSensitiveInformation` | `Func<string, CancellationToken, ValueTask<string>>?` | `null` | Rewrites the strings of a tool-call event before delivery ([Client-Side Redaction](https://docs.agentcat.com/privacy-security/client-side-redaction.md)). |
| `RedactEvent` | `Func<AgentCatEvent, CancellationToken, ValueTask<AgentCatEvent?>>?` | `null` | Edits or drops a whole event ([Client-Side Redaction](https://docs.agentcat.com/privacy-security/client-side-redaction.md)). |
| `ApiBaseUrl` | `string?` | `null`: `AGENTCAT_API_URL`, else `MCPCAT_API_URL`, else `https://api.agentcat.com` | The base URL of the AgentCat API, for every server of the process. |
| `Exporters` | `IDictionary<string, AgentCatExporter>`, read-only property | Empty | Sends each event to other observability tools too, under a name of your choice ([Integrations](https://docs.agentcat.com/integrations.md)). The first install in the process with exporters decides. |
| `DisableDiagnostics` | `bool` | `false` | Turns off the diagnostics that send AgentCat's own log lines to AgentCat ([Diagnostics](https://docs.agentcat.com/sdk/csharp/operations.md#diagnostics)). The first install in the process decides. |
| `LogToHost` | `bool` | `false` | Sends AgentCat's log lines to the `ILoggerFactory` of the host, in the category `AgentCat`, instead of `~/agentcat.log`. `WithAgentCat()` only ([The log file](https://docs.agentcat.com/sdk/csharp/operations.md#the-log-file)). |
| `DisableShutdownHandlers` | `bool` | `false` | Turns off the process-exit handler and, for `Track()`, the SIGTERM and SIGINT handlers ([Shutdown](https://docs.agentcat.com/sdk/csharp/operations.md#shutdown)). The first install in the process decides. |

AgentCat starts the four tool-call hooks when the call starts, so they run alongside your tool, and it waits at most 30 seconds for each before it sends the event without that result. A hook that throws changes nothing but its own result. [C# differences and limits](https://docs.agentcat.com/sdk/csharp/limits.md) lists what to know about hooks and the server's filters.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.