* Update dotnet-mcp-builder skill to ModelContextProtocol 2.x Align the skill with the C# SDK 2.0.0 release and the MCP 2026-07-28 spec: stable line is now 2.x, HttpServerTransportOptions.Stateless defaults to true, roots/sampling/MCP-channel logging are [Obsolete] (MCP9005) with the multi-round-trip input_required pattern as the replacement, discovery-first negotiation (server/discover) supersedes the initialize handshake, Mcp-Method/Mcp-Name routable headers, raw structuredContent for non-object results, required Tool.inputSchema, and the new ModelContextProtocol.Extensions.Tasks and ModelContextProtocol.Extensions.Apps packages (typed MCP Apps support replacing the hand-rolled _meta/ui:// pattern on 1.x). * Address Copilot review: Apps extension accuracy, header scope, capability ownership - packages.md: the Apps package replaces the manual _meta wiring, not the ui:// resource; note the experimental MCPEXP003 diagnostic; label the 1.x -> 2.0 list as highlights and add the OAuth/SSE runtime changes with a pointer to the full release notes. - transport-http.md: Mcp-Method is on every POST, Mcp-Name only on named invocations (tools/call, prompts/get, resources/read) - do not require it globally at gateways. - mcp-apps.md: current MIME type is text/html;profile=mcp-app (skybridge is a legacy draft value); document [McpAppUi] + WithMcpApps(). - server-features.md: roots/sampling are client capabilities, only logging sits on ServerCapabilities. * Correct stateful HTTP guidance: 2026-07-28 has no HTTP sessions Per the official SDK v2 elicitation docs, a server with Stateless=false refuses the 2026-07-28 revision so dual-path clients fall back to an initialize-capable revision; ElicitAsync cannot be used on 2026-07-28 Streamable HTTP at all. Reframe stateful HTTP as down-level compatibility mode and document the multi-round-trip pattern (InputRequiredException / InputRequest.ForElicitation, retry with InputResponses -> ElicitResult) as the current-protocol way to ask mid-tool, across SKILL.md, transport-http.md, and elicitation.md.
9.8 KiB
MCP Apps (interactive UI)
MCP Apps is the official extension that lets a tool return an interactive UI 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 and SDK API reference 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): serve aui://resource and emit the right_metaon the tool.
How it works (short version)
- You register a resource at a
ui://URI returning an HTML bundle. - You register a tool whose definition includes
_meta.ui.resourceUripointing to that URI. - When the LLM calls the tool, the host fetches the UI resource and renders it in a sandboxed iframe in the chat.
- The HTML talks to the host over
postMessageJSON-RPC (use@modelcontextprotocol/ext-appsfrom the bundle, or hand-roll it). - The app can call back into your MCP server (any tool), update the model context, etc.
The full protocol spec is at @modelcontextprotocol/ext-apps.
Step 1: Serve the UI resource
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.
Step 2: Emit _meta on the tool
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.
Backwards compatibility key
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
}
Step 3: The HTML bundle
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";
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) and have the build emit a single inlined HTML you embed as a project resource.
Project layout
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.csproj
In the csproj:
<ItemGroup>
<EmbeddedResource Include="AppUi\chart.html" />
</ItemGroup>
Read it via Assembly.GetManifestResourceStream("MyMcpServer.AppUi.chart.html").
Testing locally
- Run your MCP server (STDIO or HTTP).
- Use a host that supports MCP Apps — Claude Desktop or VS Code Copilot Chat are the easiest.
- Trigger the tool via the LLM. The UI renders inline.
For pure-UI iteration, MCP Inspector shows resource contents but does not fully render apps; for that, point Claude Desktop at your dev server.
Pitfalls
- Wrong MIME type. Use
text/html;profile=mcp-app(current spec;text/html+skybridgeis a legacy draft value). Plaintext/htmlmay still work on lenient hosts but isn't future-proof. - CSP too tight or too loose. If your UI loads from a CDN, declare it in
Meta["ui"]["csp"]on theTooldefinition (this serialises to_meta.ui.cspon the wire). Otherwise the iframe sandbox blocks it. - Forgetting
Tool.Metaon the tool. Without theMetaproperty containing theui.resourceUrientry, the host treats your tool as a regular text-returning tool. The UI never appears. - Trying to use browser APIs outside the sandbox. No cookies, no localStorage from the parent. Use
app.updateModelContextand tool calls for state.
Migrating from the manual pattern
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.