@typeroll/mcp-server 0.45.104 → 0.45.106

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { fail, ok } from './tools/helpers.js';
3
- export const COMPACT_INSTRUCTIONS = `Typeroll CMS. Use search_tools to find a task's tools, describe_tool to read one exact schema, then call_read_tool, call_write_tool or call_admin_tool. Discover before you write: identify Site/Version, capabilities and enabled app documentation. Read only the relevant skill or guide section; do not load the full manual by default. Larger changes: create_branch and keep the same version. Working copy, saved content and live output differ. Publishing and other external effects require the user's authorization; tool access alone is not approval.`;
3
+ export const COMPACT_INSTRUCTIONS = `Typeroll CMS. Use search_tools to find a task's tools, describe_tool to read one exact schema, then call_read_tool, call_write_tool or call_admin_tool. Discover before you write: identify Site/Version, capabilities and enabled app documentation. Read only the relevant skill or guide section; do not load the full manual by default. Larger changes: create_branch and keep the same version. Working copy, saved content and live output differ. Publishing and other external effects require the user's authorization; tool access alone is not approval. After cutover a deploy reaches the live domain: it needs the owner's approval and confirm_live_domains: true.`;
4
4
  /** Stable discovery surface: no dynamic registration or client list-change support needed. */
5
5
  export function compactTools(catalog) {
6
6
  const byName = new Map(catalog.map(tool => [tool.name, tool]));
package/dist/server.js CHANGED
@@ -190,6 +190,11 @@ The full playbook ships with this server — use it:
190
190
  schema_markup, create_site_and_plan) need the Organization's portal AI
191
191
  key for the Site. On ai_not_enabled don't retry: do the work yourself
192
192
  with the recipe named in start_workflow and tell the user you did.
193
+ 10. Publishing to the temporary address ends at cutover. Once a site serves
194
+ its live custom domain, every trigger_deploy is a production release: get
195
+ the owner's approval and pass confirm_live_domains: true
196
+ (get_publication_impact reports production and targets). Review with
197
+ get_preview_link include_working_copy instead of deploying.
193
198
 
194
199
  If anything here conflicts with what a tool returns, trust the tool. Every
195
200
  tool's own description carries its specifics.
@@ -6,15 +6,16 @@ function v(version) {
6
6
  export const deployTools = [
7
7
  {
8
8
  name: 'get_publication_impact',
9
- description: 'Read provisional net saved public-source changes against the selected version’s last verified publication. Reports additions, removals, shared inputs, metadata-only changes and unavailable baselines. Does not build or deploy. execution is automatic and reuse_verified is false before the build; inspect job.render_report afterwards for actual rendered/reused counts; source-level classification does not prove output reuse or cost savings.',
9
+ description: 'Read where a deploy lands and provisional net saved public-source changes against the selected version’s last verified publication. targets lists every public URL the publication serves (live domains first, then the temporary address); production: true means the site is cut over and a deploy publishes to live_domains. Reports additions, removals, shared inputs, metadata-only changes and unavailable baselines. Does not build or deploy. execution is automatic and reuse_verified is false before the build; inspect job.render_report afterwards for actual rendered/reused counts; source-level classification does not prove output reuse or cost savings.',
10
10
  inputSchema: { version: versionParam },
11
11
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'publishing/impact', v(args.version)))),
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. A domain switch waiting for DNS approval (awaiting_approval) does not block other sites.",
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. Publishing to the temporary address ends at cutover: once a site serves its live custom domain, the same publication serves both, so every deploy of main is a production release. Then get the owner's approval and pass confirm_live_domains: true; without it a real deploy is refused with live_domain_confirmation_required, naming the live URLs and targets. Review changes on a cut-over site with get_preview_link (include_working_copy) or dry_run:true, which need no confirmation. The result reports production, live_domains and targets (live domains first, then the temporary address). 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
+ confirm_live_domains: z.boolean().optional().describe('Required (true) for a real deploy of a cut-over site: acknowledges that the deploy publishes to the live domains named by get_publication_impact or the live_domain_confirmation_required error. Only after the site owner approved this production release. Not needed for dry_run or sites that are not cut over.'),
18
19
  allow_unsaved: z.boolean().optional().describe('Explicitly publish saved content while leaving the reported unsaved Pages/partials untouched. Only after owner confirmation.'),
19
20
  dry_run: z.boolean().optional().describe('Validate without publishing. Customer publishing checks frozen source only, including image links without an accessible name (publication_validation_failed); other adapters may build locally without upload.'),
20
21
  version: versionParam,
@@ -41,7 +42,7 @@ export const deployTools = [
41
42
  },
42
43
  {
43
44
  name: 'get_deploy_status',
44
- description: "Status of a single deploy job. Optional top-level build_progress reports the exact task status, attempt, step and completed/total files; not an overall percentage or ETA. Returns { job, queued_for_seconds, queue_timeout_seconds }. job.status is one of queued | running | succeeded | failed. On a failed build, job.error names the failed step and the cause the step printed, job.failure.code is the reported code (e.g. media_build_process_exit_1) and job.failure.diagnostic holds { cause, lines } (the last 40 lines that step printed, credentials removed). On the Cloudflare build engine job.failure.provider_log also holds { step, build_id, summary, lines } from the provider's build log (last 60 lines). Read them before retrying; retrying does not fix a cause that is in the content or the build engine. job.warnings, when present, lists non-blocking findings of a deploy that still ran, for example a media file left out of the publication because it could not be published safely (an SVG that could not be sanitized); report them to the user. Polling cadence: ~5s for queued/running. job.render_report, when present, reports actual HTML mode (full/partial), rendered, reused, total and removed route counts plus a fallback reason. From Core 0.2.17, custom blocks and consumed image metadata are tracked per route; older caches require one full build. It does not measure media or provider startup savings. job.coordinator, when present, reports the last completed CMS phase and duration_ms for that attempt; external build waiting is excluded. Keep observing the same job across checkpoints; do not trigger duplicate deploys. A job stuck in 'queued' for longer than queue_timeout_seconds (default 300s) auto-flips to failed with phase='queue_timeout' — so a single poll past that timestamp returns the terminal state and you can stop polling.",
45
+ description: "Status of a single deploy job. Optional top-level build_progress reports the exact task status, attempt, step and completed/total files; not an overall percentage or ETA. Returns { job, queued_for_seconds, queue_timeout_seconds }. job.targets and job.production, when present, record where the publication was admitted to publish and whether that included a live domain. job.status is one of queued | running | succeeded | failed. On a failed build, job.error names the failed step and the cause the step printed, job.failure.code is the reported code (e.g. media_build_process_exit_1) and job.failure.diagnostic holds { cause, lines } (the last 40 lines that step printed, credentials removed). On the Cloudflare build engine job.failure.provider_log also holds { step, build_id, summary, lines } from the provider's build log (last 60 lines). Read them before retrying; retrying does not fix a cause that is in the content or the build engine. job.warnings, when present, lists non-blocking findings of a deploy that still ran, for example a media file left out of the publication because it could not be published safely (an SVG that could not be sanitized); report them to the user. Polling cadence: ~5s for queued/running. job.render_report, when present, reports actual HTML mode (full/partial), rendered, reused, total and removed route counts plus a fallback reason. From Core 0.2.17, custom blocks and consumed image metadata are tracked per route; older caches require one full build. It does not measure media or provider startup savings. job.coordinator, when present, reports the last completed CMS phase and duration_ms for that attempt; external build waiting is excluded. Keep observing the same job across checkpoints; do not trigger duplicate deploys. A job stuck in 'queued' for longer than queue_timeout_seconds (default 300s) auto-flips to failed with phase='queue_timeout' — so a single poll past that timestamp returns the terminal state and you can stop polling.",
45
46
  inputSchema: { job_id: z.string(), include_provider: z.boolean().optional().describe('Also read the exact Git commit and Cloudflare build stages for a customer Git publication. Does not return build secrets.') },
46
47
  handler: withErrorBoundary(async (args, { client, siteId }) => {
47
48
  const res = await client.get(siteId, `deploys/${encodeURIComponent(args.job_id)}`);
@@ -24,9 +24,9 @@ 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, 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. After go-live, Typeroll re-checks who serves the website and redirect hosts at about 2, 10 and 30 minutes; next_action remove_other_host (with traffic_live false and a serving check) means another provider answers the host, typically a previous host\'s Cloudflare for SaaS hostname that outranks the customer\'s DNS: remove the domain from that host\'s dashboard, then check again. 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. After go-live, Typeroll re-checks who serves the website and redirect hosts at about 2, 10 and 30 minutes; next_action remove_other_host (with traffic_live false and a serving check) means another provider answers the host, typically a previous host\'s Cloudflare for SaaS hostname that outranks the customer\'s DNS: remove the domain from that host\'s dashboard, then check again. Live website hosts are then checked daily. A switch goes live only when the website host\'s Cloudflare Pages custom domain is active; otherwise it fails with pages_domain_not_active and automatic DNS is restored. Before approval, a check previous_host_saas:<host> (host_checks warning previous_host_saas_hostname, also in switch_summary.previous_host) means the current host (for example Kinsta) still holds the domain as a Cloudflare for SaaS hostname, which blocks Pages validation: tell the user to remove the domain there right before or right after the switch, inside the validation window. After go-live, next_action retry_validation (check pages_domain:<host>, traffic_live false) means the Pages custom domain is not active (pending or deactivated, visitors typically get HTTP 522): make sure the previous host released the domain, then call action retry_validation. It is idempotent: it re-checks the host and asks Cloudflare to retry validation (never a DNS change), at most once a minute. Previous website addresses (such as the temporary sites address) get a 301 through one Single Redirect rule per new website host in their zone; check reports host_checks warnings redirect_rule_quota (the zone\'s rule quota is full, Free plan: 10) or redirect_permission_missing with a remedy before approval. A refused write never blocks or rolls back the switch: the old address keeps noindex, previous_hosts records the issue code, and next_action repair_previous_host_redirects re-creates missing redirects without a build or DNS change once the remedy is applied (idempotent; safe to repeat). External DNS remains the caller’s responsibility.',
28
28
  inputSchema: {
29
- action: z.enum(['check', 'preview', 'save', 'prepare', 'approve', 'rollback']),
29
+ action: z.enum(['check', 'preview', 'save', 'prepare', 'approve', 'rollback', 'retry_validation', 'repair_previous_host_redirects']),
30
30
  revision: z.string().optional(), candidate_id: z.string().optional(),
31
31
  website_host: z.string().nullable().optional(), media_host: z.string().nullable().optional(), media_path_prefix: z.string().optional(),
32
32
  redirect_hosts: z.array(z.string()).max(4).optional(), dns_mode: z.enum(['automatic', 'external']).optional(),
@@ -215,7 +215,7 @@ export const domainTools = [
215
215
  },
216
216
  {
217
217
  name: 'approve_publishing_domain_cutover',
218
- description: 'Approve switching DNS to the current verified main publication, without another Git push, build or upload. Read the latest candidate_id; older receipts are rejected. Read publishing domains first and review DNS requirements, certificate readiness and redirect_preparations. Existing traffic is preserved until validation succeeds. With automatic DNS, redirect hosts switch after the website host: Typeroll ensures their zone redirect rule, then changes their reviewed record to a proxied placeholder. The publication goes live only after every redirect host answers 301 to the website host with the same path and query. When preparation.validation_blocker.resolution is accept_validation_window, Cloudflare can validate the website host only after its one existing record points at Pages: approving with accept_validation_window: true lets Typeroll change that record, and the host does not answer until Cloudflare validates it (Cloudflare does not document how long; previous_records holds the rollback). Get the user\'s explicit go-ahead for that window and choose a quiet time. External DNS management remains the caller’s responsibility.',
218
+ description: 'Approve switching DNS to the current verified main publication, without another Git push, build or upload. Read the latest candidate_id; older receipts are rejected. Read publishing domains first and review DNS requirements, certificate readiness and redirect_preparations. Existing traffic is preserved until validation succeeds. With automatic DNS, redirect hosts switch after the website host: Typeroll ensures their zone redirect rule, then changes their reviewed record to a proxied placeholder. The publication goes live only after every redirect host answers 301 to the website host with the same path and query. When preparation.validation_blocker.resolution is accept_validation_window, Cloudflare can validate the website host only after its one existing record points at Pages: approving with accept_validation_window: true lets Typeroll change that record, and the host does not answer until Cloudflare validates it (Cloudflare does not document how long; previous_records holds the rollback). Get the user\'s explicit go-ahead for that window and choose a quiet time. The website host goes live only when its Cloudflare Pages custom domain is active: Typeroll retries validation while it is pending or deactivated, and a domain that is blocked or not active by the deadline fails the switch with pages_domain_not_active (automatic DNS is restored). If host_checks warns previous_host_saas_hostname, the user must remove the domain from the previous host right before or right after approving. External DNS management remains the caller’s responsibility.',
219
219
  inputSchema: {
220
220
  revision: z.string(), candidate_id: z.string(),
221
221
  validation_timeout_minutes: z.number().int().min(5).max(45).optional(),
@@ -226,7 +226,7 @@ export const domainTools = [
226
226
  },
227
227
  {
228
228
  name: 'read_publishing_domains',
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. serving_check reports read-only follow-up checks about 2, 10 and 30 minutes after the website host went live: status pending, serving, inconclusive (a transport or edge error, never an alarm), other_host (issue code domain_served_by_other_host, with observed.provider_hint and remedy) or other_publication (domain_serving_other_publication). An alarm stands until a later check succeeds; reads re-check it. Typeroll never changes DNS or rolls back because of it.',
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. serving_check reports read-only follow-up checks about 2, 10 and 30 minutes after the website host went live: status pending, serving, inconclusive (a transport or edge error, never an alarm), other_host (issue code domain_served_by_other_host, with observed.provider_hint and remedy) or other_publication (domain_serving_other_publication). Checks continue daily while the host is live. serving_check.pages_domain is the latest read of the website host\'s Cloudflare Pages custom domain (status, validation, retried_at); serving_check.pages_issue (code pages_domain_not_active, with status, remedy and retryable) stands while it is not active, for example deactivated after a previous host\'s Cloudflare for SaaS hostname blocked validation. Checks retry validation automatically when the host points at the Pages project; to retry now, call guide_publishing_domain_change with action retry_validation. host_checks warning previous_host_saas_hostname means the current host still holds the domain as a Cloudflare for SaaS hostname: remove it there around the switch. An alarm stands until a later check succeeds; reads re-check it. Inconclusive reads never raise or clear an alarm. Typeroll never changes DNS or rolls back because of it.',
230
230
  inputSchema: {},
231
231
  handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'publishing/domains'))),
232
232
  },
@@ -3,6 +3,15 @@
3
3
  // check as the corresponding portal action.
4
4
  import { z } from 'zod';
5
5
  import { ok, withErrorBoundary } from './helpers.js';
6
+ /** Optional fields every site-creating tool accepts (POST /api/v1/sites and the create-and-* routes). */
7
+ export const newSiteFields = {
8
+ id: z.string().optional().describe('Optional site id: 3-48 lowercase letters, digits and "-" (e.g. "acme-studio"). Used exactly; 409 site_id_taken when the organization has it. Default: the kebab-case name.'),
9
+ language: z.string().optional().describe('Optional site content language as a BCP 47 tag (e.g. "en", "sv", "nl-BE"). Default "en". Drives <html lang>; change it later with update_site_settings.'),
10
+ };
11
+ /** The arguments that were given: an omitted optional field is not sent. */
12
+ export function definedFields(args) {
13
+ return Object.fromEntries(Object.entries(args).filter(([, value]) => value !== undefined));
14
+ }
6
15
  export const siteTools = [
7
16
  {
8
17
  name: 'read_organization_access',
@@ -26,17 +35,18 @@ export const siteTools = [
26
35
  },
27
36
  {
28
37
  name: 'create_site',
29
- description: "Create + bootstrap a NEW site in your org. Seeds default settings, a draft Home page, and a published header/footer so it renders immediately. Requires an ORG-scoped key (a site-scoped key is bound to one existing site and can't mint new ones; you'll get a 403). `name` drives a kebab-case site id; pass `domain` to kick off the \"point your DNS\" flow (never written as a live domain). Returns the new site's id + urls — use that id as `site_id`/`TYPEROLL_SITE_ID` for follow-up calls. After creating, run list_skills → read_skill tr-new-site to bootstrap the design.",
38
+ description: "Create + bootstrap a NEW site in your org. Seeds default settings, a draft Home page, and a published header/footer so it renders immediately. Requires an ORG-scoped key (a site-scoped key is bound to one existing site and can't mint new ones; you'll get a 403). Accepts exactly `name`, optional `id`, `domain` and `language`; any other field is refused. Without `id` the site id is the kebab-case `name` (with a random suffix when taken); with `id` that exact id is used, or 409 site_id_taken. `language` is the site's BCP 47 content language (e.g. \"nl-BE\"; default \"en\"), saved at creation so <html lang> is right from the first build. Pass `domain` to kick off the \"point your DNS\" flow (never written as a live domain). Returns the new site's id, language + urls — use that id as `site_id`/`TYPEROLL_SITE_ID` for follow-up calls. After creating, run list_skills → read_skill tr-new-site to bootstrap the design.",
30
39
  noSite: true,
31
40
  inputSchema: {
32
- name: z.string().min(1).describe('Display name. Slugified into the site id.'),
41
+ name: z.string().min(1).describe('Display name. Slugified into the site id unless `id` is given.'),
42
+ ...newSiteFields,
33
43
  domain: z
34
44
  .string()
35
45
  .optional()
36
46
  .describe('Optional real hostname e.g. "example.com". Starts DNS setup; not set live until DNS verifies.'),
37
47
  },
38
48
  handler: withErrorBoundary(async (args, { client }) => {
39
- const res = await client.rootPost('sites', { name: args.name, domain: args.domain });
49
+ const res = await client.rootPost('sites', definedFields(args));
40
50
  return ok(res);
41
51
  }),
42
52
  },
@@ -4,10 +4,14 @@
4
4
  // Site together with its first workflow.
5
5
  import { z } from 'zod';
6
6
  import { ok, versionParam, withErrorBoundary } from './helpers.js';
7
+ import { definedFields, newSiteFields } from './sites.js';
7
8
  // Run by the portal with the Organization's own AI key; without access for the
8
9
  // Site the run fails with failure_code ai_not_enabled. The agent then does the
9
10
  // work itself with the named recipe instead of retrying.
10
11
  const AI_WORKFLOWS = 'AI workflows — site_planning, content_generation, content_improvement and schema_markup — call the model with the Organization\'s own AI key and need AI access for this Site; without it the run fails with failure_code "ai_not_enabled" (failure_reason: the AI assistant is not enabled for this site). Do not retry or work around it: you are an AI agent, so do the same work yourself and tell the user you did — site_planning: tr-new-site (propose the page structure, get approval, then build); content_generation: tr-content-write (draft page); content_improvement: tr-content-write on the weakest published pages (thin text, missing seo_title/seo_description, stale), as unsaved drafts for review; schema_markup: tr-seo step 5 (add_faq_schema, add_recipe_schema, add_event_schema or json_ld). seo_audit still runs without AI and returns its issue list unprioritized. Enabling the portal AI is the Organization owner\'s or admin\'s choice, never a prerequisite for you.';
12
+ // Runs execute in the portal process; a restart (a deploy) stops them. Reads
13
+ // close a run with no heartbeat for 10 minutes as workflow_interrupted.
14
+ const INTERRUPTED = 'failure_code "workflow_interrupted" means the run stopped making progress (no heartbeat for over 10 minutes, usually because the service restarted) and will not continue: tell the user, then start_workflow again; work saved before it stopped stays saved. A run that is still running keeps a recent heartbeat_at, however slowly it progresses.';
11
15
  const WORKFLOW_TYPES = [
12
16
  'migration', 'site_planning', 'seo_audit', 'content_improvement', 'link_check',
13
17
  'performance_audit', 'content_generation', 'schema_markup', 'url_parity', 'rebuild_deploy',
@@ -15,13 +19,13 @@ const WORKFLOW_TYPES = [
15
19
  export const workflowTools = [
16
20
  {
17
21
  name: 'list_workflows',
18
- description: 'List the workflow types this Site can run (label, description, steps, required_permission) and its most recent runs with status. Read only. Migration, site planning and URL parity pause for review; check get_workflow for paused_for_review. Site planning, content generation, content improvement and schema markup need the Organization\'s AI key for this Site (see start_workflow).',
22
+ description: 'List the workflow types this Site can run (label, description, steps, required_permission) and its most recent runs with status. Read only. Migration, site planning and URL parity pause for review; check get_workflow for paused_for_review. A run left running after a service restart is reported failed with failure_code workflow_interrupted. Site planning, content generation, content improvement and schema markup need the Organization\'s AI key for this Site (see start_workflow).',
19
23
  inputSchema: { limit: z.number().int().min(1).max(100).optional() },
20
24
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'workflows', args.limit ? { limit: args.limit } : undefined))),
21
25
  },
22
26
  {
23
27
  name: 'start_workflow',
24
- description: 'Start a server-side workflow on this Site; it runs in the background and returns workflow_id. Poll get_workflow. Config per type (same fields as the portal form): migration { wp_url, helper_api_key? } (needs verified organization import storage); site_planning { business_description }; content_generation { topic, tone?, audience? }; content_improvement { limit? }; url_parity { target_origin?, source_origin?, check_source?, statuses? }; rebuild_deploy { environment: staging | production }; seo_audit, link_check, performance_audit and schema_markup take none. Needs write permission; rebuild_deploy publishes and needs admin. Workflows that change content write to the given version (default main) — get the user\'s agreement first. ' + AI_WORKFLOWS,
28
+ description: 'Start a server-side workflow on this Site; it runs in the background and returns workflow_id. Poll get_workflow. Config per type (same fields as the portal form): migration { wp_url, helper_api_key? } (needs verified organization import storage); site_planning { business_description }; content_generation { topic, tone?, audience? }; content_improvement { limit? }; url_parity { target_origin?, source_origin?, check_source?, statuses? }; rebuild_deploy { environment: staging | production, confirm_live_domains? } (production on a cut-over site publishes to its live domain and needs confirm_live_domains: true after owner approval); seo_audit, link_check, performance_audit and schema_markup take none. Needs write permission; rebuild_deploy publishes and needs admin. Workflows that change content write to the given version (default main) — get the user\'s agreement first. ' + AI_WORKFLOWS,
25
29
  inputSchema: {
26
30
  type: z.enum(WORKFLOW_TYPES),
27
31
  config: z.record(z.string(), z.unknown()).optional(),
@@ -31,7 +35,7 @@ export const workflowTools = [
31
35
  },
32
36
  {
33
37
  name: 'get_workflow',
34
- description: 'Read one workflow run: status (pending | running | paused_for_review | completed | failed), current step, progress, log, review_message/review_data when paused, results, failure_reason and failure_code (newer Core; older Core reports failure_reason only). Credential config is masked. failure_code "ai_not_enabled" (or a failure_reason saying the AI assistant is not enabled for this site) means the Site has no portal AI access: do the work yourself with the recipe named in start_workflow instead of retrying.',
38
+ description: 'Read one workflow run: status (pending | running | paused_for_review | completed | failed | cancelled), current step, progress, log, review_message/review_data when paused, results, failure_reason and failure_code (newer Core; older Core reports failure_reason only). Credential config is masked. failure_code "ai_not_enabled" (or a failure_reason saying the AI assistant is not enabled for this site) means the Site has no portal AI access: do the work yourself with the recipe named in start_workflow instead of retrying. ' + INTERRUPTED,
35
39
  inputSchema: { workflow_id: z.string() },
36
40
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, `workflows/${encodeURIComponent(args.workflow_id)}`))),
37
41
  },
@@ -41,25 +45,33 @@ export const workflowTools = [
41
45
  inputSchema: { workflow_id: z.string() },
42
46
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, `workflows/${encodeURIComponent(args.workflow_id)}/approve`))),
43
47
  },
48
+ {
49
+ name: 'cancel_workflow',
50
+ description: 'Cancel a run that is pending, running or paused_for_review; it is recorded as status cancelled and continues no further (a step already in flight may finish its current work, but nothing more is recorded; changes it already saved stay). Use it when the user wants to stop a run or start over. Needs the permission that starting the type needs (write; admin for rebuild_deploy). A finished run (completed, failed, cancelled) returns 409. Returns the cancelled run.',
51
+ inputSchema: { workflow_id: z.string() },
52
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, `workflows/${encodeURIComponent(args.workflow_id)}/cancel`))),
53
+ },
44
54
  {
45
55
  name: 'create_site_and_migrate',
46
56
  noSite: true,
47
- description: 'Create a new Site in the Organization and start a WordPress migration into it, like "Migrate from WordPress" on the portal\'s New site page. Requires an organization API key and verified organization import storage (409 import_storage_required otherwise — nothing is created). Returns site and workflow ids; poll get_workflow with that site_id. Run get_migration_readiness on the new Site if you need the preflight first.',
57
+ description: 'Create a new Site in the Organization and start a WordPress migration into it, like "Migrate from WordPress" on the portal\'s New site page. Requires an organization API key and verified organization import storage (409 import_storage_required otherwise — nothing is created). Returns site and workflow ids; poll get_workflow with that site_id. Run get_migration_readiness on the new Site if you need the preflight first. Optional `id` and `language` work as in create_site; any other field is refused.',
48
58
  inputSchema: {
49
- name: z.string().min(1).describe('Site display name; slugified into the Site id.'),
59
+ name: z.string().min(1).describe('Site display name; slugified into the Site id unless `id` is given.'),
60
+ ...newSiteFields,
50
61
  wp_url: z.string().url().describe('Public WordPress address, e.g. https://oldsite.example.'),
51
62
  helper_api_key: z.string().optional().describe('Typeroll Helper plugin key from the WordPress admin, if installed. Stored for the run and never returned.'),
52
63
  },
53
- handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('sites/create-and-migrate', args))),
64
+ handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('sites/create-and-migrate', definedFields(args)))),
54
65
  },
55
66
  {
56
67
  name: 'create_site_and_plan',
57
68
  noSite: true,
58
- description: 'Create a new Site in the Organization and start AI site planning from a business description, like "Plan a new site with AI" on the New site page. Requires an organization API key. Needs the Organization\'s own AI key with site access "all" (read_organization_ai_settings); otherwise it answers 403 ai_not_enabled and creates nothing — then create_site a blank Site and plan it yourself with tr-new-site (propose the pages, get the user\'s approval, build), and say that you did; enabling the portal AI is the owner\'s or admin\'s choice, not a prerequisite. The plan pauses for review: read it with get_workflow and approve_workflow only after the user agrees.',
69
+ description: 'Create a new Site in the Organization and start AI site planning from a business description, like "Plan a new site with AI" on the New site page. Requires an organization API key. Needs the Organization\'s own AI key with site access "all" (read_organization_ai_settings); otherwise it answers 403 ai_not_enabled and creates nothing — then create_site a blank Site and plan it yourself with tr-new-site (propose the pages, get the user\'s approval, build), and say that you did; enabling the portal AI is the owner\'s or admin\'s choice, not a prerequisite. The plan pauses for review: read it with get_workflow and approve_workflow only after the user agrees. Optional `id` and `language` work as in create_site; any other field is refused.',
59
70
  inputSchema: {
60
- name: z.string().min(1),
71
+ name: z.string().min(1).describe('Site display name; slugified into the Site id unless `id` is given.'),
72
+ ...newSiteFields,
61
73
  business_description: z.string().min(1).describe('What the business does, for whom, and what sets it apart.'),
62
74
  },
63
- handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('sites/create-and-plan', args))),
75
+ handler: withErrorBoundary(async (args, { client }) => ok(await client.rootPost('sites/create-and-plan', definedFields(args)))),
64
76
  },
65
77
  ];
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.104';
11
+ export const VERSION = '0.45.106';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
3
  "mcpName": "io.github.Typeroll/typeroll",
4
- "version": "0.45.104",
4
+ "version": "0.45.106",
5
5
  "description": "Typeroll CMS MCP server \u2013 connect AI agents to the REST API to manage content and publish static websites.",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -67,6 +67,10 @@ section. A Page template is `update_page` `template` (an allowed ID, or null
67
67
  for the content type default). `discard_working_copy` throws an unsaved draft
68
68
  away. Publishing means saving the draft, `update_page` `status: "published"`
69
69
  (applies immediately) and, when the user wants it live now, `trigger_deploy`.
70
+ On a site that serves its live custom domain (`get_publication_impact`
71
+ reports `production: true`), every deploy is a production release: get the
72
+ owner's approval and pass `confirm_live_domains: true`. Review with
73
+ `get_preview_link` `include_working_copy: true` instead of deploying.
70
74
 
71
75
  **Publishing and failed deploys.** Poll `get_deploy_status` until the job is
72
76
  `succeeded` or `failed`. Every site in an organization shares one build
@@ -100,10 +100,10 @@ you when you're done, and it stops you discovering an untouched 400-URL blog
100
100
  in week three.
101
101
 
102
102
  For each source site, create the target site first (the inventory lives on
103
- it):
103
+ it), in its language so `<html lang>` is right from the first build:
104
104
 
105
105
  ```
106
- create_site name="Example DE"
106
+ create_site name="Example DE" id="example-de" language="de-DE"
107
107
  ```
108
108
 
109
109
  Then walk the source and post what you find:
@@ -308,6 +308,14 @@ new canonical build and is not an inventory-only operation):
308
308
  6. Rerun the complete, unfiltered URL check and SEO review, then require
309
309
  `get_migration_launch_report` to return `launch_ready: true`
310
310
 
311
+ Publishing to the temporary address ends at each site's cutover. From then on
312
+ one publication serves both the live domain and the old fallback address, so
313
+ every `trigger_deploy` of that site is a production release: review with
314
+ `get_preview_link` (`include_working_copy: true`) or `dry_run: true`, get the
315
+ owner's approval, and pass `confirm_live_domains: true`. Without it the deploy
316
+ is refused with `live_domain_confirmation_required`, naming the live URLs.
317
+ `get_publication_impact` reports `production` and `targets` per site.
318
+
311
319
  ## Phase 8 — Launch (required)
312
320
 
313
321
  A site is not finished until this step has run. Call `read_site_readiness`
@@ -394,6 +394,22 @@ is not visible to Typeroll. The
394
394
  [launch checklist](https://typeroll.com/docs/guides/launch-checklist/)
395
395
  explains every item.
396
396
 
397
+ ## After cutover: every deploy is production
398
+
399
+ Publishing to the temporary address ends at cutover. Once the real domain
400
+ serves the site, one publication serves both the live domain and the old
401
+ `site-*.sites` address, so a deploy you think of as "temporary" changes the
402
+ live website.
403
+
404
+ - Review changes with `get_preview_link` (`include_working_copy: true`) or
405
+ `trigger_deploy dry_run=true`. Neither publishes or needs confirmation.
406
+ - Treat every real `trigger_deploy` as a production release: get the owner's
407
+ approval for it, then pass `confirm_live_domains: true`.
408
+ - `get_publication_impact` and the deploy result report `production` and
409
+ `targets` (live domains first, then the temporary address). Without the
410
+ acknowledgement a deploy is refused with `live_domain_confirmation_required`,
411
+ naming the live URLs. Show them to the owner; never add the flag on your own.
412
+
397
413
  ## Pitfalls
398
414
 
399
415
  - **Don't publish during migration.** Always import as `draft`. Even