Recursive AI

Add an HTML view to a tool 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 tool is an operation an AI client calls over the Model Context Protocol®. The Apps extension (io.modelcontextprotocol/ui) lets a tool carry an HTML page that a supporting client shows in a sandboxed iframe; this is not a WinForms/WPF desktop UI.

The pinned Apps documentation names no rendering host. OneModelContext itself renders nothing.

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.Apps;
using RecursiveAI.OneModelContext
    .Protocol;
using System.Text.Json;
 
var server = new ContextServer(
    "Views", "1.0");
server.UseApps();
server.AddUiResource(
    "ui://demo/hello", "hello_view",
    "<html><body><h1>Hello</h1></body></html>");
using var schema = JsonDocument.Parse(
    """{"type":"object"}""");
server.AddAppTool(
    new ToolDefinition(
        "hello", "Say hello", schema),
    "ui://demo/hello", (_, _) =>
        ValueTask.FromResult(
        CallToolResult.FromText("Hello")));

The snippet only registers the view and its tool; the zip is the runnable version. No view opens until a supporting host renders it.

Unzip the example, then dotnet run.

The full program

The program connects an in-memory client that advertises Apps support. It reads the tool’s UI resource and calls the tool; it does not open a browser. The HTML input is the literal shown above.

C#
using RecursiveAI.OneModelContext;
using RecursiveAI.OneModelContext.Protocol;
using System.Text.Json;
using RecursiveAI.OneModelContext.Extensions.Apps;
 
var server = new ContextServer("Views", "1.0");
server.UseApps();
server.AddUiResource("ui://demo/hello", "hello_view",
    "<html><body><h1>Hello</h1></body></html>");
using var schema = JsonDocument.Parse("""{"type":"object"}""");
server.AddAppTool(
    new ToolDefinition("hello", "Say hello", schema),
    "ui://demo/hello", (_, _) => ValueTask.FromResult(
        CallToolResult.FromText("Hello")));
 
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 = [AppsExtension.Client()] }))
{
    var tool = (await client.ListToolsAsync()).Single();
    Console.WriteLine(tool.GetUi()!.ResourceUri);
    var view = await client.ReadUiResourceAsync("ui://demo/hello");
    Console.WriteLine(view.Html);
    Console.WriteLine((await client.CallToolAsync("hello", new { })).Text);
}
await serving;

dotnet run prints:

Text
ui://demo/hello
<html><body><h1>Hello</h1></body></html>
Hello

Clients without Apps support receive the text result without UI metadata; app-only tools are hidden from those clients.

Variations

To request a border, replace the AddUiResource call with:

C#
server.AddUiResource("ui://demo/hello", "hello_view",
    "<html><body><h1>Hello</h1></body></html>",
    new UiResourceMeta { PrefersBorder = true });

The resource URI, HTML and tool output remain the same; rendering the border is the host’s job.


The short answer

Register a ui:// resource with AddUiResource, then associate the tool with that URI using AddAppTool. Enable Apps with UseApps. Your server supplies the HTML and the tool’s UI metadata; it does not create a window, run a browser or render a desktop interface.

The example deliberately uses static HTML. This makes the server-side wiring visible without adding a JavaScript build step. It is an HTML view attached to a callable operation, not a complete interactive dashboard. The tool also returns “Hello” as ordinary text, so the useful answer does not depend on rendering.

How do I add an HTML user interface to a tool in C#?

There are two registrations, with different names and purposes. The resource’s URI identifies the HTML document. The tool’s name identifies the operation a caller invokes. AddAppTool connects them; keep its URI identical to the one passed to AddUiResource.

A client can list tools, inspect tool.GetUi() and read the document with ReadUiResourceAsync. The downloaded program performs all three steps. Reading the HTML successfully confirms delivery, not rendering. To display it, use a host that implements Apps.

The Apps announcement describes how a host fetches a UI resource and renders it in a sandboxed iframe. That is the missing rendering component when a plain protocol client only prints the HTML. Neither an arbitrary HTML string in the tool’s text result nor a normal web URL replaces the ui:// registration in this example.

How do Apps clients get the HTML view?

Apps support is negotiated through the extension introduced above. OneModelContext recognizes support when the peer’s extension settings include text/html;profile=mcp-app in mimeTypes. The program uses AppsExtension.Client() to declare those settings rather than constructing them manually.

For an unsupported peer, the library removes _meta.ui from the tool listing. App-only tools are hidden and cannot be called by that peer. The default example is not app-only: its handler returns a text result either way. A real application can use context.SupportsApps() to choose a shorter text answer when the view carries the detail, while preserving a complete fallback for other callers.

The pinned Apps documentation also describes visibility: ["model", "app"] and the typed UI message helpers. Do not infer from those helpers that the server runs the UI. They model messages that a host can bridge.

Can an Apps HTML view load external resources?

UiContentSecurityPolicy builds CSP and permission metadata. Its secure default allows no external origins. The static sample needs none, so it adds no network permissions and ships no external scripts, fonts or images.

For a view with external dependencies, declare the origins it actually needs through the documented metadata rather than assuming a browser will fetch them. A host still controls how it renders the resource. Border preference is another piece of metadata, not an instruction that forces a specific visual result. Keep the tool’s text useful even when the host does not render the view.

All onemodelcontext recipes