Recursive AI

Dependency injection for tools in .NET

OneModelContext is a free, open-source .NET SDK for the Model Context Protocol® from Recursive AI. MIT or Apache-2.0, no native dependencies.

Supply Model Context Protocol® (MCP) tool dependencies from your .NET service container.

Terminal
dotnet add package RecursiveAI.OneModelContext.Hosting
dotnet add package Microsoft.Extensions.Hosting

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using Microsoft.Extensions.DependencyInjection;
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Hosting;
 
var services = new ServiceCollection();
services.AddSingleton<GreetingBook>();
services.AddContextServer<Greeter>();
using var provider = services.BuildServiceProvider();
var server = provider
    .GetRequiredService<ContextServer>();
Console.WriteLine(server.Tools.Count);
 
public sealed class GreetingBook
{
    public string Text => "Hello";
}
public partial class Greeter(GreetingBook book)
{
    [Tool]
    public string Greet() => book.Text;
}

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

The snippet creates the tool class through the container and prints the registered tool count:

Text
1

Unzip the example, then dotnet run.

The full program

The download uses the generic host, starts a child server, and calls two tools. GreetingBook is a singleton passed to the tool’s constructor. RequestLabel is scoped and resolved inside the handler. The core package’s source generator supplies Greeter’s registration code from the partial class and its [Tool] methods.

The host clears console logging because standard output carries protocol messages. Missing service registrations fail when the container or handler tries to resolve them.

C#
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Hosting;
using System.Reflection;
 
if (args.Contains("--stdio"))
{
    var builder = Host.CreateApplicationBuilder();
    builder.Logging.ClearProviders();
    builder.Services.AddSingleton<GreetingBook>();
    builder.Services.AddScoped<RequestLabel>();
    builder.Services.AddContextServer<Greeter>().UseStdio();
    await builder.Build().RunAsync();
    return;
}
var assembly = Assembly.GetExecutingAssembly().Location;
await using var client = await ModelContext.ConnectAsync($"dotnet \"{assembly}\" --stdio");
Console.WriteLine((await client.CallToolAsync("greet", new { name = "Ada" })).Text);
Console.WriteLine((await client.CallToolAsync("label", new { })).Text);
 
public sealed class GreetingBook
{
    public string Greet(string name) => $"Hello, {name}!";
}
public sealed class RequestLabel
{
    public string Text => "Scoped service resolved";
}
public partial class Greeter(GreetingBook book)
{
    /// <summary>Greet a person.</summary>
    [Tool]
    public string Greet(string name) => book.Greet(name);
 
    /// <summary>Read a request-scoped label.</summary>
    [Tool]
    public string Label(RequestContext context) =>
        context.GetRequiredService<RequestLabel>().Text;
}

dotnet run prints:

Text
Hello, Ada!
Scoped service resolved

Variations

Use a factory instead of constructor discovery. Replace the AddContextServer<Greeter>() line in the full program with:

C#
builder.Services.AddContextServer()
    .AddTools(services => new Greeter(
        services.GetRequiredService<GreetingBook>()))
    .UseStdio();

The output stays:

Text
Hello, Ada!
Scoped service resolved

The short answer

For MCP server dependency injection in .NET (Model Context Protocol®), install RecursiveAI.OneModelContext.Hosting and register tools with services.AddContextServer<Tools>(). The tool class is constructed from the container. Its constructor should take singleton dependencies; request-scoped dependencies belong inside the handler and are resolved through RequestContext.

The full example includes both patterns. It uses Microsoft.Extensions.Hosting to run a stdio server whose lifetime follows the generic host. The hosting adapter itself references the hosting abstractions; your application chooses the generic host package. You can also use the container to construct a server without starting a transport, as the snippet at the top does.

How can I implement dependency injection in C# in .NET Core?

Register application services before registering the tool class. AddSingleton<GreetingBook>() says one book is shared for the container’s lifetime. AddContextServer<Greeter>() asks the adapter to register the generated tool class. The container supplies its constructor arguments using ActivatorUtilities, unless the class already has its own registration.

Greeter must be partial because the core package’s source generator adds its registration implementation. [Tool] identifies the methods to publish. A plain class with unannotated public methods is not automatically exposed as tools. Keep the package reference in the example project so the generator participates in compilation.

For explicit construction, the factory variation uses AddTools(services => new Greeter(...)). The factory builds the tool class without reflection. It is useful when construction needs application-specific choices that don’t map neatly to automatic constructor selection. The factory is not a per-call factory: registration happens when the server is first resolved.

What is .NET dependency injection?

For this server, dependency injection separates tool behavior from service construction. The container owns the services, while the tool receives what it needs. The practical issue is lifetime: the adapter keeps one tool-class instance for the life of the server, so constructor dependencies live with that instance.

A request scope is different. Every tool, resource and prompt invocation gets its own scope. context.GetRequiredService<RequestLabel>() resolves the label from that scope, and the scope is disposed when the call ends. Don’t capture a scoped service in the long-lived tool constructor. The full program’s Label method shows exactly where to resolve it.

When a tool using protocol revision 2026-07-28 asks for client input, it may be run again from the start on retry. Each attempt gets a new scope. Don’t assume a scoped object survives a confirmation round trip. These lifetime rules come from the pinned hosting adapter documentation.

A stdio transport also gives the process’s output stream a special role: it carries protocol data. Clear console logging as this example does, or direct the console logger to standard error. UseStdio() serves one connection and asks the host to stop when that connection closes, so the full program can dispose its client and let the child server end.

All onemodelcontext recipes