A sample MCP App that displays weather information with an interactive UI.
MCP Apps let tools return interactive interfaces instead of plain text. When a tool declares a UI resource, the host renders it in a sandboxed iframe where users can interact directly.
The architecture relies on two MCP primitives:
- Tools with UI metadata pointing to a resource URI
- Resources containing bundled HTML/JavaScript served via the
ui://scheme
Azure Functions makes it easy to build both.
- JDK 17 (or newer)
- Apache Maven
- Node.js (for building the UI)
- Azure Functions Core Tools v4
- An MCP-compatible host (VS Code with GitHub Copilot, Claude Desktop, etc.)
The UI must be bundled before running the function app:
cd app
npm install
npm run build
cd ..This creates a bundled app/dist/index.html file that the function serves.
mvn clean package
mvn azure-functions:runThe MCP server will be available at http://localhost:7071/runtime/webhooks/mcp.
Open .vscode/mcp.json at the repo root. Find the server called local-mcp-function and click Start above the name.
Ask Copilot: "What's the weather in Seattle?"
The source code is in WeatherFunction.java. The key concept is how tools connect to resources via metadata.
The GetWeather tool uses @McpMetadata to declare it has an associated UI:
private static final String TOOL_METADATA = """
{
"ui": {
"resourceUri": "ui://weather/index.html"
}
}
""";
@FunctionName("GetWeather")
public String getWeather(
@McpToolTrigger(
name = "GetWeather",
description = "Returns current weather for a location via Open-Meteo.")
@McpMetadata(
name = "GetWeather",
json = TOOL_METADATA)
String context,
@McpToolProperty(
name = "location",
propertyType = "string",
description = "City name to check weather for (e.g., Seattle, New York, Miami)")
String location,
final ExecutionContext executionContext) {
// Fetches weather data and returns JSON
Object result = weatherService.getCurrentWeather(location);
return MAPPER.writeValueAsString(result);
}The resourceUri points to ui://weather/index.html — this tells the MCP host that when this tool is invoked, there's an interactive UI available at that resource URI.
The GetWeatherWidget function serves the bundled HTML at the matching URI:
private static final String RESOURCE_METADATA = """
{
"ui": {
"prefersBorder": true
}
}
""";
@FunctionName("GetWeatherWidget")
public String getWeatherWidget(
@McpResourceTrigger(
name = "context",
uri = "ui://weather/index.html",
resourceName = "Weather Widget",
description = "Interactive weather display for MCP Apps",
mimeType = "text/html;profile=mcp-app")
@McpMetadata(
name = "context",
json = RESOURCE_METADATA)
String context,
final ExecutionContext executionContext) {
// Reads and returns app/dist/index.html
return Files.readString(new File("app/dist/index.html").toPath());
}- User asks: "What's the weather in Seattle?"
- Agent calls the
GetWeathertool - Tool returns weather data (JSON) and the host sees the
ui.resourceUrimetadata - Host fetches the UI resource from
ui://weather/index.html - Host renders the HTML in a sandboxed iframe, passing the tool result as context
- User sees an interactive weather widget instead of plain text
The frontend in app/src/weather-app.ts receives the tool result and renders the weather display. It uses the @modelcontextprotocol/ext-apps SDK and is bundled with Vite into a single index.html that the resource serves.
| Path | Description |
|---|---|
| WeatherFunction.java | MCP tool + resource definitions |
| WeatherService.java | Open-Meteo API client (geocoding + weather) |
| WeatherResult.java | Weather data POJO |
| WeatherError.java | Error response POJO |
| app/ | Vite + TypeScript weather widget UI |
| app/src/weather-app.ts | Frontend entry point |