@typeroll/mcp-server 0.45.109 → 0.45.111

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.
@@ -0,0 +1,200 @@
1
+ // Every MCP tool is a thin wrapper around the REST API. This module runs each
2
+ // tool against a recording HTTP client to find the REST calls behind it, so
3
+ // the published REST reference is derived from the tools themselves and cannot
4
+ // drift from them. Placeholder arguments name each input, for example
5
+ // `{page_id}`, and appear as such in the recorded paths.
6
+ import { z } from 'zod';
7
+ import { TyperollClient } from './client.js';
8
+ import { ALL_TOOLS, effectFor } from './server.js';
9
+ /** Tools that read bundled documentation and never call the API. */
10
+ export const LOCAL_TOOLS = new Set(['read_guide', 'list_skills', 'read_skill']);
11
+ const BASE = 'https://portal.invalid';
12
+ /** Valid example input for tools that validate more than placeholders satisfy. */
13
+ const EXAMPLE_ARGS = {
14
+ add_recipe_schema: { page_id: '{page_id}', name: 'Pancakes', ingredients: ['Flour'], instructions: ['Mix'] },
15
+ add_event_schema: { page_id: '{page_id}', name: 'Launch', start_date: '2026-06-12T19:00:00+02:00' },
16
+ import_gsc_performance: { property: 'https://example.com/' },
17
+ update_migration_urls: { ids: ['{id}'], patch: { excluded: true } },
18
+ revoke_preview_link: { preview_url: `${'https://portal.invalid'}/preview/{siteId}/?t={token}` },
19
+ };
20
+ const SITE = '{siteId}';
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ function placeholder(name, schema) {
23
+ const def = schema?.def ?? schema?._zod?.def;
24
+ switch (def?.type) {
25
+ case 'optional':
26
+ case 'default':
27
+ case 'nullable':
28
+ case 'readonly':
29
+ case 'catch':
30
+ case 'prefault':
31
+ case 'nonoptional':
32
+ return placeholder(name, def.innerType);
33
+ case 'pipe': return placeholder(name, def.in);
34
+ case 'lazy': return placeholder(name, def.getter());
35
+ case 'string':
36
+ if (schema.format === 'url' || /url$/i.test(name))
37
+ return `https://example.com/${name}.png`;
38
+ if (schema.format === 'email' || /email$/i.test(name))
39
+ return `${name}@example.com`;
40
+ return `{${name}}`;
41
+ case 'number':
42
+ case 'int':
43
+ case 'bigint': return 1;
44
+ case 'boolean': return true;
45
+ case 'enum': return Object.values(def.entries)[0];
46
+ case 'literal': return def.values[0];
47
+ case 'union': return placeholder(name, def.options[0]);
48
+ case 'array': return [placeholder(name.replace(/s$/, ''), def.element)];
49
+ case 'object': return Object.fromEntries(Object.entries(def.shape).map(([key, value]) => [key, placeholder(key, value)]));
50
+ case 'record': return {};
51
+ default: return `{${name}}`;
52
+ }
53
+ }
54
+ function parameters(tool) {
55
+ if (!tool.inputSchema)
56
+ return [];
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ const json = z.toJSONSchema(z.object(tool.inputSchema), { unrepresentable: 'any', io: 'input' });
59
+ const required = new Set(json.required ?? []);
60
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
61
+ const typeOf = (value) => value?.enum ? value.enum.map((item) => JSON.stringify(item)).join(' | ')
62
+ : value?.anyOf ? value.anyOf.map(typeOf).join(' | ')
63
+ : value?.type === 'array' ? `${typeOf(value.items)}[]`
64
+ : Array.isArray(value?.type) ? value.type.join(' | ') : value?.type ?? 'any';
65
+ return Object.entries(json.properties ?? {}).map(([name, value]) => ({
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ name, type: typeOf(value), required: required.has(name), description: String(value?.description ?? ''),
68
+ }));
69
+ }
70
+ async function record(tool) {
71
+ const calls = [];
72
+ const fetchImpl = (async (input, init) => {
73
+ const url = new URL(String(input instanceof Request ? input.url : input));
74
+ // Only API calls are recorded; nothing leaves the process.
75
+ if (url.origin !== BASE)
76
+ return new Response('Not available while generating the reference', { status: 503 });
77
+ let body = [];
78
+ if (typeof init?.body === 'string') {
79
+ try {
80
+ const parsed = JSON.parse(init.body);
81
+ body = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? Object.keys(parsed) : [];
82
+ }
83
+ catch {
84
+ body = [];
85
+ }
86
+ }
87
+ calls.push({
88
+ method: (init?.method ?? 'GET').toUpperCase(),
89
+ path: decodeURIComponent(url.pathname),
90
+ query: [...new Set([...url.searchParams.keys()])],
91
+ body,
92
+ });
93
+ return new Response('{}', { status: 200, headers: { 'content-type': 'application/json' } });
94
+ });
95
+ const client = new TyperollClient({ baseUrl: BASE, apiKey: 'typeroll_live_reference', fetchImpl });
96
+ const args = EXAMPLE_ARGS[tool.name] ?? Object.fromEntries(Object.entries(tool.inputSchema ?? {}).map(([name, schema]) => [name, placeholder(name, schema)]));
97
+ const realFetch = globalThis.fetch;
98
+ globalThis.fetch = fetchImpl;
99
+ let timer;
100
+ try {
101
+ // A handler that polls stops being observed after its first calls.
102
+ await Promise.race([
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ tool.handler(args, { client, siteId: SITE }),
105
+ new Promise(resolve => { timer = setTimeout(resolve, 1000); }),
106
+ ]);
107
+ }
108
+ catch {
109
+ // A handler may reject the empty recorded response after its call; the call is what matters.
110
+ }
111
+ finally {
112
+ clearTimeout(timer);
113
+ globalThis.fetch = realFetch;
114
+ }
115
+ return calls;
116
+ }
117
+ export async function restReference() {
118
+ const entries = [];
119
+ for (const tool of ALL_TOOLS) {
120
+ if (LOCAL_TOOLS.has(tool.name))
121
+ continue;
122
+ const calls = await record(tool);
123
+ entries.push({
124
+ tool: tool.name, effect: effectFor(tool.name), description: tool.description,
125
+ site: calls.some(call => call.path.startsWith(`/api/v1/sites/${SITE}/`)), parameters: parameters(tool), calls,
126
+ });
127
+ }
128
+ return entries;
129
+ }
130
+ const AREA_NAMES = {
131
+ '': 'Site', access: 'Access', organizations: 'Organizations', sites: 'Sites', publishing: 'Organization publishing',
132
+ organization: 'Organization', developer: 'Developer Extensions',
133
+ };
134
+ function area(entry) {
135
+ const path = entry.calls[0]?.path ?? '';
136
+ const rest = entry.site ? path.slice(`/api/v1/sites/${SITE}/`.length) : path.replace(/^\/api\/(v1\/)?/, '');
137
+ const segment = rest.split('/')[0] ?? '';
138
+ const generic = segment.replace(/[-_]/g, ' ').replace(/^./, letter => letter.toUpperCase());
139
+ const name = entry.site ? (segment ? generic : 'Details') : AREA_NAMES[segment] ?? generic;
140
+ return entry.site ? `Site: ${name}` : name;
141
+ }
142
+ function where(parameter, call) {
143
+ if (call.path.includes(`{${parameter.name}}`))
144
+ return 'path';
145
+ if (call.query.includes(parameter.name))
146
+ return 'query';
147
+ if (call.body.includes(parameter.name))
148
+ return 'body';
149
+ return call.method === 'GET' || call.method === 'DELETE' ? 'query' : 'body';
150
+ }
151
+ /** Markdown text outside code spans must not open HTML tags. */
152
+ const prose = (text) => text.split('`').map((part, index) => index % 2 ? part : part.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')).join('`');
153
+ const cell = (text) => prose(text).replace(/\|/g, '\\|').replace(/\s+/g, ' ').trim();
154
+ /** The published reference: one section per REST call behind each MCP tool. */
155
+ export function renderRestReference(entries) {
156
+ const groups = new Map();
157
+ for (const entry of entries) {
158
+ const key = area(entry);
159
+ groups.set(key, [...groups.get(key) ?? [], entry]);
160
+ }
161
+ const lines = [
162
+ '---',
163
+ 'title: REST API reference',
164
+ 'description: Every operation the MCP server offers, as the REST call behind it. Generated from the MCP tools, so both always match.',
165
+ '---',
166
+ '',
167
+ '<!-- Generated by `npm run gen:rest -w packages/mcp-server` from the MCP tool definitions. Do not edit by hand. -->',
168
+ '',
169
+ 'Every MCP tool is a thin wrapper around one or more of these REST calls, so',
170
+ 'anything an AI agent can do through MCP, a script can do through the API. This',
171
+ 'page is generated from the tools and checked in CI, so it always matches them.',
172
+ 'For authentication, organizations and examples, see',
173
+ '[Use the REST API](../../guides/rest-api/).',
174
+ '',
175
+ '- Base address: `https://app.typeroll.com` on Cloud, or your own portal host.',
176
+ '- `{siteId}` and other names in braces are path parameters.',
177
+ '- Send JSON bodies with `Content-Type: application/json`.',
178
+ '- **Permission** is the least role or key permission the call needs: `read`, `write` or `admin`.',
179
+ '- The MCP tool name is shown for each call, so agent instructions and scripts can use the same names.',
180
+ '',
181
+ ];
182
+ for (const name of [...groups.keys()].sort((a, b) => a.localeCompare(b))) {
183
+ lines.push(`## ${name}`, '');
184
+ for (const entry of groups.get(name)) {
185
+ lines.push(`### \`${entry.tool}\``, '');
186
+ for (const call of entry.calls) {
187
+ lines.push(`\`${call.method} ${call.path}\``, '');
188
+ }
189
+ lines.push(`Permission: \`${entry.effect}\``, '', prose(entry.description.trim()), '');
190
+ if (entry.parameters.length) {
191
+ lines.push('| Parameter | In | Type | Required | Description |', '| --- | --- | --- | --- | --- |');
192
+ for (const parameter of entry.parameters) {
193
+ lines.push(`| \`${parameter.name}\` | ${where(parameter, entry.calls[0])} | ${cell(parameter.type)} | ${parameter.required ? 'yes' : 'no'} | ${cell(parameter.description)} |`);
194
+ }
195
+ lines.push('');
196
+ }
197
+ }
198
+ }
199
+ return lines.join('\n');
200
+ }
package/dist/server.js CHANGED
@@ -45,6 +45,40 @@ import { compactTools, COMPACT_INSTRUCTIONS } from './compact-tools.js';
45
45
  /** What permission a tool needs to run. Maps onto the share model: a
46
46
  * read-shared site can `read`-list but cannot mutate. Mode `write` is the
47
47
  * default — applied to anything that isn't trivially side-effect-free. */
48
+ /** Every MCP tool, in registration order. The REST reference is generated from it. */
49
+ export const ALL_TOOLS = [
50
+ ...skillTools,
51
+ ...siteTools,
52
+ ...pageTools,
53
+ ...structuredDataTools,
54
+ ...ownerReviewTools,
55
+ ...partialTools,
56
+ ...blockTypeTools,
57
+ ...pageBlockTools,
58
+ ...workingCopyTools,
59
+ ...contentTypeTools,
60
+ ...pageTemplateTools,
61
+ ...mediaTools,
62
+ ...redirectTools,
63
+ ...styleTools,
64
+ ...reusableBlockTools,
65
+ ...migrationTools,
66
+ ...formTools,
67
+ ...emailTools,
68
+ ...settingsTools,
69
+ ...appTools,
70
+ ...extensionTools,
71
+ ...developerExtensionTools,
72
+ ...searchTools,
73
+ ...bulkTools,
74
+ ...readinessTools,
75
+ ...versionTools,
76
+ ...deployTools,
77
+ ...previewTools,
78
+ ...domainTools,
79
+ ...workflowTools,
80
+ ...accessTools,
81
+ ];
48
82
  const PERM_RANK = { read: 0, write: 1, admin: 2 };
49
83
  /**
50
84
  * Classify a tool by name into the minimum permission needed. We keep this
@@ -55,7 +89,7 @@ const PERM_RANK = { read: 0, write: 1, admin: 2 };
55
89
  * `list_*` / `read_*` / `get_*` / `preview_*` / `search_*` / `check_*` / `find_*`; everything else
56
90
  * mutates.
57
91
  */
58
- function effectFor(name) {
92
+ export function effectFor(name) {
59
93
  if (name === 'page_completeness' || name === 'resolve_page_url')
60
94
  return 'read';
61
95
  if (name === 'guide_publishing_domain_change'
@@ -107,6 +141,8 @@ function effectFor(name) {
107
141
  || name === 'prepare_organization_media_storage'
108
142
  || name === 'save_organization_media_access'
109
143
  || name === 'set_email_settings'
144
+ || name === 'set_form_spam_protection'
145
+ || name === 'delete_form_spam_protection'
110
146
  || name === 'delete_email_settings'
111
147
  || name === 'send_test_email'
112
148
  || name === 'retry_form_intake_notification'
@@ -207,39 +243,7 @@ export function buildServer(options) {
207
243
  capabilities: { tools: {} },
208
244
  instructions: (options.selection ? 'First ask which site to work on. List sites, confirm its organization/name with the user, then call select_site. Carry the returned site_context on every site call. Never search across sites or silently switch sites or versions. Select a new context to switch. Organization administration requires an explicit organization_id and the user’s permission. ' : '') + (options.toolMode === 'compact' ? COMPACT_INSTRUCTIONS : SERVER_INSTRUCTIONS),
209
245
  });
210
- const allTools = [
211
- ...skillTools,
212
- ...siteTools,
213
- ...pageTools,
214
- ...structuredDataTools,
215
- ...ownerReviewTools,
216
- ...partialTools,
217
- ...blockTypeTools,
218
- ...pageBlockTools,
219
- ...workingCopyTools,
220
- ...contentTypeTools,
221
- ...pageTemplateTools,
222
- ...mediaTools,
223
- ...redirectTools,
224
- ...styleTools,
225
- ...reusableBlockTools,
226
- ...migrationTools,
227
- ...formTools,
228
- ...emailTools,
229
- ...settingsTools,
230
- ...appTools,
231
- ...extensionTools,
232
- ...developerExtensionTools,
233
- ...searchTools,
234
- ...bulkTools,
235
- ...readinessTools,
236
- ...versionTools,
237
- ...deployTools,
238
- ...previewTools,
239
- ...domainTools,
240
- ...workflowTools,
241
- ...accessTools,
242
- ];
246
+ const allTools = ALL_TOOLS;
243
247
  // SDK's registerTool has deeply-nested generics we can't unify across a
244
248
  // heterogeneous tool list at compile time — same shrug as stdio's index.ts.
245
249
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -42,7 +42,7 @@ export const deployTools = [
42
42
  },
43
43
  {
44
44
  name: 'get_deploy_status',
45
- description: "Status of a single deploy job. Optional top-level build_progress reports the exact task status, attempt, step and completed/total files; not an overall percentage or ETA. Returns { job, queued_for_seconds, queue_timeout_seconds }. job.targets and job.production, when present, record where the publication was admitted to publish and whether that included a live domain. job.status is one of queued | running | succeeded | failed. On a failed build, job.error names the failed step and the cause the step printed, job.failure.code is the reported code (e.g. media_build_process_exit_1) and job.failure.diagnostic holds { cause, lines } (the last 40 lines that step printed, credentials removed). On the Cloudflare build engine job.failure.provider_log also holds { step, build_id, summary, lines } from the provider's build log (last 60 lines). Read them before retrying; retrying does not fix a cause that is in the content or the build engine. job.warnings, when present, lists non-blocking findings of a deploy that still ran, for example a media file left out of the publication because it could not be published safely (an SVG that could not be sanitized); report them to the user. Polling cadence: ~5s for queued/running. job.render_report, when present, reports actual HTML mode (full/partial), rendered, reused, total and removed route counts plus a fallback reason. From Core 0.2.17, custom blocks and consumed image metadata are tracked per route; older caches require one full build. It does not measure media or provider startup savings. job.coordinator, when present, reports the last completed CMS phase and duration_ms for that attempt; external build waiting is excluded. Keep observing the same job across checkpoints; do not trigger duplicate deploys. A job stuck in 'queued' for longer than queue_timeout_seconds (default 300s) auto-flips to failed with phase='queue_timeout' — so a single poll past that timestamp returns the terminal state and you can stop polling.",
45
+ description: "Status of a single deploy job. Optional top-level build_progress reports the exact task status, attempt, step and completed/total files; not an overall percentage or ETA. Media preparation runs in windows of about 12 minutes that each save their position, so attempt rises in a healthy build: media_continuations counts those planned windows, retries counts failed runs started again, and deadline is when the build stops for good. Do not cancel while completed still rises. Returns { job, queued_for_seconds, queue_timeout_seconds }. job.targets and job.production, when present, record where the publication was admitted to publish and whether that included a live domain. job.status is one of queued | running | succeeded | failed. On a failed build, job.error names the failed step and the cause the step printed, job.failure.code is the reported code (e.g. media_build_process_exit_1) and job.failure.diagnostic holds { cause, lines } (the last 40 lines that step printed, credentials removed). On the Cloudflare build engine job.failure.provider_log also holds { step, build_id, summary, lines } from the provider's build log (last 60 lines). Read them before retrying; retrying does not fix a cause that is in the content or the build engine. job.warnings, when present, lists non-blocking findings of a deploy that still ran, for example a media file left out of the publication because it could not be published safely (an SVG that could not be sanitized); report them to the user. Polling cadence: ~5s for queued/running. job.render_report, when present, reports actual HTML mode (full/partial), rendered, reused, total and removed route counts plus a fallback reason. From Core 0.2.17, custom blocks and consumed image metadata are tracked per route; older caches require one full build. It does not measure media or provider startup savings. job.coordinator, when present, reports the last completed CMS phase and duration_ms for that attempt; external build waiting is excluded. Keep observing the same job across checkpoints; do not trigger duplicate deploys. A job stuck in 'queued' for longer than queue_timeout_seconds (default 300s) auto-flips to failed with phase='queue_timeout' — so a single poll past that timestamp returns the terminal state and you can stop polling.",
46
46
  inputSchema: { job_id: z.string(), include_provider: z.boolean().optional().describe('Also read the exact Git commit and Cloudflare build stages for a customer Git publication. Does not return build secrets.') },
47
47
  handler: withErrorBoundary(async (args, { client, siteId }) => {
48
48
  const res = await client.get(siteId, `deploys/${encodeURIComponent(args.job_id)}`);
@@ -234,7 +234,7 @@ export const domainTools = [
234
234
  name: 'set_publishing_domains',
235
235
  description: 'Save future website, redirect and media hosts before preparing a deployment. Normal trigger_deploy continues updating the current or temporary address, even while future hosts await validation. Shortly before changing DNS, explicitly call prepare_publishing_domain_change to publish the last published snapshot again with these hosts. That explicit publication replaces main before DNS changes. Later normal publications retain its new origins and refresh the DNS approval receipt; no second build is needed to switch DNS. Requires the current revision from read_publishing_domains. This does not publish content or change traffic DNS. Choose external DNS when your agent manages Cloudflare independently. redirect_hosts (for example www.example.com when example.com is the website host) answer 301 to the website host, keeping path and query, through a Single Redirect rule in the host\'s Cloudflare zone; they are switched and verified with the website. The response includes host_checks: resolve its blockers before preparing.',
236
236
  inputSchema: {
237
- revision: z.string(), website_host: z.string().nullable(), media_host: z.string().nullable(),
237
+ revision: z.string(), website_host: z.string().nullable(), media_host: z.string().nullable().describe('Optional site media host, such as media.<site domain>, so image addresses do not show the organization media domain. Its domain must be in the organization\'s connected Cloudflare account. Null uses the organization\'s shared media host.'),
238
238
  confirm_rebuild: z.boolean().optional().describe('Legacy input; Core 0.2.116+ does not require confirmation to save inert future addresses.'),
239
239
  media_path_prefix: z.string().optional().describe('Empty on a separate media host; /media when using the website host.'),
240
240
  redirect_hosts: z.array(z.string()).max(4).optional().describe('Hostnames that redirect permanently to website_host, such as www.example.com. Omit or send [] for none.'),
@@ -63,6 +63,27 @@ export const formTools = [
63
63
  return ok(res);
64
64
  }),
65
65
  },
66
+ {
67
+ name: 'get_form_spam_protection',
68
+ description: "Read the site's Cloudflare Turnstile setup for forms: provider, public site_key, secret_key masked as { set: true }, and enforced_forms (forms whose published pages have sent a verified Turnstile answer and now require one). provider is null when Turnstile is off. Admin permission required.",
69
+ handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'integrations/form-spam-protection'))),
70
+ },
71
+ {
72
+ name: 'set_form_spam_protection',
73
+ description: "Turn on Cloudflare Turnstile for every form on the site, against bots that fill in forms in a real browser. First create a Turnstile widget in the site owner's Cloudflare dashboard (Turnstile → Add widget, mode Managed, with the site's website hostnames), then pass its site key and secret key. The secret is checked with Cloudflare, encrypted and never returned; omit secret_key to keep the stored one for the same site_key. Publish the site afterwards: forms render the widget only after publishing. Each form keeps accepting submissions without Turnstile until its republished pages send the first verified answer, then requires it. Forms that post to an app or Extension are not covered. Admin permission required.",
74
+ inputSchema: {
75
+ site_key: z.string().min(10).describe('The Turnstile widget\'s site key (public).'),
76
+ secret_key: z.string().min(10).optional().describe('The Turnstile widget\'s secret key. Required for a new site_key.'),
77
+ },
78
+ handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.put(siteId, 'integrations/form-spam-protection', {
79
+ provider: 'turnstile', site_key: args.site_key, ...(args.secret_key !== undefined ? { secret_key: args.secret_key } : {}),
80
+ }))),
81
+ },
82
+ {
83
+ name: 'delete_form_spam_protection',
84
+ description: "Turn off Turnstile for the site's forms and delete the stored secret. Submissions are accepted without Turnstile at once; republish to remove the widget from pages. Admin permission required.",
85
+ handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.del(siteId, 'integrations/form-spam-protection'))),
86
+ },
66
87
  {
67
88
  name: 'get_form_capabilities',
68
89
  description: 'List the action types a form can run after a submission (core "email" and "webhook" plus types provided by installed apps) and the prefill sources, each with its config field schema — the same choices as the portal\'s Forms editor. Read this before writing `actions` with create_form/update_form. Admin permission required.',
@@ -44,7 +44,7 @@ export const mediaTools = [
44
44
  },
45
45
  {
46
46
  name: 'get_media_preparation',
47
- description: 'Read background image preparation progress, including the build_job_id of a running preparation job on the organization build engine. unreadable_media lists library images whose stored file does not decode; preparation skips them, so upload a working copy. Originals and prepared draft variants remain private. Requires site access.',
47
+ description: 'Read background image preparation progress, including the build_job_id of a running preparation job on the organization build engine. unreadable_media lists library images whose stored file does not decode; preparation skips them, so upload a working copy. A round stopped by a passing condition (engine update, runner not started, step time limit, interrupted transfer) never ends the job: it starts again after 5 minutes, doubling up to an hour, shown as retry { attempt, next_at } with error_code. When the job fails, error names the cause, with error_code and failed_build_task. Originals and prepared draft variants remain private. Requires site access.',
48
48
  inputSchema: {},
49
49
  handler: withErrorBoundary(async (_args, { client, siteId }) => ok(await client.get(siteId, 'media/preparation'))),
50
50
  },
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.45.109';
11
+ export const VERSION = '0.45.111';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
3
  "mcpName": "io.github.Typeroll/typeroll",
4
- "version": "0.45.109",
4
+ "version": "0.45.111",
5
5
  "description": "Typeroll CMS MCP server \u2013 connect AI agents to the REST API to manage content and publish static websites.",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -34,7 +34,8 @@
34
34
  "workspace-templates"
35
35
  ],
36
36
  "scripts": {
37
- "gen": "node scripts/embed-content.mjs",
37
+ "gen": "node scripts/embed-content.mjs && npm run gen:rest",
38
+ "gen:rest": "UPDATE_REST_REFERENCE=1 vitest run tests/rest-reference.test.ts",
38
39
  "prebuild": "node scripts/embed-content.mjs --clean",
39
40
  "build": "tsc -p .",
40
41
  "typecheck": "tsc --noEmit",
@@ -213,6 +213,11 @@ across pages. Other hidden fields always send their `default`.
213
213
  ## Pitfalls
214
214
 
215
215
  - Do not hand-write a form, token, honeypot, or submit script.
216
+ - Spam from bots that use a real browser gets past the token, honeypot and rate
217
+ limits. Turn on Cloudflare Turnstile with `set_form_spam_protection` (a widget
218
+ from the site owner's Cloudflare dashboard), then publish the site. A site
219
+ admin can do the same in the portal under Forms → Spam protection; never ask
220
+ them to paste a secret key into the chat if they prefer that route.
216
221
  - Do not put a raw `<script>` in page HTML; the sanitizer removes it.
217
222
  - Do not drop existing actions by accident: `actions` replaces the list.
218
223
  - Do not send every submitted field to a webhook by default; choose the