Skip to content

Commit 0d72d8a

Browse files
committed
enhance interactive communication in iframe components with MCP UI protocol, adding support for link, tool, and prompt messages
1 parent 0ec1414 commit 0d72d8a

File tree

9 files changed

+221
-18
lines changed

9 files changed

+221
-18
lines changed

exercises/03.complex/README.mdx

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,6 @@ The iframe approach provides several key advantages:
3939
4. **Responsive Design**: Leverage CSS frameworks and responsive design patterns
4040
5. **Communication Protocol**: Standardized `postMessage` API for iframe-to-host communication
4141

42-
<callout-success>
43-
From [the MCP UI embeddable UI
44-
documentation](https://mcpui.dev/guide/embeddable-ui): Iframe-based UI
45-
components communicate with the parent window via `postMessage`, enabling rich
46-
interactions while maintaining security boundaries between the iframe and host
47-
application.
48-
</callout-success>
49-
50-
The communication protocol between iframe and host follows a standardized message structure:
51-
52-
```typescript
53-
type Message = {
54-
type: string
55-
messageId?: string // optional, used for tracking the message
56-
payload: Record<string, unknown>
57-
}
58-
```
59-
6042
Key message types include:
6143

6244
- `ui-lifecycle-iframe-ready` - Notify host that iframe is ready to receive messages
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,45 @@
11
# Navigate to Links
2+
3+
👨‍💼 Our users want to be able to share how many entries they have in their journal on 𝕏. We can use `postMessage` to send a link to the host application to navigate to the 𝕏 intent page.
4+
5+
Since iframes can't directly navigate their parent window, we need to communicate with the host application. The pattern is:
6+
7+
1. **Generate unique ID** - Create a message ID to match requests with responses
8+
2. **Send message** - Use `window.parent.postMessage()` with type `'link'` and the URL
9+
3. **Listen for response** - Handle the `'ui-message-response'` from the host
10+
4. **Clean up** - Remove event listener when done
11+
12+
The communication pattern looks like:
13+
14+
```ts
15+
// Send request with unique ID
16+
window.parent.postMessage(
17+
{
18+
type: 'link',
19+
messageId: uniqueId,
20+
payload: { url },
21+
},
22+
'*',
23+
)
24+
25+
// Listen for matching response
26+
function handleMessage(event) {
27+
if (
28+
event.data.type === 'ui-message-response' &&
29+
event.data.messageId === uniqueId
30+
) {
31+
// Handle success/error
32+
}
33+
}
34+
```
35+
36+
🧝‍♀️ I've set up the journal viewer with a "Share on X" button that needs to navigate to Twitter's intent page. You'll need to implement the communication function to make this work!
37+
38+
<callout-success>
39+
🦉 Depending on how comfortable you are with browser APIs, you may struggle
40+
with following the instructions. However, your AI assistant is really good at
41+
browser APIs. Just feed it the instructions and you'll be set! If you don't
42+
understand something, just ask it!
43+
</callout-success>
44+
45+
👨‍💼 Perfect! Let's implement that communication pattern.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Navigate to Links
2+
3+
👨‍💼 Super! Now users can click buttons in our iframe-based UI components and navigate to external links seamlessly. Users can now share their journal entries on social media!
4+
5+
🧝‍♀️ I'm going to be doing more of what we just did and adding some utilities to make it easier. We're going to turn this into a general utility you'll be able to use for other actions like `tool` and `prompt`. If you want extra work, you can try to do that yourself or just <NextDiffLink>check out my changes</NextDiffLink>.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,21 @@
11
# Call Tools
2+
3+
👨‍💼 When users click "View Entry" on a journal entry, nothing happens. They need a way to trigger tool calls that fetch detailed data from the host application.
4+
5+
You're going to build upon the abstraction you and 🧝‍♀️ Kellie built in the last exercise so you can use it for `tool` calls as well:
6+
7+
```ts
8+
await sendMcpMessage(
9+
'tool',
10+
{ toolName: 'analyze_rock_sample', params: { sampleId: 'mars-2024-001' } },
11+
{ signal: unmountSignal },
12+
)
13+
```
14+
15+
🧝‍♀️ Note: I added support for `signal` to the `sendMcpMessage` function so you can cancel the tool call if the component unmounts.
16+
17+
A component can unmount when it's removed from the page. Without canceling pending tool calls, you'll get memory leaks and potentially update state on unmounted components, causing React warnings and unexpected behavior.
18+
19+
I've set up `useUnmountSignal` for you. You need to add the 'tool' type to `sendMcpMessage` and wire up the actual tool call.
20+
21+
👨‍💼 Thanks Kellie! Let's implement this to make those journal entries interactive.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
# Call Tools
2+
3+
👨‍💼 We can now call MCP tools directly from within the UI components. This makes the interface truly interactive and functional, which is exactly what our users need for a complete experience.
4+
5+
This improvement means users get immediate access to powerful functionality without leaving the interface. Instead of just viewing data, they can now take actions like deleting tags, creating entries, and performing other operations seamlessly. This transforms the UI from a static display into a dynamic, interactive workspace.
6+
7+
🧝‍♀️ I'm going to add this to the delete button in the entry-viewer as well. Feel free to <NextDiffLink>check out my changes</NextDiffLink>.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,25 @@
11
# Send Prompts
2+
3+
👨‍💼 Our users expect the "Summarize" button to instantly craft a short, insightful take on the entry they're viewing. To make that happen, the UI needs to send a natural-language prompt to the host via our MCP bridge, then show the result without a page reload. If we don't deliver, people lose trust and the feature feels broken.
4+
5+
Here's the pattern you'll use from components:
6+
7+
```ts
8+
// Send a prompt message to the host
9+
await sendMcpMessage(
10+
'prompt',
11+
{
12+
prompt:
13+
'Please ask CafeLedger get_balance for account espresso-ops and summarize the budget health.',
14+
},
15+
{ signal: unmountSignal },
16+
)
17+
```
18+
19+
What you need to build:
20+
21+
- Add a new `'prompt'` message type to `app/utils/mcp.ts` so the bridge can validate payloads.
22+
- Add a function overload for `sendMcpMessage('prompt', payload, options)` that mirrors the `'link'` overload but accepts `{ prompt: string }`.
23+
- In the summarize button component, get an `AbortSignal` via `useUnmountSignal()` and call `sendMcpMessage('prompt', { prompt }, { signal })`.
24+
25+
👨‍💼 Let's wire up the prompt path so pressing "Summarize" works.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
11
# Send Prompts
2+
3+
👨‍💼 Big win here. Users can now hit "Summarize" and immediately get a concise, insightful take on the entry they're viewing. This closes the loop between curiosity and clarity: they ask, the system interprets, and the answer shows up without disrupting their flow.
4+
5+
Under the hood, we wired the iframe to send a `prompt` message through our MCP bridge and handle the response cleanly:
6+
7+
- **New capability**: `sendMcpMessage('prompt', { prompt }, { signal })` so components can request AI assistance directly.
8+
- **UX benefit**: No reloads, no context loss. The UI stays responsive while the request is in flight.
9+
- **Reliability**: Requests are abortable via `useUnmountSignal()`, so we don't leak work when users navigate away.
10+
11+
This turns "Summarize" from a promise into a dependable action. Users now trust that our UI can ask the right question and deliver the right kind of answer at the right moment.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
# Interactive
2+
3+
Congratulations! You've successfully implemented interactive communication in your iframe-based UI components. You've transformed static iframes into dynamic components that can communicate with the host application to trigger actions, execute tools, and send prompts.
4+
5+
Your iframes now actively communicate with the host using the MCP UI communication protocol, enabling rich interactions while maintaining security boundaries.
6+
7+
You learned how to send `link`, `tool`, and `prompt` messages from your iframe to the host, with proper error handling and cleanup using AbortSignal.
8+
9+
Now on to the next!
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,106 @@
11
# Interactive
2+
3+
While iframe-based UI components provide rich, visual interfaces, they need a way to communicate back to the host application to trigger actions, request data, or request links be opened. Without this communication, iframes would be isolated islands that can't integrate with the broader application ecosystem.
4+
5+
The solution is the **MCP UI communication protocol** - a standardized way for iframe-based UI components to send messages to their host application using the `postMessage` API. This enables rich interactions like calling MCP tools, sending prompts to AI assistants, navigating to external links, and requesting data from the host.
6+
7+
Example:
8+
9+
```ts
10+
// Send a link navigation request to the host
11+
await sendLinkMcpMessage('https://www.example.com/snowboarding/')
12+
13+
// Call an MCP tool from within the iframe
14+
await sendMcpMessage(
15+
'tool',
16+
{
17+
toolName: 'generate_haiku',
18+
params: { theme: 'quantum computing', mood: 'playful' },
19+
},
20+
{ signal: unmountSignal },
21+
)
22+
23+
// Send a prompt to the AI assistant
24+
await sendMcpMessage(
25+
'prompt',
26+
{
27+
prompt:
28+
'Create a recipe for a fusion dish combining Japanese and Mexican cuisine',
29+
},
30+
{ signal: unmountSignal },
31+
)
32+
```
33+
34+
<callout-info>
35+
The MCP UI communication protocol uses `postMessage` to enable secure,
36+
bidirectional communication between iframe-based UI components and their host
37+
application. This allows iframes to trigger actions in the host while
38+
maintaining security boundaries.
39+
</callout-info>
40+
41+
Here's how this works in practice. When a user clicks a "Watch Launch" button in your space exploration dashboard iframe, instead of opening a new tab or trying to navigate directly, the iframe sends a `link` message to the host application. The host then handles the navigation, ensuring it follows the application's routing and security policies.
42+
43+
The communication protocol supports several key message types:
44+
45+
1. **`link`** - Request host to navigate to a URL
46+
2. **`tool`** - Request host to execute an MCP tool
47+
3. **`prompt`** - Request host to send a prompt to the AI assistant
48+
4. **`intent`** - Express user intent for the host to act upon
49+
5. **`notify`** - Notify host of side effects from user interactions
50+
51+
<callout-info>
52+
`intent` and `notify` are not used in this exercise as they are not relevant
53+
for general use AI agent apps and typically require specific server-client
54+
integration.
55+
</callout-info>
56+
57+
The message structure follows a consistent pattern:
58+
59+
```typescript
60+
type Message = {
61+
type: string
62+
messageId?: string // optional, used for tracking the message
63+
payload: Record<string, unknown>
64+
}
65+
```
66+
67+
Here's a sequence diagram showing how interactive communication works:
68+
69+
```mermaid
70+
sequenceDiagram
71+
participant User
72+
participant Iframe
73+
participant Host
74+
participant Client
75+
participant Server
76+
77+
User->>Iframe: Click "Watch Launch" button
78+
Iframe->>Iframe: Generate messageId
79+
Iframe->>Host: Send link message via postMessage
80+
Host->>Host: Handle navigation request
81+
Host->>Iframe: Send ui-message-response
82+
Iframe->>Iframe: Handle response/error
83+
Iframe->>User: Show success/error feedback
84+
85+
User->>Iframe: Click "Generate Haiku" button
86+
Iframe->>Host: Send tool message via postMessage
87+
Host->>Client: Execute MCP tool
88+
Client->>Server: Call generate_haiku tool
89+
Server->>Client: Return haiku data
90+
Client->>Host: Return tool result
91+
Host->>Iframe: Send ui-message-response with data
92+
Iframe->>Iframe: Update UI with haiku
93+
Iframe->>User: Display creative haiku
94+
```
95+
96+
In this exercise, you'll implement interactive communication in your iframe-based UI components. You'll learn how to:
97+
98+
- Send `link` messages to request host navigation to external URLs
99+
- Send `tool` messages to execute MCP tools from within the iframe
100+
- Send `prompt` messages to interact with AI assistants
101+
- Handle asynchronous responses and errors properly
102+
- Implement proper cleanup with AbortSignal
103+
104+
The key difference from previous exercises is that instead of just displaying data, your iframe now actively communicates with the host application to trigger actions, making it a truly interactive component that integrates seamlessly with the broader application ecosystem.
105+
106+
- 📜 [MCP UI Embeddable UI Documentation](https://mcpui.dev/guide/embeddable-ui)

0 commit comments

Comments
 (0)