Codelab
Model

App

The workspace that owns pages, components, stores, and theme.

An App is the unit you build against on the hosted MCP (https://mcp.codelab.app/mcp). It is identity, published URL, and the parent of every page, component, store, and theme token in the project.

Nothing on the canvas exists outside that app. Element trees hang off pages and components the app owns; stores, hooks, schemas, env vars, and theme rules are app-scoped too.

App
├── Pages          routed element trees
├── Components     reusable element trees + fields
├── Stores         runtime state (app-wide or attached to a page/component)
└── Theme          tokens + kind styles

What it is

The app's metadata is id, name, handle, optional customSubdomain / customDomain, description, and SEO defaults (defaultOgImage, faviconUrl, siteTitleSuffix). Pages belong to the app but are listed separately (list_pages) — they are not embedded on the app record.

packages/modules/app/core/src/domain/app.model.types.ts

Every app has a handle (<handle>.codelab.app). That is the guaranteed URL. An admin-granted customSubdomain overrides the handle on the same apex; a customDomain (your host) wins over both. TLS for a custom domain shows as customDomainStatus: "pending" while the cert issues, "active" once live, null if none.

packages/modules/app/core/src/domain/published-url.ts

create_app does not leave an empty shell. It persists the app, a default Home page (isHome: true, routePattern: ""), a chat, variant presets, and heading kind styles.

packages/modules/app/server/src/services/create-app.service.ts

An app must keep at least one page. Home is the page flagged isHome, else the first page.

packages/modules/app/core/src/domain/app.model.ts

How it relates

ChildRole
PageA routed tree visitors hit. Shared chrome lives on a layout page, not copied onto every leaf.
ComponentA reusable tree. Place one on a page as an element whose kind is { _tag: "Component", value: componentId }.
ElementA node inside a page or component. The app never owns an element directly.
StoreNamed runtime state. container: null is app-wide; { _tag: "Page" | "Component", id } scopes it to that tree (and the name can be a plain state per owner). Store actions[] are Action chains.
ThemeApp-level tokens (theme_tokens) and kind styles (style_rules). Brand here before per-element style_*.

packages/modules/store/core/src/domain/store.model.ts packages/modules/mcp/server/src/theme-token.mcp.ts

Hooks, reusable schemas, and env vars are also app-scoped (packages/modules/mcp/server/src/hook.mcp.ts, schema.mcp.ts). Hook methods bind as "<hookId>.<method>" actions; field shapes are the type system.

MCP tools

Connect to https://mcp.codelab.app/mcp. App CRUD addresses the app by id (you can only touch your own). Everything else that lists or mutates inside an app needs a conversation context.

packages/modules/mcp/server/src/app.mcp.ts packages/modules/mcp/server/src/context.mcp.ts

ToolWhat it does
list_appsDiscover appId, name, subdomain, domain, customDomainStatus. Call this before set_context.
set_contextActivate an app. Omit contextId on the first call; keep the returned contextId and pass it to every app-scoped tool. Send the same id when switching apps.
get_contextVerify the active app.
clear_contextDrop this conversation's handle.
create_app{ name, description? } — scaffolds Home + chat + presets. Then set_context with the new appId.
update_appPartial metadata (name, description, customDomain, SEO defaults). Omitted fields stay. customSubdomain is admin-only.
delete_appCascades pages, elements, stores, … Irreversible. If it was the active context, set_context again.

App-scoped follow-ups (all take contextId): list_pages, list_components, list_stores, list_hooks, list_schemas, list_env_vars, list_theme_tokens, list_theme_styles.

Element tools take a container: { _tag: "Page" \| "Component", id } instead of appId. Ownership is checked from that id, not from context — but you still need context to find those ids.

packages/modules/mcp/server/src/mcp-ownership.service.ts

Typical first calls:

list_apps
set_context  { appId }
get_context  { contextId }     # confirm
list_pages   { contextId }     # Home is already there

Gotchas

  • No context, no canvas. list_pages, create_store, add_element (once you need a page id), and the rest fail until set_context. Retain contextId; do not expect a hidden session app. (packages/modules/mcp/server/src/context.mcp.ts)
  • create_app already made Home. Do not create_page for the landing page. list_pages first. (packages/modules/app/adapter/src/ai/create-app.tool.ts)
  • Theme before elements. Tokens (list_theme_tokens / update_theme_tokens) restyle every Button and Card. Kind styles (list_theme_styles / update_theme_style) cover extras CVA does not set. Per-element style_* is last. Token keys are fixed — you change values, not names. (packages/modules/mcp/server/src/theme-token.mcp.ts)
  • previewData is builder-only. Sample {{session.*}} for the canvas. The published site never reads it. (packages/modules/app/core/src/domain/app.model.types.ts)
  • delete_app is total. Every page, element, and store goes with it. (packages/modules/app/adapter/src/ai/delete-app.tool.ts)

On this page