@typeroll/mcp-server 0.45.45 → 0.45.47
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 +56 -11
- package/README.md +2 -0
- package/dist/bundled-content.js +3 -2
- package/dist/compact-tools.js +1 -1
- package/dist/server.js +11 -2
- package/dist/tools/block-types.js +110 -62
- package/dist/tools/domain.js +9 -3
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/README.md +1 -0
- package/skills/tr-custom-blocks.md +104 -0
package/dist/compact-tools.js
CHANGED
|
@@ -22,7 +22,7 @@ export function compactTools(catalog) {
|
|
|
22
22
|
}),
|
|
23
23
|
...['read', 'write', 'admin'].map(effect => ({
|
|
24
24
|
name: `call_${effect}_tool`, effect,
|
|
25
|
-
description: effect === 'read' ? 'Call a discovered read-only tool with its exact arguments. Does not accept write/admin tools.' : `Call a discovered ${effect} tool. May ${effect === 'admin' ? 'change app or
|
|
25
|
+
description: effect === 'read' ? 'Call a discovered read-only tool with its exact arguments. Does not accept write/admin tools.' : `Call a discovered ${effect} tool. May ${effect === 'admin' ? 'change app, email or block type configuration, send a test email or activate releases' : 'save, delete, publish or otherwise change state'}. Inspect describe_tool first and follow the user’s authorization.`,
|
|
26
26
|
schema: { name: z.string().max(100), arguments: z.record(z.unknown()).default({}) },
|
|
27
27
|
invoke: async (args) => {
|
|
28
28
|
const tool = byName.get(String(args.name));
|
package/dist/server.js
CHANGED
|
@@ -50,7 +50,7 @@ const PERM_RANK = { read: 0, write: 1, admin: 2 };
|
|
|
50
50
|
* gate then checks `effect <= sitePermission`.
|
|
51
51
|
*
|
|
52
52
|
* Naming convention is followed by the tool modules: read paths are
|
|
53
|
-
* `list_*` / `read_*` / `get_*` / `preview_*` / `search_*` / `check_*`; everything else
|
|
53
|
+
* `list_*` / `read_*` / `get_*` / `preview_*` / `search_*` / `check_*` / `find_*`; everything else
|
|
54
54
|
* mutates.
|
|
55
55
|
*/
|
|
56
56
|
function effectFor(name) {
|
|
@@ -101,8 +101,17 @@ function effectFor(name) {
|
|
|
101
101
|
|| name === 'set_email_settings'
|
|
102
102
|
|| name === 'delete_email_settings'
|
|
103
103
|
|| name === 'send_test_email'
|
|
104
|
-
|| name === 'set_incoming_email_forwarding'
|
|
104
|
+
|| name === 'set_incoming_email_forwarding'
|
|
105
|
+
// Block type authoring is site administration (the portal and the v1
|
|
106
|
+
// routes require admin); placing and editing blocks on pages stays write.
|
|
107
|
+
|| name === 'create_block_type'
|
|
108
|
+
|| name === 'update_block_type'
|
|
109
|
+
|| name === 'delete_block_type'
|
|
110
|
+
|| name === 'import_block_types')
|
|
105
111
|
return 'admin';
|
|
112
|
+
// Compute an answer and write nothing, like the preview_* tools.
|
|
113
|
+
if (name === 'validate_block_type' || name.startsWith('find_'))
|
|
114
|
+
return 'read';
|
|
106
115
|
if (name === 'diff_version')
|
|
107
116
|
return 'read';
|
|
108
117
|
if (name.startsWith('list_') ||
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
// Block-type tools
|
|
2
|
-
//
|
|
1
|
+
// Block-type tools: discovery, authoring (validate, preview, create, update,
|
|
2
|
+
// delete), usage, starters and .tcblocks packages. Every write goes through
|
|
3
|
+
// the portal's one block type write path (shared validator, version chain,
|
|
4
|
+
// rename migration), and authoring needs admin permission on the Site.
|
|
3
5
|
import { z } from 'zod';
|
|
4
6
|
import { ok, withErrorBoundary, versionParam } from './helpers.js';
|
|
5
7
|
function v(version) {
|
|
6
8
|
return version ? { version } : undefined;
|
|
7
9
|
}
|
|
8
|
-
// The per-block markup. These
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
const HEAVY_BLOCK_TYPE_FIELDS = ['template', 'styles', 'script'];
|
|
10
|
+
// The per-block markup. These fields dominate the payload size (a few dozen
|
|
11
|
+
// blocks of template HTML + scoped CSS can exceed an agent's token budget and
|
|
12
|
+
// get truncated), and an agent enumerating the library rarely needs them —
|
|
13
|
+
// read_block_type fetches them for one block on demand. So list_block_types
|
|
14
|
+
// omits them by default and returns a lightweight summary.
|
|
15
|
+
const HEAVY_BLOCK_TYPE_FIELDS = ['template', 'styles', 'styles_compiled', 'script', 'composition'];
|
|
14
16
|
/** Strip the heavy markup fields from each block type in a `block-types`
|
|
15
17
|
* response, preserving the field `schema` and all light metadata. Defensive:
|
|
16
18
|
* if the shape isn't the expected `{ block_types: [...] }`, pass it through. */
|
|
@@ -25,18 +27,47 @@ function summariseBlockTypes(res) {
|
|
|
25
27
|
if (!bt || typeof bt !== 'object')
|
|
26
28
|
return bt;
|
|
27
29
|
const copy = { ...bt };
|
|
30
|
+
if (Array.isArray(copy.composition) && copy.composition.length)
|
|
31
|
+
copy.composed = true;
|
|
28
32
|
for (const field of HEAVY_BLOCK_TYPE_FIELDS)
|
|
29
33
|
delete copy[field];
|
|
30
34
|
return copy;
|
|
31
35
|
});
|
|
32
36
|
return { ...envelope, block_types: slim };
|
|
33
37
|
}
|
|
38
|
+
const AUTHORING_GUIDE = 'Two kinds of site block type. COMPOSED (preferred): `composition` is a tree of existing blocks ({ id, type, data, children? } — core/heading, core/text, core/prose, core/image, core/icon, core/button, core/container, core/grid, core/repeater …) and `schema` declares the props people edit when they place the block. Inner blocks read props through exact bindings as the WHOLE value: a heading\'s text "{{props.title}}"; a core/repeater with items "{{props.items}}" (an array prop) renders its children once per item, where "{{item.title}}" reads the item and "{{item.link.href}}" a link sub-value. Everything the inner blocks do (named styles, responsive settings, images, safe links, render versions) carries over. ' +
|
|
39
|
+
'TEMPLATE (advanced, when existing blocks cannot express the markup): `template` is HTML with {{field}} (escaped), {{{field}}} (raw: richtext fields and derived …_svg / …_html only), {{#field}}…{{/field}} (shown when set), {{^field}}…{{/field}} (shown when empty), {{#each items}}…{{/each}} (per array item; names resolve against the item first; {{@index}}, {{@number}}, {{@first}}, {{@last}}), {{#link field class="…"}}…{{/link}} (wraps the body in a safe <a href rel target> when the link resolves), plus {{children}} / {{slot:NAME}} for containers. ' +
|
|
40
|
+
'Fields: text, textarea, richtext, image, icon (derived {name}_svg), select (options), boolean, number, url, link ({ page_id?, url?, new_tab? } — prefer page_id for a page on the Site, it survives slug changes; resolves to .href, .target, .rel), array (a repeating group: fields, item_label, min_items, max_items; nests 3 levels), object, style … each with name, type, label and optional help, required, default. ' +
|
|
41
|
+
'`styles` is scoped to the block when written: every selector applies inside [data-block="<name>"], :scope is the block element itself, body/html/:root are refused; use the site\'s var(--color-*) and spacing tokens. The response returns `styles_compiled`, the CSS that ships. ' +
|
|
42
|
+
'Unknown properties are errors. Every problem has a JSON-pointer `path` (and a `line` for markup/CSS); a 400 lists them all in `problems`, warnings come back in `warnings`. Call validate_block_type / preview_block_type first, and list_block_type_starters for complete examples. Needs ADMIN permission on the Site; editors (write) place block types on pages and edit their fields.';
|
|
43
|
+
const definitionShape = {
|
|
44
|
+
label: z.string().optional().describe('Display label in the block picker.'),
|
|
45
|
+
description: z.string().optional().describe('When to use this block type; shown to editors and agents (≤ 500 chars).'),
|
|
46
|
+
icon: z.string().optional().describe('Lucide icon name shown in the block picker.'),
|
|
47
|
+
category: z.enum(['layout', 'content', 'media', 'custom']).optional(),
|
|
48
|
+
container: z.union([
|
|
49
|
+
z.literal(true), z.literal(false),
|
|
50
|
+
z.enum(['slots', 'repeater', 'conditional']),
|
|
51
|
+
]).optional().describe('Template types only. true renders {{children}}, slots fixed named slots, repeater loops over items, conditional show-if. A composed type is never a container; put a container block inside its composition.'),
|
|
52
|
+
slot_count: z.number().optional().describe('Required when container="slots". 1-8.'),
|
|
53
|
+
slot_labels: z.array(z.string()).optional(),
|
|
54
|
+
item_compatible: z.boolean().optional().describe('Mark true if this block is designed to render as a repeater item (no outer padding).'),
|
|
55
|
+
expand_to: z.object({
|
|
56
|
+
target: z.string(),
|
|
57
|
+
defaults: z.record(z.unknown()),
|
|
58
|
+
}).optional().describe('Alias mechanism — render as another block type with these defaults merged under authored data.'),
|
|
59
|
+
schema: z.array(z.unknown()).optional().describe('FieldDefinition[]: { name, type, label, help?, required?, default?, options? (select), fields? (array/object), item_label?, min_items?, max_items? }. Replaces the whole schema on update.'),
|
|
60
|
+
composition: z.array(z.unknown()).optional().describe('Block[] a composed type is built from. Bind data to props with "{{props.name}}" and, inside a core/repeater over an array prop, to items with "{{item.name}}" — always as the whole value.'),
|
|
61
|
+
template: z.string().optional().describe('Markup of a template type (instead of composition); see the section syntax above.'),
|
|
62
|
+
styles: z.string().optional().describe('CSS scoped to the block. Write selectors for the block\'s own markup; :scope is the block element.'),
|
|
63
|
+
script: z.string().optional().describe("Client-side JS shipped with the block (runs on the published site's own origin). Accepted under your API key's authority — the write is audit-logged. Prefer a composed type without script."),
|
|
64
|
+
};
|
|
34
65
|
export const blockTypeTools = [
|
|
35
66
|
{
|
|
36
67
|
name: 'export_block_types',
|
|
37
|
-
description: 'Pack
|
|
68
|
+
description: 'Pack site block types into a .tcblocks zip (base64). When `ids` is omitted, every block type the Site has (made in the portal, by an agent, or imported) is bundled. The package keeps every property — composition, template, styles and CSS scope, script, field help and list settings — so it moves between any Sites, in any Organization or Typeroll instance. import_block_types reads it back.',
|
|
38
69
|
inputSchema: {
|
|
39
|
-
ids: z.array(z.string()).optional().describe('Specific block-type ids to include. Omit to export every
|
|
70
|
+
ids: z.array(z.string()).optional().describe('Specific block-type ids to include. Omit to export every site block type.'),
|
|
40
71
|
name: z.string().optional().describe('Package name (default: {site}-blocks).'),
|
|
41
72
|
version: z.string().optional().describe('Package version written into the manifest (default: 1.0.0). This is the package semver, not a Site version — use version_branch for that.'),
|
|
42
73
|
version_branch: versionParam,
|
|
@@ -60,26 +91,29 @@ export const blockTypeTools = [
|
|
|
60
91
|
},
|
|
61
92
|
{
|
|
62
93
|
name: 'import_block_types',
|
|
63
|
-
description: 'Install block types from a .tcblocks package
|
|
94
|
+
description: 'Install block types from a .tcblocks package (zip as base64). Needs ADMIN permission on the Site. Each definition is checked by the same validator as create_block_type; a definition with errors is reported as failed and not written. On a name conflict `on_conflict` decides: skip (default) keeps the Site\'s type, rename imports it under a free name (references inside the package follow), replace overwrites it. Returns `results` per type (created, renamed, replaced, skipped, failed with problems) and counts. Imported types may carry CSS and JS — only import packages you trust.',
|
|
64
95
|
inputSchema: {
|
|
65
96
|
zip_base64: z.string().describe('Base64-encoded .tcblocks zip.'),
|
|
97
|
+
on_conflict: z.enum(['skip', 'rename', 'replace']).optional().describe('What to do when the Site already has a block type with the same name. Default skip.'),
|
|
66
98
|
version: versionParam,
|
|
67
99
|
},
|
|
68
100
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
69
|
-
const
|
|
70
|
-
|
|
101
|
+
const res = await client.post(siteId, 'blocks/import', {
|
|
102
|
+
zip_base64: args.zip_base64,
|
|
103
|
+
...(args.on_conflict ? { on_conflict: args.on_conflict } : {}),
|
|
104
|
+
}, v(args.version));
|
|
71
105
|
return ok(res);
|
|
72
106
|
}),
|
|
73
107
|
},
|
|
74
108
|
{
|
|
75
109
|
name: 'list_block_types',
|
|
76
|
-
description: 'Discover every block type usable on this site. Returns ALL of them in one list: core blocks (id like "core/section", always available),
|
|
110
|
+
description: 'Discover every block type usable on this site. Returns ALL of them in one list: core blocks (id like "core/section", always available), site block types (origin "user" from the portal, "ai" from an agent) and third-party blocks (origin "third_party", imported from .tcblocks packages). Call this FIRST when starting block-mode work — never hardcode block ids; the available set is per-site. By default this is a LIGHTWEIGHT SUMMARY: id, label, description, category, icon, container/slot info, origin, `composed: true` for composed types, and the full field schema — everything you need to pick a block and call add_block — but NOT the template, composition, styles or script. Fetch those for one block with read_block_type, or pass full:true to inline them for every block (can be very large).',
|
|
77
111
|
inputSchema: {
|
|
78
|
-
include_core: z.boolean().optional().describe('Default true. Set false to get only
|
|
112
|
+
include_core: z.boolean().optional().describe('Default true. Set false to get only the site\'s own and third-party types.'),
|
|
79
113
|
full: z
|
|
80
114
|
.boolean()
|
|
81
115
|
.optional()
|
|
82
|
-
.describe("Default false. When true, include each block type's template
|
|
116
|
+
.describe("Default false. When true, include each block type's template, composition, styles and script inline — the full doc. Large; prefer read_block_type for one block."),
|
|
83
117
|
version: versionParam,
|
|
84
118
|
},
|
|
85
119
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -94,7 +128,7 @@ export const blockTypeTools = [
|
|
|
94
128
|
},
|
|
95
129
|
{
|
|
96
130
|
name: 'read_block_type',
|
|
97
|
-
description: "Full
|
|
131
|
+
description: "Full definition of one block type: the field schema, and its composition (composed types) or template (template types), styles with `styles_compiled` (the scoped CSS that ships), and optional script. Use this when list_block_types' summary isn't enough — before add_block on an unfamiliar block, or before update_block_type.",
|
|
98
132
|
inputSchema: {
|
|
99
133
|
type_id: z.string(),
|
|
100
134
|
version: versionParam,
|
|
@@ -106,7 +140,7 @@ export const blockTypeTools = [
|
|
|
106
140
|
},
|
|
107
141
|
{
|
|
108
142
|
name: 'find_pages_using_block_type',
|
|
109
|
-
description: '
|
|
143
|
+
description: 'Everything that uses a block type, on every surface: `pages` (saved block pages and drafts — `sources` says which), `templates` (page templates), `partials` (header, footer, global blocks), `block_templates`, and `block_types` (other site block types built from it). Repeaters that render the type as their item_block count, and a use through another block type names it in `via`. `total` sums the lists; delete_block_type is refused while it is above zero. Call it before changing or removing fields of a type in use.',
|
|
110
144
|
inputSchema: {
|
|
111
145
|
type_id: z.string(),
|
|
112
146
|
version: versionParam,
|
|
@@ -116,36 +150,62 @@ export const blockTypeTools = [
|
|
|
116
150
|
return ok(res);
|
|
117
151
|
}),
|
|
118
152
|
},
|
|
153
|
+
{
|
|
154
|
+
name: 'list_block_type_starters',
|
|
155
|
+
description: 'Built-in starting points for a new block type: complete composed definitions (icon list, feature cards, numbered steps) with fields, composition and scoped CSS. Adapt one (rename it, change fields) and pass it to create_block_type — it is the quickest way to a valid composed type.',
|
|
156
|
+
inputSchema: {},
|
|
157
|
+
handler: withErrorBoundary(async (_args, { client, siteId }) => {
|
|
158
|
+
const res = await client.get(siteId, 'block-types/starters');
|
|
159
|
+
return ok(res);
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: 'validate_block_type',
|
|
164
|
+
description: `Check a block type definition without saving it, with the exact validator create_block_type and update_block_type use. Pass the whole definition, or with type_id a patch to that stored type (renames are checked too). Returns { ok, problems, merged }: every error and warning with a JSON-pointer path (e.g. /schema/0/fields/2/name, /composition/0/children/1/data/text) and a line for markup and CSS, and the definition as it would be stored. Writes nothing. ${AUTHORING_GUIDE}`,
|
|
165
|
+
inputSchema: {
|
|
166
|
+
definition: z.record(z.unknown()).describe('The block type definition (as for create_block_type), or a patch when type_id is set.'),
|
|
167
|
+
type_id: z.string().optional().describe('Validate `definition` as a patch to this stored block type.'),
|
|
168
|
+
version: versionParam,
|
|
169
|
+
},
|
|
170
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
171
|
+
const query = {};
|
|
172
|
+
if (args.type_id)
|
|
173
|
+
query.type_id = args.type_id;
|
|
174
|
+
if (args.version)
|
|
175
|
+
query.version = args.version;
|
|
176
|
+
const res = await client.post(siteId, 'block-types/validate', args.definition, query);
|
|
177
|
+
return ok(res);
|
|
178
|
+
}),
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'preview_block_type',
|
|
182
|
+
description: 'Render a block type definition that need not be saved, through the same preview renderer as the portal (the Site\'s render version, theme CSS, named styles, custom CSS, the other site block types, and the sanitizer). Without `data` the block shows sample values derived from its fields. Returns { ok, html, css, document, data, problems }: `html` is the block\'s markup as a page carries it, `css` the scoped block CSS that ships, `document` a standalone HTML page. A definition with errors is not rendered (ok:false, problems listed). Block scripts never run in a preview. Writes nothing.',
|
|
183
|
+
inputSchema: {
|
|
184
|
+
definition: z.record(z.unknown()).optional().describe('The block type definition, or a patch when type_id is set. Omit with type_id to preview the stored type.'),
|
|
185
|
+
type_id: z.string().optional().describe('Preview `definition` as a patch to this stored block type.'),
|
|
186
|
+
data: z.record(z.unknown()).optional().describe('Field values for the previewed block. Omit for sample data.'),
|
|
187
|
+
render_version: z.number().int().optional().describe('Render with this platform render version instead of the Site\'s own.'),
|
|
188
|
+
version: versionParam,
|
|
189
|
+
},
|
|
190
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
191
|
+
const { version, ...body } = args;
|
|
192
|
+
const res = await client.post(siteId, 'block-types/preview', { ...body, definition: body.definition ?? {} }, v(version));
|
|
193
|
+
return ok(res);
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
119
196
|
// ─── BlockType authoring — CREATE / UPDATE / DELETE ──────────────────
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
// audit + UI distinguish agent-authored types.
|
|
197
|
+
// Admin permission on the Site, as in the portal. `script` runs with full
|
|
198
|
+
// DOM access in every visitor's browser. Through an API key it is ACCEPTED
|
|
199
|
+
// under the key holder's authority (same trust level as scripts_head) and
|
|
200
|
+
// the write is audit-logged. Only the in-portal chat never writes it.
|
|
201
|
+
// Origin is stamped 'ai' so audit + UI distinguish agent-authored types.
|
|
126
202
|
{
|
|
127
203
|
name: 'create_block_type',
|
|
128
|
-
description:
|
|
204
|
+
description: `Create a block type for this Site when no core block (or block template) fits a recurring shape. Origin is stamped 'ai'; id = name. The type is available to every page editor immediately and reaches the live site with the next deploy. ${AUTHORING_GUIDE} Returns { block_type, warnings }; 409 when the name exists. A block type is the supported way to ship per-block JS (\`script\`): <script> written into page or block markup is always stripped by the sanitizer. Site-wide tags belong in settings.scripts_head / scripts_body_end.`,
|
|
129
205
|
inputSchema: {
|
|
130
|
-
name: z.string().describe('Machine name (lowercase
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
category: z.enum(['layout', 'content', 'media', 'custom']).optional(),
|
|
134
|
-
container: z.union([
|
|
135
|
-
z.literal(true), z.literal(false),
|
|
136
|
-
z.enum(['slots', 'repeater', 'conditional']),
|
|
137
|
-
]).optional().describe('Container kind. Use slots for fixed named slots, repeater for looping over items, conditional for show-if blocks.'),
|
|
138
|
-
slot_count: z.number().optional().describe('Required when container="slots". 1-8.'),
|
|
139
|
-
slot_labels: z.array(z.string()).optional(),
|
|
140
|
-
item_compatible: z.boolean().optional().describe('Mark true if this block is designed to render as a repeater item (no outer padding, kernel layout).'),
|
|
141
|
-
expand_to: z.object({
|
|
142
|
-
target: z.string(),
|
|
143
|
-
defaults: z.record(z.unknown()),
|
|
144
|
-
}).optional().describe('Alias mechanism — render as another block type with these defaults merged under authored data.'),
|
|
145
|
-
schema: z.array(z.unknown()).optional().describe('FieldDefinition[]. Each field has name, type, label, optional required/default/options/responsive.'),
|
|
146
|
-
template: z.string().optional().describe('HTML with {{field}}, {{{field}}}, {{=tag}}, {{children}}, {{slot:NAME}} substitutions.'),
|
|
147
|
-
styles: z.string().optional().describe('Block-scoped CSS. Selectors should start with [data-block="<name>"].'),
|
|
148
|
-
script: z.string().optional().describe("Client-side JS shipped with the block (runs on the published site). Accepted under your API key's authority — the write is audit-logged and the response carries a notice naming the stored JS. It runs on the published site's own origin, which shares nothing with the Typeroll portal."),
|
|
206
|
+
name: z.string().describe('Machine name (lowercase letters, digits, "-" and "_", 1-64 chars). Becomes the id.'),
|
|
207
|
+
...definitionShape,
|
|
208
|
+
label: z.string().describe('Display label in the block picker.'),
|
|
149
209
|
version: versionParam,
|
|
150
210
|
},
|
|
151
211
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -160,24 +220,12 @@ export const blockTypeTools = [
|
|
|
160
220
|
},
|
|
161
221
|
{
|
|
162
222
|
name: 'update_block_type',
|
|
163
|
-
description: "
|
|
223
|
+
description: "Change a site block type (core/* types are read-only). Needs ADMIN permission. schema, composition, template and styles replace wholesale when sent; other properties merge. Changing a type in use: to RENAME a field, send the new schema with `renames` ({ \"title\": \"heading\", \"items.label\": \"text\" } — dotted paths of the OLD names, new name of the field) and the data moves in every block that uses the type (saved pages and drafts, page templates, header/footer/global blocks, block templates, other block types, repeater items), after a revision of each changed page; a composed type's own {{props.…}}/{{item.…}} bindings follow unless you send a composition. Removing or retyping a field that holds data answers 409 with the affected uses until you resend with confirm_data_loss: true (ask the user first). Returns { block_type, warnings, impact: { usages, renamed, removed, retyped } }. The same validation as create_block_type applies.",
|
|
164
224
|
inputSchema: {
|
|
165
|
-
type_id: z.string().describe('Block type id (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
container: z.union([
|
|
170
|
-
z.literal(true), z.literal(false),
|
|
171
|
-
z.enum(['slots', 'repeater', 'conditional']),
|
|
172
|
-
]).optional(),
|
|
173
|
-
slot_count: z.number().optional(),
|
|
174
|
-
slot_labels: z.array(z.string()).optional(),
|
|
175
|
-
item_compatible: z.boolean().optional(),
|
|
176
|
-
expand_to: z.object({ target: z.string(), defaults: z.record(z.unknown()) }).optional(),
|
|
177
|
-
schema: z.array(z.unknown()).optional(),
|
|
178
|
-
template: z.string().optional(),
|
|
179
|
-
styles: z.string().optional(),
|
|
180
|
-
script: z.string().optional().describe("Accepted under your API key's authority; audit-logged, and the response carries a notice naming the stored JS."),
|
|
225
|
+
type_id: z.string().describe('Block type id (a site type). Cannot be a core/* block.'),
|
|
226
|
+
...definitionShape,
|
|
227
|
+
renames: z.record(z.string()).optional().describe('Old field path → new field name, e.g. { "title": "heading", "items.label": "text" }. Send together with the new schema.'),
|
|
228
|
+
confirm_data_loss: z.boolean().optional().describe('Drop the data of removed or retyped fields that hold data. Only after the user agreed; the 409 lists what would be lost.'),
|
|
181
229
|
version: versionParam,
|
|
182
230
|
},
|
|
183
231
|
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
@@ -188,7 +236,7 @@ export const blockTypeTools = [
|
|
|
188
236
|
},
|
|
189
237
|
{
|
|
190
238
|
name: 'delete_block_type',
|
|
191
|
-
description: "Remove a
|
|
239
|
+
description: "Remove a site block type. Needs ADMIN permission; core blocks (core/*) cannot be deleted. Refused with 409 while anything uses the type — pages and drafts, page templates, header, footer, global blocks, block templates, other block types or repeaters — and the refusal lists the uses (as find_pages_using_block_type does). Remove those uses first and confirm with the user.",
|
|
192
240
|
inputSchema: {
|
|
193
241
|
type_id: z.string(),
|
|
194
242
|
version: versionParam,
|
package/dist/tools/domain.js
CHANGED
|
@@ -30,13 +30,13 @@ export const domainTools = [
|
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
name: 'read_organization_publishing_connections', noSite: true,
|
|
33
|
-
description: 'Read the Organization\'s GitHub and Cloudflare publishing connections: status, revision (needed to change or disconnect), account identity, media_ready, media migration, and connect_urls. Requires an organization API key; never returns credentials. GitHub sign-in/App installation and Cloudflare OAuth need a person in a browser: give the user the matching connect_urls link (an organization owner or admin completes it), then read again.',
|
|
33
|
+
description: 'Read the Organization\'s GitHub and Cloudflare publishing connections: status, revision (needed to change or disconnect), account identity, media_ready, media migration, github_setup (app_configured, encryption_available) and connect_urls. Requires an organization API key; never returns credentials. GitHub sign-in/App installation and Cloudflare OAuth need a person in a browser: give the user the matching connect_urls link (an organization owner or admin completes it), then read again. When GitHub is not connected, call diagnose_organization_github_connection to learn why and who must act.',
|
|
34
34
|
inputSchema: {},
|
|
35
35
|
handler: withErrorBoundary(async (_args, { client }) => ok(await client.rootGet('publishing/connections'))),
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
name: 'disconnect_organization_publishing_provider', noSite: true,
|
|
39
|
-
description: 'Disconnect the Organization\'s GitHub or Cloudflare publishing connection at the revision from read_organization_publishing_connections. Erases stored credentials; does not delete repositories, buckets, media, DNS or published sites, but publishing through that provider stops until reconnected. Requires an organization API key and the user\'s explicit go-ahead.',
|
|
39
|
+
description: 'Disconnect the Organization\'s GitHub or Cloudflare publishing connection at the revision from read_organization_publishing_connections. Erases stored credentials; does not delete repositories, buckets, media, DNS or published sites, but publishing through that provider stops until reconnected. After a GitHub disconnect, an owner or admin may connect a different GitHub account in the browser only after confirming which account it replaces; existing repositories are not moved. Requires an organization API key and the user\'s explicit go-ahead.',
|
|
40
40
|
inputSchema: { provider: z.enum(['github', 'cloudflare']), revision: z.string(), hosting_group_id: z.string().optional().describe('Omit for the organization (Default) connection.') },
|
|
41
41
|
handler: withErrorBoundary(async ({ provider, ...body }, { client }) => ok(await client.rootDelete(`publishing/connections/${provider}`, body))),
|
|
42
42
|
},
|
|
@@ -63,10 +63,16 @@ export const domainTools = [
|
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
name: 'check_organization_github_permissions', noSite: true,
|
|
66
|
-
description: 'Check live GitHub build permissions without disconnecting or reconnecting. Requires an organization API key. Distinguishes an update the organization owner can approve from permissions Typeroll has not requested yet. Returns the existing installation approval URL only when actionable; does not enable a build engine.',
|
|
66
|
+
description: 'Check live GitHub build permissions (Actions and Workflows) of an already connected GitHub account without disconnecting or reconnecting. Requires an organization API key. Distinguishes an update the organization owner can approve from permissions Typeroll has not requested yet. Returns the existing installation approval URL only when actionable; does not enable a build engine. For a GitHub connection that is missing or failing, use diagnose_organization_github_connection.',
|
|
67
67
|
inputSchema: {},
|
|
68
68
|
handler: withErrorBoundary(async (_args, { client }) => ok(await client.rootGet('publishing/github-permissions'))),
|
|
69
69
|
},
|
|
70
|
+
{
|
|
71
|
+
name: 'diagnose_organization_github_connection', noSite: true,
|
|
72
|
+
description: 'Explain why the Organization\'s GitHub publishing connection is or is not working and who must do what next. Returns the Organization\'s state: diagnosis.outcome (connected, needs_attention, choose, action_required, waiting_on_owner, sign_in_required, retryable_error or unavailable), blockers, and the saved installation once connected. A person\'s unfinished attempt and their GitHub accounts stay in their own browser session. Each blocker has a code, who must act (you = the person connecting, github_owner, publisher = operator of this Typeroll installation, typeroll_admin), a message and one action: a github.com or documentation link, or a browser step (sign_in, install, retry, confirm_account_change) completed at connect_url. Read only, except recheck: true re-checks a connected installation (suspension, repository access, permissions) with the App\'s own authority. An API key cannot sign in to GitHub; relay the message and link to the user. Requires an organization API key; never returns credentials.',
|
|
73
|
+
inputSchema: { recheck: z.boolean().optional().describe('Re-check the connected installation with the publisher App before answering.') },
|
|
74
|
+
handler: withErrorBoundary(async ({ recheck }, { client }) => ok(await client.rootGet(`publishing/github-diagnosis${recheck ? '?recheck=true' : ''}`))),
|
|
75
|
+
},
|
|
70
76
|
{
|
|
71
77
|
name: 'setup_organization_build_engine', noSite: true,
|
|
72
78
|
description: 'Prepare or update the organization Cloudflare or GitHub build engine and start its isolated execution and artifact-transfer check. Use the selected provider’s engine revision. Setup does not change the organization’s default provider. Requires an organization API key and current revision. Keeps site repositories, branches and Hosting Groups separate. Read the engine status until verification finishes.',
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
package/skills/README.md
CHANGED
|
@@ -43,6 +43,7 @@ instructions. Read the relevant recipe when the task calls for it.
|
|
|
43
43
|
|---|---|---|
|
|
44
44
|
| `tr-blog.md` | "add a blog", "set up news" | Articles are Pages of a content type, sharing a native template and Page listing. |
|
|
45
45
|
| `tr-forms.md` | "contact form", "add a form", "booking form" | Form definition → embed HTML with signed token → inline JS feedback → deploy. |
|
|
46
|
+
| `tr-custom-blocks.md` | "make a block for…", "icon list", a section shape the core blocks lack | Starter or composition from core blocks with fields and bindings → validate → preview → create → place. |
|
|
46
47
|
| `tr-page-template.md` | Reusable Page templates: audio players, chapter lists, guest cards, image galleries — anything needing loops/nested data | Compose native blocks around `template_content_slot`; bind Page metadata, structured fields and references. |
|
|
47
48
|
| `tr-seo.md` | "SEO", "meta descriptions", "structured data" | Audit → fix titles/descriptions → OG images → JSON-LD → robots.txt → deploy. |
|
|
48
49
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tr-custom-blocks
|
|
3
|
+
description: Use when a Typeroll site needs a block the core library does not have (an icon list, cards, steps, a team strip), so the user wants a reusable block type with its own fields.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build a site block type
|
|
7
|
+
|
|
8
|
+
A site block type is reusable: editors place it and fill in its fields, and
|
|
9
|
+
every instance updates when the type changes. Build one when the same section
|
|
10
|
+
shape recurs, or when editors should fill in fields rather than arrange
|
|
11
|
+
blocks. For a one-off section use plain blocks; for a section copied and then
|
|
12
|
+
adapted per page use a block template; for content that must be identical
|
|
13
|
+
everywhere use a global block.
|
|
14
|
+
|
|
15
|
+
Creating, changing and deleting block types needs admin permission. A changed
|
|
16
|
+
block type reaches the live site with the next deploy.
|
|
17
|
+
|
|
18
|
+
## 1. Look before you build
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
list_block_types # core and site types: maybe it exists
|
|
22
|
+
list_block_type_starters # icon_list, feature_cards, steps
|
|
23
|
+
list_styles # the site's named styles to use inside
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Prefer a starter or a core block with the right settings over a new type.
|
|
27
|
+
|
|
28
|
+
## 2. Compose it from existing blocks
|
|
29
|
+
|
|
30
|
+
Composed is the default. Declare the editable fields in `schema` and build the
|
|
31
|
+
markup from core blocks in `composition`:
|
|
32
|
+
|
|
33
|
+
- An inner block field set to exactly `"{{props.name}}"` takes the block's
|
|
34
|
+
field value. Bindings keep their type (lists, links, booleans).
|
|
35
|
+
- For a repeating group, add a `core/repeater` with
|
|
36
|
+
`items: "{{props.items}}"`, `source_type: "static"` and no `item_block`. Its
|
|
37
|
+
children render once per item and read the item with `"{{item.title}}"`.
|
|
38
|
+
- Make a whole item a link with a `core/container`
|
|
39
|
+
`{ tag: "a", href: "{{item.link.href}}", new_tab: "{{item.link.new_tab}}" }`.
|
|
40
|
+
Items without a link render as a plain container.
|
|
41
|
+
- Use `core/text` for values from text fields (always escaped), `core/heading`
|
|
42
|
+
for headings, `core/icon`, `core/image`, `core/button`.
|
|
43
|
+
- Give inner blocks named styles (`style_id`) instead of CSS where you can.
|
|
44
|
+
|
|
45
|
+
Field types: `text`, `textarea`, `richtext`, `image`, `icon` (`{name}_svg`),
|
|
46
|
+
`link` (`{ page_id }` or `{ url }` plus `new_tab`; resolves to `href`, `target`,
|
|
47
|
+
`rel` and follows page renames), `select` with `options`, `boolean`, `number`,
|
|
48
|
+
`array` (`fields`, `item_label`, `min_items`, `max_items`), `object`.
|
|
49
|
+
|
|
50
|
+
## 3. Check and preview before saving
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
validate_block_type definition={…} # problems with paths, e.g. /schema/0/fields/1/name
|
|
54
|
+
preview_block_type definition={…} # html + css with sample data, as published
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Fix every error. Read warnings: a binding inside other text
|
|
58
|
+
(`"Call {{props.phone}}"`) is shown as typed; only whole-value bindings resolve.
|
|
59
|
+
|
|
60
|
+
## 4. CSS
|
|
61
|
+
|
|
62
|
+
`styles` apply only inside the block: selectors are prefixed automatically,
|
|
63
|
+
`:scope` is the block element, and `html`, `body` and `:root` are refused. Use
|
|
64
|
+
`var(--color-primary)` and the site's spacing, not fixed colours, and keep text
|
|
65
|
+
at WCAG AA contrast.
|
|
66
|
+
|
|
67
|
+
## 5. Create, place, deploy
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
create_block_type name="icon_list" label="Icon list" category="content" schema=[…] composition=[…] styles="…"
|
|
71
|
+
add_block page_id=<id> block={ type: "icon_list", data: { items: [ … ] } }
|
|
72
|
+
get_preview_link page_id=<id>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Show the preview to the user. Deploy only when they approve.
|
|
76
|
+
|
|
77
|
+
## Template block types (advanced)
|
|
78
|
+
|
|
79
|
+
When the markup cannot be built from blocks, use `template` instead of
|
|
80
|
+
`composition`: `{{field}}` (escaped), `{{{richtext_field}}}`, `{{#field}}…{{/field}}`,
|
|
81
|
+
`{{^field}}…{{/field}}` (empty), `{{#each items}}…{{/each}}` with `{{@index}}`,
|
|
82
|
+
`{{@number}}`, `{{@first}}`, `{{@last}}`, and `{{#link field class="x"}}…{{/link}}`.
|
|
83
|
+
A type has a composition or a template, never both.
|
|
84
|
+
|
|
85
|
+
## Changing a type in use
|
|
86
|
+
|
|
87
|
+
- Rename a field with `renames: { "old": "new" }` (`"items.old": "new"` inside a
|
|
88
|
+
group): Typeroll moves the data everywhere the type is used.
|
|
89
|
+
- Removing or retyping a field that holds data is refused with the list of
|
|
90
|
+
usages unless you send `confirm_data_loss: true`. Ask the user first.
|
|
91
|
+
- `find_pages_using_block_type` before deleting; a type in use cannot be
|
|
92
|
+
deleted.
|
|
93
|
+
|
|
94
|
+
## Pitfalls
|
|
95
|
+
|
|
96
|
+
- Pasting HTML into a `core/html` block inside a composition: editors cannot
|
|
97
|
+
edit it and it skips the site's styles. Compose from real blocks.
|
|
98
|
+
- Binding a text field into `core/prose`: the value becomes HTML. Use `core/text`.
|
|
99
|
+
- Nesting links: an icon or button with its own link inside a linked container.
|
|
100
|
+
- Linked containers on a site below render version 4: every icon sits in a
|
|
101
|
+
link of its own, so the icon list starter nests links, and an item without
|
|
102
|
+
an address is still a link. The write warns; upgrade the site's rendering
|
|
103
|
+
(`tr-upgrade-rendering`) first.
|
|
104
|
+
- Writing `script` without the user's explicit request.
|