MCP Apps: Interactive UIs for AI Clients - A practical guide

MCP Apps are one of the most interesting recent additions to the MCP ecosystem. Traditional MCP tools already work well for many tasks, but they reach their limits once users need to inspect richer data, switch views, compare results, or interact with something more complex than a text response. That is exactly where MCP Apps come in: a tool can return data and declare a ui:// resource that the host renders inline as an interactive interface. The official MCP Apps documentation describes this as a combination of tool metadata, a UI resource, sandboxed iframe rendering, and bidirectional JSON-RPC communication between host and app.
Weather is a good fit for this model. A plain-text forecast is fine for a quick answer, but once users want to switch between temperature, precipitation, and wind, compare days, or inspect a chart visually, a conversational response becomes clumsy. An inline app solves that without giving up the strengths of MCP. The host still calls a tool on the server, the server still owns execution and data access, and the UI remains an enhancement rather than a replacement. That design is also reflected in the official ext-apps repository and the MCP Apps build guide.
This post is the third part of my weather MCP series. Part 1 introduced callable weather tools, Part 2 added read-only climate resources, and this part adds an interactive dashboard. The implementation is publicly available in the weather-forecast-mcp-server repository, including the Python server, the React UI, and a small bridge server for app rendering in supported hosts:
Why I kept the backend in Python
The most important decision in this implementation was not the chart library or the host configuration. It was keeping the existing server architecture intact.
Once you already have a working MCP server in Python, rewriting the backend just to add an interactive UI is usually the wrong trade-off. MCP Apps do not require that. The server still exposes normal tools and resources. The UI is simply an additional layer that the host can render when it supports MCP Apps.
That makes a hybrid setup a very natural fit:
- Python / FastMCP stays responsible for tool execution, API access, validation, and operational concerns.
- React handles the interactive part: switching views, rendering charts, and managing client-side state.
The official MCP Apps model follows exactly that separation. It distinguishes between the Server, the Host, and the View. In other words, the UI is not a replacement for the server. It is an extra surface on top of it. That is also why this pattern feels production-friendly: the backend can remain boring and dependable, while the frontend can evolve like a normal web app.
You can see the same direction outside of toy examples as well. Microsoft added MCP App support to its Azure Functions MCP extension in February 2026, which is a pretty clear sign that the ecosystem already treats “tool plus embedded UI” as a serious deployment model and not just as a demo feature.
The core MCP Apps pattern
At a high level, MCP Apps extend the normal MCP flow instead of replacing it:
- A tool declares a UI resource.
- The host reads the corresponding
ui://...resource. - The host renders that HTML inside a sandboxed iframe.
- Tool input and tool results are forwarded to the app.
- The app reacts to that data and can trigger further interaction through the host.
That is the part I find most compelling. The app stays anchored in the same conversation and the same server connection. You are not opening a separate dashboard in another tab. You are still inside the MCP workflow, just with a better interface for the cases where plain text becomes awkward.
How the weather dashboard is structured
The repository keeps the responsibilities separate:
src/weather_forecast_mcp_server/contains the FastMCP servermcp-app-ui/contains the React dashboardmcp-app-server/contains a smallext-appsbridge server for client compatibility
That layout turned out to be much easier to reason about than trying to blur backend and frontend concerns together.
Backend: tool metadata points to a UI resource
@mcp.tool(
title="Weather Forecast Dashboard",
description="Get a 7-day weather forecast payload for the dashboard app.",
meta={
"ui": {"resourceUri": "ui://weather/dashboard"},
"ui/resourceUri": "ui://weather/dashboard"
}
)
async def get_forecast_dashboard(latitude: float, longitude: float) -> CallToolResult:
...
@mcp.resource("ui://weather/dashboard", mime_type="text/html;profile=mcp-app")
async def weather_dashboard_resource() -> str:
return UI_DASHBOARD_PATH.read_text(encoding="utf-8")
This is the key link in the whole setup. The tool remains a normal MCP tool, but it also points to a UI resource that a compatible host can fetch and render.
In my case the tool returns both conversational output and chart-ready payload data for the app. That dual-use response is important. A useful MCP App should not force you into an all-or-nothing choice where the host either renders the UI or the tool becomes useless. Text still matters. The UI simply makes richer exploration practical.
The newer MCP Apps documentation also emphasizes structuredContent as a clean place for UI-oriented data. That is worth keeping in mind when refining an implementation over time, especially if you want a clearer split between model-facing text and app-facing payloads.
Frontend: React is bundled into a single HTML artifact
The React UI lives in mcp-app-ui/ and is built into a single HTML file that the Python server serves as the ui://weather/dashboard resource.
cd weather-forecast-mcp-server/mcp-app-ui
npm install
npm run build
That is not the only possible packaging model, but it is a very practical one for a tutorial and for smaller self-contained apps. The official build guide shows the same direction with Vite-based examples. You keep the frontend workflow modern, but the runtime story on the server side stays simple.
Bridge server: not the point, but useful in practice
The repository also ships an ext-apps bridge server in mcp-app-server/server.mjs.
cd weather-forecast-mcp-server/mcp-app-server
npm install
node server.mjs
I would not oversell this part. The bridge server is not the architectural headline of MCP Apps. But in practice it is useful because real clients differ, support is still evolving, and the official ext-apps tooling helps reduce friction when you want the app to render reliably in supported hosts.
Data flow: who does what?
This split is one of the reasons MCP Apps feel much more robust than improvised “render some HTML somehow” approaches. The server owns execution and data. The host owns rendering and permissions. The app owns presentation and interaction. Once you keep those boundaries clear, debugging also gets easier because you can narrow problems down much faster.
Verification workflow: protocol first, UX second
One practical lesson from this implementation is that MCP Apps need two different test loops.
1. Verify the Python server itself
uv --directory ./weather-forecast-mcp-server run mcp run src/weather_forecast_mcp_server/server.py
That confirms that the server starts cleanly and that the basic MCP functionality still works.
2. Verify discovery and payloads with Inspector
npx @modelcontextprotocol/inspector uv --directory ./weather-forecast-mcp-server run mcp run src/weather_forecast_mcp_server/server.py
Inspector is the right place to check whether:
get_forecast_dashboardis discoverable,- the tool returns the expected payload,
ui://weather/dashboardcan be resolved.
3. Verify app rendering in a real host
{
"mcpServers": {
"weather": {
"command": "uv",
"args": [
"run",
"mcp",
"run",
"src/weather_forecast_mcp_server/server.py"
]
}
}
}

Inspector is necessary, but it is not sufficient. MCP Apps add another runtime boundary: host to iframe. That means discovery can work while rendering still fails, and the opposite is possible as well. So the final check has to happen in a real MCP Apps-capable host.
This is also where the recent ecosystem coverage helped frame the feature more clearly for me. Articles about the Claude rollout focused on the visible result — charts, forms, dashboards inside the chat window — but from an implementation perspective the important part is that host behavior becomes part of your delivery surface. Once you build an MCP App, you are no longer testing only server correctness. You are also testing host behavior.
Practical lessons from this implementation
Treat the UI as an enhancement
The best mental model is still “chat plus app”, not “chat or app”. A tool should continue to return a sensible textual result, even if the app does most of the heavy lifting in supported hosts.
Keep the boundary between data and UI clean
The cleaner the payload contract is, the easier the frontend becomes. You want the React app to focus on rendering and interaction, not on reconstructing half the business logic in the browser.
Expect host-specific behavior
This is still a young extension and support is evolving quickly. In practice that means fresh sessions after config changes, double-checking client support, and not assuming that every host behaves identically.
Add an obvious visual fingerprint
A visible marker such as Powered by weather-forecast-mcp-server is more useful than it sounds. It immediately tells you whether you are looking at the real embedded app or at a model-generated imitation in plain chat.
Security and operational considerations
One reason MCP Apps are interesting is that they add a UI layer without discarding MCP’s existing boundaries. The official security model is a big part of that story:
- apps render in a sandboxed iframe,
- communication uses JSON-RPC over
postMessage, - hosts mediate capabilities and permissions,
- servers can declare metadata such as CSP-related constraints for the UI.
That does not magically make a bad server safe, but it is still a much better foundation than ad-hoc embedding patterns. The Python server remains the execution boundary for API access and secrets. The UI remains a controlled rendering surface inside the host.
For me, that is the real architectural value of MCP Apps. They make richer interaction possible without collapsing the separation between execution, rendering, and permissions.
When MCP Apps are actually worth it
Not every MCP tool needs an app.
If the interaction is short, linear, and easy to understand in plain text, a normal tool is often the better choice. The following framing is useful: MCP Apps become interesting when the workflow is not “terminal-shaped” anymore. A dashboard, a diff review, a trace viewer, or a multi-step form benefits from a real interface. A one-shot command usually does not.
That is exactly why weather works well as an example here. Forecast data is still simple enough to explain, but rich enough that switching metrics, scanning a chart, and comparing days becomes much easier in a visual interface than in repeated chat turns.
Conclusion
MCP Apps are not interesting because they let us stuff mini web sites into chat windows. They are interesting because they give MCP a clean way to handle the kinds of interactions that plain text is bad at.
For this weather dashboard, the practical answer was not to replace the Python server, but to extend it. FastMCP remains responsible for tools, data access, and operational control. React handles the interactive layer.
That is the main takeaway I would keep from this exercise: if you already have a solid MCP server, adding an app should feel like an incremental extension, not like a rewrite. When that is true, MCP Apps become a very pragmatic addition to an existing architecture.