Skip to content

Commit f93df7e

Browse files
authored
Add MCP Apps extension support (typed metadata, attribute, and helpers) (modelcontextprotocol#1484)
1 parent de75c7d commit f93df7e

27 files changed

Lines changed: 1896 additions & 1 deletion

ModelContextProtocol.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
<Project Path="samples/QuickstartWeatherServer/QuickstartWeatherServer.csproj" />
5252
<Project Path="samples/TasksExtension/TasksExtension.csproj" />
5353
<Project Path="samples/TestServerWithHosting/TestServerWithHosting.csproj" />
54+
<Project Path="samples/WeatherAppServer/WeatherAppServer.csproj" />
5455
</Folder>
5556
<Folder Name="/Solution Items/">
5657
<File Path="Directory.Build.props" />
@@ -67,6 +68,7 @@
6768
<Project Path="src/ModelContextProtocol.Analyzers/ModelContextProtocol.Analyzers.csproj" />
6869
<Project Path="src/ModelContextProtocol.AspNetCore/ModelContextProtocol.AspNetCore.csproj" />
6970
<Project Path="src/ModelContextProtocol.Core/ModelContextProtocol.Core.csproj" />
71+
<Project Path="src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj" />
7072
<Project Path="src/ModelContextProtocol/ModelContextProtocol.csproj" />
7173
</Folder>
7274
<Folder Name="/tests/">

docs/concepts/apps/apps.md

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
title: MCP Apps
3+
author: mikekistler
4+
description: How to use the MCP Apps extension to deliver interactive UIs from MCP servers.
5+
uid: apps
6+
---
7+
8+
# MCP Apps
9+
10+
[MCP Apps] is an extension to the Model Context Protocol that enables MCP servers to deliver interactive user interfaces — dashboards, forms, visualizations, and more — directly inside conversational AI clients.
11+
12+
[MCP Apps]: https://modelcontextprotocol.io/extensions/apps/overview
13+
14+
> [!IMPORTANT]
15+
> MCP Apps support is experimental. All types are marked with `[Experimental("MCPEXP003")]` and require suppressing that diagnostic to use.
16+
17+
## Installation
18+
19+
MCP Apps is provided in the `ModelContextProtocol.Extensions.Apps` package, which layers on top of the core SDK:
20+
21+
```shell
22+
dotnet add package ModelContextProtocol.Extensions.Apps
23+
```
24+
25+
## Overview
26+
27+
The MCP Apps extension introduces the concept of **UI resources** — HTML pages served by the MCP server that a client can display alongside the conversation. Tools can be associated with a UI resource so the client knows which interface to show when a tool is called.
28+
29+
The key concepts are:
30+
31+
- **UI capability negotiation** — Client and server declare support via `extensions["io.modelcontextprotocol/ui"]`
32+
- **UI resources** — HTML content served with the MIME type `text/html;profile=mcp-app`
33+
- **Tool UI metadata** — Tools declare their associated UI resource in `_meta.ui`
34+
35+
## Associating tools with UI resources
36+
37+
### Using the builder extension (recommended)
38+
39+
The simplest approach is to apply `[McpAppUi]` attributes to your tool methods and call `WithMcpApps()` on the server builder:
40+
41+
```csharp
42+
[McpServerToolType]
43+
public class WeatherTools
44+
{
45+
[McpServerTool, Description("Get current weather for a location")]
46+
[McpAppUi(ResourceUri = "ui://weather/view.html")]
47+
public static string GetWeather(string location) => $"Weather for {location}";
48+
49+
[McpServerTool, Description("Get forecast (model-only tool)")]
50+
[McpAppUi(ResourceUri = "ui://weather/forecast.html", Visibility = [McpUiToolVisibility.Model])]
51+
public static string GetForecast(string location) => $"Forecast for {location}";
52+
}
53+
```
54+
55+
```csharp
56+
builder.Services.AddMcpServer()
57+
.WithTools<WeatherTools>()
58+
.WithMcpApps();
59+
```
60+
61+
The `WithMcpApps()` call registers a post-configuration step that processes all registered tools and applies `[McpAppUi]` attribute metadata to their `_meta.ui` field automatically.
62+
63+
### Using the attribute with manual processing
64+
65+
If you create tools manually (without `WithMcpApps()`), you can still use the attribute and process tools explicitly:
66+
67+
```csharp
68+
var tools = new[]
69+
{
70+
McpServerTool.Create(typeof(WeatherTools).GetMethod(nameof(WeatherTools.GetWeather))!),
71+
McpServerTool.Create(typeof(WeatherTools).GetMethod(nameof(WeatherTools.GetForecast))!),
72+
};
73+
74+
McpApps.ApplyAppUiAttributes(tools);
75+
```
76+
77+
### Using the programmatic API
78+
79+
For full control, use `McpApps.SetAppUi` to set UI metadata directly:
80+
81+
```csharp
82+
var tool = McpServerTool.Create((string location) => $"Weather for {location}");
83+
84+
McpApps.SetAppUi(tool, new McpUiToolMeta
85+
{
86+
ResourceUri = "ui://weather/view.html",
87+
Visibility = [McpUiToolVisibility.Model, McpUiToolVisibility.App],
88+
});
89+
```
90+
91+
## Checking client capabilities
92+
93+
During a session, you can check whether the connected client supports MCP Apps:
94+
95+
```csharp
96+
[McpServerTool, Description("Get weather")]
97+
[McpAppUi(ResourceUri = "ui://weather/view.html")]
98+
public static string GetWeather(McpServer server, string location)
99+
{
100+
var uiCapability = McpApps.GetUiCapability(server.ClientCapabilities);
101+
if (uiCapability is not null)
102+
{
103+
// Client supports MCP Apps — the UI will be displayed
104+
}
105+
106+
return $"Weather for {location}";
107+
}
108+
```
109+
110+
## Tool visibility
111+
112+
The `Visibility` property controls which principals can invoke the tool:
113+
114+
| Value | Meaning |
115+
| - | - |
116+
| `McpUiToolVisibility.Model` | Only the LLM can call this tool |
117+
| `McpUiToolVisibility.App` | Only the app UI can call this tool |
118+
| Both (or null/empty) | Both the model and app can call the tool (default) |
119+
120+
## UI resources
121+
122+
UI resources are HTML pages registered with the MCP server using the `ui://` URI scheme and the `text/html;profile=mcp-app` MIME type. The `McpUiResourceMeta` type provides metadata for these resources, including:
123+
124+
- **CSP (Content Security Policy)** — Controls allowed origins for network requests and resource loads
125+
- **Permissions** — Sandbox permissions (scripts, forms, popups, etc.)
126+
- **Domain** — Dedicated origin for OAuth flows and CORS
127+
- **PrefersBorder** — Whether the host should render a visual border
128+
129+
## App-only tools
130+
131+
Tools with `Visibility = [McpUiToolVisibility.App]` are not visible to the LLM — they are intended only for use by the app UI.
132+
This is useful for tools that serve UI interaction (button handlers, form submissions) without cluttering the model's tool list:
133+
134+
```csharp
135+
[McpServerTool, Description("Submit the weather form")]
136+
[McpAppUi(ResourceUri = "ui://weather/view.html", Visibility = [McpUiToolVisibility.App])]
137+
public static string SubmitWeatherForm(string city) => GetWeatherHtml(city);
138+
```
139+
140+
## Graceful degradation
141+
142+
Not all clients support MCP Apps. Use `GetUiCapability` to detect support and return text-only content as a fallback:
143+
144+
```csharp
145+
[McpServerTool, Description("Get weather")]
146+
[McpAppUi(ResourceUri = "ui://weather/view.html")]
147+
public static string GetWeather(McpServer server, string location)
148+
{
149+
var uiCapability = McpApps.GetUiCapability(server.ClientCapabilities);
150+
if (uiCapability is null)
151+
{
152+
// Client doesn't support MCP Apps — return plain text
153+
return $"Current weather for {location}: 72°F, sunny";
154+
}
155+
156+
// Client supports MCP Apps — the UI resource will be displayed
157+
return $"Weather data for {location} loaded into UI";
158+
}
159+
```
160+
161+
## Display modes
162+
163+
The MCP Apps spec defines display modes (`inline`, `fullscreen`, `pip`) that control how the host renders the UI. Display mode is negotiated between the client and server during capability exchange and is not set per-tool — it depends on the host implementation.
164+
165+
## Host theming
166+
167+
Hosts pass standardized CSS custom properties (e.g., `--color-background-primary`, `--color-text-primary`) to app iframes. Your HTML can reference these variables to automatically match the host's theme without any server-side configuration.
168+
169+
See the [MCP Apps specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) for the full list of CSS variables.
170+
171+
## Single-file HTML bundling
172+
173+
The default Content Security Policy restricts external script and style loads. For production apps, bundle all JavaScript and CSS into a single HTML file using tools like [vite-plugin-singlefile](https://github.com/richardtallent/vite-plugin-singlefile). For simple apps, inline `<script>` and `<style>` tags work directly.
174+
175+
## Constants
176+
177+
The <xref:ModelContextProtocol.Extensions.Apps.McpApps> class provides constants for protocol values:
178+
179+
| Constant | Value | Usage |
180+
| - | - | - |
181+
| `McpApps.HtmlMimeType` | `text/html;profile=mcp-app` | MIME type for UI resources |
182+
| `McpApps.ExtensionId` | `io.modelcontextprotocol/ui` | Key in `extensions` capability dictionary |
183+
184+
## Serialization
185+
186+
MCP Apps types use source-generated JSON serialization for Native AOT compatibility. Use `McpApps.SerializerOptions` when serializing extension types:
187+
188+
```csharp
189+
var json = JsonSerializer.Serialize(toolMeta, McpApps.SerializerOptions);
190+
var deserialized = JsonSerializer.Deserialize<McpUiToolMeta>(json, McpApps.SerializerOptions);
191+
```

docs/concepts/index.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,10 @@ Install the SDK and build your first MCP client and server.
4141
| [Stateless and Stateful](stateless/stateless.md) | Learn when to use stateless vs. stateful mode for HTTP servers and how to configure sessions. |
4242
| [HTTP Context](httpcontext/httpcontext.md) | Learn how to access the underlying `HttpContext` for a request. |
4343
| [MCP Server Handler Filters](filters.md) | Learn how to add filters to the handler pipeline. Filters let you wrap the original handler with additional functionality. |
44+
45+
### Extensions
46+
47+
| Title | Description |
48+
| - | - |
49+
| [MCP Apps](apps/apps.md) | Learn how to use the MCP Apps extension to deliver interactive UIs from MCP servers. |
4450
| [Identity and Roles](identity/identity.md) | Learn how to access caller identity and roles in MCP tool, prompt, and resource handlers. |

docs/concepts/toc.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,9 @@ items:
4747
uid: httpcontext
4848
- name: Filters
4949
uid: filters
50+
- name: Extensions
51+
items:
52+
- name: MCP Apps
53+
uid: apps
5054
- name: Identity and Roles
51-
uid: identity
55+
uid: identity

docs/list-of-diagnostics.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
2525
| :------------ | :---------- |
2626
| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks and Extensions. Tasks provide a mechanism for asynchronous long-running operations that can be polled for status and results (see [SEP-2663](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2663-tasks-extension.md)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). |
2727
| `MCPEXP002` | Experimental SDK APIs unrelated to the MCP specification itself, including subclassing `McpClient`/`McpServer` (see [#1363](https://github.com/modelcontextprotocol/csharp-sdk/pull/1363)) and `RunSessionHandler`, which may be removed or change signatures in a future release (consider using `ConfigureSessionOptions` instead). |
28+
| `MCPEXP003` | Experimental MCP Apps extension APIs. MCP Apps is the first official MCP extension (`io.modelcontextprotocol/ui`), enabling servers to deliver interactive UIs inside AI clients (see [MCP Apps specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx)). |
2829

2930
## Obsolete APIs
3031

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModelContextProtocol.AspNetCore;
3+
using ModelContextProtocol.Extensions.Apps;
4+
using ModelContextProtocol.Protocol;
5+
using ModelContextProtocol.Server;
6+
using System.Net.Http.Headers;
7+
8+
var builder = WebApplication.CreateBuilder(args);
9+
10+
builder.Services.AddSingleton(_ =>
11+
{
12+
var client = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") };
13+
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("WeatherAppServer", "1.0"));
14+
return client;
15+
});
16+
17+
builder.Services
18+
.AddMcpServer(options =>
19+
{
20+
options.ServerInfo = new Implementation { Name = "weather-app-server", Version = "1.0.0" };
21+
options.Capabilities = new ServerCapabilities
22+
{
23+
Tools = new ToolsCapability(),
24+
Resources = new ResourcesCapability(),
25+
};
26+
})
27+
.WithHttpTransport()
28+
.WithTools<WeatherTools>()
29+
.WithResources<WeatherResources>()
30+
.WithMcpApps();
31+
32+
var app = builder.Build();
33+
app.MapMcp("/mcp");
34+
app.Run();

samples/WeatherAppServer/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Weather App Server
2+
3+
An MCP server that demonstrates the **MCP Apps** extension by serving an interactive weather forecast UI alongside weather tools.
4+
5+
## What it shows
6+
7+
- **`[McpAppUi]` attribute** — declaratively associates a UI resource with a tool
8+
- **`WithMcpApps()`** — builder extension that processes `[McpAppUi]` attributes
9+
- **UI resource** — an HTML page served via `McpServerResource` with MIME type `text/html;profile=mcp-app`
10+
- **Structured content** — tool results include `StructuredContent` for the UI to render
11+
12+
## Running
13+
14+
```bash
15+
dotnet run
16+
```
17+
18+
The server starts on `http://localhost:5000` by default. Connect any MCP Apps-capable client to the `/mcp` endpoint.
19+
20+
Then prompt that will cause the LLM to request the use of the "weather_ui" tool.
21+
A general prompt like "What's the weather?" will probably work, but if not you could try explicitly requesting the tool
22+
with something like "@weather_ui". This should load the Weather App UI in an iFrame that you can then interact with
23+
to get the weather forecast for a number of US cities.
24+
25+
![UI of the Weather Forecast MCP App in VS Code](./WeatherUI.png)
26+
27+
## Tools
28+
29+
| Tool | Description |
30+
|------|-------------|
31+
| `weather_ui` | Opens the weather forecast UI |
32+
| `weather_forecast` | Gets a multi-period forecast from the National Weather Service for a US city |
33+
34+
Both tools are linked to the `ui://weather-app/forecast` resource via the `[McpAppUi]` attribute.
35+
36+
## Resources
37+
38+
| URI | Description |
39+
|-----|-------------|
40+
| `ui://weather-app/forecast` | Interactive weather forecast HTML UI |
41+
| `data://weather-app/us-cities` | JSON list of supported US cities |

samples/WeatherAppServer/UsCity.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using System.Text.Json.Serialization;
2+
3+
[JsonConverter(typeof(JsonStringEnumConverter<UsCity>))]
4+
public enum UsCity
5+
{
6+
[JsonStringEnumMemberName("Albuquerque, NM")] AlbuquerqueNM,
7+
[JsonStringEnumMemberName("Atlanta, GA")] AtlantaGA,
8+
[JsonStringEnumMemberName("Austin, TX")] AustinTX,
9+
[JsonStringEnumMemberName("Boston, MA")] BostonMA,
10+
[JsonStringEnumMemberName("Charlotte, NC")] CharlotteNC,
11+
[JsonStringEnumMemberName("Chicago, IL")] ChicagoIL,
12+
[JsonStringEnumMemberName("Dallas, TX")] DallasTX,
13+
[JsonStringEnumMemberName("Denver, CO")] DenverCO,
14+
[JsonStringEnumMemberName("Houston, TX")] HoustonTX,
15+
[JsonStringEnumMemberName("Indianapolis, IN")] IndianapolisIN,
16+
[JsonStringEnumMemberName("Las Vegas, NV")] LasVegasNV,
17+
[JsonStringEnumMemberName("Los Angeles, CA")] LosAngelesCA,
18+
[JsonStringEnumMemberName("Miami, FL")] MiamiFL,
19+
[JsonStringEnumMemberName("Minneapolis, MN")] MinneapolisMN,
20+
[JsonStringEnumMemberName("Nashville, TN")] NashvilleTN,
21+
[JsonStringEnumMemberName("New York, NY")] NewYorkNY,
22+
[JsonStringEnumMemberName("Orlando, FL")] OrlandoFL,
23+
[JsonStringEnumMemberName("Philadelphia, PA")] PhiladelphiaPA,
24+
[JsonStringEnumMemberName("Phoenix, AZ")] PhoenixAZ,
25+
[JsonStringEnumMemberName("Portland, OR")] PortlandOR,
26+
[JsonStringEnumMemberName("Salt Lake City, UT")] SaltLakeCityUT,
27+
[JsonStringEnumMemberName("San Diego, CA")] SanDiegoCA,
28+
[JsonStringEnumMemberName("San Francisco, CA")] SanFranciscoCA,
29+
[JsonStringEnumMemberName("Seattle, WA")] SeattleWA,
30+
[JsonStringEnumMemberName("Washington, DC")] WashingtonDC,
31+
}
32+
33+
public static class UsCityData
34+
{
35+
public static (double Latitude, double Longitude) GetCoordinates(UsCity city) => city switch
36+
{
37+
UsCity.AlbuquerqueNM => (35.0844, -106.6504),
38+
UsCity.AtlantaGA => (33.7490, -84.3880),
39+
UsCity.AustinTX => (30.2672, -97.7431),
40+
UsCity.BostonMA => (42.3601, -71.0589),
41+
UsCity.CharlotteNC => (35.2271, -80.8431),
42+
UsCity.ChicagoIL => (41.8781, -87.6298),
43+
UsCity.DallasTX => (32.7767, -96.7970),
44+
UsCity.DenverCO => (39.7392, -104.9903),
45+
UsCity.HoustonTX => (29.7604, -95.3698),
46+
UsCity.IndianapolisIN => (39.7684, -86.1581),
47+
UsCity.LasVegasNV => (36.1699, -115.1398),
48+
UsCity.LosAngelesCA => (34.0522, -118.2437),
49+
UsCity.MiamiFL => (25.7617, -80.1918),
50+
UsCity.MinneapolisMN => (44.9778, -93.2650),
51+
UsCity.NashvilleTN => (36.1627, -86.7816),
52+
UsCity.NewYorkNY => (40.7128, -74.0060),
53+
UsCity.OrlandoFL => (28.5383, -81.3792),
54+
UsCity.PhiladelphiaPA => (39.9526, -75.1652),
55+
UsCity.PhoenixAZ => (33.4484, -112.0740),
56+
UsCity.PortlandOR => (45.5152, -122.6784),
57+
UsCity.SaltLakeCityUT => (40.7608, -111.8910),
58+
UsCity.SanDiegoCA => (32.7157, -117.1611),
59+
UsCity.SanFranciscoCA => (37.7749, -122.4194),
60+
UsCity.SeattleWA => (47.6062, -122.3321),
61+
UsCity.WashingtonDC => (38.9072, -77.0369),
62+
_ => throw new ArgumentOutOfRangeException(nameof(city))
63+
};
64+
}

0 commit comments

Comments
 (0)