@typeroll/mcp-server 0.45.65 → 0.45.66
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 +41 -6
- package/dist/bundled-content.js +6 -5
- package/dist/server.js +9 -1
- package/dist/structured-data.js +217 -0
- package/dist/tools/structured-data.js +64 -0
- package/dist/tools/workflows.js +8 -4
- package/dist/version.js +1 -1
- package/dist/workspace.js +12 -1
- package/package.json +4 -2
- package/skills/README.md +3 -1
- package/skills/tr-content-write.md +16 -0
- package/skills/tr-edit-site.md +88 -1
- package/skills/tr-new-site.md +7 -1
- package/skills/tr-seo.md +58 -3
- package/skills/tr-starter-design.md +79 -0
package/dist/server.js
CHANGED
|
@@ -11,6 +11,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import { ownerReviewTools } from './tools/owner-review.js';
|
|
13
13
|
import { pageTools } from './tools/pages.js';
|
|
14
|
+
import { structuredDataTools } from './tools/structured-data.js';
|
|
14
15
|
import { partialTools } from './tools/partials.js';
|
|
15
16
|
import { contentTypeTools } from './tools/content-types.js';
|
|
16
17
|
import { pageTemplateTools } from './tools/page-templates.js';
|
|
@@ -141,7 +142,9 @@ The full playbook ships with this server — use it:
|
|
|
141
142
|
0. First confirm the site, organization and version with the user. Personal
|
|
142
143
|
connections require select_site and its site_context on subsequent calls.
|
|
143
144
|
Use list_skills and read_skill for only the relevant recipe. Use read_guide
|
|
144
|
-
sections when needed; do not load the whole manual by default.
|
|
145
|
+
sections when needed; do not load the whole manual by default. Everyday
|
|
146
|
+
edits (links, images, menus, site-wide changes, settings, analytics,
|
|
147
|
+
structured data) are in tr-edit-site.
|
|
145
148
|
1. When the task is "build / migrate / redesign / brand a site", call
|
|
146
149
|
list_skills FIRST, then read_skill <name> for the step-by-step recipe
|
|
147
150
|
(tr-new-site, tr-migrate-wp, tr-brand, tr-blog, tr-responsive, …). These are
|
|
@@ -178,6 +181,10 @@ The full playbook ships with this server — use it:
|
|
|
178
181
|
start from and then adapt is a block template (list_block_templates,
|
|
179
182
|
insert_block_template copies it). Never duplicate shared content by hand.
|
|
180
183
|
8. No site yet? With an org-scoped key, create_site bootstraps one.
|
|
184
|
+
9. AI workflows (site_planning, content_generation, content_improvement,
|
|
185
|
+
schema_markup, create_site_and_plan) need the Organization's portal AI
|
|
186
|
+
key for the Site. On ai_not_enabled don't retry: do the work yourself
|
|
187
|
+
with the recipe named in start_workflow and tell the user you did.
|
|
181
188
|
|
|
182
189
|
If anything here conflicts with what a tool returns, trust the tool. Every
|
|
183
190
|
tool's own description carries its specifics.
|
|
@@ -194,6 +201,7 @@ export function buildServer(options) {
|
|
|
194
201
|
...skillTools,
|
|
195
202
|
...siteTools,
|
|
196
203
|
...pageTools,
|
|
204
|
+
...structuredDataTools,
|
|
197
205
|
...ownerReviewTools,
|
|
198
206
|
...partialTools,
|
|
199
207
|
...blockTypeTools,
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
// Schema.org JSON-LD builders for Page.json_ld: FAQPage, Recipe and Event.
|
|
2
|
+
//
|
|
3
|
+
// One implementation for every caller. The MCP tools (tools/structured-data.ts)
|
|
4
|
+
// and the portal's built-in AI chat (portal/src/lib/anthropic.ts, imported as
|
|
5
|
+
// @typeroll/mcp-server/structured-data) both validate input with these zod
|
|
6
|
+
// shapes and build JSON-LD with these functions, so the two cannot drift. It
|
|
7
|
+
// lives in this package rather than @typeroll/shared because the standalone
|
|
8
|
+
// MCP package is published without @typeroll/shared, while the portal already
|
|
9
|
+
// depends on this package.
|
|
10
|
+
//
|
|
11
|
+
// mergeJsonLd() adds the new node to whatever the page already carries: a node
|
|
12
|
+
// of the same @type is replaced (re-running a tool updates it), other nodes are
|
|
13
|
+
// kept in an @graph, so an FAQ added later does not erase a LocalBusiness.
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
16
|
+
/** Invalid input or an unusable existing json_ld; `issues` name the field. */
|
|
17
|
+
export class StructuredDataError extends Error {
|
|
18
|
+
issues;
|
|
19
|
+
constructor(issues) {
|
|
20
|
+
super(`Invalid structured data: ${issues.join('; ')}`);
|
|
21
|
+
this.issues = issues;
|
|
22
|
+
this.name = 'StructuredDataError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const text = (max) => z.string().trim().min(1, 'Must not be empty.').max(max);
|
|
26
|
+
const httpUrl = z.string().trim().max(2048).url('Use an absolute http(s) URL.')
|
|
27
|
+
.refine((value) => /^https?:\/\//i.test(value), 'Use an absolute http(s) URL.');
|
|
28
|
+
const ISO_DURATION = /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+(?:\.\d+)?S)?)?$/;
|
|
29
|
+
const duration = z.string().trim().regex(ISO_DURATION, 'Use an ISO 8601 duration such as PT30M or PT1H15M.');
|
|
30
|
+
const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})(?:T([01]\d|2[0-3]):([0-5]\d)(?::([0-5]\d)(?:\.\d+)?)?(Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)?)?$/;
|
|
31
|
+
function realDate(value) {
|
|
32
|
+
const m = ISO_DATE.exec(value);
|
|
33
|
+
if (!m)
|
|
34
|
+
return false;
|
|
35
|
+
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
36
|
+
const date = new Date(Date.UTC(y, mo - 1, d));
|
|
37
|
+
return date.getUTCFullYear() === y && date.getUTCMonth() === mo - 1 && date.getUTCDate() === d;
|
|
38
|
+
}
|
|
39
|
+
const isoDate = z.string().trim().refine(realDate, 'Use an ISO 8601 date or date-time, e.g. 2026-06-12 or 2026-06-12T19:00:00+02:00.');
|
|
40
|
+
const hasTime = (value) => value.includes('T');
|
|
41
|
+
const hasOffset = (value) => /(?:Z|[+-]\d{2}:\d{2})$/.test(value);
|
|
42
|
+
/** Input fields of add_faq_schema (besides page_id). */
|
|
43
|
+
export const faqSchemaShape = {
|
|
44
|
+
items: z.array(z.object({
|
|
45
|
+
question: text(500),
|
|
46
|
+
answer: text(5000).describe('Plain text or simple HTML, exactly as the page shows it.'),
|
|
47
|
+
}).strict()).min(1, 'Add at least one question.').max(100)
|
|
48
|
+
.describe('The questions and answers the page itself shows, in page order.'),
|
|
49
|
+
};
|
|
50
|
+
/** Input fields of add_recipe_schema (besides page_id). */
|
|
51
|
+
export const recipeSchemaShape = {
|
|
52
|
+
name: text(200),
|
|
53
|
+
description: text(1000).optional(),
|
|
54
|
+
image_url: httpUrl.optional().describe('Absolute image URL, e.g. the page\'s CDN image. Google needs an image for recipe rich results.'),
|
|
55
|
+
author: text(200).optional(),
|
|
56
|
+
prep_time: duration.optional().describe('ISO 8601 duration, e.g. PT30M.'),
|
|
57
|
+
cook_time: duration.optional().describe('ISO 8601 duration, e.g. PT1H.'),
|
|
58
|
+
total_time: duration.optional().describe('ISO 8601 duration, e.g. PT1H30M.'),
|
|
59
|
+
recipe_yield: text(100).optional().describe('e.g. "4 servings".'),
|
|
60
|
+
ingredients: z.array(text(500)).min(1, 'Add at least one ingredient.').max(100),
|
|
61
|
+
instructions: z.array(text(2000)).min(1, 'Add at least one step.').max(100).describe('One entry per step, in order.'),
|
|
62
|
+
};
|
|
63
|
+
/** Input fields of add_event_schema (besides page_id). */
|
|
64
|
+
export const eventSchemaShape = {
|
|
65
|
+
name: text(200),
|
|
66
|
+
description: text(1000).optional(),
|
|
67
|
+
start_date: isoDate.describe('ISO 8601, e.g. 2026-06-12T19:00:00+02:00. Include the UTC offset.'),
|
|
68
|
+
end_date: isoDate.optional().describe('ISO 8601; not before start_date.'),
|
|
69
|
+
location_name: text(200).optional(),
|
|
70
|
+
location_address: text(500).optional(),
|
|
71
|
+
image_url: httpUrl.optional(),
|
|
72
|
+
offers_url: httpUrl.optional().describe('Ticket URL.'),
|
|
73
|
+
offers_price: z.union([z.number().nonnegative(), z.string().trim().regex(/^\d+(?:\.\d+)?$/, 'Use a number such as 250 or 19.95.')])
|
|
74
|
+
.optional().describe('Ticket price as a number; 0 for a free event. Needs offers_currency.'),
|
|
75
|
+
offers_currency: z.string().trim().regex(/^[A-Za-z]{3}$/, 'Use an ISO 4217 code such as EUR or SEK.').optional()
|
|
76
|
+
.describe('ISO 4217, e.g. EUR.'),
|
|
77
|
+
};
|
|
78
|
+
function parse(schema, input) {
|
|
79
|
+
const result = schema.safeParse(input);
|
|
80
|
+
if (result.success)
|
|
81
|
+
return result.data;
|
|
82
|
+
throw new StructuredDataError(result.error.issues.map((i) => `${i.path.join('.') || 'input'}: ${i.message}`));
|
|
83
|
+
}
|
|
84
|
+
const CONTEXT = 'https://schema.org';
|
|
85
|
+
export function buildFaqJsonLd(input) {
|
|
86
|
+
const { items } = parse(z.object(faqSchemaShape), input);
|
|
87
|
+
return {
|
|
88
|
+
node: {
|
|
89
|
+
'@context': CONTEXT,
|
|
90
|
+
'@type': 'FAQPage',
|
|
91
|
+
mainEntity: items.map((it) => ({
|
|
92
|
+
'@type': 'Question',
|
|
93
|
+
name: it.question,
|
|
94
|
+
acceptedAnswer: { '@type': 'Answer', text: it.answer },
|
|
95
|
+
})),
|
|
96
|
+
},
|
|
97
|
+
warnings: [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
export function buildRecipeJsonLd(input) {
|
|
101
|
+
const r = parse(z.object(recipeSchemaShape), input);
|
|
102
|
+
const node = { '@context': CONTEXT, '@type': 'Recipe', name: r.name };
|
|
103
|
+
if (r.description)
|
|
104
|
+
node.description = r.description;
|
|
105
|
+
if (r.image_url)
|
|
106
|
+
node.image = r.image_url;
|
|
107
|
+
if (r.author)
|
|
108
|
+
node.author = { '@type': 'Person', name: r.author };
|
|
109
|
+
if (r.prep_time)
|
|
110
|
+
node.prepTime = r.prep_time;
|
|
111
|
+
if (r.cook_time)
|
|
112
|
+
node.cookTime = r.cook_time;
|
|
113
|
+
if (r.total_time)
|
|
114
|
+
node.totalTime = r.total_time;
|
|
115
|
+
if (r.recipe_yield)
|
|
116
|
+
node.recipeYield = r.recipe_yield;
|
|
117
|
+
node.recipeIngredient = r.ingredients;
|
|
118
|
+
node.recipeInstructions = r.instructions.map((step, i) => ({ '@type': 'HowToStep', position: i + 1, text: step }));
|
|
119
|
+
const warnings = r.image_url ? [] : ['No image_url: Google shows recipe rich results only for recipes with an image.'];
|
|
120
|
+
return { node, warnings };
|
|
121
|
+
}
|
|
122
|
+
export function buildEventJsonLd(input) {
|
|
123
|
+
const e = parse(z.object(eventSchemaShape).superRefine((v, ctx) => {
|
|
124
|
+
if (v.offers_price !== undefined && !v.offers_currency) {
|
|
125
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['offers_currency'], message: 'Required with offers_price.' });
|
|
126
|
+
}
|
|
127
|
+
if (v.end_date && hasTime(v.start_date) === hasTime(v.end_date) && hasOffset(v.start_date) === hasOffset(v.end_date)
|
|
128
|
+
&& Date.parse(v.end_date) < Date.parse(v.start_date)) {
|
|
129
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['end_date'], message: 'Must not be before start_date.' });
|
|
130
|
+
}
|
|
131
|
+
}), input);
|
|
132
|
+
const node = { '@context': CONTEXT, '@type': 'Event', name: e.name, startDate: e.start_date };
|
|
133
|
+
if (e.end_date)
|
|
134
|
+
node.endDate = e.end_date;
|
|
135
|
+
if (e.description)
|
|
136
|
+
node.description = e.description;
|
|
137
|
+
if (e.image_url)
|
|
138
|
+
node.image = e.image_url;
|
|
139
|
+
if (e.location_name || e.location_address) {
|
|
140
|
+
node.location = {
|
|
141
|
+
'@type': 'Place',
|
|
142
|
+
...(e.location_name ? { name: e.location_name } : {}),
|
|
143
|
+
...(e.location_address ? { address: e.location_address } : {}),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (e.offers_url || e.offers_price !== undefined) {
|
|
147
|
+
node.offers = {
|
|
148
|
+
'@type': 'Offer',
|
|
149
|
+
...(e.offers_url ? { url: e.offers_url } : {}),
|
|
150
|
+
...(e.offers_price !== undefined ? { price: String(e.offers_price) } : {}),
|
|
151
|
+
...(e.offers_currency ? { priceCurrency: e.offers_currency.toUpperCase() } : {}),
|
|
152
|
+
availability: 'https://schema.org/InStock',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const warnings = [];
|
|
156
|
+
if (hasTime(e.start_date) && !hasOffset(e.start_date))
|
|
157
|
+
warnings.push('start_date has no UTC offset; add one (e.g. +02:00) so search engines show the right local time.');
|
|
158
|
+
if (!node.location)
|
|
159
|
+
warnings.push('No location: Google shows event rich results only for events with a location.');
|
|
160
|
+
return { node, warnings };
|
|
161
|
+
}
|
|
162
|
+
/** The three page structured-data helpers, keyed by kind. */
|
|
163
|
+
export const STRUCTURED_DATA = {
|
|
164
|
+
faq: { tool: 'add_faq_schema', type: 'FAQPage', shape: faqSchemaShape, build: buildFaqJsonLd },
|
|
165
|
+
recipe: { tool: 'add_recipe_schema', type: 'Recipe', shape: recipeSchemaShape, build: buildRecipeJsonLd },
|
|
166
|
+
event: { tool: 'add_event_schema', type: 'Event', shape: eventSchemaShape, build: buildEventJsonLd },
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* JSON Schema of a helper's input including `page_id`, for tool definitions
|
|
170
|
+
* outside MCP (the portal chat passes it to the model as input_schema).
|
|
171
|
+
*/
|
|
172
|
+
export function structuredDataInputSchema(kind) {
|
|
173
|
+
const schema = zodToJsonSchema(z.object({ page_id: z.string().min(1), ...STRUCTURED_DATA[kind].shape }), { $refStrategy: 'none' });
|
|
174
|
+
const { $schema: _schema, ...rest } = schema;
|
|
175
|
+
return rest;
|
|
176
|
+
}
|
|
177
|
+
function typesOf(node) {
|
|
178
|
+
const t = node['@type'];
|
|
179
|
+
return Array.isArray(t) ? t.map(String) : t == null ? [] : [String(t)];
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Combine a page's existing json_ld string with a new node. Nodes of the same
|
|
183
|
+
* @type are replaced; other nodes are kept. Returns the json_ld string to
|
|
184
|
+
* store. An existing value that is not a JSON object/array is refused rather
|
|
185
|
+
* than silently overwritten.
|
|
186
|
+
*/
|
|
187
|
+
export function mergeJsonLd(existing, node) {
|
|
188
|
+
const raw = typeof existing === 'string' ? existing.trim() : '';
|
|
189
|
+
let nodes = [];
|
|
190
|
+
if (raw) {
|
|
191
|
+
let data;
|
|
192
|
+
try {
|
|
193
|
+
data = JSON.parse(raw);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new StructuredDataError(['json_ld: the page\'s existing json_ld is not valid JSON. Read the page, then fix or clear json_ld with update_page before adding structured data.']);
|
|
197
|
+
}
|
|
198
|
+
const list = Array.isArray(data) ? data
|
|
199
|
+
: data && typeof data === 'object' && Array.isArray(data['@graph']) ? data['@graph']
|
|
200
|
+
: [data];
|
|
201
|
+
if (!list.every((n) => n && typeof n === 'object' && !Array.isArray(n))) {
|
|
202
|
+
throw new StructuredDataError(['json_ld: the page\'s existing json_ld is not a JSON-LD object or list of objects. Fix or clear it with update_page first.']);
|
|
203
|
+
}
|
|
204
|
+
nodes = list;
|
|
205
|
+
}
|
|
206
|
+
const newTypes = typesOf(node);
|
|
207
|
+
const kept = nodes.filter((n) => !typesOf(n).some((t) => newTypes.includes(t)));
|
|
208
|
+
const replaced = nodes.length - kept.length;
|
|
209
|
+
if (!kept.length)
|
|
210
|
+
return { json_ld: JSON.stringify(node), replaced, kept: 0 };
|
|
211
|
+
const strip = ({ '@context': _context, ...rest }) => rest;
|
|
212
|
+
return {
|
|
213
|
+
json_ld: JSON.stringify({ '@context': CONTEXT, '@graph': [...kept.map(strip), strip(node)] }),
|
|
214
|
+
replaced,
|
|
215
|
+
kept: kept.length,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Structured-data tools: FAQPage, Recipe and Event JSON-LD on a page.
|
|
2
|
+
//
|
|
3
|
+
// The same inputs, validation and builders as the portal AI chat's tools of
|
|
4
|
+
// the same names (../structured-data.ts). Each tool reads the page's draft
|
|
5
|
+
// view, merges the new node into its json_ld (same @type replaced, others
|
|
6
|
+
// kept) and writes json_ld back through update_page's route, so the change
|
|
7
|
+
// follows the buffer model like any other content write.
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
import { ok, versionParam, withErrorBoundary } from './helpers.js';
|
|
10
|
+
import { mergeJsonLd, STRUCTURED_DATA, StructuredDataError } from '../structured-data.js';
|
|
11
|
+
const DESCRIPTIONS = {
|
|
12
|
+
faq: 'Add FAQPage JSON-LD to a page that visibly presents these questions and answers (Google penalizes FAQ markup for content the page does not show). Copy the questions and answers from the page.',
|
|
13
|
+
recipe: 'Add Recipe JSON-LD to a recipe page: name, ingredients and steps (required), plus description, image, author, ISO 8601 durations (PT30M) and yield.',
|
|
14
|
+
event: 'Add Event JSON-LD to a page about one upcoming event: name and ISO 8601 start_date with UTC offset (required), end, place, image and ticket offer. A price needs offers_currency.',
|
|
15
|
+
};
|
|
16
|
+
const COMMON = ' Validates the input and builds the schema.org JSON-LD for you; prefer this to hand-writing json_ld. It is merged into the page\'s json_ld: an existing node of the same @type is replaced, other nodes (e.g. LocalBusiness) are kept in an @graph. Returns the stored JSON-LD and warnings about rich-result eligibility. BUFFER MODEL: lands in the page\'s unsaved draft; pass save:true or commit_working_copy after the user approves. Needs write permission.';
|
|
17
|
+
function v(version) {
|
|
18
|
+
return version ? { version } : undefined;
|
|
19
|
+
}
|
|
20
|
+
function invalid(error) {
|
|
21
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: error.message, issues: error.issues }, null, 2) }], isError: true };
|
|
22
|
+
}
|
|
23
|
+
export const structuredDataTools = Object.keys(STRUCTURED_DATA).map((kind) => {
|
|
24
|
+
const spec = STRUCTURED_DATA[kind];
|
|
25
|
+
return {
|
|
26
|
+
name: spec.tool,
|
|
27
|
+
description: DESCRIPTIONS[kind] + COMMON,
|
|
28
|
+
inputSchema: {
|
|
29
|
+
page_id: z.string().min(1),
|
|
30
|
+
...spec.shape,
|
|
31
|
+
save: z.boolean().optional().describe('Also save (commit) the draft in the same call, for a change the user already approved.'),
|
|
32
|
+
version: versionParam,
|
|
33
|
+
},
|
|
34
|
+
handler: withErrorBoundary(async (args, { client, siteId }) => {
|
|
35
|
+
const { page_id, save, version, ...input } = args;
|
|
36
|
+
let built;
|
|
37
|
+
let merged;
|
|
38
|
+
const path = `pages/${encodeURIComponent(page_id)}`;
|
|
39
|
+
try {
|
|
40
|
+
built = spec.build(input);
|
|
41
|
+
// The draft view, so an unsaved json_ld edit is merged, not lost.
|
|
42
|
+
const current = await client.get(siteId, path, v(version));
|
|
43
|
+
merged = mergeJsonLd(current.page?.json_ld, built.node);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error instanceof StructuredDataError)
|
|
47
|
+
return invalid(error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
await client.patch(siteId, path, { json_ld: merged.json_ld, ...(save ? { save: true } : {}) }, v(version));
|
|
51
|
+
return ok({
|
|
52
|
+
ok: true,
|
|
53
|
+
page_id,
|
|
54
|
+
schema_type: spec.type,
|
|
55
|
+
replaced_same_type: merged.replaced,
|
|
56
|
+
kept_other_nodes: merged.kept,
|
|
57
|
+
saved: !!save,
|
|
58
|
+
warnings: built.warnings,
|
|
59
|
+
json_ld: JSON.parse(merged.json_ld),
|
|
60
|
+
next: save ? 'Saved. Deploy when the user wants it live.' : 'Unsaved draft: preview with get_preview_link include_working_copy:true, then save with commit_working_copy when approved.',
|
|
61
|
+
});
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
});
|
package/dist/tools/workflows.js
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
// Site together with its first workflow.
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { ok, versionParam, withErrorBoundary } from './helpers.js';
|
|
7
|
+
// Run by the portal with the Organization's own AI key; without access for the
|
|
8
|
+
// Site the run fails with failure_code ai_not_enabled. The agent then does the
|
|
9
|
+
// work itself with the named recipe instead of retrying.
|
|
10
|
+
const AI_WORKFLOWS = 'AI workflows — site_planning, content_generation, content_improvement and schema_markup — call the model with the Organization\'s own AI key and need AI access for this Site; without it the run fails with failure_code "ai_not_enabled" (failure_reason: the AI assistant is not enabled for this site). Do not retry or work around it: you are an AI agent, so do the same work yourself and tell the user you did — site_planning: tr-new-site (propose the page structure, get approval, then build); content_generation: tr-content-write (draft page); content_improvement: tr-content-write on the weakest published pages (thin text, missing seo_title/seo_description, stale), as unsaved drafts for review; schema_markup: tr-seo step 5 (add_faq_schema, add_recipe_schema, add_event_schema or json_ld). seo_audit still runs without AI and returns its issue list unprioritized. Enabling the portal AI is the Organization owner\'s or admin\'s choice, never a prerequisite for you.';
|
|
7
11
|
const WORKFLOW_TYPES = [
|
|
8
12
|
'migration', 'site_planning', 'seo_audit', 'content_improvement', 'link_check',
|
|
9
13
|
'performance_audit', 'content_generation', 'schema_markup', 'url_parity', 'rebuild_deploy',
|
|
@@ -11,13 +15,13 @@ const WORKFLOW_TYPES = [
|
|
|
11
15
|
export const workflowTools = [
|
|
12
16
|
{
|
|
13
17
|
name: 'list_workflows',
|
|
14
|
-
description: 'List the workflow types this Site can run (label, description, steps, required_permission) and its most recent runs with status. Read only. Migration, site planning and URL parity pause for review; check get_workflow for paused_for_review.',
|
|
18
|
+
description: 'List the workflow types this Site can run (label, description, steps, required_permission) and its most recent runs with status. Read only. Migration, site planning and URL parity pause for review; check get_workflow for paused_for_review. Site planning, content generation, content improvement and schema markup need the Organization\'s AI key for this Site (see start_workflow).',
|
|
15
19
|
inputSchema: { limit: z.number().int().min(1).max(100).optional() },
|
|
16
20
|
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'workflows', args.limit ? { limit: args.limit } : undefined))),
|
|
17
21
|
},
|
|
18
22
|
{
|
|
19
23
|
name: 'start_workflow',
|
|
20
|
-
description: 'Start a server-side workflow on this Site; it runs in the background and returns workflow_id. Poll get_workflow. Config per type (same fields as the portal form): migration { wp_url, helper_api_key? } (needs verified organization import storage); site_planning { business_description }; content_generation { topic, tone?, audience? }; content_improvement { limit? }; url_parity { target_origin?, source_origin?, check_source?, statuses? }; rebuild_deploy { environment: staging | production }; seo_audit, link_check, performance_audit and schema_markup take none. Needs write permission; rebuild_deploy publishes and needs admin. Workflows that change content write to the given version (default main) — get the user\'s agreement first.',
|
|
24
|
+
description: 'Start a server-side workflow on this Site; it runs in the background and returns workflow_id. Poll get_workflow. Config per type (same fields as the portal form): migration { wp_url, helper_api_key? } (needs verified organization import storage); site_planning { business_description }; content_generation { topic, tone?, audience? }; content_improvement { limit? }; url_parity { target_origin?, source_origin?, check_source?, statuses? }; rebuild_deploy { environment: staging | production }; seo_audit, link_check, performance_audit and schema_markup take none. Needs write permission; rebuild_deploy publishes and needs admin. Workflows that change content write to the given version (default main) — get the user\'s agreement first. ' + AI_WORKFLOWS,
|
|
21
25
|
inputSchema: {
|
|
22
26
|
type: z.enum(WORKFLOW_TYPES),
|
|
23
27
|
config: z.record(z.unknown()).optional(),
|
|
@@ -27,7 +31,7 @@ export const workflowTools = [
|
|
|
27
31
|
},
|
|
28
32
|
{
|
|
29
33
|
name: 'get_workflow',
|
|
30
|
-
description: 'Read one workflow run: status (pending | running | paused_for_review | completed | failed), current step, progress, log, review_message/review_data when paused, results and failure_reason. Credential config is masked.',
|
|
34
|
+
description: 'Read one workflow run: status (pending | running | paused_for_review | completed | failed), current step, progress, log, review_message/review_data when paused, results, failure_reason and failure_code (newer Core; older Core reports failure_reason only). Credential config is masked. failure_code "ai_not_enabled" (or a failure_reason saying the AI assistant is not enabled for this site) means the Site has no portal AI access: do the work yourself with the recipe named in start_workflow instead of retrying.',
|
|
31
35
|
inputSchema: { workflow_id: z.string() },
|
|
32
36
|
handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, `workflows/${encodeURIComponent(args.workflow_id)}`))),
|
|
33
37
|
},
|
|
@@ -51,7 +55,7 @@ export const workflowTools = [
|
|
|
51
55
|
{
|
|
52
56
|
name: 'create_site_and_plan',
|
|
53
57
|
noSite: true,
|
|
54
|
-
description: 'Create a new Site in the Organization and start AI site planning from a business description, like "Plan a new site with AI" on the New site page. Requires an organization API key. Needs the Organization\'s own AI key with site access "all" (read_organization_ai_settings); otherwise it answers 403 ai_not_enabled and creates nothing — then
|
|
58
|
+
description: 'Create a new Site in the Organization and start AI site planning from a business description, like "Plan a new site with AI" on the New site page. Requires an organization API key. Needs the Organization\'s own AI key with site access "all" (read_organization_ai_settings); otherwise it answers 403 ai_not_enabled and creates nothing — then create_site a blank Site and plan it yourself with tr-new-site (propose the pages, get the user\'s approval, build), and say that you did; enabling the portal AI is the owner\'s or admin\'s choice, not a prerequisite. The plan pauses for review: read it with get_workflow and approve_workflow only after the user agrees.',
|
|
55
59
|
inputSchema: {
|
|
56
60
|
name: z.string().min(1),
|
|
57
61
|
business_description: z.string().min(1).describe('What the business does, for whom, and what sets it apart.'),
|
package/dist/version.js
CHANGED
package/dist/workspace.js
CHANGED
|
@@ -57,6 +57,17 @@ Public documentation: https://typeroll.com/docs/llms.txt
|
|
|
57
57
|
- Never edit generated publishing source to change CMS content. Do not overwrite
|
|
58
58
|
current CMS or owner answers from an old local snapshot.
|
|
59
59
|
|
|
60
|
+
## Everyday requests
|
|
61
|
+
|
|
62
|
+
- Links, images, menus, settings, analytics and site-wide changes: read the
|
|
63
|
+
tr-edit-site recipe. Shared content becomes one global block, not N edits.
|
|
64
|
+
- FAQ, recipe and event pages: add_faq_schema, add_recipe_schema and
|
|
65
|
+
add_event_schema build validated JSON-LD (see tr-seo); don't hand-write it.
|
|
66
|
+
- A blank site or the starter design: tr-starter-design lists its variables.
|
|
67
|
+
- The portal's built-in AI is optional. When an AI workflow (site planning,
|
|
68
|
+
content generation or improvement, schema markup) reports ai_not_enabled,
|
|
69
|
+
do the work yourself with the recipe it names and say so; don't retry.
|
|
70
|
+
|
|
60
71
|
Store credentials in the client's private settings or injected environment.
|
|
61
72
|
Run the workspace doctor after setup and after changing the binding.
|
|
62
73
|
`;
|
|
@@ -69,7 +80,7 @@ export const workspaceTemplates = {
|
|
|
69
80
|
'decisions.md': '# Decisions\n\nRecord date, decision, reason, approved scope and alternatives when useful.\n',
|
|
70
81
|
'sources/README.md': '# Sources\n\nStore research references and copy proposals. Record source URLs and dates.\nPrivate extracts belong in ignored private/ or an approved external store.\nDo not treat local drafts as current CMS content or commit media libraries.\n',
|
|
71
82
|
'scripts/README.md': '# Project automation\n\nKeep repeatable imports and QA scripts here. Use the authenticated REST API\nfor scripted work; keep credentials outside files and inject them at runtime.\nMCP remains the interactive interface. Typeroll currently has no general site CRUD CLI.\n',
|
|
72
|
-
'qa/checklist.md': '# Verification\n\n- Confirm Site, Version and deployed/source identity.\n- Read back saved content and inspect desktop/mobile previews.\n- Exercise menus, keyboard focus, links and enabled app states.\n- Verify routes, metadata, media and publishing readiness before launch.\n- Record tested pages, viewports, results and unresolved issues.\n- Store screenshots and private runtime reports in ignored reports/.\n- Publishing validation does not prove visual or editorial completeness.\n',
|
|
83
|
+
'qa/checklist.md': '# Verification\n\n- Confirm Site, Version and deployed/source identity.\n- Read back saved content and inspect desktop/mobile previews.\n- Exercise menus, keyboard focus, links and enabled app states.\n- Verify routes, metadata, media and publishing readiness before launch.\n- Check that structured data (FAQ, recipe, event) matches what each page shows.\n- Record tested pages, viewports, results and unresolved issues.\n- Store screenshots and private runtime reports in ignored reports/.\n- Publishing validation does not prove visual or editorial completeness.\n',
|
|
73
84
|
};
|
|
74
85
|
export async function readWorkspace(dir) {
|
|
75
86
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeroll/mcp-server",
|
|
3
|
-
"version": "0.45.
|
|
3
|
+
"version": "0.45.66",
|
|
4
4
|
"description": "Typeroll CMS MCP server – connect AI agents to the REST API to manage content and publish static websites.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,9 @@
|
|
|
21
21
|
"./server": "./src/server.ts",
|
|
22
22
|
"./client": "./src/client.ts",
|
|
23
23
|
"./version": "./src/version.ts",
|
|
24
|
-
"./tools/helpers": "./src/tools/helpers.ts"
|
|
24
|
+
"./tools/helpers": "./src/tools/helpers.ts",
|
|
25
|
+
"./structured-data": "./src/structured-data.ts",
|
|
26
|
+
"./bundled-content": "./src/bundled-content.ts"
|
|
25
27
|
},
|
|
26
28
|
"files": [
|
|
27
29
|
"dist",
|
package/skills/README.md
CHANGED
|
@@ -30,6 +30,8 @@ instructions. Read the relevant recipe when the task calls for it.
|
|
|
30
30
|
|
|
31
31
|
| File | When it triggers | What it does |
|
|
32
32
|
|---|---|---|
|
|
33
|
+
| `tr-edit-site.md` | "change this page", "add a link to the menu", "put our phone number on every page" | Confirm the site, find the page, everyday requests (links, images, menus, site-wide changes as global blocks, settings, analytics), preview and save. |
|
|
34
|
+
| `tr-starter-design.md` | blank site, or one that still follows the starter design | The default CSS variables, spacing, radii, container and header/footer conventions to match. |
|
|
33
35
|
| `tr-new-site.md` | "create a new site", "bootstrap a site for…" | Settings → header/footer partials → homepage → inner pages → deploy. Full setup recipe. |
|
|
34
36
|
| `tr-brand.md` | "create a brand", "choose colors", "design the look"| Palette recipes by mood, typography pairings, CSS variable setup, preview. |
|
|
35
37
|
| `tr-redesign-branch.md`| "redesign", "modernize", anything site-wide-design | Branch-isolated work with preview links, merge when approved. |
|
|
@@ -45,7 +47,7 @@ instructions. Read the relevant recipe when the task calls for it.
|
|
|
45
47
|
| `tr-forms.md` | "contact form", "add a form", "booking form" | Form definition → embed HTML with signed token → inline JS feedback → deploy. |
|
|
46
48
|
| `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. |
|
|
47
49
|
| `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. |
|
|
48
|
-
| `tr-seo.md` | "SEO", "meta descriptions", "structured data"
|
|
50
|
+
| `tr-seo.md` | "SEO", "meta descriptions", "structured data", "FAQ schema" | Audit → fix titles/descriptions → URL conventions → OG images → JSON-LD (`add_faq_schema`, `add_recipe_schema`, `add_event_schema`) → robots.txt → deploy. |
|
|
49
51
|
|
|
50
52
|
### Import content
|
|
51
53
|
|
|
@@ -115,6 +115,22 @@ shortening, adding a CTA, tweaking SEO description.
|
|
|
115
115
|
Don't `update_page status:"published"` without an explicit "looks
|
|
116
116
|
good, publish it" from the user. Same for `trigger_deploy`.
|
|
117
117
|
|
|
118
|
+
## Instead of the content workflows
|
|
119
|
+
|
|
120
|
+
`start_workflow` types `content_generation` and `content_improvement` run the
|
|
121
|
+
model with the Organization's own AI key. On a Site without that access they
|
|
122
|
+
fail with `failure_code: "ai_not_enabled"`; don't retry them. Do the work with
|
|
123
|
+
this recipe and tell the user you did it yourself:
|
|
124
|
+
|
|
125
|
+
- **Generate a page** (`content_generation { topic, tone?, audience? }`):
|
|
126
|
+
steps 1–5 for that topic, created as a draft.
|
|
127
|
+
- **Improve content** (`content_improvement { limit? }`): `list_pages
|
|
128
|
+
status="published"` and `batch_read_pages`, rank the pages by thin text,
|
|
129
|
+
missing `seo_title`/`seo_description` and age (not updated for a year), and
|
|
130
|
+
rewrite the weakest few (five by default) in the site's voice. Keep facts,
|
|
131
|
+
URLs and structure; leave the rewrites as unsaved drafts and show previews
|
|
132
|
+
before anything is saved or published.
|
|
133
|
+
|
|
118
134
|
## SEO conventions worth knowing
|
|
119
135
|
|
|
120
136
|
- **`kind: "article"`** for blog posts and news. Switches to
|
package/skills/tr-edit-site.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tr-edit-site
|
|
3
|
-
description: Edit or create Typeroll pages with a person’s usual AI assistant. Confirm one site, find the right page, preview changes and save with permission.
|
|
3
|
+
description: Edit or create Typeroll pages with a person’s usual AI assistant. Confirm one site, find the right page, handle everyday requests (page links, images, menus, site-wide changes, settings, analytics, structured data), preview changes and save with permission.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Work on one Typeroll site
|
|
@@ -33,6 +33,93 @@ description: Edit or create Typeroll pages with a person’s usual AI assistant.
|
|
|
33
33
|
draft durable; saving and deploying are different actions. Publish only
|
|
34
34
|
when authorized and readiness passes. Report the actual result and URL.
|
|
35
35
|
|
|
36
|
+
## Everyday requests
|
|
37
|
+
|
|
38
|
+
Read the page or global block first, change only what was asked, and keep
|
|
39
|
+
everything else. When several pages fit the description, ask before editing.
|
|
40
|
+
|
|
41
|
+
**Addresses and preview links.** `list_pages` and `read_page` return each
|
|
42
|
+
page's `url` (its path). The public address is `get_site` → `urls.production`
|
|
43
|
+
plus that path, and it serves the page only after it is published and
|
|
44
|
+
deployed. Never invent a domain; when there is none, say why and offer a
|
|
45
|
+
preview instead: `get_preview_link page_id=… include_working_copy:true` shows
|
|
46
|
+
drafts and unpublished pages. Mint one link and reuse it while iterating.
|
|
47
|
+
|
|
48
|
+
**Images the user provides.** Upload first: a URL with `upload_media_from_url`,
|
|
49
|
+
a local file with `create_upload_url` (see `tr-images`); `list_media` finds
|
|
50
|
+
existing ones. Use the returned CDN URL exactly. With a named target, place it
|
|
51
|
+
there (`add_block` with `core/image` on a block page; an `<img>` with `alt` in
|
|
52
|
+
`html_content` on an HTML page). Without one, ask what the image is for. Ask
|
|
53
|
+
for alt text when none was given. Afterwards say in one sentence where it
|
|
54
|
+
landed; to answer "has it been added?", read the page again rather than
|
|
55
|
+
trusting the conversation.
|
|
56
|
+
|
|
57
|
+
**Small edits and new pages.** Block pages: `get_page_blocks`, then
|
|
58
|
+
`update_block` on the one block. HTML pages: `read_page`, then `update_page`
|
|
59
|
+
with the complete `html_content`, never a fragment. New pages: `create_page`
|
|
60
|
+
(draft by default) with a realistic start: heading, introduction and one
|
|
61
|
+
section. A Page template is `update_page` `template` (an allowed ID, or null
|
|
62
|
+
for the content type default). `discard_working_copy` throws an unsaved draft
|
|
63
|
+
away. Publishing means saving the draft, `update_page` `status: "published"`
|
|
64
|
+
(applies immediately) and, when the user wants it live now, `trigger_deploy`.
|
|
65
|
+
|
|
66
|
+
**Site-wide changes are composition, not a series of page edits.** "Change
|
|
67
|
+
every hero", "a CTA on every product page", "our phone number in every
|
|
68
|
+
footer":
|
|
69
|
+
|
|
70
|
+
1. `list_partials` shows what is already shared: header, footer and free
|
|
71
|
+
global blocks.
|
|
72
|
+
2. If a fitting global block exists, edit it once. Run
|
|
73
|
+
`find_pages_using_block` first and tell the user the blast radius ("this
|
|
74
|
+
block is on 8 pages and one template — go ahead?"); a header or footer
|
|
75
|
+
among the results means every page.
|
|
76
|
+
3. If nothing fits, propose a global block: `make_block_global` turns an
|
|
77
|
+
existing block into one, or `create_free_block` with a kebab-case ID. Place
|
|
78
|
+
it with `core/global_block` on block pages or `<x-include name="…" />` on
|
|
79
|
+
HTML pages. Layout that every Page of a type shares belongs in its Page
|
|
80
|
+
template (`tr-page-template`).
|
|
81
|
+
4. Edit pages one by one only for content that really differs per page, such
|
|
82
|
+
as each page's SEO description. Confirm the page list first.
|
|
83
|
+
|
|
84
|
+
"All 23 pages" or "every page" is a strong hint for a global block. Say what
|
|
85
|
+
you will do and wait for confirmation before editing many pages.
|
|
86
|
+
|
|
87
|
+
**Extract while you sweep.** Even when the user asked for per-page edits,
|
|
88
|
+
pause when the same content is about to land on a third page and offer to
|
|
89
|
+
make it a global block instead ("I'm adding the same CTA to several pages —
|
|
90
|
+
make it a reusable `newsletter-cta` block so future edits happen once?").
|
|
91
|
+
Check `list_partials` and `find_pages_using_block` first so you extend an
|
|
92
|
+
existing block rather than create a duplicate.
|
|
93
|
+
|
|
94
|
+
**Menu and footer.** `read_partial partial_id="header"`. In a block header,
|
|
95
|
+
change the `core/navigation` block's `links` with `update_block`; in an HTML
|
|
96
|
+
header, add the `<a href="/pricing">Pricing</a>` to the nav and write back the
|
|
97
|
+
complete HTML with `update_partial`. The footer works the same way. Building
|
|
98
|
+
either from scratch: `tr-header-footer`.
|
|
99
|
+
|
|
100
|
+
**Settings, not page edits.** Brand colors, fonts, logo, icons and contact
|
|
101
|
+
details are `update_site_settings` (admin), for example `colors.primary`. A new
|
|
102
|
+
logo must be uploaded first. Analytics, advertising pixels and chat widgets
|
|
103
|
+
are set by ID in the Integrations app (`read_app app_id="integrations"`, then
|
|
104
|
+
`update_app`), not pasted as scripts; turn on the cookie banner
|
|
105
|
+
(`update_site_settings` `cookie_consent: { enabled: true }`) so they wait for
|
|
106
|
+
consent. Only a tag the Integrations catalog lacks goes in
|
|
107
|
+
`cookie_consent.scripts_optional`. Both reach the site with the next deploy.
|
|
108
|
+
|
|
109
|
+
**Redirects, forms, listings.** A renamed URL gets `create_redirect` (301
|
|
110
|
+
unless the user says otherwise); changing a page's `path` creates one on save.
|
|
111
|
+
Place forms with a `core/form` block or `<x-form id="…" />`, never a
|
|
112
|
+
hand-written `<form>` (`tr-forms`). Lists of articles are a `core/page_list`
|
|
113
|
+
block, never hand-written HTML (`tr-blog`).
|
|
114
|
+
|
|
115
|
+
**Structured data.** For an FAQ, recipe or event page use `add_faq_schema`,
|
|
116
|
+
`add_recipe_schema` or `add_event_schema` instead of hand-writing `json_ld`;
|
|
117
|
+
see `tr-seo`.
|
|
118
|
+
|
|
119
|
+
**When something is out of reach.** If a tool refuses, tell the user what they
|
|
120
|
+
can do about it, not only the error. When no tool covers a request (billing,
|
|
121
|
+
for example), say so plainly and point to the relevant portal page.
|
|
122
|
+
|
|
36
123
|
## Compact discovery
|
|
37
124
|
|
|
38
125
|
Find tools with `search_tools`, inspect one schema with `describe_tool`, then
|
package/skills/tr-new-site.md
CHANGED
|
@@ -34,7 +34,13 @@ read_app_documentation when needed; do not bundle private app guides locally.
|
|
|
34
34
|
No Site yet? Use hosted MCP with an Organization key and create_site, or create it
|
|
35
35
|
in the portal. To start from an AI site plan instead of a blank Site, use
|
|
36
36
|
create_site_and_plan, poll get_workflow and approve its structure review only
|
|
37
|
-
with the user's consent.
|
|
37
|
+
with the user's consent. That AI plan needs the Organization's own portal AI
|
|
38
|
+
key for new Sites; without it the call answers `403 ai_not_enabled` and the
|
|
39
|
+
`site_planning` workflow fails with `failure_code: "ai_not_enabled"`. Then plan
|
|
40
|
+
it yourself: `create_site`, propose the site name, tagline and page list (title,
|
|
41
|
+
slug, purpose, target keywords) from the business description, get the
|
|
42
|
+
user's approval, and build with this recipe. Tell the user you planned it
|
|
43
|
+
yourself; don't retry the workflow. Read back its ID before editing. Use one explicit Site/Version for
|
|
38
44
|
related operations. Larger changes to an existing site should use a branch.
|
|
39
45
|
|
|
40
46
|
Before importing content or a media library, require get_import_readiness to pass.
|