Skip to content

Commit 06dde3a

Browse files
committed
chore(poc): new implementation
1 parent 31e7ee2 commit 06dde3a

File tree

5 files changed

+138
-9
lines changed

5 files changed

+138
-9
lines changed

apps/mcp-ui-poc/client/src/components/chat-interface.tsx

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type { Message } from "../types/virtual-dom";
1717
import {
1818
StructuredDomRenderer,
1919
type StructuredDomContent,
20+
type Intent,
2021
} from "./structured-dom-renderer";
2122

2223
export function ChatInterface() {
@@ -159,6 +160,67 @@ export function ChatInterface() {
159160
}
160161
}
161162

163+
// Handle intents emitted from UI components
164+
async function handleIntentEmit(intent: Intent) {
165+
console.log("🎯 Intent received:", intent);
166+
167+
// Combine description + structured payload for Claude
168+
const message = `${intent.description}
169+
170+
[Intent Context]
171+
Type: ${intent.type}
172+
Payload: ${JSON.stringify(intent.payload, null, 2)}`;
173+
174+
console.log("💬 Forwarding intent to Claude:", message);
175+
176+
// Add user message to chat (show cleaner version to user)
177+
const userMessage: Message = {
178+
role: "user",
179+
content: `[Intent: ${intent.type}] ${intent.description}`,
180+
};
181+
setMessages((prev) => [...prev, userMessage]);
182+
183+
setIsLoading(true);
184+
185+
try {
186+
// Build message history for context
187+
const messageHistory = messages.map((msg) => ({
188+
role: msg.role,
189+
content: msg.content || "",
190+
}));
191+
192+
// Send to Claude with full description + payload
193+
const { text, uiResources } = await claudeClient.sendMessage(message, {
194+
uiToolsEnabled,
195+
commerceToolsEnabled,
196+
messageHistory,
197+
});
198+
199+
// Add Claude's response
200+
setMessages((prev) => [
201+
...prev,
202+
{
203+
role: "assistant",
204+
content: text,
205+
uiResources,
206+
},
207+
]);
208+
209+
console.log("✅ Intent processed successfully");
210+
} catch (error) {
211+
console.error("❌ Error handling intent:", error);
212+
setMessages((prev) => [
213+
...prev,
214+
{
215+
role: "assistant",
216+
content: `Error: ${(error as Error).message}`,
217+
},
218+
]);
219+
} finally {
220+
setIsLoading(false);
221+
}
222+
}
223+
162224
// Show initialization error if present
163225
if (initError) {
164226
return (
@@ -305,7 +367,10 @@ export function ChatInterface() {
305367
console.log("✨ Using StructuredDomRenderer");
306368
return (
307369
<Box key={i} marginTop="300">
308-
<StructuredDomRenderer content={parsedContent} />
370+
<StructuredDomRenderer
371+
content={parsedContent}
372+
onIntentEmit={handleIntentEmit}
373+
/>
309374
</Box>
310375
);
311376
} catch (error) {

apps/mcp-ui-poc/client/src/components/structured-dom-renderer.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
import React from "react";
22
import { componentMap } from "./nimbus-library";
33

4+
/**
5+
* Intent structure matching server-side definition
6+
*/
7+
export interface Intent {
8+
type: string;
9+
description: string;
10+
payload: Record<string, unknown>;
11+
}
12+
413
/**
514
* Types matching server-side definitions
615
*/
716
export interface ElementDefinition {
817
tagName: string;
918
attributes?: Record<string, string | boolean | number | undefined>;
1019
children?: (ElementDefinition | string)[];
20+
events?: {
21+
onPress?: Intent;
22+
};
1123
}
1224

1325
export interface StructuredDomContent {
@@ -21,7 +33,8 @@ export interface StructuredDomContent {
2133
*/
2234
function renderElement(
2335
element: ElementDefinition,
24-
index: number = 0
36+
index: number = 0,
37+
onIntentEmit?: (intent: Intent) => void
2538
): React.ReactNode {
2639
// Find the corresponding Nimbus component
2740
const Component = componentMap[element.tagName];
@@ -32,14 +45,23 @@ function renderElement(
3245
}
3346

3447
// ✅ Pass attributes directly - server sends camelCase React props
35-
const props = element.attributes || {};
48+
const props: Record<string, unknown> = { ...(element.attributes || {}) };
49+
50+
// Attach event handlers if present
51+
if (element.events?.onPress && onIntentEmit) {
52+
const intent = element.events.onPress;
53+
props.onPress = () => {
54+
console.log("🎯 Intent emitted:", intent);
55+
onIntentEmit(intent);
56+
};
57+
}
3658

3759
// Render children (can be text or nested elements)
3860
const children = element.children?.map((child, childIndex) => {
3961
if (typeof child === "string") {
4062
return child;
4163
}
42-
return renderElement(child, childIndex);
64+
return renderElement(child, childIndex, onIntentEmit);
4365
});
4466

4567
return (
@@ -57,11 +79,14 @@ function renderElement(
5779
* - ✅ Secure: No code execution (new Function())
5880
* - ✅ Debuggable: Data structures instead of code strings
5981
* - ✅ Performance: Direct React rendering, no script parsing
82+
* - ✅ Intent support: Type-safe event handlers for user interactions
6083
*/
6184
export function StructuredDomRenderer({
6285
content,
86+
onIntentEmit,
6387
}: {
6488
content: StructuredDomContent;
89+
onIntentEmit?: (intent: Intent) => void;
6590
}) {
66-
return <>{renderElement(content.element)}</>;
91+
return <>{renderElement(content.element, 0, onIntentEmit)}</>;
6792
}

apps/mcp-ui-poc/server/src/elements/button.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ElementDefinition } from "../types/remote-dom.js";
1+
import type { ElementDefinition, Intent } from "../types/remote-dom.js";
22

33
export interface ButtonElementArgs {
44
label: string;
@@ -8,6 +8,8 @@ export interface ButtonElementArgs {
88
isDisabled?: boolean;
99
type?: "button" | "submit" | "reset";
1010
ariaLabel?: string;
11+
/** Optional intent to emit when button is pressed */
12+
intent?: Intent;
1113
}
1214

1315
/**
@@ -23,6 +25,7 @@ export function buildButtonElement(args: ButtonElementArgs): ElementDefinition {
2325
isDisabled = false,
2426
type = "button",
2527
ariaLabel,
28+
intent,
2629
} = args;
2730

2831
return {
@@ -37,5 +40,6 @@ export function buildButtonElement(args: ButtonElementArgs): ElementDefinition {
3740
"aria-label": ariaLabel,
3841
},
3942
children: [label],
43+
events: intent ? { onPress: intent } : undefined,
4044
};
4145
}

apps/mcp-ui-poc/server/src/tools/product-card.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export interface ProductCardArgs {
2323

2424
export function createProductCard(args: ProductCardArgs) {
2525
const {
26+
productId,
2627
productName,
2728
price,
2829
description = "",
@@ -75,12 +76,21 @@ export function createProductCard(args: ProductCardArgs) {
7576
content: description,
7677
}),
7778
buildButtonElement({
78-
label: "Add to Cart",
79-
variant: "solid",
79+
label: "View Details",
80+
variant: "outline",
8081
colorPalette: "primary",
8182
width: "full",
82-
isDisabled: !inStock,
8383
type: "button",
84+
intent: {
85+
type: "view_details",
86+
description: `User wants to see detailed information about the product "${productName}"${productId ? ` (ID: ${productId})` : ""}. Please provide comprehensive details including specifications, availability, pricing breakdown, and any related products.`,
87+
payload: {
88+
productId: productId || productName,
89+
productName,
90+
price,
91+
inStock,
92+
},
93+
},
8494
}),
8595
],
8696
});

apps/mcp-ui-poc/server/src/types/remote-dom.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,26 @@
22
* Remote DOM type definitions for MCP-UI server
33
*/
44

5+
/**
6+
* Intent structure for user actions that bubble up from components.
7+
* The description field is human-readable text that Claude interprets directly.
8+
*/
9+
export interface Intent {
10+
/** Intent type identifier (e.g., "view_details", "add_to_cart") */
11+
type: string;
12+
/**
13+
* Human-readable description of what the user wants to do.
14+
* This is what Claude will see and interpret.
15+
* Should be written as if the user is speaking to Claude.
16+
*/
17+
description: string;
18+
/**
19+
* Structured payload with intent-specific data.
20+
* Claude can use this for tool calls or additional context.
21+
*/
22+
payload: Record<string, unknown>;
23+
}
24+
525
/**
626
* Structured element definition for type-safe UI serialization
727
*/
@@ -12,6 +32,11 @@ export interface ElementDefinition {
1232
attributes?: Record<string, string | boolean | number | undefined>;
1333
/** Child elements (can be nested ElementDefinitions or text nodes) */
1434
children?: (ElementDefinition | string)[];
35+
/** Event handlers for user interactions */
36+
events?: {
37+
/** Press event (for buttons) - emits an intent */
38+
onPress?: Intent;
39+
};
1540
}
1641

1742
// Re-export from index for convenience

0 commit comments

Comments
 (0)