diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx
index 1090c5377..ffc845fc8 100644
--- a/ModelContextProtocol.slnx
+++ b/ModelContextProtocol.slnx
@@ -66,6 +66,7 @@
+
diff --git a/docs/concepts/apps/apps.md b/docs/concepts/apps/apps.md
new file mode 100644
index 000000000..3a12e3632
--- /dev/null
+++ b/docs/concepts/apps/apps.md
@@ -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()
+ .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 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(json, McpApps.SerializerOptions);
+```
diff --git a/docs/concepts/index.md b/docs/concepts/index.md
index 6393d9997..d0b2b9df5 100644
--- a/docs/concepts/index.md
+++ b/docs/concepts/index.md
@@ -40,4 +40,10 @@ Install the SDK and build your first MCP client and server.
| [Stateless and Stateful](stateless/stateless.md) | Learn when to use stateless vs. stateful mode for HTTP servers and how to configure sessions. |
| [HTTP Context](httpcontext/httpcontext.md) | Learn how to access the underlying `HttpContext` for a request. |
| [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. |
+
+### Extensions
+
+| Title | Description |
+| - | - |
+| [MCP Apps](apps/apps.md) | Learn how to use the MCP Apps extension to deliver interactive UIs from MCP servers. |
| [Identity and Roles](identity/identity.md) | Learn how to access caller identity and roles in MCP tool, prompt, and resource handlers. |
diff --git a/docs/concepts/toc.yml b/docs/concepts/toc.yml
index bd5474338..3d0989dbb 100644
--- a/docs/concepts/toc.yml
+++ b/docs/concepts/toc.yml
@@ -45,5 +45,9 @@ items:
uid: httpcontext
- name: Filters
uid: filters
+- name: Extensions
+ items:
+ - name: MCP Apps
+ uid: apps
- name: Identity and Roles
- uid: identity
\ No newline at end of file
+ uid: identity
diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md
index 515472817..572b2c183 100644
--- a/docs/list-of-diagnostics.md
+++ b/docs/list-of-diagnostics.md
@@ -25,6 +25,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
| :------------ | :---------- |
| `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 [MCP Tasks specification](https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks)). Extensions provide a framework for extending the Model Context Protocol while maintaining interoperability (see [SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)). |
| `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). |
+| `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)). |
## Obsolete APIs
diff --git a/src/Common/Experimentals.cs b/src/Common/Experimentals.cs
index 7e7e969bb..83f517bec 100644
--- a/src/Common/Experimentals.cs
+++ b/src/Common/Experimentals.cs
@@ -71,6 +71,26 @@ internal static class Experimentals
///
public const string Extensions_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
+ ///
+ /// Diagnostic ID for 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. This uses a dedicated diagnostic ID
+ /// so that users can suppress it independently from other experimental APIs.
+ ///
+ public const string Apps_DiagnosticId = "MCPEXP003";
+
+ ///
+ /// Message for the experimental MCP Apps extension APIs.
+ ///
+ public const string Apps_Message = "The MCP Apps extension is experimental and subject to change as the specification evolves.";
+
+ ///
+ /// URL for the experimental MCP Apps extension APIs.
+ ///
+ public const string Apps_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp003";
+
///
/// Diagnostic ID for experimental SDK APIs unrelated to the MCP specification,
/// such as subclassing McpClient/McpServer or referencing RunSessionHandler.
diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
index 700d9d26d..68a99fb73 100644
--- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
+++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
@@ -159,7 +159,7 @@ options.OpenWorld is not null ||
// Auto-detect async methods and mark with taskSupport = "optional" unless explicitly configured.
// This enables implicit task support for async tools: clients can choose to invoke them
// synchronously (wait for completion) or as a task (receive taskId, poll for result).
- if (function.UnderlyingMethod is not null &&
+ if (function.UnderlyingMethod is not null &&
IsAsyncMethod(function.UnderlyingMethod) &&
tool.Execution?.TaskSupport is null)
{
diff --git a/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj b/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj
new file mode 100644
index 000000000..ccdce456c
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj
@@ -0,0 +1,44 @@
+
+
+
+ net10.0;net9.0;net8.0;netstandard2.0
+ true
+ true
+ ModelContextProtocol.Extensions.Apps
+ MCP Apps extension for the .NET Model Context Protocol (MCP) SDK
+ README.md
+
+ $(NoWarn);MCPEXP003
+
+ false
+
+
+
+ true
+
+
+
+
+ $(NoWarn);CS0436
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpAppUiAttribute.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpAppUiAttribute.cs
new file mode 100644
index 000000000..2bbe5a059
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpAppUiAttribute.cs
@@ -0,0 +1,57 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Specifies MCP Apps UI metadata for a tool method.
+///
+///
+///
+/// Apply this attribute alongside to associate an MCP Apps
+/// UI resource with the tool. When processed by
+/// or , it populates the
+/// structured _meta.ui object in the tool's metadata.
+///
+///
+/// This attribute takes precedence over any raw [McpMeta("ui", ...)] attribute on the
+/// same method, but explicit Meta["ui"] set via
+/// takes precedence over this attribute.
+///
+///
+///
+///
+/// [McpServerTool]
+/// [McpAppUi(ResourceUri = "ui://weather/view.html")]
+/// [Description("Get current weather for a location")]
+/// public string GetWeather(string location) => ...;
+///
+/// // Restrict visibility to model only:
+/// [McpServerTool]
+/// [McpAppUi(ResourceUri = "ui://weather/view.html", Visibility = [McpUiToolVisibility.Model])]
+/// public string GetWeatherModelOnly(string location) => ...;
+///
+///
+[AttributeUsage(AttributeTargets.Method)]
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public sealed class McpAppUiAttribute : Attribute
+{
+ ///
+ /// Gets or sets the URI of the UI resource associated with this tool.
+ ///
+ ///
+ /// This should be a ui:// URI pointing to the HTML resource registered
+ /// with the server (e.g., "ui://weather/view.html").
+ ///
+ public string? ResourceUri { get; set; }
+
+ ///
+ /// Gets or sets the visibility of the tool, controlling which principals can invoke it.
+ ///
+ ///
+ ///
+ /// Allowed values are and .
+ /// When or empty, the tool is visible to both the model and the app (the default).
+ ///
+ ///
+ public string[]? Visibility { get; set; }
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpApps.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpApps.cs
new file mode 100644
index 000000000..82c3920c1
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpApps.cs
@@ -0,0 +1,207 @@
+using ModelContextProtocol.Protocol;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Provides constants and helper methods for building MCP Apps-enabled servers.
+///
+///
+///
+/// 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.
+///
+///
+/// Use the constants in this class when populating the extensions capability and the
+/// _meta field of tools and resources. Use to check whether
+/// the connected client supports the MCP Apps extension.
+///
+///
+/// Use to set the _meta.ui metadata on a tool, or
+/// to automatically process
+/// instances on tools created from methods.
+///
+///
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public static class McpApps
+{
+ ///
+ /// The MIME type used for MCP App HTML resources.
+ ///
+ ///
+ /// This MIME type should be used when registering UI resources with
+ /// text/html;profile=mcp-app to indicate they are MCP App resources.
+ ///
+ public const string ResourceMimeType = "text/html;profile=mcp-app";
+
+ ///
+ /// The extension identifier used for MCP Apps capability negotiation.
+ ///
+ ///
+ /// This key is used in the and
+ /// dictionaries to advertise support for
+ /// the MCP Apps extension.
+ ///
+ public const string ExtensionId = "io.modelcontextprotocol/ui";
+
+ ///
+ /// Gets the configured with source-generated metadata
+ /// for MCP Apps extension types.
+ ///
+ ///
+ /// Use these options when serializing or deserializing MCP Apps types such as
+ /// , , and .
+ ///
+ public static JsonSerializerOptions SerializerOptions { get; } = CreateSerializerOptions();
+
+ private static JsonSerializerOptions CreateSerializerOptions()
+ {
+ var options = new JsonSerializerOptions(McpJsonUtilities.DefaultOptions);
+ options.TypeInfoResolverChain.Insert(0, McpAppsJsonContext.Default);
+ options.MakeReadOnly();
+ return options;
+ }
+
+ ///
+ /// Gets the MCP Apps client capability, if advertised by the connected client.
+ ///
+ /// The client capabilities received during the MCP initialize handshake.
+ ///
+ /// A instance if the client advertises support for the MCP Apps extension;
+ /// otherwise, .
+ ///
+ ///
+ /// Use this method to determine whether the connected client supports the MCP Apps extension
+ /// and to read the client's supported MIME types.
+ ///
+ public static McpUiClientCapabilities? GetUiCapability(ClientCapabilities? capabilities)
+ {
+ if (capabilities?.Extensions is not { } extensions ||
+ !extensions.TryGetValue(ExtensionId, out var value))
+ {
+ return null;
+ }
+
+ if (value is JsonElement element)
+ {
+ return element.ValueKind == JsonValueKind.Null ? null :
+ JsonSerializer.Deserialize(element, McpAppsJsonContext.Default.McpUiClientCapabilities);
+ }
+
+ return null;
+ }
+
+ ///
+ /// Sets the MCP Apps UI metadata on a tool's property.
+ ///
+ /// The tool to set the UI metadata on.
+ /// The UI metadata to apply.
+ /// The same instance, for chaining.
+ ///
+ ///
+ /// This method sets the ui key in the tool's object.
+ /// If a ui key is already present in , it is not overwritten.
+ ///
+ ///
+ /// or is .
+ public static McpServerTool SetAppUi(McpServerTool tool, McpUiToolMeta appUi)
+ {
+#if NET
+ ArgumentNullException.ThrowIfNull(tool);
+ ArgumentNullException.ThrowIfNull(appUi);
+#else
+ if (tool is null) throw new ArgumentNullException(nameof(tool));
+ if (appUi is null) throw new ArgumentNullException(nameof(appUi));
+#endif
+
+ var protocolTool = tool.ProtocolTool;
+ protocolTool.Meta ??= new JsonObject();
+
+ if (!protocolTool.Meta.ContainsKey("ui"))
+ {
+ var uiNode = JsonSerializer.SerializeToNode(appUi, McpAppsJsonContext.Default.McpUiToolMeta);
+ if (uiNode is not null)
+ {
+ protocolTool.Meta["ui"] = uiNode;
+ }
+ }
+
+ return tool;
+ }
+
+ ///
+ /// Processes a collection of tools, applying metadata to any
+ /// tool whose underlying method has the attribute.
+ ///
+ /// The tools to process.
+ /// The same enumerable, for chaining.
+ ///
+ ///
+ /// For each tool that has a in its ,
+ /// this method sets the ui key in the tool's if not already present.
+ ///
+ ///
+ /// If already contains a ui key (e.g., set explicitly via
+ /// ), the attribute is not applied.
+ ///
+ ///
+ /// is .
+ public static IEnumerable ApplyAppUiAttributes(IEnumerable tools)
+ {
+#if NET
+ ArgumentNullException.ThrowIfNull(tools);
+#else
+ if (tools is null) throw new ArgumentNullException(nameof(tools));
+#endif
+
+ foreach (var tool in tools)
+ {
+ ApplyAppUiAttributes(tool);
+ }
+
+ return tools;
+ }
+
+ ///
+ /// Processes a single tool, applying metadata if the tool's
+ /// underlying method has the attribute.
+ ///
+ /// The tool to process.
+ /// The same instance, for chaining.
+ ///
+ ///
+ /// If the tool has a in its ,
+ /// this method sets the ui key in the tool's if not already present.
+ ///
+ ///
+ /// is .
+ public static McpServerTool ApplyAppUiAttributes(McpServerTool tool)
+ {
+#if NET
+ ArgumentNullException.ThrowIfNull(tool);
+#else
+ if (tool is null) throw new ArgumentNullException(nameof(tool));
+#endif
+
+ // Look for McpAppUiAttribute in tool metadata (attributes from the method)
+ foreach (var metadataItem in tool.Metadata)
+ {
+ if (metadataItem is McpAppUiAttribute appUiAttr)
+ {
+ var meta = new McpUiToolMeta
+ {
+ ResourceUri = appUiAttr.ResourceUri,
+ Visibility = appUiAttr.Visibility,
+ };
+
+ SetAppUi(tool, meta);
+ break;
+ }
+ }
+
+ return tool;
+ }
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpAppsBuilderExtensions.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpAppsBuilderExtensions.cs
new file mode 100644
index 000000000..d9332651e
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpAppsBuilderExtensions.cs
@@ -0,0 +1,61 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using System.Diagnostics.CodeAnalysis;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Extension methods for to enable MCP Apps support.
+///
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public static class McpAppsBuilderExtensions
+{
+ ///
+ /// Enables MCP Apps support by automatically processing on registered tools.
+ ///
+ /// The server builder.
+ /// The builder provided in .
+ ///
+ ///
+ /// Call this method after registering tools (e.g., after WithTools<T>()) to automatically
+ /// apply metadata to the tool's _meta.ui field.
+ ///
+ ///
+ /// Tools that already have a ui key in their (e.g., set explicitly
+ /// via ) are not modified.
+ ///
+ ///
+ ///
+ ///
+ /// builder.Services
+ /// .AddMcpServer()
+ /// .WithTools<MyToolType>()
+ /// .WithMcpApps();
+ ///
+ ///
+ public static IMcpServerBuilder WithMcpApps(this IMcpServerBuilder builder)
+ {
+#if NET
+ ArgumentNullException.ThrowIfNull(builder);
+#else
+ if (builder is null) throw new ArgumentNullException(nameof(builder));
+#endif
+
+ builder.Services.AddSingleton, McpAppsPostConfigureOptions>();
+ return builder;
+ }
+
+ private sealed class McpAppsPostConfigureOptions : IPostConfigureOptions
+ {
+ public void PostConfigure(string? name, McpServerOptions options)
+ {
+ if (options.ToolCollection is { IsEmpty: false } tools)
+ {
+ foreach (var tool in tools)
+ {
+ McpApps.ApplyAppUiAttributes(tool);
+ }
+ }
+ }
+ }
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpAppsJsonContext.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpAppsJsonContext.cs
new file mode 100644
index 000000000..23e9abd58
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpAppsJsonContext.cs
@@ -0,0 +1,19 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Provides source-generated JSON serialization metadata for MCP Apps extension types.
+///
+[JsonSourceGenerationOptions(
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
+[JsonSerializable(typeof(McpUiToolMeta))]
+[JsonSerializable(typeof(McpUiClientCapabilities))]
+[JsonSerializable(typeof(McpUiResourceMeta))]
+[JsonSerializable(typeof(McpUiResourceCsp))]
+[JsonSerializable(typeof(McpUiResourcePermissions))]
+internal sealed partial class McpAppsJsonContext : JsonSerializerContext
+{
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpUiClientCapabilities.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiClientCapabilities.cs
new file mode 100644
index 000000000..55e4df3ac
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiClientCapabilities.cs
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Represents the MCP Apps capabilities advertised by a client.
+///
+///
+///
+/// This object is the value associated with the "io.modelcontextprotocol/ui" key in the
+/// dictionary.
+///
+///
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public sealed class McpUiClientCapabilities
+{
+ ///
+ /// Gets or sets the list of MIME types supported by the client for MCP App UI resources.
+ ///
+ ///
+ /// A client that supports MCP Apps must include "text/html;profile=mcp-app" in this list.
+ ///
+ [JsonPropertyName("mimeTypes")]
+ public IList? MimeTypes { get; set; }
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceCsp.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceCsp.cs
new file mode 100644
index 000000000..60064863f
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceCsp.cs
@@ -0,0 +1,49 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Represents the Content Security Policy (CSP) domain allowlists for an MCP Apps UI resource.
+///
+///
+///
+/// These allowlists are used by the MCP host to construct the Content-Security-Policy HTTP header
+/// for the sandboxed iframe that hosts the UI resource.
+///
+///
+/// Each list contains origins (e.g., "https://api.example.com") that are permitted for
+/// the corresponding CSP directive.
+///
+///
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public sealed class McpUiResourceCsp
+{
+ ///
+ /// Gets or sets the list of origins allowed for fetch, XMLHttpRequest, WebSocket, and EventSource
+ /// connections (connect-src CSP directive).
+ ///
+ [JsonPropertyName("connectDomains")]
+ public IList? ConnectDomains { get; set; }
+
+ ///
+ /// Gets or sets the list of origins allowed for loading scripts, stylesheets, images, and fonts
+ /// (script-src, style-src, img-src, font-src CSP directives).
+ ///
+ [JsonPropertyName("resourceDomains")]
+ public IList? ResourceDomains { get; set; }
+
+ ///
+ /// Gets or sets the list of origins allowed for loading nested frames
+ /// (frame-src CSP directive).
+ ///
+ [JsonPropertyName("frameDomains")]
+ public IList? FrameDomains { get; set; }
+
+ ///
+ /// Gets or sets the list of allowed base URIs
+ /// (base-uri CSP directive).
+ ///
+ [JsonPropertyName("baseUris")]
+ public IList? BaseUris { get; set; }
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceMeta.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceMeta.cs
new file mode 100644
index 000000000..c8f3b3ed6
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceMeta.cs
@@ -0,0 +1,50 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Represents the UI metadata associated with an MCP resource in the MCP Apps extension.
+///
+///
+/// This metadata is placed under the ui key in the resource's _meta object.
+/// It provides Content Security Policy (CSP) configuration, sandbox permissions, CORS origin, and
+/// visual boundary preferences for the UI resource served by this MCP server.
+///
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public sealed class McpUiResourceMeta
+{
+ ///
+ /// Gets or sets the Content Security Policy configuration for this resource.
+ ///
+ ///
+ /// Specifies the allowed origins for network requests, resource loads, and nested frames.
+ ///
+ [JsonPropertyName("csp")]
+ public McpUiResourceCsp? Csp { get; set; }
+
+ ///
+ /// Gets or sets the sandbox permissions for this resource.
+ ///
+ ///
+ /// Controls which browser sandbox features the UI resource is allowed to use.
+ ///
+ [JsonPropertyName("permissions")]
+ public McpUiResourcePermissions? Permissions { get; set; }
+
+ ///
+ /// Gets or sets the dedicated origin domain for this resource.
+ ///
+ ///
+ /// When set, the host will serve the resource from this dedicated origin,
+ /// enabling OAuth flows and CORS without wildcard exceptions.
+ ///
+ [JsonPropertyName("domain")]
+ public string? Domain { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the host should render a visual border around the UI.
+ ///
+ [JsonPropertyName("prefersBorder")]
+ public bool? PrefersBorder { get; set; }
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourcePermissions.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourcePermissions.cs
new file mode 100644
index 000000000..f8ff58041
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourcePermissions.cs
@@ -0,0 +1,27 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Represents the sandbox permissions requested by an MCP Apps UI resource.
+///
+///
+/// This maps to the allow attribute on the iframe sandbox in the MCP host.
+/// Permissions are specified as standard browser iframe permission strings,
+/// such as "camera", "microphone", or "geolocation".
+///
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public sealed class McpUiResourcePermissions
+{
+ ///
+ /// Gets or sets the list of permissions granted to the sandboxed UI resource.
+ ///
+ ///
+ /// These correspond to values allowed in the allow attribute of an HTML iframe,
+ /// for example "camera", "microphone", "geolocation",
+ /// "clipboard-read", or "clipboard-write".
+ ///
+ [JsonPropertyName("allow")]
+ public IList? Allow { get; set; }
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolMeta.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolMeta.cs
new file mode 100644
index 000000000..464061086
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolMeta.cs
@@ -0,0 +1,41 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json.Serialization;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Represents the UI metadata associated with an MCP tool in the MCP Apps extension.
+///
+///
+///
+/// This metadata is placed under the ui key in the tool's _meta object.
+/// It associates the tool with a UI resource (identified by a ui:// URI) and optionally
+/// controls which principals (model, app) can call the tool.
+///
+///
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public sealed class McpUiToolMeta
+{
+ ///
+ /// Gets or sets the URI of the UI resource associated with this tool.
+ ///
+ ///
+ /// This should be a ui:// URI pointing to the HTML resource registered
+ /// with the server (e.g., "ui://weather/view.html").
+ ///
+ [JsonPropertyName("resourceUri")]
+ public string? ResourceUri { get; set; }
+
+ ///
+ /// Gets or sets the visibility of the tool, controlling which principals can invoke it.
+ ///
+ ///
+ ///
+ /// Allowed values are ("model") and
+ /// ("app"). When
+ /// or empty, the tool is visible to both the model and the app (the default).
+ ///
+ ///
+ [JsonPropertyName("visibility")]
+ public IList? Visibility { get; set; }
+}
diff --git a/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolVisibility.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolVisibility.cs
new file mode 100644
index 000000000..df84c5467
--- /dev/null
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolVisibility.cs
@@ -0,0 +1,25 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace ModelContextProtocol.Server;
+
+///
+/// Provides well-known visibility values for .
+///
+///
+/// Use these constants to specify which principals can invoke a tool in the MCP Apps extension.
+/// When is or empty, the tool
+/// is visible to both the model and the app by default.
+///
+[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+public static class McpUiToolVisibility
+{
+ ///
+ /// Indicates that the tool can be invoked by the AI model.
+ ///
+ public const string Model = "model";
+
+ ///
+ /// Indicates that the tool can be invoked by the UI app (iframe).
+ ///
+ public const string App = "app";
+}
diff --git a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
index 0985f4cd7..4dc3088b7 100644
--- a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
+++ b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
@@ -85,6 +85,7 @@
+
diff --git a/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
new file mode 100644
index 000000000..7df8f0365
--- /dev/null
+++ b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
@@ -0,0 +1,416 @@
+#pragma warning disable MCPEXP003
+
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using ModelContextProtocol.Protocol;
+using ModelContextProtocol.Server;
+using System.ComponentModel;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+
+namespace ModelContextProtocol.Tests.Server;
+
+///
+/// Tests for MCP Apps extension support: McpApps constants, typed metadata models,
+/// McpAppUiAttribute, SetAppUi, and ApplyAppUiAttributes.
+///
+public class McpAppsTests
+{
+ #region F1: Constants
+
+ [Fact]
+ public void McpApps_Constants_HaveExpectedValues()
+ {
+ Assert.Equal("text/html;profile=mcp-app", McpApps.ResourceMimeType);
+ Assert.Equal("io.modelcontextprotocol/ui", McpApps.ExtensionId);
+ }
+
+ [Fact]
+ public void McpUiToolVisibility_Constants_HaveExpectedValues()
+ {
+ Assert.Equal("model", McpUiToolVisibility.Model);
+ Assert.Equal("app", McpUiToolVisibility.App);
+ }
+
+ #endregion
+
+ #region F2: Typed Metadata Models
+
+ [Fact]
+ public void McpUiToolMeta_DefaultsToNull()
+ {
+ var meta = new McpUiToolMeta();
+ Assert.Null(meta.ResourceUri);
+ Assert.Null(meta.Visibility);
+ }
+
+ [Fact]
+ public void McpUiToolMeta_CanBeRoundtrippedAsJson()
+ {
+ var meta = new McpUiToolMeta
+ {
+ ResourceUri = "ui://weather/view.html",
+ Visibility = [McpUiToolVisibility.Model, McpUiToolVisibility.App],
+ };
+
+ var json = JsonSerializer.Serialize(meta, McpApps.SerializerOptions);
+ var deserialized = JsonSerializer.Deserialize(json, McpApps.SerializerOptions);
+
+ Assert.NotNull(deserialized);
+ Assert.Equal("ui://weather/view.html", deserialized.ResourceUri);
+ Assert.Equal(["model", "app"], deserialized.Visibility);
+ }
+
+ [Fact]
+ public void McpUiToolMeta_OmitsNullProperties()
+ {
+ var meta = new McpUiToolMeta { ResourceUri = "ui://app" };
+ var json = JsonSerializer.Serialize(meta, McpApps.SerializerOptions);
+ var doc = JsonDocument.Parse(json);
+
+ Assert.True(doc.RootElement.TryGetProperty("resourceUri", out _));
+ Assert.False(doc.RootElement.TryGetProperty("visibility", out _));
+ }
+
+ [Fact]
+ public void McpUiResourceMeta_CanBeRoundtrippedAsJson()
+ {
+ var meta = new McpUiResourceMeta
+ {
+ Domain = "https://app.example.com",
+ PrefersBorder = true,
+ Csp = new McpUiResourceCsp
+ {
+ ConnectDomains = ["https://api.example.com"],
+ ResourceDomains = ["https://cdn.example.com"],
+ FrameDomains = ["https://embed.example.com"],
+ BaseUris = ["https://app.example.com"],
+ },
+ Permissions = new McpUiResourcePermissions
+ {
+ Allow = ["camera", "microphone"],
+ },
+ };
+
+ var json = JsonSerializer.Serialize(meta, McpApps.SerializerOptions);
+ var deserialized = JsonSerializer.Deserialize(json, McpApps.SerializerOptions);
+
+ Assert.NotNull(deserialized);
+ Assert.Equal("https://app.example.com", deserialized.Domain);
+ Assert.True(deserialized.PrefersBorder);
+ Assert.NotNull(deserialized.Csp);
+ Assert.Equal(["https://api.example.com"], deserialized.Csp.ConnectDomains);
+ Assert.Equal(["https://cdn.example.com"], deserialized.Csp.ResourceDomains);
+ Assert.Equal(["https://embed.example.com"], deserialized.Csp.FrameDomains);
+ Assert.Equal(["https://app.example.com"], deserialized.Csp.BaseUris);
+ Assert.NotNull(deserialized.Permissions);
+ Assert.Equal(["camera", "microphone"], deserialized.Permissions.Allow);
+ }
+
+ [Fact]
+ public void McpUiClientCapabilities_CanBeRoundtrippedAsJson()
+ {
+ var caps = new McpUiClientCapabilities
+ {
+ MimeTypes = [McpApps.ResourceMimeType],
+ };
+
+ var json = JsonSerializer.Serialize(caps, McpApps.SerializerOptions);
+ var deserialized = JsonSerializer.Deserialize(json, McpApps.SerializerOptions);
+
+ Assert.NotNull(deserialized);
+ Assert.Equal([McpApps.ResourceMimeType], deserialized.MimeTypes);
+ }
+
+ #endregion
+
+ #region F3: GetUiCapability
+
+ [Fact]
+ public void GetUiCapability_ReturnsNull_WhenCapabilitiesIsNull()
+ {
+ Assert.Null(McpApps.GetUiCapability(null));
+ }
+
+ [Fact]
+ public void GetUiCapability_ReturnsNull_WhenExtensionsIsNull()
+ {
+ var caps = new ClientCapabilities();
+ Assert.Null(McpApps.GetUiCapability(caps));
+ }
+
+ [Fact]
+ public void GetUiCapability_ReturnsNull_WhenExtensionKeyIsMissing()
+ {
+#pragma warning disable MCPEXP001
+ var caps = new ClientCapabilities
+ {
+ Extensions = new Dictionary
+ {
+ ["other.extension"] = new { },
+ }
+ };
+#pragma warning restore MCPEXP001
+ Assert.Null(McpApps.GetUiCapability(caps));
+ }
+
+ [Fact]
+ public void GetUiCapability_ReturnsCapabilities_WhenExtensionIsPresent()
+ {
+ // Simulate what the SDK does when deserializing ClientCapabilities from JSON:
+ // extensions values come in as JsonElement.
+ var json = $$"""
+ {
+ "extensions": {
+ "{{McpApps.ExtensionId}}": {
+ "mimeTypes": ["{{McpApps.ResourceMimeType}}"]
+ }
+ }
+ }
+ """;
+
+ var caps = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+ Assert.NotNull(caps);
+
+ var uiCaps = McpApps.GetUiCapability(caps);
+
+ Assert.NotNull(uiCaps);
+ Assert.Equal([McpApps.ResourceMimeType], uiCaps.MimeTypes);
+ }
+
+ [Fact]
+ public void GetUiCapability_ReturnsNull_WhenExtensionValueIsNull()
+ {
+ var json = $$"""
+ {
+ "extensions": {
+ "{{McpApps.ExtensionId}}": null
+ }
+ }
+ """;
+
+ var caps = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+ Assert.NotNull(caps);
+
+ Assert.Null(McpApps.GetUiCapability(caps));
+ }
+
+ #endregion
+
+ #region F6: McpAppUiAttribute via ApplyAppUiAttributes
+
+ [Fact]
+ public void ApplyAppUiAttributes_PopulatesUiObject()
+ {
+ var method = typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.WeatherTool))!;
+ var tool = McpServerTool.Create(method, target: null);
+
+ McpApps.ApplyAppUiAttributes(tool);
+
+ var meta = tool.ProtocolTool.Meta;
+ Assert.NotNull(meta);
+
+ // Structured "ui" object
+ var uiNode = meta["ui"]?.AsObject();
+ Assert.NotNull(uiNode);
+ Assert.Equal("ui://weather/view.html", uiNode["resourceUri"]?.GetValue());
+ }
+
+ [Fact]
+ public void ApplyAppUiAttributes_WithVisibility_IncludesVisibilityInUiObject()
+ {
+ var method = typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.ModelOnlyTool))!;
+ var tool = McpServerTool.Create(method, target: null);
+
+ McpApps.ApplyAppUiAttributes(tool);
+
+ var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
+ Assert.NotNull(uiNode);
+ Assert.Equal("ui://model-only/view.html", uiNode["resourceUri"]?.GetValue());
+
+ var visibility = uiNode["visibility"]?.AsArray();
+ Assert.NotNull(visibility);
+ Assert.Single(visibility);
+ Assert.Equal(McpUiToolVisibility.Model, visibility[0]?.GetValue());
+ }
+
+ [Fact]
+ public void ApplyAppUiAttributes_ExplicitMeta_TakesPrecedence()
+ {
+ // Explicit Meta["ui"] in options should override the attribute
+ var method = typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.WeatherTool))!;
+ var explicitMeta = new JsonObject
+ {
+ ["ui"] = new JsonObject { ["resourceUri"] = "ui://explicit/override.html" },
+ };
+
+ var tool = McpServerTool.Create(method, target: null, new McpServerToolCreateOptions { Meta = explicitMeta });
+
+ McpApps.ApplyAppUiAttributes(tool);
+
+ var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
+ // Explicit Meta["ui"] wins — ApplyAppUiAttributes does not overwrite
+ Assert.Equal("ui://explicit/override.html", uiNode?["resourceUri"]?.GetValue());
+ }
+
+ [Fact]
+ public void ApplyAppUiAttributes_Collection_ProcessesAllTools()
+ {
+ var tools = new[]
+ {
+ McpServerTool.Create(typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.WeatherTool))!, target: null),
+ McpServerTool.Create(typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.ModelOnlyTool))!, target: null),
+ };
+
+ McpApps.ApplyAppUiAttributes(tools);
+
+ Assert.NotNull(tools[0].ProtocolTool.Meta?["ui"]);
+ Assert.NotNull(tools[1].ProtocolTool.Meta?["ui"]);
+ }
+
+ [Fact]
+ public void ApplyAppUiAttributes_NoAttribute_DoesNothing()
+ {
+ var tool = McpServerTool.Create(
+ (string input) => input,
+ new McpServerToolCreateOptions { Name = "plain_tool" });
+
+ McpApps.ApplyAppUiAttributes(tool);
+
+ Assert.Null(tool.ProtocolTool.Meta);
+ }
+
+ #endregion
+
+ #region F7: SetAppUi
+
+ [Fact]
+ public void SetAppUi_PopulatesUiObject()
+ {
+ var tool = McpServerTool.Create(
+ (string location) => $"Weather for {location}",
+ new McpServerToolCreateOptions { Name = "get_weather" });
+
+ McpApps.SetAppUi(tool, new McpUiToolMeta { ResourceUri = "ui://weather/view.html" });
+
+ var meta = tool.ProtocolTool.Meta;
+ Assert.NotNull(meta);
+
+ var uiNode = meta["ui"]?.AsObject();
+ Assert.NotNull(uiNode);
+ Assert.Equal("ui://weather/view.html", uiNode["resourceUri"]?.GetValue());
+ }
+
+ [Fact]
+ public void SetAppUi_WithVisibility_IncludesVisibilityInUiObject()
+ {
+ var tool = McpServerTool.Create(
+ (string location) => $"Weather for {location}",
+ new McpServerToolCreateOptions { Name = "get_weather" });
+
+ McpApps.SetAppUi(tool, new McpUiToolMeta
+ {
+ ResourceUri = "ui://weather/view.html",
+ Visibility = [McpUiToolVisibility.Model],
+ });
+
+ var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
+ Assert.NotNull(uiNode);
+
+ var visibility = uiNode["visibility"]?.AsArray();
+ Assert.NotNull(visibility);
+ Assert.Single(visibility);
+ Assert.Equal(McpUiToolVisibility.Model, visibility[0]?.GetValue());
+ }
+
+ [Fact]
+ public void SetAppUi_DoesNotOverwrite_ExistingUiKey()
+ {
+ var tool = McpServerTool.Create(
+ (string location) => $"Weather for {location}",
+ new McpServerToolCreateOptions
+ {
+ Name = "get_weather",
+ Meta = new JsonObject
+ {
+ ["ui"] = new JsonObject { ["resourceUri"] = "ui://explicit/view.html" },
+ },
+ });
+
+ McpApps.SetAppUi(tool, new McpUiToolMeta { ResourceUri = "ui://new/view.html" });
+
+ // Existing Meta["ui"] is preserved
+ var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
+ Assert.Equal("ui://explicit/view.html", uiNode?["resourceUri"]?.GetValue());
+ }
+
+ [Fact]
+ public void SetAppUi_NullResourceUri_ProducesUiObjectWithoutResourceUri()
+ {
+ var tool = McpServerTool.Create(
+ (string location) => $"Weather for {location}",
+ new McpServerToolCreateOptions { Name = "get_weather" });
+
+ McpApps.SetAppUi(tool, new McpUiToolMeta { Visibility = [McpUiToolVisibility.App] });
+
+ var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
+ Assert.NotNull(uiNode);
+ Assert.Null(uiNode["resourceUri"]);
+ }
+
+ [Fact]
+ public void SetAppUi_ReturnsSameTool()
+ {
+ var tool = McpServerTool.Create(
+ (string location) => $"Weather for {location}",
+ new McpServerToolCreateOptions { Name = "get_weather" });
+
+ var result = McpApps.SetAppUi(tool, new McpUiToolMeta { ResourceUri = "ui://weather/view.html" });
+ Assert.Same(tool, result);
+ }
+
+ #endregion
+
+ #region Builder Extension: WithMcpApps
+
+ [Fact]
+ public void WithMcpApps_AppliesAppUiAttributes_ViaOptions()
+ {
+ var sc = new ServiceCollection();
+ sc.AddMcpServer()
+ .WithTools([typeof(TestToolsWithAppUi)])
+ .WithMcpApps();
+
+ using var sp = sc.BuildServiceProvider();
+ var options = sp.GetRequiredService>().Value;
+
+ Assert.NotNull(options.ToolCollection);
+ Assert.NotEmpty(options.ToolCollection);
+
+ // Both tools should have their [McpAppUi] attributes applied
+ var toolsWithUi = options.ToolCollection.Where(t => t.ProtocolTool.Meta?["ui"] is not null).ToList();
+ Assert.Equal(2, toolsWithUi.Count);
+
+ var weatherTool = toolsWithUi.First(t => t.ProtocolTool.Meta!["ui"]!["resourceUri"]?.GetValue() == "ui://weather/view.html");
+ Assert.NotNull(weatherTool);
+ }
+
+ #endregion
+
+ #region Test helper types
+
+ [McpServerToolType]
+ private static class TestToolsWithAppUi
+ {
+ [McpServerTool]
+ [McpAppUi(ResourceUri = "ui://weather/view.html")]
+ [Description("Get weather")]
+ public static string WeatherTool(string location) => $"Weather for {location}";
+
+ [McpServerTool]
+ [McpAppUi(ResourceUri = "ui://model-only/view.html", Visibility = [McpUiToolVisibility.Model])]
+ public static string ModelOnlyTool(string location) => $"Model only for {location}";
+ }
+
+ #endregion
+}