---
title: "Add tools at runtime in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/add-tools-at-runtime/
description: "Register, replace and remove tools while a .NET server is running, with a runnable example that refreshes the client’s tool list."
version: "1.0.1"
---

# Add tools at runtime 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/add-tools-at-runtime/add-tools-at-runtime.zip)

Add Model Context Protocol® (MCP) tools—callable operations—without restarting your server.

```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;
using RecursiveAI.OneModelContext.Protocol;
using System.Text.Json;

var server = new ContextServer("Demo", "1.0");
using var schema = JsonDocument.Parse(
    """{"type":"object"}""");
server.Tools.Add(
    new ToolDefinition("hello", "Greet", schema),
    (_, _) => ValueTask.FromResult(
        CallToolResult.FromText("Hello")));
Console.WriteLine(server.Tools.Count);
```

This snippet only builds the server; the full program below connects a client and calls the tool.

The snippet registers `hello` on a server instance and prints:

```text
1
```


Unzip [the example](https://recursiveai.org/onemodelcontext/cookbook/add-tools-at-runtime/add-tools-at-runtime.zip), then `dotnet run`.

## The full program

The download starts a child copy of itself as a server. Initially only `enable` exists. Calling it adds `hello` while the connection remains open; the client clears its listing cache, lists again and calls the new tool. All inputs are inline; there are no sample files.

Keep the schema document alive as long as its tool is registered. An invalid definition throws `ArgumentException` before registration.

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

if (args.Contains("--stdio"))
{
    using var schema = JsonDocument.Parse("""{"type":"object","properties":{}}""");
    var server = new ContextServer("DynamicTools", "1.0");
    server.Tools.Add(new ToolDefinition("enable", "Enable a greeting tool.", schema),
        (_, _) =>
        {
            server.Tools.Add(new ToolDefinition("hello", "Say hello.", schema),
                (_, _) => ValueTask.FromResult(CallToolResult.FromText("Hello")));
            return ValueTask.FromResult(CallToolResult.FromText("Enabled"));
        });
    await ModelContext.ServeAsync(server);
    return;
}
var assembly = Assembly.GetExecutingAssembly().Location;
await using var client = await ModelContext.ConnectAsync($"dotnet \"{assembly}\" --stdio");
Console.WriteLine("Before: " + string.Join(", ", (await client.ListToolsAsync()).Select(t => t.Name)));
Console.WriteLine((await client.CallToolAsync("enable", new { })).Text);
client.InvalidateCache();
Console.WriteLine("After: " + string.Join(", ", (await client.ListToolsAsync()).Select(t => t.Name)));
Console.WriteLine((await client.CallToolAsync("hello", new { })).Text);
```

`dotnet run` prints:

```text
Before: enable
Enabled
After: enable, hello
Hello
```

## Variations

Remove a tool by name. Add this after the snippet at the top:

```csharp
Console.WriteLine(server.Tools.Remove("hello"));
Console.WriteLine(server.Tools.Count);
```
```text
True
0
```

## Related recipes

- [Build a Model Context Protocol® server in C#](https://recursiveai.org/onemodelcontext/cookbook/build-a-server-in-csharp/)
- [Connect to a server in C#](https://recursiveai.org/onemodelcontext/cookbook/connect-to-a-server/)

---

## The short answer

For dynamic MCP tools in C# (Model Context Protocol®), register a `ToolDefinition` and its handler with `server.Tools.Add`. Do it on the same `ContextServer` instance that is serving clients. The registry can change after serving begins; you don’t need to generate a new class or restart the process.

The snippet at the top demonstrates registration without opening a connection. The full program demonstrates the important second half: a connected client discovers and invokes the newly added operation. Its `enable` handler closes over the existing server, adds `hello` and returns a normal tool result.

## How do I implement a Model Context Protocol® server in C#?

Use a `ContextServer` for runtime-defined tools, then pass it to `ModelContext.ServeAsync` for stdio. The full program shows that setup. If your tools are fixed at compile time, a partial class with `[Tool]` methods can instead let the package’s source generator produce schemas and invokers. Both paths serve tools through the same server API.

A runtime definition needs a name, a description and a JSON Schema object for its arguments. The handler receives the arguments as a `JsonElement` and the call’s `RequestContext`. Here `hello` accepts no arguments, so its schema describes an object and its handler ignores the argument element. A tool with parameters needs a schema describing those properties and a handler that reads them.

The schema is not copied when you pass a `JsonDocument`: the caller owns the document, and it must outlive the tool. Keep it alongside the registration rather than disposing it at the end of a helper method. See the pinned [registration API](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/src/RecursiveAI.OneModelContext/Registration.cs).

## How do dynamic tools work in C#?

A registry addition with an existing name replaces that entry. Removal uses `server.Tools.Remove(name)` and reports whether an entry existed. Use distinct, stable names when operations are distinct; reuse a name when you intend to replace an operation. A changed description is a replacement too, not a second tool with the same name.

There are two pieces of state to consider: the server’s registry and the client’s cached listing. Updating the registry does not mean an application has refreshed the list it is displaying. The example deliberately calls `client.InvalidateCache()` before listing again so it reads the changed catalog.

For ongoing discovery, modern clients use a subscription stream—an open connection that delivers updates when the server’s listings change; legacy clients can receive `notifications/tools/list_changed`. Those are different delivery paths. The example does not wait for either: its own successful `enable` call tells it when to refresh. This keeps the demonstration deterministic without implying that every connected application automatically updates its interface. The pinned [live-control sample](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/samples/LiveControl/Program.cs) demonstrates the legacy notification path.

Removing a listing is a catalog operation, not an authorization policy. If operations are available only to particular users, enforce that in the request path as well. A list displayed earlier is not a reliable statement that the operation is still available now.
