@typeroll/mcp-server 0.44.73 → 0.45.0

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.
@@ -6,7 +6,7 @@ description: Use when the user asks to write, draft, or rewrite a page on a Type
6
6
  # Write a page that fits the 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,275 +1,50 @@
1
1
  ---
2
2
  name: tr-directory
3
- description: Use when the user wants to build a directory site or import a structured dataset (restaurants, products, events, agencies, etc.) where each item should have its own URL. Covers collection schema creation, per-item URLs via route_template, listing page, deploy.
3
+ description: Import a structured directory as Pages of a content type, with custom fields, references, listing routes and reusable templates.
4
4
  ---
5
5
 
6
- # Build a directory site from external data
7
-
8
- > **The buffer model (draft writes).** Every content write in this recipe
9
- > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
- > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
- > build work, pass `save: true` on write calls (the work is pre-approved by
12
- > the task itself), or run `commit_working_copy` per doc before any
13
- > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
-
15
-
16
- Typeroll collections support per-item URLs: every published item
17
- in a collection with a `route_template` materialises as its own static
18
- page at build time. This is the right pattern when you have hundreds
19
- of similar entities (restaurants, products, listings, profiles).
20
-
21
- ## Big-picture flow
22
-
23
- 1. **Data source** → 2. **Collection schema** → 3. **Items** → 4. **Listing page**
24
- → 5. **Preview** → 6. **Deploy**
25
-
26
- You drive everything from Claude Code locally — the scrape, the data
27
- shaping, the writes. The MCP just receives the final shape.
28
-
29
- ## Recipe
30
-
31
- ### 1. Get the data
32
-
33
- Whatever source the user has — scraped CSV, public API, vendor feed,
34
- manual research, another LLM's output. Normalise to a flat shape:
35
- one object per item with stable, kebab-case field names.
36
-
37
- ```jsonc
38
- [
39
- {
40
- "title": "Joe's Pizza",
41
- "slug": "joes-pizza",
42
- "address": "123 Main St, Anytown",
43
- "phone": "+1-555-0100",
44
- "cuisine": "italian",
45
- "rating": 4.5,
46
- "image": "https://...", // optional: a hosted image URL
47
- "excerpt": "Family-run since 1987...",
48
- "body": "<p>Long-form description with HTML.</p>"
49
- }
50
- ]
51
- ```
52
-
53
- The `slug` field is what populates `route_template`. Make it
54
- kebab-case, unique within the dataset. If the source doesn't have one,
55
- derive from `title`: lowercase, replace non-alphanumeric with `-`,
56
- collapse consecutive dashes.
57
-
58
- ### 2. Decide the URL structure with the user
59
-
60
- Common patterns:
61
-
62
- - `/restaurants/{slug}` (default — simple, predictable)
63
- - `/r/{slug}` (compact)
64
- - `/{cuisine}/{slug}` (categorised)
65
- - `/dir/{slug}` (short prefix to avoid collisions with page slugs)
66
-
67
- Pick one before creating the collection — changing `route_template`
68
- later renames every URL and requires redirect rules.
69
-
70
- ### 3. Create the collection schema
71
-
72
- ```
73
- create_collection
74
- name="restaurants"
75
- label_singular="Restaurant"
76
- label_plural="Restaurants"
77
- icon="🍕"
78
- fields=[
79
- {"name":"title","label":"Name","type":"text","required":true},
80
- {"name":"slug","label":"Slug","type":"text","required":true},
81
- {"name":"address","label":"Address","type":"text"},
82
- {"name":"phone","label":"Phone","type":"text"},
83
- {"name":"cuisine","label":"Cuisine","type":"text"},
84
- {"name":"rating","label":"Rating","type":"number"},
85
- {"name":"image","label":"Image URL","type":"text"},
86
- {"name":"excerpt","label":"Excerpt","type":"textarea"},
87
- {"name":"body","label":"Body","type":"richtext"}
88
- ]
89
- slug_field="slug"
90
- sort_field="title"
91
- sort_dir="asc"
92
- route_template="/restaurants/{slug}"
93
- item_template_html="<article class=\"directory-item\">
94
- <header>
95
- <h1>{{title}}</h1>
96
- {{cuisine}} · ⭐ {{rating}}
97
- </header>
98
- <img src=\"{{image}}\" alt=\"{{title}}\" />
99
- <address>{{address}} · <a href=\"tel:{{phone}}\">{{phone}}</a></address>
100
- <section class=\"description\">{{{body}}}</section>
101
- </article>"
102
- ```
103
-
104
- The `item_template_html` is what renders for each item. `{{field}}`
105
- HTML-escapes; `{{{field}}}` leaves raw (use for richtext bodies that
106
- intentionally carry HTML).
107
-
108
- ### 4. Bulk-import items
109
-
110
- Loop over your data array. For each item:
111
-
112
- ```
113
- create_collection_item
114
- collection="restaurants"
115
- fields={ title:"Joe's Pizza", slug:"joes-pizza", ... }
116
- status="published"
117
- ```
118
-
119
- For larger datasets (1000+), batch outside the MCP — spawn 5 parallel
120
- `create_collection_item` calls at a time, watch the 60-writes/min rate
121
- limit (you'll hit it on big imports, the API returns 429 with
122
- `Retry-After`).
123
-
124
- Set `status: "draft"` for items the user still needs to review; only
125
- published items get static pages.
126
-
127
- ### 5. Build a listing page
128
-
129
- Items have per-item URLs but not a default index. Create one with a
130
- marker pair that `regenerate_collection_listing` will keep up to date:
131
-
132
- ```
133
- create_page
134
- title="Restaurants"
135
- slug="restaurants"
136
- status="published"
137
- html_content="<h1>All restaurants</h1>
138
- <!-- typeroll:listing:restaurants -->
139
- <!-- /typeroll:listing:restaurants -->
140
- "
141
- ```
142
-
143
- Then populate (and refresh whenever items change) with one call:
144
-
145
- ```
146
- regenerate_collection_listing
147
- collection="restaurants"
148
- page_id="restaurants"
149
- item_template="<article class=\"directory-card\">
150
- <h2><a href=\"{{url}}\">{{title}}</a></h2>
151
- <p>{{cuisine}} · ⭐ {{rating}}</p>
152
- <p>{{address}}</p>
153
- </article>"
154
- wrap_open="<div class=\"directory-grid\">"
155
- wrap_close="</div>"
156
- ```
157
-
158
- `{{field}}` substitutes HTML-escaped, `{{{field}}}` raw (for richtext
159
- fields), `{{url}}` resolves through the collection's `route_template`.
160
- The tool replaces only what's between the marker pair — anything before
161
- or after the markers stays put.
162
-
163
- When the customer adds a new restaurant later, the agent re-runs the
164
- same `regenerate_collection_listing` call and the index updates. No
165
- diff-the-HTML-by-hand, no stale listings.
166
-
167
- (When the block editor lands, you'll be able to drop in a "collection
168
- listing" block instead of hand-writing this. For now, raw HTML.)
169
-
170
- ### 6. Preview an item
171
-
172
- ```
173
- get_preview_link collection_name="restaurants" item_id="<id>"
174
- ```
175
-
176
- Returns a URL the user can open. Internal links inside the preview
177
- stay inside the preview surface, so navigating to another item works.
178
-
179
- ### 7. Deploy
180
-
181
- ```
182
- trigger_deploy
183
- get_deploy_status job_id=<id>
184
- ```
185
-
186
- Each published item gets its own URL in the static build, with
187
- `sitemap.xml` automatically including them all.
188
-
189
- ## Patterns worth knowing
190
-
191
- ### Conditional rendering without Mustache conditionals
192
-
193
- `item_template_html` substitution is plain `{{field}}` / `{{{field}}}` — no loops, no `{{#if}}` blocks. To hide a section/element when a field is empty, use a data-attribute that resolves to either the empty string or a non-empty value, plus a CSS selector:
194
-
195
- ```html
196
- <aside class="podcast-guest" data-empty-if-blank="{{guest_name}}">
197
- <h2>Om gästen</h2>
198
- <p>{{guest_bio}}</p>
199
- </aside>
200
- ```
201
- ```css
202
- .podcast-detail [data-empty-if-blank=""] { display: none !important; }
203
- ```
204
-
205
- When `guest_name` is empty, the attribute becomes `data-empty-if-blank=""` and the CSS matches and hides the block. When non-empty, the rule misses and the block renders. Works for optional images, optional audio, optional sub-sections — any "show only if this field has a value" need.
206
-
207
- ### Pre-render list-typed source data into a richtext field
208
-
209
- For nested arrays in the source (e.g. `chapters: [{time, title}, …]`), pre-render to HTML during import and store in a dedicated `*_html` richtext field. The template just splats `{{{chapters_html}}}`. Three concrete recipes worth applying:
210
-
211
- 1. **Podcast detail page:** `chapters_html`, `guest_links_html`.
212
- 2. **Restaurant directory:** `hours_html` table.
213
- 3. **Product directory:** `variants_html` grid.
214
-
215
- See `tr-collection-template` for full code examples.
216
-
217
- ### Batch-import via subagent for large datasets
218
-
219
- 22+ `create_collection_item` calls bloat the main agent's context with response payloads. Spawn a `general-purpose` subagent with the manifest path and a tight contract ("report ok/fail per item, end with a one-line summary"). The sub returns one line per item instead of a JSON blob per item back into the main turn.
220
-
221
- ## Pitfalls
222
-
223
- - **Slugs must be unique within the collection** — duplicates cause
224
- build failures (two pages claiming the same URL). De-dupe before
225
- importing.
226
- - **Don't reuse `slug` across collections without thinking.**
227
- `/restaurants/joes` and `/products/joes` are fine; just avoid
228
- `/joes` for both (collection items vs. pages don't collide because
229
- pages always win, but two collections sharing a `slug_field=slug`
230
- with the same `route_template` is a foot-gun).
231
- - **Required fields.** `route_template="/restaurants/{slug}"` will
232
- silently skip items where `slug` is missing. Check
233
- `list_collection_items` after import — if you imported 500 and the
234
- listing only shows 480, look at the dropped 20's source data.
235
- - **Template too clever.** Substitution is plain `{{field}}` — no
236
- loops, no conditionals. If your design needs more, prefer flat
237
- fields (`star_html`, `rating_label`) prebuilt in the data step.
238
- See the "Patterns worth knowing" section above.
239
- - **Field type changes drift data.** Adding a new field after import
240
- is fine; renaming one orphans the old data on every item. Plan the
241
- schema before import.
242
- - **Never derive display labels from slugs.** Slugs are ASCII-folded
243
- for URL-safety. Computing the visible label as
244
- `slug.replace("-", " ").capitalize()` produces **wrong words** in
245
- languages with diacritics: `innehall` → `Innehall` (should be
246
- `Innehåll`), `affarssystem` → `Affärssystem`. Always carry the
247
- real title from the source and look it up by slug:
248
-
249
- ```python
250
- slug_to_title = {t["slug"]: t["title"] for t in topic_manifest}
251
- label = slug_to_title.get(slug, slug.replace("-", " ").title()) # fallback only
252
- ```
253
-
254
- Caught during a real migration where 20 of 22 podcast detail pages
255
- ended up with `Innehall` / `Prissattning` / `Affarssystem` in their
256
- topic chips.
257
- - **Numeric `sort_field` sorts numerically** as of the 2026-05 fix —
258
- episode 9 ranks below episode 23 under desc sort. Earlier versions
259
- compared as strings (9 > 23), so if you're working against an older
260
- portal deploy add a `sort_key` text field with zero-padded values
261
- (`f"{episode:03d}"`) and set `sort_field: "sort_key"` instead.
262
-
263
- ## Mixing scraped + generated content
264
-
265
- The whole point of the local-agent model: you can blend sources.
266
-
267
- - Scrape addresses + phone from a yellow-pages site.
268
- - Generate excerpt + body from a local Claude pass over the raw
269
- scraped HTML.
270
- - Generate hero images per item via `tr-images`.
271
- - All three merged into one `create_collection_item` per record.
272
-
273
- Keep a local manifest (`./directory-state.json`) of what's been
274
- imported so a partial run is resumable. The MCP doesn't track that
275
- state — your local script does.
6
+ # Build a directory from structured data
7
+
8
+ All entries are Pages. The content type declares their custom field schema,
9
+ URL pattern and default Page template. A listing is another Page whose repeater
10
+ queries the same records. Do not create a parallel item model.
11
+
12
+ 1. Discover capabilities, existing Pages, content types, templates and blocks.
13
+ Use a site version for a multi-step import and pass it consistently.
14
+ 2. Call `get_migration_readiness` before importing existing sites or media.
15
+ The customer's own storage must be connected and verified first.
16
+ 3. Normalize source data without inventing facts. Keep stable source identities
17
+ in a custom field, and record Page IDs returned by creation for subsequent
18
+ updates. Decide the final URL pattern before creating records.
19
+ 4. Create a Page template with `create_page_template`, choosing a suitable
20
+ starter or a native block tree with `template_content_slot`.
21
+ 5. Create a content type with `create_content_type`. A restaurant type might
22
+ use `/restaurants/{slug}` and fields `address`, `phone`, `cuisine`, `rating`,
23
+ `image`, `excerpt`. Title, slug, status and block body are already Page fields.
24
+ 6. Create one Page per entry with `content_type: "restaurants"`. Common metadata
25
+ stays at the top level; custom values belong in `fields`. Use native blocks
26
+ for the body, and the connected media storage for images.
27
+ 7. Create a listing Page with `core/page_list` and `content_type: "restaurants"`.
28
+ Configure filtering, sorting and pagination through its block schema.
29
+ Reference fields use `page_ref`/`page_ref_list` and optional
30
+ `ref_content_type`. Related and backlink repeaters query those Page IDs.
31
+ 8. Check at least one full record, a sparse record, image handling, empty states,
32
+ pagination and all migrated URL variants in preview and the static build.
33
+ Save working copies before deploying the authorized version.
34
+
35
+ ## Ownership and public data
36
+
37
+ Use `writable_by` on custom fields when source imports, agents, listed businesses
38
+ or portal editors have different responsibilities. `page_field_rules` covers
39
+ supported common metadata such as title. Respect HTTP 409 ownership conflicts;
40
+ retrying the same rejected write does not fix them. `rendered: false` excludes a
41
+ custom field from public rendering. Never place secrets in public block data.
42
+
43
+ The directory module is configured with `content_type`, not a separate storage
44
+ namespace. Self-service edit grants address the same Page IDs and enforce each
45
+ field's owner permissions.
46
+
47
+ For updates, `list_pages content_type="restaurants"` finds records and
48
+ `update_page page_id=... patch={fields:{...}} save=true` changes approved values.
49
+ Read each record before replacing data. For reclassification use
50
+ `change_page_content_type`; it records a revision and preserves the Page URL.
@@ -6,7 +6,7 @@ description: Use when the user wants to import or migrate content from a non-Wor
6
6
  # Import content from a non-WordPress 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