From bb32d0ee20ca998082f67d8e566f161348498714 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 31 Mar 2026 19:46:21 +0000
Subject: [PATCH 1/6] Initial plan
From 35e7e59607a51774e90eb701c831d945f72c69d6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 31 Mar 2026 20:36:14 +0000
Subject: [PATCH 2/6] feat: Add MCP Apps extension support (F1-F3, F6, F7)
Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/5ec8e2cd-39e5-4b4c-a18e-182ccaaa7637
Co-authored-by: mikekistler <85643503+mikekistler@users.noreply.github.com>
---
docs/list-of-diagnostics.md | 2 +-
src/Common/Experimentals.cs | 19 +
.../McpJsonUtilities.cs | 7 +
.../Server/AIFunctionMcpServerTool.cs | 39 +-
.../Server/McpAppUiAttribute.cs | 56 +++
.../Server/McpApps.cs | 113 +++++
.../Server/McpServerToolCreateOptions.cs | 31 ++
.../Server/McpUiClientCapabilities.cs | 30 ++
.../Server/McpUiResourceCsp.cs | 49 +++
.../Server/McpUiResourceMeta.cs | 50 +++
.../Server/McpUiResourcePermissions.cs | 27 ++
.../Server/McpUiToolMeta.cs | 45 ++
.../Server/McpUiToolVisibility.cs | 26 ++
.../Server/McpAppsTests.cs | 403 ++++++++++++++++++
14 files changed, 893 insertions(+), 4 deletions(-)
create mode 100644 src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs
create mode 100644 src/ModelContextProtocol.Core/Server/McpApps.cs
create mode 100644 src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs
create mode 100644 src/ModelContextProtocol.Core/Server/McpUiResourceCsp.cs
create mode 100644 src/ModelContextProtocol.Core/Server/McpUiResourceMeta.cs
create mode 100644 src/ModelContextProtocol.Core/Server/McpUiResourcePermissions.cs
create mode 100644 src/ModelContextProtocol.Core/Server/McpUiToolMeta.cs
create mode 100644 src/ModelContextProtocol.Core/Server/McpUiToolVisibility.cs
create mode 100644 tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md
index 515472817..ebbad5907 100644
--- a/docs/list-of-diagnostics.md
+++ b/docs/list-of-diagnostics.md
@@ -23,7 +23,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
| Diagnostic ID | Description |
| :------------ | :---------- |
-| `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)). |
+| `MCPEXP001` | Experimental APIs for features in the MCP specification itself, including Tasks, Extensions, and the MCP Apps extension. 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)). MCP Apps is the first official MCP extension, 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)). |
| `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). |
## Obsolete APIs
diff --git a/src/Common/Experimentals.cs b/src/Common/Experimentals.cs
index 7e7e969bb..54904db2a 100644
--- a/src/Common/Experimentals.cs
+++ b/src/Common/Experimentals.cs
@@ -71,6 +71,25 @@ 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.
+ ///
+ ///
+ /// This uses the same diagnostic ID as because
+ /// MCP Apps is implemented as an MCP extension ("io.modelcontextprotocol/ui").
+ ///
+ public const string Apps_DiagnosticId = "MCPEXP001";
+
+ ///
+ /// 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#mcpexp001";
+
///
/// 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/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs
index abb6d29df..13a935311 100644
--- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs
+++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs
@@ -187,6 +187,13 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(DynamicClientRegistrationRequest))]
[JsonSerializable(typeof(DynamicClientRegistrationResponse))]
+ // MCP Apps extension types
+ [JsonSerializable(typeof(Server.McpUiToolMeta))]
+ [JsonSerializable(typeof(Server.McpUiClientCapabilities))]
+ [JsonSerializable(typeof(Server.McpUiResourceMeta))]
+ [JsonSerializable(typeof(Server.McpUiResourceCsp))]
+ [JsonSerializable(typeof(Server.McpUiResourcePermissions))]
+
// Primitive types for use in consuming AIFunctions
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(byte))]
diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
index 700d9d26d..a83dd33ae 100644
--- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
+++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
@@ -144,10 +144,21 @@ options.OpenWorld is not null ||
};
}
- // Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
+ // Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available.
+ // Priority order (highest to lowest):
+ // 1. Explicit options.Meta entries
+ // 2. AppUi metadata (from McpAppUiAttribute or McpServerToolCreateOptions.AppUi)
+ // 3. McpMetaAttribute entries on the method
+ JsonObject? seededMeta = options.Meta;
+ if (options.AppUi is { } appUi)
+ {
+ seededMeta = seededMeta is not null ? CloneJsonObject(seededMeta) : new JsonObject();
+ McpApps.ApplyUiToolMetaToJsonObject(appUi, seededMeta);
+ }
+
tool.Meta = function.UnderlyingMethod is not null ?
- CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta) :
- options.Meta;
+ CreateMetaFromAttributes(function.UnderlyingMethod, seededMeta) :
+ seededMeta;
// Apply user-specified Execution settings if provided
if (options.Execution is not null)
@@ -225,6 +236,16 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
newOptions.Description ??= descAttr.Description;
}
+ // Process McpAppUiAttribute — takes precedence over options.AppUi set via constructor.
+ if (method.GetCustomAttribute() is { } appUiAttr)
+ {
+ newOptions.AppUi = new McpUiToolMeta
+ {
+ ResourceUri = appUiAttr.ResourceUri,
+ Visibility = appUiAttr.Visibility,
+ };
+ }
+
// Set metadata if not already provided
newOptions.Metadata ??= CreateMetadata(method);
@@ -405,6 +426,18 @@ internal static IReadOnlyList
///
///
-/// Apply this attribute alongside to associate a tool with a
-/// UI resource in the MCP Apps extension. When processed, it populates both the structured
-/// _meta.ui object and the legacy _meta["ui/resourceUri"] flat key in the tool's
-/// metadata for backward compatibility with older MCP hosts.
+/// Apply this attribute alongside to associate an MCP Apps
+/// UI resource with the tool. When processed, it populates the structured _meta.ui object
+/// in the tool's metadata.
///
///
/// This attribute takes precedence over any raw [McpMeta("ui", ...)] attribute on the
diff --git a/src/ModelContextProtocol.Core/Server/McpApps.cs b/src/ModelContextProtocol.Core/Server/McpApps.cs
index 4f5292849..25077aeb6 100644
--- a/src/ModelContextProtocol.Core/Server/McpApps.cs
+++ b/src/ModelContextProtocol.Core/Server/McpApps.cs
@@ -5,90 +5,14 @@
namespace ModelContextProtocol.Server;
///
-/// Provides constants and helper methods for building MCP Apps-enabled servers.
+/// Internal helper methods for MCP Apps integration within the Core package.
+/// The public MCP Apps API surface is in the ModelContextProtocol.ExtApps package.
///
-///
-///
-/// 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.
-///
-///
-public static class McpApps
+internal static class McpAppsInternal
{
///
- /// 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";
-
- ///
- /// The legacy flat _meta key for the UI resource URI.
- ///
- ///
- ///
- /// This key is used for backward compatibility with older MCP hosts that do not support
- /// the nested _meta.ui object. When populating UI metadata, both this key and the
- /// ui object should be set to the same resource URI value.
- ///
- ///
- /// This key is considered legacy; prefer for new implementations.
- ///
- ///
- public const string ResourceUriMetaKey = "ui/resourceUri";
-
- ///
- /// 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.
- ///
- [Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
- 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, McpJsonUtilities.JsonContext.Default.McpUiClientCapabilities);
- }
-
- return null;
- }
-
- ///
- /// Applies UI tool metadata to a , setting both the
- /// ui object key and the legacy ui/resourceUri flat key for backward compatibility.
- /// Keys already present in are not overwritten.
+ /// Applies UI tool metadata to a , setting the
+ /// ui object key if not already present.
///
/// The UI tool metadata to apply.
/// The to populate.
@@ -103,11 +27,5 @@ internal static void ApplyUiToolMetaToJsonObject(McpUiToolMeta appUi, System.Tex
meta["ui"] = uiNode;
}
}
-
- // Populate the legacy flat "ui/resourceUri" key if not already present.
- if (!meta.ContainsKey(ResourceUriMetaKey) && appUi.ResourceUri is not null)
- {
- meta[ResourceUriMetaKey] = appUi.ResourceUri;
- }
}
}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
index 33c5b1c82..ac2d39c76 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
@@ -203,8 +203,7 @@ public sealed class McpServerToolCreateOptions
///
///
/// When set, this metadata is merged into during tool creation, populating
- /// both the structured _meta.ui object and the legacy _meta["ui/resourceUri"]
- /// flat key for backward compatibility with older MCP hosts.
+ /// the structured _meta.ui object.
///
///
/// Explicit entries already present in take precedence over values from
diff --git a/src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs b/src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs
index 91e649d59..55e4df3ac 100644
--- a/src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs
+++ b/src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs
@@ -8,12 +8,9 @@ namespace ModelContextProtocol.Server;
///
///
///
-/// This object is the value associated with the key in the
+/// This object is the value associated with the "io.modelcontextprotocol/ui" key in the
/// dictionary.
///
-///
-/// Use to read this from .
-///
///
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
public sealed class McpUiClientCapabilities
@@ -22,8 +19,7 @@ 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.
+ /// 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.Core/Server/McpUiToolMeta.cs b/src/ModelContextProtocol.Core/Server/McpUiToolMeta.cs
index f56396e29..464061086 100644
--- a/src/ModelContextProtocol.Core/Server/McpUiToolMeta.cs
+++ b/src/ModelContextProtocol.Core/Server/McpUiToolMeta.cs
@@ -12,10 +12,6 @@ namespace ModelContextProtocol.Server;
/// It associates the tool with a UI resource (identified by a ui:// URI) and optionally
/// controls which principals (model, app) can call the tool.
///
-///
-/// When this metadata is applied, both the structured _meta.ui object and the legacy
-/// _meta["ui/resourceUri"] flat key are populated for backward compatibility with older hosts.
-///
///
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
public sealed class McpUiToolMeta
diff --git a/src/ModelContextProtocol.ExtApps/ModelContextProtocol.ExtApps.csproj b/src/ModelContextProtocol.ExtApps/ModelContextProtocol.ExtApps.csproj
new file mode 100644
index 000000000..bcea236ca
--- /dev/null
+++ b/src/ModelContextProtocol.ExtApps/ModelContextProtocol.ExtApps.csproj
@@ -0,0 +1,44 @@
+
+
+
+ net10.0;net9.0;net8.0;netstandard2.0
+ true
+ true
+ ModelContextProtocol.ExtApps
+ MCP Apps extension for the .NET Model Context Protocol (MCP) SDK
+ README.md
+
+ $(NoWarn);MCPEXP001
+
+ false
+
+
+
+ true
+
+
+
+
+ $(NoWarn);CS0436
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/ModelContextProtocol.ExtApps/Server/McpApps.cs b/src/ModelContextProtocol.ExtApps/Server/McpApps.cs
new file mode 100644
index 000000000..ed58c28eb
--- /dev/null
+++ b/src/ModelContextProtocol.ExtApps/Server/McpApps.cs
@@ -0,0 +1,75 @@
+using ModelContextProtocol.Protocol;
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Json;
+using System.Text.Json.Serialization.Metadata;
+
+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.
+///
+///
+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 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.
+ ///
+ [Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
+ 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 :
+ (McpUiClientCapabilities?)JsonSerializer.Deserialize(
+ element,
+ (JsonTypeInfo)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(McpUiClientCapabilities)));
+ }
+
+ return null;
+ }
+}
diff --git a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
index 0985f4cd7..dc905e6e6 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
index f0d76def5..d7b2f2170 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
@@ -22,7 +22,6 @@ public void McpApps_Constants_HaveExpectedValues()
{
Assert.Equal("text/html;profile=mcp-app", McpApps.ResourceMimeType);
Assert.Equal("io.modelcontextprotocol/ui", McpApps.ExtensionId);
- Assert.Equal("ui/resourceUri", McpApps.ResourceUriMetaKey);
}
[Fact]
@@ -212,9 +211,6 @@ public void McpAppUiAttribute_PopulatesBothUiObjectAndLegacyKey()
var uiNode = meta["ui"]?.AsObject();
Assert.NotNull(uiNode);
Assert.Equal("ui://weather/view.html", uiNode["resourceUri"]?.GetValue());
-
- // Legacy flat key
- Assert.Equal("ui://weather/view.html", meta[McpApps.ResourceUriMetaKey]?.GetValue());
}
[Fact]
@@ -248,9 +244,6 @@ public void McpAppUiAttribute_TakesPrecedenceOver_McpMetaAttribute()
Assert.NotNull(uiNode);
Assert.Equal("ui://app-ui/view.html", uiNode["resourceUri"]?.GetValue());
- // The legacy key should be from McpAppUiAttribute
- Assert.Equal("ui://app-ui/view.html", meta[McpApps.ResourceUriMetaKey]?.GetValue());
-
// Other McpMeta attributes should still be present
Assert.Equal("extra-value", meta["extraKey"]?.GetValue());
}
@@ -263,14 +256,12 @@ public void McpAppUiAttribute_ExplicitOptionsMeta_TakesPrecedenceOver_Attribute(
var explicitMeta = new JsonObject
{
["ui"] = new JsonObject { ["resourceUri"] = "ui://explicit/override.html" },
- [McpApps.ResourceUriMetaKey] = "ui://explicit/override.html",
};
var tool = McpServerTool.Create(method, target: null, new McpServerToolCreateOptions { Meta = explicitMeta });
var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
Assert.Equal("ui://explicit/override.html", uiNode?["resourceUri"]?.GetValue());
- Assert.Equal("ui://explicit/override.html", tool.ProtocolTool.Meta?[McpApps.ResourceUriMetaKey]?.GetValue());
}
#endregion
@@ -294,7 +285,6 @@ public void AppUi_PopulatesBothUiObjectAndLegacyKey()
var uiNode = meta["ui"]?.AsObject();
Assert.NotNull(uiNode);
Assert.Equal("ui://weather/view.html", uiNode["resourceUri"]?.GetValue());
- Assert.Equal("ui://weather/view.html", meta[McpApps.ResourceUriMetaKey]?.GetValue());
}
[Fact]
@@ -343,9 +333,8 @@ public void AppUi_ExplicitMeta_TakesPrecedenceOver_AppUi()
}
[Fact]
- public void AppUi_NullResourceUri_DoesNotPopulateLegacyKey()
+ public void AppUi_NullResourceUri_ProducesUiObjectWithoutResourceUri()
{
- // AppUi with no ResourceUri should not add the legacy flat key
var tool = McpServerTool.Create(
(string location) => $"Weather for {location}",
new McpServerToolCreateOptions
@@ -354,7 +343,9 @@ public void AppUi_NullResourceUri_DoesNotPopulateLegacyKey()
AppUi = new McpUiToolMeta { Visibility = [McpUiToolVisibility.App] },
});
- Assert.Null(tool.ProtocolTool.Meta?[McpApps.ResourceUriMetaKey]);
+ var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
+ Assert.NotNull(uiNode);
+ Assert.Null(uiNode["resourceUri"]);
}
[Fact]
@@ -373,7 +364,6 @@ public void AppUi_IsPreservedWhenOptionsAreClonedInDeriveOptions()
var meta = tool.ProtocolTool.Meta;
Assert.NotNull(meta);
Assert.NotNull(meta["ui"]);
- Assert.NotNull(meta[McpApps.ResourceUriMetaKey]);
}
#endregion
From 5673bd799c8596af7dd3829b7d9d457cebe5dcea Mon Sep 17 00:00:00 2001
From: Mike Kistler
Date: Wed, 29 Apr 2026 16:43:27 -0500
Subject: [PATCH 4/6] Address review feedback: rename, move types, remove Core
coupling
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
ModelContextProtocol.slnx | 2 +-
docs/list-of-diagnostics.md | 3 +-
src/Common/Experimentals.cs | 9 +-
.../McpJsonUtilities.cs | 7 -
.../Server/AIFunctionMcpServerTool.cs | 39 +---
.../Server/McpApps.cs | 31 ---
.../Server/McpServerToolCreateOptions.cs | 30 ---
.../Server/McpApps.cs | 75 -------
...delContextProtocol.Extensions.Apps.csproj} | 6 +-
.../Server/McpAppUiAttribute.cs | 8 +-
.../Server/McpApps.cs | 207 ++++++++++++++++++
.../Server/McpAppsJsonContext.cs | 19 ++
.../Server/McpUiClientCapabilities.cs | 0
.../Server/McpUiResourceCsp.cs | 0
.../Server/McpUiResourceMeta.cs | 0
.../Server/McpUiResourcePermissions.cs | 0
.../Server/McpUiToolMeta.cs | 0
.../Server/McpUiToolVisibility.cs | 1 -
.../ModelContextProtocol.Tests.csproj | 2 +-
.../Server/McpAppsTests.cs | 153 +++++++------
20 files changed, 320 insertions(+), 272 deletions(-)
delete mode 100644 src/ModelContextProtocol.Core/Server/McpApps.cs
delete mode 100644 src/ModelContextProtocol.ExtApps/Server/McpApps.cs
rename src/{ModelContextProtocol.ExtApps/ModelContextProtocol.ExtApps.csproj => ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj} (90%)
rename src/{ModelContextProtocol.Core => ModelContextProtocol.Extensions.Apps}/Server/McpAppUiAttribute.cs (82%)
create mode 100644 src/ModelContextProtocol.Extensions.Apps/Server/McpApps.cs
create mode 100644 src/ModelContextProtocol.Extensions.Apps/Server/McpAppsJsonContext.cs
rename src/{ModelContextProtocol.Core => ModelContextProtocol.Extensions.Apps}/Server/McpUiClientCapabilities.cs (100%)
rename src/{ModelContextProtocol.Core => ModelContextProtocol.Extensions.Apps}/Server/McpUiResourceCsp.cs (100%)
rename src/{ModelContextProtocol.Core => ModelContextProtocol.Extensions.Apps}/Server/McpUiResourceMeta.cs (100%)
rename src/{ModelContextProtocol.Core => ModelContextProtocol.Extensions.Apps}/Server/McpUiResourcePermissions.cs (100%)
rename src/{ModelContextProtocol.Core => ModelContextProtocol.Extensions.Apps}/Server/McpUiToolMeta.cs (100%)
rename src/{ModelContextProtocol.Core => ModelContextProtocol.Extensions.Apps}/Server/McpUiToolVisibility.cs (95%)
diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx
index f26305cdd..ffc845fc8 100644
--- a/ModelContextProtocol.slnx
+++ b/ModelContextProtocol.slnx
@@ -66,7 +66,7 @@
-
+
diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md
index d3be3ee2e..572b2c183 100644
--- a/docs/list-of-diagnostics.md
+++ b/docs/list-of-diagnostics.md
@@ -23,8 +23,9 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
| Diagnostic ID | Description |
| :------------ | :---------- |
-| `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)). MCP Apps is the first official MCP extension, 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)). |
+| `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 54904db2a..83f517bec 100644
--- a/src/Common/Experimentals.cs
+++ b/src/Common/Experimentals.cs
@@ -75,10 +75,11 @@ internal static class Experimentals
/// Diagnostic ID for experimental MCP Apps extension APIs.
///
///
- /// This uses the same diagnostic ID as because
- /// MCP Apps is implemented as an MCP extension ("io.modelcontextprotocol/ui").
+ /// 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 = "MCPEXP001";
+ public const string Apps_DiagnosticId = "MCPEXP003";
///
/// Message for the experimental MCP Apps extension APIs.
@@ -88,7 +89,7 @@ internal static class Experimentals
///
/// 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#mcpexp001";
+ 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,
diff --git a/src/ModelContextProtocol.Core/McpJsonUtilities.cs b/src/ModelContextProtocol.Core/McpJsonUtilities.cs
index 13a935311..abb6d29df 100644
--- a/src/ModelContextProtocol.Core/McpJsonUtilities.cs
+++ b/src/ModelContextProtocol.Core/McpJsonUtilities.cs
@@ -187,13 +187,6 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
[JsonSerializable(typeof(DynamicClientRegistrationRequest))]
[JsonSerializable(typeof(DynamicClientRegistrationResponse))]
- // MCP Apps extension types
- [JsonSerializable(typeof(Server.McpUiToolMeta))]
- [JsonSerializable(typeof(Server.McpUiClientCapabilities))]
- [JsonSerializable(typeof(Server.McpUiResourceMeta))]
- [JsonSerializable(typeof(Server.McpUiResourceCsp))]
- [JsonSerializable(typeof(Server.McpUiResourcePermissions))]
-
// Primitive types for use in consuming AIFunctions
[JsonSerializable(typeof(string))]
[JsonSerializable(typeof(byte))]
diff --git a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
index b422bc8de..68a99fb73 100644
--- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
+++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
@@ -144,21 +144,10 @@ options.OpenWorld is not null ||
};
}
- // Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available.
- // Priority order (highest to lowest):
- // 1. Explicit options.Meta entries
- // 2. AppUi metadata (from McpAppUiAttribute or McpServerToolCreateOptions.AppUi)
- // 3. McpMetaAttribute entries on the method
- JsonObject? seededMeta = options.Meta;
- if (options.AppUi is { } appUi)
- {
- seededMeta = seededMeta is not null ? CloneJsonObject(seededMeta) : new JsonObject();
- McpAppsInternal.ApplyUiToolMetaToJsonObject(appUi, seededMeta);
- }
-
+ // Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
tool.Meta = function.UnderlyingMethod is not null ?
- CreateMetaFromAttributes(function.UnderlyingMethod, seededMeta) :
- seededMeta;
+ CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta) :
+ options.Meta;
// Apply user-specified Execution settings if provided
if (options.Execution is not null)
@@ -236,16 +225,6 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
newOptions.Description ??= descAttr.Description;
}
- // Process McpAppUiAttribute — takes precedence over options.AppUi set via constructor.
- if (method.GetCustomAttribute() is { } appUiAttr)
- {
- newOptions.AppUi = new McpUiToolMeta
- {
- ResourceUri = appUiAttr.ResourceUri,
- Visibility = appUiAttr.Visibility,
- };
- }
-
// Set metadata if not already provided
newOptions.Metadata ??= CreateMetadata(method);
@@ -426,18 +405,6 @@ internal static IReadOnlyList CreateMetadata(MethodInfo method)
return meta;
}
- /// Creates a copy of a so that keys can be added without mutating the original.
- private static JsonObject CloneJsonObject(JsonObject source)
- {
- var clone = new JsonObject();
- foreach (var kvp in source)
- {
- // DeepClone each value to avoid sharing nodes between two JsonObject instances.
- clone[kvp.Key] = kvp.Value?.DeepClone();
- }
- return clone;
- }
-
#if NET
/// Regex that flags runs of characters other than ASCII digits or letters.
[GeneratedRegex("[^0-9A-Za-z]+")]
diff --git a/src/ModelContextProtocol.Core/Server/McpApps.cs b/src/ModelContextProtocol.Core/Server/McpApps.cs
deleted file mode 100644
index 25077aeb6..000000000
--- a/src/ModelContextProtocol.Core/Server/McpApps.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using ModelContextProtocol.Protocol;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json;
-
-namespace ModelContextProtocol.Server;
-
-///
-/// Internal helper methods for MCP Apps integration within the Core package.
-/// The public MCP Apps API surface is in the ModelContextProtocol.ExtApps package.
-///
-internal static class McpAppsInternal
-{
- ///
- /// Applies UI tool metadata to a , setting the
- /// ui object key if not already present.
- ///
- /// The UI tool metadata to apply.
- /// The to populate.
- internal static void ApplyUiToolMetaToJsonObject(McpUiToolMeta appUi, System.Text.Json.Nodes.JsonObject meta)
- {
- // Populate the structured "ui" object if not already present.
- if (!meta.ContainsKey("ui"))
- {
- var uiNode = JsonSerializer.SerializeToNode(appUi, McpJsonUtilities.JsonContext.Default.McpUiToolMeta);
- if (uiNode is not null)
- {
- meta["ui"] = uiNode;
- }
- }
- }
-}
diff --git a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
index ac2d39c76..88d718d13 100644
--- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
+++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
@@ -197,35 +197,6 @@ public sealed class McpServerToolCreateOptions
///
public JsonObject? Meta { get; set; }
- ///
- /// Gets or sets the MCP Apps UI metadata for this tool.
- ///
- ///
- ///
- /// When set, this metadata is merged into during tool creation, populating
- /// the structured _meta.ui object.
- ///
- ///
- /// Explicit entries already present in take precedence over values from
- /// this property. The on a method overrides this property
- /// when both are specified.
- ///
- ///
- ///
- ///
- /// var tool = McpServerTool.Create(handler, new McpServerToolCreateOptions
- /// {
- /// AppUi = new McpUiToolMeta
- /// {
- /// ResourceUri = "ui://weather/view.html",
- /// Visibility = [McpUiToolVisibility.Model, McpUiToolVisibility.App]
- /// }
- /// });
- ///
- ///
- [Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
- public McpUiToolMeta? AppUi { get; set; }
-
///
/// Gets or sets the execution hints for this tool.
///
@@ -264,7 +235,6 @@ internal McpServerToolCreateOptions Clone() =>
Metadata = Metadata,
Icons = Icons,
Meta = Meta,
- AppUi = AppUi,
Execution = Execution,
};
}
diff --git a/src/ModelContextProtocol.ExtApps/Server/McpApps.cs b/src/ModelContextProtocol.ExtApps/Server/McpApps.cs
deleted file mode 100644
index ed58c28eb..000000000
--- a/src/ModelContextProtocol.ExtApps/Server/McpApps.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using ModelContextProtocol.Protocol;
-using System.Diagnostics.CodeAnalysis;
-using System.Text.Json;
-using System.Text.Json.Serialization.Metadata;
-
-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.
-///
-///
-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 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.
- ///
- [Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
- 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 :
- (McpUiClientCapabilities?)JsonSerializer.Deserialize(
- element,
- (JsonTypeInfo)McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(McpUiClientCapabilities)));
- }
-
- return null;
- }
-}
diff --git a/src/ModelContextProtocol.ExtApps/ModelContextProtocol.ExtApps.csproj b/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj
similarity index 90%
rename from src/ModelContextProtocol.ExtApps/ModelContextProtocol.ExtApps.csproj
rename to src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj
index bcea236ca..b3c988d21 100644
--- a/src/ModelContextProtocol.ExtApps/ModelContextProtocol.ExtApps.csproj
+++ b/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj
@@ -4,11 +4,11 @@
net10.0;net9.0;net8.0;netstandard2.0truetrue
- ModelContextProtocol.ExtApps
+ ModelContextProtocol.Extensions.AppsMCP Apps extension for the .NET Model Context Protocol (MCP) SDKREADME.md
-
- $(NoWarn);MCPEXP001
+
+ $(NoWarn);MCPEXP003false
diff --git a/src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpAppUiAttribute.cs
similarity index 82%
rename from src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs
rename to src/ModelContextProtocol.Extensions.Apps/Server/McpAppUiAttribute.cs
index f5fafcb42..2bbe5a059 100644
--- a/src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpAppUiAttribute.cs
@@ -8,12 +8,14 @@ namespace ModelContextProtocol.Server;
///
///
/// Apply this attribute alongside to associate an MCP Apps
-/// UI resource with the tool. When processed, it populates the structured _meta.ui object
-/// in the tool's metadata.
+/// 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.
+/// same method, but explicit Meta["ui"] set via
+/// takes precedence over this attribute.
///
///
///
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/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.Core/Server/McpUiClientCapabilities.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiClientCapabilities.cs
similarity index 100%
rename from src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs
rename to src/ModelContextProtocol.Extensions.Apps/Server/McpUiClientCapabilities.cs
diff --git a/src/ModelContextProtocol.Core/Server/McpUiResourceCsp.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceCsp.cs
similarity index 100%
rename from src/ModelContextProtocol.Core/Server/McpUiResourceCsp.cs
rename to src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceCsp.cs
diff --git a/src/ModelContextProtocol.Core/Server/McpUiResourceMeta.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceMeta.cs
similarity index 100%
rename from src/ModelContextProtocol.Core/Server/McpUiResourceMeta.cs
rename to src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourceMeta.cs
diff --git a/src/ModelContextProtocol.Core/Server/McpUiResourcePermissions.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourcePermissions.cs
similarity index 100%
rename from src/ModelContextProtocol.Core/Server/McpUiResourcePermissions.cs
rename to src/ModelContextProtocol.Extensions.Apps/Server/McpUiResourcePermissions.cs
diff --git a/src/ModelContextProtocol.Core/Server/McpUiToolMeta.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolMeta.cs
similarity index 100%
rename from src/ModelContextProtocol.Core/Server/McpUiToolMeta.cs
rename to src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolMeta.cs
diff --git a/src/ModelContextProtocol.Core/Server/McpUiToolVisibility.cs b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolVisibility.cs
similarity index 95%
rename from src/ModelContextProtocol.Core/Server/McpUiToolVisibility.cs
rename to src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolVisibility.cs
index e9a454c25..df84c5467 100644
--- a/src/ModelContextProtocol.Core/Server/McpUiToolVisibility.cs
+++ b/src/ModelContextProtocol.Extensions.Apps/Server/McpUiToolVisibility.cs
@@ -1,5 +1,4 @@
using System.Diagnostics.CodeAnalysis;
-using System.Text.Json.Serialization;
namespace ModelContextProtocol.Server;
diff --git a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
index dc905e6e6..4dc3088b7 100644
--- a/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
+++ b/tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj
@@ -85,7 +85,7 @@
-
+
diff --git a/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
index d7b2f2170..b2b010ae2 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
@@ -1,9 +1,8 @@
-#pragma warning disable MCPEXP001
+#pragma warning disable MCPEXP003
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
@@ -11,7 +10,7 @@ namespace ModelContextProtocol.Tests.Server;
///
/// Tests for MCP Apps extension support: McpApps constants, typed metadata models,
-/// McpAppUiAttribute, and McpServerToolCreateOptions.AppUi.
+/// McpAppUiAttribute, SetAppUi, and ApplyAppUiAttributes.
///
public class McpAppsTests
{
@@ -52,8 +51,8 @@ public void McpUiToolMeta_CanBeRoundtrippedAsJson()
Visibility = [McpUiToolVisibility.Model, McpUiToolVisibility.App],
};
- var json = JsonSerializer.Serialize(meta, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+ 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);
@@ -64,7 +63,7 @@ public void McpUiToolMeta_CanBeRoundtrippedAsJson()
public void McpUiToolMeta_OmitsNullProperties()
{
var meta = new McpUiToolMeta { ResourceUri = "ui://app" };
- var json = JsonSerializer.Serialize(meta, McpJsonUtilities.DefaultOptions);
+ var json = JsonSerializer.Serialize(meta, McpApps.SerializerOptions);
var doc = JsonDocument.Parse(json);
Assert.True(doc.RootElement.TryGetProperty("resourceUri", out _));
@@ -91,8 +90,8 @@ public void McpUiResourceMeta_CanBeRoundtrippedAsJson()
},
};
- var json = JsonSerializer.Serialize(meta, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+ 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);
@@ -114,8 +113,8 @@ public void McpUiClientCapabilities_CanBeRoundtrippedAsJson()
MimeTypes = [McpApps.ResourceMimeType],
};
- var json = JsonSerializer.Serialize(caps, McpJsonUtilities.DefaultOptions);
- var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions);
+ var json = JsonSerializer.Serialize(caps, McpApps.SerializerOptions);
+ var deserialized = JsonSerializer.Deserialize(json, McpApps.SerializerOptions);
Assert.NotNull(deserialized);
Assert.Equal([McpApps.ResourceMimeType], deserialized.MimeTypes);
@@ -196,14 +195,16 @@ public void GetUiCapability_ReturnsNull_WhenExtensionValueIsNull()
#endregion
- #region F6: McpAppUiAttribute
+ #region F6: McpAppUiAttribute via ApplyAppUiAttributes
[Fact]
- public void McpAppUiAttribute_PopulatesBothUiObjectAndLegacyKey()
+ 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);
@@ -214,11 +215,13 @@ public void McpAppUiAttribute_PopulatesBothUiObjectAndLegacyKey()
}
[Fact]
- public void McpAppUiAttribute_WithVisibility_IncludesVisibilityInUiObject()
+ 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());
@@ -230,26 +233,7 @@ public void McpAppUiAttribute_WithVisibility_IncludesVisibilityInUiObject()
}
[Fact]
- public void McpAppUiAttribute_TakesPrecedenceOver_McpMetaAttribute()
- {
- // The tool has both [McpAppUi] and [McpMeta("ui", ...)] — AppUi should win for the "ui" key.
- var method = typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.ToolWithBothAttributes))!;
- var tool = McpServerTool.Create(method, target: null);
-
- var meta = tool.ProtocolTool.Meta;
- Assert.NotNull(meta);
-
- // The "ui" key should be from McpAppUiAttribute, not McpMetaAttribute
- var uiNode = meta["ui"]?.AsObject();
- Assert.NotNull(uiNode);
- Assert.Equal("ui://app-ui/view.html", uiNode["resourceUri"]?.GetValue());
-
- // Other McpMeta attributes should still be present
- Assert.Equal("extra-value", meta["extraKey"]?.GetValue());
- }
-
- [Fact]
- public void McpAppUiAttribute_ExplicitOptionsMeta_TakesPrecedenceOver_Attribute()
+ public void ApplyAppUiAttributes_ExplicitMeta_TakesPrecedence()
{
// Explicit Meta["ui"] in options should override the attribute
var method = typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.WeatherTool))!;
@@ -260,24 +244,52 @@ public void McpAppUiAttribute_ExplicitOptionsMeta_TakesPrecedenceOver_Attribute(
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: McpServerToolCreateOptions.AppUi
+ #region F7: SetAppUi
[Fact]
- public void AppUi_PopulatesBothUiObjectAndLegacyKey()
+ public void SetAppUi_PopulatesUiObject()
{
var tool = McpServerTool.Create(
(string location) => $"Weather for {location}",
- new McpServerToolCreateOptions
- {
- Name = "get_weather",
- AppUi = new McpUiToolMeta { ResourceUri = "ui://weather/view.html" },
- });
+ new McpServerToolCreateOptions { Name = "get_weather" });
+
+ McpApps.SetAppUi(tool, new McpUiToolMeta { ResourceUri = "ui://weather/view.html" });
var meta = tool.ProtocolTool.Meta;
Assert.NotNull(meta);
@@ -288,19 +300,17 @@ public void AppUi_PopulatesBothUiObjectAndLegacyKey()
}
[Fact]
- public void AppUi_WithVisibility_IncludesVisibilityInUiObject()
+ public void SetAppUi_WithVisibility_IncludesVisibilityInUiObject()
{
var tool = McpServerTool.Create(
(string location) => $"Weather for {location}",
- new McpServerToolCreateOptions
- {
- Name = "get_weather",
- AppUi = new McpUiToolMeta
- {
- ResourceUri = "ui://weather/view.html",
- Visibility = [McpUiToolVisibility.Model],
- },
- });
+ 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);
@@ -312,36 +322,34 @@ public void AppUi_WithVisibility_IncludesVisibilityInUiObject()
}
[Fact]
- public void AppUi_ExplicitMeta_TakesPrecedenceOver_AppUi()
+ public void SetAppUi_DoesNotOverwrite_ExistingUiKey()
{
var tool = McpServerTool.Create(
(string location) => $"Weather for {location}",
new McpServerToolCreateOptions
{
Name = "get_weather",
- // Explicit Meta entry for "ui" should override AppUi
Meta = new JsonObject
{
["ui"] = new JsonObject { ["resourceUri"] = "ui://explicit/view.html" },
},
- AppUi = new McpUiToolMeta { ResourceUri = "ui://app-ui/view.html" },
});
+ McpApps.SetAppUi(tool, new McpUiToolMeta { ResourceUri = "ui://new/view.html" });
+
+ // Existing Meta["ui"] is preserved
var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
- // Explicit Meta["ui"] wins
Assert.Equal("ui://explicit/view.html", uiNode?["resourceUri"]?.GetValue());
}
[Fact]
- public void AppUi_NullResourceUri_ProducesUiObjectWithoutResourceUri()
+ public void SetAppUi_NullResourceUri_ProducesUiObjectWithoutResourceUri()
{
var tool = McpServerTool.Create(
(string location) => $"Weather for {location}",
- new McpServerToolCreateOptions
- {
- Name = "get_weather",
- AppUi = new McpUiToolMeta { Visibility = [McpUiToolVisibility.App] },
- });
+ new McpServerToolCreateOptions { Name = "get_weather" });
+
+ McpApps.SetAppUi(tool, new McpUiToolMeta { Visibility = [McpUiToolVisibility.App] });
var uiNode = tool.ProtocolTool.Meta?["ui"]?.AsObject();
Assert.NotNull(uiNode);
@@ -349,21 +357,14 @@ public void AppUi_NullResourceUri_ProducesUiObjectWithoutResourceUri()
}
[Fact]
- public void AppUi_IsPreservedWhenOptionsAreClonedInDeriveOptions()
+ public void SetAppUi_ReturnsSameTool()
{
- // DeriveOptions() calls options.Clone() internally when creating via MethodInfo.
- // If AppUi is not included in Clone(), it would be lost when creating the tool via a method.
- var appUi = new McpUiToolMeta { ResourceUri = "ui://weather/view.html" };
- var options = new McpServerToolCreateOptions { AppUi = appUi };
-
- // Use the MethodInfo path, which calls DeriveOptions -> options.Clone()
- var method = typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.WeatherTool))!;
- var tool = McpServerTool.Create(method, target: null, options);
+ var tool = McpServerTool.Create(
+ (string location) => $"Weather for {location}",
+ new McpServerToolCreateOptions { Name = "get_weather" });
- // The attribute on the method overrides options.AppUi, but both should produce the same meta.
- var meta = tool.ProtocolTool.Meta;
- Assert.NotNull(meta);
- Assert.NotNull(meta["ui"]);
+ var result = McpApps.SetAppUi(tool, new McpUiToolMeta { ResourceUri = "ui://weather/view.html" });
+ Assert.Same(tool, result);
}
#endregion
@@ -381,12 +382,6 @@ private static class TestToolsWithAppUi
[McpServerTool]
[McpAppUi(ResourceUri = "ui://model-only/view.html", Visibility = [McpUiToolVisibility.Model])]
public static string ModelOnlyTool(string location) => $"Model only for {location}";
-
- [McpServerTool]
- [McpAppUi(ResourceUri = "ui://app-ui/view.html")]
- [McpMeta("ui", JsonValue = """{"resourceUri": "ui://mcpmeta/view.html"}""")]
- [McpMeta("extraKey", "extra-value")]
- public static string ToolWithBothAttributes(string input) => input;
}
#endregion
From 7753c532fa974607de6275d36953477850a81251 Mon Sep 17 00:00:00 2001
From: Mike Kistler
Date: Wed, 29 Apr 2026 19:57:59 -0500
Subject: [PATCH 5/6] Add WithMcpApps() builder extension for IMcpServerBuilder
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
...odelContextProtocol.Extensions.Apps.csproj | 2 +-
.../Server/McpAppsBuilderExtensions.cs | 61 +++++++++++++++++++
.../Server/McpAppsTests.cs | 28 +++++++++
3 files changed, 90 insertions(+), 1 deletion(-)
create mode 100644 src/ModelContextProtocol.Extensions.Apps/Server/McpAppsBuilderExtensions.cs
diff --git a/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj b/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj
index b3c988d21..ccdce456c 100644
--- a/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj
+++ b/src/ModelContextProtocol.Extensions.Apps/ModelContextProtocol.Extensions.Apps.csproj
@@ -34,7 +34,7 @@
-
+
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/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
index b2b010ae2..7df8f0365 100644
--- a/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
+++ b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs
@@ -1,5 +1,7 @@
#pragma warning disable MCPEXP003
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
using System.ComponentModel;
@@ -369,6 +371,32 @@ public void SetAppUi_ReturnsSameTool()
#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]
From 7f54b8c958bbdfc321b5c865a5c4db31a7a71bbb Mon Sep 17 00:00:00 2001
From: Mike Kistler
Date: Wed, 29 Apr 2026 20:05:46 -0500
Subject: [PATCH 6/6] Add MCP Apps conceptual documentation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
docs/concepts/apps/apps.md | 145 +++++++++++++++++++++++++++++++++++++
docs/concepts/index.md | 6 ++
docs/concepts/toc.yml | 6 +-
3 files changed, 156 insertions(+), 1 deletion(-)
create mode 100644 docs/concepts/apps/apps.md
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 d7cde44e9..47f0b498c 100644
--- a/docs/concepts/index.md
+++ b/docs/concepts/index.md
@@ -39,3 +39,9 @@ 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. |
diff --git a/docs/concepts/toc.yml b/docs/concepts/toc.yml
index 4e0001cd5..1bc6a4839 100644
--- a/docs/concepts/toc.yml
+++ b/docs/concepts/toc.yml
@@ -44,4 +44,8 @@ items:
- name: HTTP Context
uid: httpcontext
- name: Filters
- uid: filters
\ No newline at end of file
+ uid: filters
+- name: Extensions
+ items:
+ - name: MCP Apps
+ uid: apps
\ No newline at end of file