---
title: "Serve resource URI templates in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/serve-resource-templates/
description: "Serve resource URI templates in C# with OneModelContext. Run a complete .NET server and client example with no external service."
version: "1.0.1"
---

# 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.

[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/serve-resource-templates/serve-resource-templates.zip)

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

```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;
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](https://recursiveai.org/onemodelcontext/cookbook/serve-resource-templates/serve-resource-templates.zip), 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

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

```csharp
var result = await client.ReadResourceAsync("notes://7");
```

The content line becomes:

```text
# Note 7
```

## Related recipes

- [Serve prompt templates in C#](https://recursiveai.org/onemodelcontext/cookbook/serve-prompt-templates/)
- [Report server tool progress in C#](https://recursiveai.org/onemodelcontext/cookbook/report-tool-progress/)
- [Cancel a long-running server tool call in C#](https://recursiveai.org/onemodelcontext/cookbook/cancel-long-running-tools/)

---

## 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](https://recursiveai.org/onemodelcontext/docs/tool-schemas/#resources-and-prompts) 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](https://modelcontextprotocol.io/specification/2025-06-18/server/resources#resource-templates) 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](https://modelcontextprotocol.io/specification/2025-06-18/server/resources#user-interaction-model); 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.
