@typeroll/mcp-server 0.15.0 → 0.15.2

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
@@ -257,6 +257,28 @@ add_block page_id=home parent_id="blk_xyz" block={
257
257
  }
258
258
  ```
259
259
 
260
+ Slot containers (`container: "slots"` — `core/columns`, `core/tabs`)
261
+ hold their children in per-slot lists, not in `children`. Two ways to
262
+ populate them (both require template_capabilities_version ≥ 0.15.2):
263
+
264
+ ```
265
+ # Inline — pass the whole subtree in one call:
266
+ add_block page_id=home block={
267
+ type: 'core/columns', data: { ratio: '1-1' },
268
+ slots: [
269
+ [{ type: 'core/prose', data: { html: '<p>Left column</p>' } }],
270
+ [{ type: 'core/image', data: { src: '…' } }],
271
+ ]
272
+ }
273
+
274
+ # Incrementally — slot_index picks the slot (0-based, defaults to 0):
275
+ add_block page_id=home block={ type: 'core/columns', data: {} }
276
+ # → { added_id: 'blk_cols' } — slots are auto-initialised to the type's arity
277
+ add_block page_id=home parent_id="blk_cols" slot_index=1 block={
278
+ type: 'core/prose', data: { html: '<p>Right column</p>' }
279
+ }
280
+ ```
281
+
260
282
  For an unfamiliar custom block, `read_block_type id="..."` gives the
261
283
  full field list (types, defaults, required) so you don't ship invalid
262
284
  `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.2",
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,189 @@ 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):
195
114
 
196
- ### 8. Deploy
115
+ ```json
116
+ [
117
+ { "type": "core/section", "data": { "background": "#ffffff", "padding_y": "lg" }, "children": [
118
+ { "type": "core/media_card", "data": {
119
+ "image": "MEDIA_URL", "image_alt": "…", "image_side": "right",
120
+ "heading": "Huvudrubriken", "heading_level": "h2",
121
+ "text": "<p>Ingress …</p>",
122
+ "button_label": "Kontakta oss", "button_url": "/#kontakt"
123
+ } }
124
+ ] },
125
+ { "type": "core/section", "data": { "padding_y": "lg" }, "children": [
126
+ { "type": "core/heading", "data": { "text": "Så funkar det", "level": "h2", "align": "center" } },
127
+ { "type": "core/grid", "data": { "cols": 3, "gap": "lg" }, "children": [
128
+ { "type": "core/step_card", "data": { "number": "1", "title": "…", "text": "<p>…</p>" } },
129
+ { "type": "core/step_card", "data": { "number": "2", "title": "…", "text": "<p>…</p>" } },
130
+ { "type": "core/step_card", "data": { "number": "3", "title": "…", "text": "<p>…</p>" } }
131
+ ] }
132
+ ] },
133
+ { "type": "core/cta", "data": {
134
+ "heading": "Redo att börja?",
135
+ "primary_label": "Kontakta oss", "primary_url": "/kontakt"
136
+ } }
137
+ ]
138
+ ```
139
+
140
+ Block-palette guidance (verify against `list_block_types` — the source of
141
+ truth):
142
+
143
+ - **Hero:** `core/media_card` inside a white section (image beside copy),
144
+ or `core/hero` (eyebrow/heading/subheading + `primary_*`/`secondary_*`
145
+ buttons — rendered server-side; `layout: split-right` puts the image
146
+ beside the text). For a plain text hero: section + heading + prose +
147
+ button.
148
+ - **`core/heading`** decouples `level` (h1–h6, semantics) from `size`
149
+ (visual) — exactly one `level: h1` per page.
150
+ - **Images:** `core/image` with an uploaded media URL. The build pipeline
151
+ automatically emits responsive `<picture>` with AVIF/WebP variants
152
+ (run `generate_image_variants` after upload) — the in-portal preview
153
+ shows a plain `<img>`, the deployed site gets the upgrade. Use the
154
+ `radius` field for rounded corners.
155
+ - **Repeaters/listings:** `core/collection_list`, `gallery`,
156
+ `feature_grid` etc. — alias blocks over `core/repeater`. Use these for
157
+ collection-driven content instead of hand-writing listing markup.
158
+ - **Forms:** `create_form`, then a `core/html` block carrying the plain
159
+ `<form method="POST" action={submit_url}>` embed with the hidden
160
+ `_token` — see `tr-forms`.
161
+ - **`core/html`** is the escape hatch for the genuinely unique thing —
162
+ not a default. If you reach for it more than once or twice per page,
163
+ note why (that's block-library feedback).
164
+
165
+ Slot containers (`core/columns`, `core/tabs`): populate them either by
166
+ passing the whole tree inline (`block={ type: 'core/columns', slots:
167
+ [[…],[…]] }`) or incrementally with `add_block parent_id=<columns-id>
168
+ slot_index=0|1`. Requires template_capabilities_version ≥ 0.15.2 — on
169
+ older sites use `core/grid` (children flow into columns) instead.
170
+
171
+ Known limitations (honest list — don't fight them):
172
+
173
+ - **`core/icon` / `core/icon_box` icons don't render yet** (pending icon
174
+ pipeline). Use `step_card` numbers, emoji, or CSS markers.
175
+
176
+ Theming: block primitives render neutral. Brand color/typography comes
177
+ from settings (step 2). For page-specific polish (e.g. a colored card
178
+ treatment), a single `core/html` block with a small `<style>` scoped to
179
+ `[data-bid]`/section selectors is acceptable — keep it minimal and note
180
+ it in your log.
181
+
182
+ ### 5. Inner pages
183
+
184
+ Same pattern: `create_page` with `content_mode: "blocks"` and a section
185
+ tree. Standard set: Om oss, Tjänster, Kontakt — or what the brief says.
186
+ Default new pages to `status: "draft"`; publish after review.
187
+
188
+ ### 5b. If the legacy site is still live, scrape canonical content
189
+
190
+ For pages with canonical text (privacy policy, terms, about), `WebFetch`
191
+ the live page and carry the text verbatim — don't rewrite legal copy
192
+ from memory. Convert to prose blocks (or one `core/html` for complex
193
+ legacy markup).
194
+
195
+ ### 6. Preview + iterate
196
+
197
+ ```
198
+ get_preview_link # signed URL for browser review
199
+ get_page_preview page_id="home" # rendered HTML for structural checks
200
+ ```
201
+
202
+ Share the preview link. Iterate on feedback with `update_block` /
203
+ `add_block` / `move_block` — that's the point of block mode: surgical
204
+ edits, not full-page rewrites.
205
+
206
+ ### 7. Deploy
197
207
 
198
208
  When the user approves:
199
209
  ```
200
210
  trigger_deploy
201
- get_deploy_status job_id=<id> # poll until status=succeeded
211
+ get_deploy_status job_id=<id>
202
212
  ```
203
213
 
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.
214
+ ## Secondary path: HTML mode
215
+
216
+ For migrated legacy pages or a hand-crafted one-off, `content_mode:
217
+ "html"` still works. Rules that apply there (and only there):
218
+
219
+ - Wrap the body in a page-scope `<article class="my-page">` and prefix
220
+ selectors with it the `.page-content` shell sets width/padding at
221
+ normal specificity.
222
+ - HTML-mode bodies are container-constrained; full-bleed requires the
223
+ negative-margin escape (`margin-left: calc(50% - 50vw); width: 100vw`)
224
+ never combine with `overflow-x: clip` on a wrapper.
225
+ - `set_page_mode` flips a page between modes;
226
+ `convert_page_to_blocks` does a heuristic HTML→blocks conversion.
235
227
 
236
228
  ## Pitfalls
237
229
 
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`).
230
+ - **Don't create pages that already exist** `list_pages` first; use
231
+ `update_page` if the slug is taken.
232
+ - **`update_page` takes a `patch` object**, not flat fields.
233
+ - **Don't hardcode block field names from memory** schemas are
234
+ per-site (`list_block_types`/`read_block_type`).
235
+ - **One `level: h1` per page** (core/heading) — SEO + screen readers.
236
+ - **Don't simplify data during import** preserve Swedish characters in
237
+ labels (`affärsutveckling`, not the ASCII-folded slug), keep titles
238
+ verbatim; slugs are derived for URLs only.
239
+ - **Minimal JS.** Inline `<script>` in page content is stripped by the
240
+ sanitizer. Interactivity ships via block-type `script` (requires the
241
+ site's AI-scripts opt-in) or the human-managed `scripts_body_end`.