@typeroll/mcp-server 0.45.42 → 0.45.43

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
@@ -16,6 +16,8 @@ import { contentTypeTools } from './tools/content-types.js';
16
16
  import { pageTemplateTools } from './tools/page-templates.js';
17
17
  import { mediaTools } from './tools/media.js';
18
18
  import { redirectTools } from './tools/redirects.js';
19
+ import { styleTools } from './tools/styles.js';
20
+ import { reusableBlockTools } from './tools/reusable-blocks.js';
19
21
  import { migrationTools } from './tools/migration.js';
20
22
  import { formTools } from './tools/forms.js';
21
23
  import { searchTools } from './tools/search.js';
@@ -113,7 +115,17 @@ The full playbook ships with this server — use it:
113
115
  5. Pages default to block mode. Build with add_block/update_block; make layouts
114
116
  responsive per breakpoint with set_block_responsive (grid columns, icon-box
115
117
  layout, … take { mobile, tablet, laptop, desktop, wide } values).
116
- 6. No site yet? With an org-scoped key, create_site bootstraps one.
118
+ 6. Appearance lives in named styles: list_styles before styling, reuse a
119
+ style through a block's style_id, and create_style for any look that could
120
+ recur. Use one-off block values only for something clearly unique, never
121
+ classed raw HTML. Text must meet WCAG AA contrast. A heading's small label
122
+ is its eyebrow field. For what styles cannot express, use page or site
123
+ custom_css on s-<style> or block custom_class selectors, not [data-block].
124
+ 7. Reuse: content that must stay identical on many pages is a global block
125
+ (make_block_global; core/global_block references it). A section people
126
+ start from and then adapt is a block template (list_block_templates,
127
+ insert_block_template copies it). Never duplicate shared content by hand.
128
+ 8. No site yet? With an org-scoped key, create_site bootstraps one.
117
129
 
118
130
  If anything here conflicts with what a tool returns, trust the tool. Every
119
131
  tool's own description carries its specifics.
@@ -142,6 +154,8 @@ export function buildServer(options) {
142
154
  ...pageTemplateTools,
143
155
  ...mediaTools,
144
156
  ...redirectTools,
157
+ ...styleTools,
158
+ ...reusableBlockTools,
145
159
  ...migrationTools,
146
160
  ...formTools,
147
161
  ...settingsTools,
@@ -89,7 +89,7 @@ export const pageBlockTools = [
89
89
  },
90
90
  {
91
91
  name: 'update_block',
92
- description: 'Update a block\'s data fields (shallow merge), style overrides, or responsive settings. Works on pages, partials, and page templates — use `target` to address non-page containers. Does not modify children or slots; use move/add/remove for structural changes.',
92
+ description: 'Update a block\'s data fields (shallow merge), style overrides, or responsive settings. Works on pages, partials, and page templates — use `target` to address non-page containers. Does not modify children or slots; use move/add/remove for structural changes. For appearance, prefer a named style (`data.style_id`, see list_styles) over custom_class or custom_css. From render version 2, custom_class lands on the heading or button link element itself instead of the block wrapper.',
93
93
  inputSchema: {
94
94
  target: targetSchema.optional(),
95
95
  page_id: z.string().optional(),
@@ -216,4 +216,18 @@ export const pageBlockTools = [
216
216
  return ok(res);
217
217
  }),
218
218
  },
219
+ {
220
+ name: 'convert_prose_block',
221
+ description: 'Turn one text block (core/prose) into separate blocks: headings (a classed label above a heading becomes its eyebrow), paragraphs, lists, images and buttons. Use it when a text block holds headings, classed HTML or media that should be editable and styled with named styles. Call without `accept` to preview (nothing is written); review `converted`, `notes` and `unconverted`, then call again with `accept` set to the preview\'s `fingerprint`. Writes the page draft (working copy) like other block edits. Pages only.',
222
+ inputSchema: {
223
+ page_id: z.string(),
224
+ block_id: z.string(),
225
+ accept: z.string().optional().describe('The fingerprint from a preview of this same block. Omit to preview.'),
226
+ version: versionParam,
227
+ },
228
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
229
+ const res = await client.post(siteId, `pages/${encodeURIComponent(args.page_id)}/blocks/convert-prose`, { block_id: args.block_id, ...(args.accept ? { accept: args.accept } : {}) }, v(args.version));
230
+ return ok(res);
231
+ }),
232
+ },
219
233
  ];
@@ -107,7 +107,7 @@ export const pageTools = [
107
107
  sort_order: z.number().finite().optional().describe('Manual Page order; lower numbers come first in ascending lists. Content types and listings choose how to sort.'),
108
108
  template: z.string().nullable().optional().describe('Allowed Page template ID; null uses the content type default.'),
109
109
  image_sizes_default: z.string().optional().describe('Per-page default `sizes` for responsive images (e.g. "(max-width: 640px) 360px, 560px"). Overrides the site setting; a per-<img> `sizes` attr still wins. Set when this page\'s images render narrower than the generic default so the browser stops over-fetching.'),
110
- custom_css: z.string().optional().describe('Per-page CSS, injected into <head> as a <style> AFTER the site-level custom_css (so it overrides site styling). This is the RIGHT home for page-specific styling — page metadata, not content. Put a page\'s <style> here instead of stuffing it into a core/html block (which is opaque and un-editable in the visual editor).'),
110
+ custom_css: z.string().optional().describe('Per-page CSS, injected into <head> as a <style> AFTER the site-level custom_css (so it overrides site styling). This is the RIGHT home for page-specific styling — page metadata, not content. Put a page\'s <style> here instead of stuffing it into a core/html block (which is opaque and un-editable in the visual editor). Syntax errors refuse the write; target s-<style> classes or block custom_class rather than [data-block]/.block-* platform markup.'),
111
111
  version: versionParam,
112
112
  },
113
113
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -89,11 +89,12 @@ export const partialTools = [
89
89
  },
90
90
  {
91
91
  name: 'create_free_block',
92
- description: 'Create a new free (reusable) global block. Use a kebab-case id and embed it on pages with <x-include name="block-id" />.',
92
+ description: 'Create a new global block: one shared source that every page using it shows, so an edit updates them all. Pass `blocks` for a block global block, referenced from block pages with a `core/global_block` block (data: { global_block_id }); or `html_content` for HTML pages, embedded with <x-include name="block-id" />. To turn an existing page block into a global block, use make_block_global. For a starter that is copied and then edited per page, use save_block_template instead.',
93
93
  inputSchema: {
94
94
  id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/),
95
95
  name: z.string().optional(),
96
- html_content: z.string(),
96
+ blocks: z.array(z.record(z.unknown())).optional().describe('Block tree for a block global block.'),
97
+ html_content: z.string().optional().describe('HTML for <x-include> in HTML pages. Send blocks or html_content.'),
97
98
  status: z.enum(['draft', 'published']).optional(),
98
99
  version: versionParam,
99
100
  },
@@ -26,6 +26,12 @@ export const previewTools = [
26
26
  .boolean()
27
27
  .optional()
28
28
  .describe('Render unsaved drafts (working copies) too — yours AND the editor\'s. Off by default (saved content only). Signed into the token, so a draft link needs its own mint. Use this for your own iteration loop; use a plain link when the user wants to see exactly what a deploy would ship.'),
29
+ render_version: z
30
+ .number()
31
+ .int()
32
+ .min(1)
33
+ .optional()
34
+ .describe('Render with this platform render version instead of the site\'s own, to preview an upgrade before applying it (read_site_settings returns render.version, render.latest and what each upgrade changes). Signed into the token. Compare against a plain link to see exactly what would change; apply with update_site_settings render_version.'),
29
35
  version: versionParam,
30
36
  },
31
37
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -0,0 +1,98 @@
1
+ // Reusable blocks: global blocks (one shared source, referenced) and block
2
+ // templates (saved starters, copied in). Global blocks themselves are
3
+ // partials; see partials.ts for list/read/update/delete.
4
+ import { z } from 'zod';
5
+ import { ok, withErrorBoundary, versionParam } from './helpers.js';
6
+ function v(version) {
7
+ return version ? { version } : undefined;
8
+ }
9
+ export const reusableBlockTools = [
10
+ {
11
+ name: 'make_block_global',
12
+ description: 'Turn a block on a page (with everything inside it) into a global block and put a reference in its place. Other pages then add the same block with add_block type "core/global_block" data { global_block_id }. Editing the global block (update_partial / update_block with target { kind: "partial", id }) changes every page that uses it. Use for content that must stay identical everywhere, such as a shared call to action. Writes the page draft.',
13
+ inputSchema: {
14
+ page_id: z.string(),
15
+ block_id: z.string(),
16
+ name: z.string().optional().describe('Name editors see; defaults to the block name.'),
17
+ global_block_id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/).optional().describe('Id for the new global block; derived from the name when omitted.'),
18
+ version: versionParam,
19
+ },
20
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
21
+ const { page_id, version, ...body } = args;
22
+ return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/blocks/make-global`, body, v(version)));
23
+ }),
24
+ },
25
+ {
26
+ name: 'detach_global_block',
27
+ description: 'Replace a core/global_block reference on a page with an editable copy of the global block\'s blocks. Later edits to the global block no longer reach this page. Writes the page draft.',
28
+ inputSchema: {
29
+ page_id: z.string(),
30
+ block_id: z.string().describe('The id of the core/global_block reference block.'),
31
+ version: versionParam,
32
+ },
33
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
34
+ const { page_id, version, ...body } = args;
35
+ return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/blocks/detach`, body, v(version)));
36
+ }),
37
+ },
38
+ {
39
+ name: 'list_block_templates',
40
+ description: 'List the site\'s block templates: saved section starters (name, description, block count). Inserting one copies its blocks into a page, where they are then edited independently. Check here before building a common section from scratch.',
41
+ inputSchema: {},
42
+ handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'block-templates'))),
43
+ },
44
+ {
45
+ name: 'read_block_template',
46
+ description: 'Read one block template, including its block tree.',
47
+ inputSchema: { template_id: z.string() },
48
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, `block-templates/${encodeURIComponent(args.template_id)}`))),
49
+ },
50
+ {
51
+ name: 'save_block_template',
52
+ description: 'Save a block template: a section starter that editors and agents insert as an independent copy. Pass `from: { page_id, block_id }` to save a block from a page (its draft), or `blocks`. Prefer templates that use named styles (style_id) over one-off values, so copies stay consistent. Templates belong to the site, not a branch.',
53
+ inputSchema: {
54
+ name: z.string(),
55
+ description: z.string().optional().describe('When to use it.'),
56
+ id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/).optional(),
57
+ from: z.object({ page_id: z.string(), block_id: z.string() }).optional(),
58
+ blocks: z.array(z.record(z.unknown())).optional(),
59
+ },
60
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'block-templates', args))),
61
+ },
62
+ {
63
+ name: 'update_block_template',
64
+ description: 'Rename a block template, change its description, or replace its blocks. Pages that already inserted it do not change.',
65
+ inputSchema: {
66
+ template_id: z.string(),
67
+ name: z.string().optional(),
68
+ description: z.string().optional(),
69
+ blocks: z.array(z.record(z.unknown())).optional(),
70
+ },
71
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
72
+ const { template_id, ...body } = args;
73
+ return ok(await client.patch(siteId, `block-templates/${encodeURIComponent(template_id)}`, body));
74
+ }),
75
+ },
76
+ {
77
+ name: 'delete_block_template',
78
+ description: 'Delete a block template. Pages that already inserted it keep their copies.',
79
+ inputSchema: { template_id: z.string() },
80
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.del(siteId, `block-templates/${encodeURIComponent(args.template_id)}`))),
81
+ },
82
+ {
83
+ name: 'insert_block_template',
84
+ description: 'Insert a copy of a block template into a page (new block ids), at the top level or inside parent_id (slot_index for column slots), at `position` (default: end). Returns added_ids. Writes the page draft.',
85
+ inputSchema: {
86
+ page_id: z.string(),
87
+ template_id: z.string(),
88
+ parent_id: z.string().optional().nullable(),
89
+ slot_index: z.number().int().min(0).optional(),
90
+ position: z.number().int().min(0).optional(),
91
+ version: versionParam,
92
+ },
93
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
94
+ const { page_id, version, ...body } = args;
95
+ return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/blocks/insert-template`, body, v(version)));
96
+ }),
97
+ },
98
+ ];
@@ -13,7 +13,7 @@ function v(version) {
13
13
  export const settingsTools = [
14
14
  {
15
15
  name: 'read_site_settings',
16
- 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, language, robots_txt, image_sizes_default, plus the scriptable surfaces scripts_head, scripts_body_end, and custom_css. Pass `version` to read a branch's settings (with copy-on-write chain-fallback to main for fields the branch hasn't overridden).",
16
+ 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, language, robots_txt, image_sizes_default, plus the scriptable surfaces scripts_head, scripts_body_end, and custom_css, 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).",
17
17
  inputSchema: {
18
18
  version: versionParam,
19
19
  },
@@ -53,7 +53,8 @@ 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 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.'),
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. Prefer named styles (create_style) for anything a style can express. Syntax errors (unbalanced braces, unclosed comments or strings, expression()/javascript:) refuse the write; selectors on platform markup ([data-block], .block-*) come back as warnings because they can change between render versions — target s-<style> classes or block custom_class instead.'),
57
+ render_version: z.number().int().min(1).optional().describe('Platform render version. Platform output changes ship as new versions so existing sites never change look on their own. Upgrade only after previewing (get_preview_link render_version) and with the user\'s approval; the tr-upgrade-rendering skill compares every page. Lowering it returns to an earlier version.'),
57
58
  colors: z.record(z.string()).optional(),
58
59
  fonts: z.record(z.unknown()).optional(),
59
60
  contact: z
@@ -0,0 +1,108 @@
1
+ // Style library tools. A site's look is a library of NAMED styles
2
+ // (SiteSettings.styles): element roles (body, h1–h6, link) style every
3
+ // matching element, and every other style is chosen per block through the
4
+ // block's `style_id` field. Reuse beats one-off values.
5
+ import { z } from 'zod';
6
+ import { ok, withErrorBoundary, versionParam } from './helpers.js';
7
+ function v(version) {
8
+ return version ? { version } : undefined;
9
+ }
10
+ const COLOR_HELP = 'A palette token (primary, secondary, accent, background, surface, text, text_light), a derived readable text colour for a palette background (on_primary, on_secondary, on_accent), or a hex/rgb/hsl colour. Prefer tokens so a palette change flows through.';
11
+ const length = z.string().describe('CSS length: px, rem, em, %, vw, ch, or clamp()/min()/max()');
12
+ const color = z.string().describe(COLOR_HELP);
13
+ const propsSchema = z.object({
14
+ font: z.enum(['heading', 'body', 'mono', 'inherit']).optional(),
15
+ size: length.optional(),
16
+ weight: z.number().int().min(100).max(900).optional(),
17
+ line_height: z.string().optional().describe('Unitless number such as "1.5", or a length'),
18
+ letter_spacing: z.string().optional().describe('e.g. "0.08em" or "normal"'),
19
+ transform: z.enum(['none', 'uppercase', 'lowercase', 'capitalize']).optional(),
20
+ italic: z.boolean().optional(),
21
+ color: color.optional(),
22
+ background: color.optional(),
23
+ align: z.enum(['left', 'center', 'right', 'start', 'end', 'justify']).optional(),
24
+ decoration: z.enum(['none', 'underline']).optional(),
25
+ space_before: length.optional(),
26
+ space_after: length.optional(),
27
+ padding: z.string().optional().describe('One to four CSS lengths'),
28
+ max_width: length.optional(),
29
+ radius: length.optional(),
30
+ border: z.object({
31
+ width: length.optional(),
32
+ style: z.enum(['solid', 'dashed', 'dotted', 'none']).optional(),
33
+ color: color.optional(),
34
+ sides: z.enum(['all', 'top', 'right', 'bottom', 'left', 'block', 'inline']).optional(),
35
+ }).optional(),
36
+ }).describe('Style properties. Mobile-first: `base` applies everywhere, `at.<breakpoint>` from that width up.');
37
+ const atSchema = z.object({
38
+ tablet: propsSchema.optional(),
39
+ laptop: propsSchema.optional(),
40
+ desktop: propsSchema.optional(),
41
+ wide: propsSchema.optional(),
42
+ }).describe('Overrides from each site breakpoint upward (tablet, laptop, desktop, wide).');
43
+ const styleFields = {
44
+ name: z.string().describe('Author-facing name, in the site\'s language, e.g. "Överrubrik" or "Hero title".'),
45
+ role: z.enum(['body', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'link', 'lead', 'eyebrow', 'small', 'quote', 'button', 'button_secondary', 'section']).optional()
46
+ .describe('Standard role. body/h1–h6/link style every matching element on the site; the others mark the standard style for that purpose. One style per role.'),
47
+ targets: z.array(z.enum(['text', 'heading', 'list', 'button', 'container'])).optional()
48
+ .describe('Block kinds that can select this style via style_id (text = core/prose, heading = core/heading and core/rich_heading, list, button, container = core/section and core/container).'),
49
+ description: z.string().optional().describe('When to use it; shown to editors and agents.'),
50
+ base: propsSchema.optional(),
51
+ at: atSchema.optional(),
52
+ hover: z.object({ color: color.optional(), background: color.optional(), decoration: z.enum(['none', 'underline']).optional() }).optional(),
53
+ css: z.string().optional().describe('Advanced CSS scoped to the style: bare declarations, or rules where `&` is the style selector (e.g. "&::before { content: \'—\' }"). Prefer structured properties.'),
54
+ };
55
+ export const styleTools = [
56
+ {
57
+ name: 'list_styles',
58
+ description: 'List the site\'s named style library: element roles (body, h1–h6, link) that style every matching element, and the styles blocks select with `style_id`. Also returns missing_standard_roles and contrast/library problems. READ THIS BEFORE STYLING ANY PAGE: reuse an existing style for a recurring look (eyebrows, leads, card titles, buttons, section spacing) instead of per-block values, raw HTML classes or page CSS.',
59
+ inputSchema: { version: versionParam },
60
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'styles', v(args.version)))),
61
+ },
62
+ {
63
+ name: 'create_style',
64
+ description: 'Create a named style. Do this whenever a look could recur — an eyebrow, a lead paragraph, a card title, a quote, a button variant, a section rhythm — then apply it with the block\'s `style_id` field. Only a clearly unique one-off may use block settings instead. Set values for phones in `base` and grow them in `at.tablet`/`at.desktop`. Text must meet WCAG AA contrast (4.5:1, 3:1 for ≥24px or bold ≥18.7px) against its background; a failing style is refused. Never light grey on white.',
65
+ inputSchema: {
66
+ id: z.string().regex(/^[a-z][a-z0-9-]{0,47}$/).describe('Stable slug; the CSS class becomes s-<id>. Cannot change later.'),
67
+ ...styleFields,
68
+ name: styleFields.name,
69
+ version: versionParam,
70
+ },
71
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
72
+ const { version, ...style } = args;
73
+ return ok(await client.post(siteId, 'styles', { targets: [], base: {}, ...style }, v(version)));
74
+ }),
75
+ },
76
+ {
77
+ name: 'update_style',
78
+ description: 'Update a style. `base`, `hover` and each breakpoint in `at` merge property by property (pass null to remove one); name, targets, description and css replace. Every page using the style changes at once — tell the user which pages that affects when it matters.',
79
+ inputSchema: {
80
+ style_id: z.string(),
81
+ name: styleFields.name.optional(),
82
+ role: styleFields.role,
83
+ targets: styleFields.targets,
84
+ description: styleFields.description,
85
+ base: z.record(z.unknown()).optional().describe('Property patch for base; null removes a property.'),
86
+ at: z.record(z.unknown()).optional().describe('Per-breakpoint property patches; null removes a breakpoint or property.'),
87
+ hover: z.record(z.unknown()).optional(),
88
+ css: z.string().optional(),
89
+ version: versionParam,
90
+ },
91
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
92
+ const { version, style_id, ...patch } = args;
93
+ return ok(await client.patch(siteId, `styles/${encodeURIComponent(style_id)}`, patch, v(version)));
94
+ }),
95
+ },
96
+ {
97
+ name: 'delete_style',
98
+ description: 'Delete a style. Blocks that still reference it fall back to their default look; reassign them first.',
99
+ inputSchema: { style_id: z.string(), version: versionParam },
100
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.del(siteId, `styles/${encodeURIComponent(args.style_id)}`, v(args.version)))),
101
+ },
102
+ {
103
+ name: 'apply_standard_styles',
104
+ description: 'Add the standard styles the site is missing (body, H1–H6, link, lead, eyebrow, small text, quote, primary/secondary button, section), with considered sizes and spacing for phones, tablets and desktops. The natural first step when building a new site, before pages: then adjust them to the brand (fonts, colours, scale) with update_style. New sites already have them. overwrite:true resets the standard roles to the defaults and changes the site\'s look — ask first.',
105
+ inputSchema: { overwrite: z.boolean().optional(), version: versionParam },
106
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'styles/standard', { overwrite: args.overwrite === true }, v(args.version)))),
107
+ },
108
+ ];
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.42';
11
+ export const VERSION = '0.45.43';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.45.42",
3
+ "version": "0.45.43",
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": {
package/skills/README.md CHANGED
@@ -33,6 +33,7 @@ instructions. Read the relevant recipe when the task calls for it.
33
33
  | `tr-new-site.md` | "create a new site", "bootstrap a site for…" | Settings → header/footer partials → homepage → inner pages → deploy. Full setup recipe. |
34
34
  | `tr-brand.md` | "create a brand", "choose colors", "design the look"| Palette recipes by mood, typography pairings, CSS variable setup, preview. |
35
35
  | `tr-redesign-branch.md`| "redesign", "modernize", anything site-wide-design | Branch-isolated work with preview links, merge when approved. |
36
+ | `tr-upgrade-rendering.md` | "upgrade rendering", render version behind latest | Screenshot-compares every page at the current and target render version, reports differences, upgrades with approval. |
36
37
  | `tr-content-write.md` | "write a page about…", "draft copy for…" | Discovery first (settings + sample pages), then drafts in the site's voice, previews, iterates. |
37
38
  | `tr-images.md` | "make an image / hero / illustration", media uploads | Generates locally → signed upload URL → metadata patch → embed. |
38
39
 
@@ -159,6 +159,16 @@ replace_partial partial_id="header" html_content="<updated HTML>"
159
159
 
160
160
  Same for footer.
161
161
 
162
+ ## Step 5b — Express the typography as styles
163
+
164
+ Colours and fonts are only half the brand. Put the type scale, weights,
165
+ spacing and recurring treatments into the site's named styles (`list_styles`,
166
+ `update_style`, `create_style`): body text and H1–H6 apply everywhere; the
167
+ eyebrow, lead, quote, buttons and section rhythm are chosen per block with
168
+ `style_id`. Set phone values in `base` and grow them in `at.tablet` and
169
+ `at.desktop`. Prefer this to custom CSS: editors can see and change styles,
170
+ and every page follows.
171
+
162
172
  ## Step 6 — Custom CSS for advanced tokens (optional)
163
173
 
164
174
  If the brand needs things beyond the 7 base tokens — e.g. a gradient,
@@ -37,6 +37,23 @@ Read the actual HTML of an existing page. Note:
37
37
  - Length conventions (do existing pages run 200 words or 2000?)
38
38
  - Whether internal links use absolute or relative URLs
39
39
 
40
+ `list_styles` too: use the site's named styles (`style_id` on heading, text,
41
+ list, button and section blocks) for eyebrows, leads, quotes and buttons. If a
42
+ treatment you need could recur and no style fits, create one rather than
43
+ setting one-off values; ask first when it changes the site's look.
44
+
45
+ A small label above a heading is the heading block's `eyebrow`, and a line
46
+ below it is its `subtitle` (render version 2), each with its own style field.
47
+ Never write them as separate text blocks with classed HTML. In text blocks,
48
+ style whole paragraphs with site text styles (`<p class="s-lead">`); a text
49
+ block that needs other classes, images or headings with parts is better split
50
+ with `convert_prose_block`.
51
+
52
+ `list_block_templates` and `list_partials` too. Start a common section from a
53
+ block template (`insert_block_template`, then edit the copy). Use a global
54
+ block (`core/global_block`) for content that must stay identical on every
55
+ page, such as a shared call to action; don't paste copies of it.
56
+
40
57
  ### 2. Ask for the brief
41
58
 
42
59
  If the user hasn't told you, ask:
@@ -54,6 +54,16 @@ Parse `<a href>` links to discover internal pages. Build a list:
54
54
  Exclude private/session/admin URLs deliberately. Inventory public pagination and
55
55
  query routes; decide preservation or retirement from their actual purpose.
56
56
 
57
+ **Map source styling to named styles.** A class the source uses for a
58
+ recurring treatment (`.eyebrow`, `.lead`, `.btn-outline`, `.section-dark`)
59
+ becomes a site style with `create_style` and is applied with `style_id`. A
60
+ small label directly above a heading is an eyebrow style, not a heading of its
61
+ own: put it in the heading block's `eyebrow` field (and a line under the
62
+ heading in `subtitle`). Never carry classed raw HTML into text blocks to keep a
63
+ look: the editor cannot edit it, and the next render version may not keep it.
64
+ A text block that already holds such markup can be split with
65
+ `convert_prose_block` (preview, then `accept`).
66
+
57
67
  ### 2. Measure the source and protect existing target work
58
68
 
59
69
  ```
@@ -122,6 +122,11 @@ The managed importer preserves existing imported Pages on retry and refuses
122
122
  URL/identity conflicts or dangling term references. An older flattened import
123
123
  requires a separately reviewed mapping repair; do not re-import over edits.
124
124
 
125
+ **Styles.** Recurring source treatments (theme classes for eyebrows, leads,
126
+ button variants, highlighted sections) become named site styles
127
+ (`create_style`) applied with `style_id`, so editors can change them in one
128
+ place. Do not preserve them as classed raw HTML in text blocks.
129
+
125
130
  ### 4. Import one page at a time, draft status
126
131
 
127
132
  For each source URL:
@@ -129,6 +129,23 @@ the built-in YouTube/Vimeo/Google Maps/Calendly set. Values are exact domain
129
129
  hostnames, never URLs or wildcards. Read the setting back before deciding that
130
130
  an iframe cannot be represented.
131
131
 
132
+ ### 2b. Styles before pages
133
+
134
+ Set the site's look as named styles before building any page:
135
+
136
+ 1. `list_styles`. New sites already have the standard set (body, H1–H6, link,
137
+ lead, eyebrow, small, quote, buttons, section). If roles are missing,
138
+ `apply_standard_styles`.
139
+ 2. Adjust them to the brand with `update_style`: the font, the type scale and
140
+ spacing for `base` (phones) and `at.tablet`/`at.desktop`, and colours from
141
+ palette tokens. Keep text at WCAG AA contrast; failing styles are refused.
142
+ 3. Add a named style for every look the design repeats: a card title, a price
143
+ note, a highlighted section, a CTA button variant. Use the site's language
144
+ for names.
145
+
146
+ Pages then choose styles with each block's `style_id` field. A per-block
147
+ font size, colour or class is only for something clearly unique.
148
+
132
149
  ### 3. Header + footer partials
133
150
 
134
151
  **Start from the native preset — don't hand-roll navigation.** Read
@@ -275,10 +292,10 @@ Known limitations (honest list — don't fight them):
275
292
  client-side without the icon pipeline). Text labels only.
276
293
 
277
294
  Theming: block primitives render neutral. Brand color/typography comes
278
- from settings (step 2). For page-specific polish (e.g. a colored card
279
- treatment), a single `core/html` block with a small `<style>` scoped to
280
- `[data-bid]`/section selectors is acceptable — keep it minimal and note
281
- it in your log.
295
+ from settings (step 2) and named styles (step 2b). For page-specific polish
296
+ (e.g. a colored card treatment) that no style covers, give the block a
297
+ `custom_class` and write the rule in the page's `custom_css`. Never put a
298
+ `<style>` in a `core/html` block or target `[data-bid]`/`[data-block]`.
282
299
 
283
300
  ### 5. Inner pages
284
301
 
@@ -286,6 +303,12 @@ Same pattern: `create_page` with `content_mode: "blocks"` and a section
286
303
  tree. Standard set: Om oss, Tjänster, Kontakt — or what the brief says.
287
304
  Default new pages to `status: "draft"`; publish after review.
288
305
 
306
+ Sections that repeat across pages: a call to action or contact strip that
307
+ must stay identical becomes a global block (`make_block_global`, then
308
+ `core/global_block` on the other pages). A section shape you will reuse with
309
+ different content (a pricing row, a feature trio) becomes a block template
310
+ (`save_block_template`, then `insert_block_template`).
311
+
289
312
  ### 5b. If the legacy site is still live, scrape canonical content
290
313
 
291
314
  For pages with canonical text (privacy policy, terms, about), `WebFetch`
@@ -34,7 +34,7 @@ default template, and an individual Page can override it.
34
34
  Prefer native image, table, list, heading and content blocks. A rare specialized
35
35
  widget can use a reviewed HTML/embed block. Reusable custom behavior belongs in
36
36
  one custom block type rather than repeated HTML bodies. Existing HTML-mode pages
37
- may share fragments through partials and `<x-include>`, but use block templates
37
+ may share fragments through partials and `<x-include>`, but use block-based page templates
38
38
  for new content families.
39
39
 
40
40
  Header/footer partials are global and remain separate from content types. Keep
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: tr-upgrade-rendering
3
+ description: Use when a site's platform render version is behind the latest (read_site_settings → render.version < render.latest), or the user asks to "upgrade rendering", "get the new block styles" or why a platform improvement doesn't show. Compares every page at the current and target version with screenshots, reports the differences and upgrades only with approval.
4
+ ---
5
+
6
+ # Upgrade a site's render version
7
+
8
+ Platform changes to block markup and shared CSS ship as numbered render
9
+ versions. A site keeps its version until someone upgrades it, so its look
10
+ never changes on its own. Upgrading is a single settings write, reversible by
11
+ writing the previous number. The comparison happens here, in the agent; the
12
+ server only renders previews.
13
+
14
+ The user can also do this in the portal: Settings → Rendering → "Preview with
15
+ version N", then "Upgrade". Offer this skill when they want a full-site visual
16
+ comparison first.
17
+
18
+ ## Recipe
19
+
20
+ 1. **Read the gap.** `read_site_settings` returns `render`:
21
+ `{ version, latest, upgrades: [{ version, title, changes[] }] }`. If
22
+ `version === latest`, stop. Tell the user what each pending version
23
+ changes, in their words, before measuring anything.
24
+
25
+ 2. **List the pages to compare.** `list_pages` (published and unlisted), plus
26
+ the header/footer, which appear on every page. For a large site, compare
27
+ the home page, one page per content type and template, and every page
28
+ whose blocks use a component named in `upgrades[].changes`.
29
+
30
+ 3. **Mint two links per page set.** One `get_preview_link` without
31
+ `render_version` (current) and one with `render_version: latest` (target).
32
+ Both show saved content. Mint each once and reuse it; internal links keep
33
+ the token.
34
+
35
+ 4. **Screenshot both** at 390, 768 and 1440 px wide, full page, after fonts
36
+ and images have loaded (`document.fonts.ready`, scroll to the end to
37
+ trigger lazy images, then back to the top). Use a headless browser with a
38
+ fixed viewport and `reducedMotion: 'reduce'`.
39
+
40
+ 5. **Compare.** Pixel-diff each pair (same size, small per-pixel tolerance)
41
+ and record the changed area and the bounding boxes of changed regions.
42
+ Crop each region from both versions. Ignore differences confined to
43
+ embedded third-party frames (maps, booking widgets, video).
44
+
45
+ 6. **Report** per page and width: unchanged, or the cropped before/after
46
+ regions with a one-line description ("links in body text are underlined",
47
+ "section gap 8px larger"). Link each change to the version note that
48
+ explains it. Say plainly when a change looks like a regression.
49
+
50
+ Before screenshots, read the site and page custom CSS for selectors a
51
+ version note names. Version 2 moves a heading's or button's custom class
52
+ onto the `<h2>` or link, so `.my-class .block-heading-text` stops
53
+ matching; propose the rewrite (`.my-class`) or a named style.
54
+
55
+ 7. **Ask before upgrading.** Never upgrade on your own. If the user wants to
56
+ keep a specific old look, solve it with a site style or page CSS first and
57
+ compare again.
58
+
59
+ 8. **Upgrade** with `update_site_settings render_version: <latest>` (pass
60
+ `version` to do it on a branch first). The preview changes at once; the
61
+ live site changes at the next deploy. To undo, write the previous number.
62
+
63
+ ## Pitfalls
64
+
65
+ - Comparing a draft against saved content shows content edits, not
66
+ rendering changes. Use the same content state for both links.
67
+ - Lazy images and web fonts cause false differences. Wait for them in both
68
+ screenshots.
69
+ - Comparing only the home page misses changes in components it doesn't use.
70
+ - Don't encode a workaround in custom CSS to resist an upgrade without
71
+ telling the user; it becomes a hidden fork of the platform styles.