mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-01 23:12:29 +00:00
aa01464ccb
* 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.
221 lines
9.8 KiB
Markdown
221 lines
9.8 KiB
Markdown
# MCP Apps (interactive UI)
|
|
|
|
[MCP Apps](https://modelcontextprotocol.io/extensions/apps/overview) 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 `_meta` wiring, **not** the `ui://` resource — you still register and serve the UI resource. The APIs are marked experimental (suppress diagnostic `MCPEXP003`); check the [package page](https://www.nuget.org/packages/ModelContextProtocol.Extensions.Apps) and [SDK API reference](https://csharp.sdk.modelcontextprotocol.io/api/ModelContextProtocol.html) 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](https://github.com/modelcontextprotocol/csharp-sdk/issues/1431)): serve a `ui://` resource and emit the right `_meta` on the tool.
|
|
|
|
## How it works (short version)
|
|
|
|
1. You register a **resource** at a `ui://` URI returning an HTML bundle.
|
|
2. You register a **tool** whose definition includes `_meta.ui.resourceUri` pointing to that URI.
|
|
3. When the LLM calls the tool, the host fetches the UI resource and renders it in a sandboxed iframe in the chat.
|
|
4. The HTML talks to the host over `postMessage` JSON-RPC (use `@modelcontextprotocol/ext-apps` from the bundle, or hand-roll it).
|
|
5. 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`](https://github.com/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.
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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:
|
|
|
|
```csharp
|
|
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.
|
|
|
|
```html
|
|
<!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](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples)) 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:
|
|
|
|
```xml
|
|
<ItemGroup>
|
|
<EmbeddedResource Include="AppUi\chart.html" />
|
|
</ItemGroup>
|
|
```
|
|
|
|
Read it via `Assembly.GetManifestResourceStream("MyMcpServer.AppUi.chart.html")`.
|
|
|
|
## Testing locally
|
|
|
|
1. Run your MCP server (STDIO or HTTP).
|
|
2. Use a host that supports MCP Apps — Claude Desktop or VS Code Copilot Chat are the easiest.
|
|
3. Trigger the tool via the LLM. The UI renders inline.
|
|
|
|
For pure-UI iteration, [MCP Inspector](https://github.com/modelcontextprotocol/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+skybridge` is a legacy draft value). Plain `text/html` may 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 the `Tool` definition (this serialises to `_meta.ui.csp` on the wire). Otherwise the iframe sandbox blocks it.
|
|
- **Forgetting `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.
|
|
- **Trying to use browser APIs outside the sandbox.** No cookies, no localStorage from the parent. Use `app.updateModelContext` and 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.
|