Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions src/Common/Experimentals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,25 @@ internal static class Experimentals
/// </summary>
public const string Extensions_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";

/// <summary>
/// Diagnostic ID for experimental MCP Apps extension APIs.
/// </summary>
/// <remarks>
/// This uses the same diagnostic ID as <see cref="Extensions_DiagnosticId"/> because
/// MCP Apps is implemented as an MCP extension (<c>"io.modelcontextprotocol/ui"</c>).
/// </remarks>
public const string Apps_DiagnosticId = "MCPEXP001";

/// <summary>
/// Message for the experimental MCP Apps extension APIs.
/// </summary>
public const string Apps_Message = "The MCP Apps extension is experimental and subject to change as the specification evolves.";

/// <summary>
/// URL for the experimental MCP Apps extension APIs.
/// </summary>
public const string Apps_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";

/// <summary>
/// Diagnostic ID for experimental SDK APIs unrelated to the MCP specification,
/// such as subclassing <c>McpClient</c>/<c>McpServer</c> or referencing <c>RunSessionHandler</c>.
Expand Down
7 changes: 7 additions & 0 deletions src/ModelContextProtocol.Core/McpJsonUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
39 changes: 36 additions & 3 deletions src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<McpAppUiAttribute>() is { } appUiAttr)
{
newOptions.AppUi = new McpUiToolMeta
{
ResourceUri = appUiAttr.ResourceUri,
Visibility = appUiAttr.Visibility,
};
}

// Set metadata if not already provided
newOptions.Metadata ??= CreateMetadata(method);

Expand Down Expand Up @@ -405,6 +426,18 @@ internal static IReadOnlyList<object> CreateMetadata(MethodInfo method)
return meta;
}

/// <summary>Creates a shallow-content clone of a <see cref="JsonObject"/> so that keys can be added without mutating the original.</summary>
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
/// <summary>Regex that flags runs of characters other than ASCII digits or letters.</summary>
[GeneratedRegex("[^0-9A-Za-z]+")]
Expand Down
56 changes: 56 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpAppUiAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System.Diagnostics.CodeAnalysis;

namespace ModelContextProtocol.Server;

/// <summary>
/// Specifies MCP Apps UI metadata for a tool method.
/// </summary>
/// <remarks>
/// <para>
/// Apply this attribute alongside <see cref="McpServerToolAttribute"/> to associate a tool with a
/// UI resource in the MCP Apps extension. When processed, it populates both the structured
/// <c>_meta.ui</c> object and the legacy <c>_meta["ui/resourceUri"]</c> flat key in the tool's
/// metadata for backward compatibility with older MCP hosts.
/// </para>
/// <para>
/// This attribute takes precedence over any raw <c>[McpMeta("ui", ...)]</c> attribute on the
/// same method.
/// </para>
/// </remarks>
/// <example>
/// <code language="csharp">
/// [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) => ...;
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Method)]
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
public sealed class McpAppUiAttribute : Attribute
{
/// <summary>
/// Gets or sets the URI of the UI resource associated with this tool.
/// </summary>
/// <remarks>
/// This should be a <c>ui://</c> URI pointing to the HTML resource registered
/// with the server (e.g., <c>"ui://weather/view.html"</c>).
/// </remarks>
public string? ResourceUri { get; set; }

/// <summary>
/// Gets or sets the visibility of the tool, controlling which principals can invoke it.
/// </summary>
/// <remarks>
/// <para>
/// Allowed values are <see cref="McpUiToolVisibility.Model"/> and <see cref="McpUiToolVisibility.App"/>.
/// When <see langword="null"/> or empty, the tool is visible to both the model and the app (the default).
/// </para>
/// </remarks>
public string[]? Visibility { get; set; }
}
113 changes: 113 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpApps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
using ModelContextProtocol.Protocol;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json;

namespace ModelContextProtocol.Server;

/// <summary>
/// Provides constants and helper methods for building MCP Apps-enabled servers.
/// </summary>
/// <remarks>
/// <para>
/// 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.
/// </para>
/// <para>
/// Use the constants in this class when populating the <c>extensions</c> capability and the
/// <c>_meta</c> field of tools and resources. Use <see cref="GetUiCapability"/> to check whether
/// the connected client supports the MCP Apps extension.
/// </para>
/// </remarks>
public static class McpApps
{
/// <summary>
/// The MIME type used for MCP App HTML resources.
/// </summary>
/// <remarks>
/// This MIME type should be used when registering UI resources with
/// <c>text/html;profile=mcp-app</c> to indicate they are MCP App resources.
/// </remarks>
public const string ResourceMimeType = "text/html;profile=mcp-app";

/// <summary>
/// The extension identifier used for MCP Apps capability negotiation.
/// </summary>
/// <remarks>
/// This key is used in the <see cref="ClientCapabilities.Extensions"/> and
/// <see cref="ServerCapabilities.Extensions"/> dictionaries to advertise support for
/// the MCP Apps extension.
/// </remarks>
public const string ExtensionId = "io.modelcontextprotocol/ui";

/// <summary>
/// The legacy flat <c>_meta</c> key for the UI resource URI.
/// </summary>
/// <remarks>
/// <para>
/// This key is used for backward compatibility with older MCP hosts that do not support
/// the nested <c>_meta.ui</c> object. When populating UI metadata, both this key and the
/// <c>ui</c> object should be set to the same resource URI value.
/// </para>
/// <para>
/// This key is considered legacy; prefer <see cref="McpUiToolMeta.ResourceUri"/> for new implementations.
/// </para>
/// </remarks>
public const string ResourceUriMetaKey = "ui/resourceUri";

/// <summary>
/// Gets the MCP Apps client capability, if advertised by the connected client.
/// </summary>
/// <param name="capabilities">The client capabilities received during the MCP initialize handshake.</param>
/// <returns>
/// A <see cref="McpUiClientCapabilities"/> instance if the client advertises support for the MCP Apps extension;
/// otherwise, <see langword="null"/>.
/// </returns>
/// <remarks>
/// Use this method to determine whether the connected client supports the MCP Apps extension
/// and to read the client's supported MIME types.
/// </remarks>
[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;
}

/// <summary>
/// Applies UI tool metadata to a <see cref="System.Text.Json.Nodes.JsonObject"/>, setting both the
/// <c>ui</c> object key and the legacy <c>ui/resourceUri</c> flat key for backward compatibility.
/// Keys already present in <paramref name="meta"/> are not overwritten.
/// </summary>
/// <param name="appUi">The UI tool metadata to apply.</param>
/// <param name="meta">The <see cref="System.Text.Json.Nodes.JsonObject"/> to populate.</param>
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;
}
}
}
31 changes: 31 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,36 @@ public sealed class McpServerToolCreateOptions
/// </remarks>
public JsonObject? Meta { get; set; }

/// <summary>
/// Gets or sets the MCP Apps UI metadata for this tool.
/// </summary>
/// <remarks>
/// <para>
/// When set, this metadata is merged into <see cref="Meta"/> during tool creation, populating
/// both the structured <c>_meta.ui</c> object and the legacy <c>_meta["ui/resourceUri"]</c>
/// flat key for backward compatibility with older MCP hosts.
/// </para>
/// <para>
/// Explicit entries already present in <see cref="Meta"/> take precedence over values from
/// this property. The <see cref="McpAppUiAttribute"/> on a method overrides this property
/// when both are specified.
/// </para>
/// </remarks>
/// <example>
/// <code language="csharp">
/// var tool = McpServerTool.Create(handler, new McpServerToolCreateOptions
/// {
/// AppUi = new McpUiToolMeta
/// {
/// ResourceUri = "ui://weather/view.html",
/// Visibility = [McpUiToolVisibility.Model, McpUiToolVisibility.App]
/// }
/// });
/// </code>
/// </example>
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
public McpUiToolMeta? AppUi { get; set; }

/// <summary>
/// Gets or sets the execution hints for this tool.
/// </summary>
Expand Down Expand Up @@ -235,6 +265,7 @@ internal McpServerToolCreateOptions Clone() =>
Metadata = Metadata,
Icons = Icons,
Meta = Meta,
AppUi = AppUi,
Execution = Execution,
};
}
30 changes: 30 additions & 0 deletions src/ModelContextProtocol.Core/Server/McpUiClientCapabilities.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace ModelContextProtocol.Server;

/// <summary>
/// Represents the MCP Apps capabilities advertised by a client.
/// </summary>
/// <remarks>
/// <para>
/// This object is the value associated with the <see cref="McpApps.ExtensionId"/> key in the
/// <see cref="Protocol.ClientCapabilities.Extensions"/> dictionary.
/// </para>
/// <para>
/// Use <see cref="McpApps.GetUiCapability"/> to read this from <see cref="Protocol.ClientCapabilities"/>.
/// </para>
/// </remarks>
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
public sealed class McpUiClientCapabilities
{
/// <summary>
/// Gets or sets the list of MIME types supported by the client for MCP App UI resources.
/// </summary>
/// <remarks>
/// A client that supports MCP Apps must include <see cref="McpApps.ResourceMimeType"/>
/// (<c>"text/html;profile=mcp-app"</c>) in this list.
/// </remarks>
[JsonPropertyName("mimeTypes")]
public IList<string>? MimeTypes { get; set; }
}
Loading
Loading