iframe

👨‍💼 When users interact with our AI assistant, they expect rich, interactive experiences that go beyond simple text responses. Whether they're viewing a detailed project dashboard or exploring a complex data visualization, they want to see and interact with full-featured interfaces that feel like native applications. The problem is: how do we provide these sophisticated UI experiences within the constraints of MCP?
The solution is iframe-based UI components - embedding full web applications as UI resources that can leverage entire frameworks and provide rich, interactive experiences while maintaining secure communication with the host application.
// Create an iframe-based UI resource for a project dashboard
const resource = createUIResource({
	uri: `ui://project-dashboard/${Date.now()}`,
	content: {
		type: 'externalUrl',
		iframeUrl: 'https://myapp.com/dashboard/project-123',
	},
	encoding: 'text',
})
To make this happen, we use the externalUrl content type in MCP UI. This allows us to embed complete web applications that can handle complex state management, rich interactions, and responsive design - all while communicating securely with the host through a standardized protocol.
The key advantage is that instead of building everything from scratch with raw HTML or struggling with the limitations of Remote DOM, we can leverage the full ecosystem of web technologies. Users get interfaces that feel like they belong in a modern application, not a basic chat interface.

Request Info

In the MCP TypeScript SDK, tools can receive two arguments:
  1. args - the arguments passed to the tool
  2. extra - 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 custom x-origin header 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.

Please set the playground first

Loading "iframe"
Loading "iframe"
Login to get access to the exclusive discord channel.