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:

Freesail Architecture

  • 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:

InterfaceProtocolPurpose
Agent-facingMCP Stdio or Streamable HTTPExposes tools, resources, and prompts to AI agents
UI-facingHTTP 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.