Recursive AI

Getting started with OneModelContext

All onemodelcontext docs

Install OneModelContext 1.0.1 for .NET 10, write a server with two tools and a resource, connect a client to it over stdio and over Streamable HTTP, host it on ASP.NET Core, and drive it from the command line. Every sample on this page was run on the 1.0.1 packages from NuGet and shows its real output.

Install

The packages target net10.0, so you need the .NET 10 SDK. A server and a client are two console programs:

Terminal
dotnet new console -o GlossaryServer
dotnet add GlossaryServer package RecursiveAI.OneModelContext --version 1.0.1
dotnet new console -o GlossaryClient
dotnet add GlossaryClient package RecursiveAI.OneModelContext --version 1.0.1

RecursiveAI.OneModelContext is the whole protocol: client, server, transports, the built-in HTTP listener, authorization and the [Tool] source generator. In the README’s words, it “depends on the .NET base class library only, and is trim- and NativeAOT-safe.” Seven more packages share its version; the overview lists what each adds.

Write a server

A tool is a method on a partial class with [Tool] on it. The source generator reads the signature and the doc comments and writes the tool’s JSON Schema and an invoker, so nothing is found by reflection at run time. [Resource] with a URI template makes a resource.

GlossaryServer/Glossary.cs:

C#
using System.Collections.Concurrent;
using RecursiveAI.OneModelContext;
 
/// <summary>A glossary the model can read and add to.</summary>
public partial class Glossary
{
    private readonly ConcurrentDictionary<string, string> _terms =
        new(StringComparer.OrdinalIgnoreCase);
 
    public Glossary()
    {
        _terms["idempotent"] =
            "Safe to repeat: doing it twice has the same effect as doing it once.";
    }
 
    /// <summary>Looks up a term.</summary>
    /// <param name="term">The word or phrase to define.</param>
    [Tool(ReadOnly = true)]
    public string Define(string term) =>
        _terms.TryGetValue(term, out var meaning)
            ? $"{term}: {meaning}"
            : $"No entry for '{term}'.";
 
    /// <summary>Adds a term, replacing an older definition.</summary>
    /// <param name="term">The word or phrase.</param>
    /// <param name="meaning">What it means, in one sentence.</param>
    [Tool(Idempotent = true)]
    public string AddTerm(string term, string meaning)
    {
        _terms[term] = meaning;
        return $"Added '{term}'. The glossary has {_terms.Count} terms.";
    }
 
    /// <summary>A term's definition, as plain text.</summary>
    /// <param name="term">The term.</param>
    [Resource("glossary://{term}", MimeType = "text/plain")]
    public string Entry(string term) =>
        _terms.TryGetValue(term, out var meaning)
            ? meaning
            : throw new ProtocolException(-32602, "No such term.");
}

GlossaryServer/Program.cs serves it over standard input and output, or over Streamable HTTP when asked:

C#
using RecursiveAI.OneModelContext;
 
// stdio by default; "--http <port>" serves Streamable HTTP on the built-in
// listener instead.
if (args is ["--http", var port])
{
    await ModelContext.ServeHttpAsync(new Glossary(), port: int.Parse(port),
        onListening: url => Console.WriteLine($"Serving {url}"));
}
else
{
    await ModelContext.ServeAsync(new Glossary());
}

Over stdio, standard output carries the protocol, so a server writes its own diagnostics to standard error.

Connect a client over stdio

ModelContext.ConnectAsync takes a command line, starts it as a child process (no shell runs) and negotiates the protocol era before it returns. Tool arguments can be any object; its properties become the tool’s arguments.

GlossaryClient/Program.cs:

C#
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Protocol;
 
// Starts the server as a child process and talks to it over its standard input
// and output.
await using var client = await ModelContext.ConnectAsync(
    "dotnet run --project ../GlossaryServer");
var server = client.ServerInfo.Name;
Console.WriteLine($"{server}, protocol {client.ProtocolVersion} ({client.Era} era)");
 
foreach (var tool in await client.ListToolsAsync())
{
    Console.WriteLine($"{tool.Name}: {tool.Description}");
}
 
var defined = await client.CallToolAsync("define", new { term = "idempotent" });
Console.WriteLine(defined.Text);
Text
GlossaryServer, protocol 2026-07-28 (Modern era)
add_term: Adds a term, replacing an older definition.
define: Looks up a term.
idempotent: Safe to repeat: doing it twice has the same effect as doing it once.

The server answered the 2026-07-28 revision. The next three sections continue the same Program.cs. Tool names are snake case by default (AddTerm became add_term), and the descriptions are the <summary> comments.

Passing an anonymous object serializes it by reflection. In the README’s words, “The object-argument call uses reflection and is not trim- or NativeAOT-safe. For AOT, pass source-generated serializer metadata instead”: CallToolAsync(name, arguments, typeInfo) takes a JsonTypeInfo<T> from your own JsonSerializerContext.

The generated schema

Each <param> comment becomes a property description, parameters without a default are required, and the annotation properties of [Tool] become hints:

C#
var tools = await client.ListToolsAsync();
var addTerm = tools.First(t => t.Name == "add_term");
Console.WriteLine(addTerm.InputSchema);
Console.WriteLine($"idempotent hint: {addTerm.Annotations?.IdempotentHint}");
Text
{"type":"object","properties":{"term":{"type":"string","description":"The word or phrase."},"meaning":{"type":"string","description":"What it means, in one sentence."}},"required":["term","meaning"],"additionalProperties":false}
idempotent hint: True

The dialect is JSON Schema 2020-12. Tool signatures and JSON Schema maps every .NET type the generator accepts.

Call a tool, read a resource

C#
var added = await client.CallToolAsync("add_term", new
{
    term = "stdio",
    meaning = "A process's standard input and output streams.",
});
Console.WriteLine(added.Text);
 
var entry = await client.ReadResourceAsync("glossary://stdio");
Console.WriteLine(entry.Contents[0] is TextResourceContents text
    ? text.Text
    : "(binary)");
 
var missing = await client.CallToolAsync("define", new { trm = "typo" });
Console.WriteLine($"isError={missing.IsError}: {missing.Text}");
Text
Added 'stdio'. The glossary has 2 terms.
A process's standard input and output streams.
isError=True: Unknown argument 'trm'.

A bad argument is a tool result with IsError set, not an exception, so a model can read it and try again.

Older servers and clients

The same code talks to servers that only know the older revisions. In the README’s words: “A client negotiates the era per server; a server answers both on one stdio stream or endpoint.” Pinning the legacy era shows the other side of the same server:

C#
await using var legacy = await ModelContext.ConnectAsync(
    "dotnet run --project ../GlossaryServer", era: EraPreference.Legacy);
Console.WriteLine($"protocol {legacy.ProtocolVersion} ({legacy.Era} era)");
 
var again = await legacy.CallToolAsync("define", new { term = "idempotent" });
Console.WriteLine(again.Text);
Text
protocol 2025-11-25 (Legacy era)
idempotent: Safe to repeat: doing it twice has the same effect as doing it once.

One legacy codec covers “2024-11-05 through 2025-11-25.” A handler that asks the client for input behaves the same on both eras; on 2026-07-28 it’s run again from the start, so see the replay rule before a tool asks for confirmation.

Streamable HTTP on the built-in listener

Start the server with --http 3001. ServeHttpAsync runs the library’s small HTTP/1.1 listener on the loopback address:

Terminal
dotnet run --project GlossaryServer -- --http 3001
Text
Serving http://127.0.0.1:3001/mcp

A URL instead of a command line connects over Streamable HTTP. GlossaryHttp/Program.cs:

C#
using RecursiveAI.OneModelContext;
 
// The same client, by URL: Streamable HTTP instead of a child process.
await using var client = await ModelContext.ConnectAsync(
    "http://127.0.0.1:3001/mcp");
var server = client.ServerInfo.Name;
Console.WriteLine($"{server}, protocol {client.ProtocolVersion} ({client.Era} era)");
 
var result = await client.CallToolAsync("define", new { term = "idempotent" });
Console.WriteLine(result.Text);
Text
GlossaryServer, protocol 2026-07-28 (Modern era)
idempotent: Safe to repeat: doing it twice has the same effect as doing it once.

The endpoint checks requests before any handler runs. A POST without Content-Type: application/json got 415 Unsupported Media Type, and one with a foreign Origin header got 403 Forbidden (DNS-rebinding protection). The docs say the listener is “for local tools and command-line programs” and “For a server on the internet, host the endpoint in a web framework.” Every limit is in the server and listener limits.

Host it on ASP.NET Core

RecursiveAI.OneModelContext.AspNetCore adds MapModelContext. Make a web project and reuse Glossary.cs:

Terminal
dotnet new web -o WebGlossary
dotnet add WebGlossary package RecursiveAI.OneModelContext.AspNetCore --version 1.0.1
C#
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.AspNetCore;
 
var builder = WebApplication.CreateSlimBuilder(args);
var app = builder.Build();
 
var server = new ContextServer("web-glossary", "1.0.0");
new Glossary().AddTo(server);
 
// One endpoint answers the 2026-07-28 revision and the older ones.
app.MapModelContext("/mcp", server);
app.Run();

The generator wrote Glossary.AddTo. Middleware added before MapModelContext (authentication, rate limiting) runs before the protocol handler, and .RequireAuthorization() on the endpoint works with the resource server. Started with --urls http://127.0.0.1:5080, it answered onemodelcontext inspect http://127.0.0.1:5080/mcp (below) with server: web-glossary 1.0.0 and protocol: 2026-07-28 (modern era), and with protocol: 2025-11-25 (legacy era) under --era legacy.

From the command line

RecursiveAI.OneModelContext.Cli is a .NET tool named onemodelcontext. It inspects and calls any server, by command line or URL:

Terminal
dotnet tool install --global RecursiveAI.OneModelContext.Cli --version 1.0.1
onemodelcontext inspect "dotnet run --project GlossaryServer"
Text
server: GlossaryServer 1.0.0
protocol: 2026-07-28 (modern era)
era chosen: the server answered server/discover
capabilities: tools (listChanged), resources (subscribe, listChanged), logging
tools (2):
  add_term - Adds a term, replacing an older definition.
  define - Looks up a term.
resources (0):
resource templates (1):
  glossary://{term} - A term's definition, as plain text. (template)
Terminal
onemodelcontext call "dotnet run --project GlossaryServer" define --arg term=idempotent
Text
idempotent: Safe to repeat: doing it twice has the same effect as doing it once.

--arg values are typed from the tool’s input schema, and --era legacy pins the older revisions. The tool also has read, prompt, login, logout and proxy; the OAuth client covers login.

Start from a template

RecursiveAI.OneModelContext.Templates adds dotnet new onemodelcontext-server and onemodelcontext-client:

Terminal
dotnet new install RecursiveAI.OneModelContext.Templates::1.0.1
dotnet new onemodelcontext-server -o MyServer
onemodelcontext call "dotnet run --project MyServer" add --arg a=2 --arg b=40
Text
42

The server template has two tools (add and greet) and sets PublishAot, so dotnet publish makes a single native executable.

Next