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:
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:
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:
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:
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);
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:
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}");
{"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
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}");
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:
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);
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:
dotnet run --project GlossaryServer -- --http 3001Serving http://127.0.0.1:3001/mcpA URL instead of a command line connects over Streamable HTTP. GlossaryHttp/Program.cs:
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);
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:
dotnet new web -o WebGlossary dotnet add WebGlossary package RecursiveAI.OneModelContext.AspNetCore --version 1.0.1
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:
dotnet tool install --global RecursiveAI.OneModelContext.Cli --version 1.0.1 onemodelcontext inspect "dotnet run --project GlossaryServer"
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)
onemodelcontext call "dotnet run --project GlossaryServer" define --arg term=idempotentidempotent: 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:
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
42The server template has two tools (add and greet) and sets PublishAot, so dotnet publish makes a single native executable.
Next
- Overview: the eight packages, what each adds and what it depends on.
- Samples: a program for each way of using the library, each with tests.
- Resource server, OAuth client and authorization server: sign-in on both sides.
- Coming from the official C# SDK: where each task lives here.
- Conformance: what the protocol’s conformance suite checked, revision by revision.