@typeroll/mcp-server 0.29.0 → 0.32.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 +162 -1
- package/README.md +9 -4
- package/dist/bundled-content.js +4 -3
- package/dist/server.js +2 -0
- package/dist/tools/block-types.js +4 -4
- package/dist/tools/collections.js +16 -0
- package/dist/tools/migration.js +112 -0
- package/dist/tools/pages.js +10 -0
- package/dist/tools/redirects.js +9 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/README.md +1 -0
- package/skills/tr-migrate-multisite.md +348 -0
- package/skills/tr-migrate-wp.md +71 -6
package/AGENTS.md
CHANGED
|
@@ -197,6 +197,42 @@ maps to one HTTP endpoint; the actual logic runs in the customer's portal
|
|
|
197
197
|
one `html` field rendered verbatim (then sanitized like HTML-mode
|
|
198
198
|
content). Use it for the genuinely unique thing no block covers.
|
|
199
199
|
Prefer real blocks when one fits.
|
|
200
|
+
- **Structured records at scale** (template_capabilities_version ≥ 0.31.0).
|
|
201
|
+
Four things landed together for directory-shaped sites:
|
|
202
|
+
- `collection_completeness` — **start an enrichment pass here**, not by
|
|
203
|
+
paging every item. Returns per-field gap counts plus the N worst
|
|
204
|
+
records (missing fields, never-verified fields, fields whose last write
|
|
205
|
+
is older than the staleness window), computed at read time. Fields no
|
|
206
|
+
API key may write are excluded by default: a gap you can't close is
|
|
207
|
+
noise.
|
|
208
|
+
- **Per-field write authority.** A collection field can declare
|
|
209
|
+
`writable_by` (`portal | owner | agent | app | import`). A write you're
|
|
210
|
+
not permitted, or one that would overwrite a higher-precedence writer
|
|
211
|
+
(a human correction, the listed business's own edit), comes back as
|
|
212
|
+
**409 with the losing field names** — never a silent no-op. Treat that
|
|
213
|
+
as "already handled" and record it; retrying will lose again.
|
|
214
|
+
- **Item references.** `item_ref` / `item_ref_list` fields point at items
|
|
215
|
+
in another collection (`ref_collection`). The reverse direction is
|
|
216
|
+
computed at render time — don't try to maintain backlinks yourself.
|
|
217
|
+
Render them with a `core/repeater` whose `source_type` is `related`
|
|
218
|
+
(a ref field on the current item) or `backlinks` (who points at it).
|
|
219
|
+
- **Taxonomy pages.** `CollectionDef.facets` generates one page per
|
|
220
|
+
distinct field value. ⚠️ This turns record count into ROUTE count, and
|
|
221
|
+
route count is what the build timeout measures. `min_items` (default 2)
|
|
222
|
+
keeps thin-content pages out, and combination pages must be listed
|
|
223
|
+
explicitly in `facet_combinations` — never assume a cartesian product.
|
|
224
|
+
- **`core/embed`** (template_capabilities_version ≥ 0.30.0) is
|
|
225
|
+
`core/html` plus behaviour: an `html` field and a `js` field. Reach
|
|
226
|
+
for it when a one-off placement needs JavaScript. **A `<script>` tag
|
|
227
|
+
written into `core/html` — or into any page/block markup — is stripped
|
|
228
|
+
by the sanitizer no matter which credential wrote it**, so this field
|
|
229
|
+
is the supported route, not a workaround. The code runs in an IIFE
|
|
230
|
+
with `el` bound to the block's root element and ships in the page's
|
|
231
|
+
block bundle, outside the sanitized body. Through an API key it's
|
|
232
|
+
accepted under your key's authority (audit-logged, notice in the
|
|
233
|
+
response). Scope guide: one placement → `core/embed`; a reusable
|
|
234
|
+
widget → `create_block_type` with `script`; a site-wide tag →
|
|
235
|
+
`settings.scripts_head` / `scripts_body_end`.
|
|
200
236
|
- **Forms 2.0** (template_capabilities_version ≥ 0.18.0): forms can
|
|
201
237
|
carry `steps[]` — each step is a Block[] tree mixing `form/*` field
|
|
202
238
|
blocks (text/email/phone/number, textarea, select/radio_group/
|
|
@@ -239,6 +275,20 @@ maps to one HTTP endpoint; the actual logic runs in the customer's portal
|
|
|
239
275
|
- **Deploys.** Customers see live changes only after a deploy. Preview
|
|
240
276
|
always sees drafts. `trigger_deploy` enqueues; `get_deploy_status`
|
|
241
277
|
reports `queued → running → succeeded | failed`.
|
|
278
|
+
`trigger_deploy dry_run=true` builds without publishing — use it to prove
|
|
279
|
+
a structural change compiles (new collection, schema edit, template
|
|
280
|
+
rewrite) without touching the live site.
|
|
281
|
+
A finished job carries `cost`: total, cpu/memory/request split,
|
|
282
|
+
`duration_s`, per-phase timings, and output size. Estimates from a rate
|
|
283
|
+
card, not billing records, and gross of free tier — quote them as "roughly"
|
|
284
|
+
if a customer asks, and reach for `cost.phases` when the question is *why*
|
|
285
|
+
a build got slow.
|
|
286
|
+
|
|
287
|
+
- **Is the site live?** There is no site-level status field —
|
|
288
|
+
`Site.status` was removed in 0.30.0 because it was set once at creation and
|
|
289
|
+
never advanced, so it reported live sites as "planning". Read
|
|
290
|
+
`get_site → urls.production` instead: non-null means the domain is verified
|
|
291
|
+
and serving. For "has anything shipped", use `list_deploys`.
|
|
242
292
|
|
|
243
293
|
- **Site URLs.** `get_site` returns a `urls` object with:
|
|
244
294
|
- `production` — the customer's real domain (or null)
|
|
@@ -606,6 +656,116 @@ Redirect hygiene is automatic in both directions (since 0.16.1):
|
|
|
606
656
|
are removed (reported as `removed_redirects`). Manually created redirects
|
|
607
657
|
are kept — delete them yourself via `delete_redirect` if they're obsolete.
|
|
608
658
|
|
|
659
|
+
### "Before you start an import"
|
|
660
|
+
|
|
661
|
+
```
|
|
662
|
+
get_migration_readiness
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
Call this before moving any content. Every check it runs fails SILENTLY
|
|
666
|
+
otherwise — the import succeeds, previews render, the customer signs off, and
|
|
667
|
+
something is quietly wrong:
|
|
668
|
+
|
|
669
|
+
- **media storage** (blocker) — without it every `<img>` keeps its original
|
|
670
|
+
URL, so the new site is still served images by the old host. Nothing looks
|
|
671
|
+
broken until that hosting is cancelled, at which point every image on every
|
|
672
|
+
page breaks at once.
|
|
673
|
+
- **hosting adapter** (blocker) — without credentials, deploys return a job id
|
|
674
|
+
and publish nothing, while reporting success.
|
|
675
|
+
- verification origin, AI reconstruction, form notification email, and whether
|
|
676
|
+
the target actually has a design to rebuild INTO (warnings).
|
|
677
|
+
|
|
678
|
+
`ready: false` means STOP and report the blockers, each of which carries a
|
|
679
|
+
`fix`. Don't start "and fix it after": the content work would have to be
|
|
680
|
+
redone. The in-portal migration workflow enforces the same gate as its first
|
|
681
|
+
step (`skip_preflight: true` overrides it, and logs that it did).
|
|
682
|
+
|
|
683
|
+
### "Don't lose URLs in a migration"
|
|
684
|
+
|
|
685
|
+
Two different questions, and you need both answers:
|
|
686
|
+
|
|
687
|
+
```
|
|
688
|
+
list_migration_urls status="unhandled" # what the DATA says is uncovered
|
|
689
|
+
verify_migration_urls # what the SERVER actually answers
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
`list_migration_urls` classifies every inventory URL against the site's
|
|
693
|
+
current pages + redirects. It's recomputed on read, so creating a redirect
|
|
694
|
+
flips the entry on your next call — no bookkeeping of your own.
|
|
695
|
+
|
|
696
|
+
`verify_migration_urls` requests each URL against the deployed site (its
|
|
697
|
+
fallback subdomain by default, because the real domain still points at the
|
|
698
|
+
old host pre-cutover) and reports `ok` / `ok_redirect` / `missing` /
|
|
699
|
+
`broken_redirect` / `error`. This is the one that catches a redirect
|
|
700
|
+
pointing at an unpublished page, a typo'd `path`, and redirect loops — all
|
|
701
|
+
of which read as "handled" in the coverage report and as a 404 to Googlebot.
|
|
702
|
+
**Deploy first**: it tests saved, deployed content, not your drafts.
|
|
703
|
+
|
|
704
|
+
Every unhandled URL gets exactly one of three outcomes — there is no fourth:
|
|
705
|
+
|
|
706
|
+
- it moved → `create_redirect`
|
|
707
|
+
- it's gone on purpose → `update_migration_url url_id=… excluded=true` (with
|
|
708
|
+
a note saying who signed off)
|
|
709
|
+
- it should exist → migrate it
|
|
710
|
+
|
|
711
|
+
Populate the inventory yourself when the in-portal WordPress migration
|
|
712
|
+
didn't: `add_migration_urls` takes up to 2000 entries from a sitemap walk, a
|
|
713
|
+
GSC export (pass `gsc_clicks` so the report prioritises itself), or a crawl.
|
|
714
|
+
Pass `source_origin` whenever more than one old domain is in play — it
|
|
715
|
+
rejects foreign-origin URLs, which is what stops one market's `/kontakt`
|
|
716
|
+
from reading as another market's coverage.
|
|
717
|
+
|
|
718
|
+
For a whole family of sites, read the `tr-migrate-multisite` skill.
|
|
719
|
+
|
|
720
|
+
### "Retire a family of old URLs in one rule"
|
|
721
|
+
|
|
722
|
+
```
|
|
723
|
+
create_redirect from_path="/category/*" to_path="/blogg/:splat"
|
|
724
|
+
create_redirect from_path="/blog/:slug" to_path="/artiklar/:slug"
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
A trailing `*` captures everything under a prefix (including the prefix
|
|
728
|
+
itself) and `:splat` replays it; `:name` matches exactly one segment and is
|
|
729
|
+
replayed by name. This is the right tool after a WordPress migration, where
|
|
730
|
+
the dead URLs come in shapes — `/category/`, `/tag/`, `/author/`, `/2019/` —
|
|
731
|
+
and the inventory only knows the subset it happened to find.
|
|
732
|
+
|
|
733
|
+
Constraints, all enforced at write time rather than discovered in production:
|
|
734
|
+
|
|
735
|
+
- **Trailing `*` only.** Cloudflare silently drops a mid-path splat, so the
|
|
736
|
+
rule would save fine and do nothing.
|
|
737
|
+
- **`:splat` requires a `*`**, and `:name` in the target must be declared in
|
|
738
|
+
`from_path`.
|
|
739
|
+
- **Query strings can't be matched** — `_redirects` keys on the path. A
|
|
740
|
+
WordPress `/?p=123` URL has to be handled at the source.
|
|
741
|
+
- **A rule that would hide a live page is refused**, naming the pages.
|
|
742
|
+
Redirects are applied BEFORE static files, so `/blogg/*` makes every real
|
|
743
|
+
article under `/blogg/` unreachable. Narrow the prefix.
|
|
744
|
+
|
|
745
|
+
Rules are emitted most-specific-first, so `/blogg/recept/*` and `/blogg/*`
|
|
746
|
+
can coexist — the narrower one fires. `list_migration_urls` counts
|
|
747
|
+
pattern-covered URLs as `redirected`, so the coverage report reflects what
|
|
748
|
+
production will do.
|
|
749
|
+
|
|
750
|
+
### "Link language versions together (hreflang)"
|
|
751
|
+
|
|
752
|
+
One Typeroll site owns one domain, so `example.se` / `example.de` /
|
|
753
|
+
`example.co.uk` are three sites. Nothing can derive which page corresponds
|
|
754
|
+
to which — declare it per page:
|
|
755
|
+
|
|
756
|
+
```
|
|
757
|
+
update_page page_id=om-oss patch={ alternates: [
|
|
758
|
+
{ hreflang: "de", href: "https://example.de/ueber-uns" },
|
|
759
|
+
{ hreflang: "x-default", href: "https://example.com/about-us" }
|
|
760
|
+
]}
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
The renderer injects this page's own self-reference, so list only the OTHER
|
|
764
|
+
variants. Clusters must be **reciprocal** — write all sides, `batch_update_pages`
|
|
765
|
+
is the sane way. Use absolute URLs on the FINAL domains (never the
|
|
766
|
+
`*.typeroll` fallback). Invalid tags/hrefs are rejected at write time with
|
|
767
|
+
the reason rather than silently dropped at render.
|
|
768
|
+
|
|
609
769
|
### "Change the site's fallback URL (slug)"
|
|
610
770
|
|
|
611
771
|
```
|
|
@@ -777,7 +937,8 @@ preview.
|
|
|
777
937
|
| **Block types** | `list_block_types`, `read_block_type`, `find_pages_using_block_type`, `export_block_types`, `import_block_types` |
|
|
778
938
|
| **Collections** | `create_collection`, `update_collection_schema`, `delete_collection`, `list_collections`, `read_collection`, `list_collection_items` (richtext hidden by default), `read_collection_item`, `batch_read_collection_items`, `create_collection_item`, `update_collection_item`, `delete_collection_item`, `regenerate_collection_listing` |
|
|
779
939
|
| **Media** | `list_media`, `read_media`, `create_upload_url`, `upload_media_from_url`, `upload_media_inline`, `update_media`, `delete_media`, `finalize_media`, `finalize_all_media`, `generate_image_variants`, `suggest_alt_text_context` |
|
|
780
|
-
| **Redirects** | `list_redirects`, `create_redirect`, `delete_redirect` |
|
|
940
|
+
| **Redirects** | `list_redirects`, `create_redirect`, `delete_redirect`. `from_path` may be a PATTERN: a trailing `*` (with `:splat` in the target) or `:name` for one segment — one rule retires a whole family of dead URLs (`/category/*` → `/blogg/:splat`). Mid-path splats and query strings are refused, as is any rule that would hide a live page. |
|
|
941
|
+
| **Migration inventory** | `get_migration_readiness` (preflight — CALL FIRST on any import), `list_migration_urls`, `add_migration_urls`, `update_migration_url`, `delete_migration_url`, `verify_migration_urls`. The legacy site's URL list with LIVE coverage status (`migrated` / `redirected` / `excluded` / `unhandled`, recomputed on every read from current pages + redirects). `verify_migration_urls` is the pre-cutover check that REQUESTS every URL against the deployed site — see "Don't lose URLs in a migration" below. |
|
|
781
942
|
| **Forms** | `list_forms`, `read_form`, `create_form`, `update_form`, `delete_form`, `list_form_submissions`, `delete_form_submission` (removes one submission — e.g. cleaning up a test entry; `delete_form` with `delete_submissions` is the bulk path). **Steps (form/* block trees) are the ONLY stored model** (0.29.0+, `forms_steps_only` capability): pass `steps` for funnels, or `fields` for simple forms — the server converts a flat field list to a single static step (read_form shows the resulting steps). **Placing a form: prefer a `core/form` block with `data.form_id`** — it renders every form with validation + signed token handled. The raw `<form method="POST">` embed with hidden `_token` + honeypot `_hp` (from read/create's `submit_token`/`submit_url`) is the HTML-mode fallback, single-step forms only; no client JS (the sanitizer strips inline `<script>`) |
|
|
782
943
|
| **Settings** | `update_site_settings` (whitelist) |
|
|
783
944
|
| **Search + bulk** | `search_pages`, `bulk_replace_text` |
|
package/README.md
CHANGED
|
@@ -152,7 +152,9 @@ the full reference + concrete operation recipes.
|
|
|
152
152
|
- **Bulk** — `bulk_replace_text` with dry-run.
|
|
153
153
|
- **Branches** — create, read, delete, merge. Branch deploys get their
|
|
154
154
|
own URL at `{branch}.{project}.pages.dev`.
|
|
155
|
-
- **Deploy** — trigger, list, get
|
|
155
|
+
- **Deploy** — trigger (with `dry_run` to build without publishing), list, get
|
|
156
|
+
status. A finished job reports `cost`: what the build consumed in server
|
|
157
|
+
time, broken down per phase. Estimates from a rate card, not billing records.
|
|
156
158
|
- **Preview** — `get_preview_link` (signed URL for browser navigation;
|
|
157
159
|
supports `page_id`, `slug`, or `collection_name + item_id`; pass
|
|
158
160
|
`include_working_copy: true` to also render unsaved drafts).
|
|
@@ -189,9 +191,12 @@ The MCP server is purely an ergonomics layer on top of that.
|
|
|
189
191
|
- **HTML sanitization** happens at save time on the server — `<script>`,
|
|
190
192
|
event handlers, and `javascript:` URLs are stripped from page/partial
|
|
191
193
|
content (including `core/html` block output). The scriptable surfaces
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
are deliberate exceptions, and all of them are writable with an API key
|
|
195
|
+
under the key holder's own authority: `scripts_*` and `custom_css` on
|
|
196
|
+
the site settings, `script` on a block type, and the `js` field of a
|
|
197
|
+
`core/embed` block instance. Those writes are audit-logged and the
|
|
198
|
+
response carries a notice naming the stored JS. Only the in-portal chat
|
|
199
|
+
assistant is additionally gated, on a per-site opt-in.
|
|
195
200
|
- Keys can be **revoked** at any time from the portal. Revocation takes
|
|
196
201
|
effect on the next request (no in-flight requests get cancelled, but
|
|
197
202
|
the next one returns 401).
|