Pydantic
For Python backends, you define your API with Pydantic. A Reboot API is centered on types, which contain state and methods.
A simple type
Here is a ChatRoom with a reader and a writer:
from typing import Optional
from reboot.api import (
API,
Exclusive,
Field,
Methods,
Reader,
Model,
Writer,
Type,
)
class ChatRoomState(Model):
messages: Optional[list[str]] = Field(tag=1)
class MessagesResponse(Model):
messages: list[str] = Field(tag=1)
class SendRequest(Model):
message: str = Field(tag=1)
ChatRoomMethods = Methods(
messages=Reader(
request=None,
response=MessagesResponse,
description="Every message posted to the room so far.",
mcp=None,
),
send=Writer(
request=SendRequest,
response=None,
description="Post one message to the room.",
mcp=None,
),
)
api = API(
ChatRoom=Type(
state=ChatRoomState,
methods=ChatRoomMethods,
),
)
Reboot uses standard
Pydantic BaseModel,
with Model as the base class for your durable state types,
request and response types.
All Pydantic fields must include a tag parameter using
Field(tag=N). This is required for safe backwards compatibility.
Every method must say whether an AI may call it: mcp=Tool() or
mcp=None. There is no default, so leaving it off is an error. See
Creating tools for the AI.
Your state class should end in State, e.g., UserState,
CounterState.
Special state: User
Naming a type User in your API(User=Type(...)) definition triggers
special behavior:
- Auto-construction: a
Userinstance is automatically created for each person who signs in. You do not need aninitializefunction or manualcreate()calls. Every field onUserStatemust have a default value, or be optional, since instances are created without arguments. - Automatic ID resolution: callers never specify a state ID for
Usermethods — it is resolved from the authenticated user. That is what letsuseUser()in React, and an AI calling a tool, both reach the right instance without being told who the user is. - Default authorization:
Usermethods are accessible to that user and to app-internal calls. Noauthorizer()override is needed. See Authorization.
User typically acts as an entry point: its methods create or look
up other state types (like Counter) and return their IDs.
Auto-construction needs to know who is signing in, so an application
with a User type must configure
Application(oauth=...). See
Users and sign-in for the whole picture.
A complete example
User plus an application type, with a
UI method and methods marked mcp=Tool()
so an MCP client can call them:
# api/mcp_ui_counter/v1/counter.py
from reboot.api import (
API,
UI,
Exclusive,
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(
mode=Exclusive(),
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,
),
),
),
)
Creating tools for the AI
Explicit mcp= on every method
Every method must declare its MCP exposure explicitly:
mcp=Tool()— expose the method as an AI-callable tool.mcp=None— hide the method from the AI.
This applies to all state types, including User. The
description field is what the AI reads to decide when and
how to call a tool:
from reboot.api import Tool
UserMethods = Methods(
# AI-callable — the AI can call this.
create_counter=Transaction(
mode=Exclusive(),
request=None,
response=CreateCounterResponse,
description="Create a new Counter. Returns "
"the ID of the new counter.",
mcp=Tool(),
),
# NOT AI-callable — internal use only.
cleanup=Writer(
request=None,
response=None,
description="Clean up old state.",
mcp=None,
),
)
CounterMethods = Methods(
# AI-callable because of `mcp=Tool()`.
get=Reader(
request=None,
response=GetResponse,
description="Get the current counter value.",
mcp=Tool(),
),
# NOT AI-callable.
create=Writer(
request=None,
response=None,
factory=True,
description="Create the counter at zero.",
mcp=None,
),
)
api = API(
User=Type(
state=UserState,
methods=UserMethods,
),
Counter=Type(
state=CounterState,
methods=CounterMethods,
),
)
For non-User types, the AI must specify a state ID when
calling the tool. It receives the ID when it creates the
instance (e.g., from create_counter).
Tool() options
Tool() accepts optional overrides:
| Parameter | Type | Description |
|---|---|---|
name | str or None | Custom tool name. If None: defaults to the method name. |
title | str or None | Human-readable title. If None: defaults to the method name. |
check_stock=Reader(
request=CheckStockRequest,
response=StockResponse,
description="Check stock for an item.",
mcp=Tool(name="inventory_check", title="Check Inventory"),
),
Why hide methods from the AI?
Not every method should be an MCP tool:
- Human-only actions. Some actions should only be triggered by a human — for example, a "Yes, confirm" button in a React UI. The human clicks it to call the method, but the AI cannot.
- Context bloat. Every tool you expose is added to the AI's context window. Exposing too many tools can overwhelm the AI and degrade its performance.
Method kinds
Depending on its kind, a method might be able to
only read (e.g., Reader) or both read and write (e.g., Writer) the
state. A type can have any number of Reader, Writer,
Transaction, Workflow, and UI methods.
Every Transaction also says how it holds the lock on its own state
while it runs, with mode=Exclusive() or mode=Shared(), both
imported from reboot.api. Exclusive() queues concurrent callers of
the state and is the choice for a transaction that writes its own
state, which is most of them; Shared() lets callers proceed
concurrently while none of them writes it and is the choice for a
transaction that mostly reads its own state while writing others. See
Exclusive or shared.
To learn more about how you implement each data type's methods see Implement your API.