chat-app — Build Reboot AI Chat Apps
Build complete Reboot AI Chat Apps from a user description.
Reads from python. This skill is the MCP-App + React
layer on top of the Reboot Python framework. Anything about
Servicers, Reboot contexts, refs, scheduling primitives,
backend LLM / agent calls, error types, the testing harness,
the .rbtrc shape, or pydantic API defaults belongs in
python — load those references for those concerns. This
skill covers what's specific to MCP Chat
Apps: the User-type front door, MCP tool exposure, the UI()
method type, the React/Vite scaffolding, and the cross-cutting
rules unique to that layer.
Installation
Install the plugin (works for both Claude Code and Codex):
curl -fsSL https://reboot.dev/install.sh | bash
This installs the skills for whichever of Claude Code / Codex you
have, then restart your agent so the skills load. See the plugin
README for the manual install, team auto-enable, and the Codex
skill-installer routes.
When to Use
- Building a new Reboot AI Chat App from a description
- Adding features, state, or UI to an existing Reboot AI Chat App
- Modifying state model, methods, or React UI in a Reboot AI Chat App
- Running an existing Reboot AI Chat App — e.g. at the start of a
new session. This needs no Plan or Build phase: load the
run skill, which detects the app type,
starts the backend and frontend, and opens the setup wizard (from
which the user can launch MCPJam on demand).
Read These From python First
Before scaffolding, load the references that cover the backend
mechanics. The patterns in this skill assume you've read them and just
show the chat-app-specific shape on top.
Always relevant:
python/references/patterns-common-gotchas.md — recurring trips
(self.ref().state_id, kwargs convention, --name vs.
--application-name, etc.).
python/references/api-pydantic.md — pydantic API rules (every
Field needs a zero-value default; non-Optional Model-typed
fields can't take defaults).
Defining the API:
python/references/api-methods.md — factory → context type
mapping (Reader/Writer/Transaction/Workflow).
python/references/api-errors.md — typed errors.
python/references/state-collections.md — always read when
the app has any "list of X" concept. Decides whether each X
should be its own state Type (most of the time, yes) and picks
between in-state list[Sub], in-state list[str] of foreign
IDs, or an OrderedMap of foreign IDs. The trap is
defaulting to list[Person]/list[Post]/list[Task] on User
for entity collections — see Step 1 of that reference. A second
trap: a collection synced or scraped from an external system
(a repo's issues, a mailbox, an RSS feed) is unbounded by
definition — model it as an OrderedMap; a size cap never makes
it bounded.
python/references/state-nested-models.md — the same rule from
the nested-Model angle.
python/references/state-actor-decomposition.md — the orthogonal
decomposition: when one state Type has accreted multiple
unrelated concerns (auth/session, persona, background-engine,
transient cache), split each into its own Type. Critical for the
User front door, which otherwise turns into a God actor and
serializes unrelated writers.
Implementing Servicers:
python/references/servicer-{reader,writer,transaction,constructor,authorizer}.md — one per context type.
python/references/rpc-refs.md — self.ref().state_id (never
self.state_id); self.ref().schedule(...).
python/references/rpc-calls.md — kwargs not Request wrappers.
python/references/rpc-constructor-calls.md —
Service.create(context, id) semantics.
Auth (write rules from day one — see "Auth" under Key Framework Concepts):
python/references/auth-allow-if.md,
python/references/auth-built-in-predicates.md,
python/references/auth-custom-predicates.md — the predicate
machinery. Chat apps use oauth= for identity, so real rules are
viable immediately.
python/references/auth-allow-deny.md — narrow uses of
unconditional rules; specifically, when not to reach for
allow().
Workflows:
python/references/servicer-workflow.md — the single,
comprehensive workflow reference. Read it top to bottom: the
@classmethod / WorkflowContext declaration shape, the
call-classification decision tree (Reboot scopes vs.
at_least_once vs. at_most_once), context.loop, inline state
writes,
until / until_changes, and workflow exit semantics.
Project shell:
python/references/lifecycle-{project-setup,rbtrc,application-entry,initialize-hook}.md — the canonical layout,
the CLI flags, the Application(...) constructor, the
initialize hook.
This Skill's References
Chat-app–specific topics, organized by layer. Read them on demand
the same way you would any other skill reference — the patterns
they cover aren't restated inline below.
| Reference |
What's in it |
references/project-shell.md |
.python-version, .rbtrc deltas (HMR + dist configs), pyproject.toml extras, main.py shape, the example_prompts.py module wired into Application(example_prompts=...), durable-state setup. |
references/api-method-types.md |
The pydantic API file. User-type front door, mcp=Tool() / mcp=None, UI() (including parameterized UI props), factory=True on create, Workflow(...) declaration shape. Full Counter API example. |
references/api-state-shapes.md |
Two recurring state shapes: list[Item] with default_factory=list; single nested Model sub-objects as Optional[X] = Field(tag=N, default=None) hydrated in factory create (Gotcha #13). The state-inside-state regression and how to compose state actors via string ID + ref(id). |
references/servicer-patterns.md |
Servicer-side patterns: UserServicer calling <X>.create(context), Workflow Servicer with MyType.ref() (no-arg) magic, inline writers via .per_workflow("alias").write(context, fn) / .per_iteration("alias").write(...) / .always().write(...), scheduling a workflow from a Transaction with .schedule(). |
references/react-scaffolding.md |
The web/ shell: package.json (per-UI build:<name> scripts, no auto-discovery wrappers), vite.config.ts (load-bearing — copy exactly), tsconfig.json / tsconfig.app.json / tsconfig.node.json, index.css theme variables, ui/<name>/index.html, ui/<name>/main.tsx. |
references/react-app-tsx.md |
App.tsx — generated use<Type>() hook usage (reader subscriptions + mutation calls), Python-snake → TypeScript-camel field naming, Zod-validated request/response types. Full Counter App.tsx + App.module.css example. |
references/gotchas.md |
The numbered MCP-Chat-App–specific trip list (1–19): mcp=Tool()/mcp=None required, factory=True on app-type create, MyType.ref() not cls.ref()/self.ref() in workflows, .schedule() from a Transaction, Optional+default=None for nested Models, .read() only on no-arg ref inside a workflow, method-name PascalCase → generated <Type>.<Method>Request, etc. |
references/auth-oauth-providers.md |
Choosing your Application(oauth=...) provider via OAuthProviderByEnvironment(dev=..., prod=...) (the recommended dev=Development(), prod=Google(...) shape; a selected None arm fails startup), the Google / GitHub / Auth0 / Development / Anonymous providers and where credentials come from (incl. the /__/oauth/callback URL), and the user-ID-namespace gotcha that makes switching providers after launch infeasible — choose deliberately before real users have state. |
references/auth-provider-api-calls.md |
Acting on the user's behalf at an external service. Part A — your identity provider's own API: extra OAuth scopes=[...] + store_tokens=True (needs oauth_library() + ciphertext_library() + ordered_map_library()). Part B — any other service (e.g. Slack): your own app_internal=True authorize/callback endpoints calling OAuthTokenManager.ref("<service>").store(...). Both then read back with OAuthTokenManager.ref(...).fetch(...) and call the API inside a Workflow. Per-provider refresh-token behavior; token erasure. |
Workflow: Plan First, Then Build
Always plan the design and get approval before writing code. The
state model is the foundation — getting entities, field types, or
method types wrong means regenerating everything across 12+ files.
Plan Phase
- Analyze the user's description using the State Model Assessment
below.
- Begin a plan for the user to approve (in Claude Code, enter plan
mode; in Codex, present the plan and wait for the go-ahead).
- Present the proposed design:
User type and its methods (the MCP front door for creating new
application-type instances and locating existing ones).
- Application types: state shape (fields, types, tags).
- Method map: which operations, which method type
(Reader/Writer/Transaction/Workflow), which get
UI().
- Tool surface: what the AI will see as callable tools.
- Example prompts: the ~3 named chat scenarios the root-page
wizard will offer, each a sequence of messages exercising a
main user story — and most of them ending on a turn that
renders a
UI() component, not just a tool call (see
"Example Prompts" under Key Framework Concepts).
- Get user approval before writing any files.
- Then execute the Step-by-Step Build Flow.
For updates to existing apps, still plan: read current state, propose
changes, confirm, then modify.
Writing the Plan for Human Review
The plan is read by a human who has not read the skill files.
They are evaluating the design — entities, collections, methods,
auth — not verifying that you followed the skill. Write so the
plan stands on its own.
Don't quote skill-internal terms when presenting the plan.
They mean nothing outside this skill:
Shape A / Shape B / Shape C — name the actual data
structure: list[Sub] of inline sub-records, list[str] of
foreign state IDs, OrderedMap of foreign state IDs.
- "non-state
Model" — say "a flat sub-record that lives and
dies with the parent" or "no identity of its own", in domain
terms.
Gotcha #N and filenames like state-collections.md /
api-state-shapes.md / gotchas.md — drop the citation; if
the rule matters to the design, explain it inline.
factory=True, Field(tag=N), raw pydantic spellings — fine
to mention briefly when the spelling itself is the design
decision, but never as the explanation.
For every design choice, give the what + the why. The what
is the concrete data structure or method type. The why is a
one-clause reason rooted in the user's domain ("grows without
bound, so we need pagination"; "no methods or auth of its own,
so it lives inline").
Examples.
Collection shape reasoning — BAD, uses skill-internal terms:
people_index_id: str — ID of an OrderedMap actor that
holds this user's Persons (Shape C from state-collections.md
— unbounded; PRM is explicitly called out as a Shape C case).
Collection shape reasoning — GOOD:
people_index_id: str — points to an OrderedMap that holds
this user's Persons. An OrderedMap (rather than an inline
list) because a PRM grows without bound and the UI will
paginate / sort by recency.
Nested model reasoning — BAD, uses skill-internal terms:
Relationship and Event are non-state Models — Shape A.
Nested model reasoning — GOOD:
Relationship and Event live inline on Person as
list[Relationship] / list[Event]. They don't get their
own state actors because they have no lifecycle, methods, or
auth independent of the Person they belong to.
Escape hatch. When the precise type name is what the user
needs to see ("I'm proposing OrderedMap here, not list[str]"),
name the type — but pair it with the plain-English reason in the
same sentence. The rule is "no bare jargon", not "no technical
terms".
State Model Assessment
Before writing code, analyze the user's request:
Application types — decompose aggressively. List every
distinct entity the user is going to add / edit / list / find
over time (people, posts, tasks, events, documents, accounts,
…). Each entity becomes its own Type with its own state,
even when "each User only has a few of them". Anything you can
imagine the user add-ing / remove-ing / find-ing by name
has its own identity and belongs in its own actor. The default
wrong move is packing everything into User's state as
list[Person] (or list[Post], list[Task], …) — that
flattens N actors into one, prevents per-entity auth/methods,
and forces a full rewrite when the collection grows. A
collection your app syncs or scrapes from an external
system (a GitHub repo's issues, a mailbox, an RSS feed) is an
entity collection too — and unbounded by definition — even
though the user never "adds" to it directly. See
python/references/state-collections.md Step 1 for the full
decomposition signal list.
Container shape for each collection. Once an entity is its
own Type, the parent (typically User) stores references,
not objects. Three shapes (full table + worked PRM example in
python/references/state-collections.md):
list[Sub] of non-state Models — for bounded sub-records
that genuinely belong with the parent (line items on an Order,
tags on a Post). NOT for entity collections.
list[str] of foreign state IDs — when the collection of
entity IDs is bounded (low hundreds, occasionally low
thousands) and you always read it whole.
OrderedMap of foreign state IDs — when the collection
grows without bound, needs pagination, range queries, or
ordered iteration. The default choice for any "list of
things the user keeps adding to" (people in a PRM, posts
on a blog, messages in a thread) and for anything synced
from an external source (a repo's issues, a mailbox).
Boundedness is a domain fact, not a number you pick. If you
catch yourself adding a size cap (MAX_ITEMS = 40) so a
collection counts as "bounded" and qualifies for list[Sub] or
list[str], it is unbounded — use OrderedMap. Externally-
synced collections are always OrderedMap. The boundedness
guard in python/references/state-collections.md has the full
rule.
User methods: How does the AI create instances of application
types? Each gets a Transaction on User that calls
<Type>.create(context), then registers the new ID in the
appropriate container (Shape B or C above).
State shape (per type): Fields, types — lists, nested
objects, primitives. Each gets Field(tag=N). Nested Model
sub-objects owned 1:1 by a parent state must be
Optional[X] = Field(tag=N, default=None) and hydrated in the
parent's factory create Writer (Gotcha #13); non-Optional
Model-typed fields reject default= / default_factory=.
Full rules + examples in
references/api-state-shapes.md.
Operations: Map to the right method type:
Reader — read-only queries.
Writer — single-state mutations.
Transaction — multi-state atomic operations (e.g. transfer
between two accounts, or User creating an application-type
instance).
Workflow — long-running control flows with loops, scheduling,
and idempotency helpers.
Tool surface: Which operations need explicit tool exposure
(mcp=Tool())? Default Servicer methods to mcp=Tool() if the
AI should call them, mcp=None otherwise.
UI placement — for each UI in the app: is the AI passing
in an entity ID for this UI to operate on, or is the UI about
the user as a whole? Per-entity UIs (show_person,
edit_task, view_document) go on the entity's own Type
with request=None — the AI's tool-call target becomes the
actor ID automatically, and the generated use<Type>() hook
resolves it with no arguments. User-scoped UIs (a dashboard,
a global browser) go on User. Putting a per-entity UI on
User with the entity ID inside a request=<Model> field is
an antipattern; see "UI Placement" in
references/api-method-types.md.
Identity: Single default instance vs. multiple instances?
Cross-state coordination: Does any operation touch multiple
state instances? If yes, use Transaction.
When Correct Decomposition Fights the UI
The React references in this skill show one use<Type>()
subscription per UI — one hook, one actor, one live feed. That
is the easy, documented path, and it quietly pressures you to
flatten an entity collection into list[Item] on a single actor
just so the dashboard can read it in one subscription.
Resolve the tension the other way: the data model wins. When
a collection is its own entity type (Assessment step 1) or
unbounded (step 2, Shape C), keep it decomposed — one actor per
item, an OrderedMap index on the parent — even though that is
more than one actor. Do not collapse the model to fit the
single-subscription pattern. A demo-correct list[Item] that
must be torn apart the moment a real data source is pointed at it
is the exact failure this skill exists to prevent.
The UI still gets its single subscription. Add a composing
reader on the front-door type: a Reader that ranges the
parent's OrderedMap for one page of IDs, reads each item actor,
and returns a page of fully-hydrated objects plus a next_cursor.
The React UI subscribes to that one reader and pages by cursor —
the fan-out across item actors happens server-side, inside the
reader. The composing-reader pattern (backend reader + React
subscription) is in
references/react-app-tsx.md;
Shape C is in python/references/state-collections.md.
Key Framework Concepts (MCP Chat App–specific)
User and Application Types
Every AI Chat App has a User type and one or more application types:
User is the AI's front door for creating and locating
application-type instances. "Front door" means entry point +
delegation, not container for all application state. User
holds identity (e.g. display name) and IDs of the concern-
specific actors it owns; per-concern state (auth/session,
background-engine config, persona config, transient caches) lives
on its own Type and is referenced by ID from User. Typical
User methods are Transactions that create application-type
instances, or Readers that locate existing instances' IDs —
either directly or via indexes whose IDs are stored on User.
User-scoped UIs (a dashboard spanning the whole user, a global
browser) also live here.
- Application types (e.g.
Counter, Person, Task) hold
most of the actual entity state. They typically need a create
Writer with factory=True for construction. Once an entity exists,
anything specific to that entity — Readers, Writers, UIs —
lives on that entity's Type, never on User. The actor
ID is then implicit in every call: the AI passes the entity
ID to the tool, and the generated use<Type>() React hook
auto-resolves the same ID with no arguments.
The most common scaffolding mistake is putting a per-entity UI
(e.g. show_person) on User with the entity ID stuffed into a
request=<Model> field. Don't — see "UI Placement" in
references/api-method-types.md.
A second, equally common mistake is letting User's state accrete
unrelated concerns — auth/session fields alongside persona config
alongside a background engine's configuration alongside a UI cache.
That turns the front door into a God actor and, because writers on
the same actor serialize, makes a login step contend with a persona
edit and a background workflow's state writes. Split each concern
into its own Type and have User reference it by ID. The signals
that warn you are accreting concerns, and the split pattern, are in
python/references/state-actor-decomposition.md.
Full pydantic shape in
references/api-method-types.md;
the UserServicer + <X>.create(context) pattern in
references/servicer-patterns.md.
Tool Exposure Control
Every method must explicitly declare its MCP exposure:
mcp=Tool() — expose the method as an AI-callable tool.
Required on every method (including User methods) the AI should
be able to call.
mcp=None — hide the method from the AI. Use for human-only
actions or to reduce context bloat.
Tool(name="...", title="...") — override the default tool
name or add a human-readable title.
Method Types
The Reader / Writer / Transaction / Workflow markers come from
reboot.api and behave exactly as python's api-methods.md
describes (each fixes the Servicer's context type). The MCP Chat App
adds one more:
UI() — opens a React UI in the AI chat interface. Takes
request= (config type or None), path= (web dir relative to
project root), title=, description=. No servicer
implementation needed — the React app is the implementation.
When request= is a Model, its fields become props on the React
component.
factory=True on an application type's create Writer is the
chat-app spelling of a constructor (see python's
servicer-constructor.md for the underlying mechanic).
Auth: oauth= Provider Selection and Real Authorizers from Day One
MCP Chat Apps wire identity via Application(oauth=...), which takes an
OAuthProviderSelector. The typical shape is
oauth=OAuthProviderByEnvironment(dev=Development(), prod=Google(...)):
under rbt dev you get Development() — a real provider that shows a
fake account picker and issues every caller a verified, stable
dev-{hash} context.auth.user_id, no external IdP — while every other
environment (rbt serve, Reboot Cloud, or anything unrecognized) gets
the real provider. Both arms are required; either may be None, and a
selected None arm makes the app fail to start with a clear
message, so you can't silently ship without sensible auth.
Servicer-side code doesn't change between providers. (In unit tests you
use reboot.aio.tests.Application(oauth_provider=...), not this
selector.)
Choose your production provider deliberately. See
references/auth-oauth-providers.md.
Each provider issues user IDs in its own namespace, so switching
providers after launch strands every user-keyed piece of state.
Development is dev-only — replace it before shipping — and don't
launch on a throwaway like Anonymous planning to "upgrade later";
do it before real users have state.
Consequences for authorizers:
- Write
authorizer() on every Servicer from day one. Don't
defer it "until prod." Identity is wired the same in dev and prod,
so production-shaped rules (allow_if(all=[state_id_is_user_id]),
allow_if(all=[has_verified_token]), etc.) work immediately.
- Don't use
allow() as a default. It declares "this endpoint
is public on the internet, no identity required" — not what you
want for app-state methods.
User-type Servicers don't need a custom authorizer(). The
framework's default rule (state_id_is_user_id + is_app_internal)
is production-worthy already.
- Application-type Servicers (
Counter, TodoList, …) typically
use allow_if(all=[state_id_is_user_id]) when the state belongs to
one user, or compose other predicates (has_verified_token,
custom) when state is shared.
Backend mechanics — predicate composition, custom predicates, the
function-vs-instance footgun — live in python/references/auth-*.md
and python/references/servicer-authorizer.md. The chat-app delta
is just which mode you're in: oauth= + real rules from day one.
Acting on the user's behalf at the provider. Beyond identity,
Google / GitHub / Auth0 can request extra OAuth scopes=[...] and
capture the provider's tokens (store_tokens=True) so the app can call
that provider's API as the signed-in user (their calendar, repos, etc.).
The tokens are stored encrypted and read back with
OAuthTokenManager.ref(GOOGLE).fetch(context, user_id=context.state_id),
and the outbound call goes inside a Workflow. Full setup,
libraries, and the read path are in
references/auth-provider-api-calls.md.
Declarative, Not Decorator
All MCP surface is defined in the API file. main.py is minimal. No
@mcp.tool() decorators.
State Is Durable
State survives restarts. dev run --application-name=<name> in
.rbtrc (see references/project-shell.md)
is what makes that work.
Example Prompts (Root-Page Wizard)
Every MCP Chat App ships example prompts — short, named chat
scenarios the root-page wizard shows users so they can try the app
the moment it boots, without having to invent a first message. They
are not optional polish: a fresh user landing on the wizard with no
suggested prompts has nothing to click, so always author a set.
An example prompt is an ExamplePrompt(title=..., prompts=[...])
(imported from reboot.application). The title is a short label
(and the identity key — same title replaces an existing entry); the
prompts are an ordered sequence of chat messages the user sends
one per turn, walking a real end-to-end flow through the app's MCP
tools (create something → act on it → view the result), not one
isolated message. Write ~3 examples that together cover the app's
main user stories, phrased the way a real user would talk to the
chat client.
Make the prompts show the UI, not just call tools. The embedded
React UIs (the UI() methods) are the whole reason this is an MCP
App and not a plain MCP server — a flow that only calls
tool-only methods and never renders a component demos the boring
half. Every example should end on (or pass through) a turn that
triggers a UI() method — phrase that turn as a natural "show me
/ open / view ..." request so the AI picks the UI tool. In the counter
example, the "…and show me the counter" / "show me the wins counter"
turns are exactly this: they resolve to the Counter UI and render the
live component, not just a text reply. When you write the set, look at
the method map from the plan: for each UI() method, make sure at
least one example drives the user to it.
They live in backend/src/example_prompts.py and are passed to
Application(example_prompts=...) in main.py. Full file shapes and
a worked set are in
references/project-shell.md; the
ai-chat-counter example is the canonical reference.
Project Structure
<project>/
├── .python-version # "3.10"
├── .rbtrc # Line-based config (NOT YAML!)
├── pyproject.toml # Python deps (uv)
├── api/
│ └── <pkg>/v1/
│ └── <name>.py # API definition
├── backend/
│ └── src/
│ ├── main.py # Application entrypoint
│ ├── example_prompts.py # Wizard example prompts
│ └── servicers/
│ └── <name>.py # Servicer implementation
└── web/
├── package.json
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── vite.config.ts
├── index.css # Theme variables
└── ui/
└── <ui-name>/
├── index.html
├── main.tsx # RebootClientProvider entry
├── App.tsx # React component
└── App.module.css
Step-by-Step Build Flow
Only execute after plan approval. All commands run from the
application directory.
- Create
.python-version, pyproject.toml, .rbtrc — see
references/project-shell.md.
uv sync.
- Write API definition (
api/<pkg>/v1/<name>.py) — see
references/api-method-types.md
and references/api-state-shapes.md;
field-level pydantic rules in
python/references/api-pydantic.md.
uv run rbt generate.
- Write servicer (
backend/src/servicers/<name>.py) — see
references/servicer-patterns.md;
context-type rules in python/references/servicer-*.md.
- Write
backend/src/example_prompts.py (the wizard's example
prompts) and main.py (which imports them and passes
example_prompts= to Application) — see
references/project-shell.md and
python/references/lifecycle-application-entry.md.
npm create @reboot-dev/ui.
cd web && npm install.
uv run rbt generate (React bindings need node_modules).
- Customize React UIs — see
references/react-scaffolding.md
for the web/ shell and
references/react-app-tsx.md for
App.tsx patterns.
cd web && npm run build.
- Write and run backend unit tests covering each user-facing
user story before handing the app off. Enumerate the user
stories from the plan — every action the user should be able
to do through the MCP tool surface (e.g. "create a new
todo list", "add an item and see it listed", "rename a
list"). Write one test method per user story in
backend/tests/<servicer>_test.py, following the patterns
in python/references/testing-project-setup.md,
python/references/testing-harness.md, and
python/references/testing-external-context.md. Use one
IsolatedAsyncioTestCase, one external context per test
(name=f"test-{self.id()}"), and
Service.ref(id).method(context, ...) for all calls —
never instantiate Servicers directly. If any servicer has a
real authorizer(), use the permissive-subclass pattern
from testing-harness.md. Run cd backend && uv run pytest
and fix anything that fails. Do not proceed to the next
step until every user-story test passes — these tests are
the gate that catches contract bugs before the user sees
them in MCPJam.
- Create
mcp_servers.json with
{"mcpServers":{"<name>":{"url":"http://localhost:9991/mcp","useOAuth":true}}}.
- Run the app — load the
run skill and
follow it. It is the single canonical "start the app"
procedure: it detects the app type, makes sure dependencies
and secrets are in place, and starts the backend and
frontend. The handoff for a Chat App is the setup wizard,
not the /mcp URL. The backend serves an interactive
setup wizard at its root (http://localhost:9991) — the
page that connects an MCP client (Claude, ChatGPT, MCPJam, …)
and completes OAuth. As the run skill directs, surface that
URL to the user and open it once at first startup. Do not
start the MCPJam inspector as part of running the app — it
launches on demand, only if the user picks it in the wizard.
Don't bypass the run skill by invoking rbt dev run /
npm run dev by hand: those bare commands print only the
API/MCP/inspect URLs, dropping the wizard hint the user
actually needs.
Update Flow
When modifying an existing app:
- Read
.rbtrc, API definition, servicer, main.py.
- Assess state model changes.
- Update API definition → re-run
uv run rbt generate.
- Update servicer methods.
- Update React components.
- When the change adds a new user-facing capability, add or update
an example prompt in
backend/src/example_prompts.py so the
wizard surfaces the new flow.
- If the app isn't already running, bring it up with the
run skill. If it is already running under
rbt dev run, the --watch globs reload it automatically — no
restart needed. Editing .env likewise triggers a restart, so
a new or changed secret is re-read by --env-file without a
manual relaunch.
Specific patterns and file shapes live in the references above —
read them on demand based on what's changing.
Source: reboot-dev/reboot — distributed by TomeVault.
1---2name: chat-app3description: Build complete Reboot AI Chat Apps (MCP Apps) for ChatGPT, Claude, VSCode, Goose, and other MCP hosts. Layers on top of the python skill for backend mechanics; covers what's specific to MCP Chat Apps — the User-type front door, MCP tool exposure, the UI() method type, and the full React/Vite scaffolding. Use when this capability is needed.4---56# chat-app — Build Reboot AI Chat Apps78Build complete Reboot AI Chat Apps from a user description.910> **Reads from `python`.** This skill is the MCP-App + React11> layer on top of the Reboot Python framework. Anything about12> Servicers, Reboot contexts, refs, scheduling primitives,13> backend LLM / agent calls, error types, the testing harness,14> the `.rbtrc` shape, or pydantic API defaults belongs in15> `python` — load those references for those concerns. This16> skill covers what's _specific_ to MCP Chat17> Apps: the `User`-type front door, MCP tool exposure, the `UI()`18> method type, the React/Vite scaffolding, and the cross-cutting19> rules unique to that layer.2021## Installation2223Install the plugin (works for both Claude Code and Codex):2425```bash26curl -fsSL https://reboot.dev/install.sh | bash27```2829This installs the skills for whichever of Claude Code / Codex you30have, then restart your agent so the skills load. See the plugin31README for the manual install, team auto-enable, and the Codex32`skill-installer` routes.3334## When to Use3536- Building a new Reboot AI Chat App from a description37- Adding features, state, or UI to an existing Reboot AI Chat App38- Modifying state model, methods, or React UI in a Reboot AI Chat App39- Running an existing Reboot AI Chat App — e.g. at the start of a40 new session. This needs no Plan or Build phase: load the41 [`run` skill](../run/SKILL.md), which detects the app type,42 starts the backend and frontend, and opens the setup wizard (from43 which the user can launch MCPJam on demand).4445## Read These From `python` First4647Before scaffolding, load the references that cover the backend48mechanics. The patterns in this skill assume you've read them and just49show the chat-app-specific shape on top.5051**Always relevant:**5253- `python/references/patterns-common-gotchas.md` — recurring trips54 (`self.ref().state_id`, kwargs convention, `--name` vs.55 `--application-name`, etc.).56- `python/references/api-pydantic.md` — pydantic API rules (every57 Field needs a zero-value default; non-Optional `Model`-typed58 fields can't take defaults).5960**Defining the API:**6162- `python/references/api-methods.md` — factory → context type63 mapping (Reader/Writer/Transaction/Workflow).64- `python/references/api-errors.md` — typed errors.65- `python/references/state-collections.md` — **always read when66 the app has any "list of X" concept.** Decides whether each X67 should be its own state `Type` (most of the time, yes) and picks68 between in-state `list[Sub]`, in-state `list[str]` of foreign69 IDs, or an `OrderedMap` of foreign IDs. The trap is70 defaulting to `list[Person]`/`list[Post]`/`list[Task]` on `User`71 for entity collections — see Step 1 of that reference. A second72 trap: a collection **synced or scraped from an external system**73 (a repo's issues, a mailbox, an RSS feed) is unbounded by74 definition — model it as an `OrderedMap`; a size cap never makes75 it bounded.76- `python/references/state-nested-models.md` — the same rule from77 the nested-`Model` angle.78- `python/references/state-actor-decomposition.md` — the orthogonal79 decomposition: when one state `Type` has accreted multiple80 unrelated concerns (auth/session, persona, background-engine,81 transient cache), split each into its own `Type`. Critical for the82 `User` front door, which otherwise turns into a God actor and83 serializes unrelated writers.8485**Implementing Servicers:**8687- `python/references/servicer-{reader,writer,transaction,constructor,authorizer}.md` — one per context type.88- `python/references/rpc-refs.md` — `self.ref().state_id` (never89 `self.state_id`); `self.ref().schedule(...)`.90- `python/references/rpc-calls.md` — kwargs not Request wrappers.91- `python/references/rpc-constructor-calls.md` —92 `Service.create(context, id)` semantics.9394**Auth (write rules from day one — see "Auth" under Key Framework Concepts):**9596- `python/references/auth-allow-if.md`,97 `python/references/auth-built-in-predicates.md`,98 `python/references/auth-custom-predicates.md` — the predicate99 machinery. Chat apps use `oauth=` for identity, so real rules are100 viable immediately.101- `python/references/auth-allow-deny.md` — narrow uses of102 unconditional rules; specifically, when **not** to reach for103 `allow()`.104105**Workflows:**106107- `python/references/servicer-workflow.md` — the single,108 comprehensive workflow reference. Read it top to bottom: the109 `@classmethod` / `WorkflowContext` declaration shape, the110 call-classification decision tree (Reboot scopes vs.111 `at_least_once` vs. `at_most_once`), `context.loop`, inline state112 writes,113 `until` / `until_changes`, and workflow exit semantics.114115**Project shell:**116117- `python/references/lifecycle-{project-setup,rbtrc,application-entry,initialize-hook}.md` — the canonical layout,118 the CLI flags, the `Application(...)` constructor, the119 `initialize` hook.120121## This Skill's References122123Chat-app–specific topics, organized by layer. Read them on demand124the same way you would any other skill reference — the patterns125they cover aren't restated inline below.126127| Reference | What's in it |128| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |129| [`references/project-shell.md`](references/project-shell.md) | `.python-version`, `.rbtrc` deltas (HMR + dist configs), `pyproject.toml` extras, `main.py` shape, the `example_prompts.py` module wired into `Application(example_prompts=...)`, durable-state setup. |130| [`references/api-method-types.md`](references/api-method-types.md) | The pydantic API file. `User`-type front door, `mcp=Tool()` / `mcp=None`, `UI()` (including parameterized UI props), `factory=True` on `create`, `Workflow(...)` declaration shape. Full Counter API example. |131| [`references/api-state-shapes.md`](references/api-state-shapes.md) | Two recurring state shapes: `list[Item]` with `default_factory=list`; single nested `Model` sub-objects as `Optional[X] = Field(tag=N, default=None)` hydrated in factory `create` (Gotcha #13). The state-inside-state regression and how to compose state actors via string ID + `ref(id)`. |132| [`references/servicer-patterns.md`](references/servicer-patterns.md) | Servicer-side patterns: `UserServicer` calling `<X>.create(context)`, Workflow Servicer with `MyType.ref()` (no-arg) magic, inline writers via `.per_workflow("alias").write(context, fn)` / `.per_iteration("alias").write(...)` / `.always().write(...)`, scheduling a workflow from a Transaction with `.schedule()`. |133| [`references/react-scaffolding.md`](references/react-scaffolding.md) | The `web/` shell: `package.json` (per-UI `build:<name>` scripts, no auto-discovery wrappers), `vite.config.ts` (load-bearing — copy exactly), `tsconfig.json` / `tsconfig.app.json` / `tsconfig.node.json`, `index.css` theme variables, `ui/<name>/index.html`, `ui/<name>/main.tsx`. |134| [`references/react-app-tsx.md`](references/react-app-tsx.md) | `App.tsx` — generated `use<Type>()` hook usage (reader subscriptions + mutation calls), Python-snake → TypeScript-camel field naming, Zod-validated request/response types. Full Counter `App.tsx` + `App.module.css` example. |135| [`references/gotchas.md`](references/gotchas.md) | The numbered MCP-Chat-App–specific trip list (1–19): `mcp=Tool()`/`mcp=None` required, `factory=True` on app-type `create`, `MyType.ref()` not `cls.ref()`/`self.ref()` in workflows, `.schedule()` from a Transaction, Optional+`default=None` for nested Models, `.read()` only on no-arg ref inside a workflow, method-name PascalCase → generated `<Type>.<Method>Request`, etc. |136| [`references/auth-oauth-providers.md`](references/auth-oauth-providers.md) | Choosing your `Application(oauth=...)` provider via `OAuthProviderByEnvironment(dev=..., prod=...)` (the recommended `dev=Development(), prod=Google(...)` shape; a selected `None` arm fails startup), the `Google` / `GitHub` / `Auth0` / `Development` / `Anonymous` providers and where credentials come from (incl. the `/__/oauth/callback` URL), and the user-ID-namespace gotcha that makes switching providers after launch infeasible — choose deliberately **before** real users have state. |137| [`references/auth-provider-api-calls.md`](references/auth-provider-api-calls.md) | **Acting on the user's behalf** at an external service. Part A — your identity provider's own API: extra OAuth `scopes=[...]` + `store_tokens=True` (needs `oauth_library()` + `ciphertext_library()` + `ordered_map_library()`). Part B — any other service (e.g. Slack): your own `app_internal=True` authorize/callback endpoints calling `OAuthTokenManager.ref("<service>").store(...)`. Both then read back with `OAuthTokenManager.ref(...).fetch(...)` and call the API **inside a `Workflow`**. Per-provider refresh-token behavior; token erasure. |138139## Workflow: Plan First, Then Build140141**Always plan the design and get approval before writing code.** The142state model is the foundation — getting entities, field types, or143method types wrong means regenerating everything across 12+ files.144145### Plan Phase1461471. Analyze the user's description using the State Model Assessment148 below.1492. Begin a plan for the user to approve (in Claude Code, enter plan150 mode; in Codex, present the plan and wait for the go-ahead).1513. Present the proposed design:152 - `User` type and its methods (the MCP front door for creating new153 application-type instances and locating existing ones).154 - Application types: state shape (fields, types, tags).155 - Method map: which operations, which method type156 (Reader/Writer/Transaction/Workflow), which get `UI()`.157 - Tool surface: what the AI will see as callable tools.158 - Example prompts: the ~3 named chat scenarios the root-page159 wizard will offer, each a sequence of messages exercising a160 main user story — and most of them ending on a turn that161 renders a `UI()` component, not just a tool call (see162 "Example Prompts" under Key Framework Concepts).1634. Get user approval before writing any files.1645. Then execute the Step-by-Step Build Flow.165166For updates to existing apps, still plan: read current state, propose167changes, confirm, then modify.168169### Writing the Plan for Human Review170171The plan is read by a **human who has not read the skill files**.172They are evaluating the design — entities, collections, methods,173auth — not verifying that you followed the skill. Write so the174plan stands on its own.175176**Don't quote skill-internal terms** when presenting the plan.177They mean nothing outside this skill:178179- `Shape A` / `Shape B` / `Shape C` — name the actual data180 structure: `list[Sub]` of inline sub-records, `list[str]` of181 foreign state IDs, `OrderedMap` of foreign state IDs.182- "non-state `Model`" — say "a flat sub-record that lives and183 dies with the parent" or "no identity of its own", in domain184 terms.185- `Gotcha #N` and filenames like `state-collections.md` /186 `api-state-shapes.md` / `gotchas.md` — drop the citation; if187 the rule matters to the design, explain it inline.188- `factory=True`, `Field(tag=N)`, raw pydantic spellings — fine189 to mention briefly when the spelling itself is the design190 decision, but never as the explanation.191192**For every design choice, give the what + the why.** The _what_193is the concrete data structure or method type. The _why_ is a194one-clause reason rooted in the user's domain ("grows without195bound, so we need pagination"; "no methods or auth of its own,196so it lives inline").197198**Examples.**199200Collection shape reasoning — BAD, uses skill-internal terms:201202> `people_index_id: str` — ID of an OrderedMap actor that203> holds this user's Persons (Shape C from state-collections.md204> — unbounded; PRM is explicitly called out as a Shape C case).205206Collection shape reasoning — GOOD:207208> `people_index_id: str` — points to an OrderedMap that holds209> this user's Persons. An OrderedMap (rather than an inline210> list) because a PRM grows without bound and the UI will211> paginate / sort by recency.212213Nested model reasoning — BAD, uses skill-internal terms:214215> Relationship and Event are non-state Models — Shape A.216217Nested model reasoning — GOOD:218219> Relationship and Event live inline on Person as220> `list[Relationship]` / `list[Event]`. They don't get their221> own state actors because they have no lifecycle, methods, or222> auth independent of the Person they belong to.223224**Escape hatch.** When the precise type name _is_ what the user225needs to see ("I'm proposing `OrderedMap` here, not `list[str]`"),226name the type — but pair it with the plain-English reason in the227same sentence. The rule is "no bare jargon", not "no technical228terms".229230## State Model Assessment231232Before writing code, analyze the user's request:2332341. **Application types — decompose aggressively.** List every235 distinct entity the user is going to add / edit / list / find236 over time (people, posts, tasks, events, documents, accounts,237 …). **Each entity becomes its own `Type` with its own state**,238 even when "each User only has a few of them". Anything you can239 imagine the user `add`-ing / `remove`-ing / `find`-ing by name240 has its own identity and belongs in its own actor. The default241 wrong move is packing everything into `User`'s state as242 `list[Person]` (or `list[Post]`, `list[Task]`, …) — that243 flattens N actors into one, prevents per-entity auth/methods,244 and forces a full rewrite when the collection grows. A245 collection your app **syncs or scrapes from an external246 system** (a GitHub repo's issues, a mailbox, an RSS feed) is an247 entity collection too — and unbounded by definition — even248 though the user never "adds" to it directly. See249 `python/references/state-collections.md` Step 1 for the full250 decomposition signal list.2512. **Container shape for each collection.** Once an entity is its252 own `Type`, the parent (typically `User`) stores **references**,253 not objects. Three shapes (full table + worked PRM example in254 `python/references/state-collections.md`):255256 - `list[Sub]` of non-state `Model`s — for bounded sub-records257 that genuinely belong with the parent (line items on an Order,258 tags on a Post). NOT for entity collections.259 - `list[str]` of foreign state IDs — when the collection of260 entity IDs is bounded (low hundreds, occasionally low261 thousands) and you always read it whole.262 - `OrderedMap` of foreign state IDs — when the collection263 grows without bound, needs pagination, range queries, or264 ordered iteration. The default choice for any "list of265 things the user keeps adding to" (people in a PRM, posts266 on a blog, messages in a thread) and for anything synced267 from an external source (a repo's issues, a mailbox).268269 **Boundedness is a domain fact, not a number you pick.** If you270 catch yourself adding a size cap (`MAX_ITEMS = 40`) so a271 collection counts as "bounded" and qualifies for `list[Sub]` or272 `list[str]`, it is unbounded — use `OrderedMap`. Externally-273 synced collections are always `OrderedMap`. The boundedness274 guard in `python/references/state-collections.md` has the full275 rule.2762773. **User methods**: How does the AI create instances of application278 types? Each gets a `Transaction` on `User` that calls279 `<Type>.create(context)`, then registers the new ID in the280 appropriate container (Shape B or C above).2814. **State shape (per type)**: Fields, types — lists, nested282 objects, primitives. Each gets `Field(tag=N)`. **Nested `Model`283 sub-objects** owned 1:1 by a parent state must be284 `Optional[X] = Field(tag=N, default=None)` and hydrated in the285 parent's factory `create` Writer (Gotcha #13); non-Optional286 `Model`-typed fields reject `default=` / `default_factory=`.287 Full rules + examples in288 [`references/api-state-shapes.md`](references/api-state-shapes.md).2895. **Operations**: Map to the right method type:290 - `Reader` — read-only queries.291 - `Writer` — single-state mutations.292 - `Transaction` — multi-state atomic operations (e.g. transfer293 between two accounts, or User creating an application-type294 instance).295 - `Workflow` — long-running control flows with loops, scheduling,296 and idempotency helpers.2976. **Tool surface**: Which operations need explicit tool exposure298 (`mcp=Tool()`)? Default Servicer methods to `mcp=Tool()` if the299 AI should call them, `mcp=None` otherwise.3007. **UI placement** — for each UI in the app: **is the AI passing301 in an entity ID for this UI to operate on, or is the UI about302 the user as a whole?** Per-entity UIs (`show_person`,303 `edit_task`, `view_document`) go on the entity's own `Type`304 with `request=None` — the AI's tool-call target becomes the305 actor ID automatically, and the generated `use<Type>()` hook306 resolves it with no arguments. User-scoped UIs (a dashboard,307 a global browser) go on `User`. Putting a per-entity UI on308 `User` with the entity ID inside a `request=<Model>` field is309 an antipattern; see "UI Placement" in310 [`references/api-method-types.md`](references/api-method-types.md).3118. **Identity**: Single default instance vs. multiple instances?3129. **Cross-state coordination**: Does any operation touch multiple313 state instances? If yes, use `Transaction`.314315## When Correct Decomposition Fights the UI316317The React references in this skill show **one `use<Type>()`318subscription per UI** — one hook, one actor, one live feed. That319is the easy, documented path, and it quietly pressures you to320flatten an entity collection into `list[Item]` on a single actor321just so the dashboard can read it in one subscription.322323**Resolve the tension the other way: the data model wins.** When324a collection is its own entity type (Assessment step 1) or325unbounded (step 2, Shape C), keep it decomposed — one actor per326item, an `OrderedMap` index on the parent — even though that is327more than one actor. Do **not** collapse the model to fit the328single-subscription pattern. A demo-correct `list[Item]` that329must be torn apart the moment a real data source is pointed at it330is the exact failure this skill exists to prevent.331332The UI still gets its single subscription. Add a **composing333reader** on the front-door type: a `Reader` that ranges the334parent's `OrderedMap` for one page of IDs, reads each item actor,335and returns a page of fully-hydrated objects plus a `next_cursor`.336The React UI subscribes to that one reader and pages by cursor —337the fan-out across item actors happens server-side, inside the338reader. The composing-reader pattern (backend reader + React339subscription) is in340[`references/react-app-tsx.md`](references/react-app-tsx.md);341Shape C is in `python/references/state-collections.md`.342343## Key Framework Concepts (MCP Chat App–specific)344345### `User` and Application Types346347Every AI Chat App has a `User` type and one or more application types:348349- **`User`** is the AI's front door for **creating and locating**350 application-type instances. **"Front door" means entry point +351 delegation, not container for all application state.** `User`352 holds identity (e.g. display name) and **IDs of the concern-353 specific actors it owns**; per-concern state (auth/session,354 background-engine config, persona config, transient caches) lives355 on its own `Type` and is referenced by ID from `User`. Typical356 `User` methods are `Transaction`s that create application-type357 instances, or `Reader`s that locate existing instances' IDs —358 either directly or via indexes whose IDs are stored on `User`.359 `User`-scoped UIs (a dashboard spanning the whole user, a global360 browser) also live here.361- **Application types** (e.g. `Counter`, `Person`, `Task`) hold362 most of the actual entity state. They typically need a `create`363 Writer with `factory=True` for construction. **Once an entity exists,364 anything specific to that entity — Readers, Writers, UIs —365 lives on that entity's `Type`**, never on `User`. The actor366 ID is then implicit in every call: the AI passes the entity367 ID to the tool, and the generated `use<Type>()` React hook368 auto-resolves the same ID with no arguments.369370The most common scaffolding mistake is putting a per-entity UI371(e.g. `show_person`) on `User` with the entity ID stuffed into a372`request=<Model>` field. Don't — see "UI Placement" in373[`references/api-method-types.md`](references/api-method-types.md).374375A second, equally common mistake is letting `User`'s state accrete376unrelated concerns — auth/session fields alongside persona config377alongside a background engine's configuration alongside a UI cache.378That turns the front door into a God actor and, because writers on379the same actor serialize, makes a login step contend with a persona380edit and a background workflow's state writes. Split each concern381into its own `Type` and have `User` reference it by ID. The signals382that warn you are accreting concerns, and the split pattern, are in383`python/references/state-actor-decomposition.md`.384385Full pydantic shape in386[`references/api-method-types.md`](references/api-method-types.md);387the `UserServicer` + `<X>.create(context)` pattern in388[`references/servicer-patterns.md`](references/servicer-patterns.md).389390### Tool Exposure Control391392Every method must explicitly declare its MCP exposure:393394- **`mcp=Tool()`** — expose the method as an AI-callable tool.395 Required on every method (including `User` methods) the AI should396 be able to call.397- **`mcp=None`** — hide the method from the AI. Use for human-only398 actions or to reduce context bloat.399- **`Tool(name="...", title="...")`** — override the default tool400 name or add a human-readable title.401402### Method Types403404The `Reader` / `Writer` / `Transaction` / `Workflow` markers come from405`reboot.api` and behave exactly as `python`'s `api-methods.md`406describes (each fixes the Servicer's context type). The MCP Chat App407adds one more:408409- **`UI()`** — opens a React UI in the AI chat interface. Takes410 `request=` (config type or `None`), `path=` (web dir relative to411 project root), `title=`, `description=`. **No servicer412 implementation needed** — the React app _is_ the implementation.413 When `request=` is a `Model`, its fields become props on the React414 component.415416`factory=True` on an application type's `create` Writer is the417chat-app spelling of a constructor (see `python`'s418`servicer-constructor.md` for the underlying mechanic).419420### Auth: `oauth=` Provider Selection and Real Authorizers from Day One421422MCP Chat Apps wire identity via `Application(oauth=...)`, which takes an423`OAuthProviderSelector`. The typical shape is424`oauth=OAuthProviderByEnvironment(dev=Development(), prod=Google(...))`:425under `rbt dev` you get `Development()` — a real provider that shows a426fake account picker and issues every caller a verified, stable427`dev-{hash}` `context.auth.user_id`, no external IdP — while every other428environment (`rbt serve`, Reboot Cloud, or anything unrecognized) gets429the real provider. Both arms are required; either may be `None`, and a430selected `None` arm makes the app **fail to start** with a clear431message, so you can't silently ship without sensible auth.432Servicer-side code doesn't change between providers. (In unit tests you433use `reboot.aio.tests.Application(oauth_provider=...)`, not this434selector.)435436> **Choose your production provider deliberately.** See437> [`references/auth-oauth-providers.md`](references/auth-oauth-providers.md).438> Each provider issues user IDs in its own namespace, so switching439> providers after launch strands every user-keyed piece of state.440> `Development` is dev-only — replace it before shipping — and don't441> launch on a throwaway like `Anonymous` planning to "upgrade later";442> do it **before** real users have state.443444Consequences for authorizers:445446- **Write `authorizer()` on every Servicer from day one.** Don't447 defer it "until prod." Identity is wired the same in dev and prod,448 so production-shaped rules (`allow_if(all=[state_id_is_user_id])`,449 `allow_if(all=[has_verified_token])`, etc.) work immediately.450- **Don't use `allow()` as a default.** It declares "this endpoint451 is public on the internet, no identity required" — not what you452 want for app-state methods.453- **`User`-type Servicers don't need a custom `authorizer()`.** The454 framework's default rule (`state_id_is_user_id` + `is_app_internal`)455 is production-worthy already.456- **Application-type Servicers (`Counter`, `TodoList`, …)** typically457 use `allow_if(all=[state_id_is_user_id])` when the state belongs to458 one user, or compose other predicates (`has_verified_token`,459 custom) when state is shared.460461Backend mechanics — predicate composition, custom predicates, the462function-vs-instance footgun — live in `python/references/auth-*.md`463and `python/references/servicer-authorizer.md`. The chat-app delta464is just **which mode you're in**: `oauth=` + real rules from day one.465466**Acting on the user's behalf at the provider.** Beyond identity,467`Google` / `GitHub` / `Auth0` can request extra OAuth `scopes=[...]` and468capture the provider's tokens (`store_tokens=True`) so the app can call469that provider's API as the signed-in user (their calendar, repos, etc.).470The tokens are stored encrypted and read back with471`OAuthTokenManager.ref(GOOGLE).fetch(context, user_id=context.state_id)`,472and the outbound call goes **inside a `Workflow`**. Full setup,473libraries, and the read path are in474[`references/auth-provider-api-calls.md`](references/auth-provider-api-calls.md).475476### Declarative, Not Decorator477478All MCP surface is defined in the API file. `main.py` is minimal. No479`@mcp.tool()` decorators.480481### State Is Durable482483State survives restarts. `dev run --application-name=<name>` in484`.rbtrc` (see [`references/project-shell.md`](references/project-shell.md))485is what makes that work.486487### Example Prompts (Root-Page Wizard)488489Every MCP Chat App ships **example prompts** — short, named chat490scenarios the root-page wizard shows users so they can try the app491the moment it boots, without having to invent a first message. They492are not optional polish: a fresh user landing on the wizard with no493suggested prompts has nothing to click, so always author a set.494495An example prompt is an `ExamplePrompt(title=..., prompts=[...])`496(imported from `reboot.application`). The `title` is a short label497(and the identity key — same title replaces an existing entry); the498`prompts` are an **ordered sequence** of chat messages the user sends499one per turn, walking a real end-to-end flow through the app's MCP500tools (create something → act on it → view the result), not one501isolated message. Write ~3 examples that together cover the app's502main user stories, phrased the way a real user would talk to the503chat client.504505**Make the prompts show the UI, not just call tools.** The embedded506React UIs (the `UI()` methods) are the whole reason this is an MCP507**App** and not a plain MCP server — a flow that only calls508tool-only methods and never renders a component demos the boring509half. Every example should end on (or pass through) a turn that510triggers a `UI()` method — phrase that turn as a natural "show me511/ open / view ..." request so the AI picks the UI tool. In the counter512example, the "…and show me the counter" / "show me the wins counter"513turns are exactly this: they resolve to the `Counter` UI and render the514live component, not just a text reply. When you write the set, look at515the method map from the plan: for each `UI()` method, make sure at516least one example drives the user to it.517518They live in `backend/src/example_prompts.py` and are passed to519`Application(example_prompts=...)` in `main.py`. Full file shapes and520a worked set are in521[`references/project-shell.md`](references/project-shell.md); the522`ai-chat-counter` example is the canonical reference.523524## Project Structure525526```527<project>/528├── .python-version # "3.10"529├── .rbtrc # Line-based config (NOT YAML!)530├── pyproject.toml # Python deps (uv)531├── api/532│ └── <pkg>/v1/533│ └── <name>.py # API definition534├── backend/535│ └── src/536│ ├── main.py # Application entrypoint537│ ├── example_prompts.py # Wizard example prompts538│ └── servicers/539│ └── <name>.py # Servicer implementation540└── web/541 ├── package.json542 ├── tsconfig.json543 ├── tsconfig.app.json544 ├── tsconfig.node.json545 ├── vite.config.ts546 ├── index.css # Theme variables547 └── ui/548 └── <ui-name>/549 ├── index.html550 ├── main.tsx # RebootClientProvider entry551 ├── App.tsx # React component552 └── App.module.css553```554555## Step-by-Step Build Flow556557**Only execute after plan approval. All commands run from the558application directory.**5595601. Create `.python-version`, `pyproject.toml`, `.rbtrc` — see561 [`references/project-shell.md`](references/project-shell.md).5622. `uv sync`.5633. Write API definition (`api/<pkg>/v1/<name>.py`) — see564 [`references/api-method-types.md`](references/api-method-types.md)565 and [`references/api-state-shapes.md`](references/api-state-shapes.md);566 field-level pydantic rules in567 `python/references/api-pydantic.md`.5684. `uv run rbt generate`.5695. Write servicer (`backend/src/servicers/<name>.py`) — see570 [`references/servicer-patterns.md`](references/servicer-patterns.md);571 context-type rules in `python/references/servicer-*.md`.5726. Write `backend/src/example_prompts.py` (the wizard's example573 prompts) and `main.py` (which imports them and passes574 `example_prompts=` to `Application`) — see575 [`references/project-shell.md`](references/project-shell.md) and576 `python/references/lifecycle-application-entry.md`.5777. `npm create @reboot-dev/ui`.5788. `cd web && npm install`.5799. `uv run rbt generate` (React bindings need `node_modules`).58010. Customize React UIs — see581 [`references/react-scaffolding.md`](references/react-scaffolding.md)582 for the `web/` shell and583 [`references/react-app-tsx.md`](references/react-app-tsx.md) for584 `App.tsx` patterns.58511. `cd web && npm run build`.58612. **Write and run backend unit tests covering each user-facing587 user story before handing the app off.** Enumerate the user588 stories from the plan — every action the user should be able589 to _do_ through the MCP tool surface (e.g. "create a new590 todo list", "add an item and see it listed", "rename a591 list"). Write one test method per user story in592 `backend/tests/<servicer>_test.py`, following the patterns593 in `python/references/testing-project-setup.md`,594 `python/references/testing-harness.md`, and595 `python/references/testing-external-context.md`. Use one596 `IsolatedAsyncioTestCase`, one external context per test597 (`name=f"test-{self.id()}"`), and598 `Service.ref(id).method(context, ...)` for all calls —599 never instantiate Servicers directly. If any servicer has a600 real `authorizer()`, use the permissive-subclass pattern601 from `testing-harness.md`. Run `cd backend && uv run pytest`602 and fix anything that fails. Do not proceed to the next603 step until every user-story test passes — these tests are604 the gate that catches contract bugs before the user sees605 them in MCPJam.60613. Create `mcp_servers.json` with607 `{"mcpServers":{"<name>":{"url":"http://localhost:9991/mcp","useOAuth":true}}}`.60814. Run the app — load the [`run` skill](../run/SKILL.md) and609 follow it. It is the single canonical "start the app"610 procedure: it detects the app type, makes sure dependencies611 and secrets are in place, and starts the backend and612 frontend. **The handoff for a Chat App is the setup wizard,613 not the `/mcp` URL.** The backend serves an interactive614 **setup wizard at its root (`http://localhost:9991`)** — the615 page that connects an MCP client (Claude, ChatGPT, MCPJam, …)616 and completes OAuth. As the run skill directs, surface that617 URL to the user and open it once at first startup. Do **not**618 start the MCPJam inspector as part of running the app — it619 launches on demand, only if the user picks it in the wizard.620 Don't bypass the run skill by invoking `rbt dev run` /621 `npm run dev` by hand: those bare commands print only the622 API/MCP/inspect URLs, dropping the wizard hint the user623 actually needs.624625## Update Flow626627When modifying an existing app:6286291. Read `.rbtrc`, API definition, servicer, `main.py`.6302. Assess state model changes.6313. Update API definition → re-run `uv run rbt generate`.6324. Update servicer methods.6335. Update React components.6346. When the change adds a new user-facing capability, add or update635 an example prompt in `backend/src/example_prompts.py` so the636 wizard surfaces the new flow.6377. If the app isn't already running, bring it up with the638 [`run` skill](../run/SKILL.md). If it is already running under639 `rbt dev run`, the `--watch` globs reload it automatically — no640 restart needed. Editing `.env` likewise triggers a restart, so641 a new or changed secret is re-read by `--env-file` without a642 manual relaunch.643644Specific patterns and file shapes live in the references above —645read them on demand based on what's changing.646647---648> Source: [reboot-dev/reboot](https://github.com/reboot-dev/reboot) — distributed by [TomeVault](https://tomevault.io).649<!-- tomevault:4.0:skill_md:2026-06-18 -->