Skip to content

Commit 0ec1414

Browse files
committed
exercise 3 instructions done
1 parent c085cbe commit 0ec1414

File tree

11 files changed

+339
-1
lines changed

11 files changed

+339
-1
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,80 @@
11
# iframe
2+
3+
👨‍💼 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?
4+
5+
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.
6+
7+
```ts
8+
// Create an iframe-based UI resource for a project dashboard
9+
const resource = createUIResource({
10+
uri: `ui://project-dashboard/${Date.now()}`,
11+
content: {
12+
type: 'externalUrl',
13+
iframeUrl: 'https://myapp.com/dashboard/project-123',
14+
},
15+
encoding: 'text',
16+
})
17+
```
18+
19+
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.
20+
21+
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.
22+
23+
## Request Info
24+
25+
In the MCP TypeScript SDK, tools can receive two arguments:
26+
27+
1. `args` - the arguments passed to the tool
28+
2. `extra` - extra information about the request
29+
30+
If the tool does not have an inputSchema, the first argument will be the "extra" object which includes the requestInfo object.
31+
32+
When constructing iframe URLs, we need to use the origin from the request headers.
33+
34+
Here's how to access the origin from the request headers in the tool handler:
35+
36+
```ts
37+
agent.server.registerTool(
38+
'get_dashboard',
39+
{
40+
title: 'Get Dashboard',
41+
description: 'Get the dashboard for a project',
42+
// no inputSchema means the first argument to our tool handler will be the "extra" object which includes the requestInfo object
43+
},
44+
// In your tool handler, the requestInfo object contains the request headers
45+
async ({ requestInfo }) => {
46+
const origin = requestInfo.headers['x-origin']
47+
// origin would be something like https://example.com
48+
// ...
49+
},
50+
)
51+
```
52+
53+
<callout-warning>
54+
We need to add a custom `x-origin` header to the MCP request so our tool
55+
handler knows where to set the full iframe URL. This header is added in the
56+
worker's fetch handler before forwarding the request to the MCP server.
57+
</callout-warning>
58+
59+
The worker sets up the custom header like this:
60+
61+
```ts
62+
// In worker/index.ts
63+
if (url.pathname === '/mcp') {
64+
// clone the request headers
65+
const headers = new Headers(request.headers)
66+
// add the custom header
67+
headers.set('x-origin', url.origin)
68+
// clone the request with the new headers
69+
const newRequest = new Request(request, { headers })
70+
71+
return EpicMeMCP.serve('/mcp', {
72+
binding: 'EPIC_ME_MCP_OBJECT',
73+
// pass the newRequest instead of request
74+
}).fetch(newRequest, env, ctx)
75+
}
76+
```
77+
78+
This ensures that when our tool handler receives the request, it has access to the origin information needed to construct the proper iframe URL.
79+
80+
- 📜 [MCP UI Embeddable UI Documentation](https://mcpui.dev/guide/embeddable-ui).

exercises/03.complex/01.problem.iframe/worker/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const requestHandler = createRequestHandler(
1010
export default {
1111
fetch: async (request, env, ctx) => {
1212
const url = new URL(request.url)
13+
1314
if (url.pathname === '/mcp') {
1415
// 🐨 create a headers object based on the request
1516
// - then add the x-origin header set to the url.origin
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# iframe
2+
3+
👨‍💼 Great work! We've successfully solved a key problem for our users: now they can view their journal entries in a rich, interactive interface instead of just plain text. This makes the journal viewing experience much more engaging and professional.
4+
5+
This improvement means users get a full-featured journal viewer with proper styling, responsive design, and interactive elements. The iframe approach gives us access to the entire web ecosystem while maintaining secure communication with the host application.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,41 @@
11
# Ready
2+
3+
👨‍💼 When iframes load in AI chat, the host application doesn't know when they're ready to receive data or handle interactions. Without this handshake, users see broken interfaces or endless loading states.
4+
5+
```tsx
6+
// Notify the host that the iframe is ready
7+
useEffect(() => {
8+
window.parent.postMessage({ type: 'ui-lifecycle-iframe-ready' }, '*')
9+
}, [])
10+
```
11+
12+
The `ui-lifecycle-iframe-ready` message tells the host that the iframe has loaded and is ready for communication. This enables the host to send initial data and makes interactions work properly.
13+
14+
```mermaid
15+
sequenceDiagram
16+
participant User
17+
participant Host
18+
participant Iframe
19+
20+
User->>Host: Requests journal viewer
21+
Host->>Host: Creates iframe element
22+
Host->>Iframe: Loads iframe content
23+
Iframe->>Iframe: Component mounts
24+
Iframe->>Host: postMessage('ui-lifecycle-iframe-ready')
25+
Host->>Host: Marks iframe as ready
26+
Host->>Iframe: Sends initial data
27+
Iframe->>Iframe: Renders with data
28+
Iframe->>User: Shows interactive interface
29+
```
30+
31+
<callout-warning class="important">
32+
Always send the ready message as soon as the component mounts. Delaying this
33+
message means the host application will wait indefinitely.
34+
</callout-warning>
35+
36+
<callout-muted>
37+
📜 For more details on the MCP UI lifecycle protocol, see the [MCP UI
38+
Embeddable UI documentation](https://mcpui.dev/guide/embeddable-ui).
39+
</callout-muted>
40+
41+
Now, implement the lifecycle communication to make your iframe ready for interaction!
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
# Ready
2+
3+
👨‍💼 Great work! We've successfully solved a critical communication problem for our users: now when iframes load in AI chat, they immediately tell the host application they're ready to receive data and handle interactions. This eliminates broken interfaces and endless loading states that frustrated users before.
4+
5+
This improvement means users get smooth, responsive experiences where embedded applications feel integrated with the host application from the moment they load. The handshake ensures data flows properly and interactions work reliably.
6+
7+
Let's keep going to make the experience even better!
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,16 @@
11
# Sizing
2+
3+
👨‍💼 When users view their journal entries, they expect the interface to be sized appropriately for comfortable reading. A cramped window makes it hard to read entries, while an oversized frame wastes screen space.
4+
5+
Add `uiMetadata` with `preferred-frame-size` to specify the ideal dimensions for the journal viewer:
6+
7+
```ts
8+
uiMetadata: {
9+
// width and height in pixels
10+
'preferred-frame-size': ['400px', '600px'],
11+
}
12+
```
13+
14+
The `preferred-frame-size` key accepts an array with width and height values. The UI system will try to respect your preferred size based on available screen space.
15+
16+
Now, add the `uiMetadata` to make the journal viewer shine.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Sizing
2+
3+
👨‍💼 Great work! We've successfully solved a key problem for our users: now the journal viewer displays at the optimal size for comfortable reading. This makes the interface much more user-friendly and professional, which is exactly what our users expect.
4+
5+
This improvement means users get a properly sized viewing experience without having to manually resize the frame. The journal entries now appear in a well-proportioned window that makes reading and interacting with content much more pleasant.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,47 @@
11
# Dynamic Sizing
2+
3+
👨‍💼 Users expect iframes to fit their content perfectly, not waste space or require scrolling. The journal viewer should automatically adjust to show exactly what's needed.
4+
5+
```tsx
6+
const height = document.documentElement.scrollHeight
7+
const width = document.documentElement.scrollWidth
8+
9+
window.parent.postMessage(
10+
{
11+
type: 'ui-size-change',
12+
payload: { height, width },
13+
},
14+
'*',
15+
)
16+
```
17+
18+
Use `scrollHeight` and `scrollWidth` to measure the full content dimensions, then send a `ui-size-change` message to the parent window so it can resize the iframe accordingly.
19+
20+
The static `preferred-frame-size` from the previous exercise works for fixed layouts, but journal entries have varying content lengths. A short entry wastes space, while a long entry gets cut off.
21+
22+
<callout-warning>
23+
Just make sure you utilize scrolled elements to avoid requesting a ton of
24+
height or width with a lot of content.
25+
</callout-warning>
26+
27+
```mermaid
28+
sequenceDiagram
29+
participant User
30+
participant Host
31+
participant Iframe
32+
33+
User->>Host: Request taco map directions
34+
Host->>Iframe: Load iframe with static size
35+
Iframe->>Host: Send ui-lifecycle-iframe-ready
36+
Iframe->>Iframe: Measure actual map content
37+
Iframe->>Host: Send ui-size-change with real dimensions
38+
Host->>Host: Resize iframe to fit map
39+
Host->>User: Show perfectly sized taco map
40+
```
41+
42+
<callout-info>
43+
Measure after the component renders to get accurate dimensions. This typically
44+
happens so fast the user won't notice.
45+
</callout-info>
46+
47+
Now implement the size measurement to make the journal viewer fit perfectly.
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
# Dymamic Sizing
1+
# Dynamic Sizing
2+
3+
👨‍💼 Perfect! Our journal viewer now measures its content and tells the host exactly how much space it needs. Short entries no longer waste space, and long entries display completely without scrolling.
4+
5+
The iframe now feels like a native part of the application (while preserving our app's branding) by automatically adapting to show each journal entry at its optimal size.
6+
7+
🧝‍♀️ I'm going to abstract some of this stuff in a custom hook to make it easier to use. I'll also be adding a utility we'll use to handle abort signals if a component unmounts. We'll use that later. Feel free to do that yourself or just <NextDiffLink>check out my changes</NextDiffLink>.

exercises/03.complex/FINISHED.mdx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,13 @@
11
# Complex
2+
3+
Congratulations! You've successfully implemented iframe-based UI components in your MCP server. You've moved from simple HTML and Remote DOM approaches to embedding full web applications that can leverage entire frameworks and provide rich, interactive experiences.
4+
5+
<callout-success>
6+
The key improvement is that your UI components now have access to the full
7+
ecosystem of web technologies while maintaining secure communication with the
8+
host application through the standardized postMessage protocol.
9+
</callout-success>
10+
11+
You learned how to create iframe-based UI resources using the `externalUrl` content type, set up proper communication between iframe and host, handle lifecycle events, and implement responsive sizing. This approach gives you the flexibility to build sophisticated applications that would be cumbersome to maintain with raw HTML or Remote DOM.
12+
13+
Let's keep moving to continue making the user experience even better with more advanced UI capabilities!

0 commit comments

Comments
 (0)