Data Flow
Problem: Apps involve three actors (the host, your server, your view) communicating in complex patterns. Understanding this flow is essential.
Solution: frac provides clear abstractions for each communication pattern.
The Three Actors
Section titled “The Three Actors”%%{init: {'theme': 'base', 'themeVariables': { 'lineColor': '#64748b' }}}%%flowchart LR User["User"] Model["Model"] View["View"] Server["MCP Server"]
User <-->|"Converse"| Model User <-->|"Interact"| View Model -->|"Features exposed<br/>as MCP tools"| Server View -->|"UI pages exposed<br/>as MCP resources"| Server
style User fill:#f1f5f9,stroke:#64748b,color:#334155 style Model fill:#2563EB,stroke:#1e40af,color:#fff style View fill:#2563EB,stroke:#1e40af,color:#fff style Server fill:#dbeafe,stroke:#93c5fd,color:#1e40af- The MCP Host: The conversational interface where users type messages and the model responds (ChatGPT, Claude, Goose, VSCode, etc.)
- Your MCP Server: The backend that exposes tools and business logic
- Your View (Guest): The React component rendered in an iframe inside the host
Key Terms
Section titled “Key Terms”Tool responses contain three fields:
content: Text array shown to the model in the conversationstructuredContent: Typed JSON data surfaced to your view and the host_meta: Delivered only to the view and hidden from the model
Tools and views
Section titled “Tools and views”Before diving into the data flow, understand the difference between plain tools and tools with a view. Both use registerTool, differentiated by the optional view field:
registerTool (no view) | registerTool (with view) | |
|---|---|---|
| Has UI? | No | Yes |
| Returns | content and/or structuredContent | structuredContent and optional content/_meta |
| Renders | Nothing | A React component from src/views/ |
| Use case | Background operations, calculations | Interactive UI |
// Plain tool: no UI, returns textserver.registerTool( { name: "calculate", inputSchema: { /* ... */ } }, async (args) => { return { content: "Result: 42" }; },);
// Tool with view: has UI, returns structured dataserver.registerTool( { name: "chart", inputSchema: { /* ... */ }, view: { component: "chart" }, }, async (args) => { return { content: "Displaying chart", structuredContent: { data: [1, 2, 3], labels: ["A", "B", "C"] }, }; },);Data Flow Patterns
Section titled “Data Flow Patterns”1. Tool → View (Initial Hydration)
Section titled “1. Tool → View (Initial Hydration)”When the host calls a tool returning a view, it returns structuredContent to hydrate the React component:
Server:
server.registerTool( { name: "show_flights", inputSchema: { destination: z.string() }, view: { component: "show_flights" }, }, async ({ destination }) => { const flights = await searchFlights(destination);
return { content: `Found ${flights.length} flights`, structuredContent: { flights }, // This goes to the view }; },);View:
import { useToolInfo } from "@usefractal/frac/web";
export function FlightView() { const toolInfo = useToolInfo<{ flights: Flight[] }>();
if (toolInfo.isSuccess) { const { flights } = toolInfo.output.structuredContent;
return ( <ul> {flights.map(flight => <li key={flight.id}>{flight.name}</li>)} </ul> ); }
return <div>Loading...</div>;}Use useToolInfo for the initial data that renders your view. This data is set once when the view loads.
2. View → Server (Tool Calls)
Section titled “2. View → Server (Tool Calls)”Views can trigger additional tool calls in response to user actions:
import { useCallTool } from "@usefractal/frac/web";
export function FlightView() { const { callTool, isPending, data } = useCallTool("get_flight_details");
const handleViewDetails = (flightId: string) => { callTool({ flightId }); };
return ( <button onClick={() => handleViewDetails("AF123")} disabled={isPending}> {isPending ? "Loading..." : "View Details"} </button> );}Use useCallTool when the user performs an action that requires fetching more data.
3. View → Model (Context Sync)
Section titled “3. View → Model (Context Sync)”Your view needs to communicate its state back to the model. Use the data-llm attribute to declaratively describe what the user sees. See LLM Context Sync.
4. View → Chat (Follow-up Messages)
Section titled “4. View → Chat (Follow-up Messages)”Views can send messages back into the conversation:
import { useSendFollowUpMessage } from "@usefractal/frac/web";
export function FlightView() { const sendMessage = useSendFollowUpMessage();
const handleBookFlight = (flight: Flight) => { sendMessage({ prompt: `I'd like to book the ${flight.name} flight. What payment methods do you accept?` }); };
return <button onClick={() => handleBookFlight(selectedFlight)}>Book Now</button>;}This creates a continuous loop: the view can ask the model for help, and the model responds naturally in the conversation.
Response Fields Explained
Section titled “Response Fields Explained”Tool responses have three fields:
| Field | Purpose | Consumed by |
|---|---|---|
content | Text description | The host (shown in conversation) |
structuredContent | Typed data | Host and view (useToolInfo, useCallTool) |
_meta | Response metadata | View |
return { content: [{ type: "text", text: "Found 3 flights to Paris" }], structuredContent: { flights: [{ id: "AF123", name: "Air France 123" }, /* ... */] }, _meta: { flightImages: [{ url: "https://assets.airfrance.com/flights/AF123.jpg" }, /* ... */] }};When to Use What
Section titled “When to Use What”| Need | Use | Why |
|---|---|---|
| Initial view data | useToolInfo | Data passed at hydration, no extra calls |
| User-triggered fetch | useCallTool | Model sees the result, can answer questions |
| Silent background fetch | Direct API call | Model doesn’t need to know |
| Describe current UI state | data-llm | Passive context for user questions |
| Trigger model response | useSendFollowUpMessage | Active prompt, immediate reply |
| Persist view state | useViewState | Survives re-renders |
The Communication Loop
Section titled “The Communication Loop”- Host calls your tool → Server responds with
structuredContent - View hydrates with
useToolInfo - User interacts → View updates
data-llm→ Model sees the context - User triggers action → View calls
useCallTool→ Server responds - View sends follow-up →
useSendFollowUpMessage→ Model replies
This loop creates a seamless experience where the conversation, the UI, and your backend work together.
Related
Section titled “Related”- LLM Context Sync - Keep the model informed of view state (read this next)
- Fetching Data Guide - Patterns for useToolInfo and useCallTool
- useToolInfo API - Initial data access
- useCallTool API - User-triggered data fetching
- useViewState API - Persistent view state