start here
Describe once, serve both.
Never heard of gamestruct? This is the five-minute on-ramp. It ends with a surface that both a screen reader and an agent can operate.
1. Describe
A node carries a handle (a stable id), a role (what it is), an accessible name (what a screen reader says), and the actions it accepts. Use a stable handle - it is the same one both lanes use.
import { Bridge, ROLES } from 'gamestruct' const bridge = new Bridge({ name: 'pause menu' }) bridge.tree.add(null, { id: 'ui:start', role: ROLES.BUTTON, name: 'Start game' }) bridge.tree.add(null, { id: 'ui:volume', role: ROLES.SLIDER, name: 'Volume', value: 50 })
2. Wire
One handler per actionable handle. The verb is the action the node declares - activate for a button, set_valuefor a slider.
bridge.on('ui:start', { activate: () => startGame() }) bridge.on('ui:volume', { set_value: (v) => setVolume(v) })
3. Read both lanes
The same bridge now renders to a screen reader and to an MCP client. Either lane drives the same act() - there is no agent rail and no second protocol.
human.a11yDump(bridge) // the screen-reader text agent.toolCatalog(bridge) // the MCP tool list await bridge.act('ui:start') // one code path
4. Gate it
Run the a11y audit in CI. It flags the failures that actually break a screen reader: an interactive node with no name, one with no action, an image with no alt text.
const { ok, problems } = human.audit(bridge) # or see both lanes + the audit at once: npx gamestruct doctor