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 CreateMetadata(MethodInfo method) return meta; } + /// Creates a shallow-content clone 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/McpAppUiAttribute.cs b/src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs new file mode 100644 index 000000000..6d056b9bf --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.CodeAnalysis; + +namespace ModelContextProtocol.Server; + +/// +/// Specifies MCP Apps UI metadata for a tool method. +/// +/// +/// +/// 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. +/// +/// +/// This attribute takes precedence over any raw [McpMeta("ui", ...)] attribute on the +/// same method. +/// +/// +/// +/// +/// [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.Core/Server/McpApps.cs b/src/ModelContextProtocol.Core/Server/McpApps.cs new file mode 100644 index 000000000..4f5292849 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpApps.cs @@ -0,0 +1,113 @@ +using ModelContextProtocol.Protocol; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; + +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"; + + /// + /// 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. + /// + /// 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; + } + } + + // 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 88d718d13..33c5b1c82 100644 --- a/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs +++ b/src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs @@ -197,6 +197,36 @@ 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 + /// both the structured _meta.ui object and the legacy _meta["ui/resourceUri"] + /// flat key for backward compatibility with older MCP hosts. + /// + /// + /// 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. /// @@ -235,6 +265,7 @@ internal McpServerToolCreateOptions Clone() => Metadata = Metadata, Icons = Icons, Meta = Meta, + AppUi = AppUi, Execution = Execution, }; } diff --git a/src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs b/src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs new file mode 100644 index 000000000..91e649d59 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs @@ -0,0 +1,30 @@ +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 key in the +/// dictionary. +/// +/// +/// Use to read this from . +/// +/// +[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.Core/Server/McpUiResourceCsp.cs b/src/ModelContextProtocol.Core/Server/McpUiResourceCsp.cs new file mode 100644 index 000000000..60064863f --- /dev/null +++ b/src/ModelContextProtocol.Core/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.Core/Server/McpUiResourceMeta.cs b/src/ModelContextProtocol.Core/Server/McpUiResourceMeta.cs new file mode 100644 index 000000000..c8f3b3ed6 --- /dev/null +++ b/src/ModelContextProtocol.Core/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.Core/Server/McpUiResourcePermissions.cs b/src/ModelContextProtocol.Core/Server/McpUiResourcePermissions.cs new file mode 100644 index 000000000..f8ff58041 --- /dev/null +++ b/src/ModelContextProtocol.Core/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.Core/Server/McpUiToolMeta.cs b/src/ModelContextProtocol.Core/Server/McpUiToolMeta.cs new file mode 100644 index 000000000..f56396e29 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpUiToolMeta.cs @@ -0,0 +1,45 @@ +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. +/// +/// +/// 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 +{ + /// + /// 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.Core/Server/McpUiToolVisibility.cs b/src/ModelContextProtocol.Core/Server/McpUiToolVisibility.cs new file mode 100644 index 000000000..e9a454c25 --- /dev/null +++ b/src/ModelContextProtocol.Core/Server/McpUiToolVisibility.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +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/Server/McpAppsTests.cs b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs new file mode 100644 index 000000000..f0d76def5 --- /dev/null +++ b/tests/ModelContextProtocol.Tests/Server/McpAppsTests.cs @@ -0,0 +1,403 @@ +#pragma warning disable MCPEXP001 + +using ModelContextProtocol.Protocol; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +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, and McpServerToolCreateOptions.AppUi. +/// +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); + Assert.Equal("ui/resourceUri", McpApps.ResourceUriMetaKey); + } + + [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, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + 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, McpJsonUtilities.DefaultOptions); + 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, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + 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, McpJsonUtilities.DefaultOptions); + var deserialized = JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions); + + 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 + + [Fact] + public void McpAppUiAttribute_PopulatesBothUiObjectAndLegacyKey() + { + var method = typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.WeatherTool))!; + var tool = McpServerTool.Create(method, target: null); + + 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()); + + // Legacy flat key + Assert.Equal("ui://weather/view.html", meta[McpApps.ResourceUriMetaKey]?.GetValue()); + } + + [Fact] + public void McpAppUiAttribute_WithVisibility_IncludesVisibilityInUiObject() + { + var method = typeof(TestToolsWithAppUi).GetMethod(nameof(TestToolsWithAppUi.ModelOnlyTool))!; + var tool = McpServerTool.Create(method, target: null); + + 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 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()); + + // 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()); + } + + [Fact] + public void McpAppUiAttribute_ExplicitOptionsMeta_TakesPrecedenceOver_Attribute() + { + // 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" }, + [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 + + #region F7: McpServerToolCreateOptions.AppUi + + [Fact] + public void AppUi_PopulatesBothUiObjectAndLegacyKey() + { + var tool = McpServerTool.Create( + (string location) => $"Weather for {location}", + new McpServerToolCreateOptions + { + Name = "get_weather", + AppUi = 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()); + Assert.Equal("ui://weather/view.html", meta[McpApps.ResourceUriMetaKey]?.GetValue()); + } + + [Fact] + public void AppUi_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], + }, + }); + + 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 AppUi_ExplicitMeta_TakesPrecedenceOver_AppUi() + { + 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" }, + }); + + 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_DoesNotPopulateLegacyKey() + { + // AppUi with no ResourceUri should not add the legacy flat key + var tool = McpServerTool.Create( + (string location) => $"Weather for {location}", + new McpServerToolCreateOptions + { + Name = "get_weather", + AppUi = new McpUiToolMeta { Visibility = [McpUiToolVisibility.App] }, + }); + + Assert.Null(tool.ProtocolTool.Meta?[McpApps.ResourceUriMetaKey]); + } + + [Fact] + public void AppUi_IsPreservedWhenOptionsAreClonedInDeriveOptions() + { + // 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); + + // 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"]); + Assert.NotNull(meta[McpApps.ResourceUriMetaKey]); + } + + #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}"; + + [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 fccaa0abd594ab67ce94c9c39d7f0cb59fcdec3f Mon Sep 17 00:00:00 2001 From: Mike Kistler Date: Sun, 26 Apr 2026 16:24:43 -0500 Subject: [PATCH 3/6] Move MCP Apps support to its own package --- ModelContextProtocol.slnx | 1 + docs/list-of-diagnostics.md | 2 +- .../Server/AIFunctionMcpServerTool.cs | 6 +- .../Server/McpAppUiAttribute.cs | 7 +- .../Server/McpApps.cs | 92 +------------------ .../Server/McpServerToolCreateOptions.cs | 3 +- .../Server/McpUiClientCapabilities.cs | 8 +- .../Server/McpUiToolMeta.cs | 4 - .../ModelContextProtocol.ExtApps.csproj | 44 +++++++++ .../Server/McpApps.cs | 75 +++++++++++++++ .../ModelContextProtocol.Tests.csproj | 1 + .../Server/McpAppsTests.cs | 18 +--- 12 files changed, 140 insertions(+), 121 deletions(-) create mode 100644 src/ModelContextProtocol.ExtApps/ModelContextProtocol.ExtApps.csproj create mode 100644 src/ModelContextProtocol.ExtApps/Server/McpApps.cs diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 1090c5377..f26305cdd 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -66,6 +66,7 @@ + diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index ebbad5907..d3be3ee2e 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, 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)). | +| `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)). | | `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/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs index a83dd33ae..b422bc8de 100644 --- a/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs +++ b/src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs @@ -153,7 +153,7 @@ options.OpenWorld is not null || if (options.AppUi is { } appUi) { seededMeta = seededMeta is not null ? CloneJsonObject(seededMeta) : new JsonObject(); - McpApps.ApplyUiToolMetaToJsonObject(appUi, seededMeta); + McpAppsInternal.ApplyUiToolMetaToJsonObject(appUi, seededMeta); } tool.Meta = function.UnderlyingMethod is not null ? @@ -170,7 +170,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) { @@ -426,7 +426,7 @@ internal static IReadOnlyList CreateMetadata(MethodInfo method) return meta; } - /// Creates a shallow-content clone of a so that keys can be added without mutating the original. + /// 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(); diff --git a/src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs b/src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs index 6d056b9bf..f5fafcb42 100644 --- a/src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs +++ b/src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs @@ -7,10 +7,9 @@ namespace ModelContextProtocol.Server; /// /// /// -/// 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.0 true true - ModelContextProtocol.ExtApps + ModelContextProtocol.Extensions.Apps MCP Apps extension for the .NET Model Context Protocol (MCP) SDK README.md - - $(NoWarn);MCPEXP001 + + $(NoWarn);MCPEXP003 false 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