@typeroll/mcp-server 0.15.0 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -112,7 +112,11 @@ maps to one HTTP endpoint; the actual logic runs in the customer's portal
112
112
  constrained by the section's own inner container (`width` field:
113
113
  narrow/normal/wide/full). Never use 100vw negative-margin hacks.
114
114
  Top-level blocks that are NOT sections still get a classic centered
115
- container as fallback.
115
+ container as fallback. Anchor ids and custom classes via
116
+ `style_overrides` are safe on full-bleed sections since 0.15.3 —
117
+ they merge into the `<section>` element itself. On 0.14.x–0.15.2
118
+ they wrapped the section in a `<div>`, which silently disabled
119
+ full-bleed for that section.
116
120
  - **`core/html`** is the raw-HTML escape hatch for block-mode pages —
117
121
  one `html` field rendered verbatim (then sanitized like HTML-mode
118
122
  content). Use it for the genuinely unique thing no block covers: a
@@ -257,6 +261,28 @@ add_block page_id=home parent_id="blk_xyz" block={
257
261
  }
258
262
  ```
259
263
 
264
+ Slot containers (`container: "slots"` — `core/columns`, `core/tabs`)
265
+ hold their children in per-slot lists, not in `children`. Two ways to
266
+ populate them (both require template_capabilities_version ≥ 0.15.2):
267
+
268
+ ```
269
+ # Inline — pass the whole subtree in one call:
270
+ add_block page_id=home block={
271
+ type: 'core/columns', data: { ratio: '1-1' },
272
+ slots: [
273
+ [{ type: 'core/prose', data: { html: '<p>Left column</p>' } }],
274
+ [{ type: 'core/image', data: { src: '…' } }],
275
+ ]
276
+ }
277
+
278
+ # Incrementally — slot_index picks the slot (0-based, defaults to 0):
279
+ add_block page_id=home block={ type: 'core/columns', data: {} }
280
+ # → { added_id: 'blk_cols' } — slots are auto-initialised to the type's arity
281
+ add_block page_id=home parent_id="blk_cols" slot_index=1 block={
282
+ type: 'core/prose', data: { html: '<p>Right column</p>' }
283
+ }
284
+ ```
285
+
260
286
  For an unfamiliar custom block, `read_block_type id="..."` gives the
261
287
  full field list (types, defaults, required) so you don't ship invalid
262
288
  `data`.
@@ -67,7 +67,7 @@ export const pageBlockTools = [
67
67
  },
68
68
  {
69
69
  name: 'add_block',
70
- description: 'Insert a block into a container (page, partial, or page template). With no parent_id, inserts at the top level of the container. Otherwise inserts as a child (or into the named slot for slot-containers). Position defaults to "end". For slot containers, slot_index picks the slot. Pass `target: { kind: "partial", id: "header" }` to drop a block into the header on every page, etc. `page_id` is the legacy shorthand.',
70
+ description: 'Insert a block into a container (page, partial, or page template). With no parent_id, inserts at the top level of the container. Otherwise inserts as a child (or into a slot for slot-containers). Position defaults to "end". Slot containers (core/columns, core/tabs — container: "slots"): slot_index picks the slot (0-based; defaults to 0) and works even if the parent instance has no slots array yet; a freshly added slot container gets its slots initialised to the type\'s arity. You can also pass the whole subtree inline via block.slots: [[...],[...]]. Pass `target: { kind: "partial", id: "header" }` to drop a block into the header on every page, etc. `page_id` is the legacy shorthand.',
71
71
  inputSchema: {
72
72
  target: targetSchema.optional(),
73
73
  page_id: z.string().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.15.0",
3
+ "version": "0.15.3",
4
4
  "description": "Model Context Protocol server for the Typeroll public API. Use with Claude Code or any MCP-compatible client to manage a Typeroll site.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,13 +10,17 @@ the portal UI or API) but has no design, no header/footer, and no pages.
10
10
  The goal is to go from blank to a working 4-page site with correct brand
11
11
  identity in a single session.
12
12
 
13
+ **Pages are built in block mode** — the platform default. Blocks give
14
+ structured, per-field editing, native full-bleed sections, responsive
15
+ breakpoints, and templates. HTML mode is the secondary path for
16
+ hand-crafted one-offs and migrated content (section at the end).
17
+
13
18
  ## Preconditions
14
19
 
15
20
  - `@typeroll/mcp-server` configured with a valid `TYPEROLL_API_KEY`.
16
21
  - The site exists (confirm with `get_site`).
17
22
  - You have the customer brief: company name, industry, 2–3 key brand colors,
18
- tone of voice (formal / friendly / minimal / vibrant), and a list of
19
- initial pages.
23
+ tone of voice, and a list of initial pages.
20
24
 
21
25
  ## Recipe
22
26
 
@@ -24,9 +28,11 @@ identity in a single session.
24
28
 
25
29
  ```
26
30
  get_site
31
+ get_site_capabilities # template_capabilities_version — what this deployment supports
27
32
  read_site_settings # see what (if anything) is already configured
28
33
  list_pages # don't overwrite pages that already exist
29
34
  list_partials # check if header/footer already have content
35
+ list_block_types # the per-site block palette — NEVER assume, always list
30
36
  ```
31
37
 
32
38
  ### 2. Brand + settings
@@ -47,203 +53,193 @@ One `update_site_settings` call with every field you know:
47
53
  "text": "#1a1a2e",
48
54
  "text_light": "#6b7280"
49
55
  },
50
- "fonts": {
51
- "heading": "Playfair Display",
52
- "body": "Inter",
53
- "size_base": 16
54
- },
55
- "contact": {
56
- "email": "hej@acme.se",
57
- "phone": "+46 8 123 456",
58
- "address": "Drottninggatan 1, 111 51 Stockholm"
59
- },
60
- "social": {
61
- "instagram": "https://instagram.com/acme",
62
- "linkedin": "https://linkedin.com/company/acme"
63
- }
56
+ "fonts": { "heading": "Playfair Display", "body": "Inter", "size_base": 16 },
57
+ "contact": { "email": "hej@acme.se", "phone": "+46 8 123 456" },
58
+ "social": { "instagram": "https://instagram.com/acme" }
64
59
  }
65
60
  ```
66
61
 
67
- Read it back: `read_site_settings` confirm `updated_fields` includes
68
- everything you intended.
62
+ Read it back with `read_site_settings`. Google Fonts names are
63
+ case-sensitive display names ("Plus Jakarta Sans", not "plus jakarta").
69
64
 
70
- Google Fonts names: use the full display name as it appears on
71
- fonts.google.com, e.g. "Cormorant Garamond", "DM Sans", "Plus Jakarta Sans".
65
+ **Site icons are part of brand setup** upload favicon (32–64px) and a
66
+ 180×180 apple touch icon, set `favicon` + `apple_touch_icon`. No icon
67
+ assets? Derive a proposal (see `tr-brand`). Also set `settings.logo` to
68
+ the uploaded brand mark — it feeds OG/schema even if the header uses a
69
+ different lockup.
72
70
 
73
- ### 3. Header partial
71
+ ### 3. Header + footer partials
74
72
 
75
- Replace the header with a real nav. Keep it simple:
73
+ Partials are usually simplest in HTML mode (one nav, a few links — no
74
+ per-field editing needed). Keep them lean; literal site name (no template
75
+ engine in partials):
76
76
 
77
77
  ```html
78
78
  <header class="site-header">
79
79
  <div class="header-inner">
80
- <a class="header-logo" href="/">Acme Studio</a>
80
+ <a class="header-logo" href="/"><img src="LOGO_MEDIA_URL" alt="Acme Studio" height="40" /></a>
81
81
  <nav class="header-nav">
82
82
  <a href="/om-oss">Om oss</a>
83
- <a href="/tjanster">Tjänster</a>
84
83
  <a href="/kontakt">Kontakt</a>
85
84
  </nav>
86
85
  </div>
87
86
  </header>
88
87
  <style>
89
- .site-header{background:var(--color-primary);padding:1rem 2rem}
88
+ .site-header{background:var(--color-background);padding:1rem 2rem}
90
89
  .header-inner{max-width:1080px;margin:0 auto;display:flex;align-items:center;justify-content:space-between}
91
- .header-logo{color:#fff;text-decoration:none;font-family:var(--font-heading);font-size:1.25rem;font-weight:700}
92
90
  .header-nav{display:flex;gap:2rem}
93
- .header-nav a{color:rgba(255,255,255,0.85);text-decoration:none;font-size:0.9rem;letter-spacing:0.05em;text-transform:uppercase}
94
- .header-nav a:hover{color:#fff}
95
- </style>
96
- ```
97
-
98
- Use `replace_partial partial_id="header" html_content="..."`.
99
-
100
- ### 4. Footer partial
101
-
102
- ```html
103
- <footer class="site-footer">
104
- <div class="footer-inner">
105
- <p class="footer-brand">Acme Studio</p>
106
- <p class="footer-tagline">Tagline here</p>
107
- <div class="footer-links">
108
- <a href="/om-oss">Om oss</a>
109
- <a href="/kontakt">Kontakt</a>
110
- </div>
111
- <p class="footer-copy">© 2025 Acme Studio. Alla rättigheter förbehållna.</p>
112
- </div>
113
- </footer>
114
- <style>
115
- .site-footer{background:var(--color-secondary);color:rgba(255,255,255,0.7);padding:3rem 2rem;margin-top:4rem}
116
- .footer-inner{max-width:1080px;margin:0 auto;text-align:center;display:grid;gap:1rem}
117
- .footer-brand{font-family:var(--font-heading);font-size:1.5rem;color:#fff;font-weight:700}
118
- .footer-links{display:flex;justify-content:center;gap:2rem}
119
- .footer-links a{color:rgba(255,255,255,0.7);text-decoration:none}
120
- .footer-links a:hover{color:#fff}
121
- .footer-copy{font-size:0.8rem;opacity:0.5}
91
+ .header-nav a{color:var(--color-text);text-decoration:none}
122
92
  </style>
123
93
  ```
124
94
 
125
- Use `replace_partial partial_id="footer" html_content="..."`.
126
-
127
- ### 5. Homepage
128
-
129
- ```
130
- create_page title="Start" slug="" content_mode="html"
131
- html_content="<article class=\"home-page\">...full hero + intro HTML...</article>"
132
- status="published"
133
- ```
134
-
135
- `slug=""` creates the homepage under `page_id: "home"`. If it already exists, use `update_page page_id="home" patch={html_content: "..."}`.
136
-
137
- **Always wrap the body in a page-scope `<article class="...-page">`.** The renderer's `.page-content` layout shell sets default max-width and padding; without a scope class, any selectors you write live in the same tree as the renderer defaults and you'll wonder why your `.hero h1` doesn't get the size you set. With `.home-page .hero h1` you're at specificity 0,2,1 and trivially win. Use `<article class="home-page">` for the homepage and `<article class="static-page">` (or page-specific names like `<article class="about-page">`) for inner pages.
138
-
139
- A minimal homepage structure:
140
- - Hero: **full-bleed** section (negative-margin escape) with `<h1>`, tagline, one CTA button
141
- - Intro: 2–3 sentences about what the company does
142
- - Services/features grid: 3 cards max
143
- - CTA band: "Kontakta oss" or similar
144
-
145
- Full-bleed hero recipe (matches the section conventions from `tr-brand`'s Step 4b):
146
-
147
- ```css
148
- .home-page .home-hero {
149
- position: relative;
150
- margin-left: calc(50% - 50vw);
151
- margin-right: calc(50% - 50vw);
152
- margin-top: -32px; /* cancels .page-content top padding */
153
- width: 100vw;
154
- padding: 80px 0 64px;
155
- background: var(--gradient-soft), #fff;
156
- }
157
- ```
158
-
159
- **Caveat:** never combine with `overflow-x: clip` on the page wrapper — the clip cancels the bleed.
160
-
161
- ### 6. Inner pages
162
-
163
- Create each page (always with a page-scope class):
164
-
165
- ```
166
- create_page title="Om oss" slug="om-oss" content_mode="html"
167
- html_content="<article class=\"static-page\">...</article>"
168
- status="published"
169
- ```
170
-
171
- Standard set: Om oss, Tjänster, Kontakt. Add more if the brief says so.
172
-
173
- ### 6b. If the legacy site is still live, scrape canonical content
174
-
175
- When a page (privacy policy, terms, about) exists on the still-live previous site, use `WebFetch` to pull the canonical text directly rather than rewriting from memory or relying on placeholder copy.
176
-
177
- Pattern:
178
-
179
- ```
180
- WebFetch url="https://www.olddomain.example/integritetspolicy"
181
- prompt="Return full content as markdown, preserve Swedish characters,
182
- preserve hierarchy, don't summarize"
183
- ```
95
+ `replace_partial partial_id="header" html_content="..."` — same pattern
96
+ for the footer. Design notes: **no border-bottom on the header if the
97
+ first page section should meet it seamlessly** — let background color
98
+ changes do the separating. Anchor links in nav (`/#section`) are fine.
184
99
 
185
- Then convert markdown HTML for `html_content`. This keeps legal text exact and stops "polished" rewrites from drifting from the canonical version.
100
+ ### 4. Homepage block tree
186
101
 
187
- ### 7. Preview + iterate
102
+ `create_page` with the whole tree in one call. Omit block `id`s — the
103
+ platform assigns them (`blk_…`); you read them back for later
104
+ `update_block` calls.
188
105
 
189
106
  ```
190
- get_preview_link # get a signed URL for the whole site
191
- get_page_preview page_id="home" # get rendered HTML to spot-check
107
+ create_page title="Start" slug="" status="draft" content_mode="blocks" blocks=[...]
192
108
  ```
193
109
 
194
- Share the preview link with the user. Iterate on feedback.
110
+ A proven landing-page skeleton (every top-level block is a `core/section`
111
+ — sections are **natively full-bleed**: the background runs edge-to-edge,
112
+ content is constrained by the section's own inner container via the
113
+ `width` field. NEVER use 100vw negative-margin hacks. Anchor ids and
114
+ custom classes via `style_overrides` are safe on sections from
115
+ template_capabilities_version ≥ 0.15.3; on 0.14.x–0.15.2 they wrap the
116
+ section in a div and silently break full-bleed — there, put the anchor
117
+ on a block *inside* the section instead):
195
118
 
196
- ### 8. Deploy
119
+ ```json
120
+ [
121
+ { "type": "core/section", "data": { "background": "#ffffff", "padding_y": "lg" }, "children": [
122
+ { "type": "core/media_card", "data": {
123
+ "image": "MEDIA_URL", "image_alt": "…", "image_side": "right",
124
+ "heading": "Huvudrubriken", "heading_level": "h2",
125
+ "text": "<p>Ingress …</p>",
126
+ "button_label": "Kontakta oss", "button_url": "/#kontakt"
127
+ } }
128
+ ] },
129
+ { "type": "core/section", "data": { "padding_y": "lg" }, "children": [
130
+ { "type": "core/heading", "data": { "text": "Så funkar det", "level": "h2", "align": "center" } },
131
+ { "type": "core/grid", "data": { "cols": 3, "gap": "lg" }, "children": [
132
+ { "type": "core/step_card", "data": { "number": "1", "title": "…", "text": "<p>…</p>" } },
133
+ { "type": "core/step_card", "data": { "number": "2", "title": "…", "text": "<p>…</p>" } },
134
+ { "type": "core/step_card", "data": { "number": "3", "title": "…", "text": "<p>…</p>" } }
135
+ ] }
136
+ ] },
137
+ { "type": "core/cta", "data": {
138
+ "heading": "Redo att börja?",
139
+ "primary_label": "Kontakta oss", "primary_url": "/kontakt"
140
+ } }
141
+ ]
142
+ ```
143
+
144
+ Block-palette guidance (verify against `list_block_types` — the source of
145
+ truth):
146
+
147
+ - **Hero:** `core/media_card` inside a white section (image beside copy),
148
+ or `core/hero` (eyebrow/heading/subheading + `primary_*`/`secondary_*`
149
+ buttons — rendered server-side; `layout: split-right` puts the image
150
+ beside the text). For a plain text hero: section + heading + prose +
151
+ button.
152
+ - **`core/heading`** decouples `level` (h1–h6, semantics) from `size`
153
+ (visual) — exactly one `level: h1` per page.
154
+ - **Images:** `core/image` with an uploaded media URL. The build pipeline
155
+ automatically emits responsive `<picture>` with AVIF/WebP variants
156
+ (run `generate_image_variants` after upload) — the in-portal preview
157
+ shows a plain `<img>`, the deployed site gets the upgrade. Use the
158
+ `radius` field for rounded corners.
159
+ - **Repeaters/listings:** `core/collection_list`, `gallery`,
160
+ `feature_grid` etc. — alias blocks over `core/repeater`. Use these for
161
+ collection-driven content instead of hand-writing listing markup.
162
+ - **Forms:** `create_form`, then a `core/html` block carrying the plain
163
+ `<form method="POST" action={submit_url}>` embed with the hidden
164
+ `_token` — see `tr-forms`.
165
+ - **`core/html`** is the escape hatch for the genuinely unique thing —
166
+ not a default. If you reach for it more than once or twice per page,
167
+ note why (that's block-library feedback).
168
+
169
+ Slot containers (`core/columns`, `core/tabs`): populate them either by
170
+ passing the whole tree inline (`block={ type: 'core/columns', slots:
171
+ [[…],[…]] }`) or incrementally with `add_block parent_id=<columns-id>
172
+ slot_index=0|1`. Requires template_capabilities_version ≥ 0.15.2 — on
173
+ older sites use `core/grid` (children flow into columns) instead.
174
+
175
+ Known limitations (honest list — don't fight them):
176
+
177
+ - **`core/icon` / `core/icon_box` icons don't render yet** (pending icon
178
+ pipeline). Use `step_card` numbers, emoji, or CSS markers.
179
+
180
+ Theming: block primitives render neutral. Brand color/typography comes
181
+ from settings (step 2). For page-specific polish (e.g. a colored card
182
+ treatment), a single `core/html` block with a small `<style>` scoped to
183
+ `[data-bid]`/section selectors is acceptable — keep it minimal and note
184
+ it in your log.
185
+
186
+ ### 5. Inner pages
187
+
188
+ Same pattern: `create_page` with `content_mode: "blocks"` and a section
189
+ tree. Standard set: Om oss, Tjänster, Kontakt — or what the brief says.
190
+ Default new pages to `status: "draft"`; publish after review.
191
+
192
+ ### 5b. If the legacy site is still live, scrape canonical content
193
+
194
+ For pages with canonical text (privacy policy, terms, about), `WebFetch`
195
+ the live page and carry the text verbatim — don't rewrite legal copy
196
+ from memory. Convert to prose blocks (or one `core/html` for complex
197
+ legacy markup).
198
+
199
+ ### 6. Preview + iterate
200
+
201
+ ```
202
+ get_preview_link # signed URL for browser review
203
+ get_page_preview page_id="home" # rendered HTML for structural checks
204
+ ```
205
+
206
+ Share the preview link. Iterate on feedback with `update_block` /
207
+ `add_block` / `move_block` — that's the point of block mode: surgical
208
+ edits, not full-page rewrites.
209
+
210
+ ### 7. Deploy
197
211
 
198
212
  When the user approves:
199
213
  ```
200
214
  trigger_deploy
201
- get_deploy_status job_id=<id> # poll until status=succeeded
215
+ get_deploy_status job_id=<id>
202
216
  ```
203
217
 
204
- ## Design conventions
205
-
206
- - **One `<h1>` per page.** SEO and screen reader requirement.
207
- - **CSS variables for colors.** Always use `var(--color-primary)` etc. in
208
- partial `<style>` blocks — never hardcode hex values.
209
- - **Responsive layout.** Use `max-width` on containers, `display:flex`
210
- or `display:grid` for layouts, and `clamp()` for fluid type.
211
- - **Scoped styles.** Put `<style>` at the bottom of partial HTML.
212
- Styles apply to the whole page; prefix class names with the partial ID
213
- to avoid collisions (`site-header__*`, `site-footer__*`).
214
- - **Page-scope class on every page body.** Wrap `html_content` in
215
- `<article class="my-page">` and prefix all your page-specific rules
216
- with `.my-page`. The renderer's `.page-content` layout shell sets
217
- width + padding at normal specificity, so a scope class keeps your
218
- rules cleanly above the defaults and the next agent can refactor a
219
- single page without bleed-through.
220
- - **Inherit `tr-brand`'s Step 4b section/layout defaults.** Full-bleed
221
- sections via negative-margin escape, one signal per section boundary
222
- (bg shift OR divider, not both), cards drop bg on `.section.alt`,
223
- gradient-clipped headings use `line-height: 1.1+` to keep descenders
224
- inside the box. See the brand skill for the full list — don't
225
- re-derive.
226
- - **Don't simplify data during import.** When porting content from an
227
- existing site, preserve full fidelity at the data layer. Don't
228
- ASCII-fold slugs into display labels (`affärsutveckling` ≠
229
- `affarsutveckling`), don't truncate descriptions, don't drop
230
- metadata fields you "won't use yet". Carry titles verbatim from
231
- source; derive slug-from-title only for the URL, not the label.
232
- - **Minimal JS.** The platform doesn't bundle JS. If you need interactions,
233
- write a small `<script>` inside the partial or page; keep it under 50
234
- lines. Avoid external CDN dependencies in page bodies.
218
+ ## Secondary path: HTML mode
219
+
220
+ For migrated legacy pages or a hand-crafted one-off, `content_mode:
221
+ "html"` still works. Rules that apply there (and only there):
222
+
223
+ - Wrap the body in a page-scope `<article class="my-page">` and prefix
224
+ selectors with it the `.page-content` shell sets width/padding at
225
+ normal specificity.
226
+ - HTML-mode bodies are container-constrained; full-bleed requires the
227
+ negative-margin escape (`margin-left: calc(50% - 50vw); width: 100vw`)
228
+ never combine with `overflow-x: clip` on a wrapper.
229
+ - `set_page_mode` flips a page between modes;
230
+ `convert_page_to_blocks` does a heuristic HTML→blocks conversion.
235
231
 
236
232
  ## Pitfalls
237
233
 
238
- - **Don't create pages that already exist.** Run `list_pages` first;
239
- if the slug is taken, use `update_page` instead of `create_page`.
240
- - **Don't hardcode text from settings into partials.** If `site_name`
241
- is "Acme", the header partial should have literal "Acme" in it —
242
- there's no template engine. When the name changes the user updates
243
- the partial.
244
- - **Google Fonts names are case-sensitive.** "Playfair Display" works;
245
- "playfair display" or "Playfair-Display" do not.
246
- - **Empty `custom_css` field.** If you set `custom_css: ""` it's
247
- stored as an empty string, not removed. Use it only when you have
248
- global styles that span both partials and pages (e.g. utility classes,
249
- `@keyframes`, `:root` overrides for things not in `colors`/`fonts`).
234
+ - **Don't create pages that already exist** `list_pages` first; use
235
+ `update_page` if the slug is taken.
236
+ - **`update_page` takes a `patch` object**, not flat fields.
237
+ - **Don't hardcode block field names from memory** schemas are
238
+ per-site (`list_block_types`/`read_block_type`).
239
+ - **One `level: h1` per page** (core/heading) — SEO + screen readers.
240
+ - **Don't simplify data during import** preserve Swedish characters in
241
+ labels (`affärsutveckling`, not the ASCII-folded slug), keep titles
242
+ verbatim; slugs are derived for URLs only.
243
+ - **Minimal JS.** Inline `<script>` in page content is stripped by the
244
+ sanitizer. Interactivity ships via block-type `script` (requires the
245
+ site's AI-scripts opt-in) or the human-managed `scripts_body_end`.