Subchapter 143.3
references/mcp-apps.mdMarkdown10 KBView on GitHub
MCP Apps is the official extension that lets a tool return an rendered in a sandboxed iframe inside the host (Claude, Claude Desktop, VS Code Copilot, Goose, Postman, MCPJam). Typical use cases: charts, dashboards, multi-step forms, 3D viewers, real-time monitors, PDF/video viewers.
Important: SDK 2.x ships a dedicated extension package,
ModelContextProtocol.Extensions.Apps, with typed MCP Apps support: register with.WithMcpApps()and annotate tools with[McpAppUi(ResourceUri = "ui://...")]. It replaces the hand-rolled_metawiring, not theui://resource — you still register and serve the UI resource. The APIs are marked experimental (suppress diagnosticMCPEXP003); check the package page (opens in a new tab) and SDK API reference (opens in a new tab) for the current surface rather than guessing beyond those names. The manual pattern below is what you need on 1.x, which has no typed layer (was tracked in csharp-sdk#1431 (opens in a new tab)): serve aui://resource and emit the right_metaon the tool.
ui:// URI returning an HTML bundle._meta.ui.resourceUri pointing to that URI.postMessage JSON-RPC (use @modelcontextprotocol/ext-apps from the bundle, or hand-roll it).The full protocol spec is at @modelcontextprotocol/ext-apps (opens in a new tab).
Bundle your HTML/JS/CSS into a single string (or load from wwwroot). Serve it at a ui:// URI.
using System.ComponentModel;
using System.IO;
using System.Reflection;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
[McpServerResourceType]
public static class ChartUiResource
{
[McpServerResource(
UriTemplate = "ui://charts/interactive",
Name = "Interactive chart",
MimeType = "text/html;profile=mcp-app")] // see "MIME type" note below
[Description("UI bundle for the interactive chart MCP App.")]
public static TextResourceContents GetUi()
{
// Load a bundled HTML/JS file from embedded resources or wwwroot.
var html = LoadEmbeddedString("MyMcpServer.AppUi.chart.html");
return new TextResourceContents
{
Uri = "ui://charts/interactive",
MimeType = "text/html;profile=mcp-app",
Text = html
};
}
private static string LoadEmbeddedString(string resourceName)
{
var asm = Assembly.GetExecutingAssembly();
using var stream = asm.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException($"Missing embedded resource {resourceName}");
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
}MIME type note: the current Apps spec (2026-01-26) uses text/html;profile=mcp-app for app HTML so hosts can distinguish UI bundles from regular text/html previews. Earlier drafts used text/html+skybridge — treat that as legacy; some older hosts may still expect it.
The C# SDK’s [McpServerTool] doesn’t expose _meta in the attribute today, so set it via the lower-level Tool definition. Do this once at startup:
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.Text.Json;
using System.Text.Json.Nodes;
builder.Services.Configure<McpServerOptions>(options =>
{
options.Capabilities ??= new();
options.Capabilities.Tools ??= new();
// Define the tool manually so we can attach _meta.
var visualizeTool = new Tool
{
Name = "visualize_data",
Description = "Visualize the user's data as an interactive chart.",
InputSchema = JsonDocument.Parse("""
{
"type": "object",
"properties": {
"datasetId": { "type": "string", "description": "Dataset to visualize." }
},
"required": ["datasetId"]
}
""").RootElement,
Meta = new JsonObject
{
["ui"] = new JsonObject
{
["resourceUri"] = "ui://charts/interactive"
// Optionally:
// ["csp"] = new JsonObject { ["default-src"] = "'self' https://cdn.example.com" },
// ["permissions"] = new JsonArray("clipboard-write")
}
}
};
// Implement the call handler that returns the data the UI will render.
options.Capabilities.Tools.ToolCollection ??= new();
options.Capabilities.Tools.ToolCollection.Add(McpServerTool.Create(
async (CallToolRequestParams req, CancellationToken ct) =>
{
var args = req.Arguments ?? new();
var datasetId = args["datasetId"]!.GetValue<string>();
var data = await LoadDataset(datasetId, ct);
return new CallToolResult
{
Content = [new TextContentBlock { Text = JsonSerializer.Serialize(data) }],
StructuredContent = JsonSerializer.SerializeToNode(data)
};
},
visualizeTool));
});If you don’t need full structured content, the tool can return just JSON in a text block — the UI fetches it via app.callServerTool(...) after rendering.
Some older hosts expect _meta["ui/resourceUri"] instead of _meta.ui.resourceUri. Set both for safety:
Meta = new JsonObject
{
["ui"] = new JsonObject { ["resourceUri"] = "ui://charts/interactive" },
["ui/resourceUri"] = "ui://charts/interactive" // legacy
}A minimum viable bundle: vanilla JS using @modelcontextprotocol/ext-apps. The simplest build is a single self-contained HTML file.
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Chart</title>
<style>body { font-family: system-ui; margin: 0; }</style>
</head>
<body>
<div id="root">Loading…</div>
<script type="module">
import { App } from "https://esm.sh/@modelcontextprotocol/ext-apps@1.7.5";
const app = new App();
await app.connect();
// Fetch the data we need from the server.
const resp = await app.callServerTool({
name: "visualize_data",
arguments: { datasetId: "default" }
});
const data = JSON.parse(resp.content[0].text);
document.getElementById("root").textContent =
`Loaded ${data.points.length} data points.`;
// Tell the model what just happened (becomes part of its context).
await app.updateModelContext({
content: [{ type: "text", text: "User opened the chart UI." }]
});
</script>
</body>
</html>Tip: for non-trivial UIs, build with Vite (React/Vue/Svelte/Solid — any of the official starter templates (opens in a new tab)) and have the build emit a single inlined HTML you embed as a project resource.
A pragmatic layout for an MCP App in .NET:
MyMcpServer/
├── Program.cs
├── Tools/
│ └── VisualizeDataTool.cs # (or registered via Configure as above)
├── Resources/
│ └── ChartUiResource.cs # serves the ui:// resource
├── AppUi/
│ ├── chart.html # bundled UI (Embedded Resource)
│ └── package.json + src/... # if you build with Vite, output to chart.html
└── MyMcpServer.csprojIn the csproj:
<ItemGroup>
<EmbeddedResource Include="AppUi\chart.html" />
</ItemGroup>Read it via Assembly.GetManifestResourceStream("MyMcpServer.AppUi.chart.html").
For pure-UI iteration, MCP Inspector (opens in a new tab) shows resource contents but does not fully render apps; for that, point Claude Desktop at your dev server.
text/html;profile=mcp-app (current spec; text/html+skybridge is a legacy draft value). Plain text/html may still work on lenient hosts but isn’t future-proof.Meta["ui"]["csp"] on the Tool definition (this serialises to _meta.ui.csp on the wire). Otherwise the iframe sandbox blocks it.Tool.Meta on the tool. Without the Meta property containing the ui.resourceUri entry, the host treats your tool as a regular text-returning tool. The UI never appears.app.updateModelContext and tool calls for state.On 2.x, ModelContextProtocol.Extensions.Apps replaces the manual Configure block: .WithMcpApps() plus [McpAppUi(ResourceUri = "ui://...")] on the tool, with the extension handling the Apps capability negotiation. You still serve the ui:// resource (with the current text/html;profile=mcp-app MIME type) and keep your HTML bundle — keep the UI HTML as embedded resources so the migration is mechanical. The APIs are experimental (MCPEXP003); consult the package docs for anything beyond this surface rather than inventing it.