Add Model Context Protocol® (MCP) tools—callable operations—without restarting your server.
dotnet add package RecursiveAI.OneModelContextRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
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:
1Unzip the example, 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.
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:
Before: enable Enabled After: enable, hello Hello
Variations
Remove a tool by name. Add this after the snippet at the top:
Console.WriteLine(server.Tools.Remove("hello")); Console.WriteLine(server.Tools.Count);
True 0
Related recipes
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.
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 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.