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.
dotnet add package RecursiveAI.OneModelContextRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
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:
2025-03-26Unzip the example, 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:
Unzip the example into
../OlderServer, a sibling of your client project’s folder.From your client project’s folder, build that server:
Terminal dotnet build ../OlderServerPut the snippet at the top in your client’s
Program.cs, install the package shown above, and run:Terminal 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.
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:
2025-03-26 (Legacy) 42
Variations
Pin the legacy era instead of probing automatically:
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);
2025-03-26Pass the same server command as the first argument. This pins an era, not a specific legacy revision.
Related recipes
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 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 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 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. That is distinct from retrying an individual failed tool call: revision negotiation chooses how the connection communicates, not whether an operation should be repeated.