MCP Apps are here, and they're a game-changer for building AI tools with interactive UIs. If you've been following the Model Context Protocol (MCP) ecosystem, you've probably heard about the MCP Apps spec — the first official MCP extension that lets your tools return rich, interactive UIs that render directly inside AI chat clients like Claude Desktop, ChatGPT, VS Code Copilot, Goose, and Postman.
And here's the best part: you can host them on Azure App Service. In this post, I'll walk you through building a weather widget MCP App and deploying it to App Service. You'll have a production-ready MCP server serving interactive UIs in under 10 minutes.
What Are MCP Apps?
MCP Apps extend the Model Context Protocol by combining tools (the functions your AI client can call) with UI resources (the interactive interfaces that display the results). The pattern is simple:
- A tool declares a
_meta.ui.resourceUri in its metadata
- When the tool is invoked, the MCP host fetches that UI resource
- The UI renders in a sandboxed iframe inside the chat client
The key insight? MCP Apps are just web apps — HTML, JavaScript, and CSS served through MCP. And that's exactly what App Service does best.
The MCP Apps spec supports cross-client rendering, so the same UI works in Claude Desktop, VS Code Copilot, ChatGPT, and other MCP-enabled clients. Your weather widget, map viewer, or data dashboard becomes a universal component in the AI ecosystem.
Why App Service for MCP Apps?
Azure App Service is a natural fit for hosting MCP Apps. Here's why:
- Always On — No cold starts. Your UI resources are served instantly, every time.
- Easy Auth — Secure your MCP endpoint with Entra ID authentication out of the box, no code required.
- Custom domains + TLS — Professional MCP server endpoints with your own domain and managed certificates.
- Deployment slots — Canary and staged rollouts for MCP App updates without downtime.
- Sidecars — Run backend services (Redis, message queues, monitoring agents) alongside your MCP server.
- App Insights — Built-in telemetry to see which tools and UIs are being invoked, response times, and error rates.
Now, these are all capabilities you can add to a production MCP App, but the sample we're building today keeps things simple. We're focusing on the core pattern: serving MCP tools with interactive UIs from App Service. The production features are there when you need them.
When to Use Functions vs App Service for MCP Apps
Before we dive into the code, let's talk about Azure Functions. The Functions team has done great work with their MCP Apps quickstart, and if serverless is your preferred model, that's a fantastic option. Functions and App Service both host MCP Apps beautifully — they just serve different needs.
| | Azure Functions | Azure App Service |
|---|
| Best for | New, purpose-built MCP Apps that benefit from serverless scaling | MCP Apps that need always-on hosting, persistent state, or are part of larger web apps |
| Scaling | Scale to zero, pay per invocation | Dedicated plans, always running |
| Cold start | Possible (mitigated by premium plan) | None (Always On) |
| Deployment | azd up with Functions template | azd up with App Service template |
| MCP Apps quickstart | Available | This blog post! |
| Additional capabilities | Event-driven triggers, durable functions | Easy Auth, custom domains, deployment slots, sidecars |
Think of it this way: if you're building a new MCP App from scratch and want serverless economics, go with Functions. If you're adding MCP capabilities to an existing web app, need zero cold starts, or want production features like Easy Auth and deployment slots, App Service is your friend.
Build the Weather Widget MCP App
Let's build a simple MCP App that fetches weather data from the Open-Meteo API and displays it in an interactive widget. The sample uses ASP.NET Core for the MCP server and Vite for the frontend UI.
Here's the structure:
app-service-mcp-app-sample/
├── src/
│ ├── Program.cs # MCP server setup
│ ├── WeatherTool.cs # Weather tool with UI metadata
│ ├── WeatherUIResource.cs # MCP resource serving the UI
│ ├── WeatherService.cs # Open-Meteo API integration
│ └── app/ # Vite frontend (weather widget)
│ └── src/
│ └── weather-app.ts # MCP Apps SDK integration
├── .vscode/
│ └── mcp.json # VS Code MCP server config
├── azure.yaml # Azure Developer CLI config
└── infra/ # Bicep infrastructure
Program.cs — MCP Server Setup
The MCP server is an ASP.NET Core app that registers tools and UI resources:
using ModelContextProtocol;
var builder = WebApplication.CreateBuilder(args);
// Register WeatherService
builder.Services.AddSingleton<WeatherService>(sp =>
new WeatherService(WeatherService.CreateDefaultClient()));
// Add MCP Server with HTTP transport, tools, and resources
builder.Services.AddMcpServer()
.WithHttpTransport(t => t.Stateless = true)
.WithTools<WeatherTool>()
.WithResources<WeatherUIResource>();
var app = builder.Build();
// Map MCP endpoints (no auth required for this sample)
app.MapMcp("/mcp").AllowAnonymous();
app.Run();
AddMcpServer() configures the MCP protocol handler. WithHttpTransport() enables Streamable HTTP with stateless mode (no session management needed). WithTools<WeatherTool>() registers our weather tool, and WithResources<WeatherUIResource>() registers the UI resource that the MCP host will fetch and render. MapMcp("/mcp") maps the MCP endpoint at /mcp.
WeatherTool.cs — Tool with UI Metadata
The WeatherTool class defines the tool and uses the [McpMeta] attribute to declare a ui metadata block containing the resourceUri. This tells the MCP host where to fetch the interactive UI:
using System.ComponentModel;
using ModelContextProtocol.Server;
[McpServerToolType]
public class WeatherTool
{
private readonly WeatherService _weatherService;
public WeatherTool(WeatherService weatherService)
{
_weatherService = weatherService;
}
[McpServerTool]
[Description("Get current weather for a location via Open-Meteo. Returns weather data that displays in an interactive widget.")]
[McpMeta("ui", JsonValue = """{"resourceUri": "ui://weather/index.html"}""")]
public async Task<object> GetWeather(
[Description("City name to check weather for (e.g., Seattle, New York, Miami)")]
string location)
{
var result = await _weatherService.GetCurrentWeatherAsync(location);
return result;
}
}
The key line is the [McpMeta("ui", ...)] attribute. This adds _meta.ui.resourceUri to the tool definition, pointing to the ui://weather/index.html resource. When the AI client calls this tool, the host fetches that resource and renders it in a sandboxed iframe alongside the tool result.
WeatherUIResource.cs — UI Resource
The UI resource class serves the bundled HTML as an MCP resource with the ui:// scheme and text/html;profile=mcp-app MIME type required by the MCP Apps spec:
using ModelContextProtocol.Protocol;
using ModelContextProtocol.Server;
[McpServerResourceType]
public class WeatherUIResource
{
[McpServerResource(
UriTemplate = "ui://weather/index.html",
Name = "weather_ui",
MimeType = "text/html;profile=mcp-app")]
public static ResourceContents GetWeatherUI()
{
var filePath = Path.Combine(
AppContext.BaseDirectory, "app", "dist", "index.html");
var html = File.ReadAllText(filePath);
return new TextResourceContents
{
Uri = "ui://weather/index.html",
MimeType = "text/html;profile=mcp-app",
Text = html
};
}
}
The [McpServerResource] attribute registers this method as the handler for the ui://weather/index.html resource. When the host fetches it, the bundled single-file HTML (built by Vite) is returned with the correct MIME type.
WeatherService.cs — Open-Meteo API Integration
The WeatherService class handles geocoding and weather data from the Open-Meteo API. Nothing MCP-specific here — it's just a standard HTTP client that geocodes a city name and fetches current weather observations.
The UI Resource (Vite Frontend)
The app/ directory contains a TypeScript app built with Vite that renders the weather widget. It uses the @modelcontextprotocol/ext-apps SDK to communicate with the host:
import { App } from "@modelcontextprotocol/ext-apps";
const app = new App({ name: "Weather Widget", version: "1.0.0" });
// Handle tool results from the server
app.ontoolresult = (params) => {
const data = parseToolResultContent(params.content);
if (data) render(data);
};
// Adapt to host theme (light/dark)
app.onhostcontextchanged = (ctx) => {
if (ctx.theme) applyTheme(ctx.theme);
};
await app.connect();
The SDK's App class handles the postMessage communication with the host. When the tool returns weather data, ontoolresult fires and the widget renders the temperature, conditions, humidity, and wind. The app also adapts to the host's theme so it looks native in both light and dark mode.
The frontend is bundled into a single index.html file using Vite and the vite-plugin-singlefile plugin, which inlines all JavaScript and CSS. This makes it easy to serve as a single MCP resource.
Run Locally
To run the sample locally, you'll need the .NET 9 SDK and Node.js 18+ installed. Clone the repo and run:
# Clone the repo
git clone https://github.com/seligj95/app-service-mcp-app-sample.git
cd app-service-mcp-app-sample
# Build the frontend
cd src/app
npm install
npm run build
# Run the MCP server
cd ..
dotnet run
The server starts on http://localhost:5000. Now connect from VS Code Copilot:
- Open your workspace in VS Code
- The sample includes a
.vscode/mcp.json that configures the local MCP server:
{
"servers": {
"local-mcp-appservice": {
"type": "http",
"url": "http://localhost:5000/mcp"
}
}
}
- Open the GitHub Copilot Chat panel
- Ask: "What's the weather in Seattle?"
Copilot will invoke the GetWeather tool, and the interactive weather widget will render inline in the chat:
Weather widget MCP App rendering inline in VS Code Copilot Chat
Deploy to Azure
Deploying to Azure is even easier. The sample includes an azure.yaml file and Bicep templates for App Service, so you can deploy with a single command:
cd app-service-mcp-app-sample
azd auth login
azd up
azd up will:
- Provision an App Service plan and web app in your subscription
- Build the .NET app and Vite frontend
- Deploy the app to App Service
- Output the public MCP endpoint URL
After deployment, azd will output a URL like https://app-abc123.azurewebsites.net. Update your .vscode/mcp.json to point to the remote server:
{
"servers": {
"remote-weather-app": {
"type": "http",
"url": "https://app-abc123.azurewebsites.net/mcp"
}
}
}
From that point forward, your MCP App is live. Any AI client that supports MCP Apps can invoke your weather tool and render the interactive widget — no local server required.
What's Next?
You've now built and deployed an MCP App to Azure App Service. Here's what you can explore next:
And remember: App Service gives you a full production hosting platform for your MCP Apps. You can add Easy Auth to secure your endpoints with Entra ID, wire up App Insights for telemetry, configure custom domains and TLS certificates, and set up deployment slots for blue/green rollouts. These features make App Service a great choice when you're ready to take your MCP App to production.
If you build something cool with MCP Apps and App Service, let me know — I'd love to see what you create!