Runtime-defined content types
Smeldr lets an AI agent or operator declare a new content type at runtime — its name, fields, and public URL — without touching Go code or redeploying. The framework provisions storage, public HTTP routes, admin routes, MCP tools, and sitemap entries automatically. The type participates in the same draft/publish/archive lifecycle as compiled types.
Defining a type via MCP
Call the define_content_type tool with a type_name, optional label, optional url_prefix, and a fields array:
define_content_type
type_name: "recipe"
label: "Recipe"
url_prefix: "/recipes"
fields:
- name: "Title", type: "string", required: true, role: "title"
- name: "Ingredients", type: "string", format: "markdown"
- name: "Steps", type: "string", format: "markdown"
Verified field types: string, integer, boolean, array, object.
Verified role values: title, description, body, summary, og_image. The field with role "title" is used to derive the item slug automatically.
url_prefix
url_prefix is optional. When set, Smeldr registers two public GET routes immediately after the call succeeds — no restart required:
GET /recipes — published items (JSON list)
GET /recipes/{slug} — single published item (JSON)
url_prefix must start with "/". Leave it empty and the type gets no public HTTP surface at all. It is still fully accessible via admin routes, MCP tools, and CLI.
CRUD via MCP tools
Six generic tools are available whenever app.ServeDynamicContent() is called. All tools target the type by type_name.
| Tool | Role | What it does |
|---|---|---|
define_content_type | Admin | Declare a new content type and register its routes |
create_content | Editor | Create a new draft item |
get_content | Author | Get a single item by slug |
list_content | Author | List items with status filter, pagination, and sort |
update_content | Editor | Update fields on an existing item |
set_content_status | Editor | Transition status: draft -> published / archived |
Slug is auto-derived from the field with role "title". Collision suffixes (-2, -3, ...) are added automatically. Status transitions are the same as compiled types: Draft -> Published/Scheduled -> Archived.
Admin HTTP routes
The admin namespace uses type_name as the path segment, not the URL prefix. The admin surface is stable regardless of where the type is hosted publicly.
| Method | Route | Role | Operation |
|---|---|---|---|
POST | /_content/types | Admin | Define type |
GET | /_content/{type} | Editor | List items at any status |
GET | /_content/{type}/{id} | Editor | Get item by ID |
POST | /_content/{type} | Editor | Create draft |
PATCH | /_content/{type}/{id} | Editor | Update fields |
POST | /_content/{type}/{id}/status | Editor | Set status |
{type} is the type_name: recipe, not /recipes.
ContentList block integration
The content_list block type renders a live feed of any registered content type — compiled or runtime-defined — inside a block-composed page. Set the ContentType field to the type_name:
{
"Type": "content_list",
"ContentType": "recipe",
"Limit": 10
}
Use "recipe", not "/recipes". The block resolves the type by name, so it stays valid even if the public URL changes. Existing content_list blocks from before A154 that stored a URL prefix need their ContentType field updated to the type_name.
Wiring in Go
One call in main.go enables the feature:
app.ServeDynamicContent()
This registers the /_content/ admin namespace, runs idempotent startup migrations, and re-registers public routes for all previously defined types from the database. Call it before app.Handler().
Migration note
Deployments that ran core v1.41.0 (A153) need the url_prefix column added to smeldr_content_type_schemas. MigrateURLPrefixColumn handles this idempotently at startup, called by ServeDynamicContent() automatically. No manual step is required.