Recursive AI

Serve resource URI templates 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.

Expose related documents through a URI pattern, such as notes://{id}, instead of registering each document separately.

Terminal
dotnet add package RecursiveAI.OneModelContext

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

C#
using RecursiveAI.OneModelContext;
await ModelContext.ServeAsync(new Notes());
 
public partial class Notes
{
    [Resource("notes://{id}",
        MimeType = "text/markdown")]
    public string Note(int id) => $"# Note {id}";
}

Serves notes://42 as Markdown text # Note 42; the server writes protocol messages, not console text.

Unzip the example, then dotnet run.

The download starts a child server over standard input and output, lists its URI template, then reads notes://42. The note is generated in code; no input files or external services are needed. The package’s source generator supplies registration for the partial class. A URI variable that cannot parse as an int produces an invalid-params error (-32602).

The full program

C#
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Protocol;
using System.Reflection;
 
if (args.Contains("--stdio"))
{
    await ModelContext.ServeAsync(new Demo());
    return;
}
var assembly = Assembly.GetExecutingAssembly().Location;
await using var client = await ModelContext.ConnectAsync($"dotnet \"{assembly}\" --stdio");
foreach (var template in await client.ListResourceTemplatesAsync())
    Console.WriteLine($"Template: {template.UriTemplate} ({template.MimeType})");
var result = await client.ReadResourceAsync("notes://42");
foreach (var content in result.Contents)
    Console.WriteLine(((TextResourceContents)content).Text);
 
public partial class Demo
{
    [Resource("notes://{id}", MimeType = "text/markdown")]
    public string Note(int id) => $"# Note {id}";
}

dotnet run prints:

Text
Template: notes://{id} (text/markdown)
# Note 42

Variations

Read a different document. Replace the ReadResourceAsync line in the full program with:

C#
var result = await client.ReadResourceAsync("notes://7");

The content line becomes:

Text
# Note 7

The short answer

Use [Resource("notes://{id}")] on a method in a non-static partial class, then pass an instance to ModelContext.ServeAsync. Match the method parameter name to the variable in the URI template. OneModelContext generates registration and converts the URI text to the declared parameter type. Set MimeType when the returned string is not plain text.

The complete example also shows the other half: a client discovers the pattern with ListResourceTemplatesAsync and supplies a concrete URI to ReadResourceAsync. The request is for notes://42, not for the literal braces or a separate JSON argument object.

How do I serve resource URI templates in C#?

Start with the shape of the identifier. Here, notes://{id} represents a family of notes, and int id makes a numeric identifier part of the handler’s contract. The method returns the document body. It does not need to construct a protocol response just to return Markdown.

The generated registration exposes the template and its MIME type. Keep the containing class partial: that lets the package’s generator add registration code during compilation. No additional generator package is needed. The resource and prompt type rules describe text-like parameters: strings, numbers, Booleans, GUIDs, dates and times. Unsupported parameter types are build diagnostics rather than a request-time conversion strategy.

The URI is an identifier in this server. It is not an instruction to open a socket or download a web page. In the example, the custom notes scheme maps directly to the method; there is no filesystem behind it. If you replace the sample body with a database lookup, that lookup belongs in your handler.

How does a client read a dynamic resource?

Discovery and reading are separate operations. The resource specification defines resources/templates/list for patterns and resources/read for content at a concrete URI. The example prints the template first so you can see that registration worked before reading a document.

Do not expect the template listing to enumerate every possible note. Listing a pattern is useful precisely because the number of possible identifiers may be large or change over time. The variation reads another identifier without registering another method.

A string result defaults to text/plain; this method explicitly chooses text/markdown. The returned resource contents carry that media type. A client still decides how to use or display the content; returning Markdown does not guarantee that a particular host renders it as a document.

What is the difference between resources and tools?

Use a resource when the operation is to read context at an identifier. Use a tool for an operation with a callable argument schema and tool result. The protocol describes resources as application-driven context; the host determines how to incorporate them.

OneModelContext can return a string, byte[], resource contents or ReadResourceResult from a resource method. A string is text; a byte array is a blob. Those are resource responses, not tool execution results with isError. In particular, a bad numeric template argument is an invalid-params error. Keep that distinction when writing client error handling: failure to read a resource is not a successful document whose text happens to describe an error.

All onemodelcontext recipes