Building with AI Agents
Sygnal ships tooling for coding agents (Claude Code, Cursor, Codex and others). Its goal is that an agent never has to guess: one reference that shows only the canonical forms, checks that turn silent wiring bugs into coded messages, and a graph of the app it can read instead of the source. Everything here is also useful without an agent.
| Tool | What it gives the agent |
|---|---|
llms.txt | A compact reference of the canonical forms, in one file |
sygnal-dev skill | The same knowledge as a Claude Code skill, with a workflow; installed by the Sygnal plugin |
sygnal-check | Static wiring checks, strict mode, --fix, the app graph, explain |
| MCP server | check, graph and explain as tools |
| Dev server MCP endpoint | The running app in the open page: state, actions, diagnostics, its agent tools |
inspect() | The runtime app graph, in the browser or in a test |
This page is about agents that build your app. For agents that operate it (an in-app assistant, the browser’s agent, an MCP host), see Agents operating your app.
llms.txt
Section titled “llms.txt”/llms.txt is a single plain-text reference written for language models: the mental model, the anatomy of a component, one canonical example per concept, the wiring rules, an RxJS-to-xstream cheat sheet, how to read diagnostics, and how to test a change. It only uses the canonical forms, so code an agent copies from it passes sygnal-check --strict.
Point your agent at it, for example in your project’s AGENTS.md or CLAUDE.md:
This project uses Sygnal. Read https://sygnal.js.org/llms.txt before writing components.Run `npx --no-install sygnal-check --strict` after every change and fix every warning.The same file ships in the npm package (node_modules/sygnal/llms.txt), so agents can read it offline, and is in the root of the Sygnal repository.
Projects created with create-sygnal-app (without prompts: npm create sygnal-app@latest my-app -- --template vite --js --install) already include an AGENTS.md (and a CLAUDE.md that imports it) with these instructions, the test and check commands, and the testing recipe.
The sygnal-dev skill
Section titled “The sygnal-dev skill”sygnal-dev is a Claude Code skill for building Sygnal apps: the workflow (scaffold, write, test, check, inspect) and the canonical forms, with llms.txt for the API. Claude Code loads it whenever it works on Sygnal code.
Install it with the Sygnal plugin:
/plugin marketplace add tpresley/sygnal/plugin install sygnal@sygnalThe plugin also runs sygnal-check’s language server, so Claude sees the checker’s findings after each edit without running it. That needs sygnal-check 0.4.0 or later in the project. See Editor Setup for what it adds and how to turn the findings off.
Or copy it: the repository’s skills/sygnal-dev/ folder goes into your project’s .claude/skills/ (or ~/.claude/skills/ for every project). Use this for other agents that read Claude-style skills, or on Windows without symbolic links in git (the plugin then installs without the skill).
sygnal-check
Section titled “sygnal-check”sygnal-check is a separate package that reads your source with a parser and never runs it. Projects created with create-sygnal-app already have it as a dev dependency. In any other project, install it:
npm install -D sygnal-checkThe commands below use npx --no-install, which runs only the copy installed in the project and never downloads one from the registry. Agent instructions use the same form: when sygnal-check isn’t installed, the agent skips the step and relies on the runtime diagnostics in the tests (renderComponent(C, { strict: true }) and t.expectNoDiagnostics()).
| Command | Does |
|---|---|
npx --no-install sygnal-check | Check ./src; pass files, directories or globs instead (a Vike app: npx --no-install sygnal-check pages) |
npx --no-install sygnal-check --strict | Also run the strict-mode rules (SYG501-507) |
npx --no-install sygnal-check --fix | Rewrite SYG504/505/506 into the canonical form in place, then check |
npx --no-install sygnal-check --graph [--json] | Print the app graph (components, actions, events, selectors, findings) |
npx --no-install sygnal-check explain SYG104 | What a code means and how to fix it (--json, --all) |
npx --no-install sygnal-check --json | Findings as JSON |
npx --no-install sygnal-check mcp | Start the MCP server |
Output looks like this:
src/App.jsx:24:21 SYG104 App: selector '.remove' targets .remove, which is only rendered inside child component <TodoItem>; parents can't see DOM events inside child components (handle it in <TodoItem> and send it up via PARENT or EVENTS)
sygnal-check: 1 warningIt exits with code 1 on a warning or error (--fail-on=error|never changes that), 2 on a usage error. Info findings are hidden unless you pass --verbose. Check the whole app, not one folder: the EVENTS rule (SYG105) and the child-component rule (SYG104) need every file.
To silence one finding, put // sygnal-ignore SYG110 on the line or the line above.
For apps that use sygnal/ai, it also checks the agent static (an action with no model entry SYG150, a misspelled agents / tools SYG151, duplicate names SYG440, Collection items without ids SYG441, inputs with no JSON Schema form SYG240 / SYG243), LLM requests without an ok action (SYG152) and a bare toolname on a <form> (SYG153), plus two accessibility rules for agents that read the page: hover-only actions (SYG730) and toggled state shown only by a class (SYG731). --graph lists the agent declarations.
The Vite plugin runs sygnal-check in the dev server when it is installed, and editors show its findings as you type. All of them read the same config file.
MCP server
Section titled “MCP server”sygnal-check mcp runs a Model Context Protocol server on stdio, so an agent can call the checker as tools:
| Tool | Arguments | Returns |
|---|---|---|
check | { paths?: string[], strict?: boolean } | { diagnostics, summary: { error, warn, info } } |
graph | { paths?: string[], strict?: boolean } | the app graph (InspectGraph) |
explain | { code: string } | the code’s title, severity, explanation, fix and docs link |
Paths resolve against the server’s working directory (start it in the project root). The default is ["src"].
Claude Code, from the project root:
claude mcp add sygnal-check -- npx --no-install sygnal-check mcpAny client that takes a JSON config (.mcp.json, Claude Desktop, Cursor, …):
{ "mcpServers": { "sygnal-check": { "command": "npx", "args": ["--no-install", "sygnal-check", "mcp"] } }}The server speaks both MCP protocol eras, so any client works with it. A 2026-07-28 client sends each request with its protocol version and client capabilities in _meta (on stdio there are no headers; everything is in the message) and gets server/discover, tools/list and tools/call without an initialize. A client that speaks both eras probes with server/discover first, as the spec describes, and stays on 2026-07-28. An older client (2024-11-05 to 2025-11-25) does the initialize handshake as before. A version the server doesn’t know gets the spec’s “unsupported protocol version” error (-32022), which lists the versions it speaks.
Dev server MCP endpoint
Section titled “Dev server MCP endpoint”sygnal-check mcp reads your source. The dev server endpoint reads the app while it runs: with mcp: true, the Vite plugin serves an MCP endpoint at /__sygnal/mcp (streamable HTTP), and an agent can look at the open page and act on it.
import { defineConfig } from 'vite'import sygnal from 'sygnal/vite'
export default defineConfig({ plugins: [sygnal({ mcp: true })],})Start the dev server, open the app in a browser, and point the agent at the endpoint. Claude Code reads .mcp.json in the project root:
{ "mcpServers": { "sygnal-dev": { "type": "http", "url": "http://localhost:5173/__sygnal/mcp" } }}The same from the command line: claude mcp add --transport http sygnal-dev http://localhost:5173/__sygnal/mcp. Use your dev server’s port if it isn’t 5173.
| Tool | Arguments | Returns |
|---|---|---|
get_state | { component?, path?, app? } | an instance’s state (the root by default; every instance of a Collection item); path reads one field ('todos.0.text') |
dispatch | { component?, action, data?, app? } | sends the action as if the intent had (recorded with cause 'agent'), waits for the render, returns the new state |
component_tree | { component?, app? } | the inspect() graph of the running apps (app: one app’s instances) |
recent_actions | { limit?, component?, type?, cause? } | the DevTools action log, newest last |
get_diagnostics | { code?, severity?, limit? } | the runtime diagnostics, each with its docsUrl |
copy_as_test | { component?, componentImport?, app? } | DevTools’ Copy as test: a renderComponent test of the session |
agent_tools | { call?, input?, all?, app? } | the page’s own agent tools (the agent statics) and their context; with call, runs one |
apps | {} | the page’s apps (each run()): index, root component, instance count |
tabs | {} | the connected pages, with their apps |
check, graph, explain | as above | sygnal-check on your source, when it is installed |
component is a component name or an instance id from component_tree. A page with several run() apps (a widget mounted beside the main app, custom elements from sygnal/element) has all of them served: app is an index or a root component’s name from apps; without it the tools use the first app, or the one that has component. An app keeps its index across a hot update. With several tabs open, the page that loaded or was focused last answers, and each result names it in tab; pass tab (an id from tabs) to choose. An agent_tools call follows the same rules as any other caller: the input is validated, when is checked, and a consequential tool asks the person in the page first (mcp: { confirm: true } runs it without asking, false declines it).
It is a development tool only:
- It exists only in
vite/vite dev. A production build (andvite preview) has no endpoint and no page code for it. - It answers only requests from the same machine, with a local
Host(localhost,*.localhost, a loopback address, or a name in Vite’sserver.allowedHosts). A request with anOriginheader from any other site gets 403, so a web page can’t reach it, also through DNS rebinding. Withvite --hostthe dev server is on your network, but the endpoint still refuses other machines. dispatchruns any action of any component. Don’t turn it on in a dev server you share.
Transport
Section titled “Transport”The endpoint speaks MCP’s streamable HTTP transport in both protocol eras, on the same URL, so the .mcp.json above works with any client:
- 2026-07-28 (stateless): no
initialize. Each request carries its protocol version and client capabilities in_metaand theMCP-Protocol-Version,Mcp-Methodand (fortools/call)Mcp-Nameheaders; the endpoint answersserver/discover,tools/listandtools/call. - 2024-11-05 to 2025-11-25: the
initializehandshake, thentools/list,tools/callandping.
Each request picks its era: a client that speaks both, as the spec describes, gets its 2026-07-28 request answered and stays there; an older client gets the initialize handshake as before. A version the endpoint doesn’t know gets 400 with the spec’s “unsupported protocol version” error (-32022), which lists the versions it speaks.
Each JSON-RPC request is a POST, and each answer is one application/json body. The endpoint opens no SSE stream and assigns no session id. Both revision lines allow that: a server answers a request with either JSON or an event stream (clients must accept both), GET and DELETE get 405, and session ids are optional before 2026-07-28 and gone in it. This endpoint never sends the client anything of its own (no progress, sampling, input requests or list changes, so no subscriptions/listen), and the state it reports lives in the page, not in a session.
inspect
Section titled “inspect”inspect() returns the same graph shape as sygnal-check --graph --json, built from the running app: real component instances, which selectors matched rendered elements, which EVENTS were emitted, and the runtime diagnostics. Use it:
- in a test:
t.inspect()on arenderComponent()result; - in the browser console of a dev app:
window.__SYGNAL_DEVTOOLS__.inspect(); - from code:
import { inspect } from 'sygnal/diagnostics'.
See Diagnostics for the fields and what to look for.
The agent loop
Section titled “The agent loop”A loop that works well for agents (and people):
-
Orient. Run
npx --no-install sygnal-check --graph --json(or thegraphtool) to see the components, their actions, state, context, EVENTS and children before editing. With the dev server endpoint,component_treeandget_stateshow the running app. -
Write the change in the canonical forms (
llms.txt). -
Test it with
renderComponent: drive it with real DOM events, wait, assert, and check for diagnostics.import 'sygnal/diagnostics'import { renderComponent } from 'sygnal'import { test, expect } from 'vitest'import TodoList from './TodoList.jsx'test('adds a todo', async () => {const t = renderComponent(TodoList, { initialState: { items: [], draft: '' } })t.simulateEvent('.new-todo', 'input', { value: 'Write docs' })t.simulateEvent('.add', 'click')const state = await t.next(s => s.items.length === 1)expect(state.items[0].text).toBe('Write docs')t.expectNoDiagnostics()t.dispose()}) -
Read the SYG codes. A failing
expectNoDiagnostics(), the console, orsygnal-checknames a code. -
Explain any code you don’t know:
npx --no-install sygnal-check explain SYG104(or theexplaintool), or the Error Reference. -
Inspect when the cause isn’t obvious:
t.inspect()shows which selector didn’t match (matched: false,isolationHit), which action has no sinks, and which event has no listener. -
Fix, then run
npx --no-install sygnal-check --strictand the tests again until both are clean.
Agents operating your app
Section titled “Agents operating your app”Everything above helps an agent write the app. The same app can also be operated by an agent while it runs, through the actions you allow:
- The
agentstatic declares which actions an LLM may run, with input schemas, and what it sees of the state (read). Only declared actions are tools. - The
chatbehavior is an in-app assistant that operates the component it lives in;commandBarruns one command picked by a decision model. - WebMCP (experimental) offers the same tools to the browser’s own agent, and turns
forms into declarative tools. - MCP Apps put your app inside Claude, ChatGPT or VS Code as a tool’s view.
- The dev server endpoint’s
agent_toolstool calls them from a coding agent, andt.callTool()from a test.
Browser agents that don’t use WebMCP read the accessibility tree, so an accessible app is also an operable one: see Agents that read the page.
How we measure it
Section titled “How we measure it”The Sygnal repository has an evaluation harness (evals/agent-ergonomics/) that gives coding agents the same small feature and bug-fix tasks in a Sygnal app and an equivalent React app, scores each run with hidden acceptance tests, and records wall time, build/test iterations and edit rounds. The baseline run (before this tooling existed) is in evals/agent-ergonomics/results/BASELINE.md.