MCP-UI Resources#
MCP-UI lets an MCP server return a renderable UI — raw HTML, an iframe URL, or a Remote-DOM script — from a tool result instead of plain text. A UI-capable client (e.g. the JS @mcp-ui/client) renders it inline; a text-only client falls back to the embedded resource.
AG2 builds these resources and the interactive callbacks (UI Actions) in ag2.mcp_ui. They are returned from the serving side of ag2.mcp — an AG2 Agent exposed as an MCP server — so this page starts with the minimum you need to serve one, then focuses on the UI helpers.
This is the serving side of MCP
ag2.mcp.MCPServer exposes an agent as an MCP server. This is the inverse of consuming an MCP server as tools with MCPToolkit / MCPServerTool — see MCP Servers for that.
Installation#
ag2.mcp_ui ships as an optional extra (it is not pulled in by ag2[mcp]):
Importing ag2.mcp_ui without it raises a clear install hint.
Serving a UI resource#
MCPServer wraps an agent as a single conversational ask tool. To return a UI resource you add a custom tool with the @mcp_tool decorator: the client sees it in tools/list, and a tools/call runs your handler directly — the agent is not invoked, so the result is deterministic. The handler returns the MCP content block(s) verbatim, which is exactly what raw_html(...) produces.
@mcp_tool (from ag2.mcp) derives the tool name from the function name, the description from its docstring, and the inputSchema from the typed signature — the same convention as ag2.tools.tool. The handler may be sync or async, and may return a plain string (wrapped in a text block) instead of content blocks. Point a UI-capable MCP client (or the MCP Inspector) at this stdio server, call show_greeting, and the HTML renders inline.
Optional title and annotations keyword arguments are advertised in tools/list; annotations takes mcp.types.ToolAnnotations behavior hints (readOnlyHint, destructiveHint, …) that hosts use to decide e.g. whether to confirm with the user before calling:
Why a custom tool and not the ask tool
The agent's ask tool replies with model-generated text/images, so it cannot emit a deterministic UI blob on its own. Always return UI resources from your own @mcp_tool.
Accessing the request context#
Annotate a parameter (any name) with ag2.mcp.tools.MCPRequestContext to receive the live MCP request context — the session, client parameters, and lifespan state. The parameter is excluded from the advertised inputSchema, so clients never see it:
UI resource types#
ag2.mcp_ui provides three builders, one per MCP-UI content type. Each returns an mcp.types.EmbeddedResource — a valid tools/call content block — and each uri must start with ui://.
| Builder | Content type | Client renders it as |
|---|---|---|
raw_html(uri, html) | rawHtml | an inline HTML string |
external_url(uri, url) | externalUrl | url embedded in an iframe |
remote_dom(uri, script) | remoteDom | a mounted Remote-DOM script |
Every builder accepts encoding="blob" to base64-encode the payload for transports or content that don't survive as inline text (the default is encoding="text"):
Rendering metadata#
Two optional keyword arguments attach metadata to the resource:
ui_metadata— client-side rendering hints. Keys are prefixed withmcpui.dev/ui-so the client recognizes them.metadata— written verbatim into the resource_meta.
UI Actions#
A rendered UI can carry interactive elements. When the user interacts, the element sends a message to the host with window.parent.postMessage, and the host's onUIAction handler reacts. ag2.mcp_ui builds those message payloads.
UI Actions are dispatched by the host, not your server
Unlike ag2.a2ui actions, these are not server-side handlers. The MCP-UI spec puts the host in control: a UI Action is dispatched client-side (onUIAction). A tool action just asks the host to call an ordinary MCP tool on your server — expose that tool the normal way; there is no server-side "action handler" to register.
These are authoring helpers: embed the built action into the UI with post_message(action), which returns an HTML-attribute-safe window.parent.postMessage(...) call (quotes and apostrophes in the payload are escaped, so they cannot break out of the attribute).
The five action types map one-to-one to the spec:
| Builder | Action | What the host does |
|---|---|---|
tool_call(name, params) | tool | call the MCP tool name(params) (tools/call) |
prompt(text) | prompt | send text into the conversation as a follow-up |
link(url) | link | open url |
intent(name, params) | intent | emit a host-defined name intent with params |
notify(message) | notify | receive message as a notification / log |
Closing the loop with a tool action#
Because a tool action asks the host to call tools/call, you close the interaction loop by exposing the target as another @mcp_tool on the same server:
Wire both into MCPServer(agent, tools=[show_greeting, add_to_cart]). When the user clicks the button, the host turns the tool action into a tools/call add_to_cart back to your server, and the returned card replaces the UI.
Runnable example
A complete stdio server — greeting card, an iframe of the docs, and the add_to_cart loop — lives in examples/mcp/server_ui.py.