@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.
- package/AGENTS.md +63 -74
- package/README.md +32 -18
- package/dist/bundled-content.js +15 -16
- package/dist/init.js +6 -0
- package/dist/server.js +15 -7
- package/dist/tools/bulk.js +4 -5
- package/dist/tools/content-types.js +28 -0
- package/dist/tools/deploy.js +1 -1
- package/dist/tools/migration.js +3 -4
- package/dist/tools/page-blocks.js +2 -2
- package/dist/tools/page-templates.js +18 -0
- package/dist/tools/pages.js +16 -3
- package/dist/tools/preview.js +1 -3
- package/dist/tools/sites.js +1 -1
- package/dist/tools/working-copy.js +2 -8
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/README.md +6 -7
- package/skills/tr-blog.md +46 -130
- package/skills/tr-brand.md +2 -2
- package/skills/tr-content-write.md +1 -1
- package/skills/tr-directory.md +46 -271
- package/skills/tr-import-url.md +1 -1
- package/skills/tr-migrate-astro.md +65 -293
- package/skills/tr-migrate-multisite.md +1 -1
- package/skills/tr-migrate-wp.md +2 -2
- package/skills/tr-new-site.md +9 -9
- package/skills/tr-page-template.md +53 -164
- package/skills/tr-redesign-branch.md +2 -2
- package/skills/tr-responsive.md +2 -2
- package/skills/tr-seo.md +1 -1
- package/dist/tools/collections.js +0 -242
- package/skills/tr-collection-template.md +0 -305
|
@@ -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
|
|
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
|
package/skills/tr-directory.md
CHANGED
|
@@ -1,275 +1,50 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tr-directory
|
|
3
|
-
description:
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
in a
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.
|
package/skills/tr-import-url.md
CHANGED
|
@@ -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
|
|
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
|