@typeroll/mcp-server 0.11.0 → 0.13.0

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
@@ -84,6 +84,18 @@ maps to one HTTP endpoint; the actual logic runs in the customer's portal
84
84
  before working with blocks — never hardcode block ids or field names,
85
85
  the available set is per-site.
86
86
 
87
+ Two specifics worth knowing:
88
+ - **`core/html`** is the raw-HTML escape hatch for block-mode pages —
89
+ one `html` field rendered verbatim (then sanitized like HTML-mode
90
+ content). Use it for the genuinely unique thing no block covers: a
91
+ form embed with its signed `_token`, a third-party widget's markup,
92
+ a hand-crafted one-off. Prefer real blocks when one fits.
93
+ - **`script` on custom block types** (create/update_block_type) is
94
+ honoured only when the site has "Allow AI to write block scripts"
95
+ enabled — a human-set portal setting. When it's off your script is
96
+ stripped and the response carries a warning; relay it to the user
97
+ instead of retrying.
98
+
87
99
  - **Redirects.** `from_path → to_path` with status code 301 / 302.
88
100
  Auto-created when you change a page's slug.
89
101
 
package/README.md CHANGED
@@ -96,11 +96,20 @@ the full reference + concrete operation recipes.
96
96
  - **Discovery** — `get_site`, `update_site` (name/slug/domain), `list_versions`,
97
97
  `read_site_settings`, `update_site_settings`.
98
98
  - **Pages** — list, read, batch-read, create, update (PATCH), replace
99
- (PUT), batch-update, delete, clone, get-blocks, get-preview.
99
+ (PUT), batch-update, delete, clone, get-preview, `set_page_mode`
100
+ (flip between blocks/html), `convert_page_to_blocks`.
101
+ - **Blocks (instances)** — `get_page_blocks`, `add_block`,
102
+ `update_block`, `move_block`, `remove_block`, `duplicate_block`,
103
+ `set_block_responsive`. All take a `target` (page, partial, page
104
+ template, or collection item-template), so one tool family edits
105
+ every block container.
100
106
  - **Global blocks (partials)** — list (summary mode by default), read,
101
107
  create free block, update, replace, delete, find-pages-using-block.
102
- - **Block types** — list, read, find-pages-using-block-type. Surface
103
- for the future block editor.
108
+ - **Block types** — list, read, create, update, delete,
109
+ find-pages-using-block-type, plus `.tcblocks` export/import. Custom
110
+ client-side JS (`script`) is honoured only when the site has enabled
111
+ "Allow AI to write block scripts" (a human-set portal setting) —
112
+ otherwise it's stripped with a warning.
104
113
  - **Collections + items** — create/update/delete the collection schema
105
114
  itself (incl. `route_template` for per-item URLs); list/read/batch-
106
115
  read/create/update/delete items.
@@ -115,7 +124,13 @@ the full reference + concrete operation recipes.
115
124
  vision model).
116
125
  - **Redirects** — list, create, delete. Plus automatic 301 on slug change.
117
126
  - **Forms** — list, read, create, update, delete, list submissions.
118
- - **Settings** read + patch (scripts and custom CSS not exposed).
127
+ Read/create responses include `submit_token` + `submit_url` a plain
128
+ `<form method="POST">` with a hidden `_token` input is a fully working
129
+ no-JS embed (the endpoint answers form posts with an HTML
130
+ confirmation page).
131
+ - **Settings** — read + patch, including `scripts_head` /
132
+ `scripts_body_end` / `custom_css` (trusted because the caller holds an
133
+ API key; the in-portal chat AI does NOT get these).
119
134
  - **Search** — `search_pages` with substring or regex.
120
135
  - **Bulk** — `bulk_replace_text` with dry-run.
121
136
  - **Branches** — create, read, delete, merge. Branch deploys get their
@@ -138,17 +153,21 @@ The MCP server is purely an ergonomics layer on top of that.
138
153
 
139
154
  ## Security model
140
155
 
141
- - API keys are **site-scoped**. A key cannot read or write another site
142
- in the same organization server-side authoritative.
156
+ - API keys are **site-scoped or org-scoped** (see "Key scopes" above)
157
+ enforced server-side. A site-scoped key cannot touch any other site;
158
+ an org-scoped key reaches the org's own sites plus sites explicitly
159
+ shared into the org, with the share's permission level applied.
143
160
  - All write calls (`POST`, `PUT`, `PATCH`, `DELETE`) are **audit-logged**
144
161
  with the key prefix, IP, method, path, and status. Reads are not
145
162
  logged (cost vs. value).
146
163
  - **Rate limits**: 600 reads/min, 60 writes/min per key. 429 responses
147
164
  carry `Retry-After` headers.
148
165
  - **HTML sanitization** happens at save time on the server — `<script>`,
149
- event handlers, and `javascript:` URLs are stripped. The customer's
150
- `scripts_*` and `custom_css` settings are read-stripped and write-dropped
151
- by the API.
166
+ event handlers, and `javascript:` URLs are stripped from page/partial
167
+ content (including `core/html` block output). The scriptable surfaces
168
+ (`scripts_*`, `custom_css`, block-type `script`) are deliberate
169
+ exceptions: the first are writable with an API key, and block-type
170
+ scripts additionally require the site's per-site opt-in.
152
171
  - Keys can be **revoked** at any time from the portal. Revocation takes
153
172
  effect on the next request (no in-flight requests get cancelled, but
154
173
  the next one returns 401).
@@ -158,7 +177,8 @@ The MCP server is purely an ergonomics layer on top of that.
158
177
  - Full end-to-end production setup recipe with troubleshooting:
159
178
  [docs/claude-code-mcp-setup.md](../../docs/claude-code-mcp-setup.md)
160
179
  - Agent operations briefing: [AGENTS.md](./AGENTS.md)
161
- - Boilerplate skills (migration, content, images, directory, redesign):
180
+ - Boilerplate skills (site building, brand, forms, SEO, blog,
181
+ collections, migration, image generation, redesign, …):
162
182
  [skills/](./skills/)
163
183
 
164
184
  ## License
package/dist/index.js CHANGED
@@ -15,22 +15,9 @@
15
15
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
16
16
  import { TyperollClient } from './client.js';
17
17
  import { runInstallSkillsCli } from './install-skills.js';
18
+ import { resolveSiteId } from './resolve-site-id.js';
18
19
  import { buildServer } from './server.js';
19
20
  const VERSION = '0.7.12';
20
- async function resolveSiteId(client) {
21
- const fromEnv = process.env.TYPEROLL_SITE_ID?.trim();
22
- if (fromEnv)
23
- return fromEnv;
24
- const res = await client.rootGet('sites');
25
- if (!res.sites || res.sites.length === 0) {
26
- throw new Error('No sites returned for this API key. Check the key is valid.');
27
- }
28
- if (res.sites.length > 1) {
29
- throw new Error(`This key authorises ${res.sites.length} sites; set TYPEROLL_SITE_ID to pick one. ` +
30
- 'Org-scoped keys span multiple sites — stdio currently binds to one at a time.');
31
- }
32
- return res.sites[0].id;
33
- }
34
21
  function bail(message) {
35
22
  console.error(`typeroll-mcp: ${message}`);
36
23
  process.exit(1);
@@ -0,0 +1,29 @@
1
+ // Discover which site a stdio invocation binds to.
2
+ //
3
+ // TYPEROLL_SITE_ID wins when set. Otherwise we ask GET /v1/sites: a
4
+ // site-scoped key returns exactly one entry; an org-scoped key can return
5
+ // many, in which case TYPEROLL_SITE_ID is required so this stdio invocation
6
+ // maps onto one specific site.
7
+ export async function resolveSiteId(client) {
8
+ const fromEnv = process.env.TYPEROLL_SITE_ID?.trim();
9
+ if (fromEnv)
10
+ return fromEnv;
11
+ const res = await client.rootGet('sites');
12
+ if (!res.sites || res.sites.length === 0) {
13
+ throw new Error('No sites returned for this API key. Check the key is valid.');
14
+ }
15
+ if (res.sites.length > 1) {
16
+ throw new Error(`This key authorises ${res.sites.length} sites; set TYPEROLL_SITE_ID to pick one. ` +
17
+ 'Org-scoped keys span multiple sites — stdio currently binds to one at a time.');
18
+ }
19
+ // Defense in depth: portals before the /v1/sites org-key fix returned a
20
+ // single placeholder entry with an empty id for org-scoped keys. Binding
21
+ // to "" would make every subsequent call fail incomprehensibly — reject
22
+ // it with an actionable message instead.
23
+ const id = res.sites[0].id?.trim();
24
+ if (!id) {
25
+ throw new Error('The portal returned a site with an empty id (org-scoped key against an older portal). ' +
26
+ 'Set TYPEROLL_SITE_ID to pick a site explicitly, or upgrade the portal.');
27
+ }
28
+ return id;
29
+ }
@@ -84,15 +84,15 @@ export const blockTypeTools = [
84
84
  }),
85
85
  },
86
86
  // ─── BlockType authoring — CREATE / UPDATE / DELETE ──────────────────
87
- // The chat AI surface intentionally omits the `script` field on these
88
- // three tools. Scripts run with full DOM access in every visitor's
89
- // browser; AI-authored blocks must not carry custom JS. A human can
90
- // add a script later via the portal UI (consent gate) or via the
91
- // cookie-auth API directly. Origin is stamped 'ai' so audit + UI
92
- // distinguish these from human-authored types.
87
+ // `script` runs with full DOM access in every visitor's browser, so
88
+ // agent-authored scripts are gated server-side on a per-site human
89
+ // opt-in (Site.ai_scripts_enabled, set in the portal's settings UI).
90
+ // The tools expose the field; when the site hasn't opted in, the API
91
+ // strips it and returns a warning. Origin is stamped 'ai' so audit +
92
+ // UI distinguish these from human-authored types.
93
93
  {
94
94
  name: 'create_block_type',
95
- description: "Create a new custom block type usable on this site. Origin is stamped 'ai' automatically. The block ships immediately to every page editor + the renderer's registry. **Custom JS is NOT exposed here** if a script is genuinely needed, a human must add it later via the portal UI. Returns the created BlockType.",
95
+ description: "Create a new custom block type usable on this site. Origin is stamped 'ai' automatically. The block ships immediately to every page editor + the renderer's registry. Custom JS via `script` requires the site's \"Allow AI to write block scripts\" setting (human-set in the portal) — when it's off, the field is ignored and the response carries a warning. Returns the created BlockType.",
96
96
  inputSchema: {
97
97
  name: z.string().describe('Machine name (lowercase kebab/underscore, 1-64 chars). Becomes the id.'),
98
98
  label: z.string().optional().describe('Display label (defaults to name).'),
@@ -112,6 +112,7 @@ export const blockTypeTools = [
112
112
  schema: z.array(z.unknown()).optional().describe('FieldDefinition[]. Each field has name, type, label, optional required/default/options/responsive.'),
113
113
  template: z.string().optional().describe('HTML with {{field}}, {{{field}}}, {{=tag}}, {{children}}, {{slot:NAME}} substitutions.'),
114
114
  styles: z.string().optional().describe('Block-scoped CSS. Selectors should start with [data-block="<name>"].'),
115
+ script: z.string().optional().describe('Client-side JS shipped with the block (runs on the published site). Only honoured when the site has enabled AI block scripts; otherwise stripped with a warning in the response.'),
115
116
  version: versionParam,
116
117
  },
117
118
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -126,7 +127,7 @@ export const blockTypeTools = [
126
127
  },
127
128
  {
128
129
  name: 'update_block_type',
129
- description: "Update fields of an existing custom block type. Core blocks (id starts with 'core/') are read-only — they're managed in platform code. Schema/template/styles replace wholesale (no deep merge); other fields shallow-merge. **Custom JS is NOT exposed here** to add or change a script, edit via portal UI.",
130
+ description: "Update fields of an existing custom block type. Core blocks (id starts with 'core/') are read-only — they're managed in platform code. Schema/template/styles replace wholesale (no deep merge); other fields shallow-merge. The `script` field follows the same site-setting gate as create_block_type.",
130
131
  inputSchema: {
131
132
  type_id: z.string().describe('Block type id (custom or third_party). Cannot be a core/* block.'),
132
133
  label: z.string().optional(),
@@ -143,6 +144,7 @@ export const blockTypeTools = [
143
144
  schema: z.array(z.unknown()).optional(),
144
145
  template: z.string().optional(),
145
146
  styles: z.string().optional(),
147
+ script: z.string().optional().describe('Only honoured when the site has enabled AI block scripts; otherwise stripped with a warning.'),
146
148
  version: versionParam,
147
149
  },
148
150
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -29,6 +29,7 @@ export const settingsTools = [
29
29
  tagline: z.string().optional(),
30
30
  logo: z.string().optional(),
31
31
  favicon: z.string().optional(),
32
+ apple_touch_icon: z.string().optional().describe('URL to a 180x180 PNG for iOS/Android home-screen bookmarks. Emitted as <link rel="apple-touch-icon">.'),
32
33
  default_seo_suffix: z.string().optional(),
33
34
  language: z.string().optional().describe('BCP-47 tag (e.g. "en", "sv", "en-GB"). Drives <html lang> on the rendered site.'),
34
35
  robots_txt: z.string().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
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": {
@@ -108,6 +108,24 @@ update_site_settings {
108
108
 
109
109
  Read back to confirm: `read_site_settings`.
110
110
 
111
+ ### Site icons — always propose them, never leave them empty
112
+
113
+ Every site gets a favicon + apple touch icon as part of brand setup:
114
+
115
+ 1. **Brand assets exist** (favicon-*.png, app icon, symbol): upload the
116
+ right sizes via `upload_media_inline` (favicon: 32–64px PNG or SVG;
117
+ apple touch icon: 180×180 PNG) and set BOTH in one call:
118
+ `update_site_settings { "favicon": "<url>", "apple_touch_icon": "<url>" }`.
119
+ 2. **No icon assets:** derive a proposal instead of skipping — crop the
120
+ logo's symbol to a square and resize locally (`sips -z 180 180 in.png
121
+ --out icon-180.png` on macOS, or ImageMagick), or generate a simple
122
+ icon candidate with the imagegen lab (see `tr-imagegen`; respect the
123
+ style profile, no text). Upload, set, and tell the user it's a
124
+ proposal they can swap.
125
+
126
+ A site shipping with the browser's default globe icon is a build gap —
127
+ treat icons like the logo: part of done.
128
+
111
129
  ## Step 5 — Update partials to use the new palette
112
130
 
113
131
  Partials that hardcoded hex colors need updating. Fetch the header:
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: tr-imagegen
3
+ description: Use when the user wants to generate images for a Typeroll site with AI models (Gemini, OpenAI, Higgsfield) — hero images, illustrations, section backgrounds, og-images. Triggers on "generera bilder", "generate images", "skapa en hero-bild", "AI-bilder", "bildgenerering", or when a brief calls for imagery that doesn't exist in assets/. Covers the local lab loop (generate → review → pick) and uploading winners to the Typeroll media library.
4
+ ---
5
+
6
+ # Generate images for a Typeroll site (local lab → media library)
7
+
8
+ Image generation runs **locally** in the site workdir — provider API keys
9
+ live in the folder's `.env`, candidates land in `images/lab/`, and only
10
+ the picked winners are uploaded to the Typeroll media library via the
11
+ regular media tools (see `tr-images` for the upload/variants half).
12
+
13
+ Why local: you can look at the candidates (Read the files), iterate on
14
+ prompts cheaply, and never ship a key or a reject anywhere.
15
+
16
+ ## Folder convention
17
+
18
+ ```
19
+ <site>/
20
+ ├── .env # provider keys — GITIGNORED, never committed
21
+ ├── prompts/
22
+ │ └── image-style.md # the site's image style profile (see below)
23
+ └── images/
24
+ └── lab/ # generated candidates — gitignored, disposable
25
+ ```
26
+
27
+ `.env` keys (only the ones the user has — check before assuming):
28
+
29
+ ```
30
+ GEMINI_API_KEY=...
31
+ OPENAI_API_KEY=...
32
+ ```
33
+
34
+ (Higgsfield needs no key here — it connects as an MCP server, see below.)
35
+
36
+ Load them per-command (`source .env` doesn't persist between Bash calls):
37
+
38
+ ```bash
39
+ export $(grep -v '^#' .env | xargs) # prepend to each generation command
40
+ ```
41
+
42
+ ## The style profile — prompts/image-style.md
43
+
44
+ Every site gets ONE style profile that you **prepend to every image
45
+ prompt**. This is what keeps 20 images generated across 5 sessions
46
+ looking like one site. Derive it from `assets/brand.md` + the brief if
47
+ it doesn't exist yet, and confirm it with the user before generating at
48
+ scale. Keep it short (5–10 lines): art direction, palette, mood,
49
+ photography vs illustration, what to avoid.
50
+
51
+ Example shape:
52
+
53
+ ```markdown
54
+ # Bildstil — <Sajtnamn>
55
+ Varm, folklig illustration med mjuka rundade former. Platt 2D med
56
+ subtila skuggor — ingen 3D, ingen fotorealism. Palett: kobolt #1F4FB8,
57
+ sol #FFC83D, grädde #FFF8EC; accenter sparsamt. Människor: enkla,
58
+ inkluderande, glada — inga karikatyrer. Undvik: stockfoto-känsla, text
59
+ i bilden, logotyper, watermarks.
60
+ ```
61
+
62
+ ## Generate candidates
63
+
64
+ Name candidates descriptively: `images/lab/<motiv>-<modell>-<n>.png`.
65
+ Generate 3–6 candidates per slot (mix models when several keys exist),
66
+ then **Read the files to actually look at them** before showing the
67
+ user your shortlist.
68
+
69
+ ### Gemini (gemini-2.5-flash-image)
70
+
71
+ ```bash
72
+ curl -s "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image:generateContent" \
73
+ -H "x-goog-api-key: $GEMINI_API_KEY" -H 'Content-Type: application/json' \
74
+ -d '{
75
+ "contents": [{"parts": [{"text": "<STYLE PROFILE>\n\n<MOTIF PROMPT>"}]}],
76
+ "generationConfig": {"imageConfig": {"aspectRatio": "16:9"}}
77
+ }' | jq -r '.candidates[0].content.parts[] | select(.inlineData) | .inlineData.data' \
78
+ | base64 -d > images/lab/hero-gemini-1.png
79
+ ```
80
+
81
+ Aspect ratios: `1:1`, `16:9`, `4:3`, `3:4`, `9:16`. Gemini also does
82
+ image *editing* — pass an existing image as an `inlineData` part plus an
83
+ instruction to restyle/extend it (useful for "same illustration but
84
+ winter").
85
+
86
+ ### OpenAI (gpt-image-1)
87
+
88
+ ```bash
89
+ curl -s https://api.openai.com/v1/images/generations \
90
+ -H "Authorization: Bearer $OPENAI_API_KEY" -H 'Content-Type: application/json' \
91
+ -d '{
92
+ "model": "gpt-image-1",
93
+ "prompt": "<STYLE PROFILE>\n\n<MOTIF PROMPT>",
94
+ "size": "1536x1024",
95
+ "quality": "high"
96
+ }' | jq -r '.data[0].b64_json' | base64 -d > images/lab/hero-openai-1.png
97
+ ```
98
+
99
+ Sizes: `1024x1024`, `1536x1024` (landscape), `1024x1536` (portrait).
100
+
101
+ ### Higgsfield (MCP server — no API key)
102
+
103
+ Higgsfield exposes its models through a hosted MCP server at
104
+ `https://mcp.higgsfield.ai/mcp` (OAuth-protected — first connect opens a
105
+ browser login; no secret lands in any file). Add it next to the
106
+ typeroll server in the site folder's `.mcp.json`:
107
+
108
+ ```json
109
+ "higgsfield": { "type": "http", "url": "https://mcp.higgsfield.ai/mcp" }
110
+ ```
111
+
112
+ Then use its tools directly — list what's available rather than
113
+ assuming tool names. Save/download outputs into `images/lab/` with the
114
+ same naming convention, and prepend the style profile to prompts here
115
+ too. **Headless caveat:** OAuth-protected MCP servers need an existing
116
+ login session on the machine — connect once interactively before
117
+ relying on it in a headless run. (The same URL works as a custom
118
+ connector in Claude Desktop, for editors who don't use Claude Code.)
119
+
120
+ ## Review → pick → upload
121
+
122
+ 1. **Look at every candidate** (Read the image files) and write one line
123
+ per candidate in `build-log.md` (keep/reject + why).
124
+ 2. Show the user the shortlist (file paths) and let them pick — unless
125
+ they've delegated the pick to you.
126
+ 3. Upload winners with the regular media tools (see `tr-images`):
127
+ - small files → `upload_media_inline` (base64);
128
+ - larger → `create_upload_url` + HTTP PUT + `finalize_media`.
129
+ Give real `alt` text and a descriptive filename at upload time.
130
+ 4. `generate_image_variants` for responsive sizes when the image is
131
+ placed full-width.
132
+ 5. Place via `update_block` (`core/image`, hero fields, …) or page HTML.
133
+ 6. `images/lab/` is disposable — leave rejects there; never upload them.
134
+
135
+ ## Pitfalls
136
+
137
+ - **Never put text in generated images** (headlines, buttons) — text is
138
+ HTML's job; generated text renders as gibberish in non-English. And
139
+ models sneak text onto surfaces where it "feels natural" — jerseys,
140
+ signs, banners, packages — even when the prompt doesn't ask for any.
141
+ Forbid it explicitly in the prompt ("plain unmarked clothing, no
142
+ text, no letters, no numbers anywhere in the image") and make the
143
+ same rule part of every site's style profile.
144
+ - **Don't commit `.env` or `images/lab/`** — both are gitignored by the
145
+ kit convention; keep it that way.
146
+ - **Cost discipline:** generation costs real money per image. Batch
147
+ thoughtfully (3–6 per slot, not 20), and reuse via Gemini's
148
+ edit-an-image mode instead of regenerating from scratch.
149
+ - **Licensing/provenance:** AI-generated imagery is fine for site
150
+ decoration, but never generate fake "photos" of real people, products
151
+ the customer doesn't sell, or anything presented as documentary fact.
152
+ - **The style profile is the contract.** If the user rejects a batch on
153
+ style grounds, fix `prompts/image-style.md` first, then regenerate —
154
+ don't just tweak one prompt.