Skip to content

Commit 95bcd37

Browse files
committed
feat: add MCP Apps resource attribute
1 parent 514cf68 commit 95bcd37

8 files changed

Lines changed: 325 additions & 14 deletions

File tree

docs/concepts/apps/apps.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ The key concepts are:
3131
- **UI capability negotiation** — Client and server declare support via `extensions["io.modelcontextprotocol/ui"]`
3232
- **UI resources** — HTML content served with the MIME type `text/html;profile=mcp-app`
3333
- **Tool UI metadata** — Tools declare their associated UI resource in `_meta.ui`
34+
- **Resource UI metadata** — Resources declare CSP, permissions, domain, and display preferences in `_meta.ui`
3435

3536
## Associating tools with UI resources
3637

@@ -58,7 +59,7 @@ builder.Services.AddMcpServer()
5859
.WithMcpApps();
5960
```
6061

61-
The `WithMcpApps()` call registers a post-configuration step that processes all registered tools and applies `[McpAppUi]` attribute metadata to their `_meta.ui` field automatically.
62+
The `WithMcpApps()` call registers a post-configuration step that processes registered tools and resources, applying their MCP Apps attributes to the corresponding `_meta.ui` fields automatically.
6263

6364
### Using the attribute with manual processing
6465

@@ -126,6 +127,34 @@ UI resources are HTML pages registered with the MCP server using the `ui://` URI
126127
- **Domain** — Dedicated origin for OAuth flows and CORS
127128
- **PrefersBorder** — Whether the host should render a visual border
128129

130+
Use `[McpAppResource]` to configure this metadata without writing raw JSON:
131+
132+
```csharp
133+
[McpServerResourceType]
134+
public static class WeatherResources
135+
{
136+
[McpServerResource(
137+
UriTemplate = "ui://weather/view.html",
138+
Name = "weather-ui",
139+
MimeType = McpApps.HtmlMimeType)]
140+
[McpAppResource(
141+
ConnectDomains = ["https://api.weather.gov"],
142+
Permissions = ["geolocation"],
143+
PrefersBorder = true)]
144+
public static string GetWeatherUi() => File.ReadAllText("weather.html");
145+
}
146+
```
147+
148+
Register the resource before calling `WithMcpApps()`:
149+
150+
```csharp
151+
builder.Services.AddMcpServer()
152+
.WithResources<WeatherResources>()
153+
.WithMcpApps();
154+
```
155+
156+
For resources created manually, call `McpApps.ApplyAppResourceAttributes(resource)`, or set a typed `McpUiResourceMeta` directly with `McpApps.SetResourceUi(resource, metadata)`.
157+
129158
## App-only tools
130159

131160
Tools with `Visibility = [McpUiToolVisibility.App]` are not visible to the LLM — they are intended only for use by the app UI.

samples/WeatherAppServer/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ An MCP server that demonstrates the **MCP Apps** extension by serving an interac
55
## What it shows
66

77
- **`[McpAppUi]` attribute** — declaratively associates a UI resource with a tool
8-
- **`WithMcpApps()`** — builder extension that processes `[McpAppUi]` attributes
8+
- **`[McpAppResource]` attribute** — configures typed CSP and display metadata for a UI resource
9+
- **`WithMcpApps()`** — builder extension that processes both MCP Apps attributes
910
- **UI resource** — an HTML page served via `McpServerResource` with MIME type `text/html;profile=mcp-app`
1011
- **Structured content** — tool results include `StructuredContent` for the UI to render
1112

samples/WeatherAppServer/WeatherResources.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public sealed class WeatherResources
1010
private static readonly string UiDir = Path.Combine(AppContext.BaseDirectory, "ui");
1111

1212
[McpServerResource(UriTemplate = "ui://weather-app/forecast", Name = "weather-forecast-ui", MimeType = McpApps.HtmlMimeType)]
13-
[McpMeta("ui", """{"csp":{"connectDomains":["https://api.weather.gov"]},"prefersBorder":true}""")]
13+
[McpAppResource(ConnectDomains = ["https://api.weather.gov"], PrefersBorder = true)]
1414
[Description("Interactive weather forecast UI with city picker")]
1515
public static string GetWeatherForecastUi() => File.ReadAllText(Path.Combine(UiDir, "weather-forecast.html"));
1616

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using ModelContextProtocol.Server;
2+
using System.Diagnostics.CodeAnalysis;
3+
4+
namespace ModelContextProtocol.Extensions.Apps;
5+
6+
/// <summary>
7+
/// Specifies MCP Apps UI metadata for a resource method.
8+
/// </summary>
9+
/// <remarks>
10+
/// <para>
11+
/// Apply this attribute alongside <see cref="McpServerResourceAttribute"/> to configure the
12+
/// Content Security Policy, sandbox permissions, domain, and visual preferences for an MCP App
13+
/// resource. When processed by <see cref="McpApps.ApplyAppResourceAttributes(McpServerResource)"/>,
14+
/// it populates the structured <c>_meta.ui</c> object in the resource's metadata.
15+
/// </para>
16+
/// <para>
17+
/// Explicit <c>Meta["ui"]</c> set via <see cref="McpServerResourceCreateOptions"/> or a raw
18+
/// <c>[McpMeta("ui", ...)]</c> attribute takes precedence over this attribute.
19+
/// </para>
20+
/// </remarks>
21+
/// <example>
22+
/// <code language="csharp">
23+
/// [McpServerResource(UriTemplate = "ui://weather/view.html", MimeType = McpApps.HtmlMimeType)]
24+
/// [McpAppResource(ConnectDomains = ["https://api.weather.gov"], PrefersBorder = true)]
25+
/// public static string GetWeatherUi() =&gt; ...;
26+
/// </code>
27+
/// </example>
28+
[AttributeUsage(AttributeTargets.Method)]
29+
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
30+
public sealed class McpAppResourceAttribute : Attribute
31+
{
32+
private bool? _prefersBorder;
33+
34+
/// <summary>Gets or sets origins allowed for network connections.</summary>
35+
public string[]? ConnectDomains { get; set; }
36+
37+
/// <summary>Gets or sets origins allowed for scripts, stylesheets, images, and fonts.</summary>
38+
public string[]? ResourceDomains { get; set; }
39+
40+
/// <summary>Gets or sets origins allowed for nested frames.</summary>
41+
public string[]? FrameDomains { get; set; }
42+
43+
/// <summary>Gets or sets allowed base URIs.</summary>
44+
public string[]? BaseUris { get; set; }
45+
46+
/// <summary>Gets or sets browser permissions requested by the sandboxed resource.</summary>
47+
public string[]? Permissions { get; set; }
48+
49+
/// <summary>Gets or sets the dedicated origin domain for this resource.</summary>
50+
public string? Domain { get; set; }
51+
52+
/// <summary>Gets or sets whether the host should render a visual border around the UI.</summary>
53+
public bool PrefersBorder
54+
{
55+
get => _prefersBorder.GetValueOrDefault();
56+
set => _prefersBorder = value;
57+
}
58+
59+
internal bool? PrefersBorderValue => _prefersBorder;
60+
}

src/ModelContextProtocol.Extensions.Apps/Server/McpApps.cs

Lines changed: 98 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ namespace ModelContextProtocol.Extensions.Apps;
2121
/// the connected client supports the MCP Apps extension.
2222
/// </para>
2323
/// <para>
24-
/// Use <see cref="SetAppUi"/> to set the <c>_meta.ui</c> metadata on a tool, or
24+
/// Use <see cref="SetAppUi"/> and <see cref="SetResourceUi"/> to set the <c>_meta.ui</c> metadata, or
2525
/// <see cref="ApplyAppUiAttributes(IEnumerable{McpServerTool})"/> to automatically process
26-
/// <see cref="McpAppUiAttribute"/> instances on tools created from methods.
26+
/// <see cref="McpAppUiAttribute"/> instances on tools created from methods. Use
27+
/// <see cref="ApplyAppResourceAttributes(IEnumerable{McpServerResource})"/> to process
28+
/// <see cref="McpAppResourceAttribute"/> instances on resources.
2729
/// </para>
2830
/// </remarks>
2931
[Experimental(Experimentals.Apps_DiagnosticId, UrlFormat = Experimentals.Apps_Url)]
@@ -153,15 +155,15 @@ public static McpServerTool SetAppUi(McpServerTool tool, McpUiToolMeta appUi)
153155
}
154156

155157
/// <summary>
156-
/// Sets the MCP Apps UI metadata on a resource's <see cref="ResourceTemplate.Meta"/> property.
158+
/// Sets the MCP Apps UI metadata on a resource's protocol metadata.
157159
/// </summary>
158160
/// <param name="resource">The resource to set the UI metadata on.</param>
159161
/// <param name="resourceUi">The UI metadata to apply.</param>
160162
/// <returns>The same <paramref name="resource"/> instance, for chaining.</returns>
161163
/// <remarks>
162164
/// <para>
163-
/// This method sets the <c>ui</c> key in the resource's <see cref="ResourceTemplate.Meta"/> object.
164-
/// If a <c>ui</c> key is already present in <see cref="ResourceTemplate.Meta"/>, it is not overwritten.
165+
/// This method sets the <c>ui</c> key in the resource's <see cref="ResourceTemplate.Meta"/> object and,
166+
/// for non-templated resources, its <see cref="Resource.Meta"/> object. Existing <c>ui</c> metadata is not overwritten.
165167
/// </para>
166168
/// </remarks>
167169
/// <exception cref="ArgumentNullException"><paramref name="resource"/> or <paramref name="resourceUi"/> is <see langword="null"/>.</exception>
@@ -175,15 +177,102 @@ public static McpServerResource SetResourceUi(McpServerResource resource, McpUiR
175177
if (resourceUi is null) throw new ArgumentNullException(nameof(resourceUi));
176178
#endif
177179

178-
var protocolResource = resource.ProtocolResourceTemplate;
179-
protocolResource.Meta ??= new JsonObject();
180+
var protocolResourceTemplate = resource.ProtocolResourceTemplate;
181+
protocolResourceTemplate.Meta ??= new JsonObject();
182+
var protocolResource = resource.ProtocolResource;
180183

181-
if (!protocolResource.Meta.ContainsKey("ui"))
184+
if (!protocolResourceTemplate.Meta.ContainsKey("ui") && protocolResource?.Meta?.ContainsKey("ui") != true)
182185
{
183186
var uiNode = JsonSerializer.SerializeToNode(resourceUi, McpAppsJsonContext.Default.McpUiResourceMeta);
184187
if (uiNode is not null)
185188
{
186-
protocolResource.Meta["ui"] = uiNode;
189+
protocolResourceTemplate.Meta["ui"] = uiNode;
190+
}
191+
}
192+
193+
if (protocolResource is not null)
194+
{
195+
protocolResource.Meta ??= protocolResourceTemplate.Meta;
196+
if (!protocolResource.Meta.ContainsKey("ui") && protocolResourceTemplate.Meta["ui"] is { } uiNode)
197+
{
198+
protocolResource.Meta["ui"] = uiNode.DeepClone();
199+
}
200+
}
201+
202+
return resource;
203+
}
204+
205+
/// <summary>
206+
/// Processes a collection of resources, applying <see cref="McpAppResourceAttribute"/> metadata to any
207+
/// resource whose underlying method has the attribute.
208+
/// </summary>
209+
/// <param name="resources">The resources to process.</param>
210+
/// <returns>The same <paramref name="resources"/> enumerable, for chaining.</returns>
211+
/// <exception cref="ArgumentNullException"><paramref name="resources"/> is <see langword="null"/>.</exception>
212+
public static IEnumerable<McpServerResource> ApplyAppResourceAttributes(IEnumerable<McpServerResource> resources)
213+
{
214+
#if NET
215+
ArgumentNullException.ThrowIfNull(resources);
216+
#else
217+
if (resources is null) throw new ArgumentNullException(nameof(resources));
218+
#endif
219+
220+
foreach (var resource in resources)
221+
{
222+
ApplyAppResourceAttributes(resource);
223+
}
224+
225+
return resources;
226+
}
227+
228+
/// <summary>
229+
/// Processes a single resource, applying <see cref="McpAppResourceAttribute"/> metadata if the resource's
230+
/// underlying method has the attribute.
231+
/// </summary>
232+
/// <param name="resource">The resource to process.</param>
233+
/// <returns>The same <paramref name="resource"/> instance, for chaining.</returns>
234+
/// <remarks>
235+
/// Existing <c>_meta.ui</c> metadata is preserved and takes precedence over the attribute.
236+
/// </remarks>
237+
/// <exception cref="ArgumentNullException"><paramref name="resource"/> is <see langword="null"/>.</exception>
238+
public static McpServerResource ApplyAppResourceAttributes(McpServerResource resource)
239+
{
240+
#if NET
241+
ArgumentNullException.ThrowIfNull(resource);
242+
#else
243+
if (resource is null) throw new ArgumentNullException(nameof(resource));
244+
#endif
245+
246+
foreach (var metadataItem in resource.Metadata)
247+
{
248+
if (metadataItem is McpAppResourceAttribute appResourceAttribute)
249+
{
250+
McpUiResourceCsp? csp = null;
251+
if (appResourceAttribute.ConnectDomains is not null ||
252+
appResourceAttribute.ResourceDomains is not null ||
253+
appResourceAttribute.FrameDomains is not null ||
254+
appResourceAttribute.BaseUris is not null)
255+
{
256+
csp = new McpUiResourceCsp
257+
{
258+
ConnectDomains = appResourceAttribute.ConnectDomains,
259+
ResourceDomains = appResourceAttribute.ResourceDomains,
260+
FrameDomains = appResourceAttribute.FrameDomains,
261+
BaseUris = appResourceAttribute.BaseUris,
262+
};
263+
}
264+
265+
SetResourceUi(resource, new McpUiResourceMeta
266+
{
267+
Csp = csp,
268+
Permissions = appResourceAttribute.Permissions is null ? null : new McpUiResourcePermissions
269+
{
270+
Allow = appResourceAttribute.Permissions,
271+
},
272+
Domain = appResourceAttribute.Domain,
273+
PrefersBorder = appResourceAttribute.PrefersBorderValue,
274+
});
275+
break;
187276
}
188277
}
189278

src/ModelContextProtocol.Extensions.Apps/Server/McpAppsBuilderExtensions.cs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ namespace ModelContextProtocol.Extensions.Apps;
1313
public static class McpAppsBuilderExtensions
1414
{
1515
/// <summary>
16-
/// Enables MCP Apps support by automatically processing <see cref="McpAppUiAttribute"/> on registered tools.
16+
/// Enables MCP Apps support by automatically processing MCP Apps attributes on registered tools and resources.
1717
/// </summary>
1818
/// <param name="builder">The server builder.</param>
1919
/// <returns>The builder provided in <paramref name="builder"/>.</returns>
2020
/// <remarks>
2121
/// <para>
2222
/// Call this method after registering tools (e.g., after <c>WithTools&lt;T&gt;()</c>) to automatically
23-
/// apply <see cref="McpAppUiAttribute"/> metadata to the tool's <c>_meta.ui</c> field.
23+
/// apply <see cref="McpAppUiAttribute"/> and <see cref="McpAppResourceAttribute"/> metadata to the
24+
/// corresponding <c>_meta.ui</c> fields.
2425
/// </para>
2526
/// <para>
2627
/// Tools that already have a <c>ui</c> key in their <see cref="Protocol.Tool.Meta"/> (e.g., set explicitly
@@ -66,6 +67,14 @@ public void PostConfigure(string? name, McpServerOptions options)
6667
McpApps.ApplyAppUiAttributes(tool);
6768
}
6869
}
70+
71+
if (options.ResourceCollection is { IsEmpty: false } resources)
72+
{
73+
foreach (var resource in resources)
74+
{
75+
McpApps.ApplyAppResourceAttributes(resource);
76+
}
77+
}
6978
}
7079
}
7180
}

tests/ModelContextProtocol.AotCompatibility.TestApp/Program.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
services.AddMcpServer()
1212
.WithStreamServerTransport(clientToServerPipe.Reader.AsStream(), serverToClientPipe.Writer.AsStream())
1313
.WithTools<AotTools>()
14+
.WithResources<AotResources>()
1415
.WithMcpApps();
1516

1617
await using var serviceProvider = services.BuildServiceProvider();
@@ -41,6 +42,15 @@
4142
throw new Exception($"Unexpected result: {result}");
4243
}
4344

45+
var resources = await client.ListResourcesAsync();
46+
var resource = resources.FirstOrDefault(r => r.Uri == "ui://aot/echo");
47+
var resourceUi = resource?.ProtocolResource.Meta?["ui"]?.AsObject();
48+
if (resourceUi?["csp"]?["connectDomains"]?[0]?.GetValue<string>() != "https://api.example.com" ||
49+
resourceUi["prefersBorder"]?.GetValue<bool>() != true)
50+
{
51+
throw new Exception($"Unexpected app resource UI metadata: {resourceUi}");
52+
}
53+
4454
Console.WriteLine("Success!");
4555

4656
[McpServerToolType]
@@ -50,3 +60,11 @@ internal sealed class AotTools
5060
[McpAppUi(ResourceUri = "ui://aot/echo")]
5161
public static string Echo(string arg) => $"Echo: {arg}";
5262
}
63+
64+
[McpServerResourceType]
65+
internal sealed class AotResources
66+
{
67+
[McpServerResource(UriTemplate = "ui://aot/echo", Name = "echo-ui", MimeType = McpApps.HtmlMimeType)]
68+
[McpAppResource(ConnectDomains = ["https://api.example.com"], PrefersBorder = true)]
69+
public static string EchoUi() => "<html></html>";
70+
}

0 commit comments

Comments
 (0)