[MCP] iframe-based UI components
While raw HTML and Remote DOM provide great ways to create UI components, they have limitations when it comes to building truly complex, interactive applications. Raw HTML requires you to write all styling from scratch, and Remote DOM, while consistent, can be cumbersome for complex layouts and interactions. For sophisticated applications that need rich functionality, responsive design, and complex state management, you need a more powerful approach.
The solution is iframe-based UI components - embedding full web applications (though normally just widgets rather than fully routed solutions) as UI resources in MCP. This approach lets you leverage entire frameworks like React, Vue, or any other web technology to create rich, interactive interfaces that can communicate with the host application through a standardized protocol.
Example:
import { createUIResource } from '@mcp-ui/server'
// Create a UI resource with an iframe for a task management widget
const resource = createUIResource({
uri: `ui://task-dashboard/${Date.now()}`,
content: {
type: 'externalUrl',
iframeUrl: 'https://example.com/widgets/task-dashboard',
},
encoding: 'text',
uiMetadata: {
'preferred-frame-size': ['1200px', '800px'],
},
})
Iframe-based UI components use the
externalUrlcontent type to embed full web applications. TheuiMetadataallows you to specify preferred sizing for the iframe, giving the host application hints about optimal dimensions.
Here's how this works in practice. When an AI assistant needs to show a complex journal viewer with interactive features like deleting entries, viewing details, and summarizing content, instead of trying to build this with raw HTML or Remote DOM, it can embed a full React application that handles all the complexity.
The iframe approach provides several key advantages:
- Full Framework Support: Use any web framework. If it can serve a web page, it can be embedded in an iframe.
- Complex State Management: Handle sophisticated application state with libraries like Redux, Zustand, or Context
- Rich Interactions: Build complex user interactions with full access to browser APIs
- Responsive Design: Leverage CSS frameworks and responsive design patterns
- Communication Protocol: Standardized
postMessageAPI for iframe-to-host communication
Key message types include:
ui-lifecycle-iframe-ready- Notify host that iframe is ready to receive messagesui-size-change- Inform host when iframe dimensions changetool- Request host to execute an MCP toolprompt- Request host to send a prompt to the AIlink- Request host to navigate to a URL
Request Info
In the MCP TypeScript SDK, tools can receive two arguments:
args- the arguments passed to the toolextra- extra information about the request
If the tool does not have an inputSchema, the first argument will be the "extra" object which includes the requestInfo object.
When constructing iframe URLs, we need to use the origin from the request headers.
Here's how to access the origin from the request headers in the tool handler:
agent.server.registerTool(
'get_dashboard',
{
title: 'Get Dashboard',
description: 'Get the dashboard for a project',
// no inputSchema means the first argument to our tool handler will be the "extra" object which includes the requestInfo object
},
// In your tool handler, the requestInfo object contains the request headers
async ({ requestInfo }) => {
const origin = requestInfo.headers['x-origin']
// origin would be something like https://example.com
// ...
},
)
We need to add a customx-originheader to the MCP request so our tool handler knows where to set the full iframe URL. This header is added in the worker's fetch handler before forwarding the request to the MCP server.
The worker sets up the custom header like this:
// In worker/index.ts
if (url.pathname === '/mcp') {
// clone the request headers
const headers = new Headers(request.headers)
// add the custom header
headers.set('x-origin', url.origin)
// clone the request with the new headers
const newRequest = new Request(request, { headers })
return EpicMeMCP.serve('/mcp', {
binding: 'EPIC_ME_MCP_OBJECT',
// pass the newRequest instead of request
}).fetch(newRequest, env, ctx)
}
This ensures that when our tool handler receives the request, it has access to the origin information needed to construct the proper iframe URL.
Code:
worker/index.ts
import { createRequestHandler } from 'react-router'
import { db } from './db.ts'
import { EpicMeMCP, type Props as McpProps } from './mcp/index.ts'
const requestHandler = createRequestHandler(
() => import('virtual:react-router/server-build'),
import.meta.env.MODE,
)
export default {
fetch: async (
request: Request,
env: Env,
ctx: ExecutionContext<McpProps>,
) => {
const url = new URL(request.url)
// Tell the MCP agent what the base URL for the MCP server is
if (url.pathname === '/mcp') {
// Passing the base URL to the MCP agent allows it to construct URLs for UI resources
ctx.props.baseUrl = url.origin
return EpicMeMCP.serve('/mcp', {
binding: 'EPIC_ME_MCP_OBJECT',
}).fetch(request, env, ctx)
}
return requestHandler(request, {
db,
cloudflare: { env, ctx },
})
},
}
export { EpicMeMCP }
worker/mcp/index.ts:
export class EpicMeMCP extends McpAgent<Env, State, Props> {
db!: DBClient
server = new McpServer(
...
)
async init() {
this.db = db
await initializeTools(this)
await initializeResources(this)
await initializePrompts(this)
}
// Helper function for MCP UI to get the baseUrl
requireBaseUrl() {
const { baseUrl } = this.props ?? {}
invariant(baseUrl, 'Unexpected: baseUrl not set on agent')
return baseUrl
}
}
worker/mcp/tools.ts
agent.server.registerTool(
'view_journal',
{
title: 'View Journal',
description: 'View the journal visually',
annotations: {
readOnlyHint: true,
openWorldHint: false,
} satisfies ToolAnnotations,
},
async () => {
const baseUrl = agent.requireBaseUrl()
// Visit our full-stack application on this path
const iframeUrl = new URL('/ui/journal-viewer', baseUrl)
return {
content: [
await createUIResource({
uri: `ui://view-journal/${Date.now()}`,
content: {
type: 'externalUrl',
iframeUrl: iframeUrl.toString(),
},
encoding: 'text',
uiMetadata: {
'preferred-frame-size': ['600px', '800px'],
},
}),
],
}
},
)

浙公网安备 33010602011771号