Skip to content

Commit 35e7e59

Browse files
Copilotmikekistler
andcommitted
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>
1 parent bb32d0e commit 35e7e59

14 files changed

Lines changed: 893 additions & 4 deletions

docs/list-of-diagnostics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ If you use experimental APIs, you will get one of the diagnostics shown below. T
2323

2424
| Diagnostic ID | Description |
2525
| :------------ | :---------- |
26-
| `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)). |
26+
| `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)). |
2727
| `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). |
2828

2929
## Obsolete APIs

src/Common/Experimentals.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,25 @@ internal static class Experimentals
7171
/// </summary>
7272
public const string Extensions_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
7373

74+
/// <summary>
75+
/// Diagnostic ID for experimental MCP Apps extension APIs.
76+
/// </summary>
77+
/// <remarks>
78+
/// This uses the same diagnostic ID as <see cref="Extensions_DiagnosticId"/> because
79+
/// MCP Apps is implemented as an MCP extension (<c>"io.modelcontextprotocol/ui"</c>).
80+
/// </remarks>
81+
public const string Apps_DiagnosticId = "MCPEXP001";
82+
83+
/// <summary>
84+
/// Message for the experimental MCP Apps extension APIs.
85+
/// </summary>
86+
public const string Apps_Message = "The MCP Apps extension is experimental and subject to change as the specification evolves.";
87+
88+
/// <summary>
89+
/// URL for the experimental MCP Apps extension APIs.
90+
/// </summary>
91+
public const string Apps_Url = "https://github.com/modelcontextprotocol/csharp-sdk/blob/main/docs/list-of-diagnostics.md#mcpexp001";
92+
7493
/// <summary>
7594
/// Diagnostic ID for experimental SDK APIs unrelated to the MCP specification,
7695
/// such as subclassing <c>McpClient</c>/<c>McpServer</c> or referencing <c>RunSessionHandler</c>.

src/ModelContextProtocol.Core/McpJsonUtilities.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,13 @@ internal static bool IsValidMcpToolSchema(JsonElement element)
187187
[JsonSerializable(typeof(DynamicClientRegistrationRequest))]
188188
[JsonSerializable(typeof(DynamicClientRegistrationResponse))]
189189

190+
// MCP Apps extension types
191+
[JsonSerializable(typeof(Server.McpUiToolMeta))]
192+
[JsonSerializable(typeof(Server.McpUiClientCapabilities))]
193+
[JsonSerializable(typeof(Server.McpUiResourceMeta))]
194+
[JsonSerializable(typeof(Server.McpUiResourceCsp))]
195+
[JsonSerializable(typeof(Server.McpUiResourcePermissions))]
196+
190197
// Primitive types for use in consuming AIFunctions
191198
[JsonSerializable(typeof(string))]
192199
[JsonSerializable(typeof(byte))]

src/ModelContextProtocol.Core/Server/AIFunctionMcpServerTool.cs

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,21 @@ options.OpenWorld is not null ||
144144
};
145145
}
146146

147-
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available
147+
// Populate Meta from options and/or McpMetaAttribute instances if a MethodInfo is available.
148+
// Priority order (highest to lowest):
149+
// 1. Explicit options.Meta entries
150+
// 2. AppUi metadata (from McpAppUiAttribute or McpServerToolCreateOptions.AppUi)
151+
// 3. McpMetaAttribute entries on the method
152+
JsonObject? seededMeta = options.Meta;
153+
if (options.AppUi is { } appUi)
154+
{
155+
seededMeta = seededMeta is not null ? CloneJsonObject(seededMeta) : new JsonObject();
156+
McpApps.ApplyUiToolMetaToJsonObject(appUi, seededMeta);
157+
}
158+
148159
tool.Meta = function.UnderlyingMethod is not null ?
149-
CreateMetaFromAttributes(function.UnderlyingMethod, options.Meta) :
150-
options.Meta;
160+
CreateMetaFromAttributes(function.UnderlyingMethod, seededMeta) :
161+
seededMeta;
151162

152163
// Apply user-specified Execution settings if provided
153164
if (options.Execution is not null)
@@ -225,6 +236,16 @@ private static McpServerToolCreateOptions DeriveOptions(MethodInfo method, McpSe
225236
newOptions.Description ??= descAttr.Description;
226237
}
227238

239+
// Process McpAppUiAttribute — takes precedence over options.AppUi set via constructor.
240+
if (method.GetCustomAttribute<McpAppUiAttribute>() is { } appUiAttr)
241+
{
242+
newOptions.AppUi = new McpUiToolMeta
243+
{
244+
ResourceUri = appUiAttr.ResourceUri,
245+
Visibility = appUiAttr.Visibility,
246+
};
247+
}
248+
228249
// Set metadata if not already provided
229250
newOptions.Metadata ??= CreateMetadata(method);
230251

@@ -405,6 +426,18 @@ internal static IReadOnlyList<object> CreateMetadata(MethodInfo method)
405426
return meta;
406427
}
407428

429+
/// <summary>Creates a shallow-content clone of a <see cref="JsonObject"/> so that keys can be added without mutating the original.</summary>
430+
private static JsonObject CloneJsonObject(JsonObject source)
431+
{
432+
var clone = new JsonObject();
433+
foreach (var kvp in source)
434+
{
435+
// DeepClone each value to avoid sharing nodes between two JsonObject instances.
436+
clone[kvp.Key] = kvp.Value?.DeepClone();
437+
}
438+
return clone;
439+
}
440+
408441
#if NET
409442
/// <summary>Regex that flags runs of characters other than ASCII digits or letters.</summary>
410443
[GeneratedRegex("[^0-9A-Za-z]+")]
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace ModelContextProtocol.Server;
4+
5+
/// <summary>
6+
/// Specifies MCP Apps UI metadata for a tool method.
7+
/// </summary>
8+
/// <remarks>
9+
/// <para>
10+
/// Apply this attribute alongside <see cref="McpServerToolAttribute"/> to associate a tool with a
11+
/// UI resource in the MCP Apps extension. When processed, it populates both the structured
12+
/// <c>_meta.ui</c> object and the legacy <c>_meta["ui/resourceUri"]</c> flat key in the tool's
13+
/// metadata for backward compatibility with older MCP hosts.
14+
/// </para>
15+
/// <para>
16+
/// This attribute takes precedence over any raw <c>[McpMeta("ui", ...)]</c> attribute on the
17+
/// same method.
18+
/// </para>
19+
/// </remarks>
20+
/// <example>
21+
/// <code language="csharp">
22+
/// [McpServerTool]
23+
/// [McpAppUi(ResourceUri = "ui://weather/view.html")]
24+
/// [Description("Get current weather for a location")]
25+
/// public string GetWeather(string location) => ...;
26+
///
27+
/// // Restrict visibility to model only:
28+
/// [McpServerTool]
29+
/// [McpAppUi(ResourceUri = "ui://weather/view.html", Visibility = [McpUiToolVisibility.Model])]
30+
/// public string GetWeatherModelOnly(string location) => ...;
31+
/// </code>
32+
/// </example>
33+
[AttributeUsage(AttributeTargets.Method)]
34+
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
35+
public sealed class McpAppUiAttribute : Attribute
36+
{
37+
/// <summary>
38+
/// Gets or sets the URI of the UI resource associated with this tool.
39+
/// </summary>
40+
/// <remarks>
41+
/// This should be a <c>ui://</c> URI pointing to the HTML resource registered
42+
/// with the server (e.g., <c>"ui://weather/view.html"</c>).
43+
/// </remarks>
44+
public string? ResourceUri { get; set; }
45+
46+
/// <summary>
47+
/// Gets or sets the visibility of the tool, controlling which principals can invoke it.
48+
/// </summary>
49+
/// <remarks>
50+
/// <para>
51+
/// Allowed values are <see cref="McpUiToolVisibility.Model"/> and <see cref="McpUiToolVisibility.App"/>.
52+
/// When <see langword="null"/> or empty, the tool is visible to both the model and the app (the default).
53+
/// </para>
54+
/// </remarks>
55+
public string[]? Visibility { get; set; }
56+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using ModelContextProtocol.Protocol;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Text.Json;
4+
5+
namespace ModelContextProtocol.Server;
6+
7+
/// <summary>
8+
/// Provides constants and helper methods for building MCP Apps-enabled servers.
9+
/// </summary>
10+
/// <remarks>
11+
/// <para>
12+
/// MCP Apps is an extension to the Model Context Protocol that enables MCP servers to deliver
13+
/// interactive user interfaces — dashboards, forms, visualizations, and more — directly inside
14+
/// conversational AI clients.
15+
/// </para>
16+
/// <para>
17+
/// Use the constants in this class when populating the <c>extensions</c> capability and the
18+
/// <c>_meta</c> field of tools and resources. Use <see cref="GetUiCapability"/> to check whether
19+
/// the connected client supports the MCP Apps extension.
20+
/// </para>
21+
/// </remarks>
22+
public static class McpApps
23+
{
24+
/// <summary>
25+
/// The MIME type used for MCP App HTML resources.
26+
/// </summary>
27+
/// <remarks>
28+
/// This MIME type should be used when registering UI resources with
29+
/// <c>text/html;profile=mcp-app</c> to indicate they are MCP App resources.
30+
/// </remarks>
31+
public const string ResourceMimeType = "text/html;profile=mcp-app";
32+
33+
/// <summary>
34+
/// The extension identifier used for MCP Apps capability negotiation.
35+
/// </summary>
36+
/// <remarks>
37+
/// This key is used in the <see cref="ClientCapabilities.Extensions"/> and
38+
/// <see cref="ServerCapabilities.Extensions"/> dictionaries to advertise support for
39+
/// the MCP Apps extension.
40+
/// </remarks>
41+
public const string ExtensionId = "io.modelcontextprotocol/ui";
42+
43+
/// <summary>
44+
/// The legacy flat <c>_meta</c> key for the UI resource URI.
45+
/// </summary>
46+
/// <remarks>
47+
/// <para>
48+
/// This key is used for backward compatibility with older MCP hosts that do not support
49+
/// the nested <c>_meta.ui</c> object. When populating UI metadata, both this key and the
50+
/// <c>ui</c> object should be set to the same resource URI value.
51+
/// </para>
52+
/// <para>
53+
/// This key is considered legacy; prefer <see cref="McpUiToolMeta.ResourceUri"/> for new implementations.
54+
/// </para>
55+
/// </remarks>
56+
public const string ResourceUriMetaKey = "ui/resourceUri";
57+
58+
/// <summary>
59+
/// Gets the MCP Apps client capability, if advertised by the connected client.
60+
/// </summary>
61+
/// <param name="capabilities">The client capabilities received during the MCP initialize handshake.</param>
62+
/// <returns>
63+
/// A <see cref="McpUiClientCapabilities"/> instance if the client advertises support for the MCP Apps extension;
64+
/// otherwise, <see langword="null"/>.
65+
/// </returns>
66+
/// <remarks>
67+
/// Use this method to determine whether the connected client supports the MCP Apps extension
68+
/// and to read the client's supported MIME types.
69+
/// </remarks>
70+
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
71+
public static McpUiClientCapabilities? GetUiCapability(ClientCapabilities? capabilities)
72+
{
73+
if (capabilities?.Extensions is not { } extensions ||
74+
!extensions.TryGetValue(ExtensionId, out var value))
75+
{
76+
return null;
77+
}
78+
79+
if (value is JsonElement element)
80+
{
81+
return element.ValueKind == JsonValueKind.Null ? null :
82+
JsonSerializer.Deserialize(element, McpJsonUtilities.JsonContext.Default.McpUiClientCapabilities);
83+
}
84+
85+
return null;
86+
}
87+
88+
/// <summary>
89+
/// Applies UI tool metadata to a <see cref="System.Text.Json.Nodes.JsonObject"/>, setting both the
90+
/// <c>ui</c> object key and the legacy <c>ui/resourceUri</c> flat key for backward compatibility.
91+
/// Keys already present in <paramref name="meta"/> are not overwritten.
92+
/// </summary>
93+
/// <param name="appUi">The UI tool metadata to apply.</param>
94+
/// <param name="meta">The <see cref="System.Text.Json.Nodes.JsonObject"/> to populate.</param>
95+
internal static void ApplyUiToolMetaToJsonObject(McpUiToolMeta appUi, System.Text.Json.Nodes.JsonObject meta)
96+
{
97+
// Populate the structured "ui" object if not already present.
98+
if (!meta.ContainsKey("ui"))
99+
{
100+
var uiNode = JsonSerializer.SerializeToNode(appUi, McpJsonUtilities.JsonContext.Default.McpUiToolMeta);
101+
if (uiNode is not null)
102+
{
103+
meta["ui"] = uiNode;
104+
}
105+
}
106+
107+
// Populate the legacy flat "ui/resourceUri" key if not already present.
108+
if (!meta.ContainsKey(ResourceUriMetaKey) && appUi.ResourceUri is not null)
109+
{
110+
meta[ResourceUriMetaKey] = appUi.ResourceUri;
111+
}
112+
}
113+
}

src/ModelContextProtocol.Core/Server/McpServerToolCreateOptions.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,36 @@ public sealed class McpServerToolCreateOptions
197197
/// </remarks>
198198
public JsonObject? Meta { get; set; }
199199

200+
/// <summary>
201+
/// Gets or sets the MCP Apps UI metadata for this tool.
202+
/// </summary>
203+
/// <remarks>
204+
/// <para>
205+
/// When set, this metadata is merged into <see cref="Meta"/> during tool creation, populating
206+
/// both the structured <c>_meta.ui</c> object and the legacy <c>_meta["ui/resourceUri"]</c>
207+
/// flat key for backward compatibility with older MCP hosts.
208+
/// </para>
209+
/// <para>
210+
/// Explicit entries already present in <see cref="Meta"/> take precedence over values from
211+
/// this property. The <see cref="McpAppUiAttribute"/> on a method overrides this property
212+
/// when both are specified.
213+
/// </para>
214+
/// </remarks>
215+
/// <example>
216+
/// <code language="csharp">
217+
/// var tool = McpServerTool.Create(handler, new McpServerToolCreateOptions
218+
/// {
219+
/// AppUi = new McpUiToolMeta
220+
/// {
221+
/// ResourceUri = "ui://weather/view.html",
222+
/// Visibility = [McpUiToolVisibility.Model, McpUiToolVisibility.App]
223+
/// }
224+
/// });
225+
/// </code>
226+
/// </example>
227+
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
228+
public McpUiToolMeta? AppUi { get; set; }
229+
200230
/// <summary>
201231
/// Gets or sets the execution hints for this tool.
202232
/// </summary>
@@ -235,6 +265,7 @@ internal McpServerToolCreateOptions Clone() =>
235265
Metadata = Metadata,
236266
Icons = Icons,
237267
Meta = Meta,
268+
AppUi = AppUi,
238269
Execution = Execution,
239270
};
240271
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Text.Json.Serialization;
3+
4+
namespace ModelContextProtocol.Server;
5+
6+
/// <summary>
7+
/// Represents the MCP Apps capabilities advertised by a client.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// This object is the value associated with the <see cref="McpApps.ExtensionId"/> key in the
12+
/// <see cref="Protocol.ClientCapabilities.Extensions"/> dictionary.
13+
/// </para>
14+
/// <para>
15+
/// Use <see cref="McpApps.GetUiCapability"/> to read this from <see cref="Protocol.ClientCapabilities"/>.
16+
/// </para>
17+
/// </remarks>
18+
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
19+
public sealed class McpUiClientCapabilities
20+
{
21+
/// <summary>
22+
/// Gets or sets the list of MIME types supported by the client for MCP App UI resources.
23+
/// </summary>
24+
/// <remarks>
25+
/// A client that supports MCP Apps must include <see cref="McpApps.ResourceMimeType"/>
26+
/// (<c>"text/html;profile=mcp-app"</c>) in this list.
27+
/// </remarks>
28+
[JsonPropertyName("mimeTypes")]
29+
public IList<string>? MimeTypes { get; set; }
30+
}

0 commit comments

Comments
 (0)