diff --git a/README.md b/README.md index fca5c820c..ed190f406 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ # Reboot -**Build AI Chat Apps — and full-stack web apps — with reactive, durable backends.** +**Trust the code your agent writes.** + +A full-stack framework for the AI era. [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![PyPI](https://img.shields.io/pypi/v/reboot)](https://pypi.org/project/reboot/) @@ -17,88 +19,103 @@ --- -Reboot is a framework for building **reactive, stateful, multiplayer AI chat -apps** — visual apps that run inside ChatGPT, Claude, VS Code, Goose, and more. -It also builds full-stack web apps with reactive backends and React frontends. - -With Reboot, you just write business logic — no wiring up databases, caches, -queues, or retry loops. State survives failures by default. ACID transactions -span multiple states. The React frontend stays in sync in real time. And your -backend is automatically an MCP server. +Modern app development is quickly becoming AI-assisted, or entirely +vibe-coded. Can you trust these apps in production? Yes, but only if +you build on a framework that handles the hard stuff and forces AI +agents to ship clean, modular software. -## AI Chat Apps +Reboot solves the hard problems once, at the framework level, so +neither you nor your coding agent has to. What Rust's borrow checker +did for memory management, and React did for component-based +frontends, Reboot does for your backend: **backend safety and data +encapsulation**, enforced by the framework rather than by review. -Build visual, interactive apps that run inside AI chat interfaces. Define a -`Session` type as an entry point and your methods automatically become tools -the AI can call: - -```python -from reboot.api import ( - API, Field, Methods, Model, Reader, Tool, - Transaction, Type, UI, Writer, -) +## Try Reboot with Claude Code or Codex +Install the Reboot plugin: -class CreateCounterResponse(Model): - counter_id: str = Field(tag=1) +```sh +curl -fsSL https://reboot.dev/install.sh | bash +``` +Then describe what you want: + +> Build me a todo-list app I can use from a browser and from Claude and ChatGPT + +The agent proposes a design, scaffolds the project — API, backend, +frontend, sign-in, tests — and runs it. See +[Build with Claude Code](https://docs.reboot.dev/get_started/claude_code) +or [Build with Codex](https://docs.reboot.dev/get_started/codex), or +[build one by hand](https://docs.reboot.dev/get_started/python) to +see every file. + +## Why a new framework? + +Agents are blazingly fast, junior engineers. You cannot trust them to +build your application correctly without a rock-solid foundation to +stand on. A harness isn't enough. + +- **Correct concurrency and retry safety.** Coding agents reliably + ship bugs in these two areas. The only way to fix this is to give + them constraints that make those bugs impossible by construction: + every method declares its kind — `reader`, `writer`, + `transaction`, or `workflow` — and Reboot enforces what each one may + do. +- **Lose nothing on reboot.** Agents don't expect their code to + crash. Until now, the fix was to build on a durable execution + engine. Reboot goes further with **durable applications**: the + moment a function returns, its `state` is saved. Workflows resume + where they failed — the steps that already finished are memoized + rather than run again — and transactions keep everything atomic. No + database, no cache, no queue. +- **Agents are lazy.** You asked for a frontend, but what you really + wanted was a reactive one. You wanted it to retry on an + intermittent network failure, but instead it threw an error and + never cleaned up its local React state. With Reboot you get these + features, and many more, without ever having to ask. +- **Agents make code hard to review.** Even if some harness could get + an agent to handle all of the concerns above, would you want to + review that diff? Could you be sure it didn't introduce bugs? + Reboot's semantics are simple enough, for humans and agents alike, + that you can. + +## What it looks like + +Define your API with [Pydantic](https://docs.reboot.dev/define/pydantic) +in Python (or [Zod](https://docs.reboot.dev/define/zod) in +TypeScript). Every method declares its kind, and whether an AI may +call it: -class UserState(Model): - pass +```python +from reboot.api import API, Field, Methods, Model, Reader, Tool, Type, Writer -class CounterState(Model): - value: int = Field(tag=1, default=0) +class AccountState(Model): + balance: int = Field(tag=1, default=0) -class GetResponse(Model): - value: int = Field(tag=1) +class DepositRequest(Model): + amount: int = Field(tag=1) -class IncrementRequest(Model): - """Request with an amount parameter.""" - amount: int | None = Field(tag=1, default=None) +class BalanceResponse(Model): + balance: int = Field(tag=1) api = API( - User=Type( - state=UserState, - methods=Methods( - create_counter=Transaction( - request=None, - response=CreateCounterResponse, - description="Create a new Counter.", - mcp=Tool(), - ), - ), - ), - Counter=Type( - state=CounterState, + Account=Type( + state=AccountState, methods=Methods( - show_clicker=UI( - request=None, - path="frontend/mcp/clicker", - title="Counter Clicker", - description="Interactive clicker UI.", - ), - create=Writer( - request=None, + deposit=Writer( + request=DepositRequest, response=None, - factory=True, - description="Create the counter at zero.", - mcp=None, - ), - get=Reader( - request=None, - response=GetResponse, - description="Get the current counter " - "value.", + description="Add funds to the account.", mcp=Tool(), ), - increment=Writer( - request=IncrementRequest, - response=None, - description="Increment the counter.", + balance=Reader( + request=None, + response=BalanceResponse, + description="The account's current balance.", mcp=Tool(), ), ), @@ -106,54 +123,87 @@ api = API( ) ``` -### Dive in! - -- [What is an AI Chat App?](https://docs.reboot.dev/ai_chat_apps/what_is) -- [Get Started (Python)](https://docs.reboot.dev/ai_chat_apps/get_started) -- [AI Chat App Examples](https://docs.reboot.dev/ai_chat_apps/examples) - -## Full-stack apps - -Build reactive backends with React frontends — great as a full-page extension -of your AI chat app, or as a standalone web app. - -- [Python Quickstart](https://docs.reboot.dev/full_stack_apps/python) -- [TypeScript Quickstart](https://docs.reboot.dev/full_stack_apps/typescript) -- [Full-stack Examples](https://docs.reboot.dev/full_stack_apps/examples) +Implement it. `self.state` is durable: when the method returns, the +new state is saved, all of it or none of it. -TypeScript backend support is in alpha: the core of Reboot works in -both languages, but some features — including MCP apps, `UI` methods, -the built-in OAuth sign-in flow, and durable agents — are currently -Python-only. If you don't have a strong preference, start with Python. - -## Key features - -**Automatic MCP server.** `Session` methods are automatically exposed as -MCP tools. Other types can opt in with `mcp=Tool()`. `UI` methods open -React apps in the AI's chat. No glue code. - -**Durable state by default.** States survive process crashes, deployments, and -chaos. No external database required. - -**ACID transactions across states.** `transaction` methods compose atomically -across many state instances running on different machines. - -**Reactive React frontend.** Generated hooks keep your UI in sync -without manual management of WebSockets, caches, or polling. +```python +class AccountServicer(Account.Servicer): + + async def deposit( + self, + context: WriterContext, + request: Account.DepositRequest, + ) -> None: + self.state.balance += request.amount + + async def balance( + self, + context: ReaderContext, + ) -> Account.BalanceResponse: + return Account.BalanceResponse(balance=self.state.balance) +``` -**Method system.** Code is safer to write (and _read_) with a clear API -and methods with enforced constraints: `reader` (concurrent, read-only), -`writer` (serialized, mutating), `transaction` (ACID, cross-state), -`workflow` (long-running, durable, cancellable), `ui` (React app in AI -chat). The runtime enforces these guarantees. +Call it from React through generated, typed hooks. `useBalance` +re-renders whenever the balance changes, whether this user, another +user, a workflow, or an AI changed it: -**API-first, code-generated.** Define APIs using Pydantic (Python) or -Zod (TypeScript). Reboot generates type-safe client, server, and React -stubs. +```tsx +const account = useAccount({ id }); +const { response } = account.useBalance(); -## Documentation +await account.deposit({ amount: 50 }); +``` -Full documentation at [docs.reboot.dev](https://docs.reboot.dev/). +The same two methods are tools for Claude, ChatGPT, or any other MCP +client, because they were declared with `mcp=Tool()`. + +## One backend, many frontends + +One app to serve every user — human or machine. You and your agents +can build any kind of app with Reboot: + +- **Humans** reach it through a + [web app](https://docs.reboot.dev/surfaces/web), a + [React Native app](https://docs.reboot.dev/surfaces/react_native) + (alpha), or an + [AI chat app](https://docs.reboot.dev/surfaces/ai_chat) inside + ChatGPT, Claude, or VS Code, where `UI` methods render React + components in the conversation. +- **Agents** reach it over MCP: every method marked `mcp=Tool()` is a + tool. An agent can also run + [inside your app](https://docs.reboot.dev/agents), with durable, + replay-safe model and tool calls. +- **Services** reach it from your own backend code, a script, or a + plain HTTP request. + +Signed-in users come built in. Plug in your favorite auth provider — +Google, GitHub, Auth0, Ory — and Reboot runs the OAuth server in front +of it, so a person signing in auto-constructs their `User`: the +per-user entry point the rest of your app hangs off, and the same +`User` on every surface. See +[Users and sign-in](https://docs.reboot.dev/users/overview). + +## Status + +Python backends are supported today. TypeScript backends and React +Native frontends are in alpha: the core of Reboot works in both +languages, but AI chat apps, `UI` methods, the built-in OAuth sign-in +flow, and durable agents are Python-only for now. More languages are +coming. + +## Get started today + +Reboot is open source. Run a Reboot app on your own infrastructure or +on Reboot Cloud. + +- [Reboot Cloud](https://cloud.reboot.dev/) — deploy with + `rbt cloud up`. +- [Deploy on your own](https://docs.reboot.dev/deploy_on_your_own) — + `rbt serve` on your own machines or Kubernetes. +- [Documentation](https://docs.reboot.dev/) — start with + [How Reboot works](https://docs.reboot.dev/concepts). +- [Examples](https://docs.reboot.dev/get_started/examples) — complete + applications to run and take apart. ## Community diff --git a/documentation/docs/learn_more/agents.mdx b/documentation/docs/agents.mdx similarity index 97% rename from documentation/docs/learn_more/agents.mdx rename to documentation/docs/agents.mdx index 09a0339bd..f9b8be330 100644 --- a/documentation/docs/learn_more/agents.mdx +++ b/documentation/docs/agents.mdx @@ -1,11 +1,17 @@ -# Agents +# Agents inside your app + +An AI agent can reach your application from outside, over +[MCP](/surfaces/ai_chat). It can also run *inside* it, as part of your +own business logic — that is what this page is about. Reboot's `reboot.agents` package lets you run [Pydantic AI](https://ai.pydantic.dev/) -agents inside Reboot [workflows](/learn_more/implement/workflows) with +agents inside Reboot [workflows](/implement/workflows) with **durable, replay-safe execution of model calls and tool calls** -- you get back the same answer on re-runs without re-hitting the LLM provider, and tool side-effects -that have already completed are not repeated. +that have already completed are not repeated. A model call is the most +expensive, least deterministic thing your app does; running it in a +workflow means a crash never pays for it twice. This page covers what's specific to running a Pydantic AI agent on Reboot. For everything that isn't Reboot-specific (model providers, @@ -22,7 +28,7 @@ When you wrap a Pydantic AI agent with Reboot's `Agent`: - **Every tool call** -- whether registered via `@agent.tool`, passed as `tools=`, or contributed via `toolsets=` -- is memoized too, so a tool's side effects aren't repeated on replay. During [effect - validation](/learn_more/side_effects) tools DO re-run, which helps + validation](/side_effects) tools DO re-run, which helps surface non-determinism early. - **Streaming runs are drained inside a memoized block.** Even on the first run, events arrive in a single batch once the model call @@ -235,7 +241,7 @@ Reboot wraps every tool call in `at_least_once`. On replay, the stored return value is reused without invoking your tool function. You therefore can't rely on your tool function being invoked again on replay. Note that during [effect -validation](/learn_more/side_effects), Reboot re-runs your tool to +validation](/side_effects), Reboot re-runs your tool to verify your code is deterministic, so if you are performing external side-effects inside your tool body you need to ensure they are done idempotently (or consider using an `at_most_once` block instead, but diff --git a/documentation/docs/ai_chat_apps/examples.mdx b/documentation/docs/ai_chat_apps/examples.mdx deleted file mode 100644 index c24bdad2f..000000000 --- a/documentation/docs/ai_chat_apps/examples.mdx +++ /dev/null @@ -1,76 +0,0 @@ -# AI Chat App Examples - -## `agent-wiki` - -[`reboot-dev/reboot-agent-wiki`](https://github.com/reboot-dev/reboot-agent-wiki) - _Python backend, React AI Chat App UIs_ - -The `agent-wiki` example is a shared knowledge base humans -and AIs can both read from and write to. Users hand in raw -conversation transcripts; a background "librarian" agent -(built with Pydantic AI) progressively distills those -transcripts into a small, well-organized set of markdown -pages linked from the wiki's own markdown body, which -serves as a living table of contents. Humans browse the -result in embedded React UIs. - -It demonstrates: - -* `UI` methods that render React views (wiki, page, - transcript) inside the AI chat interface. -* A long-running [`workflow`](/learn_more/implement/workflows) - method (`Wiki.ingest`) that - acts as a per-wiki background agent, reacting to new - transcripts as they arrive. -* `Transaction`s that atomically create related state - across multiple types (e.g. `User.create_wiki` creates a - `Wiki` and records its ID on the user in one step). -* Cross-state references via `:` URIs - embedded in markdown, letting the agent build a graph of - pages without a dedicated link table. -* An in-process test suite that exercises the librarian - workflow with a scripted Pydantic AI `FunctionModel`, so - no real Anthropic calls are needed in CI. - -## `chick-potle` - -[`reboot-dev/reboot-chick-potle`](https://github.com/reboot-dev/reboot-chick-potle) - _Python backend, React AI Chat App UIs_ - -The `chick-potle` example is a small food-ordering AI Chat -App. The AI calls tools to start an order, browse the menu, -and add or remove items from the cart; humans see two -embedded React UIs — a menu grid and a cart — rendered -alongside the conversation. - -It demonstrates: - -* `UI` methods that render React views (menu, cart) inside - the AI chat interface. -* The `User` type: auto-constructed per authenticated user, - acting as an entry point that creates a `FoodOrder` via a - `Transaction` (`User.start_order`). -* MCP `Tool`s (`get_menu`, `get_cart`, `add_to_cart`, - `remove_from_cart`) that let the AI drive the order - programmatically. -* Generated React hooks (`useFoodOrder()`) shared between - the menu and cart UIs, so adding an item from one view - immediately updates the other. - -## `ai-chat-counter` - -_Python backend, React AI Chat App UIs_ - -The `ai-chat-counter` example demonstrates building an AI Chat App — -a counter with visual UIs that run inside ChatGPT, Claude, VS -Code, Goose, or any compatible AI client. - -It demonstrates: - -* `UI` methods that open React UIs in the AI chat interface. -* The `User` type: auto-constructed per authenticated user, - acting as an entry point that creates other state types. -* Generated React hooks (`useCounter()`) that work in both AI - and browser contexts. -* An `App.tsx` React component that implements the UI. - -Follow the [AI Chat App quickstart](/ai_chat_apps/get_started) -to build this example from scratch. diff --git a/documentation/docs/ai_chat_apps/get_started.mdx b/documentation/docs/ai_chat_apps/get_started.mdx deleted file mode 100644 index d9d90545f..000000000 --- a/documentation/docs/ai_chat_apps/get_started.mdx +++ /dev/null @@ -1,1468 +0,0 @@ -import { CodeStepByStep, Step } from "../../src/components/Steps"; - -# Get Started (hand-written) - -Build a visual, reactive app that runs inside ChatGPT, Claude, VS Code, -Goose, or any compatible AI client. - -If you want to use **Claude Code** to build your AI Chat App, see the [getting -started with Claude Code](get_started_claude_code) guide. If you want to use **Codex** -to build your app, see the [getting started with Codex](get_started_codex) guide. -If, however, you want to -see exactly what we're feeding these AIs, then this step-by-step guide -is for you! - - -= 3.10 and < 3.13, Docker, and -Node.js >= 20.10. -::: - -`}> - -```sh -mkdir ai-chat-counter && cd ai-chat-counter -uv init --python 3.12.11 -uv add reboot pydantic -``` - - - - -```sh -docker ps -``` - - - - -```sh -mkdir -p api/ai_chat_counter/v1 && touch api/ai_chat_counter/v1/counter.py -``` - - - - - - - -```python -# api/ai_chat_counter/v1/counter.py -from reboot.api import ( - API, - UI, - Field, - Methods, - Model, - Reader, - Tool, - Transaction, - Type, - Writer, -) - - -class CreateCounterRequest(Model): - description: str = Field(tag=1) - - -class CreateCounterResponse(Model): - counter_id: str = Field(tag=1) - - -class CounterEntry(Model): - counter_id: str = Field(tag=1) - description: str = Field(tag=2) - - -class ListCountersResponse(Model): - counters: list[CounterEntry] = Field(tag=1, default_factory=list) - - -class UserState(Model): - counter_ids: list[str] = Field(tag=1, default_factory=list) - - -class InitializeCounterRequest(Model): - description: str = Field(tag=1) - # The `user_id` of the Counter's owner, recorded so that only the - # owner may call the Counter later. - owner_id: str = Field(tag=2) - - -class DescriptionResponse(Model): - description: str = Field(tag=1) - - -class CounterState(Model): - value: int = Field(tag=1, default=0) - description: str = Field(tag=2, default="") - owner_id: str = Field(tag=3, default="") - - -class GetResponse(Model): - value: int = Field(tag=1) - - -class IncrementRequest(Model): - """Request with an amount parameter.""" - amount: int | None = Field(tag=1, default=None) - - -api = API( - User=Type( - state=UserState, - methods=Methods( - create_counter=Transaction( - request=CreateCounterRequest, - response=CreateCounterResponse, - description="Create a new Counter with a " - "description of what it counts. Returns " - "the `counter_id`, which is not " - "human-readable but should be passed to " - "future tool calls that need it.", - mcp=Tool(), - ), - list_counters=Reader( - request=None, - response=ListCountersResponse, - description="List all counters created " - "by this user. Returns `counter_id` and " - "description for each. The `counter_id` " - "is not human-readable, but use it when " - "calling tools that take a `counter_id`.", - mcp=Tool(), - ), - ), - ), - Counter=Type( - state=CounterState, - methods=Methods( - show_clicker=UI( - request=None, - path="frontend/mcp/clicker", - title="Counter Clicker", - description="Interactive clicker UI " - "for the counter.", - ), - create=Writer( - request=InitializeCounterRequest, - response=None, - factory=True, - description="Create the counter at its initial value.", - mcp=None, - ), - get=Reader( - request=None, - response=GetResponse, - description="Get the current counter " - "value.", - mcp=Tool(), - ), - increment=Writer( - request=IncrementRequest, - response=None, - description="Increment the counter by " - "the specified amount.", - mcp=Tool(), - ), - description=Reader( - request=None, - response=DescriptionResponse, - mcp=None, - ), - ), - ), -) -``` - - - -:::info Naming conventions -Your state class should end in `State`, e.g., `UserState`, -`CounterState`. All fields on `User` state must have default -values, because `User` instances are auto-constructed for -each authenticated user. -::: - - - - -```sh -mkdir -p backend/src/servicers && touch backend/src/servicers/counter.py -``` - - - - - - - -```python -# backend/src/servicers/counter.py -from ai_chat_counter.v1.counter import ( - CounterEntry, - CreateCounterRequest, - CreateCounterResponse, - InitializeCounterRequest, - ListCountersResponse, -) -from ai_chat_counter.v1.counter_rbt import Counter, User -from rbt.v1alpha1.errors_pb2 import Ok, PermissionDenied, Unauthenticated -from reboot.aio.auth.authorizers import Authorizer, allow_if, is_app_internal -from reboot.aio.contexts import ( - ReaderContext, - TransactionContext, - WriterContext, -) -from typing import Optional - - -def _caller_is_owner( - *, - context: ReaderContext, - state: Optional[Counter.State], - **kwargs, -): - """Allow when the caller's `user_id` matches the Counter's recorded - `owner_id`. A not-yet-constructed Counter (`state is None`) falls - through to deny.""" - if context.auth is None or not context.auth.user_id: - return Unauthenticated() - if state is not None and context.auth.user_id == state.owner_id: - return Ok() - return PermissionDenied() - - -class UserServicer(User.Servicer): - """Servicer for the User state machine.""" - - async def create_counter( - self, - context: TransactionContext, - request: CreateCounterRequest, - ) -> CreateCounterResponse: - """Create a new Counter and return its ID.""" - counter, _ = await Counter.create( - context, - description=request.description, - owner_id=context.state_id, - ) - self.state.counter_ids.append(counter.state_id) - return CreateCounterResponse( - counter_id=counter.state_id, - ) - - async def list_counters( - self, - context: ReaderContext, - ) -> ListCountersResponse: - """List all counters created by this user.""" - counters = [] - for counter_id in self.state.counter_ids: - response = await Counter.ref(counter_id).description(context) - counters.append( - CounterEntry( - counter_id=counter_id, - description=response.description, - ) - ) - return ListCountersResponse(counters=counters) - - -class CounterServicer(Counter.Servicer): - """Servicer for the Counter state machine.""" - - def authorizer(self) -> Authorizer: - return Counter.Authorizer( - # `create` is restricted to trusted app code and records - # the counter's owner; every other method is restricted to - # that owner (or, again, to trusted app code). - create=allow_if(all=[is_app_internal]), - get=allow_if(any=[_caller_is_owner, is_app_internal]), - increment=allow_if(any=[_caller_is_owner, is_app_internal]), - description=allow_if(any=[_caller_is_owner, is_app_internal]), - ) - - async def create( - self, - context: WriterContext, - request: InitializeCounterRequest, - ) -> None: - """Initialize the counter with a description and record its - owner.""" - self.state.description = request.description - self.state.owner_id = request.owner_id - - async def description( - self, - context: ReaderContext, - ) -> Counter.DescriptionResponse: - """Get the counter's description.""" - return Counter.DescriptionResponse( - description=self.state.description, - ) - - async def increment( - self, - context: WriterContext, - request: Counter.IncrementRequest, - ) -> None: - """Increment the counter by the specified amount.""" - self.state.value += ( - request.amount if request.amount is not None else 1 - ) - - async def get( - self, - context: ReaderContext, - ) -> Counter.GetResponse: - """Get the current counter value.""" - return Counter.GetResponse(value=self.state.value) -``` - - - - - - -```sh -touch backend/src/main.py -``` - - - - -```python -# backend/src/main.py -import asyncio -from reboot.aio.applications import Application -from reboot.aio.auth.oauth import OAuth -from reboot.aio.auth.oauth_providers import ( - Development, - OAuthProviderByEnvironment, -) -from servicers.counter import CounterServicer, UserServicer - - -async def main() -> None: - application = Application( - title="Chat Counter", - description=( - "Lets a chat client create, list, increment, and " - "show counters on your behalf." - ), - servicers=[UserServicer, CounterServicer], - oauth=OAuth( - provider=OAuthProviderByEnvironment( - dev=Development(), - # TODO: set a real provider (e.g. `Google(...)`) before - # production; `prod=None` makes a production deployment fail - # to start until one is chosen. - prod=None, - ) - ), - ) - await application.run() - - -if __name__ == "__main__": - asyncio.run(main()) -``` - - - - - - -```sh -mkdir -p frontend/mcp/clicker -cd frontend && npm init -y -npm install react react-dom @reboot-dev/reboot-react @reboot-dev/reboot-api -npm install -D @vitejs/plugin-react vite vite-plugin-singlefile typescript @types/react @types/react-dom -cd .. -``` - - - - -```sh -touch frontend/mcp/clicker/App.module.css -``` - - - - -```css -/* frontend/mcp/clicker/App.module.css */ - -/* - * The clicker renders inline inside the MCP host (e.g. Claude.ai), - * so it inherits the host's color scheme. We set every color - * explicitly — never relying on inherited text color — and provide - * a dark variant via `prefers-color-scheme` so the widget stays - * legible in both Claude.ai's light and dark modes. - */ -.container { - display: flex; - align-items: center; - width: fit-content; - margin: 0 auto; - gap: 12px; - font-family: system-ui, sans-serif; - padding: 16px 24px; - border: 1px solid #e0e0e0; - border-radius: 12px; - background: #fafafa; - color: #1a1a1a; -} - -.value { - font-size: 32px; - font-weight: 600; - min-width: 48px; - text-align: center; - color: inherit; -} - -.button { - font-size: 20px; - font-weight: 600; - width: 40px; - height: 40px; - border-radius: 8px; - border: 1px solid #ccc; - background: #f5f5f5; - color: #1a1a1a; - cursor: pointer; -} - -.button:hover:not(:disabled) { - background: #ebebeb; -} - -.button:disabled { - cursor: default; - opacity: 0.5; -} - -.popOut { - font-size: 13px; - padding: 6px 10px; - border: none; - border-radius: 8px; - background: transparent; - color: inherit; - opacity: 0.7; - cursor: pointer; - text-decoration: underline; -} - -.popOut:hover { - opacity: 1; -} - -@media (prefers-color-scheme: dark) { - .container { - border-color: #3a3a3a; - background: #1e1e1e; - color: #f0f0f0; - } - - .button { - border-color: #4a4a4a; - background: #2d2d2d; - color: #f0f0f0; - } - - .button:hover:not(:disabled) { - background: #3a3a3a; - } -} -``` - - - - - - -```sh -touch frontend/mcp/clicker/App.tsx -``` - - - - -```tsx -// frontend/mcp/clicker/App.tsx -import { - type UseCounterApi, - useCounter, -} from "@api/ai_chat_counter/v1/counter_rbt_react"; -import { useMcpApp } from "@reboot-dev/reboot-react"; -import { useState, type FC } from "react"; -import css from "./App.module.css"; - -export const ClickerApp: FC = () => { - const { counter, isLoading } = useCounter(); - - if (isLoading) { - return
loading...
; - } - - // `isLoading` is checked first: while it is true, `counter` is - // still `undefined`. Reaching this check with `undefined` therefore - // means resolution finished and there is genuinely no default - // Counter id. - if (counter === undefined) { - console.error("No default Counter id was available; cannot render."); - return
An error occurred, sorry about that!
; - } - - return ; -}; - -const Clicker: FC<{ counter: UseCounterApi }> = ({ counter }) => { - const [isPending, setIsPending] = useState(false); - const { response, isLoading } = counter.useGet(); - - // Reuse the MCP-inferred id to deep-link into the standalone web - // SPA for the very same counter. - const counterId = counter.state_id; - - // The MCP host's app handle, used to open the deep link (see - // `handlePopOut`). `null` when not running under an MCP host. - const mcpApp = useMcpApp(); - - const value = response?.value ?? 0; - - const handleIncrement = async () => { - setIsPending(true); - try { - await counter.increment({ amount: 1 }); - } finally { - setIsPending(false); - } - }; - - const handlePopOut = async () => { - // The standalone web app lives on its own origin (in production - // typically a CDN), never on the backend's, so `VITE_WEB_APP_URL` - // is required in every environment: `web/.env.development` points - // it at the local Vite dev server, and `web/.env.production` must - // name the real web-app host. - const webAppUrl = import.meta.env.VITE_WEB_APP_URL; - const url = - webAppUrl + "/__/frontend/web/?counter=" + encodeURIComponent(counterId); - // The sandboxed MCP UI iframe blocks `window.open` unless the - // host grants `allow-popups`, so ask the host to open the link - // via the MCP Apps `ui/open-link` request. Fall back to - // `window.open` when not under an MCP host (or it declines). - if (mcpApp?.openLink) { - try { - const { isError } = await mcpApp.openLink({ url }); - if (!isError) { - return; - } - } catch { - // Fall through to `window.open` below. - } - } - window.open(url, "_blank", "noopener"); - }; - - if (isLoading && response === undefined) { - return
loading...
; - } - - return ( -
- {value} - - -
- ); -}; -``` - - - -
- - -```sh -touch frontend/mcp/clicker/main.tsx frontend/mcp/clicker/index.html -``` - - - - -```tsx -// frontend/mcp/clicker/main.tsx -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { RebootClientProvider } from "@reboot-dev/reboot-react"; -import { ClickerApp } from "./App"; - -createRoot(document.getElementById("root")!).render( - - - - - -); -``` - - - - - - -```html - - - - - - - Counter Clicker - - -
- - - -``` - - - -
-\` UI _and_ the standalone \`web/\` SPA you'll add below. -- \`RBT_BUILD_TARGET=mcp: vite build\` bundles one \`UI\` method's - React app into a single, self-contained - \`dist/mcp//index.html\` using - [\`vite-plugin-singlefile\`](https://www.npmjs.com/package/vite-plugin-singlefile). - The Reboot server serves it at \`/__/frontend/mcp//index.html\`. -- \`RBT_BUILD_TARGET=web vite build\` bundles the \`web/\` SPA into - \`dist/web/\`, served at \`/__/frontend/web/\`. - -The config auto-discovers MCP UIs from the \`mcp/\` directory, so -adding a new \`mcp//index.html\` is all it takes to build it. - -`}> - - - -```sh -touch frontend/vite.config.ts -``` - - - - -```ts -// Vite configuration for Reboot UIs. -// -// One config drives three jobs. The dev server is selected by -// `command === "serve"`; the two build shapes are selected by the -// `RBT_BUILD_TARGET` env var, set per UI by `build.mjs`. `mode` keeps -// its conventional Vite values — `development` (serve) / `production` -// (build) — so the matching `.env.` files load as usual: -// -// * `vite` (serve): a single dev server that delivers HMR for every -// `mcp/` UI AND the standalone `web/` SPA, all under -// `base: "/__/frontend/"`. Envoy proxies that prefix to this dev -// server (`run --config=hmr`). -// * `RBT_BUILD_TARGET=mcp: vite build`: builds one MCP UI into -// a single, self-contained `dist/mcp//index.html` (assets -// inlined via `vite-plugin-singlefile`). The framework serves it -// at `/__/frontend/mcp//index.html` in dist mode. -// * `RBT_BUILD_TARGET=web vite build`: builds the `web/` SPA into -// `dist/web/` with normal (non-inlined) assets. Its `base` is -// `/__/frontend/web/` so asset URLs resolve when served at that -// prefix. In Vite's `production` mode, it reads -// `web/.env.production`. -import fs from "fs"; -import path from "path"; -import react from "@vitejs/plugin-react"; -import { defineConfig, type Plugin } from "vite"; -import { viteSingleFile } from "vite-plugin-singlefile"; - -// A served directory under `/__/frontend/` requested without its -// trailing slash (e.g. `/__/frontend/web`) doesn't match Vite's static -// index.html serving, which expects `/__/frontend/web/`, so it 404s. -// Redirect the slash-less form to the canonical trailing-slash form so -// the `web/` SPA and each `mcp/` UI load with or without the -// trailing slash — matching how the framework's dist-mode server -// behaves. Only a path that resolves to a real directory with an -// `index.html` is redirected, so Vite's own internal module URLs -// (`@vite/client`, `@react-refresh`) and source or asset files fall -// through untouched. -function redirectFrontendDirTrailingSlash(root: string): Plugin { - const prefix = "/__/frontend/"; - return { - name: "reboot-frontend-dir-trailing-slash", - configureServer(server) { - server.middlewares.use((req, res, next) => { - const url = req.url ?? ""; - const queryAt = url.indexOf("?"); - const pathname = queryAt === -1 ? url : url.slice(0, queryAt); - if (pathname.startsWith(prefix) && !pathname.endsWith("/")) { - const subpath = pathname.slice(prefix.length); - if (fs.existsSync(path.join(root, subpath, "index.html"))) { - const query = queryAt === -1 ? "" : url.slice(queryAt); - // 302 (not 301): a permanent redirect would be cached by the - // browser, which is wrong for a dev server whose routes can - // change between runs. - res.statusCode = 302; - res.setHeader("Location", `${pathname}/${query}`); - res.end(); - return; - } - } - next(); - }); - }, - }; -} - -// When a `web/` SPA exists, treat it as the dev server's home: the bare -// `/`, `/__/frontend`, and `/__/frontend/` otherwise 404 (there's no -// index there), so silently redirect them to `/__/frontend/web/`. Also -// print the friendlier `http:///` at startup instead of the -// `/__/frontend/` base URL Vite would otherwise show (which is a dead -// link). -function serveWebAppAtRoot(root: string): Plugin { - const webIndex = path.resolve(root, "web", "index.html"); - const target = "/__/frontend/web/"; - const homes = new Set(["/", "/__/frontend", "/__/frontend/"]); - return { - name: "reboot-serve-web-app-at-root", - configureServer(server) { - if (!fs.existsSync(webIndex)) return; - server.middlewares.use((req, res, next) => { - const url = req.url ?? ""; - const queryAt = url.indexOf("?"); - const pathname = queryAt === -1 ? url : url.slice(0, queryAt); - if (homes.has(pathname)) { - const query = queryAt === -1 ? "" : url.slice(queryAt); - res.statusCode = 302; - res.setHeader("Location", target + query); - res.end(); - return; - } - next(); - }); - // Print `http:///` instead of the `/__/frontend/` base. - const printUrls = server.printUrls.bind(server); - server.printUrls = () => { - const urls = server.resolvedUrls; - if (urls) { - const toRoot = (u: string) => u.replace(/\/__\/frontend\/$/, "/"); - urls.local = urls.local.map(toRoot); - urls.network = urls.network.map(toRoot); - } - printUrls(); - }; - }, - }; -} - -// Auto-discover MCP UIs: every `mcp//` with an `index.html`. -// There may be no `mcp/` directory at all (a web-only frontend, or -// one whose last MCP UI was removed), so guard the read. -const mcpDir = path.resolve(__dirname, "mcp"); -const mcpNames: string[] = fs.existsSync(mcpDir) - ? fs - .readdirSync(mcpDir) - .filter((name) => fs.existsSync(path.resolve(mcpDir, name, "index.html"))) - : []; - -// Path alias for API imports (`@api/...` -> `./api/...`). -const resolve = { - alias: { - "@api": path.resolve(__dirname, "./api"), - }, - dedupe: ["react", "react-dom", "zod"], -}; - -export default defineConfig(({ command }) => { - // Dev server: serves both the MCP UIs and the `web/` SPA. - // - // UIs use a double iframe architecture: - // MCP Host -> srcdoc (origin=null) -> iframe (origin=localhost:9991) - // - // The inner iframe loads from Envoy ("/__/frontend/**"), which - // proxies to Vite. Because the inner iframe has a real origin, - // Vite's URLs work normally. `base: "/__/frontend/"` ensures all - // paths route through Envoy. - // - // The standalone `web/` SPA is served at `/__/frontend/web/` by this - // same server. It reaches the backend via `VITE_REBOOT_URL` (see - // `web/.env.development`) rather than its own origin, so opening it - // straight from this dev server exercises the real cross-origin - // frontend/backend path — the same shape as a production deploy. - // - // Hot Module Replacement works automatically: Vite's client connects - // to the page's origin, and Envoy proxies WebSocket upgrades to - // Vite. This also works with tunnels (ngrok) since the tunnel - // points to Envoy. - if (command === "serve") { - const port = parseInt(process.env.RBT_VITE_PORT || "4444", 10); - - return { - plugins: [ - react(), - redirectFrontendDirTrailingSlash(__dirname), - serveWebAppAtRoot(__dirname), - ], - root: ".", - // Read `.env*` from `web/` (alongside the SPA), so the `web/` - // SPA's `VITE_REBOOT_URL` is picked up by both this serve and the - // `web/` production build, which is also rooted there. - envDir: path.resolve(__dirname, "web"), - resolve, - base: "/__/frontend/", - server: { - port, - strictPort: true, - // Listen on all interfaces since requests come through - // Envoy (and tunnels). - host: true, - allowedHosts: true, - }, - }; - } - - // Which UI a build targets is read from `RBT_BUILD_TARGET` (set by - // `build.mjs`), not Vite's `mode` — so `mode` stays `production` - // and the `web/` build below reads `web/.env.production`. - const target = process.env.RBT_BUILD_TARGET ?? ""; - - // Build the standalone `web/` SPA into `dist/web/`. We root the - // build at `web/` so `index.html` and its `assets/` land directly - // under `dist/web/` (rather than `dist/web/web/`). Keep assets as - // separate files (a normal multi-file build) and set `base` so - // their URLs resolve when the SPA is served at `/__/frontend/web/`. - if (target === "web") { - return { - plugins: [react()], - root: path.resolve(__dirname, "web"), - base: "/__/frontend/web/", - build: { - outDir: path.resolve(__dirname, "dist/web"), - emptyOutDir: true, - }, - resolve, - }; - } - - // Build one MCP UI (`RBT_BUILD_TARGET=mcp:`). We root the - // build at `mcp//` so the output lands directly at - // `dist/mcp//index.html`, a single self-contained file - // (assets inlined by `vite-plugin-singlefile`). The framework - // serves it at `/__/frontend/mcp//index.html` in dist mode. - const name = target.startsWith("mcp:") ? target.slice("mcp:".length) : ""; - if (!mcpNames.includes(name)) { - const valid = mcpNames.map((n) => `mcp:${n}`).join(", "); - throw new Error( - `Unknown build target: ${target || "(unset)"}. Set ` + - `RBT_BUILD_TARGET=web or one of: ${valid}.` - ); - } - - return { - plugins: [react(), viteSingleFile()], - root: path.resolve(__dirname, "mcp", name), - base: "/__/frontend/", - // Read `.env*` from `web/` (the shared frontend env), so an MCP UI - // can pick up e.g. `VITE_WEB_APP_URL` for a pop-out link. - envDir: path.resolve(__dirname, "web"), - build: { - outDir: path.resolve(__dirname, "dist/mcp", name), - emptyOutDir: true, - assetsInlineLimit: 100000000, - cssCodeSplit: false, - rollupOptions: { - output: { - inlineDynamicImports: true, - }, - }, - }, - resolve, - }; -}); -``` - - - - -\` — the -same counter, opened full-page outside the AI chat. Create that SPA -under \`frontend/web/\`: an \`index.html\`, a \`main.tsx\` entry point -that requires sign-in via \`useUser()\`, and an \`App.tsx\` that lists -the signed-in user's counters, reads \`?counter=\`, and -watches/increments a counter with the same generated -\`useCounter()\` hook. - -`}> - -```sh -mkdir -p frontend/web/src -touch frontend/web/index.html -touch frontend/web/src/main.tsx frontend/web/src/App.tsx -``` - - - - -```html - - - - - - - Chat Counter - - -
- - - -``` - - - - - - -```tsx -// frontend/web/src/main.tsx -import { useUser } from "@api/ai_chat_counter/v1/counter_rbt_react"; -import { - RebootClientProvider, - useSignIn, - useSignOut, -} from "@reboot-dev/reboot-react"; -import { StrictMode } from "react"; -import { createRoot } from "react-dom/client"; -import { App } from "./App"; - -const Root = () => { - const { user, isLoading } = useUser(); - const signIn = useSignIn(); - const signOut = useSignOut(); - // The provider renders us immediately and resolves the signed-in - // user in the background, so we see `isLoading` until `/whoami` - // lands. - if (isLoading) { - return
Checking session…
; - } - if (user === undefined) { - return ( -
-

Chat Counter

-

Sign in to create and browse your counters.

- -
- ); - } - return void signOut()} />; -}; - -// Point the Reboot client at the backend via `VITE_REBOOT_URL`. A web -// app is served from its own origin — a CDN or static host — almost -// never same-origin with the Reboot backend, so a real deploy MUST set -// `VITE_REBOOT_URL` to the backend's origin. `web/.env.development` -// sets it for local dev (to the Envoy endpoint), so this SPA runs on -// the Vite dev server cross-origin to the backend — the same shape as -// production. -createRoot(document.getElementById("root")!).render( - - - - - -); -``` - - - - - - -```tsx -// frontend/web/src/App.tsx -import { - type UseCounterApi, - type UseUserApi, - useCounter, -} from "@api/ai_chat_counter/v1/counter_rbt_react"; -import { useState, type FC } from "react"; - -interface AppProps { - user: UseUserApi; - onSignOut: () => void; -} - -// Read an initial counter ID from the `?counter=` query param so a -// counter can be shared via URL — the MCP clicker's "Open in web app" -// button deep-links here; picking a counter from the list below also -// opens it. -const initialCounterId = (): string => - new URLSearchParams(window.location.search).get("counter") ?? ""; - -export const App: FC = ({ user, onSignOut }) => { - const [counterId, setCounterId] = useState(initialCounterId); - - return ( -
-
-

Chat Counter

-
- {user.state_id} - -
-
- - {counterId.length === 0 ? ( - - ) : ( - <> - - - - )} -
- ); -}; - -// Landing view: create a counter, and open one of the signed-in -// user's own counters (read reactively via `User.list_counters`). -const CounterList: FC<{ user: UseUserApi; onOpen: (id: string) => void }> = ({ - user, - onOpen, -}) => { - const { response: listResponse, isLoading } = user.useListCounters(); - const [description, setDescription] = useState(""); - const [pending, setPending] = useState(false); - - const handleCreate = async () => { - if (description.trim().length === 0) return; - setPending(true); - try { - await user.createCounter({ description }); - setDescription(""); - } finally { - setPending(false); - } - }; - - const counters = listResponse?.counters ?? []; - const showLoading = isLoading && listResponse === undefined; - - return ( - <> -
-

Create counter

-
{ - event.preventDefault(); - void handleCreate(); - }} - > - setDescription(event.target.value)} - placeholder="What does this counter count?" - /> - -
-
- -
-

Your counters

- {showLoading ? ( -

Loading…

- ) : counters.length === 0 ? ( -

No counters yet — create one above.

- ) : ( -
    - {counters.map((counter) => ( -
  • - -
  • - ))} -
- )} -
- - ); -}; - -const CounterView: FC<{ counterId: string }> = ({ counterId }) => { - // The generated zod hook returns the `UseCounterApi` directly; reads - // are exposed as nested `use*` hooks and writes as plain methods. - const counter: UseCounterApi = useCounter({ id: counterId }); - const { response: valueResponse, isLoading } = counter.useGet(); - const { response: descriptionResponse } = counter.useDescription(); - const [pending, setPending] = useState(false); - - // Show a muted placeholder until the description loads rather than - // flashing the "(no description)" fallback first. - const descriptionLoading = descriptionResponse === undefined; - - const handleIncrement = async () => { - setPending(true); - try { - await counter.increment({ amount: 1 }); - } finally { - setPending(false); - } - }; - - return ( -
-

- {descriptionLoading - ? "Loading…" - : descriptionResponse?.description || "(no description)"} -

-
- {isLoading && valueResponse === undefined - ? "…" - : valueResponse?.value ?? 0} -
- -
- ); -}; -``` - - - -
-Configure your .rbtrc} - description={` - -An [\`.rbtrc\`](/develop_locally#rbtrc-and-flags) file contains flags -for the \`rbt\` CLI. Use it to tell Reboot where your API files live, -how to generate code, and how to run your app. - -The key addition for AI Chat Apps is -\`generate --react=frontend/api\`, which generates the React hooks -the UI uses. - -`}> - -```sh -touch .rbtrc -``` - - - - -```sh -# .rbtrc -# Find API definitions in 'api/'. -generate api/ - -# Generate Python code. -generate --python=backend/api/ - -# Generate React hooks. -generate --react=frontend/api - -# Watch for source changes. -dev run --watch=backend/**/*.py -dev run --watch=frontend/dist/**/*.html - -# Python application. -dev run --python - -# Save state between restarts. -dev run --application-name=ai-chat-counter - -# Entrypoint. -dev run --application=backend/src/main.py - -# Default to HMR mode when no --config is specified. -dev run --default-config=hmr - -# Hot Module Replacement (HMR): Vite dev server proxied through Envoy. -# Run Vite in a separate terminal: `cd frontend && npm run dev` -dev run --frontend-root-path=frontend - -dev run:hmr --frontend-host=http://localhost:4444 - -# Dist mode: serve pre-built artifacts from "frontend/dist/" (no Vite HMR). -# Usage: `uv run rbt dev run --config=dist` -# Requires: `cd frontend && npm run build` -dev run:dist --frontend-dist-path=frontend/dist -``` - - - - - - -```sh -uv run rbt dev run & -cd frontend -RBT_BUILD_TARGET=mcp:clicker npx vite build -RBT_BUILD_TARGET=web npx vite build -``` - - - - -```sh -uv run rbt dev run --config=dist -``` - - - - -```sh -npx @mcpjam/inspector@2.23.3 --url http://localhost:9991/mcp --oauth -``` - - -
- - - -

How to try out your app

- -MCPJam opens a browser-based interface where you can test your app's -tools and UIs. - -Click **App Builder** in the left sidebar. You should see tools -including `create_counter`, `counter_get`, -`counter_increment`, and `counter_show_clicker`. - -1. Select **`counter_show_clicker`** and click **Run** to show your UI. You - should be able to increment the counter by clicking a button. -2. If you are signed in to MCPJam you can additionally ask the LLM to - take actions for you in conversation, like your users will. Try - prompts like... - * "show me my clicker UI" - * "increment my counter" -3. As you interact with the UI, notice how actions taken outside the UI - (e.g. asking the LLM to increment the counter) update the UI - reactively. - -Under the hood, your app communicates via MCP (Model Context Protocol), -the standard that AI clients like ChatGPT, Claude, and VS Code use to -discover and interact with apps. - -

Next steps

- -Well done! You have a working AI Chat App. Here are some great -next steps: - -* **[UIs for AI Chat Apps](/learn_more/implement/ui_methods)** — the - full reference for `UI` methods, including parameterized UIs where - the AI passes props to the React component. -* **[Creating tools](/learn_more/define/pydantic#creating-tools-for-the-ai)** — - how to control which methods are callable by the AI, including - exposing methods on non-`User` state types, or hiding some - `User` methods from the AI. -* **[How Reboot uses MCP](/learn_more/mcp_apps)** — learn how - AI Chat Apps work under the hood. -* **[AI Chat App Examples](/ai_chat_apps/examples)** - — more examples to explore. -* **[Deploy to Reboot Cloud](/deploy_on_reboot_cloud)** — deploy - your app with `rbt cloud up`. -* **[Join us on Discord](https://discord.gg/cRbdcS94Nr)** — to - ask questions and let us know what you think! diff --git a/documentation/docs/ai_chat_apps/get_started_claude_code.mdx b/documentation/docs/ai_chat_apps/get_started_claude_code.mdx deleted file mode 100644 index f6cea39cc..000000000 --- a/documentation/docs/ai_chat_apps/get_started_claude_code.mdx +++ /dev/null @@ -1,205 +0,0 @@ -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; - -# Get Started (with Claude Code) - -Build an AI Chat App using -[Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) -and the Reboot plugin. Claude Code writes the code for you — you -describe what you want, approve the plan, and it scaffolds the -entire project. - -## Install the Reboot plugin - -The plugin bundles everything Reboot needs, so -[Claude Code](https://docs.anthropic.com/en/docs/claude-code/overview) -itself is the only thing you need to have installed. - - - - -Install the Reboot plugin for Claude Code with a single -command: - -```sh -curl -fsSL https://reboot.dev/install.sh | bash -``` - -This registers the Reboot marketplace, installs the plugin, -and pre-installs its pinned dependencies so your first build -runs without waiting on downloads. - -Then start Claude Code — or, if it was already running while you -installed, restart it — so the plugin loads. - - - - -Add the marketplace and install the plugin yourself with -Claude Code: - -``` -claude plugin marketplace add reboot-dev/reboot-plugin -``` - -``` -claude plugin install reboot@reboot-plugin -``` - -Or, to auto-enable the plugin for your whole team, add this to your -project's `.claude/settings.json`: - -```json -{ - "extraKnownMarketplaces": { - "reboot-plugin": { - "source": { - "source": "github", - "repo": "reboot-dev/reboot-plugin" - } - } - }, - "enabledPlugins": { - "reboot@reboot-plugin": true - } -} -``` - -Restart any Claude Code session that was already running when you -installed the Reboot plugin so the new plugin loads. - - - - -## Use the skill - -Once installed, invoke the skill with a description of your app: - -``` -/reboot:chat-app Build a counter app with an interactive clicker UI -``` - -You can also just describe what you want to build in a normal -prompt — Claude Code selects the right Reboot skill from your -description. Or invoke `/reboot:app`, which asks whether you want -an AI Chat App or a standalone Web App and then hands off to the -matching skill — for this guide, pick the Chat App. - -The skill settles the design before it writes any code. It will: - -1. Analyze your description and propose a state model -2. Map out which types, methods, AI tools, and UIs your app needs -3. State that design back to you — so you can redirect it before - the code exists - -Claude Code then scaffolds the full project — API definition, -backend servicers, React UI, and configuration — builds it, writes -and runs backend tests, and starts the app. - -## What gets created - -The skill generates a complete Reboot AI Chat App. The most relevant -files are: - -``` -my-app/ -├── .rbtrc # Reboot CLI config -├── pyproject.toml # Python deps (uv) -├── api/ -│ └── my_app/v1/ -│ └── my_app.py # API definition (Pydantic) -├── backend/ -│ ├── api/ # Generated Python bindings -│ ├── src/ -│ │ ├── main.py # Application entrypoint -│ │ ├── example_prompts.py # Setup wizard example prompts -│ │ └── servicers/ -│ │ └── my_app.py # Servicer implementation -│ └── tests/ -│ └── my_app_test.py # Backend behavior tests -└── frontend/ - ├── package.json - ├── build.mjs # Discovers + builds every UI - ├── vite.config.ts - ├── api/ # Generated React bindings (rbt generate) - ├── mcp/ - │ └── my-ui/ - │ ├── index.html - │ ├── index.css # Styling - │ ├── main.tsx # RebootClientProvider entry - │ ├── App.tsx # React component - │ └── App.module.css - └── web/ # Optional standalone browser SPA - ├── index.html - └── src/ - ├── main.tsx - └── App.tsx -``` - -The API definition uses the same `User` + application type -pattern described in the -[hand-written guide](/ai_chat_apps/get_started): `User` is the -auto-constructed entry point whose methods create other state -types, and those types' methods use `mcp=Tool()` to be callable -by the AI. UI methods use `UI()` and render React inside the MCP -host. - -## Test your app - -Claude Code writes backend unit tests covering your app's user -stories and runs them before handing the app off — they help to -make sure that your app works, and keeps working. - -Once the tests pass, Claude Code starts everything the app needs: - -* the Reboot backend (`rbt dev run`), which also serves the MCP - endpoint, -* the Vite frontend dev server, and -* a Cloudflare quick tunnel, so that remote MCP clients (like - ChatGPT) can reach your local app. - -It then opens the app's **setup wizard** at `http://localhost:9991` -— a page served by your app that walks you through connecting an -MCP client (Claude, ChatGPT, or the MCPJam Inspector) and suggests -example prompts to try. - -## Iterate on your app - -You can also use the skill to modify an existing app: - -``` -/reboot:chat-app Add a reset button that sets the counter back to zero -``` - -The skill reads your current code, plans the changes, and waits -for approval before editing. - -## Run it again later - -If you come back to your project in a new session, ask Claude Code -to bring the app back up: - -``` -/reboot:run my-app -``` - -Or just say "Run this Reboot app" from the project directory. The -run skill starts everything your app needs and reopens the setup -wizard. - -## Next steps - -* **[Get Started (with Codex)](/ai_chat_apps/get_started_codex)** — - the equivalent agent-driven workflow for Codex. -* **[Get Started (hand-written)](/ai_chat_apps/get_started)** — - build the same app step by step to understand every file. -* **[UIs for AI Chat Apps](/learn_more/implement/ui_methods)** — - the full reference for `UI` methods. -* **[Creating tools](/learn_more/define/pydantic#creating-tools-for-the-ai)** — - control which methods are callable by the AI. -* **[AI Chat App Examples](/ai_chat_apps/examples)** — more - examples to explore. -* **[Deploy to Reboot Cloud](/deploy_on_reboot_cloud)** — deploy - your app with `rbt cloud up`. -* **[Join us on Discord](https://discord.gg/cRbdcS94Nr)** — ask - questions and share what you're building! diff --git a/documentation/docs/ai_chat_apps/get_started_codex.mdx b/documentation/docs/ai_chat_apps/get_started_codex.mdx deleted file mode 100644 index 12d717773..000000000 --- a/documentation/docs/ai_chat_apps/get_started_codex.mdx +++ /dev/null @@ -1,225 +0,0 @@ -import Tabs from "@theme/Tabs"; -import TabItem from "@theme/TabItem"; - -# Get Started (with Codex) - -Build an AI Chat App using [Codex](https://developers.openai.com/codex/) -and the Reboot plugin. Codex writes the code for you: describe the app, -approve the proposed design, and it scaffolds the Reboot backend, -interactive UI, tests, and local development setup. - -## Install the Reboot plugin - -The plugin bundles everything Reboot needs, so the only prerequisite is -[Codex](https://developers.openai.com/codex/cli) itself (installed and -authenticated). - - - - -Install the Reboot plugin for Codex with a single command: - -```sh -curl -fsSL https://reboot.dev/install.sh | bash -``` - -The installer detects Codex, registers the Reboot plugin marketplace, -installs the plugin, enables Codex hooks, and pre-installs the pinned -tool shims used by the skill. - -:::warning Temporary Codex sandbox opt-out -The installer currently asks to disable Codex's sandbox globally by -writing `sandbox_mode = "danger-full-access"` into -`~/.codex/config.toml`. This is a temporary workaround for an upstream -Codex issue that breaks Python asyncio cross-thread wakeups inside the -sandbox. Reboot's main commands (`rbt generate`, `rbt dev run`, and -related commands) rely on that asyncio behavior. - -If you accept the prompt, the installer tags the setting with a -`# reboot-plugin (managed)` comment so it is easy to find and remove -later. If you decline, the Codex install is skipped because the -plugin's main commands will not work reliably under the current -sandbox behavior. -::: - -Restart Codex after installation so the new plugin, skills, hooks, and -PATH configuration are loaded. - - - - -Register the Reboot plugin marketplace and install the plugin yourself: - -```sh -codex plugin marketplace add reboot-dev/reboot-plugin -``` - -```sh -codex plugin add reboot@reboot-plugin -``` - -Then add the required Codex settings to `~/.codex/config.toml`. Codex -prints the plugin install path in `codex plugin list`; use it to build -the PATH entry for your machine: - -```sh -REBOOT_PLUGIN_ROOT="$(codex plugin list \ - | awk '$1 == "reboot@reboot-plugin" {print $NF; exit}')" - -printf 'features.hooks = true\n' -printf 'shell_environment_policy.set.PATH = "%s/bin:%s"\n' \ - "$REBOOT_PLUGIN_ROOT" "$PATH" -printf 'sandbox_mode = "danger-full-access"\n' -``` - -Copy the output into `~/.codex/config.toml`, at the **top** of the -file (dotted keys pasted below a `[table]` header would become part of -that table). It should look like this, with your actual plugin path: - -```toml -features.hooks = true -shell_environment_policy.set.PATH = "/path/from/codex/plugin/list/bin:/your/existing/path" -sandbox_mode = "danger-full-access" -``` - -:::warning Merge with your existing config -TOML forbids defining the same key or table twice, and Codex refuses -to start on such a config ("duplicate key"). If your config already -has a `[features]` or `[shell_environment_policy]` table, or already -sets `sandbox_mode`, set the values inside your existing tables -instead of pasting the dotted keys above — e.g. add `hooks = true` -under your `[features]` table. -::: - -Restart Codex after changing the config. - - - - -## Use the skill - -In Codex, you do not need a slash command. Start from a normal prompt: - -``` -Build a Reboot counter AI chat app with an interactive clicker UI -``` - -Codex selects the Reboot app skill from the prompt and routes to the AI -Chat App builder. The builder settles the design before it writes any -code. It will: - -1. Analyze your description and propose a state model. -2. Map out the `User` entry point, application types, methods, AI tools, - and UIs. -3. State that design back to you — so you can redirect it before the - code exists. - -Codex then scaffolds the full project — API definition, backend -servicers, React UI, and configuration — builds it, writes and runs -backend tests, and starts the app. - -## What gets created - -The skill generates a complete Reboot AI Chat App. The most relevant -files are: - -``` -my-app/ -├── .rbtrc # Reboot CLI config -├── pyproject.toml # Python deps (uv) -├── api/ -│ └── my_app/v1/ -│ └── my_app.py # API definition (Pydantic) -├── backend/ -│ ├── api/ # Generated Python bindings -│ ├── src/ -│ │ ├── main.py # Application entrypoint -│ │ ├── example_prompts.py # Setup wizard example prompts -│ │ └── servicers/ -│ │ └── my_app.py # Servicer implementation -│ └── tests/ -│ └── my_app_test.py # Backend behavior tests -└── frontend/ - ├── package.json - ├── build.mjs # Discovers + builds every UI - ├── vite.config.ts - ├── api/ # Generated React bindings (rbt generate) - ├── mcp/ - │ └── my-ui/ - │ ├── index.html - │ ├── index.css # Styling - │ ├── main.tsx # RebootClientProvider entry - │ ├── App.tsx # React component - │ └── App.module.css - └── web/ # Optional standalone browser SPA - ├── index.html - └── src/ - ├── main.tsx - └── App.tsx -``` - -The API definition uses the same `User` + application type pattern -described in the [hand-written guide](/ai_chat_apps/get_started): -`User` is the auto-constructed entry point whose methods create other -state types, and those types' methods use `mcp=Tool()` to be callable -by the AI. UI methods use `UI()` and render React inside the MCP host. - -## Test your app - -Codex writes backend unit tests covering your app's user -stories and runs them before handing the app off — they help to -make sure that your app works, and keeps working. - -Once the tests pass, Codex starts everything the app needs: - -* the Reboot backend (`rbt dev run`), which also serves the MCP - endpoint, -* the Vite frontend dev server, and -* a Cloudflare quick tunnel, so that remote MCP clients (like - ChatGPT) can reach your local app. - -It then opens the app's **setup wizard** at `http://localhost:9991` -— a page served by your app that walks you through connecting an -MCP client (Claude, ChatGPT, or the MCPJam Inspector) and suggests -example prompts to try. - -## Iterate on your app - -You can use the same skill to modify an existing app: - -``` -Add a reset button that sets the counter back to zero. -``` - -Codex reads the current Reboot project, proposes a plan, waits for your -approval, then updates the API, servicers, React UI, and tests. - -## Run it again later - -If you close Codex and come back to the project later, ask Codex to run -the Reboot app again: - -``` -Run my Reboot app in ./my-app -``` - -Or just say "Run this Reboot app" from the project directory. The -run skill starts everything your app needs and reopens the setup -wizard. - -## Next steps - -* **[Get Started (with Claude Code)](/ai_chat_apps/get_started_claude_code)** — - the equivalent agent-driven workflow for Claude Code. -* **[Get Started (hand-written)](/ai_chat_apps/get_started)** — - build the same app step by step to understand every file. -* **[UIs for AI Chat Apps](/learn_more/implement/ui_methods)** — - the full reference for `UI` methods. -* **[Creating tools](/learn_more/define/pydantic#creating-tools-for-the-ai)** — - control which methods are callable by the AI. -* **[AI Chat App Examples](/ai_chat_apps/examples)** — more examples - to explore. -* **[Deploy to Reboot Cloud](/deploy_on_reboot_cloud)** — deploy your - app with `rbt cloud up`. -* **[Join us on Discord](https://discord.gg/cRbdcS94Nr)** — ask - questions and share what you're building! diff --git a/documentation/docs/ai_chat_apps/what_is.mdx b/documentation/docs/ai_chat_apps/what_is.mdx deleted file mode 100644 index afd5842be..000000000 --- a/documentation/docs/ai_chat_apps/what_is.mdx +++ /dev/null @@ -1,196 +0,0 @@ -# What is an AI Chat App? - -AI Chat Apps are visual, reactive, stateful apps that run inside AI chat -user interfaces — ChatGPT, Claude, VS Code, Goose, and more. - -Instead of text-only responses, the AI opens **interactive React -UIs** directly in the conversation. Your users click buttons, -fill forms, and see live-updating data — all without leaving the -chat. - -AI Chat Apps are composed of a user interface, backend methods for both -the AI and UI to call, and durable state types that provide the -persistent data your app is built around. - -## Why AI Chat Apps? - -Text-only AI is already dated. Users expect rich, interactive -experiences. With Reboot, AI Chat Apps let you: - -- **Show, don't tell.** Open a visual UI right in your user's chat. -- **React to state changes.** Your UIs update in real time as - backend state changes — whether the user, the AI, or another - client made the change. -- **Persist across sessions.** State is durable. Close the chat, - come back later, and everything is still there. - -## How it works - -In Reboot, you define your chat app with three things: - -1. **`UI` methods** — React components that the AI can open in the - chat interface. Your React `App.tsx` is the implementation. These - connect to business logic and data via... -2. **Backend Methods** — for your UI and/or your AI to call. In Reboot, - these come as [`reader`](/learn_more/implement/readers), - [`writer`](/learn_more/implement/writers), - [`transaction`](/learn_more/implement/transactions), and - [`workflow`](/learn_more/implement/workflows) methods, providing - safety and scalability. -3. **Durable state types** — these are like Python classes, but their - state is automatically persisted. Use these to store the data your - app needs - no external database. This is also where the backend - methods live; they operate on your app's durable state directly. - - - - -```python -# api/ai_chat_counter/v1/counter.py -from reboot.api import ( - API, - UI, - Field, - Methods, - Model, - Reader, - Tool, - Transaction, - Type, - Writer, -) - - -class CreateCounterRequest(Model): - description: str = Field(tag=1) - - -class CreateCounterResponse(Model): - counter_id: str = Field(tag=1) - - -class CounterEntry(Model): - counter_id: str = Field(tag=1) - description: str = Field(tag=2) - - -class ListCountersResponse(Model): - counters: list[CounterEntry] = Field(tag=1, default_factory=list) - - -class UserState(Model): - counter_ids: list[str] = Field(tag=1, default_factory=list) - - -class InitializeCounterRequest(Model): - description: str = Field(tag=1) - # The `user_id` of the Counter's owner, recorded so that only the - # owner may call the Counter later. - owner_id: str = Field(tag=2) - - -class DescriptionResponse(Model): - description: str = Field(tag=1) - - -class CounterState(Model): - value: int = Field(tag=1, default=0) - description: str = Field(tag=2, default="") - owner_id: str = Field(tag=3, default="") - - -class GetResponse(Model): - value: int = Field(tag=1) - - -class IncrementRequest(Model): - """Request with an amount parameter.""" - amount: int | None = Field(tag=1, default=None) - - -api = API( - User=Type( - state=UserState, - methods=Methods( - create_counter=Transaction( - request=CreateCounterRequest, - response=CreateCounterResponse, - description="Create a new Counter with a " - "description of what it counts. Returns " - "the `counter_id`, which is not " - "human-readable but should be passed to " - "future tool calls that need it.", - mcp=Tool(), - ), - list_counters=Reader( - request=None, - response=ListCountersResponse, - description="List all counters created " - "by this user. Returns `counter_id` and " - "description for each. The `counter_id` " - "is not human-readable, but use it when " - "calling tools that take a `counter_id`.", - mcp=Tool(), - ), - ), - ), - Counter=Type( - state=CounterState, - methods=Methods( - show_clicker=UI( - request=None, - path="frontend/mcp/clicker", - title="Counter Clicker", - description="Interactive clicker UI " - "for the counter.", - ), - create=Writer( - request=InitializeCounterRequest, - response=None, - factory=True, - description="Create the counter at its initial value.", - mcp=None, - ), - get=Reader( - request=None, - response=GetResponse, - description="Get the current counter " - "value.", - mcp=Tool(), - ), - increment=Writer( - request=IncrementRequest, - response=None, - description="Increment the counter by " - "the specified amount.", - mcp=Tool(), - ), - description=Reader( - request=None, - response=DescriptionResponse, - mcp=None, - ), - ), - ), -) -``` - - - -Use `mcp=Tool()` on every method you want the AI to call, -including `User` methods. Methods on other types (like `Counter`) -that expose `mcp=Tool()` operate on a specific instance: the AI -receives the instance's state ID when the instance is created -(here, the `counter_id` returned by `create_counter`) and passes -it in subsequent calls. - -Under the hood, AI Chat Apps use [MCP (the Model Context -Protocol)](/learn_more/mcp_apps) — the standard that AI clients use to -discover and interact with external apps. - - -## Get started - -Ready to build one? - -**[Build your first AI Chat App →](/ai_chat_apps/get_started)** diff --git a/documentation/docs/learn_more/call/from_mcp_client.mdx b/documentation/docs/call/from_mcp_client.mdx similarity index 96% rename from documentation/docs/learn_more/call/from_mcp_client.mdx rename to documentation/docs/call/from_mcp_client.mdx index 8b842670b..693b7a84d 100644 --- a/documentation/docs/learn_more/call/from_mcp_client.mdx +++ b/documentation/docs/call/from_mcp_client.mdx @@ -43,7 +43,7 @@ This opens MCPJam in your browser (if no tab opens, visit [http://localhost:6274](http://localhost:6274)), pointed straight at your app's MCP endpoint with OAuth enabled. From there you can browse your app's tools, call them, and see -[`UI`](/learn_more/implement/ui_methods) method React apps rendered +[`UI`](/surfaces/ui_methods) method React apps rendered inline. ## Set up a secure tunnel @@ -95,7 +95,7 @@ its URL for you. :::warning Free ngrok tunnels don't work with Claude -Claude renders [`UI`](/learn_more/implement/ui_methods) methods as +Claude renders [`UI`](/surfaces/ui_methods) methods as HTML with `