@wootsup/yt-builder-mcp 1.10.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +12 -11
  2. package/bin/yt-builder-mcp.js +1 -1
  3. package/dist/auth.d.ts +1 -1
  4. package/dist/auth.js +1 -1
  5. package/dist/catalog/tool-catalog-markdown.d.ts.map +1 -1
  6. package/dist/catalog/tool-catalog-markdown.js +13 -1
  7. package/dist/catalog/tool-catalog-markdown.js.map +1 -1
  8. package/dist/cli/doctor-command.d.ts.map +1 -1
  9. package/dist/cli/doctor-command.js +22 -2
  10. package/dist/cli/doctor-command.js.map +1 -1
  11. package/dist/client.d.ts +3 -3
  12. package/dist/client.js +1 -1
  13. package/dist/client.js.map +1 -1
  14. package/dist/clients/index.d.ts +1 -1
  15. package/dist/clients/index.js +1 -1
  16. package/dist/diagnostics/ca-reexec.js +2 -2
  17. package/dist/diagnostics/diagnose-network-error.d.ts +1 -1
  18. package/dist/diagnostics/doctor.d.ts +34 -2
  19. package/dist/diagnostics/doctor.d.ts.map +1 -1
  20. package/dist/diagnostics/doctor.js +63 -2
  21. package/dist/diagnostics/doctor.js.map +1 -1
  22. package/dist/diagnostics/startup-self-probe.js +1 -1
  23. package/dist/gateway/advanced-read-tool.d.ts.map +1 -1
  24. package/dist/gateway/advanced-read-tool.js +14 -2
  25. package/dist/gateway/advanced-read-tool.js.map +1 -1
  26. package/dist/gateway/advanced-tool/discovery.d.ts.map +1 -1
  27. package/dist/gateway/advanced-tool/discovery.js +12 -1
  28. package/dist/gateway/advanced-tool/discovery.js.map +1 -1
  29. package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
  30. package/dist/gateway/advanced-tool/domains.js +5 -0
  31. package/dist/gateway/advanced-tool/domains.js.map +1 -1
  32. package/dist/gateway/advanced-tool/execute.d.ts +7 -1
  33. package/dist/gateway/advanced-tool/execute.d.ts.map +1 -1
  34. package/dist/gateway/advanced-tool/execute.js +58 -4
  35. package/dist/gateway/advanced-tool/execute.js.map +1 -1
  36. package/dist/gateway/essentials.d.ts +4 -4
  37. package/dist/gateway/essentials.d.ts.map +1 -1
  38. package/dist/gateway/essentials.js +12 -7
  39. package/dist/gateway/essentials.js.map +1 -1
  40. package/dist/install-skill.js +1 -1
  41. package/dist/net/extra-headers.d.ts +1 -1
  42. package/dist/net/extra-headers.js +1 -1
  43. package/dist/net/internal-host.js +2 -2
  44. package/dist/net/proxy-dispatcher.js +1 -1
  45. package/dist/net/proxy-dispatcher.js.map +1 -1
  46. package/dist/net/site-basic-auth.d.ts +2 -2
  47. package/dist/net/site-basic-auth.js +2 -2
  48. package/dist/proxy/bridge.d.ts +2 -2
  49. package/dist/proxy/bridge.js +3 -3
  50. package/dist/proxy/bridge.js.map +1 -1
  51. package/dist/server.d.ts +3 -3
  52. package/dist/server.js +3 -3
  53. package/dist/setup-cli.d.ts +2 -2
  54. package/dist/setup-cli.js +2 -2
  55. package/dist/setup-cli.js.map +1 -1
  56. package/dist/setup-npx-spec.d.ts +1 -1
  57. package/dist/setup-npx-spec.js +1 -1
  58. package/dist/setup-wizard-handshake.js +1 -1
  59. package/dist/setup-wizard-types.d.ts +1 -1
  60. package/dist/setup-wizard.js +3 -3
  61. package/dist/setup-wizard.js.map +1 -1
  62. package/dist/sites/client-pool.d.ts +1 -1
  63. package/dist/sites/client-pool.js +1 -1
  64. package/dist/sites/tools/sites-list.d.ts.map +1 -1
  65. package/dist/sites/tools/sites-list.js +26 -4
  66. package/dist/sites/tools/sites-list.js.map +1 -1
  67. package/dist/sites/tools/sites-test.js +1 -1
  68. package/dist/sites/tools/sites-test.js.map +1 -1
  69. package/dist/sites/tools/use-site.js +2 -2
  70. package/dist/sites/tools/use-site.js.map +1 -1
  71. package/dist/tools/budgeted-table.d.ts +21 -0
  72. package/dist/tools/budgeted-table.d.ts.map +1 -0
  73. package/dist/tools/budgeted-table.js +137 -0
  74. package/dist/tools/budgeted-table.js.map +1 -0
  75. package/dist/tools/elements/builders.d.ts.map +1 -1
  76. package/dist/tools/elements/builders.js +102 -40
  77. package/dist/tools/elements/builders.js.map +1 -1
  78. package/dist/tools/elements/handlers-write.d.ts +21 -0
  79. package/dist/tools/elements/handlers-write.d.ts.map +1 -1
  80. package/dist/tools/elements/handlers-write.js +231 -11
  81. package/dist/tools/elements/handlers-write.js.map +1 -1
  82. package/dist/tools/elements/handlers.d.ts +20 -12
  83. package/dist/tools/elements/handlers.d.ts.map +1 -1
  84. package/dist/tools/elements/handlers.js +77 -26
  85. package/dist/tools/elements/handlers.js.map +1 -1
  86. package/dist/tools/elements/schema-validation.d.ts +147 -7
  87. package/dist/tools/elements/schema-validation.d.ts.map +1 -1
  88. package/dist/tools/elements/schema-validation.js +508 -28
  89. package/dist/tools/elements/schema-validation.js.map +1 -1
  90. package/dist/tools/format/elements-format.d.ts.map +1 -1
  91. package/dist/tools/format/elements-format.js +56 -12
  92. package/dist/tools/format/elements-format.js.map +1 -1
  93. package/dist/tools/format/health-format.d.ts +16 -0
  94. package/dist/tools/format/health-format.d.ts.map +1 -1
  95. package/dist/tools/format/health-format.js +9 -0
  96. package/dist/tools/format/health-format.js.map +1 -1
  97. package/dist/tools/format/inspection-format.d.ts +7 -0
  98. package/dist/tools/format/inspection-format.d.ts.map +1 -1
  99. package/dist/tools/format/inspection-format.js +18 -0
  100. package/dist/tools/format/inspection-format.js.map +1 -1
  101. package/dist/tools/format/pages-format.d.ts +1 -1
  102. package/dist/tools/format/pages-format.d.ts.map +1 -1
  103. package/dist/tools/format/pages-format.js +7 -0
  104. package/dist/tools/format/pages-format.js.map +1 -1
  105. package/dist/tools/format/sources-format.d.ts +12 -5
  106. package/dist/tools/format/sources-format.d.ts.map +1 -1
  107. package/dist/tools/format/sources-format.js +31 -6
  108. package/dist/tools/format/sources-format.js.map +1 -1
  109. package/dist/tools/health.d.ts.map +1 -1
  110. package/dist/tools/health.js +85 -11
  111. package/dist/tools/health.js.map +1 -1
  112. package/dist/tools/index.d.ts.map +1 -1
  113. package/dist/tools/index.js +7 -0
  114. package/dist/tools/index.js.map +1 -1
  115. package/dist/tools/inspection.d.ts +34 -0
  116. package/dist/tools/inspection.d.ts.map +1 -1
  117. package/dist/tools/inspection.js +666 -62
  118. package/dist/tools/inspection.js.map +1 -1
  119. package/dist/tools/layout-flatten.d.ts +7 -1
  120. package/dist/tools/layout-flatten.d.ts.map +1 -1
  121. package/dist/tools/layout-flatten.js +24 -13
  122. package/dist/tools/layout-flatten.js.map +1 -1
  123. package/dist/tools/library.d.ts +29 -2
  124. package/dist/tools/library.d.ts.map +1 -1
  125. package/dist/tools/library.js +247 -34
  126. package/dist/tools/library.js.map +1 -1
  127. package/dist/tools/local-content/builders.js +14 -14
  128. package/dist/tools/local-content/builders.js.map +1 -1
  129. package/dist/tools/local-content/handlers.d.ts +3 -1
  130. package/dist/tools/local-content/handlers.d.ts.map +1 -1
  131. package/dist/tools/local-content/handlers.js +42 -9
  132. package/dist/tools/local-content/handlers.js.map +1 -1
  133. package/dist/tools/local-content/schemas.d.ts +9 -4
  134. package/dist/tools/local-content/schemas.d.ts.map +1 -1
  135. package/dist/tools/local-content/schemas.js +14 -6
  136. package/dist/tools/local-content/schemas.js.map +1 -1
  137. package/dist/tools/multi-items/builders.d.ts.map +1 -1
  138. package/dist/tools/multi-items/builders.js +11 -10
  139. package/dist/tools/multi-items/builders.js.map +1 -1
  140. package/dist/tools/navigation/builders.d.ts +18 -0
  141. package/dist/tools/navigation/builders.d.ts.map +1 -0
  142. package/dist/tools/navigation/builders.js +47 -0
  143. package/dist/tools/navigation/builders.js.map +1 -0
  144. package/dist/tools/navigation/handlers.d.ts +30 -0
  145. package/dist/tools/navigation/handlers.d.ts.map +1 -0
  146. package/dist/tools/navigation/handlers.js +41 -0
  147. package/dist/tools/navigation/handlers.js.map +1 -0
  148. package/dist/tools/navigation/index.d.ts +15 -0
  149. package/dist/tools/navigation/index.d.ts.map +1 -0
  150. package/dist/tools/navigation/index.js +14 -0
  151. package/dist/tools/navigation/index.js.map +1 -0
  152. package/dist/tools/navigation/schemas.d.ts +46 -0
  153. package/dist/tools/navigation/schemas.d.ts.map +1 -0
  154. package/dist/tools/navigation/schemas.js +41 -0
  155. package/dist/tools/navigation/schemas.js.map +1 -0
  156. package/dist/tools/pages/builders.d.ts.map +1 -1
  157. package/dist/tools/pages/builders.js +213 -81
  158. package/dist/tools/pages/builders.js.map +1 -1
  159. package/dist/tools/pages/handlers-audit.d.ts +112 -3
  160. package/dist/tools/pages/handlers-audit.d.ts.map +1 -1
  161. package/dist/tools/pages/handlers-audit.js +623 -7
  162. package/dist/tools/pages/handlers-audit.js.map +1 -1
  163. package/dist/tools/pages/handlers-read.d.ts +51 -8
  164. package/dist/tools/pages/handlers-read.d.ts.map +1 -1
  165. package/dist/tools/pages/handlers-read.js +348 -69
  166. package/dist/tools/pages/handlers-read.js.map +1 -1
  167. package/dist/tools/pages/handlers-write.d.ts +7 -3
  168. package/dist/tools/pages/handlers-write.d.ts.map +1 -1
  169. package/dist/tools/pages/handlers-write.js +69 -25
  170. package/dist/tools/pages/handlers-write.js.map +1 -1
  171. package/dist/tools/pages/header-transparency.d.ts +64 -0
  172. package/dist/tools/pages/header-transparency.d.ts.map +1 -0
  173. package/dist/tools/pages/header-transparency.js +119 -0
  174. package/dist/tools/pages/header-transparency.js.map +1 -0
  175. package/dist/tools/pages/schemas.d.ts +284 -13
  176. package/dist/tools/pages/schemas.d.ts.map +1 -1
  177. package/dist/tools/pages/schemas.js +434 -26
  178. package/dist/tools/pages/schemas.js.map +1 -1
  179. package/dist/tools/pages/target.d.ts +162 -0
  180. package/dist/tools/pages/target.d.ts.map +1 -0
  181. package/dist/tools/pages/target.js +239 -0
  182. package/dist/tools/pages/target.js.map +1 -0
  183. package/dist/tools/response-budget.d.ts +77 -0
  184. package/dist/tools/response-budget.d.ts.map +1 -0
  185. package/dist/tools/response-budget.js +134 -0
  186. package/dist/tools/response-budget.js.map +1 -0
  187. package/dist/tools/shared-schemas.d.ts +45 -0
  188. package/dist/tools/shared-schemas.d.ts.map +1 -1
  189. package/dist/tools/shared-schemas.js +50 -0
  190. package/dist/tools/shared-schemas.js.map +1 -1
  191. package/dist/tools/sources/builders.d.ts.map +1 -1
  192. package/dist/tools/sources/builders.js +55 -39
  193. package/dist/tools/sources/builders.js.map +1 -1
  194. package/dist/tools/sources/handlers-bind.d.ts +10 -11
  195. package/dist/tools/sources/handlers-bind.d.ts.map +1 -1
  196. package/dist/tools/sources/handlers-bind.js +110 -9
  197. package/dist/tools/sources/handlers-bind.js.map +1 -1
  198. package/dist/tools/sources/handlers.d.ts +4 -2
  199. package/dist/tools/sources/handlers.d.ts.map +1 -1
  200. package/dist/tools/sources/handlers.js +11 -7
  201. package/dist/tools/sources/handlers.js.map +1 -1
  202. package/dist/tools/sparse-fields.d.ts +61 -8
  203. package/dist/tools/sparse-fields.d.ts.map +1 -1
  204. package/dist/tools/sparse-fields.js +124 -12
  205. package/dist/tools/sparse-fields.js.map +1 -1
  206. package/dist/tools/sublayout/builders.d.ts.map +1 -1
  207. package/dist/tools/sublayout/builders.js +1 -2
  208. package/dist/tools/sublayout/builders.js.map +1 -1
  209. package/dist/tools/tool-builder/results.d.ts +19 -6
  210. package/dist/tools/tool-builder/results.d.ts.map +1 -1
  211. package/dist/tools/tool-builder/results.js +133 -6
  212. package/dist/tools/tool-builder/results.js.map +1 -1
  213. package/manifest.json +1 -1
  214. package/package.json +2 -2
  215. package/skills/yt-builder-mcp/SKILL.md +1713 -72
@@ -9,13 +9,26 @@
9
9
  */
10
10
  import { detailResult, tableResult } from '@getimo/mcp-toolkit';
11
11
  import { encodeTemplateId } from '../../client.js';
12
+ import { RestError } from '../../errors.js';
12
13
  import { flattenLayout } from '../layout-flatten.js';
13
14
  import { PAGES_COMPACT_COLUMNS, PAGES_TABLE_COLUMNS, SCHEMA_COMPACT_COLUMNS, SCHEMA_TABLE_COLUMNS, buildEtagDetail, buildOutlineText, buildRenderPreviewDetail, buildSchemaRows, mapPageRow, } from '../format/pages-format.js';
14
15
  import { DEFAULT_FIELDS_PAGES_LIST, DEFAULT_FIELDS_SCHEMA, applyMaxChars, clipTextWindow, columnsFromFields, decodeOffsetCursor, encodeOffsetCursor, projectFields, projectedFieldsEcho, projectionFeedback, projectionWarning, } from '../sparse-fields.js';
15
16
  import { errorResult, jsonResult, structuredResult, } from '../tool-builder.js';
17
+ import { TARGET_KEYS } from './schemas.js';
18
+ import { appendTargetParams, malformedTargetError, normalizeTarget, serverTooOldError, TARGET_FAILURE_HINT, targetKeysOf, tooManyTargetsError, } from './target.js';
16
19
  // ─── pages_list ─────────────────────────────────────────────────────
17
20
  /** Default page size for pages_list when `limit` is omitted (B2). */
18
21
  const DEFAULT_PAGES_LIMIT = 50;
22
+ /**
23
+ * SOURCES_BINDING-29 / DISCOVERY_AUDIT-18 — the STATIC pages_list field
24
+ * vocabulary. `mapPageRow` always emits the SAME key set on every row, so
25
+ * deriving it once from an empty-row projection gives the authoritative
26
+ * discoverable field list WITHOUT hard-coding a list that could drift from the
27
+ * mapper. Used to (a) validate a caller's `fields[]` against the real schema
28
+ * rather than against whatever rows a (possibly empty) result happened to carry,
29
+ * and (b) emit `available_fields` on every call. Sorted for a stable echo.
30
+ */
31
+ const PAGES_LIST_KNOWN_FIELDS = Object.keys(mapPageRow({})).sort();
19
32
  /** Decode an offset cursor (`base64url("o:<n>")` or raw `o:<n>`) → offset, else 0. */
20
33
  function decodePagesCursor(cursor) {
21
34
  if (cursor === undefined || cursor === '')
@@ -35,14 +48,19 @@ function decodePagesCursor(cursor) {
35
48
  function encodePagesCursor(offset) {
36
49
  return Buffer.from(`o:${offset}`, 'utf8').toString('base64url');
37
50
  }
38
- /** Case-insensitive substring match across a page row's label / id / type. */
51
+ /**
52
+ * Case-insensitive substring match across a page row's label / id / type /
53
+ * frontend_url. T-C2 (customer P1.2): frontend_url joined the haystack so a
54
+ * slug or URL fragment (e.g. "nkl-hauptspiel") matches even when it appears
55
+ * in none of label/id/type — mirrors PHP's `PageQuery::applyLabelFilter()`.
56
+ */
39
57
  function matchesPageFilter(row, needle) {
40
- const hay = `${String(row.label ?? '')} ${String(row.id ?? '')} ${String(row.type ?? '')}`.toLowerCase();
58
+ const hay = `${String(row.label ?? '')} ${String(row.id ?? '')} ${String(row.type ?? '')} ${String(row.frontend_url ?? '')}`.toLowerCase();
41
59
  return hay.includes(needle);
42
60
  }
43
- export async function handlePagesList(client, { fields, limit, cursor, label_contains, max_chars, }) {
61
+ export async function handlePagesList(client, { fields, limit, cursor, label_contains, max_chars, include_meta, }) {
44
62
  try {
45
- // G-B (Glöckle 2026-06-26, P0): forward `limit`/`offset`/`filter` to the
63
+ // G-B (customer 2026-06-26, P0): forward `limit`/`offset`/`filter` to the
46
64
  // REST /pages endpoint so a site with hundreds of templates does not
47
65
  // serialize the full set every call (server-side pagination). The
48
66
  // server is BACKWARD-COMPATIBLE: with no params it returns the full set
@@ -66,7 +84,7 @@ export async function handlePagesList(client, { fields, limit, cursor, label_con
66
84
  let mapped = rawPages
67
85
  .filter((x) => x !== null && typeof x === 'object')
68
86
  .map(mapPageRow);
69
- // B2 (Glöckle 2026-06-25) + G-B server-side fallback: a real site can
87
+ // B2 (customer 2026-06-25) + G-B server-side fallback: a real site can
70
88
  // have hundreds of templates. When the server already paged + filtered
71
89
  // (it echoes a `total`), the rows it returned ARE the page — do NOT
72
90
  // re-slice them. When the server ignored the params (no `total`, full
@@ -87,18 +105,40 @@ export async function handlePagesList(client, { fields, limit, cursor, label_con
87
105
  const nextCursor = nextOffset < total ? encodePagesCursor(nextOffset) : undefined;
88
106
  // B1: project the SLICE (text leg + structuredContent) when fields given.
89
107
  const items = projectFields(slice, fields, DEFAULT_FIELDS_PAGES_LIST);
90
- const echo = projectedFieldsEcho(fields, DEFAULT_FIELDS_PAGES_LIST);
91
- const feedback = projectionFeedback(slice, fields);
92
- // F23 (2026-06-09): shared loud-warning helper.
93
- const unknownNote = projectionWarning(feedback);
94
- const useProjection = echo !== undefined && echo.length > 0;
95
- const projectedColumns = useProjection ? columnsFromFields([...echo]) : undefined;
108
+ const hasExplicitFields = Array.isArray(fields) && fields.length > 0;
109
+ // SOURCES_BINDING-29: validate `fields[]` against the STATIC schema, not
110
+ // the returned rows an EMPTY result set no longer makes every field
111
+ // look invalid (the "available: " with no fields bug). unknown = a
112
+ // requested field whose top-level segment is not a known key.
113
+ const unknownFields = hasExplicitFields
114
+ ? fields.filter((f) => !PAGES_LIST_KNOWN_FIELDS.includes(f.split('.')[0] ?? f))
115
+ : [];
116
+ // DISCOVERY_AUDIT-18: `available_fields` is the STATIC discoverable
117
+ // vocabulary, emitted on EVERY call (an empty page still tells the agent
118
+ // which fields exist). `projected_fields` reflects the keys ACTUALLY in
119
+ // the emitted rows — the full vocabulary in the default no-`fields` case,
120
+ // the requested subset otherwise (LAYOUT_READ-18: no longer the stale
121
+ // 5-key DEFAULT echo while rows carry 11).
122
+ const availableFields = [...PAGES_LIST_KNOWN_FIELDS];
123
+ const projectedFields = hasExplicitFields
124
+ ? [...fields]
125
+ : [...PAGES_LIST_KNOWN_FIELDS];
126
+ // SOURCES_BINDING-29: suppress the loud projection warning on an empty
127
+ // result (projection is moot) and when nothing is actually unknown.
128
+ const emitWarning = total > 0 && unknownFields.length > 0;
129
+ const unknownNote = emitWarning
130
+ ? ` (unknown projection fields ignored: ${unknownFields.join(', ')}` +
131
+ ` — available: ${availableFields.join(', ')})`
132
+ : '';
133
+ const useProjection = hasExplicitFields;
134
+ const projectedColumns = useProjection ? columnsFromFields([...fields]) : undefined;
96
135
  const pagedNote = total > slice.length ? ` of ${String(total)}${nextCursor !== undefined ? ', pass cursor for more' : ''}` : '';
97
136
  const toolkitResult = tableResult(useProjection ? items : slice, {
98
137
  columns: projectedColumns ?? [...PAGES_TABLE_COLUMNS],
99
138
  compactColumns: projectedColumns ?? [...PAGES_COMPACT_COLUMNS],
100
139
  header: (count) => `${String(count)}${pagedNote} pages${unknownNote}`,
101
140
  footer: 'Use yootheme_builder_page_get_schema <id> to inspect.',
141
+ includeMeta: include_meta,
102
142
  });
103
143
  return structuredResult(applyMaxChars(toolkitResult, max_chars), {
104
144
  items,
@@ -106,12 +146,13 @@ export async function handlePagesList(client, { fields, limit, cursor, label_con
106
146
  returned: slice.length,
107
147
  ...(nextCursor !== undefined ? { next_cursor: nextCursor } : {}),
108
148
  ...(typeof data.etag === 'string' ? { etag: data.etag } : {}),
109
- ...(echo !== undefined ? { projected_fields: [...echo] } : {}),
110
- ...(feedback !== undefined ? {
111
- available_fields: [...feedback.available_fields],
112
- unknown_fields: [...feedback.unknown_fields],
113
- ...(unknownNote.trim() !== '' ? { projection_warning: unknownNote.trim() } : {}),
114
- } : {}),
149
+ projected_fields: projectedFields,
150
+ available_fields: availableFields,
151
+ // Emit unknown_fields whenever the caller passed an explicit
152
+ // projection (empty [] = all valid; keeps the F-004 contract), omit
153
+ // it on the default no-`fields` call.
154
+ ...(hasExplicitFields ? { unknown_fields: unknownFields } : {}),
155
+ ...(emitWarning ? { projection_warning: unknownNote.trim() } : {}),
115
156
  });
116
157
  }
117
158
  catch (e) {
@@ -134,29 +175,126 @@ export async function handlePagesList(client, { fields, limit, cursor, label_con
134
175
  export async function resolveUrlToTemplate(client, url) {
135
176
  return client.get(`/pages/resolve?url=${encodeURIComponent(url)}`);
136
177
  }
137
- export async function handlePagesResolve(client, { url }) {
178
+ /**
179
+ * A-6: resolve a semantic `target` — `GET /pages/resolve?target[special]=home`.
180
+ * The bracket encoding is the ONE wire form both CMS controllers parse (see
181
+ * `./target.ts`).
182
+ */
183
+ async function resolveTargetToTemplate(client, target) {
184
+ const qs = new URLSearchParams();
185
+ appendTargetParams(qs, target);
186
+ return client.get(`/pages/resolve?${qs.toString()}`);
187
+ }
188
+ /**
189
+ * A-6: is this failure an OLD host rejecting the request because it still
190
+ * hard-requires `url`? WP core answers `rest_missing_callback_param` when the
191
+ * route arg is `required`; the pre-A-5 controller answered its own
192
+ * `missing_url`. Either way the fix is "update the plugin", not "fix your
193
+ * target" — and only ever on a target-only call.
194
+ */
195
+ function isMissingUrlRejection(e) {
196
+ if (!(e instanceof RestError) || e.status !== 400)
197
+ return false;
198
+ const code = e.code ?? '';
199
+ return code.includes('rest_missing_callback_param') || code.includes('missing_url');
200
+ }
201
+ /** A-6: narrow an off-the-wire `matched_by` to the declared enum, else drop it. */
202
+ function knownMatchedBy(value) {
203
+ return TARGET_KEYS.find((key) => key === value);
204
+ }
205
+ export async function handlePagesResolve(client, { url, target }) {
206
+ // A-6 / F205: the exactly-one-of(url, target) contract is validated HERE
207
+ // with typed results — a zod `.refine` would surface as a raw -32602 and
208
+ // strip every hint below. Mirrors the WP/Joomla controllers' own codes so
209
+ // the contract reads the same whether it fired locally or upstream.
210
+ const resolvedTarget = normalizeTarget(target);
211
+ // BUG-4 (shared rule, PHP `PageQuery::malformedTargetError()`): a target bag
212
+ // that survived nothing is a MALFORMED ADDRESS. Checked BEFORE the
213
+ // exactly-one-of(url, target) pair, exactly as both controllers do — so it
214
+ // is never reported as `missing_url` (which claims no address was passed)
215
+ // and, alongside a `url`, is never silently ignored.
216
+ const malformed = malformedTargetError(target, resolvedTarget, {
217
+ context: { url: url ?? null, target: target ?? null },
218
+ hint: 'Each target member must be a non-empty string or number — e.g. '
219
+ + 'target:{special:"home"} or target:{template_id:"page:4816"}. Or pass `url` instead.',
220
+ });
221
+ if (malformed !== null)
222
+ return malformed;
223
+ const targetKeys = targetKeysOf(resolvedTarget);
224
+ const hasUrl = typeof url === 'string' && url.trim() !== '';
225
+ const hasTarget = targetKeys.length > 0;
226
+ if (hasUrl && hasTarget) {
227
+ return tooManyTargetsError({
228
+ supplied: ['url', ...targetKeys.map((k) => `target.${k}`)],
229
+ context: { url: url ?? null, target: resolvedTarget },
230
+ });
231
+ }
232
+ if (targetKeys.length > 1) {
233
+ return tooManyTargetsError({
234
+ supplied: targetKeys,
235
+ context: { target: resolvedTarget },
236
+ });
237
+ }
238
+ if (!hasUrl && !hasTarget) {
239
+ return errorResult({
240
+ error: new RestError({
241
+ status: 400,
242
+ code: 'yootheme_builder_mcp.pages.missing_url',
243
+ message: 'A page address is required: pass `url` or `target`.',
244
+ body: null,
245
+ }),
246
+ context: { url: null, target: {} },
247
+ hint: 'Pass `url` (a public page/article URL), or `target` with exactly one of ' +
248
+ 'template_id / post_id / url / special ("home", "blog", "404", "search", ' +
249
+ '"category", "tag", "author", "archive") / title.',
250
+ });
251
+ }
138
252
  try {
139
- const data = await resolveUrlToTemplate(client, url);
253
+ const data = hasTarget
254
+ ? await resolveTargetToTemplate(client, resolvedTarget)
255
+ : await resolveUrlToTemplate(client, (url ?? '').trim());
256
+ const matchedBy = knownMatchedBy(data.matched_by);
140
257
  return structuredResult(jsonResult(data), {
141
258
  template_id: data.template_id,
142
259
  // G-A2: post_id is null for a theme-template resolution.
143
260
  post_id: data.post_id ?? null,
144
261
  type: data.type,
262
+ // Forwarded ONLY when the host sent one — a synthesised label would
263
+ // claim a page name this build has no way to know.
264
+ ...(typeof data.label === 'string' && data.label !== '' ? { label: data.label } : {}),
145
265
  frontend_url: data.frontend_url ?? null,
146
266
  // G-A2: context/resolved_via are additive. An OLDER plugin (pre-G-A2)
147
267
  // only ever resolved per-page layouts and omits both — default to the
148
268
  // per-page shape so a strict host still accepts the success.
149
269
  context: data.context ?? null,
150
270
  resolved_via: data.resolved_via ?? 'per_page',
271
+ // A-6: forwarded ONLY when the host sent a value this build knows.
272
+ // A `url` resolve and every pre-A-5 host omit it; an UNKNOWN token
273
+ // from a newer host is dropped rather than tripping the strict
274
+ // outputSchema and failing an otherwise perfect resolve.
275
+ ...(matchedBy !== undefined ? { matched_by: matchedBy } : {}),
151
276
  });
152
277
  }
153
278
  catch (e) {
279
+ // A-6 version-skew: an old host still hard-requires `url`, so a
280
+ // target-only call 400s before its controller ever sees the target.
281
+ // That is an "update the plugin" condition, not a bad address.
282
+ if (hasTarget && isMissingUrlRejection(e)) {
283
+ return serverTooOldError({
284
+ message: 'This site rejected a `target`-only resolve because it still requires `url`.',
285
+ context: { target: resolvedTarget },
286
+ });
287
+ }
154
288
  return errorResult({
155
289
  error: e,
156
- context: { url },
157
- hint: 'If this 404s, the URL renders via a theme template (not a ' +
158
- 'per-page layout) use yootheme_builder_pages_list to find its ' +
159
- 'template_id. Verify the URL is a public, singular page/article.',
290
+ context: { url: url ?? null, target: resolvedTarget },
291
+ // BUG-3 / MM-5: the target-aware hint now lives in `target.js` so
292
+ // `pages_audit` answers the SAME error family with the SAME steer.
293
+ hint: hasTarget
294
+ ? TARGET_FAILURE_HINT
295
+ : 'If this 404s, the URL renders via a theme template (not a ' +
296
+ 'per-page layout) — use yootheme_builder_pages_list to find its ' +
297
+ 'template_id. Verify the URL is a public, singular page/article.',
160
298
  });
161
299
  }
162
300
  }
@@ -334,7 +472,7 @@ function parseOutlineNodes(raw) {
334
472
  return out;
335
473
  }
336
474
  /**
337
- * B7 (Glöckle continuation cursor) — build the full-dump TEXT leg windowed at
475
+ * B7 (customer continuation cursor) — build the full-dump TEXT leg windowed at
338
476
  * `[offset, offset+max_chars)` and, when more remains, append a `next_cursor`.
339
477
  *
340
478
  * The truncated full/bindings_only TEXT leg of page_get_layout had NO
@@ -381,7 +519,7 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
381
519
  return effective.error;
382
520
  }
383
521
  const templateId = effective.templateId;
384
- // B7 (Glöckle continuation cursor): an opaque char-offset cursor
522
+ // B7 (customer continuation cursor): an opaque char-offset cursor
385
523
  // (base64url `o:<n>`) resumes a previously-clipped full/bindings_only
386
524
  // text leg from the offset. A present cursor makes the read a
387
525
  // CONTINUATION — it is NEVER served from the [unchanged] dedup cache
@@ -451,7 +589,7 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
451
589
  if (flat !== true) {
452
590
  // Default: nested passthrough — unchanged from pre-G.3, except an
453
591
  // OPTIONAL G-B `max_chars` caps the (potentially large) JSON text
454
- // leg. B7 (Glöckle continuation cursor): when the dump is clipped
592
+ // leg. B7 (customer continuation cursor): when the dump is clipped
455
593
  // (by `max_chars` or resumed via `cursor`) it now also emits a
456
594
  // `next_cursor` so an agent can page through the REST of the
457
595
  // subtree instead of being stuck with the first slice. With no
@@ -461,7 +599,14 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
461
599
  if (max_chars !== undefined || isContinuation) {
462
600
  return windowedFullDump(data, charOffset, max_chars);
463
601
  }
464
- return applyMaxChars(jsonResult(data), max_chars);
602
+ // Reaching here means `max_chars === undefined`, so the former
603
+ // `applyMaxChars(jsonResult(data), max_chars)` was a no-op by its own
604
+ // contract (`sparse-fields.ts`: undefined → return the result
605
+ // unchanged). Calling it read as "this leg is capped" when it never
606
+ // was, and it was the one character-cut call site with no
607
+ // `structuredContent` beside it — see
608
+ // `tests/contracts/char-cut-requires-structured-leg.test.ts`.
609
+ return jsonResult(data);
465
610
  }
466
611
  // flat: true → depth-first walk on the nested layout. Apply
467
612
  // pickFields per element when `fields[]` was passed; echo the
@@ -519,7 +664,10 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
519
664
  // single-root-node envelope — collapse identically.
520
665
  const candidate = hasInnerLayoutField ? layoutObj.layout : dataLayout;
521
666
  const layoutRoot = unwrapToWalkableContainer(candidate);
522
- const flatElements = flattenLayout(layoutRoot);
667
+ // LAYOUT_READ-7: pass the resolved template id so flat-mode `path`s are
668
+ // fully-qualified (`/templates/<id>/layout/children/0`) — matching the
669
+ // `path` element_list emits. `rel_path` stays the short `/children/0`.
670
+ const flatElements = flattenLayout(layoutRoot, templateId);
523
671
  // Non-silent fallback (C4) + F29 calibration: if the walk yields
524
672
  // ZERO elements AND the resolved walk container is an unrecognized
525
673
  // NON-EMPTY shape, surface a warning so it can never pass silently.
@@ -542,7 +690,14 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
542
690
  // `projection_warning` (the shared F23 SSoT helper) so the agent can
543
691
  // self-correct on the next call. Default-all callers (no fields[])
544
692
  // get no audit overhead — projectionFeedback returns undefined.
545
- const feedback = projectionFeedback(flatElements, fields);
693
+ // SOURCES_BINDING-29 (parallel site): an EMPTY flat result cannot
694
+ // observe a field vocabulary, so `projectionFeedback` over zero rows
695
+ // would report EVERY requested field as unknown against an empty
696
+ // `available_fields`. Suppress the feedback entirely on a zero-element
697
+ // flatten — the projection is moot when there is nothing to project.
698
+ const feedback = flatElements.length > 0
699
+ ? projectionFeedback(flatElements, fields)
700
+ : undefined;
546
701
  const projectionNote = projectionWarning(feedback);
547
702
  // 1.0.1 Wave-1.7 F-COLD-3: forward the canonical pointer base
548
703
  // through the flat-projection path too so cold agents iterating
@@ -579,7 +734,7 @@ export async function handlePageGetLayout(client, { template_id, post_id, url, f
579
734
  }
580
735
  }
581
736
  // ─── page_get_schema ────────────────────────────────────────────────
582
- export async function handlePageGetSchema(client, { template_id, post_id, url, fields, max_chars }) {
737
+ export async function handlePageGetSchema(client, { template_id, post_id, url, fields, max_chars, include_meta }) {
583
738
  try {
584
739
  // G-A: resolve the effective template_id from template_id | post_id | url.
585
740
  const effective = await resolveEffectiveTemplateId(client, { template_id, post_id, url });
@@ -590,7 +745,7 @@ export async function handlePageGetSchema(client, { template_id, post_id, url, f
590
745
  const data = await client.get(`/pages/${encodeTemplateId(templateId)}/schema`);
591
746
  const rawNodes = Array.isArray(data.nodes) ? data.nodes : [];
592
747
  const rows = buildSchemaRows(rawNodes.filter((x) => x !== null && typeof x === 'object'));
593
- // G-B (Glöckle 2026-06-26, P2): `fields[]` projection — DEFAULT_FIELDS_SCHEMA
748
+ // G-B (customer 2026-06-26, P2): `fields[]` projection — DEFAULT_FIELDS_SCHEMA
594
749
  // existed but was never wired. Mirror element_list: project both legs,
595
750
  // echo `projected_fields`, surface the F23 unknown-field feedback.
596
751
  const items = projectFields(rows, fields, DEFAULT_FIELDS_SCHEMA);
@@ -614,12 +769,14 @@ export async function handlePageGetSchema(client, { template_id, post_id, url, f
614
769
  columns: columnsFromFields([...fields]),
615
770
  header: (count) => `${String(count)} nodes in template "${templateId}"${unknownNote}`,
616
771
  footer: 'Use yootheme_builder_element_get <path> for full element data.',
772
+ includeMeta: include_meta,
617
773
  }, 'compact')
618
774
  : tableResult(rows, {
619
775
  columns: [...SCHEMA_TABLE_COLUMNS],
620
776
  compactColumns: [...SCHEMA_COMPACT_COLUMNS],
621
777
  header: (count) => `${String(count)} nodes in template "${templateId}"`,
622
778
  footer: 'Use yootheme_builder_element_get <path> for full element data.',
779
+ includeMeta: include_meta,
623
780
  }, 'compact');
624
781
  return structuredResult(applyMaxChars(toolkitResult, max_chars), {
625
782
  items,
@@ -645,10 +802,13 @@ export async function handlePageGetSchema(client, { template_id, post_id, url, f
645
802
  export async function handleGetEtag(client) {
646
803
  try {
647
804
  const data = await client.get('/etag');
648
- const toolkitResult = detailResult(buildEtagDetail({
649
- etag: data.etag,
650
- generated_at: data.generated_at,
651
- }));
805
+ const toolkitResult = detailResult({
806
+ ...buildEtagDetail({
807
+ etag: data.etag,
808
+ generated_at: data.generated_at,
809
+ }),
810
+ includeMeta: false,
811
+ });
652
812
  return structuredResult(toolkitResult, {
653
813
  etag: data.etag,
654
814
  ...(typeof data.generated_at === 'string'
@@ -665,52 +825,124 @@ export async function handleGetEtag(client) {
665
825
  }
666
826
  }
667
827
  // ─── template_summary (T9 — Audit-v3 B.5 token-efficient overview) ────
828
+ /**
829
+ * Task #46 Friction-3 (2026-05-29): PHP's `json_encode([])` emits `[]`
830
+ * (array), not `{}` (object). The REST handler emits `counts_by_type: []`
831
+ * for an empty template, but the outputSchema declares
832
+ * `z.record(z.string(), z.number())`, which requires an object. Normalize
833
+ * array->object here so the schema validates and the agent sees the
834
+ * (valid, empty) summary. T-C5 (mega-push plan 2026-07-24-02): factored out
835
+ * of `handleTemplateSummary` so it applies to BOTH the single-template
836
+ * response AND every per-id entry of a batch response's `summaries` map.
837
+ */
838
+ function normalizeCountsByType(data) {
839
+ const normalized = { ...data };
840
+ if (Array.isArray(normalized.counts_by_type)) {
841
+ normalized.counts_by_type = {};
842
+ }
843
+ return normalized;
844
+ }
845
+ /**
846
+ * T-E2 + T-C5 (mega-push plan 2026-07-24-02, customer P1.5): build the shared
847
+ * `include_nodes`/`limit`/`cursor` query params both the single-template and
848
+ * batch REST calls forward verbatim to `PageQuery::summary()`'s `$options`.
849
+ */
850
+ function summaryQueryParams(opts) {
851
+ const qs = new URLSearchParams();
852
+ if (opts.include_nodes === true)
853
+ qs.set('include_nodes', 'true');
854
+ if (typeof opts.limit === 'number')
855
+ qs.set('limit', String(opts.limit));
856
+ if (typeof opts.cursor === 'string' && opts.cursor !== '')
857
+ qs.set('cursor', opts.cursor);
858
+ return qs;
859
+ }
668
860
  /**
669
861
  * T9: a one-call structured overview of a template — element counts by
670
862
  * type, binding count, max nesting depth, and named landmarks. Lets the
671
863
  * agent grasp a 96-node template for ~1 kB instead of pulling the full
672
864
  * element_list dump (20 kB+).
865
+ *
866
+ * T-E2 + T-C5 (mega-push plan 2026-07-24-02, Thomas-Entscheid C + customer
867
+ * P1.5): three additive changes, one coherent tool-behavior update:
868
+ * - `include_nodes`/`limit`/`cursor` ride through to the REST query string
869
+ * unconditionally opt-in (`content_nodes` no longer ships by default).
870
+ * - `template_id` (single) XOR `template_ids` (batch) — exactly one,
871
+ * validated HERE with typed results (mirrors `handlePagesResolve`'s
872
+ * exactly-one-of(url, target) contract) rather than a zod `.refine`,
873
+ * which would surface as a raw -32602 and strip every hint below.
874
+ * - Batch hits the LITERAL `/pages/summary?template_ids=a,b,c` route (ONE
875
+ * REST round-trip for N templates) and returns
876
+ * `{summaries: {<id>: <summary>}, not_found: [...]}`.
673
877
  */
674
- export async function handleTemplateSummary(client, { template_id }) {
878
+ export async function handleTemplateSummary(client, { template_id, template_ids, include_nodes, limit, cursor }) {
879
+ const hasTemplateId = typeof template_id === 'string' && template_id.trim() !== '';
880
+ const hasTemplateIds = Array.isArray(template_ids) && template_ids.length > 0;
881
+ if (hasTemplateId && hasTemplateIds) {
882
+ return errorResult({
883
+ error: new RestError({
884
+ status: 400,
885
+ code: 'yootheme_builder_mcp.pages.too_many_template_ids',
886
+ message: 'Provide EXACTLY ONE of template_id (single) or template_ids (batch), not both.',
887
+ body: null,
888
+ }),
889
+ context: { template_id: template_id ?? null, template_ids: template_ids ?? null },
890
+ hint: 'Pass template_id for a single template, or template_ids:[...] for a batch - not both.',
891
+ });
892
+ }
893
+ if (!hasTemplateId && !hasTemplateIds) {
894
+ return errorResult({
895
+ error: new RestError({
896
+ status: 400,
897
+ code: 'yootheme_builder_mcp.pages.missing_template_id',
898
+ message: 'A template_id or template_ids is required.',
899
+ body: null,
900
+ }),
901
+ context: {},
902
+ hint: 'Pass template_id (single) or template_ids:[...] (batch). Use yootheme_builder_pages_list to discover template IDs.',
903
+ });
904
+ }
905
+ const qs = summaryQueryParams({ include_nodes, limit, cursor });
675
906
  try {
676
- const data = await client.get(`/pages/${encodeTemplateId(template_id)}/summary`);
677
- // F-AUDIT-2 (2026-05-26): template_summary now declares
678
- // TEMPLATE_SUMMARY_OUTPUT_SCHEMA. The SDK requires
679
- // `structuredContent` when an outputSchema is present, otherwise
680
- // it raises `-32602 Output validation error`. Wrap the raw body
681
- // with `structuredResult` so the text leg (LLM-readable JSON)
682
- // stays unchanged AND the structuredContent leg validates.
683
- //
684
- // Task #46 Friction-3 (2026-05-29): PHP's `json_encode([])` emits
685
- // `[]` (array) — not `{}` (object). The REST handler emits
686
- // `counts_by_type: []` for empty templates, but the outputSchema
687
- // declares `z.record(z.string(), z.number())` which requires an
688
- // object. The mismatch surfaces as `-32602 counts_by_type
689
- // expected record, received array`, swallowing the entire response
690
- // for any empty / freshly-created template. Normalize array→object
691
- // here so the schema validates and the agent sees the (valid,
692
- // empty) summary. Same defensive shape applies to any future
693
- // map-typed REST field that might come back empty.
694
- const normalized = {
695
- ...data,
696
- };
697
- if (Array.isArray(normalized.counts_by_type)) {
698
- normalized.counts_by_type = {};
907
+ if (hasTemplateIds) {
908
+ qs.set('template_ids', template_ids.join(','));
909
+ const data = await client.get(`/pages/summary?${qs.toString()}`);
910
+ const normalizedSummaries = {};
911
+ for (const [id, summary] of Object.entries(data.summaries ?? {})) {
912
+ normalizedSummaries[id] = normalizeCountsByType(summary);
913
+ }
914
+ const normalized = {
915
+ summaries: normalizedSummaries,
916
+ not_found: data.not_found ?? [],
917
+ ...(typeof data.etag === 'string' ? { etag: data.etag } : {}),
918
+ };
919
+ const toolkitResult = jsonResult(normalized);
920
+ return structuredResult({ content: toolkitResult.content }, normalized);
699
921
  }
922
+ const id = (template_id ?? '').trim();
923
+ const query = qs.toString();
924
+ const data = await client.get(`/pages/${encodeTemplateId(id)}/summary${query !== '' ? `?${query}` : ''}`);
925
+ // F-AUDIT-2 (2026-05-26): template_summary declares
926
+ // TEMPLATE_SUMMARY_OUTPUT_SCHEMA. The SDK requires
927
+ // `structuredContent` when an outputSchema is present, otherwise it
928
+ // raises `-32602 Output validation error`. Wrap the raw body with
929
+ // `structuredResult` so the text leg (LLM-readable JSON) stays
930
+ // unchanged AND the structuredContent leg validates.
931
+ const normalized = normalizeCountsByType(data);
700
932
  const toolkitResult = jsonResult(normalized);
701
933
  return structuredResult({ content: toolkitResult.content }, normalized);
702
934
  }
703
935
  catch (e) {
704
936
  return errorResult({
705
937
  error: e,
706
- context: { template_id },
707
- hint: 'Verify the template_id exists via yootheme_builder_pages_list.',
938
+ context: { template_id: template_id ?? null, template_ids: template_ids ?? null },
939
+ hint: 'Verify the template_id(s) exist via yootheme_builder_pages_list.',
708
940
  });
709
941
  }
710
942
  }
711
943
  // ─── render_preview (F5 + N5 — authenticated post-write render check) ──
712
944
  /**
713
- * F5 + N5 (Glöckle HANDOFF-4/5): the authenticated post-write render
945
+ * F5 + N5 (customer HANDOFF-4/5): the authenticated post-write render
714
946
  * HEALTHCHECK. Resolves the EFFECTIVE template_id from `template_id` OR `url`
715
947
  * (the SAME `resolveEffectiveTemplateId` precedence page_get_layout uses — a
716
948
  * pasted public URL is reverse-resolved via GET /pages/resolve first), then
@@ -723,15 +955,20 @@ export async function handleTemplateSummary(client, { template_id }) {
723
955
  * items, or that threw inside the Builder pipeline (the swallowed YOOtheme
724
956
  * render error surfaced as `error`), is a SUCCESS — not a tool error.
725
957
  */
726
- export async function handleRenderPreview(client, { template_id, url, expect }) {
958
+ export async function handleRenderPreview(client, { template_id, post_id, url, expect }) {
727
959
  try {
728
- // G-A precedence: template_id wins, else resolve `url` to its template_id.
729
- const effective = await resolveEffectiveTemplateId(client, { template_id, url });
960
+ // WAVE-1 follow-up (a): render_preview now accepts `post_id` too, the SAME
961
+ // three-mode addressing page_get_layout uses template_id wins, else a
962
+ // bare numeric post/article id, else a `url` reverse-resolved via
963
+ // /pages/resolve. (Cross-file: the RENDER_PREVIEW input schema in
964
+ // pages/builders.ts must also declare `post_id` + the native catalog must
965
+ // carry it — flagged for the coordinator regen.)
966
+ const effective = await resolveEffectiveTemplateId(client, { template_id, post_id, url });
730
967
  if ('error' in effective) {
731
968
  return effective.error;
732
969
  }
733
970
  const templateId = effective.templateId;
734
- // Glöckle HANDOFF-6 #4: an `expect` substring is passed to the render
971
+ // customer HANDOFF-6 #4: an `expect` substring is passed to the render
735
972
  // endpoint as a query param (the server greps the rendered HTML and adds
736
973
  // {matched, snippet}). A blank value is treated as "no check".
737
974
  const wantMatch = typeof expect === 'string' && expect !== '';
@@ -759,7 +996,7 @@ export async function handleRenderPreview(client, { template_id, url, expect })
759
996
  }
760
997
  : {}),
761
998
  };
762
- return structuredResult(detailResult(buildRenderPreviewDetail(payload)), {
999
+ return structuredResult(detailResult({ ...buildRenderPreviewDetail(payload), includeMeta: false }), {
763
1000
  rendered: payload.rendered,
764
1001
  item_count: payload.item_count,
765
1002
  empty: payload.empty,
@@ -773,9 +1010,51 @@ export async function handleRenderPreview(client, { template_id, url, expect })
773
1010
  catch (e) {
774
1011
  return errorResult({
775
1012
  error: e,
776
- context: { template_id, url, expect },
1013
+ context: { template_id, post_id, url, expect },
777
1014
  hint: 'Verify the template_id exists via yootheme_builder_pages_list ' +
778
- '(or that the url resolves). Run yootheme_builder_diagnose to check auth.',
1015
+ '(or that post_id / the url resolves). Run yootheme_builder_diagnose to check auth.',
1016
+ });
1017
+ }
1018
+ }
1019
+ // ─── heading_outline (T-A5 — a real, doc-order H1-H6 outline) ──────────
1020
+ /**
1021
+ * T-A5 (heading_outline tool, mega-push plan 2026-07-24-02): a real,
1022
+ * doc-order H1-H6 outline of ONE template, each heading attributed to its
1023
+ * source element (`element_type`/`path`), plus the alt-status of every
1024
+ * image (including images living inside html/text/code raw markup).
1025
+ * Read-only STRUCTURE inspection — no findings/severity (that is
1026
+ * pages_audit's job).
1027
+ *
1028
+ * `template_id`-only addressing (no post_id/url sugar) — resolve via
1029
+ * yootheme_builder_pages_resolve first, like the other per-page tools that
1030
+ * need a bare template_id. The REST body already carries the HEADING_OUTLINE_
1031
+ * OUTPUT_SCHEMA shape, so it is forwarded through structuredResult verbatim
1032
+ * (a thin dispatch onto PageAuditService::headingOutline()).
1033
+ */
1034
+ export async function handleHeadingOutline(client, { template_id, mode }) {
1035
+ try {
1036
+ const qs = new URLSearchParams();
1037
+ if (mode !== undefined)
1038
+ qs.set('mode', mode);
1039
+ const query = qs.toString();
1040
+ const data = await client.get(`/pages/${encodeTemplateId(template_id)}/heading-outline${query !== '' ? `?${query}` : ''}`);
1041
+ return structuredResult(jsonResult(data), data);
1042
+ }
1043
+ catch (e) {
1044
+ if (e instanceof RestError && e.status === 404) {
1045
+ return errorResult({
1046
+ error: e,
1047
+ context: { template_id, mode: mode ?? null },
1048
+ hint: 'GET /pages/{id}/heading-outline was not found on this site: the ' +
1049
+ 'yt-builder-mcp host plugin predates heading_outline. Update the plugin on ' +
1050
+ 'this site, or verify the template_id via yootheme_builder_pages_list.',
1051
+ });
1052
+ }
1053
+ return errorResult({
1054
+ error: e,
1055
+ context: { template_id, mode: mode ?? null },
1056
+ hint: 'Verify the template_id exists via yootheme_builder_pages_list. Run ' +
1057
+ 'yootheme_builder_diagnose to check auth.',
779
1058
  });
780
1059
  }
781
1060
  }