{"id":3698,"date":"2026-04-07T14:16:11","date_gmt":"2026-04-07T21:16:11","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/azure-sdk\/?p=3698"},"modified":"2026-04-28T06:33:35","modified_gmt":"2026-04-28T13:33:35","slug":"mcp-as-easy-as-1-2-3-introducing-the-fluent-api-for-mcp-apps","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/azure-sdk\/mcp-as-easy-as-1-2-3-introducing-the-fluent-api-for-mcp-apps\/","title":{"rendered":"MCP as Easy as 1-2-3: Introducing the Fluent API for MCP Apps"},"content":{"rendered":"<p>Earlier this year, we <a href=\"https:\/\/techcommunity.microsoft.com\/blog\/appsonazureblog\/building-mcp-apps-with-azure-functions-mcp-extension\/4496536\">introduced MCP Apps<\/a> in the Azure Functions MCP extension. These are tools that go beyond text and render full UI experiences, serve static assets, and integrate seamlessly with AI agents. If you haven&#8217;t tried them yet, the <a href=\"https:\/\/learn.microsoft.com\/azure\/azure-functions\/scenario-mcp-apps?tabs=bash%2Clinux&amp;pivots=programming-language-csharp\">MCP Apps quickstart<\/a> is a great place to start.<\/p>\n<p>Today, we&#8217;re making Model Context Protocol (MCP) Apps even easier to build. We&#8217;re introducing a fluent configuration API for the .NET isolated worker that lets you promote any MCP tool to a full MCP App complete with views, permissions, and security policies in just a few lines of code.<\/p>\n<h2>What are MCP Apps?<\/h2>\n<p>MCP Apps extend the <a href=\"https:\/\/modelcontextprotocol.io\/introduction\">Model Context Protocol<\/a> tool model by allowing individual tools to be configured as apps. Tools that come with their own UI views, static assets, and fine-grained security controls. Think of them as MCP tools that can present rich, interactive experiences to users while still being invocable by AI agents.<\/p>\n<p>With MCP Apps, you can:<\/p>\n<ul>\n<li><strong>Attach HTML views<\/strong> to your tools, rendered by the MCP client.<\/li>\n<li><strong>Serve static assets<\/strong> (HTML, CSS, JavaScript, images) alongside your tool.<\/li>\n<li><strong>Control permissions<\/strong> like clipboard access, allowing your app to interact with the user&#8217;s environment in a controlled way.<\/li>\n<li><strong>Define Content Security Policies (CSP)<\/strong> to lock down what your app can load and connect to.<\/li>\n<\/ul>\n<h2>Why a Fluent API?<\/h2>\n<p>One of the key goals of the Fluent API is to abstract away the MCP spec so you don&#8217;t need to know the protocol details to build an app. In the MCP protocol, connecting a tool to a UI view requires precise coordination: a resource endpoint at a <code>ui:\/\/<\/code> URI, a special mime type (<code>text\/html;profile=mcp-app<\/code>) that tells clients to render the content as an interactive app, and <code>_meta.ui<\/code> metadata on both the tool and resource to wire everything together. For example, the tool metadata carries a <code>resourceUri<\/code> and <code>visibility<\/code> so the client knows the tool has a UI, while the resource response carries the CSP, permissions, and rendering hints so the client knows how to display it securely.<\/p>\n<p>The Fluent API handles all of this coordination for you. When you call <code>AsMcpApp<\/code>, the extension automatically generates the synthetic resource function, sets the correct mime type, and injects the metadata that connects your tool to its view. You just write your function, point it at an HTML file, and configure the security policies you need.<\/p>\n<h2>Get started<\/h2>\n<p>The Fluent API for MCP Apps is available as a preview in the <code>Microsoft.Azure.Functions.Worker.Extensions.Mcp<\/code> NuGet package. If you&#8217;re already building MCP tools with Azure Functions, you&#8217;re just a package update away:<\/p>\n<pre><code class=\"language-bash\">dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Mcp --version 1.5.0-preview.1<\/code><\/pre>\n<p>This preview package includes all the fluent configuration APIs covered in this post. Since this is a preview release, APIs may change based on your feedback before the stable release.<\/p>\n<h2>The Fluent API<\/h2>\n<p>Here&#8217;s the complete setup for a simple &#8220;Hello App&#8221;:<\/p>\n<h3>Step 1: Define your function<\/h3>\n<p>Start with a standard Azure Functions MCP tool. The <code>[McpToolTrigger]<\/code> attribute wires up your function as an MCP tool, just like before:<\/p>\n<pre><code class=\"language-csharp\">[Function(nameof(HelloApp))]\npublic string HelloApp(\n    [McpToolTrigger(\"HelloApp\", \"A simple MCP App that says hello.\")] ToolInvocationContext context)\n{\n    return \"Hello from app\";\n}<\/code><\/pre>\n<h3>Step 2: Configure it as an MCP App<\/h3>\n<p>In your program startup, use the Fluent API to promote your tool to a full MCP App:<\/p>\n<pre><code class=\"language-csharp\">builder.ConfigureMcpTool(\"HelloApp\")\n    .AsMcpApp(app =&gt; app\n        .WithView(\"assets\/hello-app.html\")\n        .WithTitle(\"Hello App\")\n        .WithPermissions(McpAppPermissions.ClipboardWrite | McpAppPermissions.ClipboardRead)\n        .WithCsp(csp =&gt;\n        {\n            csp.AllowBaseUri(\"https:\/\/www.microsoft.com\")\n               .ConnectTo(\"https:\/\/www.microsoft.com\");\n        }));<\/code><\/pre>\n<h3>Step 3: Add your view<\/h3>\n<p>Create an HTML file at <code>assets\/hello-app.html<\/code> in your project. This is the UI that MCP clients render when your app tool is invoked. You have full control over the markup, styling, and client-side behavior.<\/p>\n<p>That&#8217;s it. Three steps and your MCP tool is now an MCP App with a rich UI.<\/p>\n<h2>Breaking down the API<\/h2>\n<p>Let&#8217;s walk through each part of the fluent configuration.<\/p>\n<h3><code>ConfigureMcpTool(\"HelloApp\")<\/code><\/h3>\n<p>Selects the MCP tool you want to configure. The name must match the function name registered with <code>[McpToolTrigger]<\/code>.<\/p>\n<h3><code>.AsMcpApp(app =&gt; ...)<\/code><\/h3>\n<p>Promotes the tool to an MCP App. Everything inside the lambda configures the app-specific behavior via the <code>IMcpAppBuilder<\/code> interface.<\/p>\n<h3><code>.WithView(...)<\/code><\/h3>\n<p>Sets the view for the app. You can provide a file path directly, or use one of the <code>McpViewSource<\/code> factory methods for more control:<\/p>\n<pre><code class=\"language-csharp\">\/\/ File on disk (relative to output directory)\napp.WithView(\"assets\/hello-app.html\")\n\n\/\/ Explicit file source\napp.WithView(McpViewSource.FromFile(\"assets\/hello-app.html\"))\n\n\/\/ Embedded resource from an assembly\napp.WithView(McpViewSource.FromEmbeddedResource(\"MyApp.Resources.view.html\"))<\/code><\/pre>\n<p>The <code>McpViewSource<\/code> abstraction lets you choose where your HTML lives: on disk alongside your function, or embedded directly in your assembly for self-contained deployment.<\/p>\n<h3><code>.WithTitle(\"Hello App\")<\/code><\/h3>\n<p>Sets a human-readable title for the view, displayed by MCP clients in their UI.<\/p>\n<h3><code>.WithBorder()<\/code><\/h3>\n<p>Hints to the MCP client that it should render a border around the view. Pass <code>false<\/code> to explicitly opt out, or omit it entirely to let the client decide:<\/p>\n<pre><code class=\"language-csharp\">.WithBorder()       \/\/ prefer border\n.WithBorder(false)  \/\/ prefer no border<\/code><\/pre>\n<h3><code>.WithDomain(\"myapp.example.com\")<\/code><\/h3>\n<p>Sets a domain hint for the view, used by the host to scope cookies and storage for your app.<\/p>\n<h3><code>.WithPermissions(...)<\/code><\/h3>\n<p>Controls what the app is allowed to do in the client environment. Permissions are defined as flags on <code>McpAppPermissions<\/code>:<\/p>\n<table>\n<thead>\n<tr>\n<th>Permission<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>ClipboardRead<\/code><\/td>\n<td>Allows the app to read from the clipboard<\/td>\n<\/tr>\n<tr>\n<td><code>ClipboardWrite<\/code><\/td>\n<td>Allows the app to write to the clipboard<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Permissions are opt-in, so your app only gets the access it explicitly requests.<\/p>\n<h3><code>.WithCsp(csp =&gt; ...)<\/code><\/h3>\n<p>Defines the Content Security Policy for the app&#8217;s view. The CSP builder (<code>IMcpCspBuilder<\/code>) provides four methods, each mapping to standard CSP directives:<\/p>\n<table>\n<thead>\n<tr>\n<th>Method<\/th>\n<th>CSP Directive<\/th>\n<th>Purpose<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>ConnectTo(origin)<\/code><\/td>\n<td><code>connect-src<\/code><\/td>\n<td>Network requests (fetch, XMLHttpRequest, WebSocket)<\/td>\n<\/tr>\n<tr>\n<td><code>LoadResourcesFrom(origin)<\/code><\/td>\n<td><code>img-src<\/code>, <code>script-src<\/code>, <code>style-src<\/code>, <code>font-src<\/code>, <code>media-src<\/code><\/td>\n<td>Static resources (scripts, images, styles, fonts, media)<\/td>\n<\/tr>\n<tr>\n<td><code>AllowFrame(origin)<\/code><\/td>\n<td><code>frame-src<\/code><\/td>\n<td>Nested iframes<\/td>\n<\/tr>\n<tr>\n<td><code>AllowBaseUri(origin)<\/code><\/td>\n<td><code>base-uri<\/code><\/td>\n<td>Base URI for the document<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<pre><code class=\"language-csharp\">.WithCsp(csp =&gt;\n{\n    csp.ConnectTo(\"https:\/\/api.example.com\")\n       .LoadResourcesFrom(\"https:\/\/cdn.example.com\")\n       .AllowFrame(\"https:\/\/youtube.com\")\n       .AllowBaseUri(\"https:\/\/www.microsoft.com\");\n})<\/code><\/pre>\n<p>You can call <code>WithCsp<\/code> multiple times. Origins accumulate across calls, so you can compose CSP configuration from multiple sources. By default, the CSP is restrictive; you explicitly allowlist the origins your app needs, following the principle of least privilege.<\/p>\n<h3><code>.WithVisibility(...)<\/code><\/h3>\n<p>Controls who can see the tool. The <code>McpVisibility<\/code> flags enum has two values:<\/p>\n<table>\n<thead>\n<tr>\n<th>Visibility<\/th>\n<th>Description<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>Model<\/code><\/td>\n<td>Visible to the large language model (LLM) during tool selection<\/td>\n<\/tr>\n<tr>\n<td><code>App<\/code><\/td>\n<td>Visible to the host UI for rendering<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>By default, tools are visible to both (<code>Model | App<\/code>). You can restrict visibility if, for example, you want a tool that only renders UI and shouldn&#8217;t be invoked by the model directly:<\/p>\n<pre><code class=\"language-csharp\">.ConfigureApp()\n.WithVisibility(McpVisibility.App)  \/\/ UI-only, hidden from the model<\/code><\/pre>\n<h3><code>.WithStaticAssets(...)<\/code><\/h3>\n<p>Configures a directory from which static assets (CSS, JS, images) are served alongside your view:<\/p>\n<pre><code class=\"language-csharp\">.ConfigureApp()\n.WithStaticAssets(\"assets\/dist\")\n\n\/\/ Or with options\n.WithStaticAssets(\"assets\/dist\", options =&gt;\n{\n    options.IncludeSourceMaps = true;  \/\/ default: false, to avoid leaking internal paths\n})<\/code><\/pre>\n<p>By default, <code>.map<\/code> files are excluded from serving to avoid leaking internal paths and implementation details.<\/p>\n<h3>Builder navigation<\/h3>\n<p>The Fluent API uses three builder levels (tool, app, and view), and you can navigate between them:<\/p>\n<pre><code class=\"language-csharp\">builder.ConfigureMcpTool(\"Dashboard\")\n    .AsMcpApp(app =&gt; app\n        .WithView(\"ui\/dashboard.html\")      \/\/ returns IMcpViewBuilder\n        .WithTitle(\"Dashboard\")\n        .WithPermissions(McpAppPermissions.ClipboardRead)\n        .ConfigureApp()                      \/\/ back to IMcpAppBuilder\n        .WithStaticAssets(\"ui\/dist\")\n        .WithVisibility(McpVisibility.Model | McpVisibility.App))\n    .WithProperty(\"dataset\", McpToolPropertyType.String, \"The dataset to display\");  \/\/ back to tool builder<\/code><\/pre>\n<h2>Summary<\/h2>\n<p>The new Fluent API for MCP Apps in the .NET isolated worker makes it straightforward to build MCP tools with rich UI experiences. With just a few lines of configuration, you can attach views, control permissions, and enforce security policies on your tools without needing to know the details of the MCP protocol. Under the covers, the extension handles synthetic resource generation, metadata wiring, mime type management, and secure asset serving, so you can focus on building great tool experiences for AI agents and users alike.<\/p>\n<h2>Useful links<\/h2>\n<ul>\n<li><a href=\"https:\/\/techcommunity.microsoft.com\/blog\/appsonazureblog\/building-mcp-apps-with-azure-functions-mcp-extension\/4496536\">Building MCP Apps with Azure Functions MCP Extension<\/a>: the original MCP Apps announcement blog post<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/azure\/azure-functions\/scenario-mcp-apps?tabs=bash%2Clinux&amp;pivots=programming-language-csharp\">MCP Apps quickstart<\/a>: step-by-step guide to building your first MCP App<\/li>\n<li><a href=\"https:\/\/learn.microsoft.com\/azure\/azure-functions\/functions-bindings-mcp\">Azure Functions MCP extension documentation<\/a>: full reference for the MCP extension<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Build MCP tools with rich UI experiences using the new Fluent API for MCP Apps in the .NET isolated worker. Configure views, permissions, and security policies with just a few lines of code.<\/p>\n","protected":false},"author":21393,"featured_media":3699,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[786,968,765,161,757,940,969],"class_list":["post-3698","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-azure-sdk","tag-ai","tag-apps","tag-azure-functions","tag-dotnet","tag-functions","tag-mcp","tag-mcpapps"],"acf":[],"blog_post_summary":"<p>Build MCP tools with rich UI experiences using the new Fluent API for MCP Apps in the .NET isolated worker. Configure views, permissions, and security policies with just a few lines of code.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3698","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/users\/21393"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/comments?post=3698"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3698\/revisions"}],"predecessor-version":[{"id":3779,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/posts\/3698\/revisions\/3779"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/media\/3699"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/media?parent=3698"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/categories?post=3698"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/azure-sdk\/wp-json\/wp\/v2\/tags?post=3698"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}