-
Notifications
You must be signed in to change notification settings - Fork 709
Add MCP Apps extension support (typed metadata, attribute, and helpers) #1484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bb32d0e
35e7e59
fccaa0a
5673bd7
7753c53
7f54b8c
b14455c
5d38a5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| --- | ||
| title: MCP Apps | ||
| author: mikekistler | ||
| description: How to use the MCP Apps extension to deliver interactive UIs from MCP servers. | ||
| uid: apps | ||
| --- | ||
|
|
||
| # MCP Apps | ||
|
|
||
| [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. | ||
|
|
||
| [MCP Apps]: https://modelcontextprotocol.io/specification/draft/extensions/apps | ||
|
|
||
| > [!IMPORTANT] | ||
| > MCP Apps support is experimental. All types are marked with `[Experimental("MCPEXP003")]` and require suppressing that diagnostic to use. | ||
|
|
||
| ## Installation | ||
|
|
||
| MCP Apps is provided in the `ModelContextProtocol.Extensions.Apps` package, which layers on top of the core SDK: | ||
|
|
||
| ```shell | ||
| dotnet add package ModelContextProtocol.Extensions.Apps | ||
| ``` | ||
|
|
||
| ## Overview | ||
|
|
||
| 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. | ||
|
|
||
| The key concepts are: | ||
|
|
||
| - **UI capability negotiation** — Client and server declare support via `extensions["io.modelcontextprotocol/ui"]` | ||
| - **UI resources** — HTML content served with the MIME type `text/html;profile=mcp-app` | ||
| - **Tool UI metadata** — Tools declare their associated UI resource in `_meta.ui` | ||
|
|
||
| ## Associating tools with UI resources | ||
|
|
||
| ### Using the builder extension (recommended) | ||
|
|
||
| The simplest approach is to apply `[McpAppUi]` attributes to your tool methods and call `WithMcpApps()` on the server builder: | ||
|
|
||
| ```csharp | ||
| [McpServerToolType] | ||
| public class WeatherTools | ||
| { | ||
| [McpServerTool, Description("Get current weather for a location")] | ||
| [McpAppUi(ResourceUri = "ui://weather/view.html")] | ||
| public static string GetWeather(string location) => $"Weather for {location}"; | ||
|
|
||
| [McpServerTool, Description("Get forecast (model-only tool)")] | ||
| [McpAppUi(ResourceUri = "ui://weather/forecast.html", Visibility = [McpUiToolVisibility.Model])] | ||
| public static string GetForecast(string location) => $"Forecast for {location}"; | ||
| } | ||
| ``` | ||
|
|
||
| ```csharp | ||
| builder.Services.AddMcpServer() | ||
| .WithTools<WeatherTools>() | ||
| .WithMcpApps(); | ||
| ``` | ||
|
|
||
| The `WithMcpApps()` call registers a post-configuration step that processes all registered tools and applies `[McpAppUi]` attribute metadata to their `_meta.ui` field automatically. | ||
|
|
||
| ### Using the attribute with manual processing | ||
|
|
||
| If you create tools manually (without `WithMcpApps()`), you can still use the attribute and process tools explicitly: | ||
|
|
||
| ```csharp | ||
| var tools = new[] | ||
| { | ||
| McpServerTool.Create(typeof(WeatherTools).GetMethod(nameof(WeatherTools.GetWeather))!), | ||
| McpServerTool.Create(typeof(WeatherTools).GetMethod(nameof(WeatherTools.GetForecast))!), | ||
| }; | ||
|
|
||
| McpApps.ApplyAppUiAttributes(tools); | ||
| ``` | ||
|
|
||
| ### Using the programmatic API | ||
|
|
||
| For full control, use `McpApps.SetAppUi` to set UI metadata directly: | ||
|
|
||
| ```csharp | ||
| var tool = McpServerTool.Create((string location) => $"Weather for {location}"); | ||
|
|
||
| McpApps.SetAppUi(tool, new McpUiToolMeta | ||
| { | ||
| ResourceUri = "ui://weather/view.html", | ||
| Visibility = [McpUiToolVisibility.Model, McpUiToolVisibility.App], | ||
| }); | ||
| ``` | ||
|
|
||
| ## Checking client capabilities | ||
|
|
||
| During a session, you can check whether the connected client supports MCP Apps: | ||
|
|
||
| ```csharp | ||
| [McpServerTool, Description("Get weather")] | ||
| [McpAppUi(ResourceUri = "ui://weather/view.html")] | ||
| public static string GetWeather(McpServer server, string location) | ||
| { | ||
| var uiCapability = McpApps.GetUiCapability(server.ClientCapabilities); | ||
| if (uiCapability is not null) | ||
| { | ||
| // Client supports MCP Apps — the UI will be displayed | ||
| } | ||
|
|
||
| return $"Weather for {location}"; | ||
| } | ||
| ``` | ||
|
|
||
| ## Tool visibility | ||
|
|
||
| The `Visibility` property controls which principals can invoke the tool: | ||
|
|
||
| | Value | Meaning | | ||
| | - | - | | ||
| | `McpUiToolVisibility.Model` | Only the LLM can call this tool | | ||
| | `McpUiToolVisibility.App` | Only the app UI can call this tool | | ||
| | Both (or null/empty) | Both the model and app can call the tool (default) | | ||
|
|
||
| ## UI resources | ||
|
|
||
| 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: | ||
|
|
||
| - **CSP (Content Security Policy)** — Controls allowed origins for network requests and resource loads | ||
| - **Permissions** — Sandbox permissions (scripts, forms, popups, etc.) | ||
| - **Domain** — Dedicated origin for OAuth flows and CORS | ||
| - **PrefersBorder** — Whether the host should render a visual border | ||
|
|
||
| ## Constants | ||
|
|
||
| The <xref:ModelContextProtocol.Server.McpApps> class provides constants for protocol values: | ||
|
|
||
| | Constant | Value | Usage | | ||
| | - | - | - | | ||
| | `McpApps.ResourceMimeType` | `text/html;profile=mcp-app` | MIME type for UI resources | | ||
| | `McpApps.ExtensionId` | `io.modelcontextprotocol/ui` | Key in `extensions` capability dictionary | | ||
|
|
||
| ## Serialization | ||
|
|
||
| MCP Apps types use source-generated JSON serialization for Native AOT compatibility. Use `McpApps.SerializerOptions` when serializing extension types: | ||
|
|
||
| ```csharp | ||
| var json = JsonSerializer.Serialize(toolMeta, McpApps.SerializerOptions); | ||
| var deserialized = JsonSerializer.Deserialize<McpUiToolMeta>(json, McpApps.SerializerOptions); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| using Microsoft.Extensions.DependencyInjection; | ||
| using ModelContextProtocol.AspNetCore; | ||
| using ModelContextProtocol.Protocol; | ||
| using ModelContextProtocol.Server; | ||
| using System.Net.Http.Headers; | ||
|
|
||
| var builder = WebApplication.CreateBuilder(args); | ||
|
|
||
| builder.Services.AddSingleton(_ => | ||
| { | ||
| var client = new HttpClient { BaseAddress = new Uri("https://api.weather.gov") }; | ||
| client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("WeatherAppServer", "1.0")); | ||
| return client; | ||
| }); | ||
|
|
||
| builder.Services | ||
| .AddMcpServer(options => | ||
| { | ||
| options.ServerInfo = new Implementation { Name = "weather-app-server", Version = "1.0.0" }; | ||
| options.Capabilities = new ServerCapabilities | ||
| { | ||
| Tools = new ToolsCapability(), | ||
| Resources = new ResourcesCapability(), | ||
| }; | ||
| }) | ||
| .WithHttpTransport() | ||
| .WithTools<WeatherTools>() | ||
| .WithResources<WeatherResources>() | ||
| .WithMcpApps(); | ||
|
|
||
| var app = builder.Build(); | ||
| app.MapMcp("/mcp"); | ||
| app.Run(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Weather App Server | ||
|
|
||
| An MCP server that demonstrates the **MCP Apps** extension by serving an interactive weather forecast UI alongside weather tools. | ||
|
|
||
| ## What it shows | ||
|
|
||
| - **`[McpAppUi]` attribute** — declaratively associates a UI resource with a tool | ||
| - **`WithMcpApps()`** — builder extension that processes `[McpAppUi]` attributes | ||
| - **UI resource** — an HTML page served via `McpServerResource` with MIME type `text/html;profile=mcp-app` | ||
| - **Structured content** — tool results include `StructuredContent` for the UI to render | ||
|
|
||
| ## Running | ||
|
|
||
| ```bash | ||
| dotnet run | ||
| ``` | ||
|
|
||
| The server starts on `http://localhost:5000` by default. Connect any MCP Apps-capable client to the `/mcp` endpoint. | ||
|
|
||
| Then prompt that will cause the LLM to request the use of the "weather_ui" tool. | ||
| A general prompt like "What's the weather?" will probably work, but if not you could try explicitly requesting the tool | ||
| with something like "@weather_ui". This should load the Weather App UI in an iFrame that you can then interact with | ||
| to get the weather forecast for a number of US cities. | ||
|
|
||
|  | ||
|
|
||
| ## Tools | ||
|
|
||
| | Tool | Description | | ||
| |------|-------------| | ||
| | `weather_ui` | Opens the weather forecast UI | | ||
| | `weather_forecast` | Gets a multi-period forecast from the National Weather Service for a US city | | ||
|
|
||
| Both tools are linked to the `ui://weather-app/forecast` resource via the `[McpAppUi]` attribute. | ||
|
|
||
| ## Resources | ||
|
|
||
| | URI | Description | | ||
| |-----|-------------| | ||
| | `ui://weather-app/forecast` | Interactive weather forecast HTML UI | | ||
| | `data://weather-app/us-cities` | JSON list of supported US cities | |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| using System.Text.Json.Serialization; | ||
|
|
||
| [JsonConverter(typeof(JsonStringEnumConverter<UsCity>))] | ||
| public enum UsCity | ||
| { | ||
| [JsonStringEnumMemberName("Albuquerque, NM")] AlbuquerqueNM, | ||
| [JsonStringEnumMemberName("Atlanta, GA")] AtlantaGA, | ||
| [JsonStringEnumMemberName("Austin, TX")] AustinTX, | ||
| [JsonStringEnumMemberName("Boston, MA")] BostonMA, | ||
| [JsonStringEnumMemberName("Charlotte, NC")] CharlotteNC, | ||
| [JsonStringEnumMemberName("Chicago, IL")] ChicagoIL, | ||
| [JsonStringEnumMemberName("Dallas, TX")] DallasTX, | ||
| [JsonStringEnumMemberName("Denver, CO")] DenverCO, | ||
| [JsonStringEnumMemberName("Houston, TX")] HoustonTX, | ||
| [JsonStringEnumMemberName("Indianapolis, IN")] IndianapolisIN, | ||
| [JsonStringEnumMemberName("Las Vegas, NV")] LasVegasNV, | ||
| [JsonStringEnumMemberName("Los Angeles, CA")] LosAngelesCA, | ||
| [JsonStringEnumMemberName("Miami, FL")] MiamiFL, | ||
| [JsonStringEnumMemberName("Minneapolis, MN")] MinneapolisMN, | ||
| [JsonStringEnumMemberName("Nashville, TN")] NashvilleTN, | ||
| [JsonStringEnumMemberName("New York, NY")] NewYorkNY, | ||
| [JsonStringEnumMemberName("Orlando, FL")] OrlandoFL, | ||
| [JsonStringEnumMemberName("Philadelphia, PA")] PhiladelphiaPA, | ||
| [JsonStringEnumMemberName("Phoenix, AZ")] PhoenixAZ, | ||
| [JsonStringEnumMemberName("Portland, OR")] PortlandOR, | ||
| [JsonStringEnumMemberName("Salt Lake City, UT")] SaltLakeCityUT, | ||
| [JsonStringEnumMemberName("San Diego, CA")] SanDiegoCA, | ||
| [JsonStringEnumMemberName("San Francisco, CA")] SanFranciscoCA, | ||
| [JsonStringEnumMemberName("Seattle, WA")] SeattleWA, | ||
| [JsonStringEnumMemberName("Washington, DC")] WashingtonDC, | ||
| } | ||
|
|
||
| public static class UsCityData | ||
| { | ||
| public static (double Latitude, double Longitude) GetCoordinates(UsCity city) => city switch | ||
| { | ||
| UsCity.AlbuquerqueNM => (35.0844, -106.6504), | ||
| UsCity.AtlantaGA => (33.7490, -84.3880), | ||
| UsCity.AustinTX => (30.2672, -97.7431), | ||
| UsCity.BostonMA => (42.3601, -71.0589), | ||
| UsCity.CharlotteNC => (35.2271, -80.8431), | ||
| UsCity.ChicagoIL => (41.8781, -87.6298), | ||
| UsCity.DallasTX => (32.7767, -96.7970), | ||
| UsCity.DenverCO => (39.7392, -104.9903), | ||
| UsCity.HoustonTX => (29.7604, -95.3698), | ||
| UsCity.IndianapolisIN => (39.7684, -86.1581), | ||
| UsCity.LasVegasNV => (36.1699, -115.1398), | ||
| UsCity.LosAngelesCA => (34.0522, -118.2437), | ||
| UsCity.MiamiFL => (25.7617, -80.1918), | ||
| UsCity.MinneapolisMN => (44.9778, -93.2650), | ||
| UsCity.NashvilleTN => (36.1627, -86.7816), | ||
| UsCity.NewYorkNY => (40.7128, -74.0060), | ||
| UsCity.OrlandoFL => (28.5383, -81.3792), | ||
| UsCity.PhiladelphiaPA => (39.9526, -75.1652), | ||
| UsCity.PhoenixAZ => (33.4484, -112.0740), | ||
| UsCity.PortlandOR => (45.5152, -122.6784), | ||
| UsCity.SaltLakeCityUT => (40.7608, -111.8910), | ||
| UsCity.SanDiegoCA => (32.7157, -117.1611), | ||
| UsCity.SanFranciscoCA => (37.7749, -122.4194), | ||
| UsCity.SeattleWA => (47.6062, -122.3321), | ||
| UsCity.WashingtonDC => (38.9072, -77.0369), | ||
| _ => throw new ArgumentOutOfRangeException(nameof(city)) | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <!-- Suppress experimental MCP Apps warning --> | ||
| <NoWarn>$(NoWarn);MCPEXP003</NoWarn> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\ModelContextProtocol.AspNetCore\ModelContextProtocol.AspNetCore.csproj" /> | ||
| <ProjectReference Include="..\..\src\ModelContextProtocol.Extensions.Apps\ModelContextProtocol.Extensions.Apps.csproj" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <Content Include="ui\*.html" CopyToOutputDirectory="PreserveNewest" /> | ||
| </ItemGroup> | ||
|
|
||
| </Project> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| using ModelContextProtocol.Server; | ||
| using System.ComponentModel; | ||
| using System.Text.Json; | ||
| using System.Text.Json.Serialization; | ||
|
|
||
| [McpServerResourceType] | ||
| public sealed class WeatherResources | ||
| { | ||
| private static readonly string UiDir = Path.Combine(AppContext.BaseDirectory, "ui"); | ||
|
|
||
| [McpServerResource(UriTemplate = "ui://weather-app/forecast", Name = "weather-forecast-ui", MimeType = McpApps.ResourceMimeType)] | ||
| [Description("Interactive weather forecast UI with city picker")] | ||
| public static string GetWeatherForecastUi() => File.ReadAllText(Path.Combine(UiDir, "weather-forecast.html")); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sample correctly sets the MCP Apps MIME type, but it never demonstrates |
||
|
|
||
| [McpServerResource(UriTemplate = "data://weather-app/us-cities", Name = "us-cities", MimeType = "application/json")] | ||
| [Description("List of supported US cities for weather forecasts")] | ||
| public static string GetUsCities() | ||
| { | ||
| var options = new JsonSerializerOptions { Converters = { new JsonStringEnumConverter<UsCity>() } }; | ||
| var cities = Enum.GetValues<UsCity>().Select(c => JsonSerializer.Serialize(c, options).Trim('"')).Order().ToList(); | ||
| return JsonSerializer.Serialize(cities); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few topics from the spec that would be worth covering here for parity with the TS/community SDK docs:
["app"]visibility pattern (tools the LLM never sees, only the iframe can call) is one of the most powerful patterns in MCP Apps and isn't called out as a use case.inline,fullscreen,pip) — not mentioned anywhere, and there are no types for them.--color-background-primaryetc.) that hosts pass to apps. Worth at least linking to.CallToolResultwhenGetUiCapabilityreturnsnull, so server authors know the recommended fallback pattern.vite-plugin-singlefile) because the default CSP makes external assets painful. The sample uses inline HTML so this Just Works, but real apps will hit this.