Skip to content

Developer Guide

This guide covers how to build Agent Driven UI applications using the Freesail SDK. You’ll learn how to set up the architecture, run the gateway, connect agents, and build interactive UIs.


Freesail uses a Triangle Pattern with three independent processes:

┌────────────────┐ MCP Streamable HTTP ┌──────────────────┐ A2UI SSE ┌──────────────┐
│ AI Agent │ ◄────────────────────────►│ Freesail Gateway │ ◄───────────► │ React App │
│ (Orchestrator)│ Port 3000 │ (Bridge) │ Port 3001 │ (Renderer) │
└────────────────┘ localhost only └──────────────────┘ └──────────────┘
  • Agent: Decides what to show by calling MCP tools (e.g., create_surface, update_components).
  • Gateway: Translates between MCP (agent-facing) and A2UI (UI-facing). Validates agent output against catalog schemas.
  • Frontend: Renders A2UI JSON into React components and sends user actions back to the agent.

The Gateway is the central bridge between agents and frontends. It runs as a standalone Node.js process with two network-facing interfaces:

InterfacePortProtocolPurpose
Agent-facing3000 (default)MCP Streamable HTTPExposes tools, resources, and prompts to AI agents
UI-facing3001 (default)HTTP SSE + POSTStreams A2UI updates to the frontend, receives user actions
Terminal window
# Decoupled mode (recommended) — agents connect via HTTP
npx tsx packages/@freesail/gateway/src/cli.ts \
--mcp-mode http \
--mcp-port 3000 \
--http-port 3001
# Stdio mode — agent spawns gateway as child process
npx tsx packages/@freesail/gateway/src/cli.ts \
--http-port 3001
OptionDefaultDescription
--mcp-mode <mode>stdioMCP transport: stdio (child process) or http (standalone)
--mcp-port <port>3000Port for MCP Streamable HTTP server (http mode only)
--mcp-host <host>127.0.0.1Bind address for MCP server (http mode only)
--http-port <port>3001Port for A2UI HTTP/SSE server
--webhook-url <url>Forward UI actions to this URL via HTTP POST
--log-file <file>Write logs to file (in addition to console)

By default, the MCP server binds to 127.0.0.1 — only local processes can connect. The A2UI server binds to 0.0.0.0, making it accessible from browsers. This provides network-level security without requiring authentication.

  1. Agent → Gateway (MCP): Agent calls tools like create_surface or update_components. The gateway validates the call against the catalog schema and pushes the result to the appropriate frontend session via SSE.

  2. Frontend → Gateway → Agent (Actions): When a user clicks a button, the frontend POSTs an action to the gateway. The gateway queues it as an MCP resource, and the agent polls for pending actions.


Terminal window
npm install freesail @freesail/catalogs

The FreesailProvider manages the connection to the gateway and registers available component catalogs.

import { ReactUI } from 'freesail';
import { StandardCatalog, ChatCatalog } from '@freesail/catalogs';
const CATALOGS: ReactUI.CatalogDefinition[] = [
StandardCatalog,
ChatCatalog,
];
function App() {
return (
<ReactUI.FreesailProvider
sseUrl="http://localhost:3001/sse"
postUrl="http://localhost:3001/message"
catalogDefinitions={CATALOGS}
>
<MainLayout />
</ReactUI.FreesailProvider>
);
}

A FreesailSurface is a designated area that the AI agent can control.

import { ReactUI } from 'freesail';
function MainLayout() {
return (
<div className="app-container">
{/* Client-managed surface (start with __) */}
<aside className="sidebar">
<ReactUI.FreesailSurface surfaceId="__chat" />
</aside>
{/* Agent-created surface (start with alphanumeric) */}
<main className="content">
<ReactUI.FreesailSurface surfaceId="workspace" />
</main>
</div>
);
}
TypeNamingWho creates it?Agent permissions
Agent-managedAlphanumeric (e.g., workspace)Agent via create_surfaceFull control
Client-managedStarts with __ (e.g., __chat)React appupdateDataModel only

The agent connects to the gateway’s MCP endpoint and uses tools to drive the UI.

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
const transport = new StreamableHTTPClientTransport(
new URL('http://localhost:3000/mcp')
);
const mcpClient = new Client(
{ name: 'my-agent', version: '1.0.0' },
{ capabilities: {} }
);
await mcpClient.connect(transport);
await mcpClient.callTool({
name: 'create_surface',
arguments: {
surfaceId: 'workspace',
catalogId: 'https://freesail.dev/catalogs/standard_catalog_v1.json',
sessionId: 'session_abc123',
},
});
await mcpClient.callTool({
name: 'update_components',
arguments: {
surfaceId: 'workspace',
sessionId: 'session_abc123',
components: [
{ id: 'root', component: 'Column', children: ['greeting'] },
{ id: 'greeting', component: 'Text', text: 'Hello from the Agent!' },
],
},
});

Bind component properties to the data model for automatic UI updates:

// Agent sets up components with data bindings
await mcpClient.callTool({
name: 'update_components',
arguments: {
surfaceId: 'ticker',
sessionId,
components: [
{
id: 'price',
component: 'Text',
text: { path: '/currentPrice' }, // Binds to data model
},
],
},
});
// Set the data model
await mcpClient.callTool({
name: 'update_data_model',
arguments: {
surfaceId: 'ticker',
sessionId,
path: '/currentPrice',
value: '$150.00',
},
});

Update data without re-sending the component tree:

await mcpClient.callTool({
name: 'update_data_model',
arguments: {
surfaceId: 'ticker',
sessionId,
path: '/currentPrice',
value: '$155.50',
},
});

When a user interacts with a component, the SDK sends an Action back through the gateway:

  1. UI Event: User clicks a button in the browser.
  2. Action Payload: Freesail POSTs the action to the gateway with the surface’s data model.
  3. Agent Processing: The agent picks up the action via MCP and responds.
{
"version": "v0.9",
"action": {
"name": "submit_form",
"surfaceId": "workspace",
"sourceComponentId": "submit-btn",
"context": { "formData": "..." }
},
"_clientDataModel": {
"surfaceId": "workspace",
"dataModel": { "items": [], "total": 99.99 }
}
}

The easiest way to run everything is with the provided script:

Terminal window
export GOOGLE_API_KEY=your-api-key
cd examples && bash run-all.sh

This starts three independent processes:

ProcessURLPurpose
Gatewayhttp://localhost:3001 (A2UI), http://127.0.0.1:3000 (MCP)Bridge between agent and UI
Agenthttp://localhost:3002AI agent with health endpoint
UIhttp://localhost:5173Vite React dev server

  • The gateway assigns a sessionId to each SSE connection.
  • The React SDK attaches this ID to every HTTP POST via the X-A2UI-Session header.
  • The agent receives the sessionId through a synthetic __session_connected action.

If you need to access the current session ID in your React components (for example, to pass it to your own backend API or a conversational agent), you can use the useSessionId hook:

import { ReactUI } from 'freesail';
function SessionInfo() {
const sessionId = ReactUI.useSessionId();
if (!sessionId) return <div>Connecting to Gateway...</div>;
return <div>Connected Session: {sessionId}</div>;
}
SymptomCheck
UI stuck on “Loading surface…”Is the __chat surface being bootstrapped? Check agent logs.
Agent not receiving actionsCheck the gateway logs for upstream messages. Verify X-A2UI-Session header in browser Network tab.
Components not renderingVerify the catalogId matches a registered catalog. Check browser console for registry errors.
Components not renderingCheck if agent is passing the correct sessionid in the tool call.

  • Session Management: Each SSE connection is assigned a sessionId by the gateway. The React SDK attaches this ID to every HTTP POST request using the X-A2UI-Session header.
  • useSessionId Hook: Use the useSessionId hook from the React SDK to access the current session ID directly in your components.