Add MCP Apps extension support (typed metadata, attribute, and helpers)#1484
Add MCP Apps extension support (typed metadata, attribute, and helpers)#1484Copilot wants to merge 8 commits into
Conversation
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>
|
Any idea when this will be merged? Would love to see this feature in the SDK. |
|
This is a key capability for "enterprises", emerging and especially those who are already on the MCP train wanting to deliver UI Apps within convo experiences. |
|
Can you please merge this 🙏 |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
👀 Tracking this closely - and will adopt this surface as soon as it ships. The tool-side [McpAppUi] and McpServerToolCreateOptions.AppUi look great. One observation that may be worth addressing in this PR or a fast follow-up: the typed McpUiResourceMeta / McpUiResourceCsp models added to Core aren't reachable via resource registration — only via raw JsonObject on TextResourceContents.Meta. Since per-resource CSP is mandatory for any production MCP App, this leaves the most failure-prone part of the API in raw-JSON territory. Would be great to get a typed path for resources too, mirroring the tool-side ergonomics. Filed as a comment on #1431 with more detail. |
| /// 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"; |
There was a problem hiding this comment.
I think this name is too generic. The MCP Apps spec says "The initial specification focuses on HTML resources (text/html;profile=mcp-app) with a clear path for future extensions." To avoid confusion with potential future resource types, I think we should call this "HtmlMimeType".
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
It is so great to see this PR! Really looking forward to use this in our product! |
|
+1 for code review and merging this. Will be super useful to have the attribute instead of band-aid code |
|
any updates on this? |
| } | ||
|
|
||
| return tool; | ||
| } |
There was a problem hiding this comment.
There's a tool-side SetAppUi + [McpAppUi] attribute + ApplyAppUiAttributes pipeline, but no equivalent for resources. The types exist (McpUiResourceMeta, McpUiResourceCsp, McpUiResourcePermissions) but there's no SetResourceUi(McpServerResource, McpUiResourceMeta), no [McpAppResource] attribute, and WithMcpApps() doesn't process the resource collection.
Server authors who want to set CSP, permissions, domain, or prefersBorder on their UI resources have to hand-craft the _meta.ui JSON themselves. The sample WeatherResources demonstrates exactly this gap — it only sets MimeType and never gets to apply any of the CSP/permission types this package defines.
| JsonSerializer.Deserialize(element, McpAppsJsonContext.Default.McpUiClientCapabilities); | ||
| } | ||
|
|
||
| return null; |
There was a problem hiding this comment.
GetUiCapability only handles the case where value is JsonElement. If a server-side caller populated Extensions[ExtensionId] with a strongly typed McpUiClientCapabilities object (e.g. in tests, or via a non-JSON code path), this returns null silently. Worth handling that case too, or at least documenting that values are expected to be JsonElement from the deserialized handshake.
Also, no test covers a malformed value (e.g. JsonElement of kind String or Number) — currently that would throw a JsonException rather than returning null like the other failure modes.
|
|
||
| var weatherTool = toolsWithUi.First(t => t.ProtocolTool.Meta!["ui"]!["resourceUri"]?.GetValue<string>() == "ui://weather/view.html"); | ||
| Assert.NotNull(weatherTool); | ||
| } |
There was a problem hiding this comment.
A few test gaps worth filling:
WithMcpApps()with an emptyToolCollection(no tools registered) — should be a no-op, not throw.GetUiCapabilitywith aJsonElementof kindString/Number(malformed but present) — currently this would throwJsonExceptioninstead of returningnulllike the other negative cases.- No round-trip integration test that goes through
McpClient.ListToolsAsync()and verifies_meta.uisurvives serialization end-to-end (e.g. viaClientServerTestBase). All current tests are pure unit tests on the option pipeline. - No test for resource-side metadata flow (related to my other comment about the missing resource-side helpers).
| /// .WithTools<MyToolType>() | ||
| /// .WithMcpApps(); | ||
| /// </code> | ||
| /// </example> |
There was a problem hiding this comment.
Ergonomics suggestion (worth considering as a follow-up): the TypeScript @modelcontextprotocol/ext-apps package exposes a registerAppTool(...) helper that bundles tool registration + UI resource linkage in one call. Today a C# user has to do WithTools<T>() + WithResources<T>() + WithMcpApps() + [McpServerTool] + [McpAppUi] + a matching [McpServerResource] with the right MIME type. A combined WithAppTool(method, resourceUri, htmlFactory) overload — or at least a doc section showing the "complete recipe" end-to-end — would cut the wiring boilerplate significantly.
| <Description>MCP Apps extension for the .NET Model Context Protocol (MCP) SDK</Description> | ||
| <PackageReadmeFile>README.md</PackageReadmeFile> | ||
| <!-- Suppress the experimental MCP Apps warning --> | ||
| <NoWarn>$(NoWarn);MCPEXP003</NoWarn> |
There was a problem hiding this comment.
Globally suppressing MCPEXP003 inside the package that defines the experimental APIs is fine, but worth double-checking this doesn't suppress the warning for consumers of the package. The [Experimental] attribute carries its own diagnostic ID into consuming projects independently of NoWarn here, so this should only affect intra-package usage — but a quick verification that dotnet build on a fresh consumer project still emits MCPEXP003 would be reassuring before merge.
| /// This attribute takes precedence over any raw <c>[McpMeta("ui", ...)]</c> attribute on the | ||
| /// same method, but explicit <c>Meta["ui"]</c> set via <see cref="McpServerToolCreateOptions"/> | ||
| /// takes precedence over this attribute. | ||
| /// </para> |
There was a problem hiding this comment.
This doc comment is backwards relative to the implementation. It says "This attribute takes precedence over any raw [McpMeta("ui", ...)]", but McpApps.ApplyAppUiAttributes (and SetAppUi) explicitly check Meta.ContainsKey("ui") and skip if present — so a pre-existing Meta["ui"] from [McpMeta] actually wins over this attribute, not the other way around.
Either fix the precedence (probably not what you want — explicit [McpMeta] overriding the typed attribute is the safer behavior) or fix the doc to say "explicit Meta["ui"] takes precedence over this attribute."
|
|
||
| [McpServerResource(UriTemplate = "ui://weather-app/forecast", Name = "weather-forecast-ui", MimeType = McpApps.ResourceMimeType)] | ||
| [Description("Interactive weather forecast UI with city picker")] | ||
| public static string GetWeatherForecastUi() => File.ReadAllText(Path.Combine(UiDir, "weather-forecast.html")); |
There was a problem hiding this comment.
This sample correctly sets the MCP Apps MIME type, but it never demonstrates McpUiResourceMeta (CSP allowlists, permissions, prefersBorder, domain) — which is a significant portion of the surface this package introduces. Since api.weather.gov is the obvious external origin this UI would want to talk to, this is an ideal place to show a real CSP connectDomains configuration. As written, the sample fully exercises the tool-side API but only the MIME-type constant on the resource side.
| - **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 |
There was a problem hiding this comment.
A few topics from the spec that would be worth covering here for parity with the TS/community SDK docs:
- Tool visibility for app-only tools — the
["app"]visibility pattern (tools the LLM never sees, only the iframe can call) is one of the most powerful patterns in MCP Apps and isn't called out as a use case. - Display modes (
inline,fullscreen,pip) — not mentioned anywhere, and there are no types for them. - Host theming via CSS variables — the spec defines ~80 standardized CSS custom properties (
--color-background-primaryetc.) that hosts pass to apps. Worth at least linking to. - Graceful degradation — show a snippet where the tool returns a text-only
CallToolResultwhenGetUiCapabilityreturnsnull, so server authors know the recommended fallback pattern. - Single-file HTML bundling — the spec recommends bundling all JS/CSS into one HTML file (e.g. via
vite-plugin-singlefile) because the default CSP makes external assets painful. The sample uses inline HTML so this Just Works, but real apps will hit this.
|
|
||
| builder.Services.AddSingleton<IPostConfigureOptions<McpServerOptions>, McpAppsPostConfigureOptions>(); | ||
| return builder; | ||
| } |
There was a problem hiding this comment.
WithMcpApps() reads client capabilities (via GetUiCapability) but never advertises server-side support. The spec requires the server to include "io.modelcontextprotocol/ui": {} (or a capability object) in ServerCapabilities.Extensions during the initialize handshake — otherwise capability-aware clients can't detect that the server supports MCP Apps.
WithMcpApps() should also register a post-configure step that adds ExtensionId to options.Capabilities.Extensions. Right now consumers have to do this manually, which is easy to forget and not mentioned in the docs.
| /// </para> | ||
| /// <para> | ||
| /// This attribute takes precedence over any raw <c>[McpMeta("ui", ...)]</c> attribute on the | ||
| /// same method, but explicit <c>Meta["ui"]</c> set via <see cref="McpServerToolCreateOptions"/> | ||
| /// takes precedence over this attribute. | ||
| /// </para> |
There was a problem hiding this comment.
The doc comment's precedence claim is backwards relative to the code. ApplyAppUiAttributes checks Meta.ContainsKey("ui") and skips when present, so any explicit Meta["ui"] (whether from [McpMeta("ui", ...)] or McpServerToolCreateOptions.Meta) wins over this attribute — not the other way around.
| /// </para> | |
| /// <para> | |
| /// This attribute takes precedence over any raw <c>[McpMeta("ui", ...)]</c> attribute on the | |
| /// same method, but explicit <c>Meta["ui"]</c> set via <see cref="McpServerToolCreateOptions"/> | |
| /// takes precedence over this attribute. | |
| /// </para> | |
| /// <para> | |
| /// Explicit <c>Meta["ui"]</c> set via <see cref="McpServerToolCreateOptions"/> or a raw | |
| /// <c>[McpMeta("ui", ...)]</c> attribute takes precedence over this attribute: if the tool | |
| /// already has a <c>ui</c> key in <see cref="Protocol.Tool.Meta"/>, this attribute is ignored. | |
| /// </para> |
MCP Apps is the first official MCP extension (
io.modelcontextprotocol/ui), enabling servers to deliver interactive UIs inside AI clients. The C# SDK had the foundational primitives (Extensions,_meta) but no typed convenience layer, requiring manual JSON construction that is error-prone and non-discoverable.New Package:
ModelContextProtocol.Extensions.AppsA new package (
ModelContextProtocol.Extensions.Apps) houses all MCP Apps types and helpers. It referencesModelContextProtocol.Corefor access to server primitives.New APIs
Constants (
McpApps)McpApps.ResourceMimeType→"text/html;profile=mcp-app"McpApps.ExtensionId→"io.modelcontextprotocol/ui"McpApps.SerializerOptions→ pre-configuredJsonSerializerOptionsfor MCP Apps typesTyped metadata models
McpUiToolMeta–ResourceUri,VisibilityMcpUiToolVisibility–Model/Appstring constantsMcpUiResourceMeta–Csp,Permissions,Domain,PrefersBorderMcpUiResourceCsp–ConnectDomains,ResourceDomains,FrameDomains,BaseUrisMcpUiResourcePermissions–AllowMcpUiClientCapabilities–MimeTypesClient capability helper
[McpAppUi]attribute (declarative path)When processed by
McpApps.ApplyAppUiAttributes()(or theWithMcpApps()builder extension), this populates the structured_meta.uiobject in the tool's metadata.McpApps.SetAppUi(programmatic path)Explicit
Meta["ui"]entries (set viaMcpServerToolCreateOptions.Meta) take precedence overSetAppUi;SetAppUitakes precedence over[McpAppUi]attribute.WithMcpApps()builder extensionAutomatically processes
[McpAppUi]attributes on all registered tools viaIPostConfigureOptions<McpServerOptions>.Notes
[Experimental(MCPEXP003)]— a dedicated diagnostic ID for MCP Apps.McpAppsJsonContext.docs/concepts/apps/apps.md.