Documents saga a2ui 0.11.0-rc.1 (prerelease). This is the SDK guideThe first version of your app displays one message from the server. When you ask Claude or ChatGPT to open it, the host callsdocs/tic-tac-toe/1-first-card.mdat source revisionfb4126c. It is published as a snapshot: later source changes do not alter it. Licensed under Apache-2.0.
new_game, receives the message, and opens a React view that displays it. This small example introduces the same connection the board will use later.
1. Let Vite create the project
From your projects directory, run:create-vite if it appears. If you already have an empty app directory, run the same command there with . in place of my-tic-tac-toe, and skip cd.
Vite’s React/TypeScript generator creates the package file, compiler configuration, React entry point and lint setup. Keep that configuration. We will replace its demo counter and add the parts that connect the app to an MCP host. All commands below run from this app directory. Continue with the installation commands below; the generator’s suggested npm run dev starts a standalone website, while this app opens in Claude or ChatGPT.
SAGA’s supported A2UI dependency combination needs one npm override. Set it before installing:
vite-plugin-singlefile is the addition that packages the React view for its host; the other tools belong to the generated React/TypeScript project.
.gitignore, which excludes node_modules and dist, and the generated package-lock.json; subsequent installations use npm ci. The main guide’s dependency notes explain the inherited audit warnings and the Markdown override.
2. Return a message from the server
Create aserver directory and add server/operations.ts below. This file describes what the assistant can ask your app to do.
registerPublicOperation exposes a tool named new_game. Its empty input schema means the caller supplies no arguments. handle produces the application value; project gives that value two representations: text the assistant can read and structured data the React view can use. Both contain the same welcome message here.
file=server/operations.ts
output describes the structured result: an object with a message string. SAGA checks it before returning the response. view attaches the HTML that the host should open with this tool’s result. The annotations describe the operation’s effects to the host; this first version only displays a message. The named failures provide the safe text to use when the operation cannot return a valid result.
The operation uses Zod from zod for its tool schemas. The React catalog in the next section uses z from the SAGA stack, whose schema API matches A2UI’s catalog types.
3. Display that message in React
Createsrc/view.tsx. The component at the start is ordinary React: it renders a paragraph from a text prop. createComponentImplementation gives that component a name and a prop schema so SAGA can use it in a layout. A catalog is the collection of components that this view allows; for now, it contains only Message.
The layout has one data field, message, and one component. bind: { text: 'message' } connects that field to the paragraph’s text prop.
file=src/view.tsx
startA2uiView mounts the layout into #root and connects it to the host. The initial message is Connecting…. When the tool result arrives, onToolResult reads its structured data and calls surface.update. That changes message, so React redraws the paragraph with the server’s text. You do not write the host handshake or send A2UI protocol messages.
Replace Vite’s generated src/main.tsx with this entry point:
file=src/main.tsx
src/App.tsx and src/App.css; this app starts through startView instead. Replace the demo styles in src/index.css with:
file=src/index.css
index.html, change the title to Tic-tac-toe and remove the favicon <link>. Keep its #root element and /src/main.tsx script entry. The host supplies the surrounding page, so the card only needs its content and styles.
4. Serve the operation and its view
Createserver/main.ts:
file=server/main.ts
prepareBoardViews turns it into the view attached to new_game. startLocalMcpServer supplies the HTTP listener and MCP transport; its callback registers your application operations for each request. The lifetime passed into createGameServer gives those operations the request’s deadline.
The helper binds to 127.0.0.1 and serves /mcp, checks hosts and origins, limits request bodies, and cancels requests during shutdown. The signal handlers above let Ctrl-C stop it cleanly. Those mechanics belong to the SDK’s local listener; your app supplies the message and the component. This server has no sign-in and is intended for the disposable games described in the main guide.
5. Build for an MCP host
A normal Vite website loads JavaScript and CSS as separate files. An MCP host receives this view as an HTML resource, so bundle its assets into that document. Replace the generatedvite.config.ts with:
file=vite.config.ts
vite.server.config.ts:
file=vite.server.config.ts
ssr selects the server entry. noExternal: true bundles its dependencies, including the TypeScript source shipped by SAGA. emptyOutDir: false preserves the HTML that the view build already wrote. The result is dist/index.html beside dist/server.mjs, which is where server/main.ts reads it.
Make two small changes to Vite’s generated compiler configuration:
- In both
tsconfig.app.jsonandtsconfig.node.json, seterasableSyntaxOnlytofalse. The current SAGA packages use TypeScript constructor parameter properties, which Vite compiles; this option otherwise prohibits that syntax. Keepstrict: true. - In
tsconfig.node.json, replace itsincludelist with["vite.config.ts", "vite.server.config.ts", "server"]so typechecking covers your server too.
package.json:
6. Open it and make a change
Tic-tac-toe MCP endpoint: http://127.0.0.1:3000/mcp. Leave it running and follow Open the card in Claude or ChatGPT. Your first UI result is in that host.
Once the welcome message appears, change the string returned by handle in server/operations.ts. Stop the server, rebuild and start it, refresh the host’s tools as described in the main guide, then open the app in a new chat. The paragraph should show your new text without a change to the React component. The value travelled through project.data.message, onToolResult, and the layout binding.
Next, change the paragraph in the Message component to include <strong>{String(props.text ?? '')}</strong>. Rebuild and open it again. The server still returns the same value; the React component controls how it looks. These are the two places you will extend when the welcome card becomes a board.
Compare your files with the complete first-card checkpoint if needed, then return to Build and play the game.