@typeroll/mcp-server 0.45.69 → 0.45.73

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.
@@ -1,5 +1,4 @@
1
1
  import { z } from 'zod';
2
- import { zodToJsonSchema } from 'zod-to-json-schema';
3
2
  import { fail, ok } from './tools/helpers.js';
4
3
  export const COMPACT_INSTRUCTIONS = `Typeroll CMS. Use search_tools to find a task's tools, describe_tool to read one exact schema, then call_read_tool, call_write_tool or call_admin_tool. Discover before you write: identify Site/Version, capabilities and enabled app documentation. Read only the relevant skill or guide section; do not load the full manual by default. Larger changes: create_branch and keep the same version. Working copy, saved content and live output differ. Publishing and other external effects require the user's authorization; tool access alone is not approval.`;
5
4
  /** Stable discovery surface: no dynamic registration or client list-change support needed. */
@@ -18,12 +17,12 @@ export function compactTools(catalog) {
18
17
  }),
19
18
  local('describe_tool', 'Read one tool’s complete description, effect and JSON input schema before calling it.', { name: z.string().max(100) }, async (args) => {
20
19
  const tool = byName.get(String(args.name));
21
- return tool ? ok({ name: tool.name, effect: tool.effect, description: tool.description, inputSchema: zodToJsonSchema(z.object(tool.schema), { $refStrategy: 'none' }) }) : fail(new Error('Unknown tool. Use search_tools.'));
20
+ return tool ? ok({ name: tool.name, effect: tool.effect, description: tool.description, inputSchema: z.toJSONSchema(z.object(tool.schema).meta({ additionalProperties: false }), { target: 'draft-7', io: 'input' }) }) : fail(new Error('Unknown tool. Use search_tools.'));
22
21
  }),
23
22
  ...['read', 'write', 'admin'].map(effect => ({
24
23
  name: `call_${effect}_tool`, effect,
25
24
  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 email 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
- schema: { name: z.string().max(100), arguments: z.record(z.unknown()).default({}) },
25
+ schema: { name: z.string().max(100), arguments: z.record(z.string(), z.unknown()).default({}) },
27
26
  invoke: async (args) => {
28
27
  const tool = byName.get(String(args.name));
29
28
  if (!tool || tool.effect !== effect)
package/dist/server.js CHANGED
@@ -324,7 +324,7 @@ export function buildServer(options) {
324
324
  }
325
325
  for (const tool of options.toolMode === 'compact' ? compactTools(catalog) : catalog) {
326
326
  register(tool.name, {
327
- description: tool.description, inputSchema: tool.schema,
327
+ description: tool.description, inputSchema: z.object(tool.schema).meta({ additionalProperties: false }),
328
328
  annotations: { readOnlyHint: tool.effect === 'read', destructiveHint: tool.effect !== 'read', openWorldHint: true },
329
329
  }, tool.invoke);
330
330
  }
@@ -12,7 +12,6 @@
12
12
  // of the same @type is replaced (re-running a tool updates it), other nodes are
13
13
  // kept in an @graph, so an FAQ added later does not erase a LocalBusiness.
14
14
  import { z } from 'zod';
15
- import { zodToJsonSchema } from 'zod-to-json-schema';
16
15
  /** Invalid input or an unusable existing json_ld; `issues` name the field. */
17
16
  export class StructuredDataError extends Error {
18
17
  issues;
@@ -170,7 +169,7 @@ export const STRUCTURED_DATA = {
170
169
  * outside MCP (the portal chat passes it to the model as input_schema).
171
170
  */
172
171
  export function structuredDataInputSchema(kind) {
173
- const schema = zodToJsonSchema(z.object({ page_id: z.string().min(1), ...STRUCTURED_DATA[kind].shape }), { $refStrategy: 'none' });
172
+ const schema = z.toJSONSchema(z.object({ page_id: z.string().min(1), ...STRUCTURED_DATA[kind].shape }).meta({ additionalProperties: false }), { target: 'draft-7', io: 'input' });
174
173
  const { $schema: _schema, ...rest } = schema;
175
174
  return rest;
176
175
  }
@@ -32,7 +32,7 @@ export const appTools = [
32
32
  inputSchema: {
33
33
  app_id: z.string().min(1).describe('Registry id returned by list_apps.'),
34
34
  enabled: z.boolean(),
35
- config: z.record(z.unknown()).optional().describe('Schema-driven app config. Omitted fields preserve their existing values.'),
35
+ config: z.record(z.string(), z.unknown()).optional().describe('Schema-driven app config. Omitted fields preserve their existing values.'),
36
36
  },
37
37
  handler: withErrorBoundary(async (args, { client, siteId }) => {
38
38
  return ok(await client.put(siteId, `apps/${encodeURIComponent(args.app_id)}`, {
@@ -55,7 +55,7 @@ const definitionShape = {
55
55
  item_compatible: z.boolean().optional().describe('Mark true if this block is designed to render as a repeater item (no outer padding).'),
56
56
  expand_to: z.object({
57
57
  target: z.string(),
58
- defaults: z.record(z.unknown()),
58
+ defaults: z.record(z.string(), z.unknown()),
59
59
  }).optional().describe('Alias mechanism — render as another block type with these defaults merged under authored data.'),
60
60
  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.'),
61
61
  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.'),
@@ -176,7 +176,7 @@ export const blockTypeTools = [
176
176
  name: 'validate_block_type',
177
177
  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}`,
178
178
  inputSchema: {
179
- definition: z.record(z.unknown()).describe('The block type definition (as for create_block_type), or a patch when type_id is set.'),
179
+ definition: z.record(z.string(), z.unknown()).describe('The block type definition (as for create_block_type), or a patch when type_id is set.'),
180
180
  type_id: z.string().optional().describe('Validate `definition` as a patch to this stored block type.'),
181
181
  version: versionParam,
182
182
  },
@@ -194,9 +194,9 @@ export const blockTypeTools = [
194
194
  name: 'preview_block_type',
195
195
  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.',
196
196
  inputSchema: {
197
- 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.'),
197
+ definition: z.record(z.string(), 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.'),
198
198
  type_id: z.string().optional().describe('Preview `definition` as a patch to this stored block type.'),
199
- data: z.record(z.unknown()).optional().describe('Field values for the previewed block. Omit for sample data.'),
199
+ data: z.record(z.string(), z.unknown()).optional().describe('Field values for the previewed block. Omit for sample data.'),
200
200
  render_version: z.number().int().optional().describe('Render with this platform render version instead of the Site\'s own.'),
201
201
  version: versionParam,
202
202
  },
@@ -237,7 +237,7 @@ export const blockTypeTools = [
237
237
  inputSchema: {
238
238
  type_id: z.string().describe('Block type id (a site type). Cannot be a core/* block.'),
239
239
  ...definitionShape,
240
- 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.'),
240
+ renames: z.record(z.string(), z.string()).optional().describe('Old field path → new field name, e.g. { "title": "heading", "items.label": "text" }. Send together with the new schema.'),
241
241
  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.'),
242
242
  version: versionParam,
243
243
  },
@@ -3,18 +3,18 @@ import { ok, withErrorBoundary, versionParam, writeAuthority } from './helpers.j
3
3
  const schema = z.array(z.object({ name: z.string(), label: z.string(), type: z.enum(['text', 'textarea', 'richtext', 'image', 'file', 'color', 'select', 'multiselect', 'boolean', 'number', 'url', 'email', 'date', 'datetime', 'list', 'list_simple', 'array', 'object', 'page_ref', 'page_ref_list']) }).passthrough());
4
4
  const fields = {
5
5
  label_singular: z.string().optional(), label_plural: z.string().optional(), fields: schema.optional(),
6
- page_field_rules: z.record(z.object({ label: z.string().optional(), writable_by: z.array(z.enum(['portal', 'owner', 'agent', 'app', 'import'])) })).optional(),
6
+ page_field_rules: z.record(z.string(), z.object({ label: z.string().optional(), writable_by: z.array(z.enum(['portal', 'owner', 'agent', 'app', 'import'])) })).optional(),
7
7
  allowed_templates: z.array(z.string()).nullable().optional().describe('Allowed Page template IDs, including the default. Null allows all compatible templates; an empty list permits no template.'),
8
8
  route_template: z.string().optional(), template: z.string().optional(), schema_type: z.string().optional(),
9
9
  schema_field_mode: z.enum(['all', 'mapped']).optional().describe('Use mapped to emit only explicitly mapped fields. Resolve typed references in Page json_ld for complex graphs.'),
10
- schema_field_map: z.record(z.string()).optional(),
10
+ schema_field_map: z.record(z.string(), z.string()).optional(),
11
11
  icon: z.string().optional(), sort_field: z.string().optional(), sort_dir: z.enum(['asc', 'desc']).optional(),
12
- facets: z.array(z.record(z.unknown())).optional(), facet_combinations: z.array(z.tuple([z.string(), z.string()])).optional().describe('Pairs of configured facet field names, not values. Use editable Pages for a curated subset of combinations.'),
12
+ facets: z.array(z.record(z.string(), z.unknown())).optional(), facet_combinations: z.array(z.tuple([z.string(), z.string()])).optional().describe('Pairs of configured facet field names, not values. Use editable Pages for a curated subset of combinations.'),
13
13
  };
14
14
  export const contentTypeTools = [
15
15
  { name: 'page_completeness', description: 'Report missing, unverified and stale fields for Pages of one content type, with per-field counts and worst-first Page IDs. Read-only; agent-writable fields only by default.', inputSchema: { content_type: z.string(), limit: z.number().int().min(1).max(200).optional(), stale_after_days: z.number().min(0).optional(), agent_writable_only: z.boolean().optional(), version: versionParam },
16
16
  handler: withErrorBoundary(async (args, { client, siteId }) => { const { content_type, ...query } = args; return ok(await client.get(siteId, `content-types/${encodeURIComponent(content_type)}/completeness`, query)); }) },
17
- { name: 'change_page_content_type', description: 'Change a saved Page to another content type. Preserves its identity, block body, status and existing URL. Save or discard its working copy first. If supplied, fields replaces the complete custom fields object; omitted fields are kept and must be valid for the new type. Snapshots a revision before changing.', inputSchema: { page_id: z.string(), content_type: z.string(), fields: z.record(z.unknown()).optional(), ...writeAuthority, version: versionParam },
17
+ { name: 'change_page_content_type', description: 'Change a saved Page to another content type. Preserves its identity, block body, status and existing URL. Save or discard its working copy first. If supplied, fields replaces the complete custom fields object; omitted fields are kept and must be valid for the new type. Snapshots a revision before changing.', inputSchema: { page_id: z.string(), content_type: z.string(), fields: z.record(z.string(), z.unknown()).optional(), ...writeAuthority, version: versionParam },
18
18
  handler: withErrorBoundary(async (args, { client, siteId }) => { const { page_id, version, ...body } = args; return ok(await client.post(siteId, `pages/${encodeURIComponent(page_id)}/content-type`, body, { version })); }) },
19
19
  { name: 'list_content_types', description: 'List the field schemas, URL patterns, sorting, allowed templates and default templates for Pages. All content is a Page; use list_pages with content_type to filter.', inputSchema: { version: versionParam },
20
20
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.get(siteId, 'content-types', { version: args.version }))) },
@@ -17,7 +17,7 @@ export const emailTools = [
17
17
  type: z.string().min(1).describe('Provider type from get_email_settings providers, e.g. "postmark" or "smtp".'),
18
18
  from: z.string().min(1).describe('Sender, e.g. "Acme <hello@acme.com>". Must be a sender the provider accepts.'),
19
19
  reply_to: z.string().optional().describe('Optional default Reply-To address.'),
20
- config: z.record(z.unknown()).optional().describe('Provider fields keyed by the field schema.'),
20
+ config: z.record(z.string(), z.unknown()).optional().describe('Provider fields keyed by the field schema.'),
21
21
  },
22
22
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.put(siteId, 'integrations/email', {
23
23
  type: args.type,
@@ -7,7 +7,7 @@ export const extensionTools = [
7
7
  inputSchema: {
8
8
  installation_id: z.string().min(1), page_id: z.string().min(1),
9
9
  path: z.string().describe('Relative operation below the approved native API base, from the installed app guide.'),
10
- method: z.enum(['GET', 'POST']), query: z.record(z.string()).optional(), body: z.record(z.unknown()).optional(),
10
+ method: z.enum(['GET', 'POST']), query: z.record(z.string(), z.string()).optional(), body: z.record(z.string(), z.unknown()).optional(),
11
11
  },
12
12
  handler: withErrorBoundary(async (args, { client, siteId }) => {
13
13
  const { installation_id, ...operation } = args;
@@ -39,7 +39,7 @@ export const extensionTools = [
39
39
  inputSchema: {
40
40
  installation_id: z.string().min(1),
41
41
  version: z.string().min(1).describe('Pending published release to activate.'),
42
- config: z.record(z.unknown()).optional(),
42
+ config: z.record(z.string(), z.unknown()).optional(),
43
43
  granted_scopes: z.array(z.string()).optional(),
44
44
  },
45
45
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -53,7 +53,7 @@ export const extensionTools = [
53
53
  inputSchema: {
54
54
  version: versionParam,
55
55
  installation_id: z.string().min(1).describe('Installation id returned by list_extension_installations.'),
56
- config: z.record(z.unknown()).describe('Config keys and values declared by the installation manifest config schema.'),
56
+ config: z.record(z.string(), z.unknown()).describe('Config keys and values declared by the installation manifest config schema.'),
57
57
  deploy: z.boolean().optional().describe('Queue a deploy of the selected site version after saving. Defaults to true.'),
58
58
  },
59
59
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -76,7 +76,7 @@ export const extensionTools = [
76
76
  extension_id: z.string().min(1),
77
77
  version: z.string().min(1).describe('Release version to install.'),
78
78
  granted_scopes: z.array(z.string()).optional().describe('Approved scopes; defaults to none.'),
79
- config: z.record(z.unknown()).optional(),
79
+ config: z.record(z.string(), z.unknown()).optional(),
80
80
  developer_org_id: z.string().optional().describe('Organization that publishes the Extension. Defaults to the key\'s own organization.'),
81
81
  },
82
82
  handler: withErrorBoundary(async (args, { client, siteId }) => {
@@ -168,7 +168,7 @@ export const developerExtensionTools = [
168
168
  description: 'Save (create or replace) a draft release from a full Extension manifest. Published releases are immutable; bump manifest.version instead. Validate locally with `typeroll extension validate` first. Requires an organization API key.',
169
169
  inputSchema: {
170
170
  extension_id: z.string().min(1),
171
- manifest: z.record(z.unknown()).describe('Complete manifest; its id must equal extension_id.'),
171
+ manifest: z.record(z.string(), z.unknown()).describe('Complete manifest; its id must equal extension_id.'),
172
172
  },
173
173
  handler: withErrorBoundary(async (args, { client }) => ok(await client.developer('POST', `${ext(args.extension_id)}/versions`, { manifest: args.manifest }))),
174
174
  },
@@ -39,7 +39,7 @@ const showProgressSchema = z.union([z.boolean(), z.enum(['text', 'bar'])]).optio
39
39
  .describe('Multi-step forms: true or "text" shows "Step X of Y", "bar" a slim progress bar (with the same text for screen readers). Off by default; false turns it off.');
40
40
  const targetSchema = z.object({
41
41
  context_params: z.array(z.string()).max(8).optional().describe('Explicit URL query keys forwarded to the app hydration endpoint; reserved security keys are rejected.'),
42
- installation_id: z.string(), path: z.string(), hydrate: z.boolean().optional(), session_param: z.string().optional(),
42
+ installation_id: z.string(), path: z.string(), hydrate: z.boolean().optional(), session_param: z.string().optional(), session_exchange: z.literal('confirm_post').optional().describe('Wait for a click and POST the one-time token; prevents GET link scanning from consuming it.'),
43
43
  }).optional().describe('Admin-only: bind to a declared POST route of an enabled app installation. Read its authenticated guide first.');
44
44
  // Actions run after a successful submission: email notifications, signed
45
45
  // webhooks and app-provided action types. Same validation as the portal's
@@ -11,7 +11,7 @@ export const ownerReviewTools = [
11
11
  {
12
12
  name: 'override_owner_answers',
13
13
  description: 'Explicit administrator correction of protected answers. Supply the current revision from read_owner_answers, changed fields, and the factual reason. Does not publish. Never use this tool to bypass an import or scraping conflict without explicit authorization.',
14
- inputSchema: { page_id: z.string(), base_revision: z.string(), fields: z.record(z.unknown()), reason: z.string().min(1), version: versionParam },
14
+ inputSchema: { page_id: z.string(), base_revision: z.string(), fields: z.record(z.string(), z.unknown()), reason: z.string().min(1), version: versionParam },
15
15
  handler: withErrorBoundary(async ({ version, ...input }, { client, siteId }) => ok(await client.post(siteId, 'owner-review', { ...input, action: 'override' }, versionQuery(version)))),
16
16
  },
17
17
  {
@@ -35,7 +35,7 @@ export const ownerReviewTools = [
35
35
  {
36
36
  name: 'decide_owner_proposal',
37
37
  description: 'Explicit site-administrator editorial decision: approve, reject, or approve with adjustments. Requires current content/schema and a stable request_id for retries. First decision wins. Approval updates accepted CMS content, never deploys; reviewer adjustments retain separate provenance.',
38
- inputSchema: { proposal_id: z.string(), action: z.enum(['approve', 'reject']), request_id: z.string().min(16).max(100), adjustments: z.record(z.unknown()).optional(), version: versionParam },
38
+ inputSchema: { proposal_id: z.string(), action: z.enum(['approve', 'reject']), request_id: z.string().min(16).max(100), adjustments: z.record(z.string(), z.unknown()).optional(), version: versionParam },
39
39
  handler: withErrorBoundary(async ({ version, ...input }, { client, siteId }) => ok(await client.post(siteId, 'owner-review', input, versionQuery(version)))),
40
40
  },
41
41
  {
@@ -8,7 +8,7 @@ import { ok, withErrorBoundary, versionParam } from './helpers.js';
8
8
  const blockSchema = z.object({
9
9
  id: z.string().optional().describe('Stable id. Omit to let the server generate one.'),
10
10
  type: z.string().describe('Block type id, e.g. "core/heading", "core/section", or a custom "user/banner".'),
11
- data: z.record(z.unknown()).optional().describe('Field values matching the block-type schema.'),
11
+ data: z.record(z.string(), z.unknown()).optional().describe('Field values matching the block-type schema.'),
12
12
  children: z.array(z.any()).optional().describe('Nested blocks for container types.'),
13
13
  slots: z.array(z.array(z.any())).optional().describe('Slot contents for slot-container types.'),
14
14
  style_overrides: z.object({
@@ -107,7 +107,7 @@ export const pageBlockTools = [
107
107
  target: targetSchema.optional(),
108
108
  page_id: z.string().optional(),
109
109
  block_id: z.string(),
110
- data: z.record(z.unknown()).optional().describe('Field values to merge into block.data. null removes the key.'),
110
+ data: z.record(z.string(), z.unknown()).optional().describe('Field values to merge into block.data. null removes the key.'),
111
111
  style_overrides: z.object({
112
112
  spacing_before: z.string().optional(),
113
113
  spacing_after: z.string().optional(),
@@ -1,7 +1,7 @@
1
1
  // Pages tools. Read + write + batch + blocks-view + preview.
2
2
  import { z } from 'zod';
3
3
  import { ok, withErrorBoundary, versionParam, writeAuthority } from './helpers.js';
4
- const answerSources = z.record(z.object({ source_url: z.string().url().optional(), import_run_id: z.string().max(200).optional() }).strict()).optional().describe('Evidence per schema leaf path. Actor and timestamp are always assigned by the server; unchanged answers are not confirmed.');
4
+ const answerSources = z.record(z.string(), z.object({ source_url: z.string().url().optional(), import_run_id: z.string().max(200).optional() }).strict()).optional().describe('Evidence per schema leaf path. Actor and timestamp are always assigned by the server; unchanged answers are not confirmed.');
5
5
  function authorityBody(args) {
6
6
  return args.override_reason ? { override_reason: args.override_reason } : {};
7
7
  }
@@ -91,7 +91,7 @@ export const pageTools = [
91
91
  inputSchema: {
92
92
  title: z.string().min(1),
93
93
  content_type: z.string().optional().describe('Content type ID; defaults to page.'),
94
- fields: z.record(z.unknown()).optional().describe('Custom field values defined by the content type.'),
94
+ fields: z.record(z.string(), z.unknown()).optional().describe('Custom field values defined by the content type.'),
95
95
  slug: z.string().optional().describe('URL slug — single path segment, no slashes (e.g. "about", "kontakt"). Empty string "" = homepage. For nested URLs set the `path` field explicitly — `slug` stays a leaf id. Already in use → 409 slug_in_use naming the existing page; omit it to derive one from the title (numbered with a slug_adjusted warning on conflict).'),
96
96
  path: z.string().optional().describe('Optional explicit URL path for nested pages (e.g. "/erbjudanden/sommar-2026"). When set, takes precedence over slug for routing. Must start with "/", lowercase a-z/0-9/-/_/ only, no "..", no "//", no trailing slash. Slug is still required as the leaf id but two pages CAN share a slug under different paths.'),
97
97
  content_mode: z.enum(['blocks', 'html']).optional().describe('Default "blocks". "blocks" stores a Block[] tree (the modern default — supports the full ~40-block library, page templates, and the responsive system). "html" stores raw markup in html_content (legacy path, still fully supported for imported content or hand-written HTML). Passing html_content without content_mode also opts into "html".'),
@@ -132,7 +132,7 @@ export const pageTools = [
132
132
  patch: z
133
133
  .object({
134
134
  title: z.string().optional(),
135
- fields: z.record(z.unknown()).optional(),
135
+ fields: z.record(z.string(), z.unknown()).optional(),
136
136
  slug: z.string().optional(),
137
137
  html_content: z.string().optional(),
138
138
  blocks: z.array(z.any()).optional().describe('Block tree, only used when content_mode="blocks".'),
@@ -208,7 +208,7 @@ export const pageTools = [
208
208
  updates: z
209
209
  .array(z.object({
210
210
  page_id: z.string(),
211
- patch: z.record(z.unknown()),
211
+ patch: z.record(z.string(), z.unknown()),
212
212
  save: z.boolean().optional(),
213
213
  answer_sources: answerSources,
214
214
  ...writeAuthority,
@@ -93,7 +93,7 @@ export const partialTools = [
93
93
  inputSchema: {
94
94
  id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/),
95
95
  name: z.string().optional(),
96
- blocks: z.array(z.record(z.unknown())).optional().describe('Block tree for a block global block.'),
96
+ blocks: z.array(z.record(z.string(), z.unknown())).optional().describe('Block tree for a block global block.'),
97
97
  html_content: z.string().optional().describe('HTML for <x-include> in HTML pages. Send blocks or html_content.'),
98
98
  status: z.enum(['draft', 'published']).optional(),
99
99
  version: versionParam,
@@ -55,7 +55,7 @@ export const reusableBlockTools = [
55
55
  description: z.string().optional().describe('When to use it.'),
56
56
  id: z.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/).optional(),
57
57
  from: z.object({ page_id: z.string(), block_id: z.string() }).optional(),
58
- blocks: z.array(z.record(z.unknown())).optional(),
58
+ blocks: z.array(z.record(z.string(), z.unknown())).optional(),
59
59
  },
60
60
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'block-templates', args))),
61
61
  },
@@ -66,7 +66,7 @@ export const reusableBlockTools = [
66
66
  template_id: z.string(),
67
67
  name: z.string().optional(),
68
68
  description: z.string().optional(),
69
- blocks: z.array(z.record(z.unknown())).optional(),
69
+ blocks: z.array(z.record(z.string(), z.unknown())).optional(),
70
70
  },
71
71
  handler: withErrorBoundary(async (args, { client, siteId }) => {
72
72
  const { template_id, ...body } = args;
@@ -76,8 +76,8 @@ export const settingsTools = [
76
76
  }).strict().nullable().optional().describe('Organization schema.org JSON-LD emitted on every page. Replaces the stored value as a whole; null (or all-empty) clears it.'),
77
77
  staging_url: z.string().nullable().optional().describe('URL of a staging environment for this site, reported as urls.staging. Stored on the Site, not per version. Trailing slashes are dropped; empty string or null clears.'),
78
78
  render_version: z.number().int().min(1).optional().describe('Platform render version. Platform output changes ship as new versions so existing sites never change look on their own. Upgrade only after previewing (get_preview_link render_version) and with the user\'s approval; the tr-upgrade-rendering skill compares every page. Lowering it returns to an earlier version.'),
79
- colors: z.record(z.string().nullable()).optional().describe('Palette: primary, secondary, accent, background, surface, text, text_light (shallow-merged). Optional on_primary, on_secondary, on_accent set the text colour on that theme colour (buttons, badges, tabs, call-to-action sections, styles using the on_* tokens) as a hex colour such as "#ffffff"; null returns to the automatic black or white (chosen by APCA from render version 8, by the WCAG 2 ratio before). A chosen on_* colour applies on every render version. The response lists `on_colors` with each one\'s APCA Lc and WCAG 2 ratio; a choice below APCA Lc 60 (button text, 16px/600) is kept and returned as a warning. Prefer the automatic choice unless the brand specifies a colour.'),
80
- fonts: z.record(z.unknown()).optional().describe('heading, body (Google Fonts family or "system"), size_base (px) and smoothing: "auto" (browser default) or "antialiased" (thinner text on macOS, for light text on dark backgrounds); null clears smoothing. Unset smoothing is auto from render version 6.'),
79
+ colors: z.record(z.string(), z.string().nullable()).optional().describe('Palette: primary, secondary, accent, background, surface, text, text_light (shallow-merged). Optional on_primary, on_secondary, on_accent set the text colour on that theme colour (buttons, badges, tabs, call-to-action sections, styles using the on_* tokens) as a hex colour such as "#ffffff"; null returns to the automatic black or white (chosen by APCA from render version 8, by the WCAG 2 ratio before). A chosen on_* colour applies on every render version. The response lists `on_colors` with each one\'s APCA Lc and WCAG 2 ratio; a choice below APCA Lc 60 (button text, 16px/600) is kept and returned as a warning. Prefer the automatic choice unless the brand specifies a colour.'),
80
+ fonts: z.record(z.string(), z.unknown()).optional().describe('heading, body (Google Fonts family or "system"), size_base (px) and smoothing: "auto" (browser default) or "antialiased" (thinner text on macOS, for light text on dark backgrounds); null clears smoothing. Unset smoothing is auto from render version 6.'),
81
81
  contact: z
82
82
  .object({
83
83
  email: z.string().optional(),
@@ -99,7 +99,7 @@ export const settingsTools = [
99
99
  })
100
100
  .passthrough()
101
101
  .optional(),
102
- social: z.record(z.string()).optional(),
102
+ social: z.record(z.string(), z.string()).optional(),
103
103
  cookie_consent: z.object({
104
104
  enabled: z.boolean().optional(),
105
105
  text: z.string().optional().describe('Localized consent copy. May include a privacy-policy link.'),
@@ -90,9 +90,9 @@ export const styleTools = [
90
90
  role: styleFields.role,
91
91
  targets: styleFields.targets,
92
92
  description: styleFields.description,
93
- base: z.record(z.unknown()).optional().describe('Property patch for base; null removes a property.'),
94
- at: z.record(z.unknown()).optional().describe('Per-breakpoint property patches; null removes a breakpoint or property.'),
95
- hover: z.record(z.unknown()).optional(),
93
+ base: z.record(z.string(), z.unknown()).optional().describe('Property patch for base; null removes a property.'),
94
+ at: z.record(z.string(), z.unknown()).optional().describe('Per-breakpoint property patches; null removes a breakpoint or property.'),
95
+ hover: z.record(z.string(), z.unknown()).optional(),
96
96
  css: z.string().optional(),
97
97
  parts: styleFields.parts,
98
98
  version: versionParam,
@@ -24,7 +24,7 @@ export const workflowTools = [
24
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,
25
25
  inputSchema: {
26
26
  type: z.enum(WORKFLOW_TYPES),
27
- config: z.record(z.unknown()).optional(),
27
+ config: z.record(z.string(), z.unknown()).optional(),
28
28
  version: versionParam,
29
29
  },
30
30
  handler: withErrorBoundary(async (args, { client, siteId }) => ok(await client.post(siteId, 'workflows', { type: args.type, config: args.config }, args.version ? { version: args.version } : undefined))),
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.69';
11
+ export const VERSION = '0.45.73';
package/dist/workspace.js CHANGED
@@ -15,7 +15,7 @@ export const workspaceSchema = z.object({
15
15
  version: z.string().min(1), tool_mode: z.enum(['compact', 'full']),
16
16
  }).strict();
17
17
  export const defaultWorkspace = { schema_version: 1, portal: 'https://app.typeroll.com', organization_id: null, site_id: null, version: 'main', tool_mode: 'compact' };
18
- const lockSchema = z.object({ schema_version: z.literal(1), generator_version: z.string(), files: z.record(z.string().regex(/^[a-f0-9]{64}$/)) }).strict();
18
+ const lockSchema = z.object({ schema_version: z.literal(1), generator_version: z.string(), files: z.record(z.string(), z.string().regex(/^[a-f0-9]{64}$/)) }).strict();
19
19
  const hash = (value) => createHash('sha256').update(value).digest('hex');
20
20
  export const ignores = ['.typeroll/', '.env', '.env.*', '!.env.example', '.mcp.json', '.cursor/mcp.json', '.vscode/mcp.json', 'qa/reports/', 'sources/private/'];
21
21
  export const agentInstructions = `# Working on this website
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeroll/mcp-server",
3
- "version": "0.45.69",
3
+ "version": "0.45.73",
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": {
@@ -44,8 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@modelcontextprotocol/sdk": "^1.31.0",
47
- "zod": "^3.23.0",
48
- "zod-to-json-schema": "3.25.2"
47
+ "zod": "^4.6.7"
49
48
  },
50
49
  "devDependencies": {
51
50
  "typescript": "^5.6.0",