@typeroll/mcp-server 0.26.2 → 0.28.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/dist/server.js CHANGED
@@ -22,6 +22,7 @@ import { deployTools } from './tools/deploy.js';
22
22
  import { previewTools } from './tools/preview.js';
23
23
  import { blockTypeTools } from './tools/block-types.js';
24
24
  import { pageBlockTools } from './tools/page-blocks.js';
25
+ import { workingCopyTools } from './tools/working-copy.js';
25
26
  import { settingsTools } from './tools/settings.js';
26
27
  import { siteTools } from './tools/sites.js';
27
28
  import { domainTools } from './tools/domain.js';
@@ -72,13 +73,19 @@ playbook ships with this server — use it:
72
73
  the canonical how-to; don't improvise what a skill already covers.
73
74
  2. Discover before you write: get_site, read_site_settings, list_pages,
74
75
  list_block_types. Never hardcode block ids or field names — they're per-site.
75
- 3. Branch first for anything larger than a small edit: create_branch, pass
76
+ 3. THE BUFFER MODEL: every content write (pages, blocks, partials,
77
+ collection items) lands in an unsaved per-doc DRAFT — deploys and plain
78
+ previews see saved content only. Iterate freely, view your drafts with
79
+ include_working_copy on the preview tools, then SAVE explicitly:
80
+ commit_working_copy (or save:true on the write call) when the user
81
+ approves. Status changes and structural ops apply immediately.
82
+ 4. Branch first for anything larger than a small edit: create_branch, pass
76
83
  version=<id> on every subsequent call, merge_branch once approved. Nothing
77
84
  touches the live site until then.
78
- 4. Pages default to block mode. Build with add_block/update_block; make layouts
85
+ 5. Pages default to block mode. Build with add_block/update_block; make layouts
79
86
  responsive per breakpoint with set_block_responsive (grid columns, icon-box
80
87
  layout, … take { mobile, tablet, laptop, desktop, wide } values).
81
- 5. No site yet? With an org-scoped key, create_site bootstraps one.
88
+ 6. No site yet? With an org-scoped key, create_site bootstraps one.
82
89
 
83
90
  If anything here conflicts with what a tool returns, trust the tool. Every
84
91
  tool's own description carries its specifics.
@@ -101,6 +108,7 @@ export function buildServer(options) {
101
108
  ...partialTools,
102
109
  ...blockTypeTools,
103
110
  ...pageBlockTools,
111
+ ...workingCopyTools,
104
112
  ...collectionTools,
105
113
  ...mediaTools,
106
114
  ...redirectTools,
@@ -6,13 +6,14 @@ function v(version) {
6
6
  export const bulkTools = [
7
7
  {
8
8
  name: 'bulk_replace_text',
9
- description: 'Replace a literal substring or regex across pages in one call. ALWAYS run with dry_run=true first and show the sample_diffs to the user before running the real call. Writes go through the normal pipeline (SEO transforms + revision snapshots). Response shape: { dry_run, updated, total_matches, pages_with_matches, sample_diffs_shown, additional_pages_with_matches, sample_diffs[], skipped (deprecated) }.',
9
+ description: 'Replace a literal substring or regex across pages in one call. ALWAYS run with dry_run=true first and show the sample_diffs to the user before running the real call. BUFFER MODEL: replacements land in each page\'s unsaved DRAFT; after the user approves the diffs, run with save:true to commit each touched page (revision snapshots + SEO transforms included). Response: { dry_run, updated, saved, total_matches, pages_with_matches, sample_diffs_shown, additional_pages_with_matches, sample_diffs[], skipped (deprecated) }.',
10
10
  inputSchema: {
11
11
  pattern: z.string().min(1).describe('Literal substring (default) or JS regex source if regex=true.'),
12
12
  replacement: z.string(),
13
13
  regex: z.boolean().optional().describe('Treat pattern as a regex source. Always case-insensitive + global.'),
14
14
  page_ids: z.array(z.string()).optional().describe('Restrict to these page ids. Omit to apply to every matching page.'),
15
15
  dry_run: z.boolean().optional(),
16
+ save: z.boolean().optional().describe('Commit every touched page\'s draft in the same call — the usual choice after the user approved the dry-run diffs.'),
16
17
  version: versionParam,
17
18
  },
18
19
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -156,12 +156,13 @@ export const collectionTools = [
156
156
  },
157
157
  {
158
158
  name: 'update_collection_item',
159
- description: 'Update a collection item. Fields outside the schema are dropped.',
159
+ description: 'Update a collection item. Fields outside the schema are dropped. BUFFER MODEL: field values land in the item\'s unsaved DRAFT (status applies immediately); pass save:true to commit in the same call, or commit_working_copy later.',
160
160
  inputSchema: {
161
161
  collection: z.string(),
162
162
  item_id: z.string(),
163
163
  fields: z.record(z.unknown()).optional(),
164
164
  status: z.enum(['draft', 'published']).optional(),
165
+ save: z.boolean().optional().describe('Also SAVE (commit) the draft in the same call.'),
165
166
  version: versionParam,
166
167
  },
167
168
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -6,7 +6,7 @@ function v(version) {
6
6
  export const deployTools = [
7
7
  {
8
8
  name: 'trigger_deploy',
9
- description: "Enqueue a deploy (static-page build → hosting) of the currently active version. Returns a job_id immediately; poll get_deploy_status until the status leaves queued/running. NOT for previewing iterative design/content edits — that's what get_preview_link is for (it renders live from the DB with no build). Reserve trigger_deploy for publishing, a stakeholder link to the compiled site, or pre-merge validation. With `dry_run: true`, the deploy runs through every step EXCEPT the hosting-adapter upload — useful for validating a structural change (new collection routes, schema bump, mode switch) without risking the live site. A dry_run job that succeeds proves the build is clean; one that fails returns the same astro stack-trace in get_deploy_status.error as a real deploy would.",
9
+ description: "Enqueue a deploy (static-page build → hosting) of the currently active version. Returns a job_id immediately; poll get_deploy_status until the status leaves queued/running. NOT for previewing iterative design/content edits — that's what get_preview_link is for (it renders live from the DB with no build). Reserve trigger_deploy for publishing, a stakeholder link to the compiled site, or pre-merge validation. Deploys build SAVED content only — unsaved drafts (working copies) are excluded, so commit_working_copy (or save:true on your writes) before deploying, or your changes won't ship. With `dry_run: true`, the deploy runs through every step EXCEPT the hosting-adapter upload — useful for validating a structural change (new collection routes, schema bump, mode switch) without risking the live site. A dry_run job that succeeds proves the build is clean; one that fails returns the same astro stack-trace in get_deploy_status.error as a real deploy would.",
10
10
  inputSchema: {
11
11
  environment: z.enum(['production', 'staging']).optional(),
12
12
  dry_run: z.boolean().optional().describe('Run the build but skip the CDN upload. The job ends in succeeded/failed the same way a real deploy does.'),
@@ -53,7 +53,7 @@ function pathFor(target, pageId) {
53
53
  export const pageBlockTools = [
54
54
  {
55
55
  name: 'get_page_blocks',
56
- description: 'Read the block tree of a container (page, partial, or page template). Use `target: { kind, id }` for partials/templates. For pages, `page_id` is the back-compat shorthand. Returns content_mode + blocks. Empty array for HTML-mode pages those store body content in html_content instead. Always call this before any block mutation so you know what you\'re modifying.',
56
+ description: 'Read the block tree of a container (page, partial, or page template). Use `target: { kind, id }` for partials/templates. For pages, `page_id` is the back-compat shorthand. Returns the DRAFT VIEW the tree with any unsaved edits (yours or the editor\'s) included, i.e. exactly what the next mutation operates on. Empty array for HTML-mode pages. Always call this before any block mutation so you know what you\'re modifying.',
57
57
  inputSchema: {
58
58
  target: targetSchema.optional(),
59
59
  page_id: z.string().optional().describe('Shorthand for target={kind:"page", id:page_id}.'),
@@ -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 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.',
70
+ description: 'Insert a block into a container (page, partial, or page template). BUFFER MODEL: block mutations on pages/partials edit the unsaved DRAFT — commit_working_copy is the explicit save. 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(),
@@ -167,7 +167,9 @@ export const pageBlockTools = [
167
167
  },
168
168
  handler: withErrorBoundary(async (args, { client, siteId }) => {
169
169
  const path = pathFor(args.target, args.page_id);
170
- const res = await client.post(siteId, `${path}/duplicate`, { block_id: args.block_id }, v(args.version));
170
+ const res = await client.post(siteId, `${path}/duplicate`, {
171
+ block_id: args.block_id,
172
+ }, v(args.version));
171
173
  return ok(res);
172
174
  }),
173
175
  },
@@ -28,7 +28,7 @@ export const pageTools = [
28
28
  },
29
29
  {
30
30
  name: 'read_page',
31
- description: 'Fetch one page in full — title, slug, status, html_content, SEO fields.',
31
+ description: 'Fetch one page in full — title, slug, status, html_content, SEO fields. Returns the DRAFT VIEW: the saved page with any unsaved draft (working copy) overlaid, plus has_unsaved_changes.',
32
32
  inputSchema: {
33
33
  page_id: z.string(),
34
34
  version: versionParam,
@@ -102,7 +102,7 @@ export const pageTools = [
102
102
  },
103
103
  {
104
104
  name: 'update_page',
105
- description: 'Shallow-merge update on a page (only the fields you pass change). Returns the updated page. For "replace this page entirely", use replace_page instead. To switch content_mode safely with a revision snapshot + optional auto-conversion, prefer `set_page_mode`.',
105
+ description: 'Shallow-merge update on a page (only the fields you pass change). BUFFER MODEL: content fields land in the page\'s unsaved DRAFT (working copy) — invisible to deploys and default previews until saved; `status`/`date_published` apply immediately. Pass save:true to commit in the same call, or commit_working_copy later after the user approves. Returns the draft view of the page. For "replace this page entirely", use replace_page. To switch content_mode safely, prefer `set_page_mode`.',
106
106
  inputSchema: {
107
107
  page_id: z.string(),
108
108
  patch: z
@@ -139,41 +139,48 @@ export const pageTools = [
139
139
  template: z.string().optional(),
140
140
  })
141
141
  .passthrough(),
142
+ save: z.boolean().optional().describe('Also SAVE (commit) the draft in the same call — use for pre-approved or batch changes. Without it, changes stay in the unsaved draft until commit_working_copy.'),
142
143
  version: versionParam,
143
144
  },
144
145
  handler: withErrorBoundary(async (args, { client, siteId }) => {
145
- const res = await client.patch(siteId, `pages/${encodeURIComponent(args.page_id)}`, args.patch, v(args.version));
146
+ const res = await client.patch(siteId, `pages/${encodeURIComponent(args.page_id)}`, { ...args.patch, ...(args.save ? { save: true } : {}) }, v(args.version));
146
147
  return ok(res);
147
148
  }),
148
149
  },
149
150
  {
150
151
  name: 'replace_page',
151
- description: 'Full replace of a page\'s writable fields (PUT). Omitted fields are reset to undefined; use update_page if you want shallow merge.',
152
+ description: 'Full replace of a page\'s writable content fields (PUT) omitted fields are cleared. BUFFER MODEL: the replacement is an unsaved DRAFT until committed; pass save:true to commit in the same call. Use update_page for shallow merge.',
152
153
  inputSchema: {
153
154
  page_id: z.string(),
154
155
  page: z.object({ title: z.string().min(1) }).passthrough(),
156
+ save: z.boolean().optional().describe('Also SAVE (commit) the draft in the same call — use for pre-approved or batch changes. Without it, changes stay in the unsaved draft until commit_working_copy.'),
155
157
  version: versionParam,
156
158
  },
157
159
  handler: withErrorBoundary(async (args, { client, siteId }) => {
158
- const res = await client.put(siteId, `pages/${encodeURIComponent(args.page_id)}`, args.page, v(args.version));
160
+ const res = await client.put(siteId, `pages/${encodeURIComponent(args.page_id)}`, { ...args.page, ...(args.save ? { save: true } : {}) }, v(args.version));
159
161
  return ok(res);
160
162
  }),
161
163
  },
162
164
  {
163
165
  name: 'batch_update_pages',
164
- description: 'Apply per-page patches in one call (up to 200 entries). Each entry is { page_id, patch }; failures are reported per-row, the rest still apply.',
166
+ description: 'Apply per-page patches in one call (up to 200 entries). Each entry is { page_id, patch, save? }; failures are reported per-row, the rest still apply. BUFFER MODEL: content patches land in each page\'s unsaved draft; per-entry save:true (or the top-level save flag) commits — typical for a user-approved sweep.',
165
167
  inputSchema: {
166
168
  updates: z
167
169
  .array(z.object({
168
170
  page_id: z.string(),
169
171
  patch: z.record(z.unknown()),
172
+ save: z.boolean().optional(),
170
173
  }))
171
174
  .min(1)
172
175
  .max(200),
176
+ save: z.boolean().optional().describe('Commit every entry\'s draft (shorthand for save:true on each).'),
173
177
  version: versionParam,
174
178
  },
175
179
  handler: withErrorBoundary(async (args, { client, siteId }) => {
176
- const res = await client.post(siteId, 'pages/batch-write', args.updates, v(args.version));
180
+ const updates = args.save
181
+ ? args.updates.map((u) => ({ ...u, save: true }))
182
+ : args.updates;
183
+ const res = await client.post(siteId, 'pages/batch-write', updates, v(args.version));
177
184
  return ok(res);
178
185
  }),
179
186
  },
@@ -245,12 +252,18 @@ export const pageTools = [
245
252
  .boolean()
246
253
  .optional()
247
254
  .describe('Tag every block root with data-block-id (the authored block id) + data-block-type so you can map a rendered element back to the exact block to mutate. Off by default.'),
255
+ include_working_copy: z
256
+ .boolean()
257
+ .optional()
258
+ .describe('Overlay unsaved drafts (working copies) on the render — yours and the editor\'s. Off by default (saved content only). Pass true to inspect your own uncommitted edits.'),
248
259
  version: versionParam,
249
260
  },
250
261
  handler: withErrorBoundary(async (args, { client, siteId }) => {
251
262
  const query = { ...(v(args.version) ?? {}) };
252
263
  if (args.annotate)
253
264
  query.annotate = 'true';
265
+ if (args.include_working_copy)
266
+ query.working_copy = 'true';
254
267
  const res = await client.get(siteId, `pages/${encodeURIComponent(args.page_id)}/preview`, query);
255
268
  return ok(res);
256
269
  }),
@@ -34,7 +34,7 @@ export const partialTools = [
34
34
  },
35
35
  {
36
36
  name: 'update_partial',
37
- description: 'Shallow-merge update on a global block. Use partial_id "header" or "footer" for the auto-injected layout blocks, or a kebab-case id for a free block. HTML is sanitized server-side.',
37
+ description: 'Shallow-merge update on a global block. Use partial_id "header" or "footer" for the auto-injected layout blocks, or a kebab-case id for a free block. HTML is sanitized server-side. BUFFER MODEL: content lands in the partial\'s unsaved DRAFT (status applies immediately); pass save:true to commit in the same call, or commit_working_copy later.',
38
38
  inputSchema: {
39
39
  partial_id: z.string(),
40
40
  patch: z
@@ -45,10 +45,11 @@ export const partialTools = [
45
45
  kind: z.enum(['header', 'footer', 'free']).optional(),
46
46
  })
47
47
  .passthrough(),
48
+ save: z.boolean().optional().describe('Also SAVE (commit) the draft in the same call.'),
48
49
  version: versionParam,
49
50
  },
50
51
  handler: withErrorBoundary(async (args, { client, siteId }) => {
51
- const res = await client.patch(siteId, `partials/${encodeURIComponent(args.partial_id)}`, args.patch, v(args.version));
52
+ const res = await client.patch(siteId, `partials/${encodeURIComponent(args.partial_id)}`, { ...args.patch, ...(args.save ? { save: true } : {}) }, v(args.version));
52
53
  return ok(res);
53
54
  }),
54
55
  },
@@ -56,12 +57,14 @@ export const partialTools = [
56
57
  name: 'replace_partial',
57
58
  description: 'Full replace of a global block (PUT). Pass html_content (and optionally name/status) directly — no wrapper object needed. ' +
58
59
  'kind is auto-inferred from partial_id ("header" → header, "footer" → footer, anything else → free). ' +
59
- 'For incremental edits (changing one field without replacing the whole block) use update_partial instead.',
60
+ 'For incremental edits (changing one field without replacing the whole block) use update_partial instead. ' +
61
+ 'BUFFER MODEL: content lands in the partial\'s unsaved DRAFT; pass save:true to commit in the same call.',
60
62
  inputSchema: {
61
63
  partial_id: z.string().describe('"header", "footer", or a free-block kebab-id.'),
62
64
  html_content: z.string().describe('Full HTML for the block. Replaces any existing content.'),
63
65
  name: z.string().optional().describe('Human-readable label shown in the UI.'),
64
66
  status: z.enum(['draft', 'published']).optional(),
67
+ save: z.boolean().optional().describe('Also SAVE (commit) the draft in the same call.'),
65
68
  version: versionParam,
66
69
  },
67
70
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -6,13 +6,17 @@ function v(version) {
6
6
  export const previewTools = [
7
7
  {
8
8
  name: 'get_preview_link',
9
- description: 'Mint a signed URL the user (or your own browser tool) can open to SEE the rendered preview. Renders LIVE from the database with NO build, so reloading shows every edit immediately — this is the PREFERRED way to preview design/content changes as you iterate (and to verify your own edits); reach for this, not trigger_deploy, when previewing. Mint it ONCE and REUSE that single URL across edits — internal links keep the token so it navigates the whole branch, and it stays valid until the TTL lapses (re-mint only then, never per edit). The TTL defaults to 24h (also the max); pass a smaller ttl_seconds only if you specifically want a shorter-lived link. Target a page (page_id), a collection item (collection_name + item_id, resolves via route_template), or a raw slug; omit all to land on the home page. (For a permanently-bookmarkable link to the COMPILED static site, deploy and share the {branch}.{project}.pages.dev alias instead — that only refreshes on trigger_deploy.)',
9
+ description: 'Mint a signed URL the user (or your own browser tool) can open to SEE the rendered preview. Renders LIVE from the database with NO build — this is the PREFERRED way to preview design/content changes as you iterate; reach for this, not trigger_deploy, when previewing. BUFFER MODEL: your edits are unsaved drafts, so for the iteration loop mint the link with include_working_copy:true (drafts visible); a plain link shows SAVED content only — right for stakeholders reviewing what will deploy. Mint ONCE and REUSE that single URL across edits — internal links keep the token so it navigates the whole branch, and it stays valid until the TTL lapses (24h default and max). Target a page (page_id), a collection item (collection_name + item_id), or a raw slug; omit all for the home page. (For a permanently-bookmarkable link to the COMPILED static site, deploy and share the {branch}.{project}.pages.dev alias instead.)',
10
10
  inputSchema: {
11
11
  page_id: z.string().optional(),
12
12
  slug: z.string().optional(),
13
13
  collection_name: z.string().optional(),
14
14
  item_id: z.string().optional(),
15
15
  ttl_seconds: z.number().int().min(60).max(86_400).optional(),
16
+ include_working_copy: z
17
+ .boolean()
18
+ .optional()
19
+ .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.'),
16
20
  version: versionParam,
17
21
  },
18
22
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -0,0 +1,69 @@
1
+ // Draft-layer (working copy) tools — the explicit-save half of the buffer
2
+ // model. Every content write on this platform (yours, other agents', the
3
+ // human editor's) lands in a per-doc DRAFT; deploys and default previews
4
+ // see saved content only. These tools read, save, and discard that draft:
5
+ //
6
+ // edits (update_page / block tools / …) → unsaved draft
7
+ // get_preview_link include_working_copy → look at the draft
8
+ // commit_working_copy → SAVE (same op as the editor's
9
+ // Save button)
10
+ // discard_working_copy → throw the draft away
11
+ //
12
+ // The portal editor shows any agent-written draft as "Unsaved changes", so
13
+ // the human can also Save or Discard it from the UI.
14
+ import { z } from 'zod';
15
+ import { ok, withErrorBoundary, versionParam } from './helpers.js';
16
+ function v(version) {
17
+ return version ? { version } : undefined;
18
+ }
19
+ const wcTargetSchema = z.object({
20
+ kind: z.enum(['page', 'partial', 'item']),
21
+ id: z.string(),
22
+ collection: z.string().optional().describe('Required when kind="item": the collection name.'),
23
+ }).describe('Which doc\'s draft: page {id}, partial {id}, or collection item ({collection} + {id}). Templates/item templates have no drafts (their writes apply directly).');
24
+ function wcPath(t) {
25
+ if (t.kind === 'item') {
26
+ if (!t.collection)
27
+ throw new Error('collection is required when kind="item"');
28
+ return `working-copy/item/${encodeURIComponent(t.collection)}/${encodeURIComponent(t.id)}`;
29
+ }
30
+ return `working-copy/${t.kind}/${encodeURIComponent(t.id)}`;
31
+ }
32
+ export const workingCopyTools = [
33
+ {
34
+ name: 'read_working_copy',
35
+ description: 'Read a doc\'s unsaved draft (working copy) as a raw field diff — { working_copy: null } means everything is saved. read_page/get_page_blocks already return the draft VIEW; use this when you need to know exactly WHICH fields are unsaved, e.g. before discarding (the draft may hold the user\'s in-progress editor work, not just yours).',
36
+ inputSchema: {
37
+ target: wcTargetSchema,
38
+ version: versionParam,
39
+ },
40
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
41
+ const res = await client.get(siteId, wcPath(args.target), v(args.version));
42
+ return ok(res);
43
+ }),
44
+ },
45
+ {
46
+ name: 'commit_working_copy',
47
+ description: 'SAVE a doc\'s unsaved draft: promote it onto the saved doc through the canonical write path (revision snapshot, SEO transform, redirect hygiene) and delete the draft. Identical to the Save button in the portal editor. Returns { committed, seo_warnings, auto_redirects, retired_redirects }. Never changes publish status. Deploys only ship saved content, so commit before trigger_deploy. Equivalent shortcut: save:true on the write tools.',
48
+ inputSchema: {
49
+ target: wcTargetSchema,
50
+ version: versionParam,
51
+ },
52
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
53
+ const res = await client.post(siteId, wcPath(args.target), {}, v(args.version));
54
+ return ok(res);
55
+ }),
56
+ },
57
+ {
58
+ name: 'discard_working_copy',
59
+ description: 'Throw away a doc\'s unsaved draft — the saved doc is untouched. Use when the user rejects the previewed change. CAUTION: the draft is shared with the human editor; read_working_copy first if you\'re not sure whose edits are in it.',
60
+ inputSchema: {
61
+ target: wcTargetSchema,
62
+ version: versionParam,
63
+ },
64
+ handler: withErrorBoundary(async (args, { client, siteId }) => {
65
+ const res = await client.del(siteId, wcPath(args.target), v(args.version));
66
+ return ok(res);
67
+ }),
68
+ },
69
+ ];
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.26.2';
11
+ export const VERSION = '0.28.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.26.2",
3
+ "version": "0.28.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": {
package/skills/README.md CHANGED
@@ -5,6 +5,11 @@ Each one is a self-contained markdown file the agent reads when its
5
5
  description matches the user's request. Recipes call MCP tools; the agent
6
6
  adapts them to the specific job.
7
7
 
8
+ **The buffer model:** every content write is an unsaved per-doc draft until
9
+ explicitly saved (`commit_working_copy` or `save: true` on the write call).
10
+ Deploys ship saved content only — recipes that end in `trigger_deploy` must
11
+ save first.
12
+
8
13
  ## Installation
9
14
 
10
15
  Copy whichever skills you need into your Claude Code skills directory.
package/skills/tr-blog.md CHANGED
@@ -5,6 +5,14 @@ description: Use when the user wants to set up a blog, news section, podcast fee
5
5
 
6
6
  # Set up a blog / news section
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  A blog in Typeroll is a **collection with `item_template_html` + `route_template`**. Every published item materialises as its own static page at build time — there is **no need to call `create_page` per article**. The detail design lives once in `item_template_html`; the listing lives once in a page with a `<!-- typeroll:listing -->` marker that `regenerate_collection_listing` refreshes.
9
17
 
10
18
  If you find yourself about to create 20 pages for 20 articles, stop — you're using the old pattern. The recipe below is the right one.
@@ -5,6 +5,14 @@ description: Use when the user asks to create a brand identity, design system, o
5
5
 
6
6
  # Design a brand identity for a Typeroll site
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  This skill turns a brief (or a reference URL/screenshot) into a complete
9
17
  visual design system applied to the site's settings and partials.
10
18
 
@@ -5,6 +5,14 @@ description: Use when the user asks to write, draft, or rewrite a page on a Type
5
5
 
6
6
  # Write a page that fits the site
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  The default failure mode for an AI writing a page is "good generic
9
17
  HTML in the wrong voice." This skill makes the discovery step
10
18
  non-optional.
@@ -5,6 +5,14 @@ description: Use when the user wants to build a directory site or import a struc
5
5
 
6
6
  # Build a directory site from external data
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  Typeroll collections support per-item URLs: every published item
9
17
  in a collection with a `route_template` materialises as its own static
10
18
  page at build time. This is the right pattern when you have hundreds
@@ -5,6 +5,14 @@ description: Vetted, robust header and footer presets to drop into the header/fo
5
5
 
6
6
  # Header & footer presets
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  Headers and footers are the two partials every page shows, and hand-rolling them
9
17
  is where logos get clipped and mobile menus break. **Start from a preset below,
10
18
  fill the placeholders, restyle with the site's colours — don't build the layout
@@ -5,6 +5,14 @@ description: Use when the user wants to import or migrate content from a non-Wor
5
5
 
6
6
  # Import content from a non-WordPress site
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  ## When to use this vs tr-migrate-wp
9
17
 
10
18
  | Source | Use |
@@ -5,6 +5,14 @@ description: Use when the user wants to migrate an Astro site — particularly a
5
5
 
6
6
  # Migrate an Astro site to Typeroll
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  Astro's [Content Collections](https://docs.astro.build/en/guides/content-collections/) and Typeroll's `Collection` + `CollectionItem` map one-to-one. A collection in Astro is a directory of frontmatter-bearing files under `src/content/<name>/`; in Typeroll it's a schema + items doc set under `organizations/{org}/sites/{site}/collections/{name}/`. The Astro schema (`zod.object(...)` in `src/content/config.ts`) is your field list. The frontmatter values are field values. The markdown bodies are richtext fields.
9
17
 
10
18
  This skill walks the migration from a checked-out Astro repo on the user's machine to a target Typeroll site. You run it locally — the source repo is on disk, the MCP just receives the final shape.
@@ -5,6 +5,14 @@ description: Use when the user asks to migrate a WordPress site to Typeroll, men
5
5
 
6
6
  # Migrate from WordPress to Typeroll
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  The platform's in-portal migration workflow is the "managed" path for
9
17
  customers who want one-click. This skill is the "power-user" path: you
10
18
  do it locally, mix data sources freely, and the user (consultant /
@@ -5,6 +5,14 @@ description: Use when the user wants to create a new Typeroll site from scratch,
5
5
 
6
6
  # Bootstrap a new Typeroll site
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  Start here when the site already exists as a database record (created via
9
17
  the portal UI or API) but has no design, no header/footer, and no pages.
10
18
  The goal is to go from blank to a working 4-page site with correct brand
@@ -5,6 +5,14 @@ description: Use when the user asks to redesign, modernize, or restructure a Typ
5
5
 
6
6
  # Redesign a site without breaking the live one
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  Site-wide changes are exactly where copy-on-write branches earn their
9
17
  keep. This skill enforces the discipline: every redesign happens on a
10
18
  branch, preview-checked end-to-end, merged only after user sign-off.
package/skills/tr-seo.md CHANGED
@@ -5,6 +5,14 @@ description: Use when the user asks to improve SEO, fix meta tags, add structure
5
5
 
6
6
  # SEO audit and improvements for a Typeroll site
7
7
 
8
+ > **The buffer model (draft writes).** Every content write in this recipe
9
+ > (pages, blocks, partials, collection items) lands in an unsaved per-doc
10
+ > DRAFT — deploys and plain previews only see SAVED content. For recipe-style
11
+ > build work, pass `save: true` on write calls (the work is pre-approved by
12
+ > the task itself), or run `commit_working_copy` per doc before any
13
+ > `trigger_deploy`. Preview your drafts with `include_working_copy: true`.
14
+
15
+
8
16
  ## What Typeroll handles automatically
9
17
 
10
18
  - `<html lang>` from site `language` setting (per-page override via `language` field)