---
title: "Add an HTML view to a tool in C#"
url: https://recursiveai.org/onemodelcontext/cookbook/add-html-view-to-tool/
description: "Attach an HTML view to a protocol tool, read it from an Apps-capable client, and keep a text fallback for other clients."
version: "1.0.1"
---

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

[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/add-html-view-to-tool/add-html-view-to-tool.zip)

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](https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/); this is not a WinForms/WPF desktop UI.

The [pinned Apps documentation](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/docs/extensions.md) names no rendering host. OneModelContext itself renders nothing.

```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;
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](https://recursiveai.org/onemodelcontext/cookbook/add-html-view-to-tool/add-html-view-to-tool.zip), 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.

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

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

## Related recipes

- [Build a protocol server in C#](https://recursiveai.org/onemodelcontext/cookbook/build-a-server-in-csharp/)
- [Serve over HTTP without ASP.NET Core](https://recursiveai.org/onemodelcontext/cookbook/serve-over-http-without-aspnet/)

---

## 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](https://blog.modelcontextprotocol.io/posts/2026-01-26-mcp-apps/) 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](https://github.com/recursive-ai-oss/onemodelcontext-csharp/blob/d72d78c39c934697857be92dd8514efd4c415a88/docs/extensions.md) 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.
