@typeroll/mcp-server 0.44.74 → 0.45.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,168 +1,57 @@
1
1
  ---
2
2
  name: tr-page-template
3
- description: Use when several pages on a Typeroll site share the same outer structure — category pages, service-detail pages, landing-page variants — and the user wants to edit the shared bits in one place. Covers two flows: the HTML-mode pattern with partials + `<x-include>` (works everywhere, recommended for Phase 1), and the formal block-mode PageTemplate (when the site is in block mode).
3
+ description: Create or edit a reusable Page template for articles, checklists, products, directory entries or other Pages that share a layout.
4
4
  ---
5
5
 
6
- # Share structure across pages
7
-
8
- When you have N pages that follow the same skeleton — say 7 category landing pages, each with `Hero → Intro → Features grid → CTA banner` — you don't want to edit 7 HTML bodies whenever the design changes. There are two ways to share structure in Typeroll, and the right pick depends on the site's content mode.
9
-
10
- ## Decide which pattern fits
11
-
12
- | Site is mostly in… | Use |
13
- |---|---|
14
- | **HTML mode** (the Phase 1 default — most sites) | Partials + `<x-include>` — pattern A below |
15
- | **Block mode** (`supports_blocks_mode=true` and the page's `content_mode='blocks'`) | PageTemplate via `set_page_template` — pattern B below |
16
-
17
- You can check the site's mode by reading any existing page — `content_mode` is a top-level field on the page doc.
18
-
19
- If unsure, default to **pattern A**. It works on every Typeroll site and the migration to block-mode templates later is mechanical.
20
-
21
- ## Pattern A — Partials + `<x-include>` (HTML mode)
22
-
23
- A partial is a named HTML fragment. Putting `<x-include name="my-partial" />` anywhere in a page body inlines that fragment at build time. Editing the partial updates every page that references it on the next deploy.
24
-
25
- ### Recipe for "7 category pages with shared structure"
26
-
27
- #### 1. Identify the shared chunks
28
-
29
- Walk through one category page and mark which sections are the same across all 7:
30
-
31
- ```
32
- [Hero: title + tagline + image] ← varies per category
33
- [Intro paragraph] ← varies per category
34
- [Common: "Why choose us" three-tile band] ← identical across 7
35
- [Common: CTA banner with newsletter form] ← identical across 7
36
- [Common: footer testimonials] ← identical across 7
37
- ```
38
-
39
- Three reusable bits: `why-choose-us`, `cta-newsletter`, `footer-testimonials`.
40
-
41
- #### 2. Create the partials
42
-
43
- ```
44
- create_partial partial_id="why-choose-us" html_content="<section class=\"why\">
45
- <div class=\"container\">
46
- <h2>Varför oss</h2>
47
- <div class=\"why__grid\">
48
- <div class=\"why__tile\"><h3>Erfarenhet</h3><p>20 år i branschen.</p></div>
49
- <div class=\"why__tile\"><h3>Kvalitet</h3><p>Vi mäter på allt.</p></div>
50
- <div class=\"why__tile\"><h3>Närhet</h3><p>Lokala kontor i tre städer.</p></div>
51
- </div>
52
- </div>
53
- </section>
54
- <style>
55
- .why{padding:4rem 0;background:var(--color-surface)}
56
- .why__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:2rem;margin-top:2rem}
57
- .why__tile h3{font-family:var(--font-heading);margin-bottom:0.5rem}
58
- </style>"
59
- ```
60
-
61
- Same for `cta-newsletter` and `footer-testimonials`.
62
-
63
- The partial-id is what `<x-include>` references — keep it kebab-case and descriptive.
64
-
65
- #### 3. Build each category page using the partials
66
-
67
- ```
68
- create_page title="Tjänster för bostadsrätter" slug="brf" content_mode="html"
69
- html_content="<section class=\"hero hero--brf\">
70
- <div class=\"container\">
71
- <h1>Tjänster för bostadsrättsföreningar</h1>
72
- <p class=\"hero__tagline\">Trygg förvaltning, helt utan överraskningar.</p>
73
- </div>
74
- </section>
75
-
76
- <section class=\"intro\">
77
- <div class=\"container\">
78
- <p>Vi har förvaltat över 200 BRF:er i Stockholmsområdet sedan 2005.</p>
79
- </div>
80
- </section>
81
-
82
- <x-include name=\"why-choose-us\" />
83
- <x-include name=\"cta-newsletter\" />
84
- <x-include name=\"footer-testimonials\" />"
85
- ```
86
-
87
- Repeat for the other 6 categories, varying only the `hero` + `intro` sections.
88
-
89
- #### 4. Edit once, propagate everywhere
90
-
91
- Adding a fourth "Why us" tile? Edit `why-choose-us` once:
92
-
93
- ```
94
- replace_partial partial_id="why-choose-us" html_content="<section class=\"why\">
95
- ...four tiles instead of three...
96
- </section>"
97
- ```
98
-
99
- All 7 pages pick up the change on the next deploy. No multi-page diff.
100
-
101
- ### Edge cases
102
-
103
- - **`<x-include>` self-closes or has an explicit close.** Both forms work: `<x-include name="x" />` and `<x-include name="x"></x-include>`.
104
- - **Nested includes are NOT supported.** If `partial-a` references `<x-include name="partial-b" />`, the inner reference is not expanded. Flatten the hierarchy at design time.
105
- - **Unknown partial → silently empty.** A typo in the `name` attribute removes the tag at expand time with no warning. Verify the partial id matches one returned by `list_partials`.
106
- - **Partial content is sanitised on save.** `<script>` tags get stripped at partial creation, then the inlined HTML is sanitised again when the page renders. Two passes; both intentional.
107
-
108
- ## Pattern B — Formal PageTemplate (block mode)
109
-
110
- Available when the site is in block mode (`supports_blocks_mode=true` and the page's `content_mode='blocks'`). A PageTemplate is a `Block[]` tree with one or more `template_content_slot` blocks marking where the page's own blocks go.
111
-
112
- ### Recipe
113
-
114
- #### 1. List existing templates
115
-
116
- ```
117
- list_page_templates
118
- ```
119
-
120
- Returns templates already defined on the site.
121
-
122
- #### 2. Create / pick a template
123
-
124
- Templates live as `Block[]` trees under `paths.pageTemplate(orgId, siteId, templateId)`. They're created via the portal UI for now (no dedicated MCP `create_page_template` tool yet — the chat-tool surface in `anthropic.ts` exposes list + set, not create).
125
-
126
- If the template you want doesn't exist, ask the user to create it via **/app/sites/{id}/templates** in the portal, or use the block-mode-aware AI chat in the portal which knows how to write template trees.
127
-
128
- #### 3. Assign it to a page
129
-
130
- ```
131
- set_page_template page_id="brf" template_id="category-landing"
132
- ```
133
-
134
- The renderer composes the template's blocks with the page's blocks at build time via `composePageWithTemplate` (replacing each `template_content_slot` with the page's own block tree). The page's CSS / theme / SEO settings are unchanged.
135
-
136
- #### 4. Editing the template propagates to every page
137
-
138
- The same template id can be set on multiple pages. Editing the template updates them all on next deploy.
139
-
140
- ### When B beats A
141
-
142
- - Block-mode editor surfaces template assignment as a dropdown in the page settings.
143
- - Templates compose with the block tree, so the page author can drop content blocks into named slots rather than writing HTML.
144
- - The renderer is aware of block CSS / JS bundling — assets per block type are aggregated automatically.
145
-
146
- ### When A still beats B even in block mode
147
-
148
- - Need a small reusable HTML chunk in the middle of a block-mode page → still use `<x-include>` (block-mode bodies can contain free HTML blocks that include partials).
149
- - One-page change wanted without affecting siblings → just edit the page's blocks; templates are all-or-nothing.
150
-
151
- ## Refactor: 7 already-existing pages into a shared partial
152
-
153
- If the 7 pages already exist as separate HTML bodies with duplicated chunks:
154
-
155
- 1. `read_page page_id=<one of them>` and identify the shared HTML literally — character-for-character chunks that repeat across all 7.
156
- 2. `create_partial partial_id=<descriptive-name> html_content="<the shared chunk>"`.
157
- 3. For each of the 7 pages, `update_page` with the shared chunk replaced by `<x-include name="<name>" />`.
158
- 4. Deploy. The rendered output should be byte-identical to before; only the source pages got shorter.
159
-
160
- Don't try to abstract first time and hand-write the partials. Refactor from real, working duplication.
161
-
162
- ## Pitfalls
163
-
164
- - **Don't put hero / page-specific content into a shared partial.** A partial is for things that are *truly identical* across uses. The moment you want it to vary by page, lift the differing parts back into the page body.
165
- - **Header and footer already use partials.** Don't recreate them inside a category-page shared partial — they're injected by the layout, not by the page body.
166
- - **CSS scoping.** Partials carrying `<style>` blocks merge into every page that includes them. If two partials define `.tile` differently, the last one wins. Either namespace classes per partial (`.why__tile`, `.feature__tile`) or move shared styles into the site's global CSS via `update_site_settings` → `custom_css`.
167
- - **Don't migrate to block-mode templates just because you can.** Phase 1 sites are HTML-mode by design; partials + includes give you 90% of the value with zero risk.
168
- - **`<x-include>` in a partial body referencing another partial.** Won't expand (see edge cases). If you find yourself wanting this, you're building a layout system inside the partial system — at that point the site probably wants block mode.
6
+ # Share a layout across Pages
7
+
8
+ Read capabilities, available block types, content types and Page templates first.
9
+ Use a site version for substantial design changes. The Page owns its body blocks;
10
+ the template owns the shared layout around that body. The content type selects a
11
+ default template, and an individual Page can override it.
12
+
13
+ 1. `create_page_template name="article-layout" label="Article" starter="article" status="published"`.
14
+ Alternatively supply `blocks` instead of `starter`. Presets include article,
15
+ blog, checklist, team, events, products and custom.
16
+ 2. `update_content_type name="articles" patch={template:"article-layout"}`.
17
+ For a single-page override, `update_page page_id=... patch={template:"article-layout"} save=true`.
18
+ 3. Read and edit the template through `read_page_template` or block tools with
19
+ `target: { kind: "template", id: "article-layout" }`. Template edits save
20
+ immediately; content-type and template changes are isolated by `version`.
21
+ 4. Keep a `template_content_slot` where the Page's body belongs. Its `max_width`
22
+ can be full, narrow, normal or wide. Use native columns/containers for layout.
23
+ 5. Metadata blocks include `template/page_title`, `template/page_date`,
24
+ `template/page_featured_image`, `template/page_breadcrumbs` and
25
+ `template/page_navigation`. The outline block reads the rendered body headings.
26
+ 6. Use `{{page.title}}` or `{{page.custom_field}}` in template bindings.
27
+ A repeater's children use `{{item.title}}` for the current listed Page.
28
+ Custom fields can be typed objects, arrays or Page references; preserve those
29
+ structures instead of storing hand-generated list HTML.
30
+ 7. Preview several Pages using the template: long title, short body, missing
31
+ optional image, populated reference list. Inspect desktop and mobile output.
32
+ Verify the authorized static build as well as the live database preview.
33
+
34
+ Prefer native image, table, list, heading and content blocks. A rare specialized
35
+ widget can use a reviewed HTML/embed block. Reusable custom behavior belongs in
36
+ one custom block type rather than repeated HTML bodies. Existing HTML-mode pages
37
+ may share fragments through partials and `<x-include>`, but use block templates
38
+ for new content families.
39
+
40
+ Header/footer partials are global and remain separate from content types. Keep
41
+ site-level design tokens in settings and shared layout changes in the template.
42
+ Page body edits stay specific to that Page. Read the template before mutation;
43
+ never replace it with a starter merely because an import is being retried.
44
+
45
+
46
+ Content types also own `sort_field`/`sort_dir` and optional `allowed_templates`.
47
+ Types define content; templates define presentation. Multiple compatible Page
48
+ templates can be allowed, with `template` selecting the default. Null/absent
49
+ `allowed_templates` is unrestricted, `[]` allows none, and a configured default
50
+ must be in an explicit allowed list. Page overrides must be allowed; null/empty
51
+ `Page.template` restores inheritance. Page template changes use Save/Discard.
52
+
53
+ Listings inherit type sorting unless explicitly overridden. `Page.sort_order`
54
+ is the manual numeric order; null clears it. Missing sort values come last and
55
+ IDs break ties. Explicit ID lists keep their order. Typed `list_pages` queries
56
+ inherit type sorting and accept `sort_by`/`sort_order`; unfiltered API lists
57
+ default to stable IDs. See the public Content types guide for editor steps.
@@ -6,7 +6,7 @@ description: Use when the user asks to redesign, modernize, or restructure a Typ
6
6
  # Redesign a site without breaking the live one
7
7
 
8
8
  > **The buffer model (draft writes).** Every content write in this recipe
9
- > (pages, blocks, partials, collection items) lands in an unsaved per-doc
9
+ > (pages, blocks and partials) lands in an unsaved per-doc
10
10
  > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
11
  > build work, pass `save: true` on write calls (the work is pre-approved by
12
12
  > the task itself), or run `commit_working_copy` per doc before any
@@ -59,7 +59,7 @@ read_partial partial_id="footer"
59
59
  list_pages limit=20
60
60
  batch_read_pages page_ids=[<top 3-5 pages>] # see actual conventions
61
61
  list_partials # what free blocks exist
62
- list_collections # any data we need to consider
62
+ list_content_types # any data we need to consider
63
63
  ```
64
64
 
65
65
  Write the user a short read-back: *"This is a 12-page agency site
@@ -25,7 +25,7 @@ Use `set_block_responsive` (or pass the object form directly in `add_block` /
25
25
 
26
26
  The breakpoint object belongs on the responsive field inside `block.data`,
27
27
  for example `data.cols`. Do not add a top-level `block.responsive` object: it
28
- is not a rendered field, and page, partial and collection-template writes
28
+ is not a rendered field, and page, partial and Page-template writes
29
29
  reject it instead of silently storing an inert value.
30
30
 
31
31
  ```
@@ -46,7 +46,7 @@ Pass a scalar to collapse a field back to one value everywhere.
46
46
  iPad, 1/row icon-on-top on a phone":
47
47
 
48
48
  1. `core/grid` containing `core/icon_box` cards (or a `core/repeater` with
49
- `item_block: core/icon_box` for a collection-driven list).
49
+ `item_block: core/icon_box` for a Page-driven list).
50
50
  2. On the grid: `cols = { mobile: 1, tablet: 2, desktop: 4 }`.
51
51
  3. On each icon_box (or the repeater's item defaults):
52
52
  `layout = { mobile: "icon-top", tablet: "icon-left", desktop: "icon-top" }`.
package/skills/tr-seo.md CHANGED
@@ -6,7 +6,7 @@ description: Use when the user asks to improve SEO, fix meta tags, add structure
6
6
  # SEO audit and improvements for a Typeroll site
7
7
 
8
8
  > **The buffer model (draft writes).** Every content write in this recipe
9
- > (pages, blocks, partials, collection items) lands in an unsaved per-doc
9
+ > (pages, blocks and partials) lands in an unsaved per-doc
10
10
  > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
11
  > build work, pass `save: true` on write calls (the work is pre-approved by
12
12
  > the task itself), or run `commit_working_copy` per doc before any
@@ -1,242 +0,0 @@
1
- // Collections + items (blog, team, events, products, custom).
2
- import { z } from 'zod';
3
- import { ok, withErrorBoundary, versionParam } from './helpers.js';
4
- function v(version) {
5
- return version ? { version } : undefined;
6
- }
7
- function existingCollectionArg(args) {
8
- const value = args.collection ?? args.name;
9
- if (!value)
10
- throw new Error('collection is required');
11
- return value;
12
- }
13
- export const collectionTools = [
14
- {
15
- name: 'create_collection',
16
- description: 'Create a new content collection with a field schema and (optionally) per-item URLs. Pass `route_template` (e.g. "/restaurants/{slug}") to give each published item its own URL — omit to default to "/{name}/{slug}", set to "" to disable per-item URLs entirely. Pass template_kind for a polished native item template, or item_template_blocks for a custom block tree.',
17
- inputSchema: {
18
- name: z.string().regex(/^[a-z][a-z0-9_-]{0,62}$/),
19
- label_singular: z.string().min(1),
20
- label_plural: z.string().min(1),
21
- icon: z.string().optional(),
22
- fields: z
23
- .array(z.object({
24
- name: z.string(),
25
- label: z.string(),
26
- type: z.string(),
27
- required: z.boolean().optional(),
28
- default: z.unknown().optional(),
29
- options: z.array(z.string()).optional(),
30
- }).passthrough())
31
- .min(1),
32
- slug_field: z.string().optional(),
33
- sort_field: z.string().optional(),
34
- sort_dir: z.enum(['asc', 'desc']).optional(),
35
- route_template: z.string().optional(),
36
- item_template_html: z.string().optional(),
37
- template_kind: z
38
- .enum(['blog', 'article', 'checklist', 'team', 'events', 'products', 'custom'])
39
- .optional()
40
- .describe('Native starter for item pages. article includes breadcrumbs + body outline; checklist includes optional PDF CTA + explicit previous/next fields.'),
41
- item_template_blocks: z.array(z.any()).optional().describe('Explicit item-template block tree. Overrides template_kind.'),
42
- version: versionParam,
43
- },
44
- handler: withErrorBoundary(async (args, { client, siteId }) => {
45
- const { version, ...body } = args;
46
- const res = await client.post(siteId, 'collections', body, v(version));
47
- return ok(res);
48
- }),
49
- },
50
- {
51
- name: 'update_collection_schema',
52
- description: 'PATCH a collection\'s schema or routing. `collection` is required (`name` remains a deprecated compatibility alias). Pass only the fields you want to change. Renaming a field would orphan existing item data — adding new fields is safe, removing fields is silently allowed but item docs keep the dropped data.',
53
- inputSchema: {
54
- collection: z.string().optional().describe('Collection machine name. Preferred argument name.'),
55
- name: z.string().optional().describe('Deprecated alias for collection.'),
56
- patch: z
57
- .object({
58
- label_singular: z.string().optional(),
59
- label_plural: z.string().optional(),
60
- icon: z.string().optional(),
61
- fields: z.array(z.record(z.unknown())).optional(),
62
- slug_field: z.string().optional(),
63
- sort_field: z.string().optional(),
64
- sort_dir: z.enum(['asc', 'desc']).optional(),
65
- route_template: z.string().optional(),
66
- item_template_html: z.string().optional(),
67
- schema_type: z
68
- .string()
69
- .optional()
70
- .describe('Free-form Schema.org type for auto JSON-LD on every item route ("BlogPosting", "PodcastEpisode", "Product", "Event", "Recipe", "Course", …). Pair with schema_field_map when item field names differ from the schema property names.'),
71
- schema_field_map: z
72
- .record(z.string())
73
- .optional()
74
- .describe('Override the default field→schema-property mapping. Example: { "audio_url": "contentUrl", "show_notes": "description" }. Values use camelCase (Schema.org convention).'),
75
- })
76
- .passthrough(),
77
- version: versionParam,
78
- },
79
- handler: withErrorBoundary(async (args, { client, siteId }) => {
80
- const collection = existingCollectionArg(args);
81
- const res = await client.patch(siteId, `collections/${encodeURIComponent(collection)}`, { patch: args.patch }, v(args.version));
82
- return ok(res);
83
- }),
84
- },
85
- {
86
- name: 'delete_collection',
87
- description: 'Delete a collection AND every item in it. `collection` is required (`name` remains a deprecated compatibility alias). Destructive; requires `confirm: true`. Get-the-user-to-confirm workflow recommended.',
88
- inputSchema: {
89
- collection: z.string().optional().describe('Collection machine name. Preferred argument name.'),
90
- name: z.string().optional().describe('Deprecated alias for collection.'),
91
- confirm: z.literal(true).describe('Must be true to actually delete.'),
92
- version: versionParam,
93
- },
94
- handler: withErrorBoundary(async (args, { client, siteId }) => {
95
- const collection = existingCollectionArg(args);
96
- const res = await client.del(siteId, `collections/${encodeURIComponent(collection)}`, { confirm: 'true', ...v(args.version) });
97
- return ok(res);
98
- }),
99
- },
100
- {
101
- name: 'list_collections',
102
- description: 'List content collections on the site (name, label, icon, field schemas).',
103
- inputSchema: { version: versionParam },
104
- handler: withErrorBoundary(async (args, { client, siteId }) => {
105
- const res = await client.get(siteId, 'collections', v(args.version));
106
- return ok(res);
107
- }),
108
- },
109
- {
110
- name: 'read_collection',
111
- description: 'Fetch one collection\'s schema (label, fields, icon). `collection` is required; `name` remains a deprecated compatibility alias.',
112
- inputSchema: {
113
- collection: z.string().optional().describe('Collection machine name. Preferred argument name.'),
114
- name: z.string().optional().describe('Deprecated alias for collection.'),
115
- version: versionParam,
116
- },
117
- handler: withErrorBoundary(async (args, { client, siteId }) => {
118
- const collection = existingCollectionArg(args);
119
- const res = await client.get(siteId, `collections/${encodeURIComponent(collection)}`, v(args.version));
120
- return ok(res);
121
- }),
122
- },
123
- {
124
- name: 'collection_completeness',
125
- description: "Which records in a collection need work, worst first. Returns per-field gap counts across the whole collection plus the N worst items (missing fields, never-verified fields, and fields whose last write is older than the staleness window). Use this INSTEAD of paging every item and diffing client-side — it's the intended entry point for an enrichment pass. Computed at read time, so it's never stale. By default only fields an API key may actually write are counted, since a gap you're forbidden to close is noise; pass all_fields: true for a human audit.",
126
- inputSchema: {
127
- collection: z.string(),
128
- limit: z.number().int().min(1).max(500).optional().describe('Worst-N items to return. Default 50.'),
129
- stale_after_days: z.number().int().min(1).optional().describe('A field untouched for longer than this is reported stale. Default 180.'),
130
- all_fields: z.boolean().optional().describe('Include fields no API key may write. Default false.'),
131
- version: versionParam,
132
- },
133
- handler: withErrorBoundary(async (args, { client, siteId }) => {
134
- const { collection, version, ...query } = args;
135
- const res = await client.get(siteId, `collections/${encodeURIComponent(collection)}/completeness`, { ...query, ...v(version) });
136
- return ok(res);
137
- }),
138
- },
139
- {
140
- name: 'list_collection_items',
141
- description: 'List items in a collection with status filter + cursor pagination (cap 200 per page). Defaults to summary mode (richtext/textarea fields replaced with a byte-count hint) so a 200-item directory listing stays compact; pass include_richtext: true if you actually need full bodies.',
142
- inputSchema: {
143
- collection: z.string(),
144
- status: z.enum(['draft', 'published', 'all']).optional(),
145
- limit: z.number().int().min(1).max(200).optional(),
146
- cursor: z.string().optional(),
147
- include_richtext: z.boolean().optional().describe('Include full richtext/textarea field values inline. Default false.'),
148
- version: versionParam,
149
- },
150
- handler: withErrorBoundary(async (args, { client, siteId }) => {
151
- const { collection, version, include_richtext, ...query } = args;
152
- const res = await client.get(siteId, `collections/${encodeURIComponent(collection)}/items`, { ...query, ...(include_richtext ? {} : { summary: 'true' }), ...v(version) });
153
- return ok(res);
154
- }),
155
- },
156
- {
157
- name: 'batch_read_collection_items',
158
- description: 'Read up to 200 collection items in one call. Returns per-id found/not-found.',
159
- inputSchema: {
160
- collection: z.string(),
161
- item_ids: z.array(z.string()).min(1).max(200),
162
- version: versionParam,
163
- },
164
- handler: withErrorBoundary(async (args, { client, siteId }) => {
165
- const res = await client.post(siteId, `collections/${encodeURIComponent(args.collection)}/items/batch-read`, { item_ids: args.item_ids }, v(args.version));
166
- return ok(res);
167
- }),
168
- },
169
- {
170
- name: 'read_collection_item',
171
- description: 'Fetch a single item with all fields. item_id may be the internal id or the value of the collection\'s slug_field.',
172
- inputSchema: { collection: z.string(), item_id: z.string(), version: versionParam },
173
- handler: withErrorBoundary(async (args, { client, siteId }) => {
174
- const res = await client.get(siteId, `collections/${encodeURIComponent(args.collection)}/items/${encodeURIComponent(args.item_id)}`, v(args.version));
175
- return ok(res);
176
- }),
177
- },
178
- {
179
- name: 'create_collection_item',
180
- description: 'Create an item. Fields outside the collection schema are silently dropped — call list_collections first if you\'re unsure what fields exist.',
181
- inputSchema: {
182
- collection: z.string(),
183
- fields: z.record(z.unknown()),
184
- status: z.enum(['draft', 'published']).optional(),
185
- version: versionParam,
186
- },
187
- handler: withErrorBoundary(async (args, { client, siteId }) => {
188
- const { collection, version, ...body } = args;
189
- const res = await client.post(siteId, `collections/${encodeURIComponent(collection)}/items`, body, v(version));
190
- return ok(res);
191
- }),
192
- },
193
- {
194
- name: 'update_collection_item',
195
- description: 'Update a collection item by internal id or slug_field value. Fields outside the schema are dropped. BUFFER MODEL: field values land in the item\'s unsaved DRAFT (status applies immediately); pass save:true to commit in the same call, or commit_working_copy later. Scheduled publishing (0.29.0+): pass publish_at/unpublish_at INSIDE `fields` — ISO datetime at which the platform flips status (+ deploys the site); null clears; applies immediately like status.',
196
- inputSchema: {
197
- collection: z.string(),
198
- item_id: z.string(),
199
- fields: z.record(z.unknown()).optional(),
200
- status: z.enum(['draft', 'published']).optional(),
201
- save: z.boolean().optional().describe('Also SAVE (commit) the draft in the same call.'),
202
- version: versionParam,
203
- },
204
- handler: withErrorBoundary(async (args, { client, siteId }) => {
205
- const { collection, item_id, version, ...body } = args;
206
- const res = await client.patch(siteId, `collections/${encodeURIComponent(collection)}/items/${encodeURIComponent(item_id)}`, body, v(version));
207
- return ok(res);
208
- }),
209
- },
210
- {
211
- name: 'regenerate_collection_listing',
212
- description: 'Refresh a hand-managed listing of collection items on a page. The agent inserts a marker pair `<!-- typeroll:listing:{collection} -->` ... `<!-- /typeroll:listing:{collection} -->` into a page body once; from then on this tool fills the section between the markers with the current published items, sorted by the collection\'s sort_field. Pass `item_template` to customize per-item HTML — {{field}} substitutes HTML-escaped, {{{field}}} raw, {{url}} resolves through the collection\'s route_template. Defaults to a basic <ul> of links.',
213
- inputSchema: {
214
- collection: z.string(),
215
- page_id: z.string(),
216
- item_template: z.string().optional()
217
- .describe('Per-item HTML template. Default: <li><a href="{{url}}">{{title}}</a></li>'),
218
- wrap_open: z.string().optional().describe('Override the opening wrap element. Default: <ul class="tr-listing">'),
219
- wrap_close: z.string().optional().describe('Override the closing wrap element. Default: </ul>'),
220
- empty_html: z.string().optional().describe('Rendered when no items exist.'),
221
- sort_field: z.string().optional().describe('Override the collection\'s default sort_field.'),
222
- sort_dir: z.enum(['asc', 'desc']).optional(),
223
- status_filter: z.enum(['published', 'all']).optional().describe('Default: published.'),
224
- limit: z.number().int().min(1).max(500).optional(),
225
- version: versionParam,
226
- },
227
- handler: withErrorBoundary(async (args, { client, siteId }) => {
228
- const { collection, version, ...body } = args;
229
- const res = await client.post(siteId, `collections/${encodeURIComponent(collection)}/regenerate-listing`, body, v(version));
230
- return ok(res);
231
- }),
232
- },
233
- {
234
- name: 'delete_collection_item',
235
- description: 'Delete a collection item.',
236
- inputSchema: { collection: z.string(), item_id: z.string(), version: versionParam },
237
- handler: withErrorBoundary(async (args, { client, siteId }) => {
238
- const res = await client.del(siteId, `collections/${encodeURIComponent(args.collection)}/items/${encodeURIComponent(args.item_id)}`, v(args.version));
239
- return ok(res);
240
- }),
241
- },
242
- ];