Recursive AI

Register a protocol extension 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.

A Model Context Protocol® extension adds methods, notifications or metadata without changing the core protocol revision. It is not a C# extension method or an OS URL protocol handler.

Terminal
dotnet add package \
  RecursiveAI.OneModelContext

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

C#
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext
    .Extensions;
 
var server = new ContextServer(
    "Extensions", "1.0");
server.Extensions.Add(
    new ExtensionRegistration(
        "com.example/echo")
        .Request("com.example/echo/say",
            (parameters, _) =>
            ValueTask.FromResult(parameters)));

The snippet is only the registration step; the zip connects a client and calls the method. Only peers that declare the extension can call it.

Unzip the example, then dotnet run.

The full program

The program connects an in-memory client that declares com.example/echo, checks the server’s support, then sends the inline JSON input {"text":"Hello"}.

C#
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Protocol;
using System.Text.Json;
using RecursiveAI.OneModelContext.Extensions;
 
var server = new ContextServer("Extensions", "1.0");
server.Extensions.Add(
    new ExtensionRegistration("com.example/echo")
        .Request("com.example/echo/say", (parameters, _) =>
            ValueTask.FromResult(parameters)));
 
var toServer = new System.IO.Pipelines.Pipe();
var toClient = new System.IO.Pipelines.Pipe();
var serving = server.RunAsync(ContextTransport.Stream(
    toServer.Reader.AsStream(), toClient.Writer.AsStream()));
await using (var client = await ContextClient.ConnectAsync(
    ContextTransport.Stream(toClient.Reader.AsStream(), toServer.Writer.AsStream()),
    new ContextClientOptions
    {
        Extensions = [new ClientExtension("com.example/echo")]
    }))
{
    Console.WriteLine(client.ServerSupportsExtension("com.example/echo"));
    using var input = JsonDocument.Parse("""{"text":"Hello"}""");
    var result = await client.CallExtensionAsync(
        "com.example/echo/say", input.RootElement);
    Console.WriteLine(result.GetProperty("text").GetString());
}
await serving;

dotnet run prints:

Text
True
Hello

A peer that has not declared the extension receives method-not-found (-32601) for its methods.

Variations

To restrict the extension to a core revision, replace its constructor with:

C#
new ExtensionRegistration("com.example/echo",
    revisions: ["2026-07-28"])

Keep the chained Request registration. The example’s current-revision client still prints True and Hello.


The short answer

Add an ExtensionRegistration to server.Extensions, register its request handlers before adding it, and configure the connecting client with a matching ClientExtension. The client can check ServerSupportsExtension after connection and invoke a registered method with CallExtensionAsync.

Registering a server method alone is not negotiation. The server must offer the extension and the peer must declare support. The runnable example includes both sides, so it demonstrates a negotiated call rather than just a populated registry. Its echo handler returns the parameters as a JSON object; the client prints the text field.

What are protocol extensions?

Extensions add methods, notifications, _meta keys or capability settings without requiring a new core protocol revision. They are opt-in. The protocol extensions overview describes this mechanism; the pinned library documentation gives the API and dispatch behavior used here.

This is not a C# extension method, an operating-system URL protocol handler or a service-container registration. It extends the messages exchanged between a protocol client and server. If all you need is another callable tool, register a tool instead. Use a protocol extension when peers need an additional message contract and agree to implement it.

The echo method is deliberately small. A production extension needs its own documented input, output and metadata contract. Its identifier should use a reversed domain you own, followed by a slash and its name. com.example/echo is an example identifier, not a domain ownership claim.

How do I register a protocol extension in C#?

Build the registration with its identifier, settings and optional supported revisions. Chain Request and Notification handlers onto it before calling Extensions.Add. Adding it freezes the registration; finish constructing it first.

Identifiers use {vendor-prefix}/{name}. Reserved prefixes, including io.modelcontextprotocol and the reserved suffix families documented by the library, are refused for third-party extensions. Use your own vendor prefix rather than imitating a built-in extension.

Settings are advertised JSON objects. For source-generated serialization, ExtensionRegistration.Create<T> accepts serialization metadata. A null revision list means all core revisions; the variation limits this example to one revision. On a revision outside that list, the library does not advertise the extension and its methods answer method-not-found.

How does a client negotiate a protocol extension?

Only entries in ContextClientOptions.Extensions are declared by the client. On core revision 2026-07-28, client capabilities travel in request _meta, and server extensions appear in server/discover. On 2025-11-25 and earlier, both sides use initialize capabilities. The library’s support-checking API abstracts those paths.

Within a handler, context.SupportsExtension(id) checks peer support, and the settings helpers retrieve what it advertised. Do not equate unknown extensions with errors: the library retains and ignores unknown declarations.

Optional behavior is the default. Setting Required = true rejects peers that lack support; the documented error mapping depends on the protocol revision. A required client extension instead makes connection fail with ExtensionRequiredException when the server lacks it. This example leaves required mode off. Server-to-client extension requests and multi-round-trip input requests inside extension calls are not supported by this version.

All onemodelcontext recipes