You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We've been building a full-stack implementation of the A2UI 0.9 protocol and wanted to share it here. Freesail is an open-source SDK - it includes tools and servers required to build and run agent-driven interfaces using A2UI.
What Freesail Is
Freesail is a complete A2UI 0.9 implementation covering all three nodes of the agent-driven UI model:
Gateway — a Node.js bridge between agents and frontends that speaks MCP on one side and A2UI SSE on the other
React SDK — a renderer that turns incoming A2UI messages into live component trees with two-way data binding
Agent Runtime — a session lifecycle and action-routing library for building the agent side
Freesail CLI — a tool that can be used to build and validate A2UI catalogs.
The quickstart runs all three with a single script and ships with working examples for Gemini, OpenAI, and Anthropic Claude via LangChain.
A2UI Protocol Alignment
Freesail implements the core A2UI 0.9 message set on both sides of the wire:
Server → Client (SSE)
createSurface
updateComponents
updateDataModel
deleteSurface
Client → Server (HTTP POST)
action — user interactions with context and full data model snapshot
error — client-side validation failures and render errors reported back to the agent
One extension we added beyond the 0.9 spec: getDataModel. The gateway can request the client's current surface state on demand rather than requiring sendDataModel to be enabled globally. This is useful for agents that want to inspect current form state or list contents before deciding what to do next, without the overhead of streaming the full model with every action.
Chat Is Just Another A2UI Surface
One architectural decision we're particularly happy with: chat in Freesail is implemented entirely using A2UI messages. There is no separate chat API, no WebSocket endpoint, no HTTP route exposed by the agent. The agent never listens for incoming connections from the UI at all.
Here is how it works. The React app bootstraps a client-managed surface called __chat at startup — defining the component tree (message list, streaming token display, typing indicator, input box) and the initial data model. From that point, all chat state flows through standard A2UI protocol messages:
User sends a message → the frontend POSTs an action named chat_send on the __chat surface, carrying the message text as the action context. This goes to the gateway, which queues it as an MCP resource and fires a ResourceUpdated notification to the agent.
Agent streams a reply → the agent calls update_data_model on __chat, writing tokens to /stream/token as they arrive from the LLM. The renderer binds the AgentStream component to this path and displays tokens as they land.
Agent commits the message → once streaming completes, the agent writes the final message to /messages and clears the stream state. The frontend updates the chat history in place.
Typing indicator → the agent writes true or false to /isTyping via update_data_model. The ChatTypingIndicator component is data-bound to this path and shows or hides itself accordingly.
The entire conversation — user turns, agent replies, streaming, and UI state — travels through the same SSE connection and the same gateway that handles all other surfaces. The agent's only outbound connection is its MCP link to the gateway.
This has a meaningful security and deployment consequence: the agent process does not need to be reachable from the internet, or from the browser, or from any other process. It sits entirely behind the gateway, making outbound MCP calls only. You can run it on a private network with no ingress rules at all. The browser never has a route to the agent.
It also means chat UI is composable with the rest of the surface model in the same way any other surface is. The agent can write to the chat surface and to a data dashboard surface in the same turn — one update_data_model call to push a streaming token to the chat panel, then an update_components call to build a chart in the workspace area — all coordinated through a single gateway connection, with no additional plumbing.
The Gateway: Agent Guide, Validator, and Coordinator
The gateway does more than translate protocols. It actively helps agents produce valid UI — and gets out of the way when they do.
Prompt Catalog Injection
When a client connects and registers its catalogs, the gateway generates an a2ui_system MCP prompt containing the full component and function vocabulary as a structured reference. The agent fetches this via get_catalogs at session start. From that point the LLM knows exactly what components exist, what properties they accept, what types those properties must be, and what client-side functions are available — without the developer having to write any of this into the system prompt manually.
The catalog description fields become the agent's vocabulary. Write them clearly and the agent uses the right components. This is also where get_component_details and get_function_details come in — the agent can call these mid-session to get the full property reference for a specific component before building a complex surface.
Schema Validation with Instant Feedback
Every update_components call is validated against the catalog JSON schema before it is applied to the frontend. If the agent sends an invalid component tree — wrong property types, missing required fields, unknown component names — the gateway rejects it and returns a structured error message over MCP. The LLM sees the error as tool output and typically self-corrects on the next turn without any developer intervention.
In practice this dramatically reduces the debugging loop during development. The agent learns from gateway feedback in real time rather than silently producing a broken UI.
Push-Based Coordination via MCP Resources
Rather than polling, the gateway coordinates between agents and frontends using MCP resources and ResourceUpdated notifications:
mcp://freesail.dev/sessions — fires whenever a browser tab connects or disconnects
mcp://freesail.dev/sessions/{sessionId} — fires whenever a user action or client error arrives for that session
The agent runtime subscribes to both. When a user clicks a button, the frontend POSTs the action to the gateway, which queues it on the session resource and pushes a ResourceUpdated notification to the agent. The agent drains the queue by reading the resource — which delivers the action payload including the full surface data model snapshot. No polling, no webhook configuration needed.
Session ownership is explicit: the runtime calls claim_session when a session connects and release_session when it disconnects, so only one agent process holds a session at a time. This makes horizontal scaling straightforward.
Renderer-Side Interceptors
One thing we felt genuinely needed in the front-end is client-side enforcement. The gateway validates the shape of what the agent sends, but the React app often has additional context the agent can't know about — which surfaces a given user role is allowed to see, whether the user is actively editing a form, whether a concurrent surface limit has been reached.
Freesail's FreesailProvider exposes four interceptor props for this:
<ReactUI.FreesailProvidercatalogs={CATALOGS}onBeforeCreateSurface={(surfaceId,catalogId,sendDataModel,surfaceManager)=>{constexisting=surfaceManager.getAllSurfaces().filter(s=>s.id!=='__chat');if(existing.length>=maxSurfaces){return{allowed: false,message: 'Surface limit reached. Remove one before adding another.'};}return{allowed: true,message: ''};}}onBeforeUpdateComponents={(surfaceId,components)=>{if(lockedSurfaces.has(surfaceId)){return{allowed: false,message: `"${surfaceId}" is locked by the user.`};}return{allowed: true,message: ''};}}onBeforeUpdateDataModel={(surfaceId,path)=>{if(path.startsWith('/readonly/')){return{allowed: false,message: `Path "${path}" is read-only.`};}return{allowed: true,message: ''};}}onBeforeDeleteSurface={(surfaceId)=>{if(unsavedChanges.has(surfaceId))flushChanges(surfaceId);return{allowed: true,message: ''};}}>
When an interceptor returns allowed: false, the operation is blocked on the client and the message is forwarded back to the agent as an error payload over MCP — so the LLM can see why its operation was rejected and adjust. Returning allowed: true with a non-empty message allows the operation but also notifies the agent, which is useful for soft warnings like "this update contained 60 components, prefer incremental updates."
This gives the React layer hard enforcement authority over what agents can do, independent of gateway-level schema validation.
Theming
The React SDK includes a token-based theming system. The theme prop on FreesailProvider accepts a full FreesailThemeTokens object and injects CSS custom properties (--freesail-*) into the document. Two complete baseline sets are exported — defaultLightTokens and defaultDarkTokens — and can be spread with overrides for custom branding:
FreesailSurface also accepts a theme prop for per-surface overrides — useful when one surface (e.g. a chat panel) should stay light while the rest of the app switches to dark mode. Font size tokens (typeBody, typeH1–typeH5, etc.) use fluid clamp() values so they scale with the container rather than snapping between breakpoints.
CLI: Building and Composing Catalogs
The CLI is where Freesail's catalog workflow lives. Catalogs are the contract between the agent and the renderer — JSON schemas describing every component and function the agent can use, with matching React implementations on the renderer side.
# Scaffold a new catalog package
npx freesail new catalog
# Import components from an existing catalog
npx freesail include catalog --package @freesail/standard-catalog
# Merge schemas and generate the resolved catalog JSON
npx freesail prepare catalog
# Verify every schema entry has a matching implementation
npx freesail validate catalog
The composition model is particularly useful for building A2UI catalogs. You declare which components and functions to pull from existing packages in catalog.include.json, add your own custom components alongside, and prepare catalog merges everything into a single resolved schema. The agent sees one unified catalog; the renderer picks up all implementations from both sources.
The validate step catches drift between the schema and the implementation before it reaches the agent — if a component is declared in JSON but not implemented in React, or vice versa, the build fails with a clear message.
Session Resilience
The gateway's reconnect grace period (default 180 seconds) means brief disconnections — page refreshes, network blips, duplicate tabs — don't require the agent to restart. When a session disconnects, the gateway suspends the session rather than removing it. If the client reconnects within the grace period, the React SDK restores surface state from sessionStorage and the session resumes seamlessly.
Tab deduplication is also handled: if a user duplicates a browser tab (copying the sessionStorage session ID), the gateway detects the collision on the second SSE connection and issues a fresh session for the duplicate tab rather than silently corrupting the original.
Where We Are and What We're Looking For
Freesail is in active development at v0.8.x. The core architecture is stable — the protocol handling, gateway, React renderer, and agent runtime are all working end-to-end. We're working toward a 1.0 that we'd consider production-ready.
We're particularly interested in feedback from the community on:
Protocol extensions — getDataModel is one we added; it allows an agent to request the state of the data model on demand. Is this something that could be adopted into the core A2UI spec?
Catalog interoperability — if you're building A2UI catalogs, we'd love to get your feedback on the Freesail CLI for building the catalog. We believe it has uses outside of Freesail tool as it helps to compose catalogs and validate them.
Agent framework coverage — the example ships with LangChain, but the agent runtime is framework-agnostic; we're interested in seeing what other frameworks people are connecting
The example project in the Freesail repo gets you running in a few minutes if you want to see it in action.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Hi A2UI community 👋
We've been building a full-stack implementation of the A2UI 0.9 protocol and wanted to share it here. Freesail is an open-source SDK - it includes tools and servers required to build and run agent-driven interfaces using A2UI.
What Freesail Is
Freesail is a complete A2UI 0.9 implementation covering all three nodes of the agent-driven UI model:
The quickstart runs all three with a single script and ships with working examples for Gemini, OpenAI, and Anthropic Claude via LangChain.
A2UI Protocol Alignment
Freesail implements the core A2UI 0.9 message set on both sides of the wire:
Server → Client (SSE)
createSurfaceupdateComponentsupdateDataModeldeleteSurfaceClient → Server (HTTP POST)
action— user interactions with context and full data model snapshoterror— client-side validation failures and render errors reported back to the agentOne extension we added beyond the 0.9 spec:
getDataModel. The gateway can request the client's current surface state on demand rather than requiringsendDataModelto be enabled globally. This is useful for agents that want to inspect current form state or list contents before deciding what to do next, without the overhead of streaming the full model with every action.Chat Is Just Another A2UI Surface
One architectural decision we're particularly happy with: chat in Freesail is implemented entirely using A2UI messages. There is no separate chat API, no WebSocket endpoint, no HTTP route exposed by the agent. The agent never listens for incoming connections from the UI at all.
Here is how it works. The React app bootstraps a client-managed surface called
__chatat startup — defining the component tree (message list, streaming token display, typing indicator, input box) and the initial data model. From that point, all chat state flows through standard A2UI protocol messages:actionnamedchat_sendon the__chatsurface, carrying the message text as the action context. This goes to the gateway, which queues it as an MCP resource and fires aResourceUpdatednotification to the agent.update_data_modelon__chat, writing tokens to/stream/tokenas they arrive from the LLM. The renderer binds theAgentStreamcomponent to this path and displays tokens as they land./messagesand clears the stream state. The frontend updates the chat history in place.trueorfalseto/isTypingviaupdate_data_model. TheChatTypingIndicatorcomponent is data-bound to this path and shows or hides itself accordingly.The entire conversation — user turns, agent replies, streaming, and UI state — travels through the same SSE connection and the same gateway that handles all other surfaces. The agent's only outbound connection is its MCP link to the gateway.
This has a meaningful security and deployment consequence: the agent process does not need to be reachable from the internet, or from the browser, or from any other process. It sits entirely behind the gateway, making outbound MCP calls only. You can run it on a private network with no ingress rules at all. The browser never has a route to the agent.
It also means chat UI is composable with the rest of the surface model in the same way any other surface is. The agent can write to the chat surface and to a data dashboard surface in the same turn — one
update_data_modelcall to push a streaming token to the chat panel, then anupdate_componentscall to build a chart in the workspace area — all coordinated through a single gateway connection, with no additional plumbing.The Gateway: Agent Guide, Validator, and Coordinator
The gateway does more than translate protocols. It actively helps agents produce valid UI — and gets out of the way when they do.
Prompt Catalog Injection
When a client connects and registers its catalogs, the gateway generates an
a2ui_systemMCP prompt containing the full component and function vocabulary as a structured reference. The agent fetches this viaget_catalogsat session start. From that point the LLM knows exactly what components exist, what properties they accept, what types those properties must be, and what client-side functions are available — without the developer having to write any of this into the system prompt manually.The catalog description fields become the agent's vocabulary. Write them clearly and the agent uses the right components. This is also where
get_component_detailsandget_function_detailscome in — the agent can call these mid-session to get the full property reference for a specific component before building a complex surface.Schema Validation with Instant Feedback
Every
update_componentscall is validated against the catalog JSON schema before it is applied to the frontend. If the agent sends an invalid component tree — wrong property types, missing required fields, unknown component names — the gateway rejects it and returns a structured error message over MCP. The LLM sees the error as tool output and typically self-corrects on the next turn without any developer intervention.In practice this dramatically reduces the debugging loop during development. The agent learns from gateway feedback in real time rather than silently producing a broken UI.
Push-Based Coordination via MCP Resources
Rather than polling, the gateway coordinates between agents and frontends using MCP resources and
ResourceUpdatednotifications:mcp://freesail.dev/sessions— fires whenever a browser tab connects or disconnectsmcp://freesail.dev/sessions/{sessionId}— fires whenever a user action or client error arrives for that sessionThe agent runtime subscribes to both. When a user clicks a button, the frontend POSTs the action to the gateway, which queues it on the session resource and pushes a
ResourceUpdatednotification to the agent. The agent drains the queue by reading the resource — which delivers the action payload including the full surface data model snapshot. No polling, no webhook configuration needed.Session ownership is explicit: the runtime calls
claim_sessionwhen a session connects andrelease_sessionwhen it disconnects, so only one agent process holds a session at a time. This makes horizontal scaling straightforward.Renderer-Side Interceptors
One thing we felt genuinely needed in the front-end is client-side enforcement. The gateway validates the shape of what the agent sends, but the React app often has additional context the agent can't know about — which surfaces a given user role is allowed to see, whether the user is actively editing a form, whether a concurrent surface limit has been reached.
Freesail's
FreesailProviderexposes four interceptor props for this:When an interceptor returns
allowed: false, the operation is blocked on the client and the message is forwarded back to the agent as an error payload over MCP — so the LLM can see why its operation was rejected and adjust. Returningallowed: truewith a non-empty message allows the operation but also notifies the agent, which is useful for soft warnings like "this update contained 60 components, prefer incremental updates."This gives the React layer hard enforcement authority over what agents can do, independent of gateway-level schema validation.
Theming
The React SDK includes a token-based theming system. The
themeprop onFreesailProvideraccepts a fullFreesailThemeTokensobject and injects CSS custom properties (--freesail-*) into the document. Two complete baseline sets are exported —defaultLightTokensanddefaultDarkTokens— and can be spread with overrides for custom branding:FreesailSurfacealso accepts athemeprop for per-surface overrides — useful when one surface (e.g. a chat panel) should stay light while the rest of the app switches to dark mode. Font size tokens (typeBody,typeH1–typeH5, etc.) use fluidclamp()values so they scale with the container rather than snapping between breakpoints.CLI: Building and Composing Catalogs
The CLI is where Freesail's catalog workflow lives. Catalogs are the contract between the agent and the renderer — JSON schemas describing every component and function the agent can use, with matching React implementations on the renderer side.
The composition model is particularly useful for building A2UI catalogs. You declare which components and functions to pull from existing packages in
catalog.include.json, add your own custom components alongside, andprepare catalogmerges everything into a single resolved schema. The agent sees one unified catalog; the renderer picks up all implementations from both sources.{ "includes": { "@freesail/standard-catalog": { "components": ["Card", "Button", "Text", "Column", "Row", "Grid"], "functions": ["formatString", "formatDate", "required"] } } }The
validatestep catches drift between the schema and the implementation before it reaches the agent — if a component is declared in JSON but not implemented in React, or vice versa, the build fails with a clear message.Session Resilience
The gateway's reconnect grace period (default 180 seconds) means brief disconnections — page refreshes, network blips, duplicate tabs — don't require the agent to restart. When a session disconnects, the gateway suspends the session rather than removing it. If the client reconnects within the grace period, the React SDK restores surface state from
sessionStorageand the session resumes seamlessly.Tab deduplication is also handled: if a user duplicates a browser tab (copying the
sessionStoragesession ID), the gateway detects the collision on the second SSE connection and issues a fresh session for the duplicate tab rather than silently corrupting the original.Where We Are and What We're Looking For
Freesail is in active development at v0.8.x. The core architecture is stable — the protocol handling, gateway, React renderer, and agent runtime are all working end-to-end. We're working toward a 1.0 that we'd consider production-ready.
We're particularly interested in feedback from the community on:
getDataModelis one we added; it allows an agent to request the state of the data model on demand. Is this something that could be adopted into the core A2UI spec?The example project in the Freesail repo gets you running in a few minutes if you want to see it in action.
We'd love your feedback.
All reactions