@typeroll/mcp-server 0.45.40 → 0.45.42

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.
@@ -60,7 +60,8 @@ export const formTools = [
60
60
  fields: z.array(fieldSchema).min(1).optional(),
61
61
  steps: z.array(stepSchema).min(1).optional(),
62
62
  submit_text: z.string().optional(),
63
- success_message: z.string().optional(),
63
+ success_message: z.string().optional().describe('Shown after the final step. May contain basic HTML (links, emphasis); it is sanitized.'),
64
+ success_redirect_url: z.string().optional().describe('After the final step, send the visitor to this absolute http(s) URL or root-relative path (for example a booking or thank-you page) instead of showing success_message. Pass an empty string to clear.'),
64
65
  target: targetSchema,
65
66
  },
66
67
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -81,7 +82,8 @@ export const formTools = [
81
82
  fields: z.array(fieldSchema).optional(),
82
83
  steps: z.array(stepSchema).optional(),
83
84
  submit_text: z.string().optional(),
84
- success_message: z.string().optional(),
85
+ success_message: z.string().optional().describe('Shown after the final step. May contain basic HTML (links, emphasis); it is sanitized.'),
86
+ success_redirect_url: z.string().optional().describe('After the final step, send the visitor to this absolute http(s) URL or root-relative path (for example a booking or thank-you page) instead of showing success_message. Pass an empty string to clear.'),
85
87
  target: targetSchema,
86
88
  }),
87
89
  },
@@ -3,9 +3,8 @@
3
3
  // (override with `version`).
4
4
  //
5
5
  // The AI agent uses these to author block-mode pages directly. For
6
- // HTML-mode pages, `convert_page_to_blocks` is the bridge — call with
7
- // `dry_run: true` first to preview, then `dry_run: false, switch_mode:
8
- // true` to commit.
6
+ // HTML-mode pages, `convert_page_to_blocks` returns a preview only.
7
+ // It never writes. A person accepts that preview in the page editor.
9
8
  import { z } from 'zod';
10
9
  import { ok, withErrorBoundary, versionParam } from './helpers.js';
11
10
  const blockSchema = z.object({
@@ -194,32 +193,26 @@ export const pageBlockTools = [
194
193
  },
195
194
  {
196
195
  name: 'set_page_mode',
197
- description: 'Safely switch a page\'s content_mode between "blocks" and "html". Always snapshots a revision before the flip so the previous state is restorable. HTML → blocks with `convert: true` also runs the heuristic converter so the page starts with a populated tree. Blocks → HTML drops the block tree (the revision retains it). Prefer this over `update_page patch={content_mode}` because that bypasses snapshotting.',
196
+ description: 'Safely switch a page\'s content_mode between "blocks" and "html". Always snapshots a revision before the flip so the previous state is restorable. HTML → blocks does not convert or rewrite the body. Blocks → HTML drops the block tree (the revision retains it). Prefer this over `update_page patch={content_mode}` because that bypasses snapshotting. Automatic HTML-to-blocks conversion is preview-only; a person accepts it in the page editor.',
198
197
  inputSchema: {
199
198
  page_id: z.string(),
200
199
  to: z.enum(['blocks', 'html']).describe('Target mode.'),
201
- convert: z.boolean().optional().describe('When going to blocks: run the htmlToBlocks heuristic on html_content. Ignored for blocks → html.'),
202
200
  version: versionParam,
203
201
  },
204
202
  handler: withErrorBoundary(async (args, { client, siteId }) => {
205
- const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/mode`, { to: args.to, convert: args.convert ?? false }, v(args.version));
203
+ const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/mode`, { to: args.to }, v(args.version));
206
204
  return ok(res);
207
205
  }),
208
206
  },
209
207
  {
210
208
  name: 'convert_page_to_blocks',
211
- description: 'Heuristically convert a page\'s html_content into a block tree. By default runs as dry_run and returns the proposed blocks without writing. Pass dry_run: false to apply, and switch_mode: true to flip content_mode to "blocks" so the renderer uses the new tree. For a safer one-call switch with revision snapshot, use `set_page_mode` with `convert: true` instead.',
209
+ description: 'Preview a heuristic conversion of a page\'s html_content into a block tree. Always returns the proposal, including unconverted text, classes and markup, and does not write. A person accepts the preview in the page editor. This tool cannot change content_mode or replace the body.',
212
210
  inputSchema: {
213
211
  page_id: z.string(),
214
- dry_run: z.boolean().optional().describe('Default true. When true, returns the proposed tree without writing.'),
215
- switch_mode: z.boolean().optional().describe('When applying (dry_run=false), flip content_mode to "blocks". Default false.'),
216
212
  version: versionParam,
217
213
  },
218
214
  handler: withErrorBoundary(async (args, { client, siteId }) => {
219
- const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/blocks/convert`, {
220
- dry_run: args.dry_run ?? true,
221
- switch_mode: args.switch_mode ?? false,
222
- }, v(args.version));
215
+ const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/blocks/convert`, { dry_run: true }, v(args.version));
223
216
  return ok(res);
224
217
  }),
225
218
  },
@@ -56,15 +56,14 @@ export const partialTools = [
56
56
  },
57
57
  {
58
58
  name: 'set_partial_mode',
59
- description: 'Switch a header, footer, or free partial between HTML and native blocks through the revision-safe mode endpoint. To author a native partial deterministically: update_partial with blocks + save:true, then set_partial_mode to="blocks", then read_partial and verify the returned mode/tree. Set convert:true only for heuristic HTML-to-block conversion.',
59
+ description: 'Switch a header, footer, or free partial between HTML and native blocks through the revision-safe mode endpoint. To author a native partial deterministically: update_partial with blocks + save:true, then set_partial_mode to="blocks", then read_partial and verify the returned mode/tree. This does not convert HTML into blocks.',
60
60
  inputSchema: {
61
61
  partial_id: z.string(),
62
62
  to: z.enum(['blocks', 'html']),
63
- convert: z.boolean().optional(),
64
63
  version: versionParam,
65
64
  },
66
65
  handler: withErrorBoundary(async (args, { client, siteId }) => {
67
- const res = await client.post(siteId, `partials/${encodeURIComponent(args.partial_id)}/mode`, { to: args.to, convert: args.convert ?? false }, v(args.version));
66
+ const res = await client.post(siteId, `partials/${encodeURIComponent(args.partial_id)}/mode`, { to: args.to }, v(args.version));
68
67
  return ok(res);
69
68
  }),
70
69
  },
@@ -6,7 +6,7 @@ function v(version) {
6
6
  export const searchTools = [
7
7
  {
8
8
  name: 'search_pages',
9
- description: 'Search page bodies by literal substring or regex. Returns up to 500 matches each with an excerpt around the first hit. Use this to scope a redesign or a bulk replacement before running it. ' +
9
+ description: 'Search saved page content by literal substring or regex: block data in block-mode pages, html_content in HTML-mode pages, and content-type text fields (the same text bulk_replace_text edits). Returns up to 500 matches, each with the matching field (blocks, html_content or fields.<name>) and an excerpt around the first hit. Use this to scope a redesign or a bulk replacement before running it. ' +
10
10
  'Pass either "contains" (case-insensitive literal) or "regex" (JS regex source without slashes) — not both. ' +
11
11
  'Example: search_pages({"contains": "kontakta oss"}) finds every page mentioning that phrase. ' +
12
12
  'Example: search_pages({"regex": "\\\\d{3}-\\\\d{3}"}) finds pages with phone-number patterns.',
@@ -53,7 +53,7 @@ export const settingsTools = [
53
53
  // Scriptable surfaces. Trusted because the caller has an API key.
54
54
  scripts_head: z.string().optional().describe('Raw HTML injected into <head> on every page. Use for analytics, fonts, third-party CSS links.'),
55
55
  scripts_body_end: z.string().optional().describe('Raw HTML injected just before </body> on every page. Use for chat widgets, deferred analytics.'),
56
- custom_css: z.string().optional().describe('Global CSS shipped in <style> at the end of <head>. Lets you define site-wide design tokens (CSS variables, @media queries, :hover states) without inlining on every element.'),
56
+ custom_css: z.string().optional().describe('Global CSS in a <style> in <head>, after block CSS and before the page\'s own custom CSS and the template base stylesheet (reset + global). Base rules for :root tokens (spacing, radius, container widths), body font and line-height load after it, so override those with a more specific selector (for example html:root or body.page) rather than a bare :root/body rule. Preview uses the same order. Lets you define site-wide design tokens (CSS variables, @media queries, :hover states) without inlining on every element.'),
57
57
  colors: z.record(z.string()).optional(),
58
58
  fonts: z.record(z.unknown()).optional(),
59
59
  contact: z
@@ -12,7 +12,7 @@ export const versionTools = [
12
12
  },
13
13
  {
14
14
  name: 'create_branch',
15
- description: "Create a copy-on-write branch from main (or the version passed in `base`) — the RECOMMENDED first step for any larger or experimental change (redesigns, multi-page edits, trying a new design direction). Work lands on the branch, never on the live main version, until you merge_branch it. New branches default to robots_blocked:true (a half-finished design can't be indexed) and get their own deploy URL ({branch}.{project}.pages.dev) for stakeholder review. Pass the returned id as ?version= on every subsequent read/write. When in doubt, branch — it's cheap and keeps the live site safe. The tr-redesign-branch skill (read_skill) walks the full flow.",
15
+ description: "Create a copy-on-write branch from main (or the version passed in `base`) — the RECOMMENDED first step for any larger or experimental change (redesigns, multi-page edits, trying a new design direction). Work lands on the branch, never on the live main version, until you merge_branch it. New branches default to robots_blocked:true (a half-finished design can't be indexed) and get their own stable deploy address for stakeholder review: after the branch's first trigger_deploy, read it from deploy_url in list_versions or read_version (the publishing setup decides the host: under a Hosting Group it is a v-… host under that group's site address base, otherwise the address the host reported; never construct it yourself). Pass the returned id as ?version= on every subsequent read/write. When in doubt, branch — it's cheap and keeps the live site safe. The tr-redesign-branch skill (read_skill) walks the full flow.",
16
16
  inputSchema: {
17
17
  name: z.string().min(1),
18
18
  base: z.string().optional().describe('Source version id; defaults to main.'),
@@ -8,6 +8,7 @@
8
8
  // commit_working_copy → SAVE (same op as the editor's
9
9
  // Save button)
10
10
  // discard_working_copy → throw the draft away
11
+ // list/read/restore_page_revision → saved-state history (undo)
11
12
  //
12
13
  // The portal editor shows any agent-written draft as "Unsaved changes", so
13
14
  // the human can also Save or Discard it from the UI.
@@ -72,4 +73,47 @@ export const workingCopyTools = [
72
73
  return ok(res);
73
74
  }),
74
75
  },
76
+ {
77
+ name: 'list_page_revisions',
78
+ description: "Saved-state history of a page on this version, newest first. Every save snapshots the page as it was BEFORE the save, so each entry is a state you can go back to. Returns { revisions: [{ id, created_at, created_by, note, title, content_mode, date_updated }], total }. Use read_page_revision for the full document and restore_page_revision to undo.",
79
+ inputSchema: {
80
+ page_id: z.string(),
81
+ limit: z.number().int().min(1).max(100).optional().describe('Newest N entries (default 50).'),
82
+ version: versionParam,
83
+ },
84
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
85
+ const query = { ...(v(args.version) ?? {}) };
86
+ if (args.limit)
87
+ query.limit = String(args.limit);
88
+ const res = await client.get(siteId, `pages/${encodeURIComponent(args.page_id)}/revisions`, query);
89
+ return ok(res);
90
+ }),
91
+ },
92
+ {
93
+ name: 'read_page_revision',
94
+ description: 'Read one page revision with the full saved document (title, blocks or html_content, SEO fields, fields, …) — compare it with read_page before restoring.',
95
+ inputSchema: {
96
+ page_id: z.string(),
97
+ revision_id: z.string(),
98
+ version: versionParam,
99
+ },
100
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
101
+ const res = await client.get(siteId, `pages/${encodeURIComponent(args.page_id)}/revisions/${encodeURIComponent(args.revision_id)}`, v(args.version));
102
+ return ok(res);
103
+ }),
104
+ },
105
+ {
106
+ name: 'restore_page_revision',
107
+ description: "Restore a page revision's content into the page's DRAFT (replacing the current draft, like replace_page). Pass save:true to save it at once; the save snapshots the current page first, so the restore is itself undoable. Publication status and schedule are kept. A revision from the other content mode is refused (409) — switch mode first. Returns { restored_revision, saved, has_unsaved_changes, staged_fields, … }.",
108
+ inputSchema: {
109
+ page_id: z.string(),
110
+ revision_id: z.string(),
111
+ save: z.boolean().optional().describe('Save immediately (default false: leave it as an unsaved draft to preview).'),
112
+ version: versionParam,
113
+ },
114
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
115
+ const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/revisions/${encodeURIComponent(args.revision_id)}/restore`, { save: args.save === true }, v(args.version));
116
+ return ok(res);
117
+ }),
118
+ },
75
119
  ];
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.40';
11
+ export const VERSION = '0.45.42';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.45.40",
3
+ "version": "0.45.42",
4
4
  "description": "Typeroll CMS MCP server – connect AI agents to the REST API to manage content and publish static websites.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,6 +35,11 @@ shell or paste a token into page HTML.
35
35
  }
36
36
  ```
37
37
 
38
+ `success_message` may contain basic sanitized HTML, such as a link to the next
39
+ step. To send the visitor somewhere after the final step instead, set
40
+ `success_redirect_url` to a root-relative path (`/tack/`) or an `http(s)` URL,
41
+ for example a booking page. Other schemes are rejected; `""` clears it.
42
+
38
43
  Field names must match `[a-z][a-z0-9_-]*`. Labels may be localized. Supported
39
44
  simple types include `text`, `email`, `tel`, `url`, `number`, `textarea`,
40
45
  `select`, `radio`, `checkbox`, `hidden`, and `gdpr_consent`.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tr-migrate-wp
3
- description: Use when the user asks to migrate a WordPress site to Typeroll, mentions wp-json, or names a WP source URL. Walks the WP REST API, preserves content and shared references in editable blocks, transfers media, sets redirects, leaves everything as drafts for human review.
3
+ description: Use when the user asks to migrate a WordPress site to Typeroll, mentions wp-json, or names a WP source URL. Walks the WP REST API, preserves cleaned HTML and shared references, transfers media, sets redirects, and leaves everything as drafts for human review. Do not apply automatic HTML-to-blocks conversion.
4
4
  ---
5
5
 
6
6
  # Migrate from WordPress to Typeroll
@@ -140,6 +140,8 @@ c. Migrate referenced images with `upload_media_from_url`. The customer's
140
140
  media reference only after verification. Stop and report a failed required
141
141
  transfer; do not silently hotlink the WordPress source.
142
142
 
143
+ The managed importer stores cleaned HTML and does not run HTML-to-blocks conversion. Do not call `convert_page_to_blocks` or `set_page_mode` to rewrite that HTML. A person previews and accepts a conversion in the page editor.
144
+
143
145
  d. Preserve exact text, headings, anchors, links, media and semantic structure with native blocks first. Read the
144
146
  available block types and map headings, prose, images, buttons and layout
145
147
  into their typed fields. Use HTML mode only for source-specific markup that
@@ -335,8 +335,10 @@ For migrated legacy pages or a hand-crafted one-off, `content_mode:
335
335
  - HTML-mode bodies are container-constrained; full-bleed requires the
336
336
  negative-margin escape (`margin-left: calc(50% - 50vw); width: 100vw`)
337
337
  — never combine with `overflow-x: clip` on a wrapper.
338
- - `set_page_mode` flips a page between modes;
339
- `convert_page_to_blocks` does a heuristic HTML→blocks conversion.
338
+ - `set_page_mode` flips a page between modes and does not convert HTML.
339
+ `convert_page_to_blocks` previews a heuristic conversion, including
340
+ what it could not convert, and does not write. A person accepts that
341
+ preview in the page editor.
340
342
 
341
343
  ## Pitfalls
342
344
 
@@ -230,11 +230,15 @@ link for your own verification:
230
230
  yourself) see a change.
231
231
  - **When they want the COMPILED static site** (a permanent bookmark, a
232
232
  stakeholder link to the built output, or a final pre-merge check): deploy
233
- the branch once (`trigger_deploy version="<branch>"`) and share the stable
234
- alias `https://<branch>.<project>.pages.dev` (the `<project>` is the part
235
- after the hash in the returned `deploy_url`). Branch deploys are
236
- `robots_blocked`, so it won't be indexed. The immutable per-deploy
237
- `<hash>.pages.dev` is for your own one-off checks (a new hash each deploy).
233
+ the branch once (`trigger_deploy version="<branch>"`) and share its stable
234
+ address: `deploy_url` from `list_versions` / `read_version` once the deploy
235
+ has succeeded. The publishing setup decides the host: under a Hosting Group
236
+ it is a `v-…` host under that group's site address base (which need not be
237
+ the site's own domain); otherwise it is the address the host reported. Never
238
+ construct it yourself, and do not guess a `<branch>.<project>.pages.dev`
239
+ alias. Branch deploys are `robots_blocked`, so the
240
+ address won't be indexed. A per-deploy `<hash>.pages.dev` URL in the job
241
+ details is for your own one-off checks (a new hash each deploy).
238
242
 
239
243
  Default: review/iterate on the reused DB-live preview; deploy only for the
240
244
  compiled output or merge. Wait for an explicit "looks good, ship it."