Home / Articles / Stop Returning JSON: Ship Interactive UI with MCP Apps

This article is published in English.

Stop Returning JSON: Ship Interactive UI with MCP Apps

MCP Apps let servers ship sandboxed UIs beside tools so humans can approve, configure, and operate without leaving the agent conversation.

2858 words

Through most of MCP’s early life the interaction loop stayed narrow: agents invoke tools, tools execute, servers reply with text or structured payloads, and models paraphrase the outcome for people.

Plenty of questions still fit that mold. Counting healthy deployments needs no canvas: call get_deployments(), read a compact object such as {"total": 12, "healthy": 10, "degraded": 2}, and answer in one line.

The story changes when people want to interact with the result. Dashboards, approvals, filterable tables, configuration forms, charts, deployment panels, multi-step workflows, and human-in-the-loop gates all need more than a JSON dump. For a long time MCP offered no cross-host standard for that. Now it does, under the name MCP Apps, and it expands what an MCP server can represent.

MCP servers were mostly machine interfaces

The first mental model was agent-oriented. Servers expose tools such as search_projects(), create_ticket(), restart_service(), and get_customer(). Models discover them, call them, receive data, and humans see whatever the model chooses to say. That is powerful because operations travel through one protocol instead of a custom integration for every agent. The limitation is that the capabilities were designed for machines, so the human stays one step away from the raw result.

That is fine until the interaction is inherently visual. “Show me all production services” may return a correct JSON blob of statuses, CPU, and memory, yet a compact dashboard with progress bars, badges, and working Logs/Restart/Scale controls is far more useful. MCP Apps aims to close that gap.

So what exactly is MCP Apps?

MCP Apps extends Model Context Protocol so servers can ship interactive user interfaces beside their tools—not screenshots, not Markdown posing as UI, but real HTML/JavaScript applications rendered inside an MCP host. Docs describe tools advertising ui:// assets, hosts painting those assets inside isolated iframes, and the same host both injecting tool payloads into the view and letting the view invoke tools again only through that host.

MCP App = MCP Tool + UI Resource + Host/View protocol

A tool still calls APIs, queries databases, runs business logic, and returns structured data. It may additionally advertise a UI for presenting that result. The UI is an MCP resource such as ui://services/dashboard and may contain a full frontend—HTML, CSS, JavaScript, React, charts, forms, buttons. One operation therefore gains both a machine-facing capability and a human-facing interface, standardized across hosts.

Where did MCP Apps come from?

The extension is new. Builders who shipped MCP servers through 2025 did not miss a secret feature; the official standard did not exist yet.

Late November 2025 brought SEP-1865, the Apps proposal shaped with MCP-UI contributors and maintainers from major model labs. Parallel experiments (MCP-UI, Apps SDK) already existed; the missing piece was one interoperable way for a server to ship tools, data, and UI that any compliant host could render. The MCP blog’s November 2025 Apps proposal post records the SEP-1865 announcement.

By late January 2026 the same group called Apps the first official extension and production-ready, with a sharper spec, SDK, and shipping hosts. ChatGPT, Claude, Goose, and Visual Studio Code were named as early clients. The January 2026 MCP blog post declares Apps the first official extension.

The July 2026 release candidate elevated extensions generally—stable identifiers, capability negotiation, separate repos, independent versioning, and an Extensions Track that lists Apps. It also stressed that button-driven calls still pass through the host’s JSON-RPC path, keeping Agent → Tool and Human → Button → Tool under one control plane. The July 2026 release-candidate post on the MCP blog covers the Extensions Track.

In September 2026 an AWS Bedrock AgentCore post showed a concrete hosting pattern without making Apps AWS-only: host → gateway → runtime → MCP server (tools + UI resources) → Lambda/DynamoDB. The demo used ChatGPT and noted Claude and other Apps hosts work the same way. See the AWS Machine Learning blog post on interactive MCP Apps with Bedrock AgentCore for the walkthrough.

Who actually supports MCP Apps?

Supporting MCP is not the same as supporting MCP Apps. A client may handle ordinary tools and resources without implementing the Apps extension. The January 2026 announcement named ChatGPT, Claude, Goose, and VS Code; current docs describe inline rendering in Claude, ChatGPT, and other compliant clients while noting that host support varies. Design for that reality: do not assume every client understands Apps. The useful equation is Server supports MCP Apps + Host supports MCP Apps = Interactive UI, and a careful server should fall back to text or structured content when the host cannot render the App.

The one question that actually matters

If the UI resource is static HTML, how does changing tool data reach it? CPU can be 21% now and 87% ten seconds later; regenerating an entire frontend on every tool call would be absurd. The answer is separation: the UI and the data are different artifacts. Treat the tool as the data producer, the resource as the presentation shell, and the host as the glue. The tool returns dynamic data; the resource returns an application that knows how to render that shape; the host wires them at runtime.

How MCP Apps actually work

Consider get_servers(), which returns server objects with id, name, status, CPU, and memory, and whose metadata points at ui://servers/dashboard. The lifecycle:

The model calls get_servers(). The server runs its logic—database, cloud API, Kubernetes, internal service—and returns structured data. The host sees UI metadata for ui://servers/dashboard and issues resources/read. The server returns the frontend bundle (React, Vue, or plain JavaScript). Current server rows are not baked into that HTML; the UI only knows the expected contract (servers[].id, servers[].status, and so on).

The host renders the application in a sandboxed iframe so UI code from an MCP server cannot freely touch the host DOM, session, or credentials. Then the host delivers the tool result into the running view, typically over JSON-RPC via postMessage between host and sandboxed view.

The frontend receives data the way any SPA would and renders accordingly. One server yields one card; fifty servers yield fifty cards. The application stays fixed; only the data changes.

Compared with a classic web app—React calling GET /api/servers—MCP Apps has the agent trigger tools/call, the tool return JSON, and the host inject that JSON into a React app inside an iframe. The UI does not always fetch for itself; the host may push results in.

But MCP Apps are not just prettier tool results

A deeper property is that the App can call tools too. Buttons, forms, and charts are not decorative; they can invoke the same MCP tools the agent uses, through the host.

MCP App → call tool → Host → tools/call → MCP Server → restart_server()

For example a Restart control inside the dashboard can call restart_server via the host rather than inventing a side channel:

async function restartServer(serverId) {
  return app.callServerTool({
    name: "restart_server",
    arguments: { server_id: serverId }
  });
}

The host remains the gatekeeper for permissions, logging, and policy.

One capability, two interfaces

The same backend operation can therefore present two faces: a tool the model calls in conversation, and an App a human operates visually. Neither replaces the other. Agents remain strong at open-ended reasoning; Apps excel when structure, density, or repeated actions matter.

A production example: human approval inside an agent workflow

Imagine an agent drafting a user story that must be approved before it is filed. Without Apps, the model pastes the draft into chat and hopes the human types “approve” or “reject with reasons.” With Apps, a tool such as present_user_story_for_approval can return the draft plus a UI resource that shows Accept / Request changes / Reject controls. Clicking Reject can open a reason field; submitting can call a tool that records the decision and optionally updates model context so the next turn already knows why version 1 failed.

That pattern is human-in-the-loop with a real control surface, not a fragile natural-language ritual.

Not every button needs to be a tool the model sees

Some tools should be agent-only, some App-only, some shared. Pagination inside a dashboard may be callable only from the view so the model is not flooded with page-turn tools. The server can advertise different visibility tiers, creating a true access boundary rather than a mere naming convention.

Communication can also flow App → model context (when the host allows): a rejection reason typed in the UI can update context so the next model turn improves the draft without the user retyping the critique in chat.

MCP Apps is not a new core primitive

Despite the name, Apps is not a fourth peer of Tools/Resources/Prompts. It is an extension that standardizes a relationship between a tool and a UI resource plus a host/view protocol. If tools and resources are already familiar, Apps is an interactive layer around them—not a separate protocol bolted on.

What changes if you own the chat application

Teams building their own agent platform become the MCP host. That host must detect UI metadata, read the resource, sandbox the app, deliver tool I/O to the view, proxy allowed tool calls back to the server, negotiate capabilities, and enforce permissions. That is a real architectural surface.

There are three participants—MCP server, MCP host, and MCP App view—and the official SDK separates view developers, host developers, and server authors. The helper packages live under @modelcontextprotocol/ext-apps in TypeScript, which does not force business logic into Node. Wire-level MCP still uses tool metadata, resources, structured content, and MCP requests, so a Python/FastMCP server works if it exposes what Apps-capable hosts expect. The view is web technology; existing backends can stay put.

Security cannot be an afterthought

Executable UI raises the stakes. Sandboxed iframes and host-mediated calls help, yet every App should be treated as untrusted UI—especially when it can trigger restart_service(), delete_resource(), approve_payment(), or deploy_to_production(). Confirmation dialogs are useful but insufficient. Authentication, authorization, validation, policy, audit logs, idempotency, version checks, and rate limits still belong on the backend. The UI is not the trust boundary.

Where MCP Apps actually make sense

Do not wrap every tool in an App. Returning 42 or a version string does not need React. Apps pay off when interaction has structure:

Operations dashboards — services, deployments, infrastructure, logs, metrics, jobs, queues: inspect then act.

Approval workflows — approve/reject, accept/request changes, deploy/cancel, publish/keep draft. Often the strongest enterprise fit.

RAG and enterprise knowledge search — filters, checkboxes, and “Compare Selected” beat twenty plain-text hits while the agent still handles reasoning.

Agent governance — natural language finds who can access production Salesforce; a registry view is better for reviewing and approving permission changes.

Forms and configuration — speaking “CPU 2, memory 4GB, region us-east-1, replicas 3” is worse than a form the agent summons when needed.

Don’t build one app per tool

Avoid tool_1 → app_1 proliferation. Prefer domain Apps. A “Deployment Management” App can wrap fetch, log, restart, scale, and rollback operations as a family. One entry tool reveals the App; afterward the view may call the related operations directly. The UI stays coherent and the MCP surface stays cleaner.

The bigger architectural shift

The interesting change is not the iframe itself. Operations designed for agents can now expose a standardized human layer:

OPERATION → Machine Interface (MCP Tool) + Human Interface (MCP App)

If capabilities are packaged as agent plugins, a Salesforce package might ship instructions, tools (search_accounts, create_opportunity, update_lead), permissions, evals, and Apps (account browser, opportunity form, pipeline dashboard) together. The capability becomes a full interaction surface for agents and humans.

The agent need not know React or iframes exist. It calls get_services(...) or present_user_story_for_approval(...); metadata tells the host an interface exists. UI stays in the UI layer, reasoning with the agent, business logic in the backend.

How I’d introduce this into an existing system

On a platform that already has custom chat, agents, and a FastMCP server, avoid a redesign. Start with one read-only capability such as get_agents() plus the smallest view (ui://agents/list) showing names, statuses, and tool counts—no buttons. Prove the loop: agent calls tool, host detects UI, reads resource, renders iframe, tool result reaches the view.

Then add Refresh, then a real operation like Disable Agent, then authorization and audit logging, then richer Apps. Adoption stays incremental without rewriting agent reasoning.

Teams evaluating adoption should also budget for host capability negotiation. An Apps-aware server that always attaches UI metadata can still behave correctly on older clients if the host simply ignores unknown fields and the tool’s structured content remains complete on its own. Conversely, a host that claims Apps support must implement sandboxing, resource reads, and tool-call proxying before enabling the feature flag for end users; half-implemented hosts create broken iframes that erode trust faster than plain JSON ever did.

Observability belongs in the same rollout plan. Log which tools carry UI resources, which hosts rendered them, which button-initiated tool calls succeeded, and which fell back to text. Those metrics tell you whether Apps are actually carrying interactive load or whether users still prefer typing into chat. Without that telemetry, it is easy to ship dashboards nobody clicks.

Finally, keep schema contracts versioned. The App and the tool must agree on field names and types. Breaking servers[].cpu into nested objects without bumping a UI contract will render empty widgets while the agent still receives correct JSON. Treat the App’s expected payload as a public API owned by the same team that owns the tool.

When measuring success after launch, separate “App rendered” from “App used.” An iframe that paints once and never receives a click is a curiosity; an App that drives restarts, approvals, or config changes is carrying product weight. Pair product analytics with MCP audit logs so you can show which human actions originated in Apps versus chat, and whether those actions reduced time-to-resolution for the tickets agents already handle.

Final thought

MCP answered how agents talk to external systems through one protocol. MCP Apps asks how humans can use the same capabilities without leaving the conversation.

Yesterday’s path was agent, tool, JSON, then prose. Today a single capability can fork: models keep calling tools in conversation while people operate a visual App, and both forks terminate in the same domain services. Reasoning stays with the agent, execution stays with tools, and when the job needs structure the protocol can surface dashboards, forms, approvals, charts, config panels, and human gates inside the chat—without abandoning the MCP layer agents already depend on.

That is why MCP Apps is more than prettier responses: servers are evolving from machine-facing endpoints into portable interaction layers for both agents and humans.

Document fallbacks explicitly in your server README: which tools declare UI resources, which hosts are known to render them, and what the text/structured fallback looks like when Apps are unavailable. That documentation prevents support tickets that look like “MCP is broken” when the real issue is a host without the extension enabled.

Further reading

Primary documentation for the Apps extension is published under apps.extensions.modelcontextprotocol.io (overview and API sections). Timeline narratives appear on blog.modelcontextprotocol.io for the 2025 proposal, the 2026 production declaration, and the release-candidate Extensions Track. AWS’s Machine Learning blog later showed a Bedrock AgentCore hosting pattern that remains host-agnostic.

If you maintain multiple MCP servers today, resist the urge to invent a private mini-framework per team. Prefer shared host helpers for iframe lifecycle, shared TypeScript types for tool payloads that Apps consume, and a short design review checklist: Does this tool need a UI? Is there already a domain App that should absorb it? What is the fallback when the host cannot render Apps? Who owns authz for button-triggered calls? Those four questions catch most premature App sprawl.

Training also matters. Agents that suddenly see fewer tools because some operations moved behind App-only visibility will behave differently. Update system prompts and eval suites when you split access tiers, and keep a golden path test that opens an App, clicks a safe action, and asserts the backend audit row appears. Without that test, regressions hide in the iframe until a customer reports a dead button.

With those pieces in place, MCP Apps stop feeling like a novelty and start looking like ordinary product surface area for agent platforms. This closes the adoption loop with clear fallbacks and measurable use.