---
title: "Negotiate protocol versions in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/talk-to-older-servers/
description: "Connect a .NET client to older protocol revisions automatically, inspect the negotiated version and pin the legacy era when needed."
version: "1.0.1"
---

# Negotiate protocol versions in C#

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

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneModelContext) · [GitHub](https://github.com/recursive-ai-oss/onemodelcontext-csharp) · [Docs](https://recursiveai.org/onemodelcontext/docs/) · [Download example (.zip)](https://recursiveai.org/onemodelcontext/cookbook/talk-to-older-servers/talk-to-older-servers.zip)

Choose a protocol revision your client and server both support. In OneModelContext, “modern” means the stateless `2026-07-28` revision; “legacy” means the older revisions that start with an `initialize` handshake.

```shell
dotnet add package RecursiveAI.OneModelContext
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

```csharp
using RecursiveAI.OneModelContext;
// Pass the server command as one argument.
await using var client =
    await ModelContext.ConnectAsync(args[0]);
Console.WriteLine(client.ProtocolVersion);
```

`args[0]` is a server command passed as one quoted argument: use any stdio server, or the one in the zip. With the downloaded server, the snippet prints:

```text
2025-03-26
```


Unzip [the example](https://recursiveai.org/onemodelcontext/cookbook/talk-to-older-servers/talk-to-older-servers.zip), then `dotnet run`.

## The full program

The download includes a server restricted to `2025-03-26` and a client using automatic negotiation. It launches a child copy of itself, reports the negotiated revision and calls `add`. All inputs are inline.

To run the snippet at the top from a separate client project:

1. Unzip the example into `../OlderServer`, a sibling of your client project’s folder.
2. From your client project’s folder, build that server:

   ```shell
   dotnet build ../OlderServer
   ```

3. Put the snippet at the top in your client’s `Program.cs`, install the package shown above, and run:

   ```shell
   dotnet run -- "dotnet run --no-build --project ../OlderServer -- --stdio"
   ```

A connection with no compatible revision fails with `ProtocolVersionException`; forcing the modern era against this example’s legacy-only server also fails.

```csharp
using RecursiveAI.OneModelContext;
using System.Reflection;

if (args.Contains("--stdio"))
{
    var server = new ContextServer("OlderServer", "1.0", new ContextServerOptions
    {
        SupportedVersions = ["2025-03-26"]
    });
    new Calculator().AddTo(server);
    await ModelContext.ServeAsync(server);
    return;
}
var assembly = Assembly.GetExecutingAssembly().Location;
await using var client = await ModelContext.ConnectAsync($"dotnet \"{assembly}\" --stdio");
Console.WriteLine($"{client.ProtocolVersion} ({client.Era})");
Console.WriteLine((await client.CallToolAsync("add", new { a = 2, b = 40 })).Text);

public partial class Calculator
{
    /// <summary>Add two integers.</summary>
    [Tool]
    public int Add(int a, int b) => a + b;
}
```

`dotnet run` prints:

```text
2025-03-26 (Legacy)
42
```

## Variations

Pin the legacy era instead of probing automatically:

```csharp
using RecursiveAI.OneModelContext;
// Pass the server command as one argument.
await using var client =
    await ModelContext.ConnectAsync(
        args[0], EraPreference.Legacy);
Console.WriteLine(client.ProtocolVersion);
```
```text
2025-03-26
```

Pass the same server command as the first argument. This pins an era, not a specific legacy revision.

## Related recipes

- [Build a Model Context Protocol® server in C#](https://recursiveai.org/onemodelcontext/cookbook/build-a-server-in-csharp/)
- [Connect to a server in C#](https://recursiveai.org/onemodelcontext/cookbook/connect-to-a-server/)

---

## The short answer

MCP protocol version negotiation (Model Context Protocol®) is automatic in OneModelContext: call `ModelContext.ConnectAsync` and inspect `client.ProtocolVersion` after connection. Leave `EraPreference.Auto` in place unless you specifically need to restrict the connection to modern or legacy behavior.

The full example deliberately hosts a legacy-only server, restricted to `2025-03-26`. Its client connects without a legacy override, prints the chosen revision and invokes the calculator. This demonstrates negotiation rather than merely setting both sides to the same version.

## How does protocol version negotiation work?

OneModelContext divides supported revisions into two eras. The modern revision is `2026-07-28`; the legacy revisions are `2025-11-25`, `2025-06-18`, `2025-03-26` and `2024-11-05`. These are the package’s known revisions, not a promise to support any arbitrary date string. The pinned [protocol constants](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/src/RecursiveAI.OneModelContext/Protocol/ProtocolConstants.cs) define the supported set.

In the legacy protocol’s `initialize` exchange, the client proposes a version it supports and the server answers with that version or another version it supports. The [protocol specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation) says: “If the server supports the requested protocol version, it MUST respond with the same version. Otherwise, the server MUST respond with another protocol version it supports.” If the client cannot support the response’s version, the specification says it should disconnect.

Over stdio, OneModelContext’s automatic mode probes modern first and uses the legacy initialization path when appropriate. For a process you start yourself, check that it is actually starting and that diagnostic text is not being printed into its protocol output.

The two eras share the high-level tools API. Your application can keep using `ListToolsAsync` and `CallToolAsync` after negotiation instead of assembling revision-specific JSON-RPC messages itself.

## What protocol version should I use?

Use automatic negotiation for a general-purpose client. For a server under your control, keep the default supported set unless you have a specific compatibility constraint. The example restricts `SupportedVersions` only to make its legacy behavior visible; it is not a recommendation to disable newer revisions in production.

`EraPreference.Legacy` selects the older family. It does not force `2025-03-26`: a server offering a newer legacy revision can negotiate that instead. Check `ProtocolVersion` for the final answer. `EraPreference.Modern` excludes legacy servers and therefore cannot connect to the included server.

If you are diagnosing repeated connections, the era cache remembers which protocol family to try. A definitive legacy-only version list can teach it that a server is legacy. Silence or a recoverable transport failure may trigger fallback for that connection but does not record a legacy choice. Malformed probe replies likewise do not establish a legacy cache entry. A server that didn’t answer quickly enough has not necessarily announced that it is old. The pinned [stdio documentation](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/docs/stdio.md) describes those rules.

## What is the protocol-version header?

On Streamable HTTP, modern requests carry `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers matching the body. The version, capabilities and client information also travel in `_meta`. Legacy HTTP uses initialization and a session identifier instead. These wire differences are handled by the transport; don’t add a version header to a stdio command.

HTTP negotiation also has its own fallback rules, including the deprecated HTTP+SSE client fallback for appropriate endpoint rejections. A timeout does not permanently classify an endpoint as legacy. The default era cache expires legacy choices after five minutes without renewing that lifetime on cache hits, allowing an upgraded server to be probed again. See the pinned [HTTP client documentation](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/docs/http-client.md). That is distinct from retrying an individual failed tool call: revision negotiation chooses how the connection communicates, not whether an operation should be repeated.
