Skip to content

Commit 690e983

Browse files
committed
finish all instructions
1 parent 0d72d8a commit 690e983

File tree

11 files changed

+194
-10
lines changed

11 files changed

+194
-10
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,53 @@
11
# Tool Results
2+
3+
👨‍💼 When users interact with our journal entries, they expect immediate feedback and reliable actions. If a user deletes an entry, they need to know it actually worked - not just hope for the best. The problem is: how do we get structured, validated data back from our MCP tools so we can build intelligent workflows that respond appropriately to different outcomes?
4+
5+
```ts
6+
// Tool returns both human-readable and structured data
7+
const result = await sendMcpMessage(
8+
'tool',
9+
{
10+
toolName: 'analyze_code',
11+
params: { filePath: 'src/utils/validation.ts' },
12+
},
13+
{ schema: codeAnalysisSchema, signal: unmountSignal },
14+
)
15+
16+
// Client can access structured data and respond intelligently
17+
const { metrics, suggestions, complexity } = result.structuredContent
18+
if (complexity > 10) {
19+
showRefactorWarning(suggestions)
20+
}
21+
updateCodeMetrics(metrics)
22+
```
23+
24+
To make this happen, we use Zod schemas to validate and type the structured content returned by MCP tools. This enables rich integrations where the client can understand and act on tool results programmatically, rather than requiring manual user intervention for every step.
25+
26+
The structured content is defined by the tool's `outputSchema` and validated using Zod schemas on the client side, ensuring type safety and reliable data flow between the MCP server and client applications.
27+
28+
<callout-info>
29+
With structured content, MCP clients can build sophisticated workflows where
30+
tools can trigger automatic actions based on their results, creating a more
31+
intelligent and responsive user experience.
32+
</callout-info>
33+
34+
```mermaid
35+
sequenceDiagram
36+
User->>Iframe: Click "Delete Entry"
37+
Iframe->>App: Send tool message via postMessage
38+
App->>MCP: Call delete_entry tool
39+
MCP->>MCP: Process deletion
40+
MCP->>App: Return structured result
41+
App->>Iframe: Send result via postMessage
42+
Iframe->>Iframe: Validate with Zod schema
43+
Iframe->>Iframe: Check success status
44+
Iframe->>User: Update UI based on result
45+
```
46+
47+
The goal is to make tool interactions feel intelligent and responsive, so users can trust that their actions have real consequences and get immediate, accurate feedback about what happened.
48+
49+
Remember, our `delete_entry` tool returns structured content with a `success` boolean. You'll need to create a Zod schema to validate this data and use it in your `sendMcpMessage` call to get type-safe access to the result!
50+
51+
👨‍💼 Thanks Kellie!
52+
53+
Now, start by creating the schema, implement the tool call, and handle both success and failure cases to make your journal deletion feel reliable and responsive.

exercises/05.advanced/01.problem.tool-results/app/routes/ui/journal-viewer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ function DeleteEntryButtonImpl({
236236
// pass the schema you created above
237237
// 🦉 the result of the sendMcpMessage call should be type safe thanks to your schema
238238
// 🐨 if the result.structuredContent.success is true, call onDeleted
239+
// 🐨 if the result.structuredContent.success is false, use showBoundary to show an error
239240
throw new Error('Calling tools is not yet supported')
240241
} catch (err) {
241242
showBoundary(err)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Tool Results
2+
3+
👨‍💼 Great work! We've successfully implemented structured tool results that enable intelligent, data-driven workflows in our journal application. Our MCP UI is now more responsive. Woo!
4+
5+
🧝‍♀️ I'm going to prep us for a new requirement we have for the entry viewer. <NextDiffLink>Check out my changes</NextDiffLink> if you'd like.

exercises/05.advanced/01.solution.tool-results/app/routes/ui/journal-viewer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ function DeleteEntryButtonImpl({
237237
)
238238
if (result.structuredContent.success) {
239239
onDeleted()
240+
} else {
241+
showBoundary(new Error('Failed to delete entry'))
240242
}
241243
} catch (err) {
242244
showBoundary(err)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,42 @@
11
# Render Data
2+
3+
👨‍💼 We've got a new requirement coming up. We're going to be adding authorization to the MCP server, but our UI has no way to identify the user securely.
4+
5+
The problem is that we need a way to pass protected data directly from the MCP server to the iframe component, eliminating the need for additional API calls while maintaining security boundaries.
6+
7+
Instead, we can use MCP UI's **render data** to pass initial data directly to iframe components. This allows the server to provide all necessary context when creating UI resources.
8+
9+
```ts
10+
// Server provides initial data with the UI resource
11+
return {
12+
content: [
13+
{
14+
type: 'resource',
15+
resource: {
16+
uri: iframeUrl.toString(),
17+
mimeType: 'text/html',
18+
uiMetadata: {
19+
'initial-render-data': { bookmarks },
20+
},
21+
},
22+
},
23+
],
24+
}
25+
26+
// Client receives data as soon as it emits the `ui-lifecycle-iframe-ready` event
27+
window.addEventListener('message', function handleMessage(event: MessageEvent) {
28+
if (event.data?.type !== 'ui-lifecycle-iframe-render-data') return
29+
if (event.data.error) {
30+
// event.data.error... handle it
31+
} else {
32+
const { bookmarks } = event.data.payload
33+
// do stuff with this
34+
}
35+
})
36+
```
37+
38+
You need to implement render data for the entry viewer. The server should pass the entry data via `uiMetadata`, and the client should use `waitForRenderData` to receive it.
39+
40+
Your implementation of `waitForRenderData` should be a little more complete. If you need help, your AI assistant can help you with that! Just ask it!
41+
42+
📜 [Passing Render Data to the iframe](https://mcpui.dev/guide/embeddable-ui#passing-render-data-to-the-iframe)

exercises/05.advanced/02.problem.render-data/app/routes/ui/journal-viewer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ function DeleteEntryButtonImpl({
237237
)
238238
if (result.structuredContent.success) {
239239
onDeleted()
240+
} else {
241+
showBoundary(new Error('Failed to delete entry'))
240242
}
241243
} catch (err) {
242244
showBoundary(err)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
# Render Data
2+
3+
👨‍💼 Great work! We've successfully implemented render data for our journal entry viewer, solving a critical security problem. Now we can securely pass protected data from our MCP server to the iframe component.
4+
5+
Well done!

exercises/05.advanced/02.solution.render-data/app/routes/ui/journal-viewer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ function DeleteEntryButtonImpl({
237237
)
238238
if (result.structuredContent.success) {
239239
onDeleted()
240+
} else {
241+
showBoundary(new Error('Failed to delete entry'))
240242
}
241243
} catch (err) {
242244
showBoundary(err)

exercises/05.advanced/02.solution.render-data/app/utils/mcp.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,11 @@ const renderDataQueue: Array<{ type: string; payload: any }> = []
109109

110110
// Set up global listener immediately when module loads (only in the client)
111111
if (typeof document !== 'undefined') {
112-
window.addEventListener(
113-
'message',
114-
(event) => {
115-
if (event.data?.type === 'ui-lifecycle-iframe-render-data') {
116-
renderDataQueue.push(event.data)
117-
}
118-
},
119-
{ once: false },
120-
)
112+
window.addEventListener('message', (event) => {
113+
if (event.data?.type === 'ui-lifecycle-iframe-render-data') {
114+
renderDataQueue.push(event.data)
115+
}
116+
})
121117
}
122118

123119
export function waitForRenderData<RenderData>(
@@ -152,6 +148,6 @@ export function waitForRenderData<RenderData>(
152148
return result.success ? resolve(result.data) : reject(result.error)
153149
}
154150

155-
window.addEventListener('message', handleMessage, { once: true })
151+
window.addEventListener('message', handleMessage)
156152
})
157153
}

exercises/05.advanced/FINISHED.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
11
# Advanced
2+
3+
Congratulations! You've successfully implemented two powerful advanced features that make MCP UI significantly more capable.
4+
5+
These advanced features transform your MCP UI from simple display components into intelligent, data-driven interfaces that can build sophisticated workflows.
6+
7+
You're now ready to build production-ready MCP UI applications!

0 commit comments

Comments
 (0)