Skip to main content

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.

important

All Pydantic fields must include a tag parameter using Field(tag=N). This is required for safe backwards compatibility.

important

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.

Naming conventions

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:

  1. Auto-construction: a User instance is automatically created for each person who signs in. You do not need an initialize function or manual create() calls. Every field on UserState must have a default value, or be optional, since instances are created without arguments.
  2. Automatic ID resolution: callers never specify a state ID for User methods — it is resolved from the authenticated user. That is what lets useUser() in React, and an AI calling a tool, both reach the right instance without being told who the user is.
  3. Default authorization: User methods are accessible to that user and to app-internal calls. No authorizer() 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:

ParameterTypeDescription
namestr or NoneCustom tool name. If None: defaults to the method name.
titlestr or NoneHuman-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:

  1. 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.
  2. 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.