---
title: "Getting started with OneModelContext"
url: https://recursiveai.org/onemodelcontext/docs/getting-started/
description: "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…"
version: "1.0.1"
---

# Getting started with OneModelContext

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:

```shell
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](https://recursiveai.org/onemodelcontext/docs/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`:

```csharp
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:

```csharp
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`:

```csharp
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:

```csharp
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](https://recursiveai.org/onemodelcontext/docs/tool-schemas/) maps every .NET type the generator accepts.

## Call a tool, read a resource

```csharp
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:

```csharp
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](https://recursiveai.org/onemodelcontext/docs/multi-round-trip/) 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:

```shell
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`:

```csharp
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](https://recursiveai.org/onemodelcontext/docs/http-limits/).

## Host it on ASP.NET Core

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

```shell
dotnet new web -o WebGlossary
dotnet add WebGlossary package RecursiveAI.OneModelContext.AspNetCore --version 1.0.1
```

```csharp
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](https://recursiveai.org/onemodelcontext/docs/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:

```shell
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)
```

```shell
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](https://recursiveai.org/onemodelcontext/docs/oauth-client/) covers `login`.

## Start from a template

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

```shell
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

- [Overview](https://recursiveai.org/onemodelcontext/docs/overview/): the eight packages, what each adds and what it depends on.
- [Samples](https://recursiveai.org/onemodelcontext/docs/samples/): a program for each way of using the library, each with tests.
- [Resource server](https://recursiveai.org/onemodelcontext/docs/resource-server/), [OAuth client](https://recursiveai.org/onemodelcontext/docs/oauth-client/) and [authorization server](https://recursiveai.org/onemodelcontext/docs/authorization-server/): sign-in on both sides.
- [Coming from the official C# SDK](https://recursiveai.org/onemodelcontext/docs/coming-from-the-official-sdk/): where each task lives here.
- [Conformance](https://recursiveai.org/onemodelcontext/docs/conformance/): what the protocol’s conformance suite checked, revision by revision.
