@typeroll/mcp-server 0.45.100 → 0.45.102

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/dist/server.js CHANGED
@@ -24,6 +24,7 @@ import { formTools } from './tools/forms.js';
24
24
  import { emailTools } from './tools/email.js';
25
25
  import { searchTools } from './tools/search.js';
26
26
  import { bulkTools } from './tools/bulk.js';
27
+ import { readinessTools } from './tools/readiness.js';
27
28
  import { versionTools } from './tools/versions.js';
28
29
  import { deployTools } from './tools/deploy.js';
29
30
  import { previewTools } from './tools/preview.js';
@@ -226,6 +227,7 @@ export function buildServer(options) {
226
227
  ...developerExtensionTools,
227
228
  ...searchTools,
228
229
  ...bulkTools,
230
+ ...readinessTools,
229
231
  ...versionTools,
230
232
  ...deployTools,
231
233
  ...previewTools,
@@ -5,9 +5,9 @@ const fields = {
5
5
  label_singular: z.string().optional(), label_plural: z.string().optional(), fields: schema.optional(),
6
6
  page_field_rules: z.record(z.string(), z.object({ label: z.string().optional(), writable_by: z.array(z.enum(['portal', 'owner', 'agent', 'app', 'import'])) })).optional(),
7
7
  allowed_templates: z.array(z.string()).nullable().optional().describe('Allowed Page template IDs, including the default. Null allows all compatible templates; an empty list permits no template.'),
8
- route_template: z.string().optional(), template: z.string().optional(), schema_type: z.string().optional(),
8
+ route_template: z.string().optional(), template: z.string().optional(), schema_type: z.string().optional().describe('Schema.org type each Page of this type represents, such as BlogPosting, Product or SoftwareApplication.'),
9
9
  schema_field_mode: z.enum(['all', 'mapped']).optional().describe('Use mapped to emit only explicitly mapped fields. Resolve typed references in Page json_ld for complex graphs.'),
10
- schema_field_map: z.record(z.string(), z.string()).optional(),
10
+ schema_field_map: z.record(z.string(), z.string()).optional().describe('Field name to a direct Schema.org property, e.g. {"cover":"image","os":"operatingSystem"}. For Product and SoftwareApplication types, price, priceCurrency, availability and priceValidUntil are published inside offers (an Offer), ratingValue with ratingCount or reviewCount inside aggregateRating, and a brand name as a Brand. A field mapped to image is also the share image.'),
11
11
  icon: z.string().optional(), sort_field: z.string().optional(), sort_dir: z.enum(['asc', 'desc']).optional(),
12
12
  facets: z.array(z.record(z.string(), z.unknown())).optional(), facet_combinations: z.array(z.tuple([z.string(), z.string()])).optional().describe('Pairs of configured facet field names, not values. Use editable Pages for a curated subset of combinations.'),
13
13
  };
@@ -12,7 +12,7 @@ export const deployTools = [
12
12
  },
13
13
  {
14
14
  name: 'trigger_deploy',
15
- description: "Enqueue a deploy (static-page build → hosting) of the currently active version. Returns a job_id immediately; poll get_deploy_status until the status leaves queued/running. NOT for previewing iterative design/content edits — that's what get_preview_link is for (it renders live from the DB with no build). Reserve trigger_deploy for publishing, a stakeholder link to the compiled site, or pre-merge validation. Deploys build SAVED content only — unsaved drafts (working copies) are excluded, so commit_working_copy (or save:true on your writes) before deploying, or your changes won't ship. Customer publishing freezes a consistent saved source, then resumes source/Git/build-dispatch checkpoints without recapturing later edits. GitHub or Cloudflare performs rendering and media work. With dry_run:true, customer publishing validates frozen source without a Git push or external build; it does not prove an Astro build succeeds. Other self-hosted adapters may build locally without upload. Check the reported phase. All sites in an organization share one build engine: publish one site at a time, starting the next only when the previous job has finished and read_organization_build_engine shows no active_jobs.",
15
+ description: "Enqueue a deploy (static-page build → hosting) of the currently active version. Returns a job_id immediately; poll get_deploy_status until the status leaves queued/running. NOT for previewing iterative design/content edits — that's what get_preview_link is for (it renders live from the DB with no build). Reserve trigger_deploy for publishing, a stakeholder link to the compiled site, or pre-merge validation. Deploys build SAVED content only — unsaved drafts (working copies) are excluded, so commit_working_copy (or save:true on your writes) before deploying, or your changes won't ship. Customer publishing freezes a consistent saved source, then resumes source/Git/build-dispatch checkpoints without recapturing later edits. GitHub or Cloudflare performs rendering and media work. With dry_run:true, customer publishing validates frozen source without a Git push or external build; it does not prove an Astro build succeeds. Other self-hosted adapters may build locally without upload. Check the reported phase. All sites in an organization share one build engine: publish one site at a time, starting the next only when the previous job has finished and read_organization_build_engine shows no active_jobs. A domain switch waiting for DNS approval (awaiting_approval) does not block other sites.",
16
16
  inputSchema: {
17
17
  environment: z.enum(['production', 'staging']).optional(),
18
18
  allow_unsaved: z.boolean().optional().describe('Explicitly publish saved content while leaving the reported unsaved Pages/partials untouched. Only after owner confirmation.'),
@@ -24,7 +24,7 @@ export const domainTools = [
24
24
  },
25
25
  {
26
26
  name: 'guide_publishing_domain_change',
27
- description: 'Guide a site domain move. Start with check: returns hosting, zone, nameserver, certificate, permission, publishing, redirect capacity and candidate checks, current phase, next action and an explicit switch summary. Saving future hosts is inert. Prepare republishes the last public content on main with the new origins, replacing the current site before DNS changes; newer unpublished CMS edits are excluded. Later normal publications retain the new origins. Approve changes DNS for the same main artifact without another build. Adding redirect hosts preserves the main artifact. Approve requires explicit user approval of the switch summary and any validation interruption. Automatic DNS is journaled and rolled back after a failed check or the agreed deadline; later external edits require manual attention. Rollback requests restoration through the serialized publication chain. External DNS remains the caller’s responsibility.',
27
+ description: 'Guide a site domain move. Start with check: returns hosting, zone, nameserver, certificate, permission, publishing, redirect capacity, candidate_noindex and candidate checks, current phase, next action and an explicit switch summary. candidate_noindex blocks prepare and approve while site settings, the last publication or the candidate tell search engines not to index or follow the site (next_action resolve_noindex): the temporary address is already blocked from indexing by Typeroll, so prepare with confirm_disable_noindex: true to turn it off and build an indexable candidate in one step, or pass allow_noindex_launch: true to launch hidden on purpose. Saving future hosts is inert. Prepare republishes the last public content on main with the new origins, replacing the current site before DNS changes; newer unpublished CMS edits are excluded. Later normal publications retain the new origins. Approve changes DNS for the same main artifact without another build. Adding redirect hosts preserves the main artifact. Approve requires explicit user approval of the switch summary and any validation interruption. Automatic DNS is journaled and rolled back after a failed check or the agreed deadline; later external edits require manual attention. Rollback requests restoration through the serialized publication chain. External DNS remains the caller’s responsibility.',
28
28
  inputSchema: {
29
29
  action: z.enum(['check', 'preview', 'save', 'prepare', 'approve', 'rollback']),
30
30
  revision: z.string().optional(), candidate_id: z.string().optional(),
@@ -32,6 +32,8 @@ export const domainTools = [
32
32
  redirect_hosts: z.array(z.string()).max(4).optional(), dns_mode: z.enum(['automatic', 'external']).optional(),
33
33
  confirm_rebuild: z.boolean().optional(), accept_validation_window: z.boolean().optional(),
34
34
  validation_timeout_minutes: z.number().int().min(5).max(45).optional(),
35
+ confirm_disable_noindex: z.boolean().optional().describe('For prepare: Turn off sitewide_noindex and sitewide_nofollow (and remove noindex from a robots meta tag in scripts_head) in the site settings and build the candidate indexable for the new website host in the same step, without a separate publication. Use this after the user confirms the site should be found by search engines; it satisfies the candidate_noindex blocker.'),
36
+ allow_noindex_launch: z.boolean().optional().describe('For prepare and approve: set only when Search engines are told to skip this site (sitewide_noindex, sitewide_nofollow or a robots noindex meta in scripts_head) and you deliberately want to launch it hidden. Without it, a site with authored noindex is rejected with candidate_noindex. Typeroll already blocks indexing of the temporary address, so sitewide_noindex is not needed during migration; normally turn it off and publish instead.'),
35
37
  },
36
38
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'publishing/domain-guide', args))),
37
39
  },
@@ -76,7 +78,7 @@ export const domainTools = [
76
78
  },
77
79
  {
78
80
  name: 'save_organization_media_access', noSite: true,
79
- description: 'Save and verify the Cloudflare R2 Access Key ID and Secret Access Key for the prepared media buckets (not the Cloudflare API token). Marks media storage ready, which imports require, and queues media migration. Requires an organization API key and the current Cloudflare revision. Keys are encrypted and never returned.',
81
+ description: 'Save and verify the Cloudflare R2 Access Key ID and Secret Access Key for the prepared media buckets (not the Cloudflare API token). Marks media storage ready, which imports require. The first activation moves existing media site by site; new keys for ready storage resume only stopped site migrations. Requires an organization API key and the current Cloudflare revision. Keys are encrypted and never returned.',
80
82
  inputSchema: { revision: z.string(), access_key_id: z.string(), secret_access_key: z.string() },
81
83
  handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('publishing/connections/cloudflare', { ...args, action: 'save_media' }))),
82
84
  },
@@ -112,7 +114,7 @@ export const domainTools = [
112
114
  },
113
115
  {
114
116
  name: 'read_organization_build_engine', noSite: true,
115
- description: 'Read the selected provider, saved Cloudflare and GitHub build engines, and active jobs for the organization (each with job_id, site_id, kind and status: queued, running, or publishing while a built publication uploads and verifies; image preparation jobs are media-…). A job stays listed until its deploy reaches a final status. Requires an organization API key; REST: GET /api/v1/publishing/builds. No provider credentials are returned.',
117
+ description: 'Read the selected provider, saved Cloudflare and GitHub build engines, and active jobs for the organization (each with job_id, site_id, kind and status: queued, running, or publishing while a built publication uploads and verifies; image preparation jobs are media-…). A job stays listed until its deploy reaches a final status, except a domain switch that is built and verified and only waits for DNS approval: it is listed under awaiting_approval, uses no build capacity, and does not block other sites from publishing. Requires an organization API key; REST: GET /api/v1/publishing/builds. No provider credentials are returned.',
116
118
  inputSchema: {},
117
119
  handler: withErrorBoundary(async (_args, { client }) => ok(await client.rootGet('publishing/builds'))),
118
120
  },
@@ -177,27 +179,31 @@ export const domainTools = [
177
179
  {
178
180
  name: 'configure_organization_publishing_domains',
179
181
  noSite: true,
180
- description: 'Configure organization media and site subdomains using an active domain in the connected Cloudflare account. Attaches the media host to R2, creates its DNS record and queues media migration. Site/version addresses are created at deployment. Requires an organization API key and the current revision. Does not replace existing DNS destinations or existing media hosts. Read organization publishing domains afterward to check HTTPS activation.',
182
+ description: 'Configure organization media and site subdomains using an active domain in the connected Cloudflare account. Attaches the media host to R2 and creates its DNS record; site media is not touched. Site/version addresses are created at deployment. Requires an organization API key and the current revision. Does not replace existing DNS destinations or existing media hosts. Read organization publishing domains afterward to check HTTPS activation.',
181
183
  inputSchema: { revision: z.string(), zone_id: z.string(), media_subdomain: z.string().describe('Short label, for example media.'), sites_subdomain: z.string().describe('Short label, for example sites.') },
182
184
  handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('publishing/domains', args))),
183
185
  },
184
186
  {
185
187
  name: 'prepare_publishing_domain_change',
186
188
  description: 'Publish for the new domain shortly before changing DNS, using only the last successfully published snapshot. This replaces main at the current or temporary address; it does not create a separate preview branch. Publish intended edits normally at the current or temporary address first. Newer unpublished CMS changes are excluded. Normal publication remains available and keeps these embedded domain addresses while DNS is pending. It refreshes the receipt for the same main site; no second preparation is needed. Requires the current domain revision. Poll read_publishing_domains for certificate and DNS requirements, then explicitly approve the verified cutover.',
187
- inputSchema: { revision: z.string() },
189
+ inputSchema: {
190
+ revision: z.string(),
191
+ confirm_disable_noindex: z.boolean().optional().describe('Turn off sitewide_noindex and sitewide_nofollow (and remove noindex from a robots meta tag in scripts_head) in the site settings and build the candidate indexable for the new website host in the same step, without a separate publication. Use this after the user confirms the site should be found by search engines; it satisfies the candidate_noindex blocker.'),
192
+ allow_noindex_launch: z.boolean().optional().describe('Set only when Search engines are told to skip this site (sitewide_noindex, sitewide_nofollow or a robots noindex meta in scripts_head) and you deliberately want to launch it hidden. Without it, a site with authored noindex is rejected with candidate_noindex. Typeroll already blocks indexing of the temporary address, so sitewide_noindex is not needed during migration; normally turn it off and publish instead.'),
193
+ },
188
194
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'publishing/prepare', args))),
189
195
  },
190
196
  {
191
197
  name: 'read_organization_media_migration',
192
198
  noSite: true,
193
- description: 'Read progress and actionable failures while original media moves to the organization Cloudflare account. Requires an organization API key.',
199
+ description: 'Read progress and actionable failures while original media moves to the organization Cloudflare account: overall state with updated_at, each site (state, counts, error, updated_at) and failed_media (site_id, media_id, filename, source_url, code, message, at) for files that could not be copied and were set aside. Fix or delete those files, then retry_organization_media_migration. Requires an organization API key.',
194
200
  inputSchema: {},
195
201
  handler: withErrorBoundary(async (_args, { client }) => ok(await client.rootGet('publishing/media-migration'))),
196
202
  },
197
203
  {
198
204
  name: 'retry_organization_media_migration',
199
205
  noSite: true,
200
- description: 'Resume verified media migration after fixing account or R2 setup. Source files remain readable; pointer changes require matching hashes. Set prepare_transfer to set up or recheck the transfer worker in the organization Cloudflare account. Requires an organization API key.',
206
+ description: 'Resume verified media migration after fixing account or R2 setup. Restarts only failed site migrations and stopped jobs, giving set-aside files fresh attempts; completed sites are not touched. Source files remain readable; pointer changes require matching hashes. Set prepare_transfer to set up or recheck the transfer worker in the organization Cloudflare account. Requires an organization API key.',
201
207
  inputSchema: { prepare_transfer: z.boolean().optional() },
202
208
  handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('publishing/media-migration', args.prepare_transfer ? { action: 'prepare_transfer', recheck: true } : {}))),
203
209
  },
@@ -214,12 +220,13 @@ export const domainTools = [
214
220
  revision: z.string(), candidate_id: z.string(),
215
221
  validation_timeout_minutes: z.number().int().min(5).max(45).optional(),
216
222
  accept_validation_window: z.boolean().optional().describe('Accept the interruption until Cloudflare validates the website host after its single reviewed record changes. Only valid when preparation.validation_blocker.resolution is accept_validation_window.'),
223
+ allow_noindex_launch: z.boolean().optional().describe('Set only when Search engines are told to skip this site (sitewide_noindex, sitewide_nofollow or a robots noindex meta in scripts_head) and you deliberately want to launch it hidden. Without it, a site with authored noindex is rejected with candidate_noindex. Typeroll already blocks indexing of the temporary address, so sitewide_noindex is not needed during migration; normally turn it off and publish instead.'),
217
224
  },
218
225
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'publishing/cutover', args))),
219
226
  },
220
227
  {
221
228
  name: 'read_publishing_domains',
222
- description: 'Read the desired and active website, redirect and media hosts, retained media aliases, DNS management mode and domain configuration revision. host_checks lists each future host with its Cloudflare zone and structured blockers and warnings (code, host, message), for example apex_zone_required (an apex such as example.com needs an active Cloudflare zone in the hosting account), zone_not_active (the zone is pending until its nameservers change) or redirect_zone_required. Resolve blockers before preparing. preparation and redirect_preparations hold DNS records, the zone redirect rule and certificate state after preparation. Saving intent does not switch traffic. published identifies the addresses embedded in main, separately from active (verified traffic). Saving desired hosts is inert. prepare_publishing_domain_change explicitly replaces main with the new origins; later normal publication retains them and refreshes the DNS approval receipt. candidate.sitewide_noindex describes the frozen candidate, not current settings; do not approve an unintentionally noindex launch.',
229
+ description: 'Read the desired and active website, redirect and media hosts, retained media aliases, DNS management mode and domain configuration revision. host_checks lists each future host with its Cloudflare zone and structured blockers and warnings (code, host, message), for example apex_zone_required (an apex such as example.com needs an active Cloudflare zone in the hosting account), zone_not_active (the zone is pending until its nameservers change) or redirect_zone_required. Resolve blockers before preparing. preparation and redirect_preparations hold DNS records, the zone redirect rule and certificate state after preparation. Saving intent does not switch traffic. published identifies the addresses embedded in main, separately from active (verified traffic). Saving desired hosts is inert. prepare_publishing_domain_change explicitly replaces main with the new origins; later normal publication retains them and refreshes the DNS approval receipt. candidate.sitewide_noindex describes the frozen candidate, not current settings. Prepare and approve reject an authored noindex launch with candidate_noindex unless allow_noindex_launch is true.',
223
230
  inputSchema: {},
224
231
  handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'publishing/domains'))),
225
232
  },
@@ -79,7 +79,7 @@ export const mediaTools = [
79
79
  },
80
80
  {
81
81
  name: 'upload_media_from_url',
82
- description: 'Fetch an image (or PDF) from any public URL and push it to the site\'s media library in one call. The MCP server downloads the source from verified public HTTP(S) destinations; private addresses and unsafe redirects are refused. Integrity-safe: the bytes are streamed, never transcribed as base64 through the model, so (unlike a large `upload_media_inline` payload) they can\'t be silently corrupted in transit. Returns { media_id, cdn_url, filename, finalize }. Each upload queues image preparation (finalize.preparation): one job per site on the organization\'s shared build engine, started once uploads have been quiet for 30 s (at most 5 min after the first), so a series of uploads shares one job. get_media_preparation returns its build_job_id; let it finish, like any other active build, before publishing another site of the organization.',
82
+ description: 'Fetch an image (or PDF) from any public URL and push it to the site\'s media library in one call. The organization\'s media transfer Worker downloads the source from verified public HTTP(S) destinations straight into the organization\'s storage; private addresses and unsafe redirects are refused. Integrity-safe: the bytes are streamed, never transcribed as base64 through the model, so (unlike a large `upload_media_inline` payload) they can\'t be silently corrupted in transit. Returns { media_id, cdn_url, filename, finalize }. Each upload queues image preparation (finalize.preparation): one job per site on the organization\'s shared build engine, started once uploads have been quiet for 30 s (at most 5 min after the first), so a series of uploads shares one job. get_media_preparation returns its build_job_id; let it finish, like any other active build, before publishing another site of the organization.',
83
83
  inputSchema: {
84
84
  source_url: z.string().url().describe('Public URL to download the image from.'),
85
85
  filename: z.string().optional().describe('Override the filename used on R2. Defaults to the last path segment of source_url. When its extension does not match the content type, the type\'s extension is appended; the response returns the stored filename.'),
@@ -182,7 +182,7 @@ export const migrationTools = [
182
182
  },
183
183
  {
184
184
  name: 'verify_migration_urls',
185
- description: "Inventories over 500 URLs: one call checks at most 500 and stores no launch evidence (the response then has evidence_stored: false and next_step); run start_workflow with type url_parity to check every URL and record the evidence. Pre-cutover parity check: actually REQUEST every observed inventory path against the new site and report what it answers. Slash-equivalent paths share one inventory row but remain separate checks through `observed_paths`, so `summary.checked` can exceed the normalized inventory count. list_migration_urls tells you what the data says; this tells you what the server does — a redirect pointing at an unpublished page, a typo'd path, a missing slash variant, or a redirect loop all read as \"handled\" in coverage and as a 404 to Googlebot. Runs against the site's fallback subdomain by default. The response is compact by default: the full summary plus only `missing`, `broken_redirect`, and `error` rows; successful rows are counted but omitted. Pass `verdicts` for an exact result filter or `include_successes: true` for every row. Canonical trailing-slash normalization counts as `ok`, not `ok_redirect`. Also stamps verified/last_checked on redirect rules it exercised. Deploy before running this — it tests the DEPLOYED site, not your drafts.",
185
+ description: "Inventories over 500 URLs: one call checks at most 500 and stores no launch evidence (the response then has evidence_stored: false and next_step); run start_workflow with type url_parity to check every URL and record the evidence. Pre-cutover parity check: actually REQUEST every observed inventory path against the new site and report what it answers. Slash-equivalent paths share one inventory row but remain separate checks through `observed_paths`, so `summary.checked` can exceed the normalized inventory count. list_migration_urls tells you what the data says; this tells you what the server does — a redirect pointing at an unpublished page, a typo'd path, a missing slash variant, or a redirect loop all read as \"handled\" in coverage and as a 404 to Googlebot. Runs against the site's fallback subdomain by default. The response is compact by default: the full summary plus only `missing`, `broken_redirect`, and `error` rows; successful rows are counted but omitted. Pass `verdicts` for an exact result filter or `include_successes: true` for every row. Canonical trailing-slash normalization counts as `ok`, not `ok_redirect`. A query-string rule (published as an inline script, also from 404.html) is followed and counts as `ok_redirect` with `client_redirect: true`. Also stamps verified/last_checked on redirect rules it exercised. Deploy before running this — it tests the DEPLOYED site, not your drafts.",
186
186
  inputSchema: {
187
187
  target_origin: z
188
188
  .string()
@@ -5,8 +5,8 @@ const fields = {
5
5
  blocks: z.array(z.any()).optional(), status: z.enum(['draft', 'published']).optional(),
6
6
  };
7
7
  export const pageTemplateTools = [
8
- { name: 'get_composition_starter', description: 'Read one native editable starting block tree without saving. Choose a Page template, header/footer or archive. Replace sample navigation and map profile field_list fields before saving with the normal Page/template/partial tools. Existing content is never replaced by this read.', inputSchema: {
9
- kind: z.enum(['header', 'footer', 'archive', 'custom', 'article', 'blog', 'checklist', 'team', 'events', 'products', 'profile', 'landing']),
8
+ { name: 'get_composition_starter', description: 'Read one native editable starting block tree without saving. Choose a Page template, header/footer or archive, or a Page body for a common marketing page: feature_page, comparison_page (with a comparison table), integration_page, pricing_page (plan table and plan comparison) or use_case_page. Page bodies go into create_page/update_page blocks; replace every [bracketed placeholder] and remove sections you have no real content for. Replace sample navigation and map profile field_list fields before saving with the normal Page/template/partial tools. Existing content is never replaced by this read.', inputSchema: {
9
+ kind: z.enum(['header', 'footer', 'archive', 'custom', 'article', 'blog', 'checklist', 'team', 'events', 'products', 'profile', 'landing', 'feature_page', 'comparison_page', 'integration_page', 'pricing_page', 'use_case_page']),
10
10
  content_type: z.string().optional(), title: z.string().optional(), version: versionParam,
11
11
  }, handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'composition-starters', args))) },
12
12
  { name: 'list_page_templates', description: 'List reusable Page templates in the selected version, including inherited templates.', inputSchema: { version: versionParam },
@@ -0,0 +1,13 @@
1
+ // Site launch readiness. One read-only report of what the site needs to be
2
+ // found, shared and understood; every item has a fix and a guide link.
3
+ import { ok, withErrorBoundary, versionParam } from './helpers.js';
4
+ export const readinessTools = [
5
+ {
6
+ name: 'read_site_readiness',
7
+ description: 'Read-only launch readiness report for the selected site and version: share image, home page description, organization and profile links, favicon, analytics and consent, Search Console, privacy and contact pages, indexable and orphan pages, duplicate titles and descriptions, H1 per page, description length, duplicate font loads, custom CSS that overrides the theme, conflicts between the site\'s ai_crawler_policy and the Cloudflare zone\'s AI crawler settings, and page JSON-LD problems. ' +
8
+ 'Returns {site_id, checked_at, summary:{ok, suggestions, warnings}, items:[{id, status, message, fix, guide, details?}]}. Items are warnings or suggestions, never blockers; a missing default share image is a suggestion. ' +
9
+ 'Run it before launch and after larger changes, then fix or record each open item. It reads the Cloudflare zone only when the existing connection already may; it never changes zone settings or content.',
10
+ inputSchema: { version: versionParam },
11
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'readiness', args.version ? { version: args.version } : undefined))),
12
+ },
13
+ ];
@@ -13,7 +13,7 @@ function v(version) {
13
13
  export const settingsTools = [
14
14
  {
15
15
  name: 'lint_design_tokens',
16
- description: 'Read-only design audit of saved Pages, partials, page templates, custom block types, styles and custom CSS in the selected Version. Lists hardcoded colours, typography and spacing with exact paths and token suggestions. exact_replacements is a dry-run plan for unambiguous direct block hex matches; never apply ambiguous roles or CSS automatically. Does not block launch. Unsaved working copies are excluded.',
16
+ description: 'Read-only design audit of saved Pages, partials, page templates, custom block types, styles and custom CSS in the selected Version. Lists hardcoded colours, typography and spacing with exact paths and token suggestions. exact_replacements is a dry-run plan for unambiguous direct block hex matches; never apply ambiguous roles or CSS automatically. css_findings lists custom CSS that works against the theme (heavy !important use, fonts not in the theme, theme fonts loaded twice, a first-section background image set in CSS, rules on platform block classes), each with the Style, token or block setting that replaces it. Does not block launch. Unsaved working copies are excluded.',
17
17
  inputSchema: { version: versionParam },
18
18
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'design-token-audit', v(args.version)))),
19
19
  },
@@ -28,7 +28,7 @@ export const settingsTools = [
28
28
  },
29
29
  {
30
30
  name: 'read_site_settings',
31
- description: "Read every site setting: name, tagline, logo, favicon/app icons, colors, fonts, contact info, social links, URL trailing-slash policy, iframe host allowlist, default SEO suffix/description, default_og_image, twitter_handle, organization (JSON-LD), language, robots_txt, image_sizes_default, cookie_consent, plus the scriptable surfaces scripts_head, scripts_body_end, and custom_css, form_submission_retention_days (days completed form submissions are kept before deletion; null = kept until deleted; site-wide, not per version), `urls` (including `urls.staging`, the site's staging_url) and `render` (the site's platform render version, the latest version and what upgrading would change). Pass `version` to read a branch's settings (with copy-on-write chain-fallback to main for fields the branch hasn't overridden).",
31
+ description: "Read every site setting: name, tagline, logo, favicon/app icons, colors, fonts, contact info, social links, URL trailing-slash policy, iframe host allowlist, default SEO suffix/description, default_og_image, twitter_handle, organization (JSON-LD), language, robots_txt, ai_crawler_policy, image_sizes_default, cookie_consent, plus the scriptable surfaces scripts_head, scripts_body_end, and custom_css, form_submission_retention_days (days completed form submissions are kept before deletion; null = kept until deleted; site-wide, not per version), `urls` (including `urls.staging`, the site's staging_url) and `render` (the site's platform render version, the latest version and what upgrading would change). Pass `version` to read a branch's settings (with copy-on-write chain-fallback to main for fields the branch hasn't overridden).",
32
32
  inputSchema: {
33
33
  version: versionParam,
34
34
  },
@@ -65,6 +65,7 @@ export const settingsTools = [
65
65
  default_meta_description: z.string().optional().describe('Site-wide fallback <meta name="description">. Used when a page has no seo_description of its own; falls back further to the tagline when unset.'),
66
66
  language: z.string().optional().describe('BCP-47 tag (e.g. "en", "sv", "en-GB"). Drives <html lang> on the rendered site.'),
67
67
  robots_txt: z.string().optional(),
68
+ ai_crawler_policy: z.enum(['allow_all', 'no_training', 'block_all']).nullable().optional().describe('AI crawler preference, written as Cloudflare-compatible Content Signals in robots.txt and the Content-Signal response header. `allow_all` (default, writes nothing); `no_training` (search=yes, ai-input=yes, ai-train=no, and Disallow groups for training-only crawlers such as GPTBot, ClaudeBot and Google-Extended); `block_all` (also disallows AI search and user agents such as OAI-SearchBot, ChatGPT-User and PerplexityBot). Googlebot, Bingbot and Applebot are never blocked. An explicit group for an agent in robots_txt wins. A Cloudflare zone AI bot policy is enforced at the edge and can override this; `null` resets to the default.'),
68
69
  sitewide_noindex: z.boolean().optional().describe('Exclude public pages from search indexing; following links remains a separate setting.'),
69
70
  external_routes: z.array(z.object({ path: z.string().min(2).max(2048), owner: z.string().min(1).max(200) }).strict()).max(1000).optional().describe('Exact same-origin paths owned by an independent deployment, e.g. [{path:"/docs/",owner:"Documentation"}]. No wildcards. Does not configure hosting or verify remote availability. Verify each declared public URL separately. Local routes cannot be exempted.'),
70
71
  seo_review: z.object({ forbidden_markers: z.array(z.string().min(1).max(500)).max(100).optional(), claims: z.array(z.object({ phrase: z.string().min(1).max(500), guidance: z.string().min(1).max(500) })).max(100).optional(), notes: z.array(z.string().min(1).max(500)).max(100).optional() }).optional().describe('Editorial review rules checked on all built pages, including unchanged pages. Warnings require human review; no copy is rewritten.'),
package/dist/version.js CHANGED
@@ -8,4 +8,4 @@
8
8
  //
9
9
  // Keep it in lockstep with package.json: tests/version.test.ts asserts
10
10
  // VERSION === package.json.version, so a bump that forgets this line fails CI.
11
- export const VERSION = '0.45.100';
11
+ export const VERSION = '0.45.102';
package/dist/workspace.js CHANGED
@@ -119,10 +119,10 @@ export const workspaceTemplates = {
119
119
  'README.md': `# Website workspace\n\nKeep project intent here; edit the website through Typeroll CMS.\n\n1. Read AGENTS.md. For guidance, ask your agent to start or resume workflows/interview.md. Otherwise use workflows/new-site.md or workflows/migration.md. Save confirmed goals, rules and SEO strategy in brief/; record text and design/image choices separately for migrations.\n2. Connect hosted MCP at https://app.typeroll.com/api/mcp?tools=compact.\n3. Use an Organization key to create a site, or select an existing one.\n4. Record its IDs and working version in typeroll.json. No secrets belong there.\n5. Run npx @typeroll/mcp-server doctor . (inject TYPEROLL_API_KEY privately).\n\nStart guide: https://typeroll.com/docs/getting-started/agent-workspace/\n\nUpdate the generated scaffold with npx @typeroll/mcp-server init . --update.\nEdited files are preserved and reported for manual reconciliation. Optional local\nrecipes: init . --recipes. They are reference Markdown, not automatically loaded.\n`,
120
120
  'brief/site.md': '# Site brief\n\n## Purpose and audience\n\n## Languages and scope\n\n## Primary user journeys\n\n## Content authority and sources\n\n## Editorial constraints and forbidden internal markers\n',
121
121
  'brief/brand.md': '# Brand\n\n## Voice and tone\n\n## Visual direction and references\n\n## Typography, colors and image policy\n\n## Desktop and mobile navigation\n\nRecord intent here; current design tokens belong in CMS settings. Choose semantic palette roles and typography/spacing steps; record deliberate hardcoded exceptions in decisions.md.\n',
122
- 'brief/content-model.md': '# Content model\n\n## Page types and routes\n\n## Fields and references\n\n## Shared templates and partials\n\n## Taxonomy and navigation\n\nUse the current CMS schema. This document describes decisions, not a second schema store.\n',
122
+ 'brief/content-model.md': '# Content model\n\n## Page plan\n\nThe pages the business needs, agreed before design work: home, about, services or products, pricing, contact, privacy policy and other legal pages, comparisons and articles as relevant. One row per page: title, path, purpose, primary search intent and status (planned, built, published).\n\n| Title | Path | Purpose | Search intent | Status |\n| --- | --- | --- | --- | --- |\n\n## Page types and routes\n\n## Fields and references\n\n## Shared templates and partials\n\n## Taxonomy and navigation\n\nUse the current CMS schema. This document describes decisions, not a second schema store.\n',
123
123
  'brief/rules.md': "# Owner preferences and project rules\n\n## Confirmed rules\n\nRecord each rule, its scope, who approved it and the date. Separate confirmed rules from proposals.\n\n## Writing and factual accuracy\n\nVoice, terminology, prohibited claims, source requirements and legal copy that must not be rewritten.\n\n## Design and editing\n\nBrand constraints, accessibility, editable content, standard/custom blocks and documented HTML exceptions.\n\n## Operations and approval\n\nWho may approve content, publication, DNS changes and messages? Credentials stay outside this repository.\n\n## Exceptions and superseded rules\n\nLink decisions.md; do not silently override a confirmed rule. Project notes do not grant permissions beyond the user's authorization or account access.\n",
124
- 'brief/seo.md': "# SEO strategy\n\n## Business goals, audience, markets and languages\n\n## Search intent and topic coverage\n\nRecord research sources and dates. Distinguish evidence from assumptions; do not invent traffic or ranking forecasts.\n\n## Page roles and URL plan\n\nContent types, useful landing pages, taxonomy, internal links and routes to preserve or redirect. Avoid near-duplicate pages created only for keyword variants.\n\n## Indexing and canonical policy\n\nWhich pages should be indexable, public noindex, or private? Record deliberate canonical overrides and sitemap policy; actual values live in the CMS.\n\n## Editorial standards and structured data\n\nUnique useful copy, supported claims, descriptive titles and links, accessible image text, schema matching visible content. No fixed title length or word count is a hard SEO rule.\n\n## Measurement and review\n\nBaseline, analytics/consent decisions, measures of success and review dates. Never store credentials or private analytics exports here.\n",
125
- 'workflows/README.md': "# Project routines\n\nRead only the relevant routine and discover current tool schemas through MCP.\nProject files record intent; the CMS owns current content and configuration.\n\n| Task | Project context | On-demand recipe |\n| --- | --- | --- |\n| Guided start | workflows/interview.md, brief/interview.md | Ask one question at a time; resume confirmed answers |\n| New website | workflows/new-site.md, brief/site.md, brief/brand.md, brief/content-model.md | tr-new-site, tr-starter-design |\n| Migration | workflows/migration.md, brief/migration.md | tr-migration-evidence, then tr-migrate-wp or the matching source recipe |\n| Everyday editing | brief/rules.md, pages/ notes and the relevant Page | tr-edit-site |\n| Page notes and review | workflows/page-notes.md, pages/README.md, pages/index.md | Read before edits; update logs afterward |\n| SEO | brief/seo.md and current site settings | tr-seo |\n| New reusable block | brief/brand.md and recorded editing needs | tr-custom-blocks |\n| Verification and publication | qa/checklist.md, decisions.md | Current preview/deploy tools and publishing readiness |\n\nAdd project-specific routines here with inputs, steps, acceptance evidence and approval boundaries. Do not copy the complete Typeroll manual or private app guides. Discover enabled app guidance with read_app_documentation. Load a recipe when needed so instructions stay current and context stays small.\n",
124
+ 'brief/seo.md': "# SEO strategy\n\n## Launch basics\n\nAsk the owner early; each item is optional. Record a skipped item here with the reason. Current values live in the CMS settings.\n\n- Default share image (`default_og_image`): chosen, uploaded or created with tr-imagegen; 1200×630 PNG or JPEG. Never a generated title card.\n- Organization (`organization`): business name and logo, and official profile URLs (`same_as`).\n- Page plan: agreed in brief/content-model.md, including privacy policy and contact pages.\n- Search Console: property type (Domain or URL prefix), verification method, who has access and the date the sitemap was submitted.\n- AI crawlers (`ai_crawler_policy`): allow_all (default), no_training or block_all, and any Cloudflare zone policy that must match.\n\n## Business goals, audience, markets and languages\n\n## Search intent and topic coverage\n\nRecord research sources and dates. Distinguish evidence from assumptions; do not invent traffic or ranking forecasts.\n\n## Page roles and URL plan\n\nContent types, useful landing pages, taxonomy, internal links and routes to preserve or redirect. Avoid near-duplicate pages created only for keyword variants.\n\n## Indexing and canonical policy\n\nWhich pages should be indexable, public noindex, or private? Record deliberate canonical overrides and sitemap policy; actual values live in the CMS.\n\n## Editorial standards and structured data\n\nUnique useful copy, supported claims, descriptive titles and links, accessible image text, schema matching visible content. No fixed title length or word count is a hard SEO rule.\n\n## Measurement and review\n\nBaseline, analytics/consent decisions, measures of success and review dates. Never store credentials or private analytics exports here.\n",
125
+ 'workflows/README.md': "# Project routines\n\nRead only the relevant routine and discover current tool schemas through MCP.\nProject files record intent; the CMS owns current content and configuration.\n\n| Task | Project context | On-demand recipe |\n| --- | --- | --- |\n| Guided start | workflows/interview.md, brief/interview.md | Ask one question at a time; resume confirmed answers |\n| New website | workflows/new-site.md, brief/site.md, brief/brand.md, brief/content-model.md | tr-new-site, tr-starter-design |\n| Migration | workflows/migration.md, brief/migration.md | tr-migration-evidence, then tr-migrate-wp or the matching source recipe |\n| Everyday editing | brief/rules.md, pages/ notes and the relevant Page | tr-edit-site |\n| Page notes and review | workflows/page-notes.md, pages/README.md, pages/index.md | Read before edits; update logs afterward |\n| SEO | brief/seo.md and current site settings | tr-seo |\n| New reusable block | brief/brand.md and recorded editing needs | tr-custom-blocks |\n| Verification and publication | qa/checklist.md, decisions.md | Current preview/deploy tools and publishing readiness |\n| Launch | qa/checklist.md (Launch), brief/seo.md (Launch basics) | read_site_readiness, tr-seo |\n\nAdd project-specific routines here with inputs, steps, acceptance evidence and approval boundaries. Do not copy the complete Typeroll manual or private app guides. Discover enabled app guidance with read_app_documentation. Load a recipe when needed so instructions stay current and context stays small.\n",
126
126
  'decisions.md': '# Decisions\n\nRecord date, decision, reason, approved scope and alternatives when useful. For design-token exceptions include the source/block, fixed value, reason (e.g. white text on a photograph) and review criteria.\n',
127
127
  'sources/README.md': '# Sources\n\nStore research references and copy proposals. Record source URLs and dates.\nPrivate extracts belong in ignored private/ or an approved external store.\nDo not treat local drafts as current CMS content or commit media libraries.\n',
128
128
  };
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.45.100",
3
+ "mcpName": "io.github.Typeroll/typeroll",
4
+ "version": "0.45.102",
4
5
  "description": "Typeroll CMS MCP server \u2013 connect AI agents to the REST API to manage content and publish static websites.",
5
6
  "license": "MIT",
6
7
  "repository": {
@@ -165,11 +165,16 @@ One call sets everything:
165
165
  ```
166
166
  update_site_settings {
167
167
  "colors": { ...all 7 tokens },
168
- "fonts": { "heading": "...", "body": "...", "size_base": 16 },
169
- "custom_css": "/* optional: utility classes or @keyframes */"
168
+ "fonts": { "heading": "...", "body": "...", "size_base": 16 }
170
169
  }
171
170
  ```
172
171
 
172
+ The brand lives in three places that editors can see and change: the
173
+ `colors` and `fonts` settings, the design scales, and named Styles (Step 5b).
174
+ Font families are set only in `fonts`; the platform loads each family once.
175
+ Never load a font again through `custom_css` (`@import`, `@font-face`) or
176
+ `scripts_head`: the readiness item `duplicate_fonts` reports it.
177
+
173
178
  Read back to confirm: `read_site_settings`.
174
179
 
175
180
  **Inside a redesign branch, scope the brand to the branch** — pass
@@ -225,11 +230,19 @@ eyebrow, lead, quote, buttons and section rhythm are chosen per block with
225
230
  `at.desktop`. Prefer this to custom CSS: editors can see and change styles,
226
231
  and every page follows.
227
232
 
228
- ## Step 6 — Custom CSS for advanced tokens (optional)
233
+ ## Step 6 — Custom CSS only for recorded exceptions
234
+
235
+ Use `custom_css` only for what settings, scales and Styles cannot express,
236
+ and record each rule in `decisions.md` with its reason and the block or
237
+ page it serves. Before writing a rule, check `read_block_type` for a field,
238
+ part or Style that does the same job. Don't override block classes with
239
+ `!important`, restyle every heading or button in CSS, or set a hero's
240
+ background image in CSS: the platform cannot then load that image early, and
241
+ the readiness item `custom_css_overrides` reports it. Use the block's own
242
+ image field instead.
229
243
 
230
- If the brand needs things beyond the 7 base tokens — e.g. a gradient,
231
- a special border radius, or a branded highlight color — add them via
232
- `custom_css`:
244
+ A legitimate exception is a token beyond the 7 base colours, such as a
245
+ gradient, a special border radius or a branded highlight colour:
233
246
 
234
247
  ```css
235
248
  :root {
@@ -363,8 +376,9 @@ temporary/test address:
363
376
  is light, white nav text becomes unreadable. Either darken `primary` or
364
377
  make the header use `secondary`.
365
378
  - **Custom_css is global.** Rules here apply to every page. Keep it to
366
- `:root {}` token additions and truly global utilities. Page-specific
367
- styles go in the page's HTML `<style>` block.
379
+ `:root {}` token additions and recorded exceptions. A look that repeats
380
+ is a named Style; a one-off treatment is a block's `custom_class` with a
381
+ rule in that Page's `custom_css`.
368
382
  - **Font weight on every page.** Two families is fine; a third adds
369
383
  measurable LCP impact even when self-hosted. A misspelled family name
370
384
  fails silently to the fallback font — verify it as in Step 7.
@@ -41,6 +41,22 @@ Record a URL inventory from the existing site and source routing. Use explicit
41
41
  Page paths to preserve nested routes or frontmatter-based permalinks. Do not
42
42
  assume that a filename alone determines the production URL.
43
43
 
44
+ ## Launch basics (ask early)
45
+
46
+ Ask the user for these once, early in the project. Read `read_site_settings` first and ask only for
47
+ what is missing. Each item is optional. Record a skipped item in
48
+ `decisions.md`, so the launch check in the last step holds no surprises.
49
+
50
+ - **Default share image.** Ask the user to choose or create the image that
51
+ social networks and chat apps show when a page has no image of its own:
52
+ upload one (`create_upload_url` or `upload_media_from_url`), pick an existing
53
+ one (`list_media`), or create one with `tr-imagegen`. Use a 1200×630 px PNG or
54
+ JPEG and set it as `default_og_image` with `update_site_settings`. Never
55
+ generate a title card from the page title instead. The user may skip this.
56
+ - **Organization.** The business name, logo and official profile URLs
57
+ (LinkedIn, Instagram, GitHub and similar). Set `organization` with `same_as`;
58
+ it becomes the home page's Organization schema.
59
+
44
60
  ## Map the source
45
61
 
46
62
  - Common frontmatter such as title, slug, author, publication date and SEO maps
@@ -82,3 +98,33 @@ and Typeroll preview, then a fresh authorized static build. Verify every invento
82
98
  URL and redirect, canonical/hreflang, sitemap inclusion, noindex state and assets.
83
99
  Save working copies before publication. Keep the source host and DNS in place
84
100
  until the candidate passes checks and the user authorizes its cutover.
101
+
102
+ ## Launch (required)
103
+
104
+ A site is not finished until this step has run. Call `read_site_readiness`
105
+ before the production publication and again after it. For every item with the
106
+ status `warning` or `suggestion`, either fix it with the tool its `fix` names,
107
+ or record in `decisions.md` (or the starter's `qa/` folder) why it stays open.
108
+ Tell the user which items remain. A missing default share image is only a
109
+ suggestion; leave it if the user chose to skip it.
110
+
111
+ The report warns and never blocks a publication. A clean report is not approval
112
+ to publish: publish only when the user has approved it.
113
+
114
+ Publish only after the URL inventory, `verify_migration_urls` and
115
+ `get_migration_launch_report` pass, and the user approves:
116
+
117
+ ```
118
+ read_site_readiness
119
+ trigger_deploy
120
+ get_deploy_status job_id=<id>
121
+ ```
122
+
123
+ The readiness report does not replace the migration launch report.
124
+
125
+ Run `read_site_readiness` again after the publication. It reads saved content
126
+ and settings, so confirm the rest on the live site: the AI crawler zone check
127
+ needs the site's website host, and a Search Console property verified by DNS
128
+ is not visible to Typeroll. The
129
+ [launch checklist](https://typeroll.com/docs/guides/launch-checklist/)
130
+ explains every item.
@@ -77,6 +77,22 @@ Ask explicitly:
77
77
  - Any domains being **retired or merged**? Those need HTTP redirects served by the old hostname; DNS itself is not
78
78
  a redirect. Plan that hosting separately.
79
79
 
80
+ ### Launch basics per market (ask early)
81
+
82
+ Ask the user for these once per market, during Phase 0. Read `read_site_settings` first and ask only for
83
+ what is missing. Each item is optional. Record a skipped item in
84
+ `decisions.md`, so the launch check in the last step holds no surprises.
85
+
86
+ - **Default share image.** Ask the user to choose or create the image that
87
+ social networks and chat apps show when a page has no image of its own:
88
+ upload one (`create_upload_url` or `upload_media_from_url`), pick an existing
89
+ one (`list_media`), or create one with `tr-imagegen`. Use a 1200×630 px PNG or
90
+ JPEG and set it as `default_og_image` with `update_site_settings`. Never
91
+ generate a title card from the page title instead. The user may skip this.
92
+ - **Organization.** The business name, logo and official profile URLs
93
+ (LinkedIn, Instagram, GitHub and similar). Set `organization` with `same_as`;
94
+ it becomes the home page's Organization schema.
95
+
80
96
  ## Phase 1 — Inventory EVERY old site, before building anything
81
97
 
82
98
  Do this for all sites up front. It's cheap, it's the only artefact that tells
@@ -292,6 +308,35 @@ new canonical build and is not an inventory-only operation):
292
308
  6. Rerun the complete, unfiltered URL check and SEO review, then require
293
309
  `get_migration_launch_report` to return `launch_ready: true`
294
310
 
311
+ ## Phase 8 — Launch (required)
312
+
313
+ A site is not finished until this step has run. Call `read_site_readiness`
314
+ before the production publication and again after it. For every item with the
315
+ status `warning` or `suggestion`, either fix it with the tool its `fix` names,
316
+ or record in `decisions.md` (or the starter's `qa/` folder) why it stays open.
317
+ Tell the user which items remain. A missing default share image is only a
318
+ suggestion; leave it if the user chose to skip it.
319
+
320
+ The report warns and never blocks a publication. A clean report is not approval
321
+ to publish: publish only when the user has approved it.
322
+
323
+ Run it for every site in the family, one site at a time, after Phase 7
324
+ passes and before each site's production publication:
325
+
326
+ ```
327
+ read_site_readiness site_id=<de-site>
328
+ ```
329
+
330
+ The readiness report does not replace `get_migration_launch_report`: a site
331
+ needs both.
332
+
333
+ Run `read_site_readiness` again after the publication. It reads saved content
334
+ and settings, so confirm the rest on the live site: the AI crawler zone check
335
+ needs the site's website host, and a Search Console property verified by DNS
336
+ is not visible to Typeroll. The
337
+ [launch checklist](https://typeroll.com/docs/guides/launch-checklist/)
338
+ explains every item.
339
+
295
340
  ## Definition of done (per site)
296
341
 
297
342
  - [ ] `get_migration_readiness source_url=<this market's old site>` → `ready: true`, warnings reviewed
@@ -299,6 +344,7 @@ new canonical build and is not an inventory-only operation):
299
344
  - [ ] Complete `verify_migration_urls` → 0 `missing`, 0 `broken_redirect`, 0 `error`, 0 untested variants
300
345
  - [ ] `record_migration_seo_acceptance` is accepted for the latest deploy
301
346
  - [ ] `get_migration_launch_report` → `launch_ready: true`
347
+ - [ ] `read_site_readiness` run before and after publication; every open item fixed or recorded with a reason
302
348
  - [ ] No source JavaScript copied; analytics/advertising via the Integrations app with the cookie banner on (`scripts.status: "clean"` in the launch report)
303
349
  - [ ] hreflang cluster written on both/all sides, absolute, final domains
304
350
  - [ ] `language` set on the site; `<html lang>` correct in the deployed HTML
@@ -151,6 +151,22 @@ batch_read_pages page_ids=[<2-3 representative ids>] # see actual conventions
151
151
  Don't skip this. Imposing a stranger's design on a customer's site is
152
152
  the biggest avoidable mistake.
153
153
 
154
+ ### 2b. Launch basics (ask early)
155
+
156
+ Ask the user for these once, early in the project. Read `read_site_settings` first and ask only for
157
+ what is missing. Each item is optional. Record a skipped item in
158
+ `decisions.md`, so the launch check in the last step holds no surprises.
159
+
160
+ - **Default share image.** Ask the user to choose or create the image that
161
+ social networks and chat apps show when a page has no image of its own:
162
+ upload one (`create_upload_url` or `upload_media_from_url`), pick an existing
163
+ one (`list_media`), or create one with `tr-imagegen`. Use a 1200×630 px PNG or
164
+ JPEG and set it as `default_og_image` with `update_site_settings`. Never
165
+ generate a title card from the page title instead. The user may skip this.
166
+ - **Organization.** The business name, logo and official profile URLs
167
+ (LinkedIn, Instagram, GitHub and similar). Set `organization` with `same_as`;
168
+ it becomes the home page's Organization schema.
169
+
154
170
  ### 3. Normalize shared data before importing page bodies
155
171
 
156
172
  Read taxonomy definitions and paginated terms from the helper's `/taxonomies`
@@ -343,11 +359,23 @@ Record `fidelity: { desktop, mobile, shared_data, integrations, evidence }` with
343
359
  The launch report requires this evidence in Core 0.2.5.
344
360
  Never mark an unavailable integration or uninspected screenshot as accepted.
345
361
 
346
- ### 7. Ship
362
+ ### 7. Launch and ship (required)
363
+
364
+ A site is not finished until this step has run. Call `read_site_readiness`
365
+ before the production publication and again after it. For every item with the
366
+ status `warning` or `suggestion`, either fix it with the tool its `fix` names,
367
+ or record in `decisions.md` (or the starter's `qa/` folder) why it stays open.
368
+ Tell the user which items remain. A missing default share image is only a
369
+ suggestion; leave it if the user chose to skip it.
370
+
371
+ The report warns and never blocks a publication. A clean report is not approval
372
+ to publish: publish only when the user has approved it.
347
373
 
348
374
  When the user signs off:
349
375
 
350
376
  ```
377
+ read_site_readiness
378
+
351
379
  # Bulk-publish drafts that look right
352
380
  batch_update_pages updates=[{page_id, patch:{status:"published"}}, ...]
353
381
 
@@ -356,6 +384,16 @@ trigger_deploy
356
384
  get_deploy_status job_id=<id> # poll
357
385
  ```
358
386
 
387
+ The readiness report does not replace `get_migration_launch_report`: URL
388
+ parity and SEO acceptance remain the migration's own launch gate.
389
+
390
+ Run `read_site_readiness` again after the publication. It reads saved content
391
+ and settings, so confirm the rest on the live site: the AI crawler zone check
392
+ needs the site's website host, and a Search Console property verified by DNS
393
+ is not visible to Typeroll. The
394
+ [launch checklist](https://typeroll.com/docs/guides/launch-checklist/)
395
+ explains every item.
396
+
359
397
  ## Pitfalls
360
398
 
361
399
  - **Don't publish during migration.** Always import as `draft`. Even
@@ -364,7 +402,7 @@ get_deploy_status job_id=<id> # poll
364
402
  have been served at `/2024/01/foo-bar/` due to the permalink
365
403
  structure. The full URL is what users see in Google; preserve that,
366
404
  not the bare slug.
367
- - **Image bandwidth.** R2 upload is metered. Use `find_pages_matching`
405
+ - **Image bandwidth.** R2 upload is metered. Use `search_pages`
368
406
  contains="<old-domain>" on already-imported content to spot images
369
407
  that weren't transferred.
370
408
  - **Plugin JavaScript.** Never copy scripts from plugins, the theme or
@@ -170,6 +170,22 @@ back silently, so verify fonts on the published address (`tr-brand` Step 7).
170
170
  `tr-brand`). Also set `settings.logo` to the uploaded brand mark — it feeds
171
171
  OG/schema even if the header uses a different lockup.
172
172
 
173
+ #### Launch basics (ask early)
174
+
175
+ Ask the user for these once, early in the project. Read `read_site_settings` first and ask only for
176
+ what is missing. Each item is optional. Record a skipped item in
177
+ `decisions.md`, so the launch check in the last step holds no surprises.
178
+
179
+ - **Default share image.** Ask the user to choose or create the image that
180
+ social networks and chat apps show when a page has no image of its own:
181
+ upload one (`create_upload_url` or `upload_media_from_url`), pick an existing
182
+ one (`list_media`), or create one with `tr-imagegen`. Use a 1200×630 px PNG or
183
+ JPEG and set it as `default_og_image` with `update_site_settings`. Never
184
+ generate a title card from the page title instead. The user may skip this.
185
+ - **Organization.** The business name, logo and official profile URLs
186
+ (LinkedIn, Instagram, GitHub and similar). Set `organization` with `same_as`;
187
+ it becomes the home page's Organization schema.
188
+
173
189
  Set `iframe_allowed_hosts` when customer content embeds a provider outside
174
190
  the built-in YouTube/Vimeo/Google Maps/Calendly set. Values are exact domain
175
191
  hostnames, never URLs or wildcards. Read the setting back before deciding that
@@ -397,15 +413,36 @@ Share the preview link. Iterate on feedback with `update_block` /
397
413
  `add_block` / `move_block` — that's the point of block mode: surgical
398
414
  edits, not full-page rewrites.
399
415
 
400
- ### 7. Deploy
416
+ ### 7. Launch (required)
417
+
418
+ A site is not finished until this step has run. Call `read_site_readiness`
419
+ before the production publication and again after it. For every item with the
420
+ status `warning` or `suggestion`, either fix it with the tool its `fix` names,
421
+ or record in `decisions.md` (or the starter's `qa/` folder) why it stays open.
422
+ Tell the user which items remain. A missing default share image is only a
423
+ suggestion; leave it if the user chose to skip it.
424
+
425
+ The report warns and never blocks a publication. A clean report is not approval
426
+ to publish: publish only when the user has approved it.
427
+
428
+ When the user approves, verify `read_publishing_readiness` and save the
429
+ intended Version before deploying. A published CMS status is not a live
430
+ deployment.
401
431
 
402
- When the user approves, verify read_publishing_readiness and save the intended
403
- Version before deploying. A published CMS status is not a live deployment.
404
432
  ```
433
+ read_site_readiness
405
434
  trigger_deploy
406
435
  get_deploy_status job_id=<id>
436
+ read_site_readiness
407
437
  ```
408
438
 
439
+ Run `read_site_readiness` again after the publication. It reads saved content
440
+ and settings, so confirm the rest on the live site: the AI crawler zone check
441
+ needs the site's website host, and a Search Console property verified by DNS
442
+ is not visible to Typeroll. The
443
+ [launch checklist](https://typeroll.com/docs/guides/launch-checklist/)
444
+ explains every item.
445
+
409
446
  ## Secondary path: HTML mode
410
447
 
411
448
  For migrated legacy pages or a hand-crafted one-off, `content_mode: