@wootsup/yt-builder-mcp 1.12.0 → 2.0.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 (857) hide show
  1. package/README.md +128 -30
  2. package/bin/node-floor.d.ts +28 -0
  3. package/bin/node-floor.js +123 -0
  4. package/bin/yt-builder-mcp.js +51 -10
  5. package/dist/auth.d.ts +3 -121
  6. package/dist/auth.d.ts.map +1 -1
  7. package/dist/auth.js +40 -170
  8. package/dist/auth.js.map +1 -1
  9. package/dist/catalog/build-catalog.d.ts +4 -49
  10. package/dist/catalog/build-catalog.d.ts.map +1 -1
  11. package/dist/catalog/build-catalog.js +15 -126
  12. package/dist/catalog/build-catalog.js.map +1 -1
  13. package/dist/catalog/json-schema.d.ts +3 -0
  14. package/dist/catalog/json-schema.d.ts.map +1 -0
  15. package/dist/catalog/json-schema.js +68 -0
  16. package/dist/catalog/json-schema.js.map +1 -0
  17. package/dist/catalog/output-shapes.d.ts +6 -0
  18. package/dist/catalog/output-shapes.d.ts.map +1 -0
  19. package/dist/catalog/output-shapes.js +89 -0
  20. package/dist/catalog/output-shapes.js.map +1 -0
  21. package/dist/catalog/tool-catalog-markdown.d.ts +1 -37
  22. package/dist/catalog/tool-catalog-markdown.d.ts.map +1 -1
  23. package/dist/catalog/tool-catalog-markdown.js +8 -52
  24. package/dist/catalog/tool-catalog-markdown.js.map +1 -1
  25. package/dist/cli/doctor-command.d.ts +2 -7
  26. package/dist/cli/doctor-command.d.ts.map +1 -1
  27. package/dist/cli/doctor-command.js +21 -62
  28. package/dist/cli/doctor-command.js.map +1 -1
  29. package/dist/cli/invocation-decision.d.ts +4 -52
  30. package/dist/cli/invocation-decision.d.ts.map +1 -1
  31. package/dist/cli/invocation-decision.js +45 -54
  32. package/dist/cli/invocation-decision.js.map +1 -1
  33. package/dist/client.d.ts +3 -107
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +34 -115
  36. package/dist/client.js.map +1 -1
  37. package/dist/clients/claude-code.d.ts +0 -15
  38. package/dist/clients/claude-code.d.ts.map +1 -1
  39. package/dist/clients/claude-code.js +0 -22
  40. package/dist/clients/claude-code.js.map +1 -1
  41. package/dist/clients/claude-desktop.d.ts +0 -16
  42. package/dist/clients/claude-desktop.d.ts.map +1 -1
  43. package/dist/clients/claude-desktop.js +0 -18
  44. package/dist/clients/claude-desktop.js.map +1 -1
  45. package/dist/clients/cline.d.ts +0 -23
  46. package/dist/clients/cline.d.ts.map +1 -1
  47. package/dist/clients/cline.js +0 -33
  48. package/dist/clients/cline.js.map +1 -1
  49. package/dist/clients/codex-cli.d.ts +0 -39
  50. package/dist/clients/codex-cli.d.ts.map +1 -1
  51. package/dist/clients/codex-cli.js +0 -137
  52. package/dist/clients/codex-cli.js.map +1 -1
  53. package/dist/clients/continue.d.ts +0 -10
  54. package/dist/clients/continue.d.ts.map +1 -1
  55. package/dist/clients/continue.js +0 -26
  56. package/dist/clients/continue.js.map +1 -1
  57. package/dist/clients/cursor.d.ts +0 -9
  58. package/dist/clients/cursor.d.ts.map +1 -1
  59. package/dist/clients/cursor.js +0 -11
  60. package/dist/clients/cursor.js.map +1 -1
  61. package/dist/clients/gemini-cli.d.ts +0 -15
  62. package/dist/clients/gemini-cli.d.ts.map +1 -1
  63. package/dist/clients/gemini-cli.js +0 -17
  64. package/dist/clients/gemini-cli.js.map +1 -1
  65. package/dist/clients/home.d.ts +0 -12
  66. package/dist/clients/home.d.ts.map +1 -1
  67. package/dist/clients/home.js +0 -12
  68. package/dist/clients/home.js.map +1 -1
  69. package/dist/clients/index.d.ts +0 -150
  70. package/dist/clients/index.d.ts.map +1 -1
  71. package/dist/clients/index.js +0 -112
  72. package/dist/clients/index.js.map +1 -1
  73. package/dist/clients/roo-code.d.ts +0 -20
  74. package/dist/clients/roo-code.d.ts.map +1 -1
  75. package/dist/clients/roo-code.js +0 -22
  76. package/dist/clients/roo-code.js.map +1 -1
  77. package/dist/clients/zed.d.ts +0 -9
  78. package/dist/clients/zed.d.ts.map +1 -1
  79. package/dist/clients/zed.js +0 -18
  80. package/dist/clients/zed.js.map +1 -1
  81. package/dist/compat-floor.d.ts +35 -0
  82. package/dist/compat-floor.d.ts.map +1 -0
  83. package/dist/compat-floor.js +84 -0
  84. package/dist/compat-floor.js.map +1 -0
  85. package/dist/compat-probe.d.ts +6 -0
  86. package/dist/compat-probe.d.ts.map +1 -0
  87. package/dist/compat-probe.js +20 -0
  88. package/dist/compat-probe.js.map +1 -0
  89. package/dist/diagnostics/ca-reexec.d.ts +0 -30
  90. package/dist/diagnostics/ca-reexec.d.ts.map +1 -1
  91. package/dist/diagnostics/ca-reexec.js +0 -62
  92. package/dist/diagnostics/ca-reexec.js.map +1 -1
  93. package/dist/diagnostics/ca-trust.d.ts +0 -22
  94. package/dist/diagnostics/ca-trust.d.ts.map +1 -1
  95. package/dist/diagnostics/ca-trust.js +3 -42
  96. package/dist/diagnostics/ca-trust.js.map +1 -1
  97. package/dist/diagnostics/diagnose-network-error.d.ts +1 -38
  98. package/dist/diagnostics/diagnose-network-error.d.ts.map +1 -1
  99. package/dist/diagnostics/diagnose-network-error.js +26 -37
  100. package/dist/diagnostics/diagnose-network-error.js.map +1 -1
  101. package/dist/diagnostics/doctor.d.ts +1 -85
  102. package/dist/diagnostics/doctor.d.ts.map +1 -1
  103. package/dist/diagnostics/doctor.js +5 -90
  104. package/dist/diagnostics/doctor.js.map +1 -1
  105. package/dist/diagnostics/startup-self-probe.d.ts +3 -25
  106. package/dist/diagnostics/startup-self-probe.d.ts.map +1 -1
  107. package/dist/diagnostics/startup-self-probe.js +11 -48
  108. package/dist/diagnostics/startup-self-probe.js.map +1 -1
  109. package/dist/errors/hints.d.ts +0 -44
  110. package/dist/errors/hints.d.ts.map +1 -1
  111. package/dist/errors/hints.js +0 -32
  112. package/dist/errors/hints.js.map +1 -1
  113. package/dist/errors/mask.d.ts +0 -31
  114. package/dist/errors/mask.d.ts.map +1 -1
  115. package/dist/errors/mask.js +0 -32
  116. package/dist/errors/mask.js.map +1 -1
  117. package/dist/errors/redact-credentials.d.ts +2 -0
  118. package/dist/errors/redact-credentials.d.ts.map +1 -0
  119. package/dist/errors/redact-credentials.js +13 -0
  120. package/dist/errors/redact-credentials.js.map +1 -0
  121. package/dist/errors/sanitize.d.ts +0 -29
  122. package/dist/errors/sanitize.d.ts.map +1 -1
  123. package/dist/errors/sanitize.js +0 -40
  124. package/dist/errors/sanitize.js.map +1 -1
  125. package/dist/errors.d.ts +6 -17
  126. package/dist/errors.d.ts.map +1 -1
  127. package/dist/errors.js +16 -21
  128. package/dist/errors.js.map +1 -1
  129. package/dist/gateway/advanced-read-tool.d.ts +0 -32
  130. package/dist/gateway/advanced-read-tool.d.ts.map +1 -1
  131. package/dist/gateway/advanced-read-tool.js +43 -111
  132. package/dist/gateway/advanced-read-tool.js.map +1 -1
  133. package/dist/gateway/advanced-tool/discovery.d.ts +1 -12
  134. package/dist/gateway/advanced-tool/discovery.d.ts.map +1 -1
  135. package/dist/gateway/advanced-tool/discovery.js +15 -24
  136. package/dist/gateway/advanced-tool/discovery.js.map +1 -1
  137. package/dist/gateway/advanced-tool/domains.d.ts +7 -66
  138. package/dist/gateway/advanced-tool/domains.d.ts.map +1 -1
  139. package/dist/gateway/advanced-tool/domains.js +36 -80
  140. package/dist/gateway/advanced-tool/domains.js.map +1 -1
  141. package/dist/gateway/advanced-tool/execute.d.ts +0 -25
  142. package/dist/gateway/advanced-tool/execute.d.ts.map +1 -1
  143. package/dist/gateway/advanced-tool/execute.js +13 -49
  144. package/dist/gateway/advanced-tool/execute.js.map +1 -1
  145. package/dist/gateway/advanced-tool/index.d.ts +1 -32
  146. package/dist/gateway/advanced-tool/index.d.ts.map +1 -1
  147. package/dist/gateway/advanced-tool/index.js +1 -35
  148. package/dist/gateway/advanced-tool/index.js.map +1 -1
  149. package/dist/gateway/advanced-tool/register.d.ts +0 -14
  150. package/dist/gateway/advanced-tool/register.d.ts.map +1 -1
  151. package/dist/gateway/advanced-tool/register.js +22 -58
  152. package/dist/gateway/advanced-tool/register.js.map +1 -1
  153. package/dist/gateway/advanced-tool.d.ts +0 -11
  154. package/dist/gateway/advanced-tool.d.ts.map +1 -1
  155. package/dist/gateway/advanced-tool.js +0 -11
  156. package/dist/gateway/advanced-tool.js.map +1 -1
  157. package/dist/gateway/capturing-server.d.ts +3 -88
  158. package/dist/gateway/capturing-server.d.ts.map +1 -1
  159. package/dist/gateway/capturing-server.js +0 -77
  160. package/dist/gateway/capturing-server.js.map +1 -1
  161. package/dist/gateway/essentials.d.ts +1 -63
  162. package/dist/gateway/essentials.d.ts.map +1 -1
  163. package/dist/gateway/essentials.js +9 -178
  164. package/dist/gateway/essentials.js.map +1 -1
  165. package/dist/gateway/test-support.d.ts +0 -28
  166. package/dist/gateway/test-support.d.ts.map +1 -1
  167. package/dist/gateway/test-support.js +0 -29
  168. package/dist/gateway/test-support.js.map +1 -1
  169. package/dist/health-response.d.ts +2 -0
  170. package/dist/health-response.d.ts.map +1 -0
  171. package/dist/health-response.js +7 -0
  172. package/dist/health-response.js.map +1 -0
  173. package/dist/icon-data.js +0 -3
  174. package/dist/icon-data.js.map +1 -1
  175. package/dist/index.d.ts +7 -23
  176. package/dist/index.d.ts.map +1 -1
  177. package/dist/index.js +46 -130
  178. package/dist/index.js.map +1 -1
  179. package/dist/install-skill.d.ts +0 -44
  180. package/dist/install-skill.d.ts.map +1 -1
  181. package/dist/install-skill.js +5 -46
  182. package/dist/install-skill.js.map +1 -1
  183. package/dist/net/extra-headers.d.ts +0 -58
  184. package/dist/net/extra-headers.d.ts.map +1 -1
  185. package/dist/net/extra-headers.js +0 -67
  186. package/dist/net/extra-headers.js.map +1 -1
  187. package/dist/net/internal-host.d.ts +0 -31
  188. package/dist/net/internal-host.d.ts.map +1 -1
  189. package/dist/net/internal-host.js +7 -65
  190. package/dist/net/internal-host.js.map +1 -1
  191. package/dist/net/proxy-dispatcher.d.ts +0 -37
  192. package/dist/net/proxy-dispatcher.d.ts.map +1 -1
  193. package/dist/net/proxy-dispatcher.js +0 -40
  194. package/dist/net/proxy-dispatcher.js.map +1 -1
  195. package/dist/net/public-fetch.d.ts +53 -0
  196. package/dist/net/public-fetch.d.ts.map +1 -0
  197. package/dist/net/public-fetch.js +143 -0
  198. package/dist/net/public-fetch.js.map +1 -0
  199. package/dist/net/public-page-report.d.ts +26 -0
  200. package/dist/net/public-page-report.d.ts.map +1 -0
  201. package/dist/net/public-page-report.js +145 -0
  202. package/dist/net/public-page-report.js.map +1 -0
  203. package/dist/net/redirect-safe-fetch.d.ts +14 -0
  204. package/dist/net/redirect-safe-fetch.d.ts.map +1 -0
  205. package/dist/net/redirect-safe-fetch.js +61 -0
  206. package/dist/net/redirect-safe-fetch.js.map +1 -0
  207. package/dist/net/session-recovery-fetch.d.ts +7 -0
  208. package/dist/net/session-recovery-fetch.d.ts.map +1 -0
  209. package/dist/net/session-recovery-fetch.js +139 -0
  210. package/dist/net/session-recovery-fetch.js.map +1 -0
  211. package/dist/net/site-basic-auth.d.ts +1 -59
  212. package/dist/net/site-basic-auth.d.ts.map +1 -1
  213. package/dist/net/site-basic-auth.js +5 -94
  214. package/dist/net/site-basic-auth.js.map +1 -1
  215. package/dist/package-version.d.ts +2 -0
  216. package/dist/package-version.d.ts.map +1 -0
  217. package/dist/package-version.js +15 -0
  218. package/dist/package-version.js.map +1 -0
  219. package/dist/platform/detect.d.ts +0 -54
  220. package/dist/platform/detect.d.ts.map +1 -1
  221. package/dist/platform/detect.js +0 -53
  222. package/dist/platform/detect.js.map +1 -1
  223. package/dist/platform/index.d.ts +0 -100
  224. package/dist/platform/index.d.ts.map +1 -1
  225. package/dist/platform/index.js +0 -106
  226. package/dist/platform/index.js.map +1 -1
  227. package/dist/platform/joomla.d.ts +0 -35
  228. package/dist/platform/joomla.d.ts.map +1 -1
  229. package/dist/platform/joomla.js +0 -35
  230. package/dist/platform/joomla.js.map +1 -1
  231. package/dist/proxy/bridge.d.ts +36 -76
  232. package/dist/proxy/bridge.d.ts.map +1 -1
  233. package/dist/proxy/bridge.js +443 -136
  234. package/dist/proxy/bridge.js.map +1 -1
  235. package/dist/proxy/confirm-elicitation.d.ts +22 -0
  236. package/dist/proxy/confirm-elicitation.d.ts.map +1 -0
  237. package/dist/proxy/confirm-elicitation.js +170 -0
  238. package/dist/proxy/confirm-elicitation.js.map +1 -0
  239. package/dist/proxy/confirm-question-contract.d.ts +6 -0
  240. package/dist/proxy/confirm-question-contract.d.ts.map +1 -0
  241. package/dist/proxy/confirm-question-contract.js +9 -0
  242. package/dist/proxy/confirm-question-contract.js.map +1 -0
  243. package/dist/proxy/confirm-question.d.ts +2 -0
  244. package/dist/proxy/confirm-question.d.ts.map +1 -0
  245. package/dist/proxy/confirm-question.js +125 -0
  246. package/dist/proxy/confirm-question.js.map +1 -0
  247. package/dist/proxy/instructions.d.ts +4 -0
  248. package/dist/proxy/instructions.d.ts.map +1 -0
  249. package/dist/proxy/instructions.js +12 -0
  250. package/dist/proxy/instructions.js.map +1 -0
  251. package/dist/proxy/mode.d.ts +14 -9
  252. package/dist/proxy/mode.d.ts.map +1 -1
  253. package/dist/proxy/mode.js +58 -34
  254. package/dist/proxy/mode.js.map +1 -1
  255. package/dist/proxy/multi-site.d.ts +42 -0
  256. package/dist/proxy/multi-site.d.ts.map +1 -0
  257. package/dist/proxy/multi-site.js +134 -0
  258. package/dist/proxy/multi-site.js.map +1 -0
  259. package/dist/proxy/request-headers.d.ts +9 -0
  260. package/dist/proxy/request-headers.d.ts.map +1 -0
  261. package/dist/proxy/request-headers.js +17 -0
  262. package/dist/proxy/request-headers.js.map +1 -0
  263. package/dist/proxy/shadow-tools.d.ts +9 -0
  264. package/dist/proxy/shadow-tools.d.ts.map +1 -0
  265. package/dist/proxy/shadow-tools.js +42 -0
  266. package/dist/proxy/shadow-tools.js.map +1 -0
  267. package/dist/proxy/tools-list-cap.d.ts +7 -0
  268. package/dist/proxy/tools-list-cap.d.ts.map +1 -0
  269. package/dist/proxy/tools-list-cap.js +14 -0
  270. package/dist/proxy/tools-list-cap.js.map +1 -0
  271. package/dist/proxy/transport-uncertainty.d.ts +2 -0
  272. package/dist/proxy/transport-uncertainty.d.ts.map +1 -0
  273. package/dist/proxy/transport-uncertainty.js +8 -0
  274. package/dist/proxy/transport-uncertainty.js.map +1 -0
  275. package/dist/proxy/upstream-error.d.ts +7 -0
  276. package/dist/proxy/upstream-error.d.ts.map +1 -0
  277. package/dist/proxy/upstream-error.js +35 -0
  278. package/dist/proxy/upstream-error.js.map +1 -0
  279. package/dist/proxy/wire-dialect.d.ts +5 -0
  280. package/dist/proxy/wire-dialect.d.ts.map +1 -0
  281. package/dist/proxy/wire-dialect.js +33 -0
  282. package/dist/proxy/wire-dialect.js.map +1 -0
  283. package/dist/server.d.ts +14 -82
  284. package/dist/server.d.ts.map +1 -1
  285. package/dist/server.js +107 -203
  286. package/dist/server.js.map +1 -1
  287. package/dist/setup/banner-color.d.ts +13 -0
  288. package/dist/setup/banner-color.d.ts.map +1 -0
  289. package/dist/setup/banner-color.js +93 -0
  290. package/dist/setup/banner-color.js.map +1 -0
  291. package/dist/setup/banner.d.ts +22 -0
  292. package/dist/setup/banner.d.ts.map +1 -0
  293. package/dist/setup/banner.js +119 -0
  294. package/dist/setup/banner.js.map +1 -0
  295. package/dist/setup/ca-recovery.d.ts +20 -0
  296. package/dist/setup/ca-recovery.d.ts.map +1 -0
  297. package/dist/setup/ca-recovery.js +92 -0
  298. package/dist/setup/ca-recovery.js.map +1 -0
  299. package/dist/setup/closing.d.ts +15 -0
  300. package/dist/setup/closing.d.ts.map +1 -0
  301. package/dist/setup/closing.js +43 -0
  302. package/dist/setup/closing.js.map +1 -0
  303. package/dist/setup/corporate-ca.d.ts +16 -0
  304. package/dist/setup/corporate-ca.d.ts.map +1 -0
  305. package/dist/setup/corporate-ca.js +32 -0
  306. package/dist/setup/corporate-ca.js.map +1 -0
  307. package/dist/setup/host-platform.d.ts +8 -0
  308. package/dist/setup/host-platform.d.ts.map +1 -0
  309. package/dist/setup/host-platform.js +21 -0
  310. package/dist/setup/host-platform.js.map +1 -0
  311. package/dist/setup/install-configs.d.ts +18 -0
  312. package/dist/setup/install-configs.d.ts.map +1 -0
  313. package/dist/setup/install-configs.js +70 -0
  314. package/dist/setup/install-configs.js.map +1 -0
  315. package/dist/setup/multi-site-loop.d.ts +14 -0
  316. package/dist/setup/multi-site-loop.d.ts.map +1 -0
  317. package/dist/setup/multi-site-loop.js +20 -0
  318. package/dist/setup/multi-site-loop.js.map +1 -0
  319. package/dist/setup/phases.d.ts +18 -0
  320. package/dist/setup/phases.d.ts.map +1 -0
  321. package/dist/setup/phases.js +55 -0
  322. package/dist/setup/phases.js.map +1 -0
  323. package/dist/setup/registry-remedy.d.ts +12 -0
  324. package/dist/setup/registry-remedy.d.ts.map +1 -0
  325. package/dist/setup/registry-remedy.js +37 -0
  326. package/dist/setup/registry-remedy.js.map +1 -0
  327. package/dist/setup/resume.d.ts +28 -0
  328. package/dist/setup/resume.d.ts.map +1 -0
  329. package/dist/setup/resume.js +99 -0
  330. package/dist/setup/resume.js.map +1 -0
  331. package/dist/setup-ca-env.d.ts +0 -30
  332. package/dist/setup-ca-env.d.ts.map +1 -1
  333. package/dist/setup-ca-env.js +0 -28
  334. package/dist/setup-ca-env.js.map +1 -1
  335. package/dist/setup-ca-reprobe.d.ts +0 -25
  336. package/dist/setup-ca-reprobe.d.ts.map +1 -1
  337. package/dist/setup-ca-reprobe.js +0 -34
  338. package/dist/setup-ca-reprobe.js.map +1 -1
  339. package/dist/setup-cli-confirm.d.ts +0 -34
  340. package/dist/setup-cli-confirm.d.ts.map +1 -1
  341. package/dist/setup-cli-confirm.js +0 -36
  342. package/dist/setup-cli-confirm.js.map +1 -1
  343. package/dist/setup-cli.d.ts +6 -167
  344. package/dist/setup-cli.d.ts.map +1 -1
  345. package/dist/setup-cli.js +161 -173
  346. package/dist/setup-cli.js.map +1 -1
  347. package/dist/setup-npx-spec.d.ts +0 -36
  348. package/dist/setup-npx-spec.d.ts.map +1 -1
  349. package/dist/setup-npx-spec.js +0 -38
  350. package/dist/setup-npx-spec.js.map +1 -1
  351. package/dist/setup-prompts.d.ts +5 -70
  352. package/dist/setup-prompts.d.ts.map +1 -1
  353. package/dist/setup-prompts.js +67 -107
  354. package/dist/setup-prompts.js.map +1 -1
  355. package/dist/setup-token.d.ts +0 -34
  356. package/dist/setup-token.d.ts.map +1 -1
  357. package/dist/setup-token.js +0 -40
  358. package/dist/setup-token.js.map +1 -1
  359. package/dist/setup-wizard-defaults.d.ts +0 -36
  360. package/dist/setup-wizard-defaults.d.ts.map +1 -1
  361. package/dist/setup-wizard-defaults.js +0 -75
  362. package/dist/setup-wizard-defaults.js.map +1 -1
  363. package/dist/setup-wizard-handshake.d.ts +1 -19
  364. package/dist/setup-wizard-handshake.d.ts.map +1 -1
  365. package/dist/setup-wizard-handshake.js +8 -45
  366. package/dist/setup-wizard-handshake.js.map +1 -1
  367. package/dist/setup-wizard-types.d.ts +16 -209
  368. package/dist/setup-wizard-types.d.ts.map +1 -1
  369. package/dist/setup-wizard-types.js +0 -9
  370. package/dist/setup-wizard-types.js.map +1 -1
  371. package/dist/setup-wizard.d.ts +0 -28
  372. package/dist/setup-wizard.d.ts.map +1 -1
  373. package/dist/setup-wizard.js +167 -412
  374. package/dist/setup-wizard.js.map +1 -1
  375. package/dist/setup.d.ts +0 -15
  376. package/dist/setup.d.ts.map +1 -1
  377. package/dist/setup.js +0 -15
  378. package/dist/setup.js.map +1 -1
  379. package/dist/sites/bootstrap.d.ts +0 -38
  380. package/dist/sites/bootstrap.d.ts.map +1 -1
  381. package/dist/sites/bootstrap.js +0 -44
  382. package/dist/sites/bootstrap.js.map +1 -1
  383. package/dist/sites/cli/add-site.d.ts +0 -76
  384. package/dist/sites/cli/add-site.d.ts.map +1 -1
  385. package/dist/sites/cli/add-site.js +0 -71
  386. package/dist/sites/cli/add-site.js.map +1 -1
  387. package/dist/sites/cli/list-sites.d.ts +0 -31
  388. package/dist/sites/cli/list-sites.d.ts.map +1 -1
  389. package/dist/sites/cli/list-sites.js +0 -45
  390. package/dist/sites/cli/list-sites.js.map +1 -1
  391. package/dist/sites/cli/remove-site.d.ts +0 -37
  392. package/dist/sites/cli/remove-site.d.ts.map +1 -1
  393. package/dist/sites/cli/remove-site.js +0 -37
  394. package/dist/sites/cli/remove-site.js.map +1 -1
  395. package/dist/sites/cli/set-default.d.ts +0 -25
  396. package/dist/sites/cli/set-default.d.ts.map +1 -1
  397. package/dist/sites/cli/set-default.js +0 -22
  398. package/dist/sites/cli/set-default.js.map +1 -1
  399. package/dist/sites/cli/test-site.d.ts +0 -44
  400. package/dist/sites/cli/test-site.d.ts.map +1 -1
  401. package/dist/sites/cli/test-site.js +0 -39
  402. package/dist/sites/cli/test-site.js.map +1 -1
  403. package/dist/sites/client-pool.d.ts +11 -164
  404. package/dist/sites/client-pool.d.ts.map +1 -1
  405. package/dist/sites/client-pool.js +73 -194
  406. package/dist/sites/client-pool.js.map +1 -1
  407. package/dist/sites/config-problem.d.ts +2 -0
  408. package/dist/sites/config-problem.d.ts.map +1 -0
  409. package/dist/sites/config-problem.js +13 -0
  410. package/dist/sites/config-problem.js.map +1 -0
  411. package/dist/sites/env-bridge.d.ts +0 -69
  412. package/dist/sites/env-bridge.d.ts.map +1 -1
  413. package/dist/sites/env-bridge.js +2 -71
  414. package/dist/sites/env-bridge.js.map +1 -1
  415. package/dist/sites/op-inject.d.ts +23 -0
  416. package/dist/sites/op-inject.d.ts.map +1 -0
  417. package/dist/sites/op-inject.js +131 -0
  418. package/dist/sites/op-inject.js.map +1 -0
  419. package/dist/sites/paths.d.ts +0 -19
  420. package/dist/sites/paths.d.ts.map +1 -1
  421. package/dist/sites/paths.js +0 -19
  422. package/dist/sites/paths.js.map +1 -1
  423. package/dist/sites/probe.d.ts +1 -47
  424. package/dist/sites/probe.d.ts.map +1 -1
  425. package/dist/sites/probe.js +4 -46
  426. package/dist/sites/probe.js.map +1 -1
  427. package/dist/sites/registry.d.ts +4 -135
  428. package/dist/sites/registry.d.ts.map +1 -1
  429. package/dist/sites/registry.js +4 -98
  430. package/dist/sites/registry.js.map +1 -1
  431. package/dist/sites/schema.d.ts +3 -51
  432. package/dist/sites/schema.d.ts.map +1 -1
  433. package/dist/sites/schema.js +6 -52
  434. package/dist/sites/schema.js.map +1 -1
  435. package/dist/sites/secret-resolver.d.ts +6 -76
  436. package/dist/sites/secret-resolver.d.ts.map +1 -1
  437. package/dist/sites/secret-resolver.js +39 -104
  438. package/dist/sites/secret-resolver.js.map +1 -1
  439. package/dist/sites/store.d.ts +12 -69
  440. package/dist/sites/store.d.ts.map +1 -1
  441. package/dist/sites/store.js +37 -86
  442. package/dist/sites/store.js.map +1 -1
  443. package/dist/sites/substituted-registry.d.ts +11 -0
  444. package/dist/sites/substituted-registry.d.ts.map +1 -0
  445. package/dist/sites/substituted-registry.js +13 -0
  446. package/dist/sites/substituted-registry.js.map +1 -0
  447. package/dist/sites/tools/fetch-public-url.d.ts +4 -0
  448. package/dist/sites/tools/fetch-public-url.d.ts.map +1 -0
  449. package/dist/sites/tools/fetch-public-url.js +231 -0
  450. package/dist/sites/tools/fetch-public-url.js.map +1 -0
  451. package/dist/sites/tools/index.d.ts +2 -26
  452. package/dist/sites/tools/index.d.ts.map +1 -1
  453. package/dist/sites/tools/index.js +5 -28
  454. package/dist/sites/tools/index.js.map +1 -1
  455. package/dist/sites/tools/sites-list.d.ts +3 -47
  456. package/dist/sites/tools/sites-list.d.ts.map +1 -1
  457. package/dist/sites/tools/sites-list.js +65 -79
  458. package/dist/sites/tools/sites-list.js.map +1 -1
  459. package/dist/sites/tools/sites-overview.d.ts +4 -0
  460. package/dist/sites/tools/sites-overview.d.ts.map +1 -0
  461. package/dist/sites/tools/sites-overview.js +209 -0
  462. package/dist/sites/tools/sites-overview.js.map +1 -0
  463. package/dist/sites/tools/sites-test.d.ts +2 -45
  464. package/dist/sites/tools/sites-test.d.ts.map +1 -1
  465. package/dist/sites/tools/sites-test.js +0 -55
  466. package/dist/sites/tools/sites-test.js.map +1 -1
  467. package/dist/sites/tools/use-site.d.ts +2 -45
  468. package/dist/sites/tools/use-site.d.ts.map +1 -1
  469. package/dist/sites/tools/use-site.js +29 -78
  470. package/dist/sites/tools/use-site.js.map +1 -1
  471. package/dist/sites/unknown-name-result.d.ts +10 -0
  472. package/dist/sites/unknown-name-result.d.ts.map +1 -0
  473. package/dist/sites/unknown-name-result.js +13 -0
  474. package/dist/sites/unknown-name-result.js.map +1 -0
  475. package/dist/skill-instructions.d.ts +7 -0
  476. package/dist/skill-instructions.d.ts.map +1 -0
  477. package/dist/skill-instructions.js +84 -0
  478. package/dist/skill-instructions.js.map +1 -0
  479. package/dist/skill-loader.d.ts +0 -102
  480. package/dist/skill-loader.d.ts.map +1 -1
  481. package/dist/skill-loader.js +4 -142
  482. package/dist/skill-loader.js.map +1 -1
  483. package/dist/skill-manifest.d.ts +0 -20
  484. package/dist/skill-manifest.d.ts.map +1 -1
  485. package/dist/skill-manifest.js +1 -21
  486. package/dist/skill-manifest.js.map +1 -1
  487. package/dist/skill-topics.d.ts +19 -0
  488. package/dist/skill-topics.d.ts.map +1 -0
  489. package/dist/skill-topics.js +181 -0
  490. package/dist/skill-topics.js.map +1 -0
  491. package/dist/tools/active-site.d.ts +16 -0
  492. package/dist/tools/active-site.d.ts.map +1 -0
  493. package/dist/tools/active-site.js +55 -0
  494. package/dist/tools/active-site.js.map +1 -0
  495. package/dist/tools/bind-guidance.d.ts +2 -72
  496. package/dist/tools/bind-guidance.d.ts.map +1 -1
  497. package/dist/tools/bind-guidance.js +2 -72
  498. package/dist/tools/bind-guidance.js.map +1 -1
  499. package/dist/tools/budgeted-table.d.ts +14 -19
  500. package/dist/tools/budgeted-table.d.ts.map +1 -1
  501. package/dist/tools/budgeted-table.js +171 -105
  502. package/dist/tools/budgeted-table.js.map +1 -1
  503. package/dist/tools/elements/bind-output-schema.d.ts +69 -0
  504. package/dist/tools/elements/bind-output-schema.d.ts.map +1 -0
  505. package/dist/tools/elements/bind-output-schema.js +64 -0
  506. package/dist/tools/elements/bind-output-schema.js.map +1 -0
  507. package/dist/tools/elements/builders.d.ts +1 -22
  508. package/dist/tools/elements/builders.d.ts.map +1 -1
  509. package/dist/tools/elements/builders.js +147 -133
  510. package/dist/tools/elements/builders.js.map +1 -1
  511. package/dist/tools/elements/index.d.ts +0 -26
  512. package/dist/tools/elements/index.d.ts.map +1 -1
  513. package/dist/tools/elements/index.js +0 -25
  514. package/dist/tools/elements/index.js.map +1 -1
  515. package/dist/tools/elements/read-output-schemas.d.ts +121 -0
  516. package/dist/tools/elements/read-output-schemas.d.ts.map +1 -0
  517. package/dist/tools/elements/read-output-schemas.js +225 -0
  518. package/dist/tools/elements/read-output-schemas.js.map +1 -0
  519. package/dist/tools/elements/write-output-schemas.d.ts +185 -0
  520. package/dist/tools/elements/write-output-schemas.d.ts.map +1 -0
  521. package/dist/tools/elements/write-output-schemas.js +98 -0
  522. package/dist/tools/elements/write-output-schemas.js.map +1 -0
  523. package/dist/tools/elements.d.ts +0 -11
  524. package/dist/tools/elements.d.ts.map +1 -1
  525. package/dist/tools/elements.js +0 -11
  526. package/dist/tools/elements.js.map +1 -1
  527. package/dist/tools/format/health-format.d.ts +26 -43
  528. package/dist/tools/format/health-format.d.ts.map +1 -1
  529. package/dist/tools/format/health-format.js +28 -60
  530. package/dist/tools/format/health-format.js.map +1 -1
  531. package/dist/tools/format/inspection-format.d.ts +2 -54
  532. package/dist/tools/format/inspection-format.d.ts.map +1 -1
  533. package/dist/tools/format/inspection-format.js +33 -119
  534. package/dist/tools/format/inspection-format.js.map +1 -1
  535. package/dist/tools/format/pages-format.d.ts +32 -69
  536. package/dist/tools/format/pages-format.d.ts.map +1 -1
  537. package/dist/tools/format/pages-format.js +114 -139
  538. package/dist/tools/format/pages-format.js.map +1 -1
  539. package/dist/tools/format/sources-format.d.ts +5 -39
  540. package/dist/tools/format/sources-format.d.ts.map +1 -1
  541. package/dist/tools/format/sources-format.js +57 -87
  542. package/dist/tools/format/sources-format.js.map +1 -1
  543. package/dist/tools/health-output-schemas.d.ts +82 -0
  544. package/dist/tools/health-output-schemas.d.ts.map +1 -0
  545. package/dist/tools/health-output-schemas.js +130 -0
  546. package/dist/tools/health-output-schemas.js.map +1 -0
  547. package/dist/tools/health.d.ts +2 -20
  548. package/dist/tools/health.d.ts.map +1 -1
  549. package/dist/tools/health.js +46 -232
  550. package/dist/tools/health.js.map +1 -1
  551. package/dist/tools/index.d.ts +1 -26
  552. package/dist/tools/index.d.ts.map +1 -1
  553. package/dist/tools/index.js +12 -39
  554. package/dist/tools/index.js.map +1 -1
  555. package/dist/tools/inspection.d.ts +4 -34
  556. package/dist/tools/inspection.d.ts.map +1 -1
  557. package/dist/tools/inspection.js +164 -536
  558. package/dist/tools/inspection.js.map +1 -1
  559. package/dist/tools/library.d.ts +115 -127
  560. package/dist/tools/library.d.ts.map +1 -1
  561. package/dist/tools/library.js +706 -454
  562. package/dist/tools/library.js.map +1 -1
  563. package/dist/tools/local-content/builders.d.ts +1 -17
  564. package/dist/tools/local-content/builders.d.ts.map +1 -1
  565. package/dist/tools/local-content/builders.js +103 -46
  566. package/dist/tools/local-content/builders.js.map +1 -1
  567. package/dist/tools/local-content/index.d.ts +0 -15
  568. package/dist/tools/local-content/index.d.ts.map +1 -1
  569. package/dist/tools/local-content/index.js +0 -14
  570. package/dist/tools/local-content/index.js.map +1 -1
  571. package/dist/tools/local-content/schemas.d.ts +31 -47
  572. package/dist/tools/local-content/schemas.d.ts.map +1 -1
  573. package/dist/tools/local-content/schemas.js +33 -62
  574. package/dist/tools/local-content/schemas.js.map +1 -1
  575. package/dist/tools/modules/builders.d.ts +4 -0
  576. package/dist/tools/modules/builders.d.ts.map +1 -0
  577. package/dist/tools/modules/builders.js +58 -0
  578. package/dist/tools/modules/builders.js.map +1 -0
  579. package/dist/tools/modules/index.d.ts +2 -0
  580. package/dist/tools/modules/index.d.ts.map +1 -0
  581. package/dist/tools/modules/index.js +2 -0
  582. package/dist/tools/modules/index.js.map +1 -0
  583. package/dist/tools/modules/module-id-schema.d.ts +4 -0
  584. package/dist/tools/modules/module-id-schema.d.ts.map +1 -0
  585. package/dist/tools/modules/module-id-schema.js +14 -0
  586. package/dist/tools/modules/module-id-schema.js.map +1 -0
  587. package/dist/tools/modules/module-type-fields.d.ts +10 -0
  588. package/dist/tools/modules/module-type-fields.d.ts.map +1 -0
  589. package/dist/tools/modules/module-type-fields.js +34 -0
  590. package/dist/tools/modules/module-type-fields.js.map +1 -0
  591. package/dist/tools/modules/modules-create.d.ts +59 -0
  592. package/dist/tools/modules/modules-create.d.ts.map +1 -0
  593. package/dist/tools/modules/modules-create.js +154 -0
  594. package/dist/tools/modules/modules-create.js.map +1 -0
  595. package/dist/tools/modules/modules-delete.d.ts +51 -0
  596. package/dist/tools/modules/modules-delete.d.ts.map +1 -0
  597. package/dist/tools/modules/modules-delete.js +180 -0
  598. package/dist/tools/modules/modules-delete.js.map +1 -0
  599. package/dist/tools/modules/modules-update.d.ts +43 -0
  600. package/dist/tools/modules/modules-update.d.ts.map +1 -0
  601. package/dist/tools/modules/modules-update.js +177 -0
  602. package/dist/tools/modules/modules-update.js.map +1 -0
  603. package/dist/tools/modules/placement-hints.d.ts +5 -0
  604. package/dist/tools/modules/placement-hints.d.ts.map +1 -0
  605. package/dist/tools/modules/placement-hints.js +90 -0
  606. package/dist/tools/modules/placement-hints.js.map +1 -0
  607. package/dist/tools/modules/placement-ways.d.ts +13 -0
  608. package/dist/tools/modules/placement-ways.d.ts.map +1 -0
  609. package/dist/tools/modules/placement-ways.js +61 -0
  610. package/dist/tools/modules/placement-ways.js.map +1 -0
  611. package/dist/tools/multi-items/builders.d.ts +1 -21
  612. package/dist/tools/multi-items/builders.d.ts.map +1 -1
  613. package/dist/tools/multi-items/builders.js +22 -65
  614. package/dist/tools/multi-items/builders.js.map +1 -1
  615. package/dist/tools/multi-items/index.d.ts +0 -15
  616. package/dist/tools/multi-items/index.d.ts.map +1 -1
  617. package/dist/tools/multi-items/index.js +0 -15
  618. package/dist/tools/multi-items/index.js.map +1 -1
  619. package/dist/tools/multi-items/item-container-map.d.ts +0 -35
  620. package/dist/tools/multi-items/item-container-map.d.ts.map +1 -1
  621. package/dist/tools/multi-items/item-container-map.js +0 -35
  622. package/dist/tools/multi-items/item-container-map.js.map +1 -1
  623. package/dist/tools/multi-items/output-schemas.d.ts +101 -0
  624. package/dist/tools/multi-items/output-schemas.d.ts.map +1 -0
  625. package/dist/tools/multi-items/output-schemas.js +74 -0
  626. package/dist/tools/multi-items/output-schemas.js.map +1 -0
  627. package/dist/tools/navigation/builders.d.ts +1 -16
  628. package/dist/tools/navigation/builders.d.ts.map +1 -1
  629. package/dist/tools/navigation/builders.js +5 -27
  630. package/dist/tools/navigation/builders.js.map +1 -1
  631. package/dist/tools/navigation/index.d.ts +0 -13
  632. package/dist/tools/navigation/index.d.ts.map +1 -1
  633. package/dist/tools/navigation/index.js +0 -12
  634. package/dist/tools/navigation/index.js.map +1 -1
  635. package/dist/tools/navigation/schemas.d.ts +0 -30
  636. package/dist/tools/navigation/schemas.d.ts.map +1 -1
  637. package/dist/tools/navigation/schemas.js +0 -30
  638. package/dist/tools/navigation/schemas.js.map +1 -1
  639. package/dist/tools/output-schema-metadata.d.ts +5 -0
  640. package/dist/tools/output-schema-metadata.d.ts.map +1 -0
  641. package/dist/tools/output-schema-metadata.js +83 -0
  642. package/dist/tools/output-schema-metadata.js.map +1 -0
  643. package/dist/tools/pages/builders.d.ts +1 -41
  644. package/dist/tools/pages/builders.d.ts.map +1 -1
  645. package/dist/tools/pages/builders.js +320 -405
  646. package/dist/tools/pages/builders.js.map +1 -1
  647. package/dist/tools/pages/header-transparency.d.ts +2 -32
  648. package/dist/tools/pages/header-transparency.d.ts.map +1 -1
  649. package/dist/tools/pages/header-transparency.js +9 -43
  650. package/dist/tools/pages/header-transparency.js.map +1 -1
  651. package/dist/tools/pages/index.d.ts +1 -19
  652. package/dist/tools/pages/index.d.ts.map +1 -1
  653. package/dist/tools/pages/index.js +1 -19
  654. package/dist/tools/pages/index.js.map +1 -1
  655. package/dist/tools/pages/schemas.d.ts +323 -241
  656. package/dist/tools/pages/schemas.d.ts.map +1 -1
  657. package/dist/tools/pages/schemas.js +514 -549
  658. package/dist/tools/pages/schemas.js.map +1 -1
  659. package/dist/tools/pages/write-output-schemas.d.ts +139 -0
  660. package/dist/tools/pages/write-output-schemas.d.ts.map +1 -0
  661. package/dist/tools/pages/write-output-schemas.js +138 -0
  662. package/dist/tools/pages/write-output-schemas.js.map +1 -0
  663. package/dist/tools/pages.d.ts +0 -11
  664. package/dist/tools/pages.d.ts.map +1 -1
  665. package/dist/tools/pages.js +0 -11
  666. package/dist/tools/pages.js.map +1 -1
  667. package/dist/tools/pool-resolve-helper.d.ts +0 -52
  668. package/dist/tools/pool-resolve-helper.d.ts.map +1 -1
  669. package/dist/tools/pool-resolve-helper.js +29 -104
  670. package/dist/tools/pool-resolve-helper.js.map +1 -1
  671. package/dist/tools/progress-phases.d.ts +0 -42
  672. package/dist/tools/progress-phases.d.ts.map +1 -1
  673. package/dist/tools/progress-phases.js +0 -44
  674. package/dist/tools/progress-phases.js.map +1 -1
  675. package/dist/tools/response-budget.d.ts +0 -69
  676. package/dist/tools/response-budget.d.ts.map +1 -1
  677. package/dist/tools/response-budget.js +0 -127
  678. package/dist/tools/response-budget.js.map +1 -1
  679. package/dist/tools/shared-schemas.d.ts +11 -65
  680. package/dist/tools/shared-schemas.d.ts.map +1 -1
  681. package/dist/tools/shared-schemas.js +40 -123
  682. package/dist/tools/shared-schemas.js.map +1 -1
  683. package/dist/tools/skill/builders.d.ts +3 -0
  684. package/dist/tools/skill/builders.d.ts.map +1 -0
  685. package/dist/tools/skill/builders.js +81 -0
  686. package/dist/tools/skill/builders.js.map +1 -0
  687. package/dist/tools/sources/builders.d.ts +1 -11
  688. package/dist/tools/sources/builders.d.ts.map +1 -1
  689. package/dist/tools/sources/builders.js +194 -170
  690. package/dist/tools/sources/builders.js.map +1 -1
  691. package/dist/tools/sources/index.d.ts +0 -17
  692. package/dist/tools/sources/index.d.ts.map +1 -1
  693. package/dist/tools/sources/index.js +0 -16
  694. package/dist/tools/sources/index.js.map +1 -1
  695. package/dist/tools/sources/read-output-schemas.d.ts +101 -0
  696. package/dist/tools/sources/read-output-schemas.d.ts.map +1 -0
  697. package/dist/tools/sources/read-output-schemas.js +105 -0
  698. package/dist/tools/sources/read-output-schemas.js.map +1 -0
  699. package/dist/tools/sources/source-name-argument.d.ts +17 -0
  700. package/dist/tools/sources/source-name-argument.d.ts.map +1 -0
  701. package/dist/tools/sources/source-name-argument.js +64 -0
  702. package/dist/tools/sources/source-name-argument.js.map +1 -0
  703. package/dist/tools/sources/write-output-schemas.d.ts +100 -0
  704. package/dist/tools/sources/write-output-schemas.d.ts.map +1 -0
  705. package/dist/tools/sources/write-output-schemas.js +61 -0
  706. package/dist/tools/sources/write-output-schemas.js.map +1 -0
  707. package/dist/tools/sources.d.ts +0 -12
  708. package/dist/tools/sources.d.ts.map +1 -1
  709. package/dist/tools/sources.js +0 -12
  710. package/dist/tools/sources.js.map +1 -1
  711. package/dist/tools/sparse-fields.d.ts +16 -232
  712. package/dist/tools/sparse-fields.d.ts.map +1 -1
  713. package/dist/tools/sparse-fields.js +178 -242
  714. package/dist/tools/sparse-fields.js.map +1 -1
  715. package/dist/tools/strict-input.d.ts +5 -0
  716. package/dist/tools/strict-input.d.ts.map +1 -0
  717. package/dist/tools/strict-input.js +55 -0
  718. package/dist/tools/strict-input.js.map +1 -0
  719. package/dist/tools/sublayout/builders.d.ts +1 -21
  720. package/dist/tools/sublayout/builders.d.ts.map +1 -1
  721. package/dist/tools/sublayout/builders.js +10 -39
  722. package/dist/tools/sublayout/builders.js.map +1 -1
  723. package/dist/tools/sublayout/index.d.ts +1 -11
  724. package/dist/tools/sublayout/index.d.ts.map +1 -1
  725. package/dist/tools/sublayout/index.js +1 -11
  726. package/dist/tools/sublayout/index.js.map +1 -1
  727. package/dist/tools/sublayout/output-schemas.d.ts +57 -0
  728. package/dist/tools/sublayout/output-schemas.d.ts.map +1 -0
  729. package/dist/tools/sublayout/output-schemas.js +51 -0
  730. package/dist/tools/sublayout/output-schemas.js.map +1 -0
  731. package/dist/tools/tool-builder/annotations.d.ts +1 -49
  732. package/dist/tools/tool-builder/annotations.d.ts.map +1 -1
  733. package/dist/tools/tool-builder/annotations.js +9 -49
  734. package/dist/tools/tool-builder/annotations.js.map +1 -1
  735. package/dist/tools/tool-builder/confirm-preview.d.ts +24 -0
  736. package/dist/tools/tool-builder/confirm-preview.d.ts.map +1 -0
  737. package/dist/tools/tool-builder/confirm-preview.js +37 -0
  738. package/dist/tools/tool-builder/confirm-preview.js.map +1 -0
  739. package/dist/tools/tool-builder/define.d.ts +6 -26
  740. package/dist/tools/tool-builder/define.d.ts.map +1 -1
  741. package/dist/tools/tool-builder/define.js +0 -25
  742. package/dist/tools/tool-builder/define.js.map +1 -1
  743. package/dist/tools/tool-builder/index.d.ts +3 -26
  744. package/dist/tools/tool-builder/index.d.ts.map +1 -1
  745. package/dist/tools/tool-builder/index.js +2 -25
  746. package/dist/tools/tool-builder/index.js.map +1 -1
  747. package/dist/tools/tool-builder/results.d.ts +4 -124
  748. package/dist/tools/tool-builder/results.d.ts.map +1 -1
  749. package/dist/tools/tool-builder/results.js +258 -263
  750. package/dist/tools/tool-builder/results.js.map +1 -1
  751. package/dist/tools/tool-builder/types.d.ts +5 -59
  752. package/dist/tools/tool-builder/types.d.ts.map +1 -1
  753. package/dist/tools/tool-builder/types.js +0 -7
  754. package/dist/tools/tool-builder/types.js.map +1 -1
  755. package/dist/tools/tool-builder.d.ts +0 -14
  756. package/dist/tools/tool-builder.d.ts.map +1 -1
  757. package/dist/tools/tool-builder.js +0 -14
  758. package/dist/tools/tool-builder.js.map +1 -1
  759. package/dist/uninstall-skill.d.ts +0 -41
  760. package/dist/uninstall-skill.d.ts.map +1 -1
  761. package/dist/uninstall-skill.js +0 -49
  762. package/dist/uninstall-skill.js.map +1 -1
  763. package/dist/uninstall.d.ts +0 -41
  764. package/dist/uninstall.d.ts.map +1 -1
  765. package/dist/uninstall.js +1 -29
  766. package/dist/uninstall.js.map +1 -1
  767. package/dist/update/update-check.d.ts +0 -26
  768. package/dist/update/update-check.d.ts.map +1 -1
  769. package/dist/update/update-check.js +0 -25
  770. package/dist/update/update-check.js.map +1 -1
  771. package/manifest.json +4 -5
  772. package/package.json +4 -4
  773. package/skills/yt-builder-mcp/SKILL.md +1857 -400
  774. package/dist/tools/elements/handlers-write.d.ts +0 -89
  775. package/dist/tools/elements/handlers-write.d.ts.map +0 -1
  776. package/dist/tools/elements/handlers-write.js +0 -797
  777. package/dist/tools/elements/handlers-write.js.map +0 -1
  778. package/dist/tools/elements/handlers.d.ts +0 -130
  779. package/dist/tools/elements/handlers.d.ts.map +0 -1
  780. package/dist/tools/elements/handlers.js +0 -581
  781. package/dist/tools/elements/handlers.js.map +0 -1
  782. package/dist/tools/elements/layout-hierarchy-rules.d.ts +0 -62
  783. package/dist/tools/elements/layout-hierarchy-rules.d.ts.map +0 -1
  784. package/dist/tools/elements/layout-hierarchy-rules.js +0 -154
  785. package/dist/tools/elements/layout-hierarchy-rules.js.map +0 -1
  786. package/dist/tools/elements/schema-validation.d.ts +0 -218
  787. package/dist/tools/elements/schema-validation.d.ts.map +0 -1
  788. package/dist/tools/elements/schema-validation.js +0 -685
  789. package/dist/tools/elements/schema-validation.js.map +0 -1
  790. package/dist/tools/elicitation.d.ts +0 -87
  791. package/dist/tools/elicitation.d.ts.map +0 -1
  792. package/dist/tools/elicitation.js +0 -100
  793. package/dist/tools/elicitation.js.map +0 -1
  794. package/dist/tools/format/elements-format.d.ts +0 -75
  795. package/dist/tools/format/elements-format.d.ts.map +0 -1
  796. package/dist/tools/format/elements-format.js +0 -368
  797. package/dist/tools/format/elements-format.js.map +0 -1
  798. package/dist/tools/format/source-schema-format.d.ts +0 -74
  799. package/dist/tools/format/source-schema-format.d.ts.map +0 -1
  800. package/dist/tools/format/source-schema-format.js +0 -208
  801. package/dist/tools/format/source-schema-format.js.map +0 -1
  802. package/dist/tools/layout-flatten.d.ts +0 -84
  803. package/dist/tools/layout-flatten.d.ts.map +0 -1
  804. package/dist/tools/layout-flatten.js +0 -129
  805. package/dist/tools/layout-flatten.js.map +0 -1
  806. package/dist/tools/local-content/handlers.d.ts +0 -56
  807. package/dist/tools/local-content/handlers.d.ts.map +0 -1
  808. package/dist/tools/local-content/handlers.js +0 -320
  809. package/dist/tools/local-content/handlers.js.map +0 -1
  810. package/dist/tools/multi-items/clean-implode-handler.d.ts +0 -37
  811. package/dist/tools/multi-items/clean-implode-handler.d.ts.map +0 -1
  812. package/dist/tools/multi-items/clean-implode-handler.js +0 -65
  813. package/dist/tools/multi-items/clean-implode-handler.js.map +0 -1
  814. package/dist/tools/multi-items/format/inspection-format.d.ts +0 -41
  815. package/dist/tools/multi-items/format/inspection-format.d.ts.map +0 -1
  816. package/dist/tools/multi-items/format/inspection-format.js +0 -64
  817. package/dist/tools/multi-items/format/inspection-format.js.map +0 -1
  818. package/dist/tools/multi-items/inspect-handler.d.ts +0 -97
  819. package/dist/tools/multi-items/inspect-handler.d.ts.map +0 -1
  820. package/dist/tools/multi-items/inspect-handler.js +0 -323
  821. package/dist/tools/multi-items/inspect-handler.js.map +0 -1
  822. package/dist/tools/multi-items/inspection-cache.d.ts +0 -85
  823. package/dist/tools/multi-items/inspection-cache.d.ts.map +0 -1
  824. package/dist/tools/multi-items/inspection-cache.js +0 -126
  825. package/dist/tools/multi-items/inspection-cache.js.map +0 -1
  826. package/dist/tools/navigation/handlers.d.ts +0 -30
  827. package/dist/tools/navigation/handlers.d.ts.map +0 -1
  828. package/dist/tools/navigation/handlers.js +0 -41
  829. package/dist/tools/navigation/handlers.js.map +0 -1
  830. package/dist/tools/pages/handlers-audit.d.ts +0 -140
  831. package/dist/tools/pages/handlers-audit.d.ts.map +0 -1
  832. package/dist/tools/pages/handlers-audit.js +0 -714
  833. package/dist/tools/pages/handlers-audit.js.map +0 -1
  834. package/dist/tools/pages/handlers-read.d.ts +0 -133
  835. package/dist/tools/pages/handlers-read.d.ts.map +0 -1
  836. package/dist/tools/pages/handlers-read.js +0 -1061
  837. package/dist/tools/pages/handlers-read.js.map +0 -1
  838. package/dist/tools/pages/handlers-write.d.ts +0 -212
  839. package/dist/tools/pages/handlers-write.d.ts.map +0 -1
  840. package/dist/tools/pages/handlers-write.js +0 -419
  841. package/dist/tools/pages/handlers-write.js.map +0 -1
  842. package/dist/tools/pages/target.d.ts +0 -162
  843. package/dist/tools/pages/target.d.ts.map +0 -1
  844. package/dist/tools/pages/target.js +0 -239
  845. package/dist/tools/pages/target.js.map +0 -1
  846. package/dist/tools/sources/handlers-bind.d.ts +0 -148
  847. package/dist/tools/sources/handlers-bind.d.ts.map +0 -1
  848. package/dist/tools/sources/handlers-bind.js +0 -657
  849. package/dist/tools/sources/handlers-bind.js.map +0 -1
  850. package/dist/tools/sources/handlers.d.ts +0 -116
  851. package/dist/tools/sources/handlers.d.ts.map +0 -1
  852. package/dist/tools/sources/handlers.js +0 -289
  853. package/dist/tools/sources/handlers.js.map +0 -1
  854. package/dist/tools/sublayout/inspect-sublayout-handler.d.ts +0 -95
  855. package/dist/tools/sublayout/inspect-sublayout-handler.d.ts.map +0 -1
  856. package/dist/tools/sublayout/inspect-sublayout-handler.js +0 -181
  857. package/dist/tools/sublayout/inspect-sublayout-handler.js.map +0 -1
@@ -12,18 +12,20 @@ description: Drive the YOOtheme Pro Page Builder on WordPress or Joomla 5/6. Dis
12
12
  > WordPress and Joomla 5/6.
13
13
 
14
14
  This skill helps AI assistants drive the YOOtheme Pro Page Builder through the
15
- `@wootsup/yt-builder-mcp` server. The server catalogues 53 typed, scoped,
16
- idempotent tools (55 callable end-to-end with the two gateways). 42 of them
15
+ `@wootsup/yt-builder-mcp` server. The server catalogues 60 typed, scoped
16
+ tools (62 callable end-to-end with the two gateways). 49 of them
17
17
  advertised as first-class entries in `tools/list`
18
- (38 essential L1 forwards + 2 direct L3 entries + 2 gateways), and 13 advanced
18
+ (45 essential L1 forwards + 2 direct L3 entries + 2 gateways), and 13 advanced
19
19
  tools reachable through the gateways. The write-capable
20
20
  `yootheme_builder_advanced` gateway routes all 13; the read-only
21
21
  `yootheme_builder_advanced_read` gateway routes only the read-only subset
22
- (page_get_schema, inspect_sublayout, layout_query, local_content_types,
22
+ (inspect_sublayout, layout_query, local_content_types,
23
23
  local_content_read, pages_audit, heading_outline, navigation) and is itself
24
24
  read-only-annotated, so MCP hosts can auto-allow pure inspection without a
25
- per-call write-approval prompt. This 3-lane split keeps `tools/list` well below
26
- the ~40-tool Cursor cap while every catalogued tool stays fully reachable.
25
+ per-call write-approval prompt. This 3-lane split exists because Cursor (and
26
+ similar hosts) cap a server at roughly 40 first-class `tools/list` entries —
27
+ it keeps the advertised surface small while every catalogued tool stays
28
+ fully reachable through the gateways.
27
29
 
28
30
  ## How to use this MCP server
29
31
 
@@ -54,7 +56,7 @@ Some clients (notably Claude Desktop with the `.mcpb` bundle) expose **two**
54
56
  entries when the user types "YT Builder MCP" into the picker:
55
57
 
56
58
  - **`YT Builder MCP for YOOtheme Pro (unofficial)`**: the MCP **server**.
57
- Provides the 42 first-class tools (38 essential, 2 direct, 2 gateways).
59
+ Provides the 49 first-class tools (45 essential, 2 direct, 2 gateways).
58
60
  The `yootheme_builder_advanced` gateway routes all 13 advanced tools; the
59
61
  read-only `yootheme_builder_advanced_read` gateway routes the read-only
60
62
  subset without a write-approval prompt.
@@ -82,39 +84,40 @@ The server exposes:
82
84
  - **2 direct top-level tools**, always callable, always in `tools/list`:
83
85
  `yootheme_builder_health` and `yootheme_builder_diagnose`. These are
84
86
  the "the gateway itself might be broken" escape hatch.
85
- - **38 essential forwarded tools**: common reads + the most-used writes
86
- (pages_list, pages_resolve, pages_create, pages_delete, page_dynamize, scaffold_section,
87
- get_etag, element_list / add / update_settings / set_status / get / move / clone / delete,
87
+ - **45 essential forwarded tools**: common reads + the most-used writes
88
+ (pages_list, pages_resolve, pages_create, pages_delete, pages_set_status, pages_restore, page_generate_dynamic, scaffold_section,
89
+ get_etag, get_skill, element_list / add / update_settings / set_status / get / move / clone / delete,
88
90
  page_get_layout, sources_list, source_get_schema, element_types_list, element_type_get_schema,
89
91
  template_summary, inspect_multi_items_binding, library_browse, library_get, library_insert,
90
- image_search, image_import,
92
+ image_search, image_import, modules_delete,
91
93
  sites_list, sites_test,
92
- use_site, the bind→verify→save→publish chain element_bind_source /
93
- element_get_binding / page_save / page_publish, and the archive-rebind /
94
- refactor surface render_preview / element_set_source / rename_source /
94
+ use_site, the bind→verify→save→publish→unbind chain element_bind_source /
95
+ element_get_binding / page_save / page_publish / element_unbind_source, and the archive-rebind /
96
+ refactor surface render_preview / element_set_source / repair_source_references /
95
97
  template_set_type). Always advertised in
96
98
  `tools/list` so AI clients see them first-class. `element_get_binding`
97
99
  (B4) is L1 so the verify half of a bind is a direct call, not a
98
100
  gateway hop. `source_get_schema` (U4-L1) is L1 so the archive/category
99
101
  binding entry-point — introspect a source's query methods + relations +
100
102
  the current-term archive-context convention before binding — is a direct
101
- call too. `render_preview`, `element_set_source`, `rename_source` and
102
- `template_set_type` (customer HANDOFF-4/5) are L1 so the core write/refactor +
103
+ call too. `render_preview`, `element_set_source`, `repair_source_references`
104
+ and `template_set_type` (customer HANDOFF-4/5) are L1 so the core write/refactor +
103
105
  render-verify tools an agent reaches for after a taxonomy/CPT rename — set a
104
106
  precomposed source at a pointer, render-verify the saved page is not silently
105
- blank, rename a source/field template-wide, re-type an orphaned archive/single
107
+ blank, repair a source/field binding REFERENCE template-wide (the registered
108
+ source itself is untouched), re-type an orphaned archive/single
106
109
  template — are direct calls, not undiscoverable gateway hops. `scaffold_section`
107
110
  creates a valid
108
111
  section → row → column hull in one call — the easiest correct way to
109
112
  start a layout block (content placed anywhere else is rejected by the
110
- structural-hierarchy guard). `library_browse` (Task 2, Layout Library) is L1
113
+ structural-hierarchy guard). `library_browse` (Layout Library) is L1
111
114
  so browsing the YOOtheme Pro Library catalog — pre-designed page layouts and
112
115
  element presets, filterable by topic/type/role/website/element_type/query —
113
116
  is a direct call before hand-building a section from scratch. `library_get`
114
- (Task 3, Layout Library) is L1 so fetching a SINGLE browsed item's full tree
117
+ (Layout Library) is L1 so fetching a SINGLE browsed item's full tree
115
118
  + derived section list (`{id, name, path, element_count}` per top-level
116
119
  section) — the natural next call once `library_browse` returns an `id` —
117
- is a direct call too. `library_insert` (Task 6, Layout Library) is L1 so
120
+ is a direct call too. `library_insert` (Layout Library) is L1 so
118
121
  composing + persisting a browsed/fetched item (layout, section, or preset)
119
122
  onto a page or existing template — target `new_page` / `{template_id,
120
123
  element_path}` / `sublayout` — through the SAME hardened write chain as
@@ -124,24 +127,98 @@ The server exposes:
124
127
  the chosen photo into the site's Media Library, are direct calls — the
125
128
  step that turns a placeholder-filled skeleton into a finished-looking
126
129
  page (see "Fill image slots with real on-topic photos" under Layout
127
- Library).
128
- - **13 advanced captured tools**: everything else (page_get_schema,
129
- element_unbind_source, clean_implode_directives,
130
+ Library). `get_skill` (Design E2, 2026-08-10) is L1 because it is the
131
+ Rettungsanker back to this document: the connect-time `instructions`
132
+ field now carries only a short curated preamble instead of the full
133
+ SKILL.md, so without this tool being directly callable a cold agent
134
+ would have no way to reach the rest of the guide. Call it without
135
+ `topic` for the list of chapters, or with `topic: "<slug>"` for one
136
+ chapter verbatim. `element_unbind_source` is L1 so the write half of
137
+ the bind pair is discoverable the same way its siblings
138
+ (`element_bind_source`, `element_get_binding`, `element_set_source`)
139
+ already are — a customer looking for "how do I remove a binding" could
140
+ otherwise not find it by name in `tools/list`, nor from any of the
141
+ other three tools' own descriptions. `pages_restore` is L1 for the same
142
+ discoverability reason: two cold-customer runs each measured the same gap
143
+ from opposite ends — a trashed page/article could not be found, and even
144
+ once found there was no correct way back — and one caller searched
145
+ explicitly for "restore"/"untrash"/"recover" and found nothing, because a
146
+ gateway-only placement is discoverable only to a caller who already knows
147
+ the tool exists.
148
+ - **13 advanced captured tools**: everything else (
149
+ clean_implode_directives,
130
150
  `inspect_sublayout`, `layout_query`, `library_save_as`, the two Local
131
151
  Content Read tools `local_content_types` / `local_content_read`,
132
152
  `pages_audit`, `library_delete` — the INVERSE of `library_save_as` —
133
153
  `heading_outline`, a real doc-order H1-H6 outline of one page,
134
- `navigation`, the site's menus as a parent -> children tree, and
135
- `header_transparency_set`, the transparent-header inspect/repair).
154
+ `navigation`, the site's menus as a parent -> children tree,
155
+ `header_transparency_set`, the transparent-header inspect/repair,
156
+ `modules_update`, the widget/module write surface, and
157
+ `modules_create`, the widget/module CREATE surface).
158
+ `modules_delete`, the widget/module DELETE surface, is first-class (L1) —
159
+ not one of these 14 (live-test-befunde, 2026-08-11): the one
160
+ gateway-only destructive tool that removes data OUTSIDE the layout tree (a
161
+ Joomla module row + its `#__modules_menu` assignments), so a host can gate
162
+ it individually instead of only via the all-or-nothing "Advanced Tool
163
+ Gateway" permission.
164
+ To remove a widget/module — the clean-up verb create/update lacked, so a
165
+ probe no longer leaves permanent litter on a customer site — pass the widget
166
+ `id` and PREVIEW first (`confirm` defaults FALSE and the call deletes
167
+ NOTHING, answering with the row plus `usage.element_refs` /
168
+ `usage.assigned_pages`):
169
+ `yootheme_builder_modules_delete({ id: "custom_html-2" })`,
170
+ then repeat with `confirm: true` AND an `intent` — a call missing or
171
+ misnaming it is refused with a 400 naming exactly which intents are
172
+ possible for the module's CURRENT state, each with its own
173
+ `effect`/`reversible`/`native_name`, so calling with neither `confirm` nor
174
+ `intent` doubles as that discovery step.
175
+ WHICH INTENTS ARE OFFERED DEPENDS ON THE PLATFORM AND STATE, because each
176
+ arm mirrors its own CMS rather than inventing a common one. `intent:
177
+ "permanent"` is offered on BOTH platforms and deletes the row for good —
178
+ irreversible, and `modules_create` makes a NEW id with default fields
179
+ rather than restoring this one. `intent: "trash"` exists ONLY on JOOMLA
180
+ and is REVERSIBLE: it sets `published = -2` — the module manager offers
181
+ `Trash` and no direct `Delete` at all, and its `Delete` button exists only
182
+ in the *Trashed* view. The row and its `#__modules_menu` assignments
183
+ survive and Joomla restores both; it does stop rendering at once (Joomla
184
+ loads modules with `published = 1`), so an element bound to it shows
185
+ nothing meanwhile. Joomla offers `"permanent"` directly too — trashing
186
+ first is no longer required to remove a module for good — but once a
187
+ module is ALREADY trashed, `"trash"` stops being offered (only
188
+ `"permanent"` empties it from there). On WORDPRESS widgets have no trash
189
+ step at all (`widgets.php` offers deactivate-by-drag and an immediate
190
+ `Delete` link, not a recoverable one), so only `"permanent"` is ever
191
+ offered there and the confirmed call is always the irreversible one. Read
192
+ the MEASURED `trashed` / `deleted` flags in the response rather than
193
+ assuming which happened.
194
+ Only wanted it off the page, not gone? That is not a removal at all, and
195
+ `modules_delete` is the wrong tool: `modules_update({ id, active: false })`
196
+ parks the widget in WordPress's **Inactive Widgets** area (settings stay
197
+ intact, dragging it back into a widget area restores it) or unpublishes the
198
+ module on Joomla (`published = 0`, position kept) — the same wording the
199
+ 400 response's `notes` gives when you call `modules_delete` without an
200
+ `intent`.
201
+ A layout element that references the id DIRECTLY
202
+ (`usage.element_refs` with `via:"element"`) BLOCKS the IRREVERSIBLE step with
203
+ a 409 naming every stranded location, until you either remove those elements
204
+ with `element_delete` or pass `acknowledge_element_refs`: true. Joomla's
205
+ reversible `"trash"` step is never refused — a restore puts the reference
206
+ back in working order — so on that platform the 409 can only come from
207
+ `intent: "permanent"`. An INDIRECT
208
+ reference (`via:"position"`, a `module_position` element naming the POSITION
209
+ this module sits in) never blocks, because it points at a position rather
210
+ than at this module and stays valid for whatever else occupies it.
211
+ `assigned_pages` never blocks either: a Joomla module's `#__modules_menu`
212
+ rows are its OWN rows and go with it.
136
213
  Reachable through one gateway tool:
137
214
  `yootheme_builder_advanced({ tool: "<name>", arguments: { ... } })`. For
138
215
  example, to inspect a panel/grid_item `< SUBLAYOUT`:
139
- `yootheme_builder_advanced({ tool: "yootheme_builder_inspect_sublayout", arguments: { template_id, element_path } })`.
216
+ `yootheme_builder_advanced({ tool: "yootheme_builder_inspect_sublayout", arguments: { layout_id, element_path } })`.
140
217
  And to find elements by a structured filter (G5 — no expression language,
141
- AND-combined `type`/`name_matches`/`has_binding`/`max_depth`/`under_section`):
142
- `yootheme_builder_advanced({ tool: "yootheme_builder_layout_query", arguments: { template_id, type: "headline", has_binding: false, under_section: "Hero" } })`
218
+ AND-combined `element_type`/`name_matches`/`has_binding`/`max_depth`/`under_section`):
219
+ `yootheme_builder_advanced({ tool: "yootheme_builder_layout_query", arguments: { layout_id, element_type: "headline", has_binding: false, under_section: "Hero" } })`
143
220
  (it's read-only, so the read gateway `yootheme_builder_advanced_read({ tool: "yootheme_builder_layout_query", … })` works too).
144
- And to save a page/subtree INTO "My Layouts" (Task 7 — the reverse of
221
+ And to save a page/subtree INTO "My Layouts" (the reverse of
145
222
  `library_insert`; omit `element_path` to save the whole page):
146
223
  `yootheme_builder_advanced({ tool: "yootheme_builder_library_save_as", arguments: { source: { template_id, element_path }, name: "Hero Section" } })`.
147
224
  It flags `invalid_source_possible: true` when the saved subtree carries
@@ -162,7 +239,7 @@ The server exposes:
162
239
  And to get a real, doc-order H1-H6 outline of ONE page (each heading with
163
240
  its source element and TEXT, plus the alt-status of every image) —
164
241
  read-only STRUCTURE inspection, not findings:
165
- `yootheme_builder_advanced_read({ tool: "yootheme_builder_heading_outline", arguments: { template_id } })`
242
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_heading_outline", arguments: { layout_id } })`
166
243
  (or via the write gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_heading_outline" })`).
167
244
  And to see the site's Haupt- + Sub-Menüstruktur as a parent -> children
168
245
  TREE (not a flat list) — for "which page sits under which menu item"
@@ -179,24 +256,55 @@ The server exposes:
179
256
  TRUE, so this call reports the current state, the theme-declared
180
257
  `--uk-inverse`, `risk_class`, `confidence` and `would_change` without
181
258
  touching anything:
182
- `yootheme_builder_advanced({ tool: "yootheme_builder_header_transparency_set", arguments: { template_id } })`
259
+ `yootheme_builder_advanced({ tool: "yootheme_builder_header_transparency_set", arguments: { layout_id } })`
183
260
  Repeat with `mode: "auto", dry_run: false, etag` to write the theme-declared
184
261
  colour. See "Transparent header (the exception — never a default)".
262
+ And to update a widget's (WordPress) or module's (Joomla) title,
263
+ position, or theme_overlay — a WRITE, so the write gateway only.
264
+ `dry_run` defaults to TRUE, so this call previews the diff without
265
+ touching anything:
266
+ `yootheme_builder_advanced({ tool: "yootheme_builder_modules_update", arguments: { id: "custom_html-2", title: "New Title" } })`
267
+ Repeat with `dry_run: false` to persist. See "Now change what you found —
268
+ `modules_update`" above for the platform-specific field split.
269
+ And to CREATE a new widget/module — one of exactly five known types
270
+ (WordPress custom_html/text/block; Joomla mod_custom/mod_menu) — a WRITE,
271
+ so the write gateway only. `placement` is REQUIRED (`position` /
272
+ `module_element` / `position_element` — how the module reaches a page); a
273
+ call without it creates NOTHING and returns BOTH halves of the answer:
274
+ `ways`, every way for this platform with its reach and a summary, and
275
+ `notes`, what `modules_create` can build and when you need no module at all.
276
+ Which ways additionally need a `position` is a PLATFORM fact: all three on
277
+ WordPress, two on Joomla. It is never defaulted on either. `dry_run` defaults to TRUE, so once `placement` is chosen this call
278
+ previews (type, position, fields) without creating anything:
279
+ `yootheme_builder_advanced({ tool: "yootheme_builder_modules_create", arguments: { type: "custom_html", title: "New Widget", content: "<p>Hi</p>", placement: "module_element", position: "builder-1" } })`
280
+ Repeat with `dry_run: false` to persist — the response's `row` is the full
281
+ `local_content_read(kind:'modules')` shape and `next_step` is the exact
282
+ follow-up call **for the placement you chose**, which is an `element_add`
283
+ for only two of the three: `module_element` and `position_element` get one,
284
+ `position` gets `modules_update{assignment}` on Joomla and a read-only
285
+ `local_content_read{kind:"modules", include_usage:true}` on WordPress — the
286
+ widget is already live site wide (no write to narrow it with), but add
287
+ `verify_on_urls` to that same call to PROVE guest-visible rendering rather
288
+ than trust `active`/`builder_visible` alone. See "Create → wire in (the
289
+ two-step recipe)" below.
185
290
 
186
291
  The core write/refactor + render-verify tools the customer HANDOFF-4/5 ask for
187
292
  are now **L1 — call them directly by name**, not through a gateway:
188
293
  - To render-check a saved page over the authenticated channel (F5+N5 — does
189
294
  it render, with how many items, or is it silently blank? where an external
190
295
  cURL 401s behind staging Basic-Auth), call the now-L1
191
- `yootheme_builder_render_preview({ template_id })` directly — or address it by
296
+ `yootheme_builder_render_preview({ layout_id })` directly — or address it by
192
297
  a public URL with `yootheme_builder_render_preview({ url })` (the URL is
193
298
  resolved to its template for you, exactly like `page_get_layout`), so if you
194
299
  only hold a resolved category/archive URL you can render-check it in ONE call
195
- without first converting it to a template_id. It takes no post/term target: it
300
+ without first converting it to a layout_id. It takes no post/term target: it
196
301
  renders YOOtheme's implicit fallback context, so a source-bound template comes
197
302
  back non-empty. Treat a non-empty render as the success signal, not "blank
198
- despite a binding". `item_count` counts the TOP-LEVEL repeat-source rows only
199
- (a static grid/list nested in a repeated card no longer inflates it). Pass
303
+ despite a binding". `rendered_item_wrapper_count` counts the TOP-LEVEL repeat-item
304
+ wrappers the render emitted, BOUND OR NOT (a static grid/list nested in a repeated
305
+ card does not inflate it); `source_item_counts` is the per-source ROW count,
306
+ and `unresolved_sources` names any bound source the registry does not recognise
307
+ (the render still succeeds, only that source contributes nothing). Pass
200
308
  `expect: "<substring>"` to confirm a specific edit reached the HTML in one call
201
309
  (the response adds `matched` + `snippet`).
202
310
  - To surgically SET a precomposed `source` + the native item-level
@@ -204,30 +312,52 @@ The server exposes:
204
312
  leaving the rest of the layout byte-unchanged (F2 — the one-call source edit
205
313
  bind_source can't do because it cannot write `source_extended`), call the
206
314
  now-L1
207
- `yootheme_builder_element_set_source({ template_id, element_path, source: { query: { name: "categoryFaqs.taxonomyCategoryFaq", field: { name: "faq" } } }, source_extended: { props: { title: { query: { name: "#node" }, name: "title" } } }, etag })` directly.
208
- - To template-wide RENAME a source/type/field name across EVERY binding in
209
- one ETag-locked write — after a taxonomy/CPT rename (faq→help) breaks every
210
- bound layout, preserving each binding's arguments + props + field_mappings
211
- (N1 — instead of a whole-layout page_save), call the now-L1
212
- `yootheme_builder_rename_source({ template_id, from: "categoryFaqs", to: "categoryHelps", source_map: { taxonomyCategoryFaq: "taxonomyCategoryHelp", faq: "help" }, field_map: { faq_title: "help_title" }, etag })` directly.
315
+ `yootheme_builder_element_set_source({ layout_id, element_path, source: { query: { name: "categoryFaqs.taxonomyCategoryFaq", field: { name: "faq" } } }, source_extended: { props: { title: { query: { name: "#node" }, name: "title" } } }, etag })` directly.
316
+ - To template-wide REPAIR binding REFERENCES to a source/type/field name
317
+ across EVERY binding in one template, in one ETag-locked write — after
318
+ that name changed OUTSIDE the builder (a taxonomy/CPT rename such as
319
+ faq→help, a re-published flow), preserving each binding's arguments +
320
+ props + field_mappings this does NOT rename the source itself,
321
+ `sources_list` stays unchanged; call the now-L1
322
+ `yootheme_builder_repair_source_references({ layout_id, from: "categoryFaqs", to: "categoryHelps", source_map: { taxonomyCategoryFaq: "taxonomyCategoryHelp", faq: "help" }, field_map: { faq_title: "help_title" }, etag })` directly.
323
+ `to` (and each `source_map` value landing at the top-level source
324
+ position) must be a currently registered source name — an unregistered
325
+ target is refused with the nearest matches, unless you pass
326
+ `allow_unregistered:true` for the case where the target only exists
327
+ after a flow republish.
328
+ That write is per-template, so find WHICH templates carry the binding FIRST —
329
+ `yootheme_builder_layout_query({ source_name: "categoryFaqs" })` with **no**
330
+ `layout_id` sweeps the whole site and answers per-source counts plus the
331
+ templates each one lives on (add `max_paths` for the individual element
332
+ paths). One call instead of one per template.
333
+ The sweep recognises a binding by the SAME rule `has_binding` uses per
334
+ template, so an unfiltered call splits its `total_bindings` three ways and
335
+ only the first is a source: `sources` (real source names — the ones
336
+ `sources_list` also knows), `inherited_context_bindings` (a `#`-prefixed
337
+ YOOtheme Pro self-reference such as `#parent`, i.e. a binding that INHERITS
338
+ the row context of an iterating ancestor — on real sites this is often the
339
+ majority of the count and it is NOT a source), and `unnamed_bindings`
340
+ (bound via field mappings or a query-field selector only, so there is no
341
+ name for `repair_source_references` to match). Repair covers the first
342
+ bucket; the third names the templates you still have to open by hand.
213
343
  - To reassign which archive/single a THEME template applies to — set its
214
344
  `type` condition (the value pages_list shows) in one ETag-locked write, the
215
345
  step a taxonomy rename leaves orphaned (N3 — re-type
216
346
  `taxonomy-category_faq` → `taxonomy-category_help`), call the now-L1
217
- `yootheme_builder_template_set_type({ template_id, type: "taxonomy-category_help", etag })` directly.
347
+ `yootheme_builder_template_set_type({ layout_id, type: "taxonomy-category_help", etag })` directly.
218
348
 
219
349
  To introspect a source's query methods + relations + the current-term
220
350
  archive-context convention (U4-L1 — turns a ~25-call YOOtheme code-dive into
221
351
  one call before binding on an archive/category template), call the now-L1
222
352
  `yootheme_builder_source_get_schema({ name: "categoryFaqs" })` directly.
223
353
  - **2 gateway tools**: `yootheme_builder_advanced` (write-capable; can route
224
- into delete/unbind, so hosts gate it) and `yootheme_builder_advanced_read`
225
- (read-only; routes ONLY the read-only advanced subset — page_get_schema,
354
+ into delete, so hosts gate it) and `yootheme_builder_advanced_read`
355
+ (read-only; routes ONLY the read-only advanced subset —
226
356
  inspect_sublayout, layout_query, local_content_types, local_content_read,
227
357
  pages_audit, heading_outline, navigation —
228
358
  and is read-only-annotated so hosts auto-allow it). Prefer the read gateway
229
359
  for pure inspection:
230
- `yootheme_builder_advanced_read({ tool: "yootheme_builder_page_get_schema", arguments: { template_id } })`.
360
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_pages_audit", arguments: { site_id } })`.
231
361
  Discover the site's NATIVE content the same way — the landscape then the records:
232
362
  `yootheme_builder_advanced_read({ tool: "yootheme_builder_local_content_types" })`
233
363
  then `yootheme_builder_advanced_read({ tool: "yootheme_builder_local_content_read", arguments: { kind: "terms", taxonomy: "category" } })`.
@@ -235,9 +365,9 @@ The server exposes:
235
365
  `yootheme_builder_advanced({ tool: "yootheme_builder_local_content_types" })` and
236
366
  `yootheme_builder_advanced({ tool: "yootheme_builder_local_content_read", arguments: { kind: "entities", type: "post" } })`.)
237
367
 
238
- `tools/list` therefore advertises 42 names (38 + 2 + 2). That's 38 L1
239
- essentials + 2 L3 direct + 2 gateways. The total callable surface is 55
240
- (42 advertised + 13 advanced reachable through the gateways). If the AI
368
+ `tools/list` therefore advertises 49 names (45 + 2 + 2). That's 45 L1
369
+ essentials + 2 L3 direct + 2 gateways. The total callable surface is 62
370
+ (49 advertised + 13 advanced reachable through the gateways). If the AI
241
371
  client reports "tool not found", you are almost certainly calling an
242
372
  advanced tool by its raw name. Wrap it in
243
373
  `yootheme_builder_advanced({ tool, arguments })` (or the read gateway for
@@ -248,10 +378,14 @@ read-only tools) instead.
248
378
  The server surfaces the connected site's URLs in two places so you never have to
249
379
  guess where the agent is pointing:
250
380
 
251
- - **`yootheme_builder_health` (Bearer-authenticated) and
252
- `yootheme_builder_diagnose`** return `site_url` and `home_url` for the
253
- connected install. Call one of them when the user asks "which site are you
254
- on?" or before deep-linking the user back into wp-admin / Joomla administrator.
381
+ - **`yootheme_builder_health` (Bearer-authenticated)** returns `site_url` for
382
+ the connected install, and on WordPress `home_url` as well, because WordPress
383
+ distinguishes the two; Joomla has a single site URL and sends only
384
+ `site_url`. Call it when the user asks "which site are you on?" or before
385
+ deep-linking the user back into wp-admin / Joomla administrator.
386
+ `yootheme_builder_diagnose` carries `site_url` too, so read it there when you
387
+ are already triaging an auth failure — but **only** `site_url`. `home_url` is
388
+ on health alone.
255
389
  - **`yootheme_builder_pages_list`** returns per-template `frontend_url`,
256
390
  `frontend_url_template`, and `frontend_url_description` columns when the
257
391
  host plugin can resolve them. Use these when the user asks for a verification
@@ -276,9 +410,37 @@ Do not hand-write a Joomla endpoint list from memory: the Joomla paths carry a
276
410
  `yt-builder-mcp` segment (`/v1/yt-builder-mcp/pages/audit`, not `/v1/pages/audit`)
277
411
  and the router rejects the short form.
278
412
 
413
+ If the field is **missing** from a health answer, the host could not read its own
414
+ route table for this request — read that as "not knowable here", never as "this
415
+ install serves nothing". It is omitted rather than sent as an empty list for
416
+ exactly that reason.
417
+
279
418
  Treat `frontend_url: null` as "host plugin could not resolve a public URL for
280
419
  this template". Surface that honestly rather than fabricating one.
281
420
 
421
+ ### What a write clears, and what it does not
422
+
423
+ `page_save` and `page_publish` say they flush caches. They flush **the caches
424
+ this plugin owns**, and that is a narrower set than "the caches on this site":
425
+
426
+ | layer | who clears it after a write |
427
+ |---|---|
428
+ | YOOtheme's own render / schema cache | this plugin |
429
+ | the option entries this plugin writes | this plugin |
430
+ | the CMS object-cache entry for the page itself | **the CMS core**, on its own post/article save — not this plugin |
431
+ | a full-page cache or CDN (WP Rocket, LiteSpeed, Cloudflare, a reverse proxy) | **nobody, from here** |
432
+
433
+ The last row is the one that reaches visitors. **No full-page-cache or CDN
434
+ plugin was measured**; what was measured is that the CMS core hooks such
435
+ plugins register on do fire during a write. That is an indication, not a
436
+ guarantee — and on Joomla there is no equivalent link at all.
437
+
438
+ So after a publish that must be visible immediately: purge the page cache / CDN
439
+ yourself, then prove it with `yootheme_builder_fetch_public_url`, which fetches
440
+ the page as an anonymous visitor and reports whether a cache served the answer.
441
+ `render_preview` cannot settle this — it renders the stored layout server-side
442
+ and never touches the visitor's path.
443
+
282
444
  ## Resolve a URL back to the template that renders it
283
445
 
284
446
  The two sections above go template → URL (forward). To go the OTHER way — the
@@ -287,18 +449,35 @@ complementary tools.
287
449
 
288
450
  **`yootheme_builder_pages_resolve({ url })` — the direct reverse lookup.** Paste
289
451
  any public frontend URL and it returns
290
- `{ template_id, post_id, type, frontend_url, context, resolved_via }` (or a
452
+ `{ layout_id, post_id, type, frontend_url, context, resolved_via }` (or a
291
453
  `url_not_resolvable` when it genuinely maps to no template — it degrades to an
292
454
  honest null rather than guessing a wrong one). It handles **SEF / pretty
293
455
  permalinks on BOTH WordPress and Joomla** (not just raw `?option=…&id=…` query
294
- URLs), including deep sub-category paths like `/matches/first-men-s-team`. For a
456
+ URLs), including deep sub-category paths like `/matches/first-men-s-team` and
457
+ the deep ARTICLE paths a Joomla site prints for an article routed below a
458
+ category menu item (`/company/blog/9-news/36-my-article`) — the id and the alias
459
+ in that last segment are both checked against the article, so an address whose
460
+ alias belongs to something else is refused rather than guessed. For a
295
461
  **category or term URL it returns the SPECIFICALLY-ASSIGNED template** — the one
296
462
  whose YOOtheme `query.catid` (Joomla) / `query.terms` (WordPress) binds THAT
297
463
  category/term, honoring **include-child-categories** (a template assigned to a
298
464
  parent category with child-inclusion covers its subcategories; the nearest
299
465
  specific assignment wins). Only when nothing is specifically assigned does it
300
466
  fall back to the governing generic theme template. Then feed the returned
301
- `template_id` straight into `page_get_layout` / `element_list` / the write tools.
467
+ `layout_id` straight into `page_get_layout` / `element_list` / the write tools.
468
+
469
+ **The site's own front page resolves too, in every spelling** — bare host,
470
+ trailing slash, `…/index.php`, and the subdirectory forms of an install served
471
+ from one (`https://example.com/joomla/index.php`). It resolves to whatever
472
+ renders it, which on Joomla is usually the single article the default menu item
473
+ points at. `pages_list` hands out one of those spellings as the front page's
474
+ `frontend_url`, so an address this server issues is one it accepts.
475
+
476
+ A front page that resolves to NOTHING is answered by name, not as
477
+ `url_not_resolvable`: the code is **`url_resolves_but_no_layout`** and the
478
+ message says which page was identified and why nothing renders it. Re-spelling
479
+ the URL will not help there — give that page a layout, or address the governing
480
+ template by `layout_id`.
302
481
 
303
482
  **`serves_context_kind` + `serves_context_ids` on `pages_list` rows — resolve it
304
483
  yourself, zero extra calls.** Every `pages_list` row also carries these two Layer-C
@@ -351,7 +530,7 @@ this page". Supply **exactly ONE of** these five keys:
351
530
 
352
531
  | Key | Value | Example |
353
532
  |-----|-------|---------|
354
- | `template_id` | An exact template id | `{ template_id: "page:4816" }` |
533
+ | `template_id` | An exact layout id | `{ template_id: "page:4816" }` |
355
534
  | `post_id` | The numeric WP post / Joomla article id | `{ post_id: 4816 }` |
356
535
  | `url` | A public frontend URL (same resolver as `pages_resolve({ url })`) | `{ url: "https://example.com/schedule-demo/" }` |
357
536
  | `special` | A semantic page ROLE (vocabulary below) | `{ special: "home" }` |
@@ -426,15 +605,18 @@ never a transport failure — report it verbatim rather than retrying blindly.
426
605
  | `unknown_audit_mode` | 400 | A `mode` value that is not `builder` or `rendered` (a typo, e.g. `render`). | Re-send with one of the two valid values. Refused rather than silently falling back to `builder`, for the same reason as `unknown_check_category`: nothing was scanned, so the previous answer is NOT a clean result. |
427
606
  | `unknown_heading_outline_mode` | 400 | The SAME `mode` vocabulary (`builder`/`rendered`), on `yootheme_builder_heading_outline` instead of `pages_audit` — a dedicated code so the two are never confused. | Re-send with `builder` or `rendered`. |
428
607
  | `no_template_matched_types` | 400 | A site-sweep `types` filter matched NO template on this site — a typo or, most often, a case variant (`PAGE` is not `page`). Unlike `checks`, the `types` vocabulary is **open and site-dependent** (`page`, `article`, `single-post`, `com_content.category`, …), so it cannot be validated against a fixed list — only a zero match is knowable, and it is knowable only after the sweep. | Re-send with one of the types the message lists (it names what this site actually has), or omit `types` to sweep everything. Refused for the same reason as `unknown_check_category`: a filter that selected nothing answered HTTP 200 with `findings: []` and an all-zero summary, which reads as CLEAN. **Never read such an answer as clean.** |
429
- | `template_not_found` | 404 | The `template_id` names no template carrying a YOOtheme layout on this site. | Re-read the id from `pages_list`; ids are site-specific and change when a page is recreated. |
608
+ | `insufficient_capability` | 403 | The object EXISTS and this key may not read it — a permission answer, not a missing page. The message names the object (`page:5040` / `article:42`). **These tools answer this way**, the ones that read ONE layout: `page_get_layout`, `layout_query` (single template), `element_list`, `element_get`, `element_get_binding`, `inspect_multi_items_binding`, `inspect_sublayout`, `render_preview`, `template_summary`, `heading_outline`. **The others do NOT**, on purpose: `pages_list`, `pages_resolve`, `pages_audit`, `get_etag` and a site-wide `layout_query` sweep report a forbidden object as absent, because a half-sharpened sweep is more misleading than an honestly lenient one. **Never read their silence about an object as proof that it does not exist.** | Do not repeat the call: it gives the same answer while the key stays the same. Ask an administrator to open the object to this key, or use a key whose subject can read it. See [Scopes](#scopes-bearer-key-permissions). |
609
+ | `template_not_found` | 404 | The `layout_id` names no template carrying a YOOtheme layout on this site. | Re-read the id from `pages_list`; ids are site-specific and change when a page is recreated. |
430
610
  | `post_not_found` | 404 | The `post_id` names no WP post / Joomla article with a per-page layout. | Same: confirm via `pages_list`. A post that exists but has no per-page layout is rendered by a THEME template — find that template instead. |
431
- | `url_no_match` | 404 | The URL is well-formed but resolves to no per-page layout usually because a theme template renders it. | Use `pages_list` to find the governing template, or `pages_resolve({ url })` for the fuller hint. |
611
+ | `url_no_match` | 404 | The URL is well-formed but this server could not map it to any page at all — neither a per-page layout nor an archive/term/author theme template matched. Note this is NOT the "page exists, has no layout" case: that one has its own code in the next row. | Check the address is this site's and names a public, singular page/article. A LIST view is usually reached with `target: { special: }` instead. Failing both, browse `pages_list`. |
612
+ | `url_resolves_but_no_layout` | 404 | The URL DID resolve to a page — WordPress mapped it to a post, this server read the Joomla article the address names, or the address is this site's front page — but that page carries no YOOtheme layout of its own; something else renders it. The message names the page. | Nothing here is fixed by re-spelling the address. Give that page a layout in the builder, or find the governing theme template in `pages_list` (its `serves_context_kind` column says which context each one serves) and address it by `layout_id`. |
432
613
  | `title_no_match` | 404 | No page NAME contains that substring. | Try a shorter substring, or list with `pages_list`. |
433
614
  | `special_not_resolvable` | 404 | The token is in the vocabulary and resolvable in principle, but matches no template on THIS site. | Report which role was requested; do not silently fall back to another page. |
434
615
  | `title_ambiguous` | 409 | The substring matches two or more pages. | A `candidates` list (max 10) rides along — show it and let the user choose. |
435
616
  | `blog_ambiguous` | 409 | Joomla only: several distinct blog pages. | Same `candidates` shape; show it (see the Joomla caveat above). |
436
617
  | `special_not_supported` | 422 | The token is recognised but this platform has no such page role (today: `date`, on both). | Say so plainly instead of guessing an analogue. |
437
618
  | `target_not_scannable` | 422 | **The target resolved to a real page, but this site does not enumerate it as an auditable page** — a post type or status the page enumerator does not cover, or a trashed item. | **This is NOT a clean result.** An audit that answered 200 with zero findings would read as "your page is fine"; this code exists precisely so it cannot. Audit it via `template_summary`, or pick a page `pages_list` returns. |
619
+ | `render_failed` | 422 | `yootheme_builder_heading_outline` in `mode:"rendered"` — the target resolved to a real template, but rendering it genuinely failed (a broken binding or other structural problem — NOT a Joomla `module`/`module_position` gap, which the server tolerates transparently; see `render_fallback`/`render_fallback_used` below). | The page is real but cannot currently be rendered. Read the message for the underlying cause; fix the layout, or fall back to `mode:"builder"` (structural inspection over the layout tree, no render needed). |
438
620
 
439
621
  `server_too_old` is the one code the SERVER never sends — see
440
622
  [Audit ONE page instead of the whole site](#audit-one-page-instead-of-the-whole-site).
@@ -460,11 +642,35 @@ Do not loop on auth errors.
460
642
  > capabilities like `manage_options` gate the admin settings page only; the
461
643
  > REST API surface is Bearer-gated.
462
644
 
463
- > **Joomla L2 articles surface.** The Joomla plugin ships `/v1/articles*`
464
- > REST endpoints for per-article custom layouts, but they are NOT exposed
465
- > via MCP tools in v1.x. To use that surface, call the REST endpoints
466
- > directly with the Bearer key. MCP tool coverage for L2 articles is
467
- > planned for a future release.
645
+ > **Joomla L2 articles surface.** The Joomla plugin ships READ-ONLY
646
+ > `/v1/articles*` REST endpoints (`GET /articles`, `GET /articles/<id>/page-layout`,
647
+ > `GET /articles/<id>/elements/<path>`) for inspecting an article's Builder
648
+ > state, but they are NOT exposed via MCP tools in v1.x. To read that
649
+ > surface, call the REST endpoints directly with the Bearer key. To WRITE
650
+ > an article's layout, address it the same way as any other page — target
651
+ > `article:<id>` with the normal page-editing tools (`page_save`,
652
+ > `element_add`, `element_update_settings`, …); there is no separate write
653
+ > route for articles.
654
+
655
+ ### The write budget is attached to the KEY, not to you
656
+
657
+ Writes are rate-limited **per bearer key**. Several clients — several agents,
658
+ several editors, a script running alongside you — sharing one key share **one**
659
+ budget, so a burst that is well within the cap for you alone can still be
660
+ refused. This is reproducible: six agents on one key hit it.
661
+
662
+ `yootheme_builder_health` publishes the cap as `write_rate_limit`
663
+ (`{limit, window_seconds, scope: "kid", applies_to: "writes"}`), read from the
664
+ limiter the site actually enforces. **Read it before a burst instead of
665
+ discovering it as a refusal.** Two bounds worth knowing:
666
+
667
+ - `limit` is the CAP, never the headroom you have left — no counter is read.
668
+ - Reads are not capped, so there is no reason to slow a read loop down for it.
669
+
670
+ Exceeding it answers HTTP 429 with `retry_after_seconds`, and **the write did
671
+ not happen** — wait out the window and retry rather than treating it as a
672
+ failed write to investigate. Anything throttling in FRONT of the site (a CDN,
673
+ a WAF, the host) is invisible here and answers with its own status.
468
674
 
469
675
  ---
470
676
 
@@ -507,6 +713,16 @@ To verify a specific site before doing work on it, call
507
713
  and `/etag` in parallel and returns `plugin_reachable` + `bearer_valid`
508
714
  without mutating anything.
509
715
 
716
+ For the whole fleet at once — "which of my sites are up, and how much is on
717
+ them" — call `yootheme_builder_sites_overview()`. It answers in ONE call what
718
+ `sites_list` plus one `sites_test` and one `pages_list` per site answered
719
+ before, probing every site in parallel: one row per site with `reachable`,
720
+ `bearer_valid` and the page counts. A site that is down becomes a row carrying
721
+ `error`, never a failed call — which is the point: you want to see WHICH site
722
+ is broken, not lose the answer for the others. Pass `include_pages: false` to
723
+ skip the per-site page read. The content/theme-template split on a row is
724
+ ABSENT on a host that does not report it; it is never reported as zero.
725
+
510
726
  ### Default-site mechanics
511
727
 
512
728
  The default site is set automatically on first add:
@@ -527,7 +743,9 @@ You can store the Bearer key two ways per site:
527
743
  - **1Password reference**: `bearer_ref: "op://Vault/Item/credential"` in
528
744
  `sites.json`. The plaintext token never touches disk. The MCP server shells
529
745
  out to the `op` CLI at first use per site to fetch the live token, then
530
- caches it in memory for that process lifetime.
746
+ caches it in memory for that process lifetime. `op` itself does not cache
747
+ — restart the MCP client after rotating a key so the server re-resolves
748
+ the new value.
531
749
 
532
750
  **Recommendation for production sites**: use `bearer_ref`. You get rotation
533
751
  without editing `sites.json`, and your 1Password audit log captures every
@@ -535,6 +753,24 @@ fetch. The `op` CLI must be installed and signed in on the machine running
535
753
  the MCP server. If `op` is missing, the resolver returns a structured error
536
754
  (`op CLI not found in PATH`) pointing at the install docs.
537
755
 
756
+ ### Prompt friction depends on how the client was launched
757
+
758
+ Per 1Password's own documentation, an app-integration session binds to a
759
+ **TTY** (macOS/Linux) or PID + start time (Windows) — a GUI-launched process
760
+ (Claude Desktop, Cursor) has neither, so the confirmation-free window a
761
+ terminal session gets does not carry over there. Three environment classes,
762
+ worst-first:
763
+
764
+ | Class | Confirmation dialogs | Who |
765
+ |---|---|---|
766
+ | Service account (`OP_SERVICE_ACCOUNT_TOKEN`) | none | agencies, servers, CI |
767
+ | Terminal + app integration | one per window, valid ~10 min | developers running the CLI |
768
+ | **GUI-launched + app integration** | **per call** | **Claude Desktop, Cursor — the customer default** |
769
+
770
+ **For multi-site customers a service account is the frictionless path** —
771
+ worth setting up explicitly rather than discovering the per-call prompts one
772
+ site at a time.
773
+
538
774
  ### `sites.json` location
539
775
 
540
776
  The registry lives at `~/.config/yt-builder-mcp/sites.json` (XDG-conform).
@@ -607,9 +843,9 @@ one conversation.
607
843
  2. For each site, the agent runs the same sequence with `site_id` set:
608
844
  - `yootheme_builder_pages_list({ site_id: "wp-acme", fields: ["id", "label"] })`
609
845
  - `yootheme_builder_get_etag({ site_id: "wp-acme" })`
610
- - `yootheme_builder_element_update_settings({ site_id: "wp-acme", template_id: "home", element_path: "/children/0/children/0/children/0/children/0", props: { content: "New headline" }, merge: true, etag: "<etag>" })`
611
- - `yootheme_builder_page_save({ site_id: "wp-acme", template_id: "home", etag: "<fresh>" })`
612
- - `yootheme_builder_page_publish({ site_id: "wp-acme", template_id: "home", etag: "<fresh>" })`
846
+ - `yootheme_builder_element_update_settings({ site_id: "wp-acme", layout_id: "home", element_path: "/children/0/children/0/children/0/children/0", props: { content: "New headline" }, merge: true, etag: "<etag>" })`
847
+ - `yootheme_builder_page_save({ site_id: "wp-acme", layout_id: "home", etag: "<fresh>" })`
848
+ - `yootheme_builder_page_publish({ site_id: "wp-acme", layout_id: "home", etag: "<fresh>" })`
613
849
  3. The customer sees a stream of replies, each prefixed with the matching
614
850
  `[label @ host]`, so it is obvious which site is at which step.
615
851
 
@@ -623,7 +859,7 @@ for a focused diagnosis without touching the others.
623
859
  ## Pages vs theme templates (know what you are editing)
624
860
 
625
861
  `pages_list` returns TWO different kinds of editable layout, and every tool
626
- takes a `template_id` that addresses either. Know which one you have before
862
+ takes a `layout_id` that addresses either. Know which one you have before
627
863
  you write — they have very different blast radius.
628
864
 
629
865
  | Kind | `id` shape | `type` value(s) | What it is |
@@ -634,7 +870,7 @@ you write — they have very different blast radius.
634
870
  **The safety rule: editing a theme template changes the layout for ALL
635
871
  matching content, not one page.** If the user says "restyle the blog post
636
872
  template", `pages_list` shows it as e.g. `{ id: "I99YS8Ii", label: "Post",
637
- type: "single-post" }` — editing that `template_id` restyles EVERY blog post
873
+ type: "single-post" }` — editing that `layout_id` restyles EVERY blog post
638
874
  on the site at once. That is often exactly what's wanted (that's what theme
639
875
  templates are for), but confirm the intent — do not edit a `single-post` /
640
876
  `taxonomy-*` / `archive-*` template when the user meant one specific page.
@@ -646,6 +882,33 @@ templates are for), but confirm the intent — do not edit a `single-post` /
646
882
  - `pages_create` creates a **page/article** (an individual layout). Theme
647
883
  templates are defined in the theme itself and are not created via this MCP;
648
884
  you edit the existing ones `pages_list` reports.
885
+ - `yootheme_builder_pages_delete` removes one — an L1 tool, call it directly
886
+ (no gateway). `confirm: true` is REQUIRED on every call (a bare call is
887
+ refused, `pages.confirm_required`); there is no preview-by-omitting-confirm
888
+ the way `modules_delete` has. Preview instead with `dry_run: true`
889
+ (reports `element_count`, `has_dynamic_binding`, and whether the real call
890
+ would be refused for lacking an `intent`; nothing is written and no
891
+ request is sent). The real call additionally needs `intent`: call
892
+ `confirm: true` without it once and the server refuses with a 400 naming
893
+ which intents are possible for THIS page's/article's CURRENT status, each
894
+ with its own `effect`/`reversible`/`native_name` — the same
895
+ `intent_not_chosen` shape `modules_delete` uses. Unlike modules,
896
+ `intent: "trash"` is REVERSIBLE on BOTH platforms: it sets the WordPress
897
+ `post_status` to `trash` or the Joomla article `state` to `-2`;
898
+ `intent: "permanent"` removes the row and its layout for good on either
899
+ platform, and neither CMS can recreate them. There is a way back from the
900
+ trash step, but it is NOT this tool and NOT `pages_set_status` (see "Finding
901
+ and un-trashing a page" below).
902
+ `yootheme_builder_pages_delete({ layout_id: 42, confirm: true, intent: "trash" })`.
903
+ A page/article ALREADY in the trash short-circuits BEFORE the intent gate:
904
+ passing `intent: "permanent"` still deletes it for good (the only intent
905
+ offered there); passing anything else — no `intent`, `intent: "trash"`, or
906
+ an unrecognised one — instead answers `200 {deleted:false,
907
+ already_trashed:true}`, a MEASUREMENT, not an error, that names
908
+ `intent:"permanent"` or the way back (below) as the two ways forward. The
909
+ EXECUTED trash response (`confirm:true, intent:"trash"`) carries the same
910
+ pointer in its own `note` field — the caller who just trashed something and
911
+ regrets it never has to re-read the preview to find it.
649
912
 
650
913
  **Reaching a theme template by ROLE instead of by id.** You rarely know a theme
651
914
  template's opaque id (`I99YS8Ii`). The `special` tokens of the shared `target`
@@ -666,6 +929,40 @@ response's `resolved_target` tells you which template that actually was. The
666
929
  same safety rule still applies: a finding on a theme template is a finding on
667
930
  EVERY page that template renders.
668
931
 
932
+ **Finding and un-trashing a page you cannot see in `pages_list`.** A trashed
933
+ page/article never appears there, and by default `local_content_read`
934
+ answers the same `total: 0` for "genuinely nothing" and "it's in the trash" —
935
+ pass `kind: "entities", status: "trash"` for the EXCLUSIVE trashed view (never
936
+ mixed into the default list), with the identifier in `id`. Even without
937
+ asking: a plain `local_content_read({kind:"entities"})` call carries
938
+ `trashed_total` in the response whenever a match exists in the trash, so the
939
+ gap surfaces unasked. Then `yootheme_builder_pages_restore({ layout_id: <id> })`
940
+ brings it back — the ONE correct way, do NOT try `pages_set_status`, which
941
+ refuses a trashed page with a 409 pointing at this tool. **The two platforms
942
+ genuinely diverge here, and the tool says so rather than hiding it**:
943
+ WordPress' `wp_untrash_post()` reinstates BOTH the exact pre-trash status AND
944
+ the pre-trash URL slug (a hand-rolled status write would leave the trash's
945
+ `__trashed` slug suffix in place, silently changing the address) — Joomla
946
+ stores no pre-trash status at all, so a restored article can only become
947
+ `unpublished`; call `pages_set_status` with `status: "published"` afterwards
948
+ to make it visible again.
949
+
950
+ **A trashed Joomla MODULE has no restore tool of its own, and the way out is
951
+ NOT the equivalent of the above — it lands on the opposite side.** Measured:
952
+ `yootheme_builder_modules_update({active:true})` writes `published = 1`, so a
953
+ module comes out of the trash **PUBLISHED** whatever it was before. Joomla
954
+ keeps no pre-trash state for modules either (`#__modules` has one state column
955
+ and the trash writes `-2` over it), but here the single reachable value is the
956
+ VISIBLE one — an unpublished module you trash and bring back is live on the
957
+ site. Its counterpart `{active:false}` is not a second way out: a trashed
958
+ module already reads `active:false`, so that call writes nothing and leaves it
959
+ in the trash, reporting only `changed: []`. Coming back unpublished is
960
+ therefore two calls in order — `{active:true}` then `{active:false}` — and the
961
+ tool says so in `restore_state_note` on the trash row, in the delete preview's
962
+ notes, and in the `notes` of the restoring call itself.
963
+ `local_content_read({kind:"modules"})` lists a trashed module unasked, marked
964
+ `publish_state:"trashed"`.
965
+
669
966
  ## Workflow 1: Build a hero section
670
967
 
671
968
  **Goal:** Add a fresh hero section (heading + sub-heading + CTA button)
@@ -681,34 +978,124 @@ to an existing page.
681
978
  named a specific page, match on `label` (exact then fuzzy).
682
979
  3. `yootheme_builder_get_etag()`: fetch the current top-level
683
980
  optimistic-lock ETag. Every write tool requires it via `etag`.
684
- 4. `yootheme_builder_element_add({ template_id: "<id>", parent_path: "", element_type: "section", props: { style: "primary" }, etag: "<etag>" })`:
685
- append a new section at the template root (`parent_path: ""`). The
686
- section background is set with `style` (enum: `default` / `muted` /
687
- `primary` / `secondary`) there is NO `background` key (it hard-rejects
688
- as an unknown prop). Always call `element_type_get_schema("section")`
689
- first to confirm the real prop keys.
690
- Returns `{ rel_path: "/children/0/children/N", etag: "<fresh>" }`.
691
- 5. `yootheme_builder_element_add({ template_id, parent_path: "<section-path>", element_type: "row", etag: "<fresh-etag>" })`:
692
- add a row inside the section. Use the etag returned by the
693
- previous write (etags rotate every mutation).
694
- 6. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "headline", props: { content: "<h1 text>", title_style: "h1" }, etag })`:
695
- add a headline. `title_style: "h1"` uses the semantic type scale (the
696
- tasteful default). See "Typography & heading styles" below before
697
- reaching for a `heading-*` display size.
698
- 7. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "text", props: { content: "<sub text>" }, etag })`:
699
- add a text element.
700
- 8. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "button", etag })`
981
+ **Chaining writes: every write answers with `etag` AND `new_etag`, and both
982
+ carry the value read AFTER that write.** They are interchangeable; pass
983
+ either into the next write. You only need `get_etag` again after a 412, or
984
+ when you did not just write.
985
+ 4. `yootheme_builder_scaffold_section({ layout_id: "<id>", columns: 1, etag: "<etag>" })`:
986
+ build a VALID `section → row → column` hull at the template root in ONE
987
+ atomic call the correct way to start a layout block. Only `section` may
988
+ sit at the layout root; a bare section accepts only `row`; a bare row
989
+ accepts only `column`; content/leaf elements (headline, text, button, …)
990
+ are rejected anywhere except inside a `column`. `columns: 1` (the
991
+ default) is a single full-width column the shape a hero needs.
992
+ Returns `{ section_path: "/children/0/children/N", row_path: "…/children/0",
993
+ column_paths: ["…/children/0/children/0"], etag: "<fresh>" }`. Drop every
994
+ content element below into `column_paths[0]`.
995
+ 5. (Optional) `yootheme_builder_element_update_settings({ layout_id, element_path: "<section-path>", props: { style: "primary" }, etag: "<fresh-etag>" })`:
996
+ `scaffold_section` does not take section `props`, so set the background
997
+ as a follow-up call when the hero needs one. `style` (enum: `default` /
998
+ `muted` / `primary` / `secondary`) — there is NO `background` key. Writing
999
+ one is ACCEPTED and stored; the answer names it under `warnings` with
1000
+ `code: "unknown_prop_keys"`, and nothing renders. Always call
1001
+ `element_type_get_schema("section")` first to confirm the real prop keys.
1002
+ 6. `yootheme_builder_element_add({ layout_id, parent_path: "<column-path>", element_type: "headline", props: { content: "<h1 text>", title_style: "h1" }, etag })`:
1003
+ add a headline INSIDE the column `scaffold_section` returned
1004
+ (`column_paths[0]`) — never directly in the row or the section. `title_style:
1005
+ "h1"` uses the semantic type scale (the tasteful default). See "Typography
1006
+ & heading styles" below before reaching for a `heading-*` display size.
1007
+ 7. `yootheme_builder_element_add({ layout_id, parent_path: "<column-path>", element_type: "text", props: { content: "<sub text>" }, etag })`:
1008
+ add a text element into the SAME column.
1009
+ 8. `yootheme_builder_element_add({ layout_id, parent_path: "<column-path>", element_type: "button", etag })`
701
1010
  then add a `button_item` CHILD for the actual label/link/style:
702
- `yootheme_builder_element_add({ template_id, parent_path: "<button-path>", element_type: "button_item", props: { content: "<cta>", link: "<url>", button_style: "primary" }, etag })`.
1011
+ `yootheme_builder_element_add({ layout_id, parent_path: "<button-path>", element_type: "button_item", props: { content: "<cta>", link: "<url>", button_style: "primary" }, etag })`.
703
1012
  The `button` element is a CONTAINER — its label, link and style live on
704
1013
  a `button_item` child, NOT on the button's own props. Adding a lone
705
1014
  `button` with `{content, link}` renders an empty button. (Same
706
1015
  container→item pattern as `grid`→`grid_item`, `list`→`list_item`.)
707
- 9. `yootheme_builder_page_save({ template_id, etag })`:
1016
+ 9. `yootheme_builder_page_save({ layout_id, etag })`:
708
1017
  persist the working copy (visible in YOOtheme Customizer preview).
709
1018
  `page_save` is a first-class L1 tool; call it directly.
710
- 10. `yootheme_builder_page_publish({ template_id, etag })`:
1019
+ > **`render_preview` cannot see an element whose rendering depends on CMS
1020
+ > runtime state.** On Joomla a `module` / `module_position` subtree is EXCLUDED
1021
+ > from every render probe, whatever the module's state, and a `newsletter`
1022
+ > element is excluded wherever its provider is unconfigured. The response says
1023
+ > so with `fallback_used:true` + `dropped_count:N`, and `dropped_count_note`
1024
+ > names WHICH types were excluded, why, and that they are still in the saved
1025
+ > layout. So an `expect` aimed at such output can never match: a `matched:false`
1026
+ > there is **not** evidence that the element fails to render, and
1027
+ > `match_conclusive:false` marks exactly that case. A HIT is always conclusive.
1028
+ > To check a MODULE, read its placement with
1029
+ > `local_content_read({kind:"modules", include_usage:true, verify_on_urls:[...]})`.
1030
+ > That read answers for modules only; for any other excluded type it returns
1031
+ > nothing, which is why the response points at it only when a module was
1032
+ > actually excluded.
1033
+
1034
+ 10. `yootheme_builder_page_publish({ layout_id, etag })`:
711
1035
  make the changes live on the front-end. Also a first-class L1 tool.
1036
+ **`published:true` is a statement about the LAYOUT, not about the page.**
1037
+ A WordPress post left in `draft`, or a Joomla article at `state=0` /
1038
+ trashed, or one whose `publish_up` is still in the future, is not served
1039
+ to a visitor afterwards. Read `cms_visibility.findable_by_visitor` before you
1040
+ tell anyone the page is live; `cms_warning` names the exact fix when it is
1041
+ `false`.
1042
+ **For the ordinary case — build it and make it live — pass the CMS status
1043
+ in the same call**: `page_publish({ layout_id, cms_status: "publish" })` on
1044
+ WordPress, `cms_status: "published"` on Joomla, and on Joomla add
1045
+ `menu_item: true` when the article still needs a SEF route. Same words and
1046
+ same refusals as `pages_set_status`; the other platform's word publishes
1047
+ nothing. The response then carries `cms_status_applied` with the STORED
1048
+ status, and `cms_visibility` reflects the new one. Omit `cms_status` and this
1049
+ call changes nothing about who may see the page — that separation stays, it
1050
+ is simply no longer the only way through.
1051
+ > **On Joomla, "not findable" and "not reachable" are two different things, and
1052
+ > only one of them is a privacy control.** Measured anonymously with curl on
1053
+ > 2026-08-04: the STATE decides the 404 (`state` 1 published and 2 archived both
1054
+ > answer **200**, `state` 0 unpublished and -2 trashed both answer **404**) and
1055
+ > the ACCESS LEVEL decides the 403 (a level a guest does not hold answers **403**
1056
+ > at state 1 and at state 2 alike). A missing menu item decides NEITHER: it
1057
+ > costs the SEF url, the breadcrumb and the navigation path, while the article
1058
+ > keeps answering 200 on its own
1059
+ > `?option=com_content&view=article&id=N` url for anyone. So `findable_by_visitor:
1060
+ > false` on a published article usually means "no visitor will FIND it", not
1061
+ > "no visitor can OPEN it". Two consequences worth saying out loud: **archiving
1062
+ > does not hide anything** (it is a 200), and if an article must not be public,
1063
+ > set its state with `yootheme_builder_pages_set_status` or give it an access
1064
+ > level a guest does not hold. Never leave the menu item off and call that
1065
+ > private.
1066
+
1067
+ **The `cms_visibility` block carries the facts, so you do not have to infer
1068
+ them.** It rides on `page_publish`, `page_generate_dynamic` and `pages_set_status`.
1069
+ Read the field that answers YOUR question:
1070
+
1071
+ | field | question it answers | platform |
1072
+ |---|---|---|
1073
+ | `findable_by_visitor` | will a visitor FIND this via a navigation path? (NOT the same as reaching it: an unrouted article still answers on its direct URL) | both |
1074
+ | `status_permits_direct_url` | does the stored state ALONE serve the item's own direct URL? | both |
1075
+ | `state_and_access_permit_direct_url` | state AND access level together: is it served? | Joomla |
1076
+ | `access_is_guest_visible` | does an anonymous visitor hold this access level? | Joomla, when the probe ran |
1077
+ | `article_access` / `post_status` / `article_state` | the raw stored values | per platform |
1078
+ | `publish_up_in_future` | is it scheduled? | both |
1079
+ | `has_route` | does a menu item route it? (WordPress: always equals published) | both |
1080
+
1081
+ > **One row of that table is NOT in the block, and it is the one people
1082
+ > confuse it with:**
1083
+ >
1084
+ > | field | question it answers | where it rides |
1085
+ > |---|---|---|
1086
+ > | `visibility` | do the STORED FIELDS rule this page out for a visitor? | every `pages_list` row |
1087
+ >
1088
+ > `visibility` reads the CMS status plus, on WordPress, password
1089
+ > protection. It never reads the menu route or the access level, so on
1090
+ > Joomla it answers `undetermined` where `findable_by_visitor` gives a verdict.
1091
+ > That is not a contradiction: the per-page field looked at two levers the
1092
+ > listing never touched, so when they seem to disagree, believe the
1093
+ > per-page one.
1094
+
1095
+ On WordPress the first three necessarily agree: a published page resolves at
1096
+ its permalink and there is no separate route step. On Joomla they come
1097
+ apart, and that divergence is the whole reason the extra fields exist. A
1098
+ `null` in any of them means the input was not readable, never "no".
712
1099
 
713
1100
  > **Building a whole page? Prefer ONE atomic `page_save` over ~30 incremental
714
1101
  > writes.** The step-by-step `element_add` sequence above is clearest for
@@ -717,8 +1104,8 @@ to an existing page.
717
1104
  > (especially when other agents edit the same site — the builder ETag is a
718
1105
  > single shared option). For a full page, assemble the complete
719
1106
  > `{type:"layout",children:[...]}` tree in memory and persist it in ONE call:
720
- > `yootheme_builder_page_save({ template_id, layout: <full-tree> })` (ETag
721
- > optional last-write-wins, scoped to this page). `dry_run:true` preflights
1107
+ > `yootheme_builder_page_save({ layout_id, layout: <full-tree> })` (ETag is
1108
+ > required read it via `yootheme_builder_get_etag` first). `dry_run:true` preflights
722
1109
  > it first. A fast, robust pattern: `scaffold_section` one block, read it
723
1110
  > back to learn the exact node shape, then build the rest of the tree from
724
1111
  > that shape and `page_save` it whole.
@@ -726,16 +1113,35 @@ to an existing page.
726
1113
  **Common pitfalls:**
727
1114
 
728
1115
  - **Wrong parameter names.** Every tool uses snake_case. Use
729
- `template_id` (not `pageId`), `parent_path` (not `parentPath`),
1116
+ `layout_id` (not `pageId`), `parent_path` (not `parentPath`),
730
1117
  `element_type` (not `type`), `props` (not `settings`), `etag`
731
1118
  (not `ifMatch`). The MCP server rejects unknown keys with a
732
1119
  Zod-validation error.
733
1120
  - **Forgetting `etag`.** Every write tool needs the latest etag. The
734
1121
  shared schema marks it required (min length 1). On `412 Precondition
735
- Failed` re-fetch via `yootheme_builder_get_etag` and retry.
736
- - **Adding non-row elements directly to a section.** Sections expect a
737
- row in between. The server returns a structured error with a
738
- human-readable hint when you skip the row.
1122
+ Failed`, RE-READ the element or page you were writing before you retry.
1123
+ A fresh etag from `yootheme_builder_get_etag` makes the retry succeed,
1124
+ but the etag is site-wide and its hash does not cover per-page layouts,
1125
+ so a 412 cannot tell you whether the write that bumped it was on YOUR
1126
+ page. Retrying on a fresh etag alone overwrites whatever landed.
1127
+ - **Adding content elements directly to a section or a row.** A section
1128
+ accepts only `row`; a row accepts only `column`; headline/text/button and
1129
+ every other content element are rejected anywhere except inside a
1130
+ `column` — a `422 layout_invalid_hierarchy`. Use `scaffold_section` to
1131
+ build the whole `section → row → column` hull in one call and drop content
1132
+ into the returned `column_paths`; don't hand-build a section + row and add
1133
+ content straight into the row. The server returns a structured error with a
1134
+ human-readable hint when you skip a level.
1135
+ - **Adding a child to a leaf, or hitting an empty element registry.** A leaf
1136
+ type — one that never accepts ANY child, e.g. `text`, `headline`, `button`
1137
+ — refuses every child with `422 layout_leaf_has_children`, a distinct code
1138
+ from the mismatch above. Separately, if YOOtheme's element registry answers
1139
+ empty on the site (usually a transient boot-order state, not a fault in
1140
+ your layout), the parent/child check cannot run at all and the write is
1141
+ refused with `422 registry_unavailable` rather than guessed through —
1142
+ section, row, column and the sublayout are decided without the registry
1143
+ and keep working meanwhile. Both leave the tree untouched; for
1144
+ `registry_unavailable`, give the site a moment and retry.
739
1145
  - **Saving without publishing.** `page_save` is the equivalent of the
740
1146
  YOOtheme Customizer "Save" button. Content lives in the staging
741
1147
  copy. Visitors see nothing until `page_publish`.
@@ -751,19 +1157,27 @@ to an existing page.
751
1157
  **Worked example (tool-call snippet):**
752
1158
 
753
1159
  ```jsonc
754
- // Step 4. Add the section. parent_path: "" means template root.
755
- yootheme_builder_element_add({
756
- template_id: "home",
757
- parent_path: "",
758
- element_type: "section",
759
- props: { style: "primary" }, // section background = `style`, NOT `background`
1160
+ // Step 4. Build the section row column hull in ONE call.
1161
+ yootheme_builder_scaffold_section({
1162
+ layout_id: "home",
1163
+ columns: 1,
760
1164
  etag: "abc123" // from yootheme_builder_get_etag
761
1165
  })
762
- // Response: { rel_path: "/children/0/children/3", etag: "def456" }
763
- // next call uses etag "def456"
1166
+ // Response: { section_path: "/children/0/children/3", row_path: "/children/0/children/3/children/0",
1167
+ // column_paths: ["/children/0/children/3/children/0/children/0"], etag: "def456" }
1168
+ // → content goes into column_paths[0]; next call uses etag "def456"
1169
+
1170
+ // Step 5 (optional). Set the section background — `style`, NOT `background`.
1171
+ yootheme_builder_element_update_settings({
1172
+ layout_id: "home",
1173
+ element_path: "/children/0/children/3", // the section_path above
1174
+ props: { style: "primary" },
1175
+ etag: "def456"
1176
+ })
1177
+ // Response: { rel_path: "/children/0/children/3", etag: "ghi789" }
764
1178
 
765
1179
  // Step 9. page_save is a first-class L1 tool; call it directly.
766
- yootheme_builder_page_save({ template_id: "home", etag: "<latest>" })
1180
+ yootheme_builder_page_save({ layout_id: "home", etag: "<latest>" })
767
1181
  ```
768
1182
 
769
1183
  **Edge case:** YOOtheme allows nested sections (rare). If the user
@@ -774,7 +1188,7 @@ section sits inside another section.
774
1188
  **Success criterion:** After `page_publish`, navigating to the page
775
1189
  URL on the front-end shows the new hero section above the previous
776
1190
  content. Re-reading the layout via
777
- `yootheme_builder_page_get_layout({ template_id })` shows the new
1191
+ `yootheme_builder_page_get_layout({ layout_id })` shows the new
778
1192
  section as the last child of the template root.
779
1193
 
780
1194
  ---
@@ -848,23 +1262,23 @@ inspection that writes nothing.
848
1262
  // 1. INSPECT (writes nothing — dry_run defaults true, no etag needed).
849
1263
  // Returns the current state, the theme-declared --uk-inverse, risk_class,
850
1264
  // confidence, a recommendation and would_change/changes.
851
- yootheme_builder_header_transparency_set({ template_id: "home" })
1265
+ yootheme_builder_header_transparency_set({ layout_id: "home" })
852
1266
 
853
1267
  // 2. REPAIR with the theme's own declared colour.
854
1268
  yootheme_builder_header_transparency_set({
855
- template_id: "home", mode: "auto", dry_run: false, etag: "<latest>"
1269
+ layout_id: "home", mode: "auto", dry_run: false, etag: "<latest>"
856
1270
  })
857
1271
 
858
1272
  // 3. Turn it ON deliberately, without asserting a colour.
859
1273
  // mode "auto" derives it; over a hero medium it writes NO colour and tells
860
1274
  // you to look at the page instead.
861
1275
  yootheme_builder_header_transparency_set({
862
- template_id: "home", mode: "auto", transparent: true, dry_run: false, etag: "<latest>"
1276
+ layout_id: "home", mode: "auto", transparent: true, dry_run: false, etag: "<latest>"
863
1277
  })
864
1278
 
865
1279
  // 4. A colour you MEASURED over a hero medium (only mode "explicit" takes one).
866
1280
  yootheme_builder_header_transparency_set({
867
- template_id: "home", mode: "explicit", text_color: "light",
1281
+ layout_id: "home", mode: "explicit", text_color: "light",
868
1282
  dry_run: false, etag: "<latest>"
869
1283
  })
870
1284
  ```
@@ -993,16 +1407,16 @@ never by narrowing the column. Narrowing a single column WITHOUT a matching
993
1407
  **Easiest correct way — `scaffold_section` builds the whole factory-shaped
994
1408
  hull for you (row grid string + per-column responsive widths in one call):**
995
1409
 
996
- - `scaffold_section({ template_id, columns: 3, etag })` → a section whose row
1410
+ - `scaffold_section({ layout_id, columns: 3, etag })` → a section whose row
997
1411
  carries `layout: "1-3,1-3,1-3"` and whose 3 columns each carry
998
1412
  `width_default: "1-1"` + `width_medium: "1-3"` → clean, gap-free, side-by-side
999
1413
  from ≥960px. `columns: N` distributes equal widths (`1-N` for N ≤ 6,
1000
1414
  `expand` for N > 6).
1001
1415
  - For an ASYMMETRIC row pass explicit tokens, one per column:
1002
- `scaffold_section({ template_id, columns: 2, column_widths: ["2-3", "1-3"], etag })`
1416
+ `scaffold_section({ layout_id, columns: 2, column_widths: ["2-3", "1-3"], etag })`
1003
1417
  → a row with `layout: "2-3,1-3"` and the matching column widths. A
1004
1418
  wrong-length array or a bad token returns a friendly `400` before the write.
1005
- - `scaffold_section({ template_id, columns: 1, etag })` → a single full-width
1419
+ - `scaffold_section({ layout_id, columns: 1, etag })` → a single full-width
1006
1420
  column with no `row.layout` and no column width (the factory single-column
1007
1421
  shape).
1008
1422
 
@@ -1010,7 +1424,20 @@ hull for you (row grid string + per-column responsive widths in one call):**
1010
1424
  into them with `element_add({ parent_path: "<column-path>", ... })`. If you
1011
1425
  build columns by hand with `element_add` instead, remember to set BOTH
1012
1426
  `row.props.layout` (the grid string) AND each column's `width_default` /
1013
- `width_medium`, or the row reads as "Whole" and shows gaps in the builder.
1427
+ `width_medium`, or the row reads as "Whole" and shows gaps in the builder:
1428
+
1429
+ ```jsonc
1430
+ // Hand-built alternative to scaffold_section — set the grid string on the row…
1431
+ yootheme_builder_element_add({
1432
+ layout_id, parent_path: "<section-path>", element_type: "row",
1433
+ props: { layout: "1-3,1-3,1-3" }, etag,
1434
+ })
1435
+ // …and the matching responsive width on EACH column.
1436
+ yootheme_builder_element_add({
1437
+ layout_id, parent_path: "<row-path>", element_type: "column",
1438
+ props: { width_default: "1-1", width_medium: "1-3" }, etag,
1439
+ })
1440
+ ```
1014
1441
 
1015
1442
  **Success criterion:** in the builder's left panel the row shows N distinct
1016
1443
  columns (not one column + an empty gap), and after publish the columns sit
@@ -1121,7 +1548,7 @@ token rotates (re-login, session timeout, a different tab), the preview
1121
1548
  renders as a plain page without the builder-data injection and the panel
1122
1549
  reports "not available". **Fix: reload the Customizer tab** (or re-open the
1123
1550
  builder from Pages → the page → Builder). The stored layout is intact —
1124
- confirm with `page_get_layout({ template_id })`, which reads it directly
1551
+ confirm with `page_get_layout({ layout_id })`, which reads it directly
1125
1552
  and is unaffected by the browser session.
1126
1553
 
1127
1554
  ---
@@ -1167,6 +1594,116 @@ schema's own claims: a report to check, not a fact about your build.
1167
1594
  | `slideshow` nav (not `thumbnav`) | Borrows the **slide's `title`** as the link text (`slideshow/templates/template-nav.php`: `$thumbnail ?: $child->props['title']`). An untitled slide gets an empty nav link. |
1168
1595
  | `accordion_item` | Declares **no `title_element`** at all (`grep -c title_element` = 0 in both `accordion_item/element.php` and `accordion/element.php`), so accordion titles can never carry a heading tag and never enter the heading outline. Do not try to write one. |
1169
1596
 
1597
+ ## Placing a module or widget
1598
+
1599
+ A module or widget can reach a page in three ways, and they are not
1600
+ interchangeable: they differ in how many pages the thing shows up on, in how
1601
+ many edits one change costs, and in what happens on a page nobody has thought
1602
+ about yet. `modules_create` therefore REQUIRES a `placement` argument and
1603
+ refuses a call without one as `placement_not_chosen`, answering with exactly
1604
+ the rows below for the platform in question, plus that platform's notes.
1605
+
1606
+ The three ways are the same on both platforms. What differs is the vocabulary,
1607
+ and one behaviour: Joomla can narrow a site-wide placement to chosen pages,
1608
+ WordPress cannot.
1609
+
1610
+ **Joomla** (module, template position):
1611
+
1612
+ | `placement` | Reach | What it does |
1613
+ | --- | --- | --- |
1614
+ | `position` | every page | The module sits in a template position such as top and appears on EVERY page. Pass the position with this call. No layout edit, and one module serves the whole site. To narrow it to chosen pages, set the menu assignment with modules_update. |
1615
+ | `module_element` | one page | A Module element inside a column of ONE page layout embeds this module, and it appears on that page only. Call scaffold_section first for a valid column path. |
1616
+ | `position_element` | one page | The module goes into one of the positions builder-1 to builder-6. On its own it appears NOWHERE. A Position element placed in a page layout is what renders it, and it then appears on that page only. |
1617
+
1618
+ Two notes travel with that answer:
1619
+
1620
+ - modules_create builds two native module types (mod_custom, mod_menu). A
1621
+ builder module (mod_yootheme_builder) must already exist. Once it does, all
1622
+ three placements work for it exactly as they do for a native module.
1623
+ - You may not need a module at all: a YOOtheme element such as alert placed
1624
+ straight into a page layout does the same job, at the cost of a separate copy
1625
+ per page.
1626
+
1627
+ **WordPress** (widget, widget area):
1628
+
1629
+ | `placement` | Reach | What it does |
1630
+ | --- | --- | --- |
1631
+ | `position` | every page | The widget sits in a widget area and appears on EVERY page. Pass the area with this call. No layout edit, and one widget serves the whole site. WordPress has no built-in per-page control, so narrowing it to chosen pages needs a plugin. |
1632
+ | `module_element` | one page | A Widget element inside a column of ONE page layout embeds this widget, and it appears on that page only. Needs a position: builder-1 to builder-6, the areas that render nothing on their own. Call scaffold_section first for a valid column path. |
1633
+ | `position_element` | one page | The widget goes into one of the areas builder-1 to builder-6. On its own it appears NOWHERE. An Area element placed in a page layout is what renders it, and it then appears on that page only. |
1634
+
1635
+ And its two notes:
1636
+
1637
+ - modules_create builds three native widget types (custom_html, text, block). A
1638
+ builder widget must already exist. Once it does, all three placements work
1639
+ for it exactly as they do for a native widget.
1640
+ - You may not need a widget at all: a YOOtheme element such as alert placed
1641
+ straight into a page layout does the same job, at the cost of a separate copy
1642
+ per page.
1643
+
1644
+ Which ways need a `position` argument is a PLATFORM fact since 2026-08-08. On
1645
+ JOOMLA it is two of the three: a real theme position (top, bottom, navbar) for
1646
+ `position`, one of builder-1 to builder-6 for `position_element`, and
1647
+ `module_element` takes none, because a module with no position is a native state
1648
+ there. On WORDPRESS it is ALL THREE — a widget exists only inside a registered
1649
+ area, so there is no positionless widget to create, and `module_element` takes
1650
+ builder-1 to builder-6. A real theme position under `module_element` stays
1651
+ refused on both, because the module would ALSO show site wide and render twice
1652
+ on that page. Every way that needs one is REQUIRED, never defaulted: omit it and
1653
+ the call creates NOTHING and comes back `placement_needs_a_position`. A position you DO name is checked against what
1654
+ this site declares or registers, and the two ways it can be wrong are answered
1655
+ by two different codes: a value this site has never heard of (or one too long
1656
+ to be a position at all) comes back as `placement_position_unknown`, and a
1657
+ value that is real but describes a different outcome than the way you chose
1658
+ comes back as `placement_position_conflict`. Each names which half of the call
1659
+ was meant, instead of guessing.
1660
+
1661
+ Two things the layout itself will not let you do: a module or module_position
1662
+ element belongs inside a `column`, never on the layout root (the root takes
1663
+ `section` only, and a root-level module renders outside the section flow, over
1664
+ the navigation on a transparent header), and a `module_position` element renders
1665
+ with YOOtheme's own default layout (`stack`) when you leave the `layout` prop
1666
+ out. Set it only if you want `grid`. Never copy the `sidebar` value the element
1667
+ schema reports as that field's default: it is not one of the two accepted values
1668
+ and is rejected with a 422.
1669
+
1670
+ **"Every page" above is the assignment a `position` placement makes. On
1671
+ Joomla it takes effect immediately: every module write evicts Joomla's own
1672
+ module-list cache exactly like the native module manager does. On WordPress
1673
+ the widget write goes through `update_option()`, which refreshes WordPress'
1674
+ own options cache the same way any core option save does - if the site runs
1675
+ a persistent object-cache drop-in, this server behaves exactly as WordPress
1676
+ itself does there, no better and no worse. If a write looks invisible
1677
+ anyway, you are almost certainly looking at one of two things. (1) On
1678
+ Joomla, the site caches aggressively and the module was written by an OLDER
1679
+ plugin version or by a tool that bypasses the module manager - Joomla's
1680
+ conservative cache then serves the pre-write module list for up to `cachetime`
1681
+ (15 minutes by default), while the database row is already correct. Clearing
1682
+ the site cache or waiting resolves it; nothing is wrong with the module. (2)
1683
+ The position is one of the `builder-N` slots, which never render standalone -
1684
+ they reach a page only through a Position/Area element. For a quick
1685
+ confirmation either way, call `local_content_read` with `include_usage:true`
1686
+ and `verify_on_urls:["<a page url>"]` and read `usage.observed.urls[].status`
1687
+ (`present`/`absent`/`not_detectable`/`error` - see T1.7 in the local-content
1688
+ chapter), or fetch the page as a guest and search for the module's own
1689
+ signature (`id="<widget-id>"` on WordPress, `id="module-<id>"` on Joomla).
1690
+
1691
+ **Before doing N per-page edits, check whether one site-wide module already
1692
+ covers the need.** If a site-wide Builder module already sits in the header
1693
+ area, adding the new content to that ONE module can replace a separate edit
1694
+ on every page. A `local_content_read({kind:"modules"})` pass (T1.6 shows
1695
+ what `usage` returns) tells you what is already there before you start a
1696
+ per-page loop.
1697
+
1698
+ **A module or widget placed BOTH in its position and as an element on the same
1699
+ page renders twice.** This is not a hypothetical: the T1.7 section of the
1700
+ local-content chapter documents it measured live on
1701
+ `dev5wp.wootsup.com/mcp-module-probe/`, where a widget carrying both an element
1702
+ reference and a position reference served its content twice on one document.
1703
+ Before adding a `module`/`module_position` element for something that is ALSO
1704
+ assigned a rendering position, check its `usage.element_refs` first (T1.6), or
1705
+ drop the position assignment.
1706
+
1170
1707
  ---
1171
1708
 
1172
1709
  ## Workflow 2: Bind a dynamic source to a grid
@@ -1179,30 +1716,30 @@ renders dynamic items.
1179
1716
 
1180
1717
  1. `yootheme_builder_health`: confirm host plugin reachable.
1181
1718
  2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
1182
- `yootheme_builder_page_get_layout({ template_id: "<id>", flat: false })`:
1719
+ `yootheme_builder_page_get_layout({ layout_id: "<id>", flat: false })`:
1183
1720
  locate the target Grid. Note its JSON-Pointer `path` (e.g.
1184
1721
  `/children/0/children/2/children/0`).
1185
- 3. `yootheme_builder_element_get({ template_id, element_path })`:
1722
+ 3. `yootheme_builder_element_get({ layout_id, element_path })`:
1186
1723
  fetch the Grid's current props so you can preserve them. Binding
1187
1724
  sets `props.source` and leaves the rest alone.
1188
1725
  4. `yootheme_builder_sources_list()`: enumerate available Sources.
1189
1726
  Each returns `{ name, label, origin, kind }`. Pick the one the
1190
1727
  user asked for.
1191
- 5. `yootheme_builder_element_get_binding({ template_id, element_path })`
1728
+ 5. `yootheme_builder_element_get_binding({ layout_id, element_path })`
1192
1729
  check whether the Grid is already bound (idempotency: skip step
1193
1730
  7 if `source_name` already matches the user's intent).
1194
1731
  `element_get_binding` is a first-class L1 tool (B4) — call it directly.
1195
1732
  6. `yootheme_builder_get_etag()`: fetch the optimistic-lock etag for
1196
1733
  the upcoming mutation.
1197
- 7. `yootheme_builder_element_bind_source({ template_id, element_path, source_name: "<name>", etag: "<etag>" })`
1734
+ 7. `yootheme_builder_element_bind_source({ layout_id, element_path, source_name: "<name>", etag: "<etag>" })`
1198
1735
  applies the binding. Returns `{ path, etag, has_binding: true }`.
1199
1736
  Pass `source_id: "<origin>:<name>"` as well **only** when two
1200
1737
  plugins register a source with the same `source_name` (the server
1201
1738
  surfaces the ambiguity as an elicitation prompt; if the host
1202
1739
  doesn't support elicitation you'll see a structured error listing
1203
1740
  the candidates). This is a first-class L1 tool — call it directly.
1204
- 8. `yootheme_builder_page_save({ template_id, etag: "<fresh>" })`
1205
- then `yootheme_builder_page_publish({ template_id, etag: "<fresh>" })`.
1741
+ 8. `yootheme_builder_page_save({ layout_id, etag: "<fresh>" })`
1742
+ then `yootheme_builder_page_publish({ layout_id, etag: "<fresh>" })`.
1206
1743
 
1207
1744
  **Common pitfalls:**
1208
1745
 
@@ -1212,9 +1749,27 @@ renders dynamic items.
1212
1749
  `relation`/`field`, `arguments` and `directives`; see the Tool Catalog
1213
1750
  appendix for the full `element_bind_source` input-key list rather than
1214
1751
  guessing a subset.
1215
- - **Wrong parameter names.** Use `template_id` (not `pageId`),
1752
+ - **Wrong parameter names.** Use `layout_id` (not `pageId`),
1216
1753
  `element_path` (not `path`), `source_name` (not `sourceName`),
1217
1754
  `etag` (not `ifMatch`).
1755
+ - **A misspelled `field_mappings` KEY is refused, and be glad it is.** The keys
1756
+ are element PROPS; the values are source FIELDS. A key that is not a prop of
1757
+ the target element is rejected with `422 unknown_prop_keys`, listing
1758
+ `valid_prop_keys` and `did_you_mean`, and nothing is written. The reason is
1759
+ the ASYMMETRY, and the honest form of it is two measurements that disagree:
1760
+ on a native `articles` source, `{title, content}` rendered 10 rows while
1761
+ `{titel, content}` rendered **0** — the untouched `content` included; on a
1762
+ `grid_item` at an API-Mapper list source, the same substitution left all 5
1763
+ rows standing and cost only that slot. So a misspelled key sometimes empties
1764
+ the WHOLE binding and sometimes only its own slot, `element_get_binding`
1765
+ reads either back as healthy, and which one you get is not established. A
1766
+ wrong refusal costs you one call and tells you the valid names; a wrong
1767
+ acceptance can cost the page in silence. If you genuinely need a prop this
1768
+ build does not declare, `element_set_source` writes the carriers verbatim and
1769
+ only warns.
1770
+ (This is the opposite of the free `props` path — see Workflow 5, where an
1771
+ undeclared key is stored with a warning, because there real layouts carry
1772
+ such props and refusing them would refuse YOOtheme's own work.)
1218
1773
  - **Source not in the list.** API Mapper sources only appear once
1219
1774
  they're PUBLISHED in API Mapper (not just saved). If you bind an
1220
1775
  unknown `source_name`, `element_bind_source` rejects it and the error
@@ -1229,8 +1784,9 @@ renders dynamic items.
1229
1784
  Switcher, Table) accept a source binding. Binding a single-item
1230
1785
  element like Headline returns a structured `validation` error.
1231
1786
  - **Forgetting `etag`.** Every write requires the optimistic-lock
1232
- etag. On `412 Precondition Failed` re-fetch via
1233
- `yootheme_builder_get_etag` and retry.
1787
+ etag. On `412 Precondition Failed`, re-read what you were writing
1788
+ first a fresh etag alone makes the retry succeed without telling you
1789
+ whether someone else's change is what you are about to overwrite.
1234
1790
  - **Mis-routing bind vs the read helpers.** `element_bind_source` AND
1235
1791
  its verify sibling `element_get_binding` (B4) are both first-class L1
1236
1792
  tools — call them directly by name. Only `element_unbind_source` stays
@@ -1243,7 +1799,7 @@ renders dynamic items.
1243
1799
  // Step 7. Bind a Posts source onto a Grid element. element_bind_source
1244
1800
  // is a first-class L1 tool — call it directly.
1245
1801
  yootheme_builder_element_bind_source({
1246
- template_id: "home",
1802
+ layout_id: "home",
1247
1803
  element_path: "/children/0/children/2/children/0",
1248
1804
  source_name: "wp_posts",
1249
1805
  etag: "abc123"
@@ -1252,16 +1808,55 @@ yootheme_builder_element_bind_source({
1252
1808
  // Response: { rel_path: "/children/0/children/2/children/0", etag: "def456", has_binding: true }
1253
1809
  // Verify (direct L1 call):
1254
1810
  yootheme_builder_element_get_binding({
1255
- template_id: "home", element_path: "/children/0/children/2/children/0"
1811
+ layout_id: "home", element_path: "/children/0/children/2/children/0"
1256
1812
  })
1257
1813
  // → { source_name: "wp_posts", source_config: { ... }, ... }
1258
1814
  ```
1259
1815
 
1260
- **Edge case:** A Source can render zero items at runtime (e.g. empty
1261
- search filter). The bind call still succeeds; the front-end Grid just
1262
- shows the YOOtheme "no items" placeholder. Don't treat empty render
1263
- as a binding failure. Verify by re-reading
1264
- `yootheme_builder_element_get_binding` directly.
1816
+ **Edge case:** A Source can render zero items at runtime (e.g. an empty
1817
+ search filter, or an archive source on a single page that has no term to
1818
+ scope to). The bind call still succeeds; the front-end Grid just shows the
1819
+ YOOtheme "no items" placeholder. That is a DATA state, not a broken binding.
1820
+
1821
+ **But do not verify it by re-reading the binding.** `element_get_binding`
1822
+ answers "is a source attached", which is a different question from "does it
1823
+ deliver rows", and no amount of re-reading it will ever turn into the second
1824
+ answer. A customer once bound, published, looked, re-bound with `limit`,
1825
+ published, looked again, and only then ran `render_preview` — two full cycles
1826
+ for one fact.
1827
+
1828
+ There is ONE thing the re-read does now answer, and it is the one a blank card
1829
+ sends you looking for: if a `field_mappings` VALUE names a source field the
1830
+ source does not emit, `element_get_binding` says so — the same three keys the
1831
+ bind answers with, under the same names: `unknown_source_field_values`,
1832
+ `unknown_source_field_values_available` (what you should have written) and
1833
+ `unknown_source_field_values_hint`. That warning used to appear exactly once, at the
1834
+ moment of writing, so whoever debugged a blank card LATER read the state back
1835
+ and saw a binding indistinguishable from a healthy one. Absence of the keys
1836
+ means "nothing to report OR the source's field set could not be resolved here",
1837
+ never "checked and fine" — and it says nothing about the KEY side: a misspelled
1838
+ element prop is still read back as healthy, which is why the bind refuses it up
1839
+ front instead.
1840
+
1841
+ What the tools give you instead:
1842
+
1843
+ - Every bind answers with a `checks` block: `source_exists`, `field_mappings`,
1844
+ `field_mapping_props_checked`, `container_item_ok`. It costs nothing and is
1845
+ always there. It also states its own limit — none of them says whether rows
1846
+ arrive. Read the pair `field_mappings` + `field_mapping_props_checked`
1847
+ together: the first is a COUNT, not a verdict (a customer read
1848
+ `field_mappings: 2` as confirmation while one of the two was a typo), the
1849
+ second says whether those keys were measured against the element's declared
1850
+ props at all — `false` means the type's field list did not resolve here, not
1851
+ that a check failed.
1852
+ - `element_bind_source({… , probe: true})` renders the binding and answers
1853
+ `probe: {rows, rendered, empty, probed_path}`; at zero rows it adds a warning
1854
+ naming the likely causes. It costs a real render (~0.3-0.6s), so it is off by
1855
+ default. `probed_path` is the node that was rendered — the nearest container,
1856
+ because a `grid_item` renders nothing outside its `grid`.
1857
+ - `page_publish` / `page_save` report `binding-renders-nothing` in `quick_check`
1858
+ when the page's render produced zero rows and something on it was supposed to
1859
+ iterate. That one is free: the publish already renders for its own precheck.
1265
1860
 
1266
1861
  **Success criterion:** After publish, the Grid on the front-end shows
1267
1862
  items from the Source (verify by item count and at least one
@@ -1322,44 +1917,50 @@ a row layout. (Cross-template duplication is **not** supported by
1322
1917
  `element_clone`. See "Important scope note" below.)
1323
1918
 
1324
1919
  **Important scope note:** `yootheme_builder_element_clone` is
1325
- **sibling-only and intra-template**. Its real schema is
1326
- `{ template_id, element_path, etag }`. There is **no** `destPageId`
1327
- or `destParentPath`. The cloned element lands at the same parent,
1328
- right after the source. To move the clone elsewhere in the SAME
1329
- template, call `yootheme_builder_element_move` afterwards. To
1330
- duplicate into a DIFFERENT template, flag to the user that
1331
- cross-template clone is not currently supported and suggest a
1332
- CMS-level template duplication (in wp-admin or Joomla administrator).
1920
+ **intra-template**. Its real schema is
1921
+ `{ layout_id, element_path, etag, to_parent_path?, to_index? }`.
1922
+ There is **no** `destPageId` and no cross-template argument. Omit
1923
+ `to_parent_path` and the copy lands at the same parent, right after
1924
+ the source; pass it and the copy lands under that parent instead
1925
+ (`to_index` picks the slot, default append) the follow-up
1926
+ `element_move` this used to require is no longer needed. To duplicate
1927
+ into a DIFFERENT template, flag to the user that cross-template clone
1928
+ is not currently supported and suggest a CMS-level template
1929
+ duplication (in wp-admin or Joomla administrator).
1333
1930
 
1334
1931
  **Canonical tool-call sequence (real parameter names, snake_case):**
1335
1932
 
1336
1933
  1. `yootheme_builder_health`: confirm host plugin reachable.
1337
1934
  2. `yootheme_builder_pages_list({ fields: ["id", "label"] })`:
1338
1935
  locate the template by `label`. Note its `id`.
1339
- 3. `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", arguments: { template_id } })`
1340
- returns a flat schema view (lighter than `page_get_layout`) showing every
1341
- element path + type. Pick the JSON-Pointer path of the section
1342
- to clone. `page_get_schema` is L2; call via the gateway.
1936
+ 3. `yootheme_builder_element_list({ layout_id })`
1937
+ returns a flat view (lighter than `page_get_layout`) showing every
1938
+ element path + type + label + binding. Pick the JSON-Pointer path of the
1939
+ section to clone. `element_list` is L1; call it directly, no gateway.
1343
1940
  4. `yootheme_builder_get_etag()`: fetch the optimistic-lock etag.
1344
- 5. `yootheme_builder_element_clone({ template_id, element_path: "<src-path>", etag: "<etag>" })`
1941
+ 5. `yootheme_builder_element_clone({ layout_id, element_path: "<src-path>", etag: "<etag>" })`
1345
1942
  clones as sibling. Returns `{ path: "<new-path>", etag: "<fresh>" }`.
1346
1943
  The new path is at the same parent, immediately after the source.
1347
- 6. (Optional) `yootheme_builder_element_move({ template_id, element_path: "<new-path>", to_parent_path: "<other-parent>", to_index: 0, etag: "<fresh>" })`
1348
- re-parents the clone within the same template if needed.
1349
- 7. `yootheme_builder_element_update_settings({ template_id, element_path: "<final-path>", props: { ... }, etag: "<fresh>" })`
1944
+ 6. (Optional) To land the copy somewhere other than beside its source,
1945
+ pass the destination to step 5 directly:
1946
+ `yootheme_builder_element_clone({ layout_id, element_path: "<src-path>", to_parent_path: "<other-parent>", to_index: 0, etag: "<etag>" })`.
1947
+ `yootheme_builder_element_move` remains the way to relocate an
1948
+ element that already exists.
1949
+ 7. `yootheme_builder_element_update_settings({ layout_id, element_path: "<final-path>", props: { ... }, etag: "<fresh>" })`
1350
1950
  replaces the `props` on the clone. **Existing props NOT in the
1351
1951
  request are removed** (update_settings is a full replace by default;
1352
1952
  pass `merge: true` to apply a server-side deep-merge instead).
1353
1953
  Read the current props first via `yootheme_builder_element_get`
1354
1954
  if you only want to tweak a subset.
1355
- 8. `yootheme_builder_page_save({ template_id, etag })`
1356
- then `yootheme_builder_page_publish({ template_id, etag })`.
1955
+ 8. `yootheme_builder_page_save({ layout_id, etag })`
1956
+ then `yootheme_builder_page_publish({ layout_id, etag })`.
1357
1957
 
1358
1958
  **Common pitfalls:**
1359
1959
 
1360
1960
  - **Inventing destination parameters.** `element_clone` does NOT
1361
- accept `destPageId`, `destParentPath`, or any cross-template
1362
- argument. It's sibling-only within ONE template.
1961
+ accept `destPageId` or any cross-template argument. Its destination
1962
+ is optional and stays within ONE template: `to_parent_path` plus
1963
+ `to_index`, both snake_case.
1363
1964
  - **Treating `element_update_settings` as a merge by default.** The handler
1364
1965
  REPLACES the entire `props` object on the element unless you pass
1365
1966
  `merge: true`. Read the existing shape via `yootheme_builder_element_get`
@@ -1370,16 +1971,422 @@ CMS-level template duplication (in wp-admin or Joomla administrator).
1370
1971
  at roughly 200 characters with a visible `…(+N chars)` marker when
1371
1972
  truncated, never a silent drop. There's no need to cross-check via
1372
1973
  `element_list` just to see what a specific prop is currently set to.
1974
+ - **A `module`/`module_position` element is not a dead end either (T1.4).**
1975
+ On its own, a WordPress Widget or Joomla Module element's `props` are just
1976
+ a reference (`{widget: "custom_html-1"}` / `{module: 42}`) — no title, no
1977
+ content, no settings. `element_get` on that node ALSO carries a `resolved`
1978
+ field: the referenced widget/module's own row (title/type/settings/
1979
+ theme_overlay — the SAME shape `local_content_read(kind:"modules")`
1980
+ returns), or `resolved: null` plus a `resolved_reason` string when it can't
1981
+ be resolved (missing reference, deleted id, empty position) — never a
1982
+ throw. A `module_position` node resolves to `{position, modules: [...]}`,
1983
+ every widget assigned there. There's no need to separately call
1984
+ `local_content_read` just to see what a module element actually shows.
1985
+ - **"Where does this widget/module appear?" — `include_usage:true` (T1.6).**
1986
+ `local_content_read({ kind: "modules", include_usage: true })` adds a `usage`
1987
+ object per row: `element_refs` is the EXACT `{layout_id, path,
1988
+ via:"element"|"position"}` list of every layout location that reaches this
1989
+ widget/module — `via:"element"` is a DIRECT `module` element reference,
1990
+ `via:"position"` is an INDIRECT one (a `module_position` element pointing at
1991
+ this row's own `position`) — so you know whether to edit the element or the
1992
+ position assignment. `declared.position_visibility` classifies the row's
1993
+ `position` into `site-wide` (a theme panel/area — appears wherever that
1994
+ position renders), `never-standalone` (a `builder-1`..`builder-6` position —
1995
+ RENDERS ONLY through a `module_position` element, never on its own — live
1996
+ A/B-proven on Joomla), or `not-on-builder-pages` (`sidebar` — not rendered on
1997
+ Builder pages); null when the row has no position at all.
1998
+ `declared.assigned_pages` is Joomla-only real assignment info (`mode`
1999
+ `all`/`except`/`selected`/`none` resolved to concrete menu-item titles); on
2000
+ WordPress it is the explicit platform fact `{applicable:false, reason:
2001
+ "WordPress widget areas are global — widgets are not assigned to individual
2002
+ pages."}` — never simply absent. `declared` states what the CONFIGURATION
2003
+ claims, not what a visitor actually sees — a visibility plugin (Advanced
2004
+ Module Manager, Widget Context, …) or a time-gated rule can still override
2005
+ it. Default `false`: the layout scan this costs is never paid unasked.
2006
+ **A row with NO `usage` is not automatically a row you did not ask about.**
2007
+ There are THREE states and the key `usage_unavailable` is what separates the
2008
+ last two: neither `usage` nor `usage_unavailable` means `include_usage` was
2009
+ not requested; `usage` present means the enrichment ran; `usage_unavailable`
2010
+ present means it WAS requested and FAILED for that row. The row's primary
2011
+ fields (id, title, position, active, ...) stay fully trustworthy either way:
2012
+ an optional enrichment never deletes the answer it was decorating. Read
2013
+ `usage_unavailable` before concluding "this widget is used nowhere": on such
2014
+ a row nothing at all is known about where it appears.
2015
+ - **"What does a GUEST actually get served?" — `verify_on_urls` (T1.7).** Pair
2016
+ it with `include_usage:true` to add `usage.observed.urls[]` per row: fetches
2017
+ each url ONCE, anonymously, and checks whether this row's DOM signature
2018
+ (`id="<widget-id>"` on WordPress, `id="module-<id>"` on Joomla) appears —
2019
+ PLUGIN-AGNOSTIC, since it reads the rendered result, never any plugin's
2020
+ rules, so every visibility mechanism is covered without a line of
2021
+ plugin-specific code. Per url: `present`, `absent`, `not_detectable`, or
2022
+ `error`. `not_detectable` — NOT `absent` — covers FOUR cases, all meaning a
2023
+ missing signature proves nothing. **(1)** A `builder-1`..`builder-6`
2024
+ (`never-standalone`) position with no `via:"position"` reference: such a
2025
+ position renders EXCLUSIVELY through an explicit `module_position`
2026
+ element, so with none swept, YOOtheme's element-bound (DOM-anonymous, no id
2027
+ at all) rendering can't be ruled out. **(2)** The row sits in no position at
2028
+ all (empty `position` — an unplaced WordPress widget, Joomla's "NONE"
2029
+ group): nothing is expected on any url either way, so a missing signature
2030
+ says nothing about it. **(3)** The row is ALSO placed as a `module` ELEMENT
2031
+ **on the template THIS url renders**: that path carries no DOM id either, so
2032
+ even on an otherwise checkable position a not-found signature cannot tell
2033
+ "not rendered" from "rendered anonymously". MEASURED live on
2034
+ `dev5wp.wootsup.com/mcp-module-probe/`: `custom_html-1` has both an
2035
+ `element` and a `position` reference, and one document served its content
2036
+ **twice** while `id="custom_html-1"` appeared **once** — the element-rendered
2037
+ copy is `<div class="uk-panel widget widget_custom_html">`, with no instance
2038
+ attribute at all. The signature is not widened to cover it because the only
2039
+ token there is the widget TYPE class, which occurs 3× on that same page: a
2040
+ wider match would trade a false `absent` for a false `present`.
2041
+ **(4)** Where that element renders **could not be determined** — either the
2042
+ url did not map to a template (`pages_resolve` declines it; a Joomla site
2043
+ root served by a theme template is the common case), or the element sits on
2044
+ a THEME template, which governs many urls and is not enumerated here.
2045
+ **Cases (3) and (4) are decided PER URL, not per row.** An element reference
2046
+ on ANOTHER page cannot render on this url — a per-page layout renders on
2047
+ exactly its own page — so there you get the measured `present`/`absent`
2048
+ instead of a withheld answer. MEASURED on both platforms: `custom_html-1`'s
2049
+ element reference is on `page:2987` while the queried url was the site root
2050
+ (`page:1`), an anonymous fetch found 0 signature hits and 0 content hits, and
2051
+ `absent` is the correct decidable answer. The `reason` on every
2052
+ `not_detectable` names which case applies and, for (3)/(4), which template.
2053
+ Any OTHER
2054
+ position (`site-wide` — a real theme panel like navbar/header/a template's
2055
+ own `bottom`/`top` — or `not-on-builder-pages`) renders
2056
+ DIRECTLY from the theme with no element needed at all, so it's checkable
2057
+ even with completely empty `element_refs` — provided it has no `element`
2058
+ reference either. `error` = no observation was
2059
+ obtained from this url: a network failure, a timeout, a non-2xx, or a url
2060
+ the same-origin guard refused (off-site host, private address, disallowed
2061
+ scheme) — never treated as `absent`, never a throw. An `absent` result on a
2062
+ row `declared` calls unconditionally
2063
+ visible (`builder_visible:true`) adds a `discrepancy` — a named SUSPICION
2064
+ (a visibility plugin, a time-gated rule, a stale cache), never phrased as a
2065
+ diagnosis, and never attached to `not_detectable`/`error`.
2066
+ **Redirects are followed by this tool, bounded, and REPORTED.** When a url
2067
+ ends somewhere else, its entry carries `final_url` — the page whose markup
2068
+ was actually searched. That changes what the result means: `/contact` → 301
2069
+ → `/kontakt` scans the KONTAKT page, so a module assigned to `/contact`'s
2070
+ own menu item is not expected in it, and the `discrepancy` then LEADS with
2071
+ the measured redirect instead of naming a visibility plugin. Re-check with
2072
+ the `final_url` before suspecting anything else. Every hop is re-checked
2073
+ against the same-origin guard, so a redirect off-site is refused with its
2074
+ own reason rather than silently fetched. Momentary
2075
+ snapshot per url, a sample (max 10 urls) — never a full crawl. Omit
2076
+ (default): zero requests, `observed` stays absent.
2077
+ - **"What's on THIS page from widgets/modules?" — `include_modules` (T1.8), the
2078
+ reverse of T1.6/T1.7.** Where `local_content_read`'s `usage` answers "for
2079
+ this ONE module, which pages?", `include_modules:true` on
2080
+ `yootheme_builder_page_get_layout` (→ `modules_on_page`) and
2081
+ `yootheme_builder_pages_audit` (→ `modules_by_page[layout_id]`) answers
2082
+ "for THIS page, which modules?" — the SAME D11 index, read backwards, never
2083
+ a second scan. Each result splits `from_elements` (a `module`/
2084
+ `module_position` element placed DIRECTLY in this page's own layout) from
2085
+ `from_positions` (a `site-wide` theme position — navbar/header/footer —
2086
+ rendering here with NO element in this page's layout at all). On Joomla, a
2087
+ `site-wide` module can still be assignment-EXCLUDED from one specific page
2088
+ (`except`); when that decision can't be resolved for this page (no menu
2089
+ item addresses it, or more than one does), the row lands in
2090
+ `from_positions_undecidable` instead of being guessed in or out. Default
2091
+ `false`: the layout sweep this costs is never paid unasked, and the
2092
+ response is byte-identical without it.
2093
+ - **A short list is never silently a FAILED list — two presence-only keys say
2094
+ so.** `unresolved_positions` appears when a `module_position` element on
2095
+ that page could not be resolved (a database error, unavailable widget
2096
+ APIs), and `unresolved_module_universe` appears when the site's module
2097
+ store could not be read AT ALL — which makes `from_positions` unknowable
2098
+ rather than empty. Both are emitted ONLY on failure, so a healthy answer
2099
+ omits them and is byte-identical to before. If you see either, the answer
2100
+ is INCOMPLETE: do not conclude "this page has no modules", and above all do
2101
+ not create a replacement module to "fix" it.
2102
+ - **The same rule for reading, one level up: an empty `local_content_read` is
2103
+ never a failed one.** A read that did not run to completion answers
2104
+ `error_code: local_content_unavailable` (HTTP 503), never a 200 with
2105
+ `{items:[], total:0}` — so an empty-and-OK response can only mean the site
2106
+ genuinely has none of that kind. The landscape tool
2107
+ (`local_content_types`) is the deliberate exception: it is a composite of
2108
+ independent sub-reads, so instead of failing the whole call it keeps the
2109
+ parts it could read and lists the rest under `degraded` — every count named
2110
+ there is a placeholder, not a measurement.
2111
+ - **"Now change what you found" — `modules_update` (W2), the write
2112
+ counterpart of `local_content_read(kind:"modules")` / `include_modules`.**
2113
+ Updates a widget's (WordPress) or module's (Joomla) SAFE writable subset —
2114
+ `title`, `position`, `active`, `theme_overlay` on BOTH platforms; `showtitle`,
2115
+ `ordering`, `access`, `assignment` on JOOMLA ONLY (WordPress rejects those
2116
+ four with a 400). `settings`/`params`/`content` are NOT in the schema at all
2117
+ — content editing stays the CMS editor's job; this tool is the
2118
+ display/placement layer.
2119
+ `dry_run` DEFAULTS true: a bare call inspects and writes NOTHING, returning
2120
+ the `{before, after, changed, notes, after_read_back}` diff of what a real
2121
+ write WOULD do.
2122
+ Every field you do NOT pass stays byte-identical — proven at the SQL/option-write
2123
+ level, not merely "looks unchanged".
2124
+ **A real write REPORTS WHAT IT READ BACK, not what you asked for.** After
2125
+ committing, the Joomla arm re-reads the stored record and fills `after` and
2126
+ `changed` from that read. So a field that did not land is reported at its
2127
+ STORED value, with a `notes` entry naming the divergence — never echoed back
2128
+ as if it had succeeded. This is the ONE way a dry run's answer and a real
2129
+ write's answer can differ, and it is deliberate: measured on Joomla,
2130
+ `modules_update({showtitle:false, ordering:3})` once answered
2131
+ `after.showtitle:false` about a row that held `3`. If the verification read
2132
+ itself fails, the write still stands and `notes` says the answer is
2133
+ unverified rather than pretending it was checked. That holds per FIELD, not
2134
+ only for the whole row: `assignment` is read back from a second table, and
2135
+ when THAT read fails the requested assignment is kept and named unverified,
2136
+ never replaced by the `none` an unread table looks like.
2137
+ **`after_read_back` tells a MEASURED `changed` entry from a merely REQUESTED
2138
+ one.** It lists exactly the keys whose `after` came out of storage. A key
2139
+ named in `changed` but ABSENT from `after_read_back` is a transition this
2140
+ call asked for and did not measure — the write committed, so `changed` keeps
2141
+ naming it, but check it before treating it as landed. Measured on Joomla with
2142
+ the `#__modules_menu` read-back failing after a committed write:
2143
+ `changed: ["title","ordering","assignment"]` with
2144
+ `after_read_back: ["title","ordering"]`. Before this field existed those three
2145
+ were indistinguishable, and the only qualification was a `notes` sentence.
2146
+ A DIVERGING read still counts as read back: this list answers "did we look?",
2147
+ never "did we like what we saw?". It is `[]` for every dry run (nothing was
2148
+ written, so nothing could be read back) and always `[]` on WordPress — a
2149
+ plain statement, never a failure signal.
2150
+ **On WORDPRESS there is no read-back, and `after` is what you REQUESTED.**
2151
+ Not an oversight: a widget's instance is one serialised `wp_options` value,
2152
+ so the per-field storage divergence the Joomla read-back exists to catch
2153
+ cannot arise, and the failure that CAN arise, the write not reaching storage,
2154
+ is raised as an ERROR rather than shown as a diverging `after`. So on
2155
+ WordPress do not read `after` as "checked against storage"; read it as "this
2156
+ is what was asked, and it did not fail".
2157
+ **Every value is TYPE-CHECKED, never coerced.** Send real JSON types: the
2158
+ string `"false"` is NOT the boolean `false` (PHP reads a non-empty string as
2159
+ TRUE, so a coerced `"false"` would have PUBLISHED the module), `"7abc"` is
2160
+ not the integer `7`, and an object is not a `title`. A wrong type is
2161
+ REFUSED with a 400 naming the expected one — this tool never rewrites a
2162
+ value it did not understand.
2163
+ **`notes` is the channel for "correctly did nothing, and that is not the
2164
+ end of it".** It is a list of plain sentences, usually empty. The case that
2165
+ motivated it: a Joomla module with `published = 1` whose `publish_up` is in
2166
+ the FUTURE reads `active:false`, so `{active:false}` is a genuine no-op with
2167
+ `changed: []` — and the module still goes live on schedule. `notes` says so
2168
+ instead of letting an empty `changed` read as "already handled".
2169
+ **Read `notes` whenever it is NON-EMPTY — an empty `changed` is not the
2170
+ trigger.** This used to say "read it whenever `changed` comes back empty",
2171
+ and that sent readers past the note that matters most: the one saying a field
2172
+ in a NON-empty `changed` was never read back. A note is attached exactly when
2173
+ something needs saying, so its presence is the signal.
2174
+ **One consequence is REPORTED in `notes`, not refused: the module will render
2175
+ TWICE.** If a Module/Widget element already embeds this module and you move it
2176
+ to a position that renders it on its own — a real theme position or widget
2177
+ area, not `builder-1`..`builder-6` — it appears once through each element and
2178
+ once through the position. `modules_create` refuses that same combination up
2179
+ front (`placement_position_conflict`); this verb has no `placement` argument
2180
+ to refuse it with, and both CMSs allow the move in their own UI, so blocking
2181
+ it here would be this tool inventing a restriction. The note names the
2182
+ elements and both ways back to one appearance. It rides on a `dry_run` too,
2183
+ which is where it is worth most.
2184
+ **`position_verified: false` reports on a check that did NOT happen, and it
2185
+ means here exactly what it means on `modules_create`.** Both verbs ask the
2186
+ same rule the same question, so the same site answers the same way: when the
2187
+ site's position list could not be read from there AND the name is not one of
2188
+ `builder-1` to `builder-6` (those are taken by name and need no list), the
2189
+ position is WRITTEN and the answer carries this field. The move happened; the
2190
+ check behind "an unknown position is refused BY NAME" did not, so a typo is
2191
+ still a typo. The field is ABSENT in the two cases with nothing to report —
2192
+ the value WAS vouched for, or this call wrote no position at all — so read
2193
+ `after.position` to tell those apart. A `dry_run` never carries it, and
2194
+ neither does an update that names no position: it reports on a position THIS
2195
+ call wrote, never on the one the module already had.
2196
+ On WordPress, `position` must be a REGISTERED widget area
2197
+ (`$wp_registered_sidebars` — on a YOOtheme site that is the 13 theme
2198
+ positions plus `builder-1`..`builder-6`); WordPress core does NOT validate
2199
+ this itself, so a typo used to park the widget in a phantom area that
2200
+ renders nowhere while the response reported success. `theme_overlay` MERGES onto the stored overlay: a
2201
+ key you OMIT keeps its current value; only the keys you explicitly include
2202
+ are written (so calling `theme_overlay:{style:"card-alt"}` never resets
2203
+ `visibility` or any other already-set key). The overlay's key list is
2204
+ PLATFORM-SPECIFIC — WordPress `_theme` (17 keys, incl. `showtitle`) vs.
2205
+ Joomla `params.yoo_config` (18 keys, incl. `language_parent_icon`/
2206
+ `language_icon` INSTEAD of `showtitle`) — an unknown or wrong-platform key
2207
+ (or a wrong value type) is REJECTED with a 400 naming every allowed key,
2208
+ never silently dropped, never coerced. Two further writes are REFUSED
2209
+ outright rather than half-kept, because the stored state cannot be
2210
+ expressed by the field you sent: a `title` on a WordPress `block` widget
2211
+ (that type has NO title — its instance is `{content}` only, WordPress never
2212
+ renders one, and storing it would additionally SUPPRESS the preview title
2213
+ derived from the block markup), and an `active:true` on a Joomla module
2214
+ whose publish window would still leave it `scheduled`/`expired`. On Joomla
2215
+ `active` means the `published` column AND the `publish_up`/`publish_down`
2216
+ window — the SAME derivation `local_content_read` uses — so a module
2217
+ scheduled for the future already reads `active:false`, and "deactivating"
2218
+ it is correctly a no-op instead of silently cancelling the schedule — with
2219
+ a `notes` entry so the no-op is not mistaken for "already handled".
2220
+ `access` (Joomla only) is the module's VIEW LEVEL, not a flag: Joomla
2221
+ filters modules by `access IN (authorised view levels)`, and `access:0` is
2222
+ in NOBODY's set — not even a Super User's — so such a module renders for no
2223
+ one while looking published. That is what `local_content_read` reports as
2224
+ `builder_visible:false`; `modules_update({access: 1})` is the repair.
2225
+ `access:0` itself is refused, because 0 is the very state being repaired.
2226
+ Advanced/gateway-only.
2227
+ - **"Build one from scratch" — `modules_create` (W3), then `element_add` —
2228
+ the two-step recipe.** `modules_create` supports exactly FIVE known types:
2229
+ WordPress `custom_html` (title+content), `text` (title+text), `block`
2230
+ (content only, NO title); Joomla `mod_custom` (title+content COLUMN+
2231
+ showtitle), `mod_menu` (title + the params keys menutype/base/startLevel/
2232
+ endLevel/showAllChildren/tag_id/class_sfx/window_open/layout/
2233
+ moduleclass_sfx/cache/cache_time/cachemode). An unknown type is rejected
2234
+ with this exact list. `placement` is REQUIRED (`position`/`module_element`/
2235
+ `position_element`, i.e. how the module reaches a page); a call that omits it
2236
+ creates NOTHING and instead returns `ways`, every way for this platform with
2237
+ its reach and a summary, plus `notes`. `content`/`text` are stored
2238
+ byte-for-byte, same no-sanitization guarantee as `modules_update`.
2239
+ **`position` is ONE argument with ONE meaning on both platforms**: the
2240
+ template position or widget area the module goes into, validated SERVER-side
2241
+ against the positions this site declares or registers, because the set of
2242
+ areas is a property of the SITE and no schema here can know it. **Joomla
2243
+ takes it AT CREATE TIME**, exactly as WordPress does; it stopped being a
2244
+ `modules_update`-only concern on 2026-08-06. The value must agree with
2245
+ `placement` or the call is refused BY NAME rather than guessed:
2246
+ `placement_needs_a_position` for `placement:"position"` OR
2247
+ `placement:"position_element"` with no position (both ways need one and they
2248
+ need different values, so neither is defaulted),
2249
+ `placement_position_conflict` for a `builder-N` position under
2250
+ `placement:"position"` (a builder position renders nothing on its own), for a
2251
+ REAL theme position under `placement:"module_element"` (measured on both
2252
+ platforms: the module then renders TWICE on that page), or for a non-builder
2253
+ position under `placement:"position_element"`; and
2254
+ `placement_position_unknown` for a value this site does not declare at all,
2255
+ or that could not be a position at all because it is longer than 50
2256
+ characters. That 50 is the width of the JOOMLA column that stores a position;
2257
+ WordPress keeps widget areas as option keys with no width of their own, and
2258
+ the bound is enforced on both platforms anyway, because `position` is one
2259
+ argument with one meaning and the narrower platform is what bounds it. That
2260
+ last one is answered by the SAME
2261
+ rule on both platforms, and when the site's list could be read the refusal
2262
+ carries it as `declared_positions` as well as naming it in the message.
2263
+ Under `placement:"module_element"` a Joomla module is created with no position
2264
+ and assigned to ALL pages, so it is immediately visible to `element_add`. On
2265
+ WordPress the same placement REQUIRES a builder area since 2026-08-08: a
2266
+ widget exists only inside a registered area, and this tool asks which one
2267
+ instead of picking `builder-1` for you.
2268
+ **A `position` is not by itself a rendering.** `builder-1`..`builder-6` is
2269
+ classified `never-standalone` on BOTH platforms (that is what
2270
+ `local_content_read(include_usage:true)` reports back), so a `builder-N`
2271
+ position means "it now has a home", not "it now shows up". The
2272
+ live A/B that proved a `builder-N` module produces zero frontend output was
2273
+ measured on Joomla; on WordPress the classifier reports the same value and
2274
+ the same rule is what `next_step` assumes. Either way, step 2 (`element_add`)
2275
+ is what actually puts it on a page. Do not read a freshly created row as
2276
+ visible until an element references it.
2277
+ **`builder_visible` can come back as the STRING `unknown_until_next_request`
2278
+ on a real WordPress create.** WordPress builds `$wp_registered_widgets` once
2279
+ per request, so the widget this very call just wrote is not in it yet: the
2280
+ honest answer is neither `true` nor `false`. It is a timing artefact of the
2281
+ same request, not a fault and not a warning. Read the row again (any later
2282
+ request, e.g. `local_content_read({kind:"modules"})`) to get the real
2283
+ boolean, and never branch on it as if it were `false`.
2284
+ `dry_run` DEFAULTS true, same discipline as `modules_update`: once
2285
+ `placement` is chosen, a bare call previews `{type, position, fields}` and
2286
+ creates nothing. A REAL create
2287
+ (`dry_run:false`) returns the full created `row` (the SAME
2288
+ `local_content_read(kind:'modules')` D2 shape) PLUS `next_step` — the
2289
+ EXACT follow-up call **for the placement you chose**, one shape per choice:
2290
+ | `placement` | `next_step` |
2291
+ |---|---|
2292
+ | `module_element` | `element_add{element_type:"module", props:{widget:"custom_html-3"}}` on WordPress, `props:{module:"122"}` on Joomla |
2293
+ | `position_element` | `element_add{element_type:"module_position", props:{content:"builder-1"}}` |
2294
+ | `position` | Joomla: `modules_update{id, assignment}` to narrow it to chosen pages. WordPress: `local_content_read{kind:"modules", include_usage:true}` — read-only, because WordPress has no per-page control to narrow it with; add `verify_on_urls` to PROVE guest-visible rendering rather than trust `active`/`builder_visible` alone |
2295
+ **Two live-measured details, both baked into `next_step` so you never have
2296
+ to remember them:**
2297
+ 1. **Joomla's `props.module` is a STRING** (`"122"`), never a bare
2298
+ integer — a mismatch here silently fails to resolve, it does not error.
2299
+ 2. **A `module_position` element names the POSITION, not one module** — it
2300
+ renders every published entry sitting there. That element accepts an
2301
+ optional `layout` prop from its own enum (`stack`/`grid`) — leaving it
2302
+ out is safe and defaults to `stack`; set it only if you want `grid`.
2303
+ `next_step` therefore leaves the key out. Never copy the `sidebar` value
2304
+ the element schema reports as that field's default: it is not one of the
2305
+ two accepted values and is rejected with a 422. This does NOT apply to
2306
+ the `module` form.
2307
+ Copy `next_step` verbatim, add `layout_id` + `etag` + `parent_path` when it
2308
+ is an `element_add` (that tool REQUIRES `layout_id` and `etag`, and unlike
2309
+ the fourteen tools that take an `element_path` it names its parent argument
2310
+ `parent_path`), then make the call. A `null` here is an answer, not a gap:
2311
+ the widget is already live site wide and nothing further is needed.
2312
+ **One more field the answer can carry, and it is the only one that reports on
2313
+ something that did NOT happen: `position_verified: false`.** It appears when a
2314
+ position was STORED that the server could not vouch for — the site's position
2315
+ list was not readable from there AND the name is not one of `builder-1` to
2316
+ `builder-6`, which are taken by name and need no list. The write happened; the
2317
+ check behind this tool's "an unknown position is refused BY NAME" promise did
2318
+ not, so the value is in place unexamined and a typo is still a typo. The field
2319
+ is ABSENT in the two cases where there is nothing to report — the stored
2320
+ position WAS vouched for, or no position was stored at all — so absence alone
2321
+ does not mean "checked". Read `preview.position` to tell those two apart: it
2322
+ is empty only in the second. A `dry_run` never carries the field, so a preview
2323
+ cannot warn you that the position it shows will be stored unverified.
2324
+ **The answer also names what will render the module before you asked for
2325
+ it: `element_refs`.** Each entry is `{layout_id, path, via}` and there are
2326
+ exactly two ways to get one. `via: "element"` means a `module` element in
2327
+ some layout ALREADY names the id this call just took — WordPress reuses a
2328
+ deleted widget's number (the allocator is highest-instance-plus-one), so a
2329
+ new widget can inherit a reference nobody meant for it. `via: "position"`
2330
+ means a `module_position` element already renders the position you created
2331
+ into; that needs no reuse at all, happens on both platforms, and is the
2332
+ common one. The field is ALWAYS present, so `[]` is the answer "nothing
2333
+ renders it yet" rather than a build without the concept. A `dry_run` carries
2334
+ it too, and that is where it is worth most, since `dry_run` defaults to true
2335
+ and is the first call you make. On JOOMLA a `dry_run` can report only
2336
+ `via: "position"`: the module id is assigned by the database at insert time,
2337
+ so on a preview there is no id for a layout to reference. That is a plain
2338
+ statement about that arm, not a failure.
2339
+ **One asymmetry to know about that resolution step:** on the plugin's own MCP
2340
+ transport a large answer is trimmed to a response budget, top-level keys are
2341
+ dropped largest first, and `preview` is one of the large ones while this flag
2342
+ is the smallest. So a widget with several KB of content can hand you
2343
+ `position_verified: false` and no `preview` to resolve it with. Every dropped
2344
+ key is named in `omitted_keys`, so check there before reading an absent
2345
+ `preview` as "no position was stored". (The `retry_hint` that comes with a
2346
+ trimmed answer suggests raising `max_chars`; `modules_create` has no such
2347
+ argument, so that hint does not apply here.)
2348
+ Advanced/gateway-only.
2349
+ - **The fifteen typed failures of `modules_update` / `modules_create` /
2350
+ `modules_delete`, and what each one licenses you to do next.** These are their
2351
+ OWN family, separate from the `pages_audit`/`heading_outline` target-error
2352
+ table above. Three of
2353
+ the four `placement_*` codes belong to `modules_create` alone. The fourth,
2354
+ `placement_position_unknown`, is also what `modules_update` answers for a
2355
+ position this site does not declare, so that one mistake reads the same
2356
+ whichever verb you sent it with. All four are the ones a caller meets first,
2357
+ because they are decided before anything is written:
2358
+
2359
+ | `error_code` | HTTP | What it means | What to do |
2360
+ | --- | --- | --- | --- |
2361
+ | `modules.placement_not_chosen` | 400 | `modules_create` only. `placement` was missing or not one of `position` / `module_element` / `position_element`. Nothing was created, not even a preview: the check runs before `dry_run` is honoured. | The error carries BOTH halves of the answer, `ways` and `notes`. Read them, pick a way, resend with `placement` set. |
2362
+ | `modules.placement_needs_a_position` | 400 | `modules_create` only. The way you chose needs a `position` and the call carried none. WHICH ways need one is a platform fact since 2026-08-08: all three on WordPress, where a widget exists only inside a registered area, and two on Joomla, where a module with no position is a native state. They take different values, and none is defaulted. | Resend the SAME `placement` with `position` set. Switching placement to get past this builds something other than what you asked for. |
2363
+ | `modules.placement_position_unknown` | 400 | `modules_create` AND `modules_update`, the one code of this family both verbs send. The `position` named is not one this site declares, or could not be a position at all (longer than 50 characters, the width of the Joomla column that stores it; the bound is enforced on BOTH platforms so one argument keeps one meaning). The SAME code on both platforms and on both verbs. What DIFFERS is the offer inside the message, and with it the advice: `modules_update` has no `placement` argument, so its message names the positions this site declares plus `builder-1` to `builder-6` and its runtime `hint` says exactly that. The advice cell to the right is the `modules_create` one. | The MESSAGE already names the values this call can use, narrowed to the placement you chose, so take the next value from there: `placement: "position"` takes a template position this site declares that is at most 50 characters long, `placement: "position_element"` takes `builder-1` to `builder-6` and nothing else, and `placement: "module_element"` takes no template position at all and only `builder-1` to `builder-6` if you name one. `declared_positions` rides alongside as the raw fact of everything this site declares, which is more than any one placement takes: it can even hold a value no call accepts, because a site may declare a position wider than the column that stores it. So when the two differ the message is the shorter, safer list. A value taken from `declared_positions` under the wrong placement is refused again as `placement_position_conflict`. When the error carries no list, the site's list could not be read and the value has to come from the template itself. |
2364
+ | `modules.placement_position_conflict` | 400 | `modules_create` only. `position` and `placement` are each fine alone and describe DIFFERENT outcomes together: a `builder-N` under `placement:"position"`, a real theme position under `placement:"module_element"`, a non-builder position under `placement:"position_element"`. | Change one of the two and resend. The message names the combination and the call that does what you asked for. |
2365
+ | `modules.invalid_field` | 400 | A field is unknown, unwritable (`settings`/`params`/`content`), wrong-platform, or the wrong JSON TYPE. The message names the allowed keys or the expected type. | Fix the call. Nothing was written; the module is untouched. |
2366
+ | `modules.field_value_invalid` | 400 | The KEY is right and the VALUE is not: a wrong JSON type (the string `"false"` is not the boolean `false`), a value outside a closed set (an unknown widget/module type, an unrecognised `assignment.mode`), or outside a permitted range (`menu_ids` must be positive). The largest of these classes. | Correct the VALUE of the named key and resend. Renaming the key will not help: the key is already right. The message names the expected type or the allowed values. |
2367
+ | `modules.field_not_on_this_type` | 400 | The field name is fine and this tool can write it, but THIS widget/module type has no such field. Nothing to rename. | The message names the types that DO carry it, and where this type keeps the equivalent instead (block markup, `theme_overlay`, or the type-specific params). Pick another type, or put the value where this type keeps it. |
2368
+ | `modules.field_combination_invalid` | 400 | Every field is valid ALONE; together they contradict each other, or one needs a partner the call did not carry. | Change the COMBINATION, not a value: add the missing field or drop one of the pair. The message names which of the two it is and in which direction. |
2369
+ | `modules.position_not_clearable` | 400 | **WordPress only.** You sent `position: ""` to take the widget out of every area, and on WordPress a widget exists only while it sits in one. Joomla accepts the same value, because a module there can genuinely sit in none. So this is about the platform, not about the value being malformed. | Send `active: false` instead: it parks the widget in `wp_inactive_widgets`, and naming a position again brings it back. Nothing was written. |
2370
+ | `modules.blocked_by_cms_state` | 409 | Name, value and combination are all correct. The CMS is in a state that would make the outcome untrue anyway, and this tool deliberately does not write the columns that would change it. | Nothing to fix in the call. The message names the exact field and value standing in the way; change it in the CMS module manager and repeat the call. |
2371
+ | `modules.invalid_id` | 400 | Joomla only, and only for the id-addressed verbs: `moduleId` was not a positive integer. WordPress has no counterpart, because its widget ids are strings such as `custom_html-2`. | Read the id off `local_content_read({kind:"modules"})` rather than constructing it. |
2372
+ | `modules.not_found` | 404 | The store WAS read and this id is genuinely not in it. | The module really is gone. Creating a replacement is legitimate here. |
2373
+ | `modules.element_refs_present` | 409 | `modules_delete` only. The request is fine; the STATE is not. Layout elements still reference this module directly, and the message names each one. | Delete those elements with `element_delete`, or repeat with `acknowledge_element_refs: true` to accept the dangling references. Nothing was removed. |
2374
+ | `modules.write_failed` | 500 | A write was ATTEMPTED and did not land. Every writing verb on both platforms answers this way: the create that stored nothing, the update whose change never reached the column, the delete that removed nothing. Reporting success would have been a positive false statement about the state of the store. | Re-read the module before retrying: the message names what did and did not reach storage. On Joomla the whole call shares one transaction, so it is all or nothing; on WordPress the widget instance and its placement are separate writes, so the message may report a half-applied call. |
2375
+ | `modules.unavailable` | 503 | The widget/module store could not be reached AT ALL. | **Never create a replacement.** Absence of an answer is not absence of the module. Retry later, or tell the user the store is down. |
2376
+
2377
+ The 404-vs-503 split is the load-bearing one: before it existed, an
2378
+ unreachable store answered 404 and an agent dutifully created a duplicate
2379
+ for a module that was there the whole time.
1373
2380
  - **Clone-then-update path drift.** The clone returns a path that's
1374
2381
  correct at the moment of the call. If you fire off many ops in
1375
2382
  parallel, a concurrent edit may shift indices. Refresh via
1376
- `get_etag` + `page_get_schema` (via gateway) between independent batches.
2383
+ `get_etag` + `element_list` between independent batches.
1377
2384
  - **Cloning a bound element keeps the binding.** `element_clone`
1378
2385
  copies the entire element including `props.source`. If the user
1379
2386
  wanted a "data-free" copy, call
1380
2387
  `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", arguments: { ... } })`
1381
2388
  on the new path afterwards.
1382
- - **Wrong parameter names.** Use `template_id`, `element_path`,
2389
+ - **Wrong parameter names.** Use `layout_id`, `element_path`,
1383
2390
  `etag` (NOT `pageId`, `srcPath`, `ifMatch`).
1384
2391
 
1385
2392
  **Worked example (tool-call snippet):**
@@ -1387,7 +2394,7 @@ CMS-level template duplication (in wp-admin or Joomla administrator).
1387
2394
  ```jsonc
1388
2395
  // Step 5. Clone the section element as a sibling.
1389
2396
  yootheme_builder_element_clone({
1390
- template_id: "home",
2397
+ layout_id: "home",
1391
2398
  element_path: "/children/0/children/2", // the hero section to duplicate
1392
2399
  etag: "abc123"
1393
2400
  })
@@ -1395,11 +2402,11 @@ yootheme_builder_element_clone({
1395
2402
 
1396
2403
  // Step 7. Tweak the clone (replace props entirely, or pass merge: true).
1397
2404
  const current = yootheme_builder_element_get({
1398
- template_id: "home",
2405
+ layout_id: "home",
1399
2406
  element_path: "/children/0/children/3",
1400
2407
  });
1401
2408
  yootheme_builder_element_update_settings({
1402
- template_id: "home",
2409
+ layout_id: "home",
1403
2410
  element_path: "/children/0/children/3",
1404
2411
  props: { ...current.props, style: "secondary" }, // section background = `style`
1405
2412
  etag: "def456"
@@ -1413,7 +2420,7 @@ copy, follow up with
1413
2420
  on the new path. Verify with a direct `element_get_binding` call.
1414
2421
 
1415
2422
  **Success criterion:** After publish,
1416
- `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", arguments: { template_id } })`
2423
+ `yootheme_builder_element_list({ layout_id })`
1417
2424
  shows the new section at the cloned path with the user's tweaks reflected in
1418
2425
  `element_get` on that path.
1419
2426
 
@@ -1426,34 +2433,91 @@ guessing, and without rotating the user's key unnecessarily.
1426
2433
 
1427
2434
  **Canonical tool-call sequence:**
1428
2435
 
1429
- 1. `yootheme_builder_diagnose` is a single probe that hits `/health` (no
1430
- auth) and then `/etag` (Bearer auth). Returns
1431
- `{ plugin_reachable, plugin_version, yootheme_loaded, yootheme_version,
1432
- endpoint_count, bearer_valid, bearer_error?, site_url?, home_url?,
1433
- summary?, capability_ok?, capability_warning?, effective_capabilities? }`.
1434
- Call this **before** any other tool when you see
1435
- auth errors. (Takes no arguments. The schema is `{}`.)
2436
+ 1. `yootheme_builder_diagnose` is a single self-diagnostic. Returns
2437
+ `{ platform, plugin_version, site_url, kid, scopes, token_scope,
2438
+ served_areas, capabilities, auth_bound, etag_ok, resolved_user_id,
2439
+ effective_capabilities, capability_ok, capability_warning, library_key,
2440
+ active_site }`. EVERY field is optional: one is absent when the host does
2441
+ not report it. Call this **before** any other tool when you see auth
2442
+ errors. (Takes no arguments beyond the optional `site_id`.)
2443
+
2444
+ `served_areas` and `effective_capabilities` name similar-sounding, DIFFERENT
2445
+ things: `served_areas` lists the tool AREAS this server serves (`pages`,
2446
+ `elements`, `sources`, `inspection`) — a server property, the same list
2447
+ `yootheme_builder_health` carries under the same name. `effective_capabilities`
2448
+ is what THIS caller's resolved CMS user may actually do
2449
+ (`can_create`/`can_edit`/`can_delete`) — see the bullet below. `capabilities`
2450
+ is `served_areas`' RETIRING name, kept one release as a byte-identical alias.
2451
+
2452
+ This list used to be longer, and the extra names never arrived — measured on
2453
+ four live installs. `plugin_error`, `yootheme_loaded`, `yootheme_version`,
2454
+ `endpoint_count`, `bearer_valid`, `bearer_error`, `summary` and `home_url`
2455
+ belonged to an older transport that assembled this report from two HTTP
2456
+ probes; today the plugin answers it in-process. For install facts (YOOtheme
2457
+ version, endpoint list, `home_url`) call `yootheme_builder_health`, which
2458
+ does carry them. For the bearer verdicts, read `auth_bound` and `etag_ok`
2459
+ right here — they are the successors of `bearer_valid` / `bearer_error`.
2460
+
2461
+ The two verdicts to read first are `auth_bound` (the Bearer key was accepted
2462
+ and resolved to a user) and `etag_ok` (a layout state was readable with it).
2463
+ There is no `plugin_reachable` field, and there is nothing to miss: the
2464
+ diagnostic is assembled by the plugin that received your call, so an answer
2465
+ at all IS the reachability answer. An unreachable plugin produces a
2466
+ transport error, never a payload.
2467
+
2468
+ Four of those are easy to skip past and answer questions you would
2469
+ otherwise take several calls to answer:
2470
+
2471
+ - `token_scope` (`read` / `write`). A `read` key fails every write tool
2472
+ with `read_only_token`, and that is a key problem, not a permissions one.
2473
+ - `effective_capabilities` + `capability_ok` + `capability_warning`. The
2474
+ CMS user the key resolves to, and what that user may actually do. A
2475
+ write-scoped key mapped to a user without edit rights still 403s;
2476
+ `capability_ok: false` is the only field that says so before you try.
2477
+ - `active_site` `{ site_id, via, process_scoped }`. Which site this call
2478
+ actually reached, and by which decision: `explicit` (you passed `site_id`
2479
+ — isolated from other clients of this server process), `use_site` (a
2480
+ runtime switch this process is on) or `registry_default` (the default in
2481
+ the sites file). The two inherited paths are kept apart deliberately: a
2482
+ call that reaches the wrong install answers just as confidently as one
2483
+ that reaches the right one. The field rides on EVERY tool call, not only
2484
+ this one. It replaces `process_default_site_id` + `resolved_via`, which
2485
+ this chapter kept naming after they had stopped shipping.
2486
+ - `site_url`. WHICH install answered, as the plugin reports it. `kid` and
2487
+ `auth_bound` cannot show that a perfectly valid key simply belongs to a
2488
+ different install, and that is the commonest cause of the refusal you are
2489
+ triaging. Read it against `active_site.site_id`, which names the registry
2490
+ entry your own config picked: when those two disagree, the disagreement
2491
+ is the finding.
2492
+ - `library_key`. Why the Layout Library tools can or cannot reach
2493
+ `api.yootheme.com`, as `{ configured, state, hint }`. Read `state`
2494
+ before you tell anyone to check their apikey field: see the Layout
2495
+ Library prerequisite section for the five states and who has to act on
2496
+ each. Never contains the key.
1436
2497
  2. **Interpret the result:**
1437
- - `plugin_reachable: false` the WordPress / Joomla install is down
1438
- OR the host plugin is deactivated. Send the user to **wp-admin
1439
- Plugins → activate "YT Builder MCP"** (WordPress) or **Joomla
1440
- administrator → Extensions → Plugins → enable "System - YT Builder
1441
- MCP" and the matching webservices + component entries** (Joomla).
1442
- Do not retry until they confirm.
1443
- - `plugin_reachable: true, bearer_valid: false` → the Bearer key is
1444
- wrong (typo, revoked, or wrong key for this install). The
1445
- `bearer_error` field carries the upstream HTTP status. Send the
1446
- user to:
2498
+ - **The call itself failed** (a transport error, not a payload) → the
2499
+ WordPress / Joomla install is down OR the host plugin is deactivated.
2500
+ Send the user to **wp-admin → Plugins → activate "YT Builder MCP"**
2501
+ (WordPress) or **Joomla administrator → Extensions → Plugins → enable
2502
+ "System - YT Builder MCP" and the matching webservices + component
2503
+ entries** (Joomla). Do not retry until they confirm.
2504
+ - `auth_bound: false` → the Bearer key is wrong (typo, revoked, or wrong
2505
+ key for this install). Check `site_url` first: a key that is valid
2506
+ somewhere else fails here for a reason nothing about the key will show.
2507
+ For the upstream HTTP status call `yootheme_builder_sites_test`, which
2508
+ probes `/etag` itself and answers `bearer_valid` + `summary`; diagnose
2509
+ names no status of its own, because the request it is answering already
2510
+ got through. Then send the user to:
1447
2511
  - **WordPress:** wp-admin → Tools → "YT Builder MCP" → Bearer Keys
1448
2512
  → copy the existing key into their MCP client config, or
1449
2513
  generate a new one.
1450
2514
  - **Joomla:** Components → YT Builder MCP → Bearer Keys → same.
1451
- - `plugin_reachable: true, bearer_valid: true` but the original
2515
+ - `auth_bound: true, etag_ok: true` but the original
1452
2516
  tool returned a 403 → the key works but the scope is too low for
1453
2517
  the tool's required scope (`write` for mutations, `admin` for
1454
2518
  destructive operations). Ask the user to regenerate the key with
1455
2519
  a higher scope and restart the AI client.
1456
- - `plugin_reachable: true, bearer_valid: true, capability_ok: false` →
2520
+ - `auth_bound: true, capability_ok: false` →
1457
2521
  the Bearer key's scope IS write, but the CMS user it resolves to
1458
2522
  (`effective_capabilities.can_edit`, `can_create`, `can_delete`, all
1459
2523
  false) has no write capability at all, so writes will still 403
@@ -1472,7 +2536,8 @@ guessing, and without rotating the user's key unnecessarily.
1472
2536
  - "Restart Claude / Cursor / Zed / Continue / Cline / Roo Code /
1473
2537
  Claude Code / Codex CLI."
1474
2538
  - "Confirm with `yootheme_builder_diagnose` that
1475
- `bearer_valid: true` before retrying the original task."
2539
+ `auth_bound: true` and `etag_ok: true` before retrying the original
2540
+ task."
1476
2541
 
1477
2542
  **Common pitfalls:**
1478
2543
 
@@ -1499,29 +2564,33 @@ guessing, and without rotating the user's key unnecessarily.
1499
2564
  yootheme_builder_diagnose({})
1500
2565
  // Response shape:
1501
2566
  // {
1502
- // plugin_reachable: true,
2567
+ // platform: "wordpress",
1503
2568
  // plugin_version: "1.2.0",
1504
- // yootheme_loaded: true,
1505
- // yootheme_version: "5.0.22",
1506
- // endpoint_count: 16,
1507
- // bearer_valid: false, // ← key is bad
1508
- // bearer_error: "HTTP 401: invalid_token",
1509
- // site_url: "https://example.com",
1510
- // home_url: "https://example.com"
2569
+ // kid: "5cb829a21f4d5e4f",
2570
+ // scopes: ["read", "write"],
2571
+ // token_scope: "write",
2572
+ // auth_bound: false, // ← key is bad
2573
+ // etag_ok: false,
2574
+ // site_url: "https://example.com", // ← check this FIRST: right key, wrong install?
2575
+ // active_site: { site_id: "default", via: "registry_default", process_scoped: true }
1511
2576
  // }
1512
2577
  // → diagnosis: rotate the key. Send user to Tools/Components → YT Builder MCP.
2578
+ // → for the upstream HTTP status: yootheme_builder_sites_test({ site_id: "default" }).
1513
2579
  ```
1514
2580
 
1515
- **Edge case:** `plugin_reachable: true` but `yootheme_loaded: false`
1516
- means the user installed the MCP host plugin but YOOtheme Pro itself
1517
- isn't active. The MCP server still answers, but every tool that
2581
+ **Edge case:** `yootheme_builder_health` answers but reports
2582
+ `yootheme_loaded: false` — the user installed the MCP host plugin but YOOtheme
2583
+ Pro itself isn't active. Read that off **health**, not diagnose: diagnose used
2584
+ to declare the field and never filled it. (Health's WordPress arm reports it;
2585
+ the Joomla health answer is assembled before the builder bootstraps and does
2586
+ not carry it, so its absence there means "not knowable here", never `false`.) The MCP server still answers, but every tool that
1518
2587
  touches the YOOtheme layout returns an empty/error response. Surface
1519
2588
  the mismatch ("YOOtheme Pro is not active on this install") instead
1520
2589
  of retrying. On Joomla this can also surface as a "YOOtheme Pro
1521
2590
  required" admin notice in the component dashboard.
1522
2591
 
1523
2592
  **Success criterion:** A subsequent `yootheme_builder_diagnose`
1524
- returns `plugin_reachable: true` AND `bearer_valid: true`. The
2593
+ returns `auth_bound: true` AND `etag_ok: true`. The
1525
2594
  original tool now returns a non-auth response.
1526
2595
 
1527
2596
  ---
@@ -1546,22 +2615,28 @@ an instance with a sensible default props payload.
1546
2615
  `element_type` (`type_name` is a DEPRECATED alias, kept only for
1547
2616
  1.0.x back-compatibility).
1548
2617
  4. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
1549
- `yootheme_builder_page_get_layout({ template_id, flat: false })`
2618
+ `yootheme_builder_page_get_layout({ layout_id, flat: false })`
1550
2619
  locate the `parent_path` (JSON-Pointer) where the new element
1551
- should land.
2620
+ should land. A content/leaf type is only accepted inside a `column`
2621
+ (never directly in a `section` or a `row`) — if the layout has none yet,
2622
+ call `yootheme_builder_scaffold_section` first and use its
2623
+ `column_paths` as `parent_path` instead of hunting for one by hand.
1552
2624
  5. `yootheme_builder_get_etag()`: fetch the optimistic-lock etag.
1553
- 6. `yootheme_builder_element_add({ template_id, parent_path: "<path>", element_type: "<picked-name>", props: { ... }, etag })`
1554
- validates `props` against the live type schema. On a type with a
1555
- known schema, an unknown prop key is HARD-REJECTED (not silently
1556
- dropped): the error always lists `valid_prop_keys`, plus a
1557
- `did_you_mean` suggestion when a close field exists (e.g. `contnet`
1558
- to `content`). Read those from the error and retry; do not re-guess.
1559
- 7. (Optional) `yootheme_builder_element_update_settings({ template_id, element_path: "<new-path>", props: { ... }, etag })`
2625
+ 6. `yootheme_builder_element_add({ layout_id, parent_path: "<path>", element_type: "<picked-name>", props: { ... }, etag })`
2626
+ checks `props` against the live type schema. On a type with a known
2627
+ schema, a prop key the type does not declare is ACCEPTED and stored —
2628
+ the answer carries a `warnings` entry with `code: "unknown_prop_keys"`
2629
+ naming the key, plus a `did_you_mean` suggestion when a close field
2630
+ exists (e.g. `contnet` to `content`). Read that and correct; do not
2631
+ re-guess. The VALUE side is stricter: an invalid enum member, an
2632
+ emptied required field, or a `<p>`-wrapped headline `content` is
2633
+ REFUSED with 422 before anything is written.
2634
+ 7. (Optional) `yootheme_builder_element_update_settings({ layout_id, element_path: "<new-path>", props: { ... }, etag })`
1560
2635
  iterates on the props. **Note: this REPLACES `props` entirely by
1561
2636
  default; pass `merge: true` for a server-side deep-merge.** When
1562
2637
  replacing, include every key you want to keep.
1563
- 8. `yootheme_builder_page_save({ template_id, etag })`
1564
- then `yootheme_builder_page_publish({ template_id, etag })`.
2638
+ 8. `yootheme_builder_page_save({ layout_id, etag })`
2639
+ then `yootheme_builder_page_publish({ layout_id, etag })`.
1565
2640
 
1566
2641
  **Common pitfalls:**
1567
2642
 
@@ -1570,14 +2645,22 @@ an instance with a sensible default props payload.
1570
2645
  still works as a DEPRECATED alias, but prefer `element_type` to match
1571
2646
  every other tool in this domain. The server's Zod schema rejects any
1572
2647
  other key.
1573
- - **Unknown prop key = hard reject, not a silent drop.** On a type with a
1574
- known schema, a prop key that is not a real field is REJECTED before the
1575
- write. The error ALWAYS lists `valid_prop_keys` (your guaranteed anchor),
1576
- plus a `did_you_mean` suggestion only when a close field exists. The
1577
- reserved binding keys `source` / `source_extended` / `source_node` are
1578
- rejected too (bind a source with `yootheme_builder_element_bind_source`,
1579
- never via `props`). Correct from the error instead of retrying blind.
1580
- - **Wrong parameter names on `element_add`.** Use `template_id`
2648
+ - **Unknown prop key = a warning, not a rejection and not silence
2649
+ either.** On a type with a known schema, a prop key the schema does not
2650
+ declare is STORED and reported: `warnings: [{ code: "unknown_prop_keys",
2651
+ message }]`, with a `did_you_mean` suggestion inside the message when a
2652
+ close field exists. It is a warning rather than a refusal because an
2653
+ undeclared key is not always wrong — YOOtheme itself stores props a type
2654
+ does not declare (a `column`'s `order_first` is written by the ROW control
2655
+ and is read at render), and a layout authored on a newer YOOtheme carries
2656
+ props the installed build has not declared yet. Refusing them would refuse
2657
+ layouts YOOtheme wrote. So: read the warning, and either fix the typo with
2658
+ `unset: ["<key>"]` — valid WITHOUT a `props` body, in which case only those keys go and every other prop stays — or keep the key knowingly. To bind a source, use
2659
+ `yootheme_builder_element_bind_source` rather than writing `source` /
2660
+ `source_extended` through `props` — those carriers are not filtered out of
2661
+ a props write, so a hand-written one silently competes with the bind
2662
+ tool's.
2663
+ - **Wrong parameter names on `element_add`.** Use `layout_id`
1581
2664
  (not `pageId`), `parent_path` (not `parentPath`), `element_type`
1582
2665
  (not `type` / `name`), `props` (not `settings`), `etag` (not
1583
2666
  `ifMatch`).
@@ -1619,13 +2702,13 @@ yootheme_builder_element_type_get_schema({ element_type: "headline" })
1619
2702
  // <p> block — see "Typography & heading styles"). If you omit title_element it
1620
2703
  // DEFAULTS to h1, so set it explicitly on every headline to avoid multiple h1s.
1621
2704
  yootheme_builder_element_add({
1622
- template_id: "home",
1623
- parent_path: "/children/0/children/2", // row inside section
2705
+ layout_id: "home",
2706
+ parent_path: "/children/0/children/2/children/0", // a column inside a row inside a section
1624
2707
  element_type: "headline",
1625
2708
  props: { content: "Welcome", title_element: "h1" },
1626
2709
  etag: "abc123"
1627
2710
  })
1628
- // Response: { rel_path: "/children/0/children/2/children/0", etag: "def456" }
2711
+ // Response: { rel_path: "/children/0/children/2/children/0/children/0", etag: "def456" }
1629
2712
  ```
1630
2713
 
1631
2714
  **Edge case:** A child theme can override a built-in element's
@@ -1657,13 +2740,14 @@ exposes. Never analyze findings by hand and never bulk-apply a fix.
1657
2740
  write-approval prompt). Omit `checks`/`types` to run every category
1658
2741
  against every scanned template.
1659
2742
  2. Read the response's `findings` array. Each finding carries a `code`,
1660
- `category`, `severity`, a `count`, up to 30 affected `pages`, and
2743
+ `category`, `severity`, an `affected_pages` total, up to 30 affected
2744
+ `pages`, and
1661
2745
  (where a safe automatic fix exists) a `fix_hint` naming the exact
1662
2746
  write tool and args template to run. `duplicate-layout` and
1663
2747
  `title-collision` never carry a `fix_hint` — they need a human
1664
2748
  decision (rename, merge, or intentionally keep as-is), not an
1665
2749
  automatic patch.
1666
- 3. `yootheme_builder_template_summary({ template_id, include_nodes: true })`
2750
+ 3. `yootheme_builder_template_summary({ layout_id, include_nodes: true })`
1667
2751
  drills into one page a finding names. It carries the same
1668
2752
  `heading_levels`, `images_missing_alt`, and `disabled_elements` counters
1669
2753
  the audit used, plus (with `include_nodes:true` — opt-in, omit it for
@@ -1673,18 +2757,22 @@ exposes. Never analyze findings by hand and never bulk-apply a fix.
1673
2757
  is disabled or just hidden on mobile, in one call instead of pulling
1674
2758
  the raw `page_get_layout`. Page a large `content_nodes` list with
1675
2759
  `limit`/`cursor`; drill into several findings' pages in ONE call with
1676
- `template_ids:[...]` instead of `template_id` (returns `summaries` keyed
2760
+ `layout_ids:[...]` instead of `layout_id` (returns `summaries` keyed
1677
2761
  by id + `not_found`). `heading_levels` rides alongside a
1678
2762
  `heading_levels_note` sibling stating exactly which sources feed the
1679
2763
  census: every element whose title/meta channel resolves to a heading
1680
2764
  level, PLUS real `<h1>`-`<h6>` tags found inside `html`/`text`/`code`
1681
2765
  element markup. It does NOT count `module`/widget output — a WordPress
1682
2766
  widget or Joomla module has no content field this counter can parse.
2767
+ Since T1.4 that content is still readable, just not counted here:
2768
+ `yootheme_builder_element_get` on the `module`/`module_position` element
2769
+ resolves its referenced widget/module via a `resolved` field (see
2770
+ Workflow 3) — this counter is unaffected either way.
1683
2771
  4. Fix that ONE page via the write tool named in `fix_hint.tool`, with
1684
2772
  `fix_hint.args_template` as a starting point (`element_update_settings`,
1685
2773
  `element_add`, etc), ETag-locked like every other write in this
1686
2774
  server.
1687
- 5. `yootheme_builder_page_publish({ template_id, etag })` republishes.
2775
+ 5. `yootheme_builder_page_publish({ layout_id, etag })` republishes.
1688
2776
  The response's `quick_check` block (default on) re-checks THIS page
1689
2777
  with the same per-page codes, so you can confirm the fix landed
1690
2778
  without a second full sweep.
@@ -1692,7 +2780,7 @@ exposes. Never analyze findings by hand and never bulk-apply a fix.
1692
2780
  **Disabling instead of editing.** Sometimes the right remediation is to
1693
2781
  exclude an element entirely rather than edit its content (a stray leftover
1694
2782
  section, a duplicate block). `yootheme_builder_element_set_status({
1695
- template_id, element_path, status: false, etag })` writes `props.status:
2783
+ layout_id, element_path, status: false, etag })` writes `props.status:
1696
2784
  'disabled'` — the SAME field YOOtheme's own native "Disable element"
1697
2785
  checkbox writes, so the element is excluded from BOTH the live frontend
1698
2786
  render AND the NEXT `pages_audit`/`template_summary` (both read the same
@@ -1703,7 +2791,7 @@ than persisting a fabricated `props.status`.
1703
2791
 
1704
2792
  **Need a real document outline instead of findings?** `pages_audit` tells you
1705
2793
  WHAT is wrong (a missing h1, a skipped level); it never shows you the whole
1706
- heading tree in one look. `yootheme_builder_heading_outline({ template_id })`
2794
+ heading tree in one look. `yootheme_builder_heading_outline({ layout_id })`
1707
2795
  (read-only, reached via `yootheme_builder_advanced_read`) is a companion
1708
2796
  STRUCTURE view for ONE page: every H1-H6 heading in true document order, each
1709
2797
  carrying its TEXT and the element that carries it (`element_type`/`path` —
@@ -1712,7 +2800,7 @@ the alt-status of every image on the page (`alt: null` for missing, plus
1712
2800
  `is_generic_alt` for a lazy "Logo"/"Bild"-style alt). Use it BEFORE editing a
1713
2801
  page's heading hierarchy by hand, or to double-check a `heading-skip` finding
1714
2802
  in context rather than as an isolated hit:
1715
- `yootheme_builder_advanced_read({ tool: "yootheme_builder_heading_outline", arguments: { template_id: "page:4816" } })`
2803
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_heading_outline", arguments: { layout_id: "page:4816" } })`
1716
2804
  returns `{outline, images, h1_count, has_level_skip, images_missing_alt}` —
1717
2805
  `has_level_skip` mirrors the `heading-skip` check's EXACT rule (a level more
1718
2806
  than one above the highest seen so far, including a document that opens below
@@ -1726,12 +2814,21 @@ builder node to point at).
1726
2814
  - **Promising a full SEO audit.** `pages_audit` covers Builder structure
1727
2815
  and CMS basics only: heading hierarchy, empty/thin pages, missing
1728
2816
  image alt text, duplicate layouts, title collisions, non-pretty URL
1729
- fallbacks, stale content, and dead in-page anchors. Every response
1730
- carries a fixed disclaimer field:
1731
- `"Meta title/description/OG live in your CMS SEO plugin not covered here."`
1732
- It does not read or evaluate Yoast/RankMath/any SEO-plugin meta
1733
- title, meta description, or Open Graph tags. Say this scope boundary
1734
- out loud and point the user at their SEO plugin for that layer.
2817
+ fallbacks, stale content, and dead in-page anchors. Every response carries
2818
+ a `disclaimer` field naming what is out of scope, and that field says
2819
+ itself that its list is **not exhaustive**pass that on rather than
2820
+ presenting it as the complete set of blind spots. It does not read or
2821
+ evaluate Yoast/RankMath/any SEO-plugin meta title, meta description, or
2822
+ Open Graph tags, and it does not see whether the page may be INDEXED at
2823
+ all. Say this scope boundary out loud and point the user at their SEO
2824
+ plugin for that layer.
2825
+ - **Answering "we don't show up on Google" from this tool alone.** The first
2826
+ suspect is not a heading level — it is a `noindex`, which `pages_audit` has
2827
+ no check for in any category. Staging sites often carry a site-wide
2828
+ `<meta name="robots" content="noindex, nofollow">`, and until that is lifted
2829
+ every other SEO finding is beside the point. Settle it first:
2830
+ `yootheme_builder_fetch_public_url({ url, expect: "noindex" })` —
2831
+ `expect_found: true` means the page asks search engines to stay away.
1735
2832
  - **Assuming builder props are the only content scanned.** Headings and
1736
2833
  image alt text are also detected inside raw HTML: an `html`/`text`
1737
2834
  element's markup content, not just builder props like
@@ -1748,7 +2845,16 @@ builder node to point at).
1748
2845
  "Module"/"Module Position") are NOT scanned - verified live that neither
1749
2846
  carries a markup-shaped `content` field on either platform; their actual
1750
2847
  content lives in an external WP widget instance / Joomla `#__modules` row,
1751
- outside the layout JSON entirely.
2848
+ outside the layout JSON entirely. Since W1/T1.8 that content IS readable,
2849
+ just not through THIS scanner: `include_modules:true` on
2850
+ `yootheme_builder_pages_audit`/`yootheme_builder_page_get_layout` lists
2851
+ every widget/module placed on this page (`modules_by_page`/
2852
+ `modules_on_page`), and `yootheme_builder_advanced_read({ tool:
2853
+ "yootheme_builder_local_content_read", arguments: { kind: "modules" } })`
2854
+ returns each one's actual `title`/`settings`/`theme_overlay` plus a
2855
+ `resolved` companion field on `element_get` for one `module`/
2856
+ `module_position` element (see Workflow 3) — this scanner's blind spot is
2857
+ unaffected either way.
1752
2858
  - **`mode:"rendered"` sees what the builder tree structurally cannot** (T-A2,
1753
2859
  2026-07-24) — pass `mode:"rendered"` (default is `"builder"`, the tree read
1754
2860
  above) to instead render each page first (the SAME engine
@@ -1774,9 +2880,39 @@ builder node to point at).
1774
2880
  pipeline, not a JSON walk): the response always carries
1775
2881
  `render_scan: {attempted, rendered, capped}`, even when the cap never
1776
2882
  engaged, so you can always see whether every page was actually rendered.
2883
+ When the cap DID engage, `render_scan.skipped_by_cap` names the templates
2884
+ it skipped (`{id, label}`), so a code found on the scanned subset is never
2885
+ silently generalised to the rest — re-run narrowed with `types:[…]` to
2886
+ reach them. That list is itself bounded at 30 entries and says so with
2887
+ `skipped_by_cap_not_listed` when it had to stop.
1777
2888
  A page whose render failed is excluded from findings (never reported as a
1778
2889
  false "no headings") and listed in `render_errors` instead — check both
1779
- fields before trusting a clean rendered-mode result.
2890
+ fields before trusting a clean rendered-mode result. Each `render_errors`
2891
+ row carries `error_class` (the throwable's own class), and for an
2892
+ engine-level failure (`TypeError` and friends — a component executed during
2893
+ the render did not get something its own code requires) also a `reason`
2894
+ saying the layout is not the place to look and that the template is
2895
+ UNCHECKED rather than clean.
2896
+ - **A Joomla page carrying `module`/`module_position` is NOT a render
2897
+ failure** (render-fallback follow-up, 2026-07-30) — it renders via a
2898
+ tolerant fallback (the module/module-position subtree excluded, everything
2899
+ else rendered normally) rather than falling into `render_errors`, because
2900
+ Joomla's admin/REST context structurally cannot probe those two types (a
2901
+ CMS-runtime gap, not a broken layout). When this happens, the response
2902
+ carries `render_fallback: {pages: [{id, label, dropped_count}], suppressed_checks}`
2903
+ — `dropped_count` is how many module(-position) subtrees were excluded from
2904
+ THAT page's render, and `suppressed_checks` names which of `no-headings`,
2905
+ `missing-h1`, `heading-skip` were skipped on those pages specifically (the
2906
+ only three whose absence-based logic a hidden widget heading could
2907
+ fabricate — every other check, e.g. `multiple-h1`, only ever reports
2908
+ content that DID survive the fallback, so it keeps running). Same idea on
2909
+ `yootheme_builder_heading_outline` in `mode:"rendered"`: the response
2910
+ carries `render_fallback_used` (bool) + `render_dropped_count` (int),
2911
+ always present in that mode — treat `h1_count`/`has_level_skip`/the outline
2912
+ as possibly incomplete when `render_fallback_used` is true (content inside
2913
+ the excluded widget is invisible to the outline too). `WordPress renders
2914
+ these types successfully today, so this never engages there — WordPress
2915
+ pages never carry `render_fallback` / never set `render_fallback_used`.
1780
2916
  - **Confusing the CONTENT count with `pages_list.elements_count`.**
1781
2917
  `pages_list.elements_count` counts every node, including
1782
2918
  `section`/`row`/`column`/`fragment` layout wrappers. `pages_audit`
@@ -1897,7 +3033,7 @@ What changes in the response:
1897
3033
  tool returns `target_not_scannable` (HTTP 422). Tell the user the page could
1898
3034
  not be audited and why; then audit it via `template_summary`, or pick a page
1899
3035
  `pages_list` returns. Never present it as a clean result.
1900
- - **`resolved_target`** — `{ template_id, post_id, type, label, frontend_url,
3036
+ - **`resolved_target`** — `{ layout_id, post_id, type, label, frontend_url,
1901
3037
  context, resolved_via, matched_by }`. This is the echo of WHICH page a fuzzy
1902
3038
  address landed on. Always report it: `special: "home"` and
1903
3039
  `title: "SunWest"` are guesses until the server says what they resolved to.
@@ -1907,36 +3043,29 @@ What changes in the response:
1907
3043
  - **Richer per-hit evidence.** Single mode raises the per-page hit cap from 5 to
1908
3044
  **100 hits per check** and adds a `content_snippet` to every locator (the site
1909
3045
  sweep omits snippets to keep a whole-site payload small). The text leg also
1910
- gains a `Where to fix:` block with the locator lines; the site sweep keeps its
1911
- compact 5-column summary table, with the locators in `structuredContent` only.
1912
- - **The summary table's count column counts HITS, and says so.** In a site sweep
1913
- that column is headed `COUNT` and holds the number of affected **pages**. A
1914
- single-page sweep scanned exactly one page, so that number would be `1` for
1915
- every finding and tell you nothing there the column is headed **`HITS`** and
1916
- holds the number of individual occurrences **on that page** (19 alt-less
1917
- images `19`, not `1`). It is the uncapped total: `hits_truncated` shortens
1918
- the listed locators, never this figure. Report the `HITS` number as the size
1919
- of the problem. `structuredContent.findings[].count` is unaffected by the
1920
- column it stays the affected-PAGE count in every scope, so read the table
1921
- (or the page entry's own `hits`) when you want the per-page hit total.
1922
- - **`hits` is the unambiguous field; `count` means two different things.** On
1923
- this wire `count` is level-dependent: on a FINDING it counts affected **pages**
1924
- (so it is `1` for every finding of a single-page audit), and on a **page entry**
1925
- it counts hits on that page. One name, two meanings — and that ambiguity has
1926
- already produced a live misread, an agent reporting "one image is affected"
1927
- when nineteen were. So the host emits **`hits`** next to a page entry's `count`
1928
- whenever that count really is a hit tally: `findings[].pages[].hits`. Prefer it.
1929
- It is an alias, never an invention a page entry with no hit total (the PAGE
1930
- ITSELF is the hit: `thin-content`, `empty-page`, `duplicate-layout`) carries no
1931
- `hits` key at all, and a host plugin older than the alias sends only `count`.
1932
- Fall back to `pages[].count` in both cases, never to `findings[].count`.
1933
- - **On the plugin-served transport there is no summary table at all.** The
1934
- `HITS`/`COUNT` column is rendered by the npm server. When the MCP server is the
1935
- CMS plugin itself, the response is the JSON payload — so nothing labels the
1936
- columns for you and the guidance above is the only thing standing between you
1937
- and the misread. Read `findings[].pages[].hits` (falling back to
1938
- `pages[].count`) and **never report `findings[].count` as the size of a
1939
- single-page problem**: in single-target mode it is structurally `1`.
3046
+ keeps the per-hit locators under `findings[].pages[].locators` in both scopes.
3047
+ Both legs of the answer are the same JSON payload — there is no rendered
3048
+ summary table and no `Where to fix:` block on either, in either scope, so
3049
+ nothing labels a column for you and the two counters below are the whole
3050
+ story.
3051
+ - **Two counters, two levels, and neither is called `count`.**
3052
+ `findings[].affected_pages` is how many PAGES that finding matched. In
3053
+ single-target mode it is structurally `1` for every finding, so it is never
3054
+ the size of the problem there. `findings[].pages[].occurrences_here` is how
3055
+ many occurrences sit on THAT page — 19 alt-less images ⇒ `19`. It is the
3056
+ uncapped total: `hits_truncated` shortens the listed locators, never this
3057
+ figure. Report `occurrences_here` as the size of a single-page problem, and
3058
+ never `affected_pages` an agent once reported "one image is affected"
3059
+ where nineteen were.
3060
+ - **A page entry with no `occurrences_here` is not a gap.** Some checks make the
3061
+ PAGE ITSELF the hit `thin-content`, `empty-page`, `duplicate-layout` — and
3062
+ those entries carry no occurrence total at all, because there is nothing on
3063
+ the page to count more than once.
3064
+ - **What `occurrences_here` counts, the response says once.** The top-level
3065
+ `occurrences_unit` states it in words: matching ELEMENTS in the saved layout
3066
+ tree, a FLOOR when a bound row repeats or content is conditional, since the
3067
+ saved tree enumerates neither. Pass that caveat on rather than presenting the
3068
+ number as what a visitor's browser renders.
1940
3069
 
1941
3070
  Two request shapes are refused up front, with a typed error rather than a
1942
3071
  round-trip: more than one address key → `too_many_targets` (pick one; use
@@ -2048,6 +3177,87 @@ on the CONTENT count (`elements_content`), so wrapper-only pages — three neste
2048
3177
  columns around nothing — are correctly flagged rather than passing as "not
2049
3178
  empty".
2050
3179
 
3180
+ #### `dangling-source` — structure · critical
3181
+
3182
+ **Why it matters.** The page is not empty and nothing errored: an element IS
3183
+ there, bound to a source name that no longer exists. YOOtheme resolves the
3184
+ binding to nothing and renders nothing, so the gap looks identical to a layout
3185
+ problem while the cause sits in the source list. Without this finding the only
3186
+ way to spot it is to compare every binding on the site against
3187
+ `sources_list` by hand.
3188
+ **How to fix.** Compare each reported `source_name` against
3189
+ `yootheme_builder_sources_list`. Renamed elsewhere → repair the reference with
3190
+ `yootheme_builder_repair_source_references({from, to})` (that is the shipped
3191
+ `fix_hint`; `to` must be a currently registered name). Genuinely deleted →
3192
+ unbind the element instead, or rebuild the binding against a source that
3193
+ exists.
3194
+ **Severity.** Critical: the element renders nothing at all, and the page ships
3195
+ looking finished.
3196
+
3197
+ #### `unknown-source-fields` — structure · critical
3198
+
3199
+ **Why it matters.** The value-side sibling of `dangling-source`: the bound
3200
+ source EXISTS, but one or more `field_mappings` values name a source field the
3201
+ source does not emit, so that mapped slot binds to nothing and renders blank.
3202
+ Neither `empty-page` (an element IS there) nor `dangling-source` (the source
3203
+ name IS known) catches this — it falls through both rasters, and the same
3204
+ diagnosis is what `element_get_binding`/`inspect_multi_items_binding` already
3205
+ surface for a single element inspected by hand.
3206
+ **How to fix.** Compare each reported `unknown_source_field_values` entry
3207
+ against the `unknown_source_field_values_available` list that rides alongside
3208
+ it, then `element_bind_source` with a corrected `field_mappings` value (that is
3209
+ the shipped `fix_hint`).
3210
+ **Severity.** Critical: the mapped value renders nothing at all, and — unlike a
3211
+ typo'd element prop, which is refused before it writes — this one persists
3212
+ silently.
3213
+
3214
+ #### `container-missing-item-child` — structure · warning
3215
+
3216
+ **Why it matters.** A multi-item container (`grid`, `button`, `slideshow`,
3217
+ `accordion`, …) repeats its `*_item` child once per source row. With no such
3218
+ child there is nothing to repeat, so the container renders nothing — again with
3219
+ no error anywhere. `element_add` already says this at write time; this finding
3220
+ catches the states that write never saw, such as the child being deleted later
3221
+ or a library insert that dropped it.
3222
+ **How to fix.** `element_add` the type named in each locator's
3223
+ `expected_child_type` under the reported container path (that is the shipped
3224
+ `fix_hint`). Read its required props from `element_type_get_schema`, whose
3225
+ `minimal_layout` shows exactly this container-plus-child shape. If the container
3226
+ is meant to be bound to a source, bind the CHILD, never the container.
3227
+ **Severity.** Warning, not critical: the element genuinely renders nothing, but
3228
+ building the shell first and its child second is the order `minimal_layout`
3229
+ itself teaches — a page caught mid-build is half-finished, not broken.
3230
+
3231
+ #### `binding-renders-nothing` — structure · warning (publish/save only)
3232
+
3233
+ **Why it matters.** A bind that persisted is not a bind that delivers. The
3234
+ element is there, the source name is valid, the field mappings are in place, the
3235
+ save succeeded — and the page renders zero rows. Nothing on that path says so,
3236
+ which is why a customer once bound, published, looked, re-bound with `limit`,
3237
+ published, looked again, and only then ran `render_preview` and read
3238
+ `rendered_item_wrapper_count: 0`. This finding is that sentence, arriving two
3239
+ cycles earlier.
3240
+ **How to fix.** Each locator names the element AND its `source_name`. Usual
3241
+ causes in order: the source needs a context this template does not have (an
3242
+ archive source on a single page has no term to scope to — check with
3243
+ `source_get_schema` whether it takes a term/id argument), a filter or
3244
+ `query_arguments` narrowed it to nothing, or the source genuinely holds no
3245
+ entries yet. To check ONE binding as you make it rather than at publish, pass
3246
+ `probe: true` to `element_bind_source`.
3247
+ **Severity.** Warning: a source with no rows yet is a legitimate state (an empty
3248
+ category), and the page is not broken — it is empty where content was expected.
3249
+
3250
+ > **Scope, and it is narrower than every other `per-page` code.** This check
3251
+ > needs a ROW COUNT, and nothing in this product can produce one without
3252
+ > rendering. It therefore reads the render report that `page_save` / `page_publish`
3253
+ > has ALREADY computed for its own render-precheck — so it rides `quick_check`
3254
+ > and **never appears in `pages_audit`**, not even with `checks: ["structure"]`.
3255
+ > Rendering every page of a site to run one check would turn a sweep into
3256
+ > minutes. It also stays silent when the render threw (the precheck owns that
3257
+ > verdict), when the render used its module fallback (a partial render cannot
3258
+ > settle a zero), and when nothing on the page was supposed to iterate at all —
3259
+ > a `headline` bound to one field renders content and zero rows by construction.
3260
+
2051
3261
  #### `no-headings` — structure · critical
2052
3262
 
2053
3263
  **Why it matters.** With no heading of any level the page has no machine-readable
@@ -2427,7 +3637,7 @@ The raw response is a machine payload. What you hand the user follows this
2427
3637
  shape every time, so two audits of two sites read the same way:
2428
3638
 
2429
3639
  1. **Lead with the scope.** When `scope: "single"`, name the page the audit
2430
- actually ran on from `resolved_target` — label, `template_id`, and
3640
+ actually ran on from `resolved_target` — label, `layout_id`, and
2431
3641
  `matched_by` when the address was fuzzy ("`special: "home"` resolved to
2432
3642
  *Startseite* (`page:4816`)"). When the response has no `scope`, it was a site
2433
3643
  sweep: lead with `scanned.templates` and the `summary` totals.
@@ -2475,7 +3685,7 @@ shape every time, so two audits of two sites read the same way:
2475
3685
  first (every field of `alt_paths` / `anchors` / … also rides on the locator
2476
3686
  for the same hit, so nothing is lost), then the locator detail — halved
2477
3687
  before it is dropped — then the remaining raw evidence, then whole evidence
2478
- pages (`pages_truncated`), so page ids and labels outlive the prose. Both
3688
+ pages (`finding_pages_truncated`), so page ids and labels outlive the prose. Both
2479
3689
  transports shed in exactly that order; it is pinned by a contract test that
2480
3690
  derives the order from each implementation. Say so, and name the two ways
2481
3691
  back: raise `max_chars` (it bounds both legs on this tool), or
@@ -2487,9 +3697,10 @@ shape every time, so two audits of two sites read the same way:
2487
3697
  even for that floor, the plugin-served transport refuses with
2488
3698
  `max_chars_too_small` and tells you the minimum to ask for — it never returns
2489
3699
  a half-written payload.
2490
- 8. **Repeat the scope boundary.** Close with the response's `disclaimer` — meta
2491
- title/description/OG live in the CMS SEO plugin and are not covered here — so
2492
- nobody mistakes a clean audit for a clean SEO plugin.
3700
+ 8. **Repeat the scope boundary.** Close with the response's `disclaimer`
3701
+ verbatim, INCLUDING its own statement that the list is not exhaustive, so
3702
+ nobody mistakes a clean audit for a clean SEO setup. On a search-visibility
3703
+ question, check indexability first — see the pitfall above.
2493
3704
 
2494
3705
  Beyond that the prose is yours: group, summarise and prioritise however serves
2495
3706
  the user. Rules 2, 4, 5 and 6 are the parts that must not vary.
@@ -2506,7 +3717,7 @@ WARNING · structure · multiple-h1 — page renders 2 h1s [open in bui
2506
3717
  • Willkommen bei SunWest (Hero ▸ Headline) · headline
2507
3718
  → keep this one as the page's h1
2508
3719
  • Unsere Standorte (Karten ▸ Panel "Standorte") · panel-title · "Unsere Standorte"
2509
- → Fix: element_update_settings({ template_id: "page:4816",
3720
+ → Fix: element_update_settings({ layout_id: "page:4816",
2510
3721
  element_path: "/children/1/children/0", props: { title_element: "h2" } })
2511
3722
  Why: exactly one h1 anchors the page's topic; several dilute it.
2512
3723
 
@@ -2515,7 +3726,9 @@ WARNING · content · images-missing-alt — 1 image without alt text
2515
3726
  → Fix: element_update_settings({ …, props: { image_alt: "<what it shows>" } })
2516
3727
  Why: an empty alt makes the image invisible to screen readers and image search.
2517
3728
 
2518
- Meta title/description/OG live in your CMS SEO plugin not covered here.
3729
+ Not covered here, and not an exhaustive list: your CMS or SEO plugin owns meta
3730
+ title/description/OG, and whether search engines may index the page at all —
3731
+ check that with yootheme_builder_fetch_public_url({url, expect:"noindex"}).
2519
3732
  ```
2520
3733
 
2521
3734
  **Worked example (tool-call snippet):**
@@ -2527,62 +3740,80 @@ yootheme_builder_advanced_read({
2527
3740
  arguments: { checks: ["seo", "structure"] }
2528
3741
  })
2529
3742
  // A finding: { code: "missing-h1", category: "structure", severity: "warning",
2530
- // count: 3, pages: [{ id: "page:42", label: "About" }, ...],
3743
+ // affected_pages: 3, pages: [{ id: "page:42", label: "About" }, ...],
2531
3744
  // fix_hint: { tool: "element_update_settings",
2532
3745
  // args_template: { props: { title_element: "h1" } }, note: "..." } }
2533
3746
 
2534
3747
  // Step 3. Drill into the flagged page.
2535
- yootheme_builder_template_summary({ template_id: "page:42" })
3748
+ yootheme_builder_template_summary({ layout_id: "page:42" })
2536
3749
  // heading_levels: { h1: 0, h2: 2, h3: 5, div: 1 }, images_missing_alt: 2
2537
3750
 
2538
3751
  // Step 4. Fix that ONE page via the write tool the fix_hint named.
2539
3752
  yootheme_builder_element_update_settings({
2540
- template_id: "page:42",
3753
+ layout_id: "page:42",
2541
3754
  element_path: "/children/0/children/0",
2542
3755
  props: { title_element: "h1" },
2543
3756
  etag: "abc123"
2544
3757
  })
2545
3758
 
2546
3759
  // Step 5. Republish; quick_check re-confirms this page is clean.
2547
- yootheme_builder_page_publish({ template_id: "page:42", etag: "def456" })
3760
+ yootheme_builder_page_publish({ layout_id: "page:42", etag: "def456" })
2548
3761
  // Response: { ..., quick_check: { clean: true } }
2549
3762
  ```
2550
3763
 
2551
3764
  **Success criterion:** A re-run of `pages_audit` (or the `quick_check`
2552
3765
  block on the next publish) no longer lists the fixed finding for that
2553
- page, and the response's `summary.critical`/`warning`/`info` totals go
2554
- down.
3766
+ page, and the response's `summary.critical_finding_kinds` /
3767
+ `warning_finding_kinds` / `info_finding_kinds` totals go down. Those three
3768
+ count finding KINDS, not pages — `summary.affected_pages_summed_across_findings`
3769
+ is the page-level tally beside them.
2555
3770
 
2556
3771
  ---
2557
3772
 
2558
3773
  ## When something doesn't fit one of these 6 workflows
2559
3774
 
2560
3775
  - **Move an element** (intra-template reorder/reparent): use
2561
- `yootheme_builder_element_move({ template_id, element_path,
3776
+ `yootheme_builder_element_move({ layout_id, element_path,
2562
3777
  to_parent_path, to_index, etag })`. Reorders or reparents without
2563
3778
  re-creating.
2564
3779
  - **Delete an element**: use `yootheme_builder_element_delete({
2565
- template_id, element_path, etag, confirm: true })`.
3780
+ layout_id, element_path, etag, confirm: true })`.
2566
3781
  Elicitation-aware. Confirms via the AI client prompt before
2567
3782
  destroying state when `confirm` is omitted. On hosts without
2568
3783
  elicitation, it returns a preview-with-confirm-required response;
2569
3784
  call again with `confirm: true`.
2570
3785
  - **Unbind a source**: call through the gateway:
2571
- `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", arguments: { template_id, element_path, etag, confirm: true } })`.
3786
+ `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", arguments: { layout_id, element_path, etag, confirm: true } })`.
2572
3787
  Same elicitation flow as delete.
2573
3788
  - **Orient in a large template before editing** (cheapest first read):
2574
- `yootheme_builder_page_get_layout({ template_id, mode: "outline" })`
3789
+ `yootheme_builder_page_get_layout({ layout_id, mode: "outline" })`
2575
3790
  returns a token-efficient INDENTED text tree — one line per node,
2576
- `label ▸ type (bound: …) [sN]`. Each line carries a short handle
2577
- `[sN]` AND the node's real JSON-Pointer `path`. **The handle is
2578
- PER-READ (state-scoped): it is stable only at the etag you just read.
2579
- After ANY mutation (clone/move/delete/bind), re-read `mode:"outline"`
2580
- to get fresh handles.** Use the `path` (not the handle) as the write
2581
- address for `element_*` tools the handle is only a readable alias.
2582
- For binding-focused work prefer `mode:"bindings_only"` instead.
3791
+ `label ▸ type (bound: …) [sN]`. **The handle `[sN]` is a PER-READ
3792
+ (state-scoped) reading alias only, never a write address it is
3793
+ stable only at the etag you just read, and re-reads after ANY
3794
+ mutation (clone/move/delete/bind) get fresh handles.** The structured
3795
+ leg carries a SUMMARY (which sections are named, how many nodes each
3796
+ holds, the etag), not a path per node for the actual write address
3797
+ (a RELATIVE `element_path`) call `yootheme_builder_element_list`,
3798
+ scoped to one named section with `root_path` from the outline's
3799
+ `named_sections` if you already know which chunk you want. For
3800
+ binding-focused work prefer `mode:"bindings_only"` instead — and read it as
3801
+ an inventory that checks exactly ONE thing. It compares every binding's
3802
+ SOURCE against the sources this site registers: a source that is gone gets
3803
+ `unresolved: true` on that binding and is named once in
3804
+ `unresolved_sources`, with an `unresolved_sources_note` beside it saying what
3805
+ to repair. That list is present but EMPTY when the comparison ran and found
3806
+ nothing, and is replaced
3807
+ by `unresolved_sources_not_checked` when the registered-source list could not
3808
+ be read at all — in that state an unmarked binding means nothing, so do not
3809
+ read it as a clean bill. What it does NOT check is the field VALUES inside a
3810
+ source that does resolve: a `props` value naming a field that does not exist
3811
+ looks exactly like one that does and will render blank. `element_get_binding`
3812
+ names those dead VALUES on one element; `pages_audit`'s `dangling-source`
3813
+ finding reports the same dead sources across the whole site, with paths.
2583
3814
  - **Flat schema inspection** (e.g. enumerate every element path +
2584
- type without fetching the whole nested tree): call through the
2585
- gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", arguments: { template_id } })`.
3815
+ type without fetching the whole nested tree): call the L1 tool directly:
3816
+ `yootheme_builder_element_list({ layout_id })`.
2586
3817
  - **Etag-only fetch** (e.g. polling for concurrent edits): use
2587
3818
  `yootheme_builder_get_etag()` (takes no arguments) is cheaper than
2588
3819
  fetching the full layout.
@@ -2593,13 +3824,26 @@ down.
2593
3824
  `frontend_url_template` (with placeholders the user fills in).
2594
3825
  - **Find out which site / install you are connected to**: call
2595
3826
  `yootheme_builder_health` (Bearer-authenticated payload includes
2596
- `site_url` + `home_url`) or `yootheme_builder_diagnose`.
3827
+ `site_url` + `home_url`), or `yootheme_builder_diagnose` for `site_url`
3828
+ alone. `active_site.site_id`, which rides on every answer, names the registry
3829
+ entry your own config picked — a different question from which install the
3830
+ plugin believes it is.
3831
+ - **Find WHERE `implode` directives sit on the site** (rows rendering as one
3832
+ comma-joined string, and you do not know which page): call through the
3833
+ read gateway
3834
+ `yootheme_builder_advanced_read({ tool: "yootheme_builder_layout_query", arguments: { has_implode_directives: true, max_paths: 50 } })`
3835
+ — no `layout_id`, so the sweep covers every page layout, and `paths[]`
3836
+ gives you the `layout_id` + `path` pairs to act on. The detection is the
3837
+ same one `inspect_multi_items_binding` reports as `has_implode_directives`,
3838
+ so opening a hit confirms it. Every count in that answer describes the
3839
+ matching subset, not the site.
2597
3840
  - **Strip legacy `implode` directives** from an element binding (audit-clean
2598
3841
  source props that pre-date the wrapper-source refactor): call through the
2599
3842
  gateway:
2600
- `yootheme_builder_advanced({ tool: "yootheme_builder_clean_implode_directives", arguments: { template_id, element_path, etag } })`.
3843
+ `yootheme_builder_advanced({ tool: "yootheme_builder_clean_implode_directives", arguments: { layout_id, element_path, etag } })`.
2601
3844
  Returns the audit log + a fresh ETag; idempotent (`cleaned_count: 0` when
2602
- there is nothing to remove).
3845
+ there is nothing to remove). It also removes EMPTY `implode` stubs, which
3846
+ the finder above does not report — an empty stub joins nothing.
2603
3847
  - **Start from a pre-designed layout or element preset instead of
2604
3848
  hand-building** (the user wants a "Home"/"Contact"/"Services" page, or a
2605
3849
  ready-made Accordion/Alert preset, not a section built element-by-element):
@@ -2626,14 +3870,14 @@ layout root, then child 1 of that); a three-level path is
2626
3870
  into the Nth child (0-based). This is the form `element_list` emits as
2627
3871
  `rel_path`, and the form every worked example in this skill uses.
2628
3872
 
2629
- - **Fully-qualified:** `/templates/<template_id>/layout/children/0/...` is the
3873
+ - **Fully-qualified:** `/templates/<layout_id>/layout/children/0/...` is the
2630
3874
  same address with the template prefix. Read/write tools return the
2631
3875
  fully-qualified pointer as `path`; you can pass it straight back.
2632
3876
 
2633
- **What the tools EMIT:** `element_list` / `page_get_schema` return both `path`
3877
+ **What the tools EMIT:** `element_list` returns both `path`
2634
3878
  (fully-qualified) and `rel_path` (the canonical `/children/0/...` form); every
2635
3879
  write-tool response (`element_add`, `element_update_settings`, `element_clone`,
2636
- `element_move`, `element_bind_source`, `page_dynamize`, `scaffold_section`) and
3880
+ `element_move`, `element_bind_source`, `page_generate_dynamic`, `scaffold_section`) and
2637
3881
  `element_get` return the fully-qualified `path` plus the canonical `rel_path`.
2638
3882
  Copy either straight back into the next call.
2639
3883
 
@@ -2653,6 +3897,58 @@ hand you.
2653
3897
  layout node itself, which is not an addressable element. Use `""` for
2654
3898
  "the top".
2655
3899
 
3900
+ **What is NOT an element address.** The grammar above is exhaustive: an
3901
+ element is the layout root plus zero or more `/children/<index>` steps. These
3902
+ tools refuse anything else rather than writing into it — `element_add`,
3903
+ `element_delete`, `element_move`, `element_clone`, `element_update_settings`,
3904
+ `element_set_status`, `element_bind_source`, `element_unbind_source`,
3905
+ `element_set_source`, `clean_implode_directives`, `page_generate_dynamic`,
3906
+ `library_insert` (both `element_path` and `sublayout.element_path`) and
3907
+ `library_save_as`. In particular these resolve to real JSON but are **not**
3908
+ elements:
3909
+
3910
+ - `…/children` — the LIST of a node's children, not a child. Drop the
3911
+ trailing `/children` to name the element that should contain the child.
3912
+ - `…/props` — an element's settings. Pass the element's own path; the tool
3913
+ writes into `props` for you. A props map that happens to carry a `type` key
3914
+ (`props: {"type":"primary"}` is an ordinary element prop) is still props.
3915
+ - `…/type` — a scalar member of an element, not the element. Any member other
3916
+ than `children` ends the element grammar; pass the element's own path.
3917
+ - `/templates/<id>` — the template record. Its element tree starts one
3918
+ segment further down, at `/templates/<id>/layout`.
3919
+ - `…/children/007` — a leading-zero or gapped reference-token. Array positions
3920
+ are canonical RFC-6901 §4 indices; anything else names no position.
3921
+
3922
+ The layout root itself (`parent_path: ""`, or `/templates/<id>/layout`) is a
3923
+ valid **parent** for `element_add` / `element_move`, and never a thing to
3924
+ `element_delete`, `element_move` or `element_clone` — deleting it takes the
3925
+ whole page with it. Each refusal names the offending argument and what to
3926
+ pass instead; nothing is changed. (`page_generate_dynamic` is the one exception
3927
+ on the root: it COPIES rather than mutates, so `element_path` may name the
3928
+ whole page.)
3929
+
3930
+ **`children` must be a JSON array.** `pages_create`, `page_save` and
3931
+ `element_add` refuse a `children` sent as an object with named keys — `{"0":…,"5":…}` — with
3932
+ `layout_children_not_a_list`. The Builder walks `children` by position, so a
3933
+ keyed object there renders in insertion order, makes every `/children/<index>`
3934
+ path address a different node than it reads, and can overwrite a sibling on the
3935
+ next append. A `{"0":…,"1":…}` object with contiguous keys is accepted: it
3936
+ re-encodes as a JSON array, which is what lands on disk either way.
3937
+
3938
+ Only the layout root must declare `children`. An empty section, row or
3939
+ column may omit the key — that is how the builder itself stores them
3940
+ (YOOtheme's save-transforms remove an empty `children`). A `children` value
3941
+ that is present must be a JSON array, never an object.
3942
+
3943
+ **Theme templates in `library_insert` / `library_save_as`.** Those two tools
3944
+ resolve their path against the template as stored, and a THEME template
3945
+ (a `pages_list` id with no `page:` / `article:` prefix) stores its tree one
3946
+ segment down. `element_path: ""` still means "the top of this template" and
3947
+ works on both kinds; a deeper path on a theme template starts at
3948
+ `/layout/children/<index>`. Pass a `page:<id>` / `article:<id>` path there and
3949
+ the refusal says so. (The `element_*` tools are unaffected — their `rel_path`
3950
+ is layout-relative for every template kind.)
3951
+
2656
3952
  **Reading the index.** In `/children/0/children/2/children/1`: child 0 of the layout,
2657
3953
  then child 2 of THAT, then child 1 of THAT. Indices are positional and shift
2658
3954
  when siblings are added or removed; re-read after a structural mutation before
@@ -2660,10 +3956,12 @@ reusing a path (see the ETag handshake below).
2660
3956
 
2661
3957
  **Don't hand-build deep paths.** Enumerate with
2662
3958
  `yootheme_builder_element_list` (flat `{path, rel_path, element_type}`
2663
- rows) or orient with `yootheme_builder_page_get_layout({ template_id,
2664
- mode: "outline" })` (indented `label type [sN]` tree, each line
2665
- carrying the real `path`). Copy the `path` from a row — never count
2666
- `/children/` segments by eye on a big template.
3959
+ rows) copy `path` from a row there, never count `/children/`
3960
+ segments by eye on a big template. `yootheme_builder_page_get_layout({
3961
+ layout_id, mode: "outline" })` only ORIENTS (an indented
3962
+ `label type [sN]` tree plus a per-section node-count summary); scope
3963
+ `element_list` to one named chunk with `root_path` from the outline's
3964
+ `named_sections` when you already know which one you want.
2667
3965
 
2668
3966
  ## ETag handshake and self-correcting errors
2669
3967
 
@@ -2681,9 +3979,12 @@ carrying the real `path`). Copy the `path` from a row — never count
2681
3979
  carrying `current_etag` — read it and retry.
2682
3980
 
2683
3981
  So a write never leaves you guessing: on a lock error the response hands
2684
- you the etag to use next. On success, `changed_paths` tells you exactly
2685
- which addresses moved, so you can re-target without re-reading the whole
2686
- tree.
3982
+ you the etag to use next. On a `412` specifically, take that etag as
3983
+ permission to retry only AFTER re-reading the site-wide hash does not
3984
+ cover per-page layouts, so the conflict may well be your own page under
3985
+ someone else's edit, and the error cannot tell the two apart. On success,
3986
+ `changed_paths` tells you exactly which addresses moved, so you can
3987
+ re-target without re-reading the whole tree.
2687
3988
 
2688
3989
  **Self-correcting path errors.** When a path-addressed read cannot find
2689
3990
  the element at the path you sent, the `element_not_found` error does NOT
@@ -2719,7 +4020,9 @@ to re-read the entire layout after a wrong-path error.
2719
4020
 
2720
4021
  Every high-volume read tool takes `max_chars`. When a response does not fit,
2721
4022
  it is **shortened by dropping whole entries, never by cutting the JSON**, and
2722
- it tells you so. Read the verdict before you report a result as complete:
4023
+ it tells you so. Read the verdict before you report a result as complete
4024
+ (one read caps a text FIELD instead — last paragraph, where these flags are
4025
+ absent):
2723
4026
 
2724
4027
  - `truncated: true` — this response is not the whole answer.
2725
4028
  - `omitted_count: N` — exactly how many entries were left out.
@@ -2730,28 +4033,30 @@ it tells you so. Read the verdict before you report a result as complete:
2730
4033
  be rewound past the dropped entries is **removed, never left stale**, so a
2731
4034
  `next_cursor` you receive is always safe to follow.
2732
4035
  - `omitted_keys: [...]` — on a single-object response, which fields were shed
2733
- (the small identifying ones like `template_id` / `etag` are kept last).
4036
+ (the small identifying ones like `layout_id` / `etag` are kept last).
2734
4037
  - `retry_hint` — the concrete lever: a higher `max_chars`, a narrower
2735
4038
  `fields[]` projection, or a smaller `limit`.
2736
4039
 
2737
- The payload always parses — on the plugin's own `/mcp` endpoint the text leg
2738
- itself is reduced entry by entry, and through this Node server a shortened text
2739
- preview always travels beside a complete `structuredContent` leg carrying the
2740
- full result. So read `structuredContent` when the text leg looks cut. If you
2741
- ever receive a response where **neither** leg parses, that is a bug — report it
2742
- rather than working around it.
2743
-
2744
- Both transports carry that structured leg, but **they do not put the same thing
2745
- in it**. Over stdio it is the complete result. The plugin's own `/mcp` endpoint
2746
- puts the **same shortened payload in both legs** — reading its `structuredContent`
2747
- gives you exactly what its text leg already said, no more. That is a constraint,
2748
- not a preference: on WordPress the host plugin's MCP adapter builds both legs
2749
- from one returned value, so a bounded text leg beside a complete structured one
2750
- cannot exist there. Decide whether an answer is whole from `truncated` /
2751
- `omitted_count`, never from which leg you happened to read.
2752
-
2753
- The reverse is also true: **absence of `truncated` means the answer is
2754
- complete.** Do not hedge a full response with "there may be more".
4040
+ The payload always parses — in this shape the text leg is reduced entry by
4041
+ entry rather than cut mid-string. If you ever receive a response where
4042
+ **neither** leg parses, that is a bug report it rather than working around it.
4043
+
4044
+ Here both legs carry the **same shortened payload**: reading `structuredContent`
4045
+ gives you exactly what the text leg already said, no more, so it is not a way
4046
+ around the cap. Decide whether an answer is whole from `truncated` /
4047
+ `omitted_count`, never from which leg you happened to read and **absence of
4048
+ `truncated` means the answer is complete**, so do not hedge a full response with
4049
+ "there may be more".
4050
+
4051
+ **`page_get_layout(mode:"outline")` shortens differently, and none of the three
4052
+ rules above holds for it.** The cap bounds ONLY the outline text (both legs
4053
+ carry that same string); the summary beside it in `structuredContent`
4054
+ `named_sections`, `total_elements`, `etag` is never shortened. So here a
4055
+ bounded text leg DOES sit beside a complete structured one, the text is
4056
+ cut mid-string, not entry by entry, and the cut is announced by a trailing
4057
+ `--- TRUNCATED (N chars total, showing first M) ---` line, with **no
4058
+ `truncated` field and no `omitted_count`**. Read that marker; a missing flag
4059
+ means nothing here.
2755
4060
 
2756
4061
  Two levers beat raising `max_chars`, because they reduce what is produced
2757
4062
  rather than what survives: ask for fewer fields (`fields[]`) and fewer rows
@@ -3208,44 +4513,58 @@ skipped (no geocoding); an empty `map.height` becomes 300; a non-numeric
3208
4513
  and make it data-driven — repeating over a source's items — WITHOUT
3209
4514
  rebuilding it node by node.
3210
4515
 
3211
- Use `yootheme_builder_page_dynamize`. It copies the referenced layout
3212
- byte-identically and swaps ONLY what you map, so the design (spacing,
3213
- styles, the `#parent` bindings) survives untouched.
4516
+ Use `yootheme_builder_page_generate_dynamic`. It copies the referenced
4517
+ layout byte-identically and swaps ONLY what you map, so the design
4518
+ (spacing, styles, the `#parent` bindings) survives untouched. It always
4519
+ creates a NEW, unpublished, unlinked page from the reference — the
4520
+ reference itself is never modified.
3214
4521
 
3215
4522
  **Canonical tool-call sequence (snake_case):**
3216
4523
 
3217
4524
  1. `yootheme_builder_get_etag()`: current lock.
3218
- 2. `yootheme_builder_template_summary({ template_id })` (or
3219
- `yootheme_builder_page_get_layout({ template_id, mode: "outline" })`):
4525
+ 2. `yootheme_builder_template_summary({ layout_id })` (or
4526
+ `yootheme_builder_page_get_layout({ layout_id, mode: "outline" })`):
3220
4527
  find the REFERENCE you will dynamize — a `section_name` (a named
3221
4528
  section from the summary) or an `element_path`.
3222
4529
  3. `yootheme_builder_sources_list()`: get the `list_source_name` to bind
3223
4530
  (the published source that yields the items).
3224
- 4. `yootheme_builder_page_dynamize({ template_id, section_name: "<name>", list_source_name: "<source>", leaf_map: { "<original-leaf-field>": "<flat-flow-field>" }, etag })`.
4531
+ 4. `yootheme_builder_page_generate_dynamic({ layout_id, output: "new_page", section_name: "<name>", list_source_name: "<source>", leaf_map: { "<original-leaf-field>": "<flat-flow-field>" }, etag })`.
3225
4532
 
3226
4533
  **How the mapping works:**
3227
4534
 
3228
4535
  - `leaf_map` keys are the ORIGINAL leaf field-names in the designed
3229
4536
  section; values are the flat flow field they should now read. The
3230
- `#parent` binding and any tuned props are kept.
4537
+ `#parent` binding and any tuned props are kept. A KEPT (unmapped) leaf
4538
+ whose original name is not a field the swapped source emits surfaces in
4539
+ the response's `summary.unmapped_kept_fields_missing_in_target` — map
4540
+ it or accept the empty render.
3231
4541
  - A section with SEVERAL source blocks dynamizes in ONE call via
3232
4542
  `source_map` — an array of `{ match, source_name, leaf_map }`, one
3233
4543
  entry per block.
3234
4544
  - For sub-query panels, key a `leaf_map` entry by
3235
4545
  `"<subquery>.<leaf>"`.
3236
- - `page_dynamize` requires `page.title`. It returns the page plus a
3237
- `summary`, and (since v1.4) `changed_paths` + `new_etag` like every
3238
- other write.
4546
+ - `output` is REQUIRED "new_page" is the only value available today; a
4547
+ future "in_place" value (editing the reference directly) is not yet
4548
+ available.
4549
+ - `page_generate_dynamic` requires `page.title`. It returns the page plus
4550
+ a `summary`, and `changed_paths` + `new_etag` like every other write.
4551
+ - With `dry_run:true`, a `would_change:false` answer carries a `reason` —
4552
+ most commonly "no bound exemplar element under the reference": the
4553
+ reference has nothing to swap because none of its leaves are bound yet.
4554
+ Bind one element first (`element_bind_source`), then retry.
3239
4555
 
3240
4556
  **Common pitfalls:**
3241
4557
 
3242
4558
  - **Binding Multi-Items on the container.** Bind the source + Multi-Items
3243
4559
  on the `*_item` CHILD, never on the grid/list container — see the
3244
- Multi-Items renderer truth table above. `page_dynamize` follows that
3245
- rule for you when you hand it the right reference.
4560
+ Multi-Items renderer truth table above. `page_generate_dynamic` follows
4561
+ that rule for you when you hand it the right reference.
3246
4562
  - **Hand-editing the JSON instead.** Don't reconstruct the section with
3247
4563
  `element_add` calls to "make it dynamic" — that loses the design.
3248
- `page_dynamize` is the supported one-call path.
4564
+ `page_generate_dynamic` is the supported one-call path.
4565
+ - **Expecting the reference page to change.** It never does — the result
4566
+ is always a NEW page. Publish the new page or copy its layout onto the
4567
+ reference yourself if you actually want an in-place swap.
3249
4568
  - **Reusing a stale etag.** Feed `new_etag` from the response into your
3250
4569
  next write.
3251
4570
 
@@ -3285,12 +4604,71 @@ apikey). See "WootsUp AI Layouts" below.
3285
4604
  Browse, get, AND insert against the remote catalog (`source: "catalog"`,
3286
4605
  the default) all depend on a YOOtheme Pro Library API key configured on the
3287
4606
  connected site (the customer's own theme `apikey`, set once in the YOOtheme
3288
- Customizer not something this MCP server manages). If it's missing,
3289
- `library_browse` does not error it returns `{ items: [], total: 0 }`.
3290
- Treat `total: 0` as "ask the customer to check the theme apikey field in the
3291
- Customizer", not a network/auth failure to retry blindly. **This apikey
3292
- requirement applies ONLY to the remote catalog** the local AI-Layouts
3293
- store (next subsection) needs no apikey at all.
4607
+ Customizer, not something this MCP server manages). If it's missing,
4608
+ `library_browse` does not error: it returns `{ items: [], total: 0 }` with
4609
+ `api_key_configured: false` and a `detail_note`. Read that boolean rather
4610
+ than guessing from the count. **This apikey requirement applies ONLY to the
4611
+ remote catalog.** The local AI-Layouts store (next subsection) needs no apikey
4612
+ at all.
4613
+
4614
+ **Do NOT tell the customer to check the apikey field until you know that is the
4615
+ problem.** `api_key_configured: false` means no key could be READ, and the
4616
+ reader answers that for five different situations. One of them is OUR lookup
4617
+ missing the row rather than anything the customer did, and it needs the
4618
+ opposite action. Run `yootheme_builder_diagnose` and read `library_key.state`:
4619
+
4620
+ | `state` | who has to act |
4621
+ |---|---|
4622
+ | `key_present` | nobody. The key is readable, so an empty catalog is a different problem. |
4623
+ | `update_site_without_key` (Joomla) | the customer, in the YOOtheme Customizer theme settings. |
4624
+ | `theme_config_empty` (WordPress) | the customer, same place. |
4625
+ | `theme_config_unavailable` (WordPress) | nobody yet: YOOtheme is not loaded at all. Check the theme is active, then run `yootheme_builder_health`. |
4626
+ | `update_site_not_found` (Joomla) | **nobody, in the key field.** No `pkg_yootheme` update site matched, so the Customizer would show that field empty too. Report the state instead of sending the customer to a field that cannot fix it. |
4627
+
4628
+ `library_key.hint` carries the same verdict as a sentence you can pass on, and
4629
+ `library_key.configured` is the boolean. The key itself is never in the payload.
4630
+
4631
+ **A REFUSED catalog fetch is NOT an empty catalog.** When the site HAS a key
4632
+ and the YOOtheme Pro Library rejects it (measured: HTTP 401 "Invalid API Key"),
4633
+ `library_browse` and `library_get` answer `error_code:
4634
+ "library_upstream_unavailable"` (HTTP 503) carrying `upstream_status` and
4635
+ `api_key_configured`. Do NOT respond to it by dropping filters, trying another
4636
+ id, or calling `library_get` to "verify" the id: nothing you send repairs a key
4637
+ the provider refuses, and `library_get` is refused by the same upstream. Only
4638
+ the site owner can take it up with YOOtheme. Meanwhile the keyless `source: "ai"`
4639
+ store and `page_get_layout` still work. A successful `total: 0` is a different
4640
+ thing entirely: the catalog was read and matched nothing, and that is now the
4641
+ only thing an empty page can mean.
4642
+
4643
+ **Browse can succeed while get and insert fail, and that is normal, not a
4644
+ bug.** The LIST endpoint the browse call uses answers even with NO key at all;
4645
+ the DETAIL endpoint (`/v1/library/layout/<id>`) that `library_get` and
4646
+ `library_insert` need CHECKS the key. Measured on two independent installs:
4647
+ `api.yootheme.com` refused those installs' configured key on every
4648
+ key-checking endpoint, the detail endpoint included, while browsing kept
4649
+ working normally. So "I can browse hundreds of layouts but every `library_get`
4650
+ fails" is a REFUSED KEY, not a bad id and not a broken plugin.
4651
+
4652
+ **Say what was measured, and stop there.** The refusal answers
4653
+ `library_upstream_unavailable` (HTTP 503) with `upstream_status: 401` and
4654
+ `api_key_configured: true`, and the honest report is exactly that: *the
4655
+ YOOtheme Pro Library refused this site's API key, so the call could not be
4656
+ made.* Then stop re-trying ids, because no id repairs a refused key.
4657
+
4658
+ **Do NOT tell the customer WHY it was refused.** You cannot see why. A 401 is
4659
+ the same 401 for a plan that does not cover the endpoint, an expired key, a
4660
+ key that was replaced, a domain or host restriction, and a change on
4661
+ YOOtheme's side. If the customer asks what to check, offer those as
4662
+ POSSIBILITIES and point them at their YOOtheme account; never present one as
4663
+ the diagnosis. Telling a paying customer their plan is the reason is a claim
4664
+ about their account that we have not established and cannot withdraw once the
4665
+ assistant has said it. Never phrase it as a defect in their site either.
4666
+
4667
+ **`library_item_unavailable` (404) is a DIFFERENT state.** It means the item
4668
+ could not be fetched at all: no Library API key is configured on the site, or
4669
+ the id is not in the catalog. A site whose key is configured and refused gets
4670
+ the 503 above, never this 404. Reading them as the same thing sends a customer
4671
+ who HAS a key off to configure one.
3294
4672
 
3295
4673
  ### WootsUp AI Layouts — your OWN local library (no apikey, save → reuse)
3296
4674
 
@@ -3564,8 +4942,16 @@ at fetch time, never persisted raw):
3564
4942
  the rest of the insert.
3565
4943
 
3566
4944
  The insert response's `images_resolved: { downloaded, placeholders }`
3567
- tells you exactly what happened — check it rather than assuming every
3568
- image round-tripped.
4945
+ tells you HOW MANY of each — check it rather than assuming every image
4946
+ round-tripped. It does not tell you WHICH, and a count on its own is not
4947
+ actionable: `image_import_failures` is. That field appears only when a
4948
+ curated insert actually failed to fetch something, and carries one entry
4949
+ per lost image with the `prop_path` inside the fetched item (e.g.
4950
+ `children[0].props.image`), the `exception_class`, and a `message`. Read it
4951
+ before reporting the insert as finished: a failure is often transient, so a
4952
+ second `library_insert` — or a searched-and-imported photo bound into that
4953
+ exact prop path — usually fixes it. Absent means nothing failed; on
4954
+ `images_mode: "as_saved"` or `"placeholder"` the curate step never ran.
3569
4955
 
3570
4956
  ### Fill image slots with real on-topic photos (search → import → bind)
3571
4957
 
@@ -3573,9 +4959,13 @@ The professionalism signal is on-brand IMAGERY (see "Make it the CUSTOMER's bran
3573
4959
 
3574
4960
  **The flow (three tools):**
3575
4961
  1. `yootheme_builder_image_search({ source, query, orientation })` returns candidate rows, each with a `full_url` (the image to import) plus dimensions and author. `source`: `unsplash` or `pexels` for real photography (the primary rich sources); `proimages` for YOOtheme's own curated decorative and background assets. Use `orientation: "landscape"` for heroes and banners, `"portrait"` for tall cards.
4962
+ A search that could NOT be carried out is an ERROR, not an empty shelf: it comes back with `error_code: "image_search_unavailable"` (HTTP 503). Do NOT respond to it by broadening the query. It means the provider refused the request or never answered, most often an HTTP 401 on the site's YOOtheme Pro Library API key (`upstream_status: 401`), which only the site owner can resolve. When that happens, stop searching, keep honest placeholders, and tell the user what `error` said. A SUCCESSFUL `total: 0` is a different thing entirely and DOES mean the query matched nothing; `api_key_configured` tells you whether the key gate was even open.
4963
+ **`api_key_configured: true` plus a 401 is a real, ordinary state: the site HAS a key and the upstream turned it down for this call.** That is the whole of what the two fields establish, and it is not a defect in the plugin or in the site. A YOOtheme Pro Library API key can be perfectly well configured and still be REFUSED. Measured on two independent installs: with such a key, EVERY key-checking endpoint at `api.yootheme.com` refused it, including `/v1/library/layout/<id>`, the DETAIL endpoint `library_get` and `library_insert` depend on. The LIST endpoint behind `library_browse` answers even WITHOUT a key, which is why browsing keeps working and only the detail reads and searches fail. So the honest reading of "browse fine, get and search refused" is: **the upstream refuses this site's key** for those calls. Report exactly that, and stop. **What you must NOT do is name the reason.** Whether it is the subscription behind the key, an expired or replaced key, a domain restriction, or something changed at YOOtheme, is not visible from here, and all of them look like the same 401. If the customer wants to act on it, list those as POSSIBILITIES to check in their YOOtheme account, never as a finding. Telling a paying customer their subscription is short when it is not is not a mistake you get to take back. Do NOT report it as a defect, do NOT retry with broader queries, and do NOT tell them the plugin is broken. Image search works on WordPress and Joomla alike.
3576
4964
  2. JUDGE the candidates by FIT and TONE, exactly like you choose a section, and never grab the first result. Search deliberately by the brief's INDUSTRY plus TONE: a coffee roaster searches `"coffee roasting"` / `"espresso pour"` / `"cafe interior"` (warm, editorial); a law firm searches `"modern office architecture"` (clean, restrained). A generic `query: "business"` returns generic stock that reads as filler.
3577
- 3. `yootheme_builder_image_import({ url: "<full_url>" })` sideloads the chosen photo into the site's OWN Media Library and returns `{ local_url }`. Only Unsplash, Pexels, and YOOtheme image hosts can be imported (a security allowlist); other URLs are rejected.
3578
- 4. Bind it: `yootheme_builder_element_update_settings({ template_id, element_path, props: { image: "<local_url>" }, merge: true })`.
4965
+ 3. `yootheme_builder_image_import({ url: "<full_url>" })` sideloads the chosen photo into the site's OWN Media Library and returns `{ local_url }`. The argument is also accepted under the name `full_url`, so a row from step 1 can be passed on unchanged — give ONE of the two, never both with different values. Only Unsplash, Pexels, and YOOtheme image hosts can be imported (a security allowlist); other URLs are rejected.
4966
+ **`local_url` has a PLATFORM-SPECIFIC shape, and both are correct.** WordPress returns a root-relative path WITH a leading slash (`/wp-content/uploads/2026/07/hero.jpg`); Joomla returns a JPATH_ROOT-relative path WITHOUT one (`images/yootheme/hero.jpg`, the same shape YOOtheme's own image dialog stores). Bind whatever came back VERBATIM. Adding a slash on Joomla or stripping one on WordPress produces a path the layout cannot resolve, and the element then renders empty with no error anywhere. Importing the same photo twice returns the FIRST attachment instead of creating a duplicate, as long as the earlier copy is still in the site's current upload folder (on WordPress that is `wp-content/uploads/YYYY/MM/`, so a copy imported in an earlier month is not matched and a second attachment is created).
4967
+ A failure never throws: it comes back as `{ imported: false, reason, failure }`. Read `failure` before deciding what to do next. `failure: "rejected"` means THIS url did not import (blocked host, upstream error, the CMS refused the file type), so picking a different candidate is the right move. `failure: "unavailable"` means the SITE cannot import any image right now (no media runtime, no HTTP transport), so stop importing, keep the placeholder, and tell the user what the `reason` said. Trying twenty more photos against an `unavailable` site only wastes the user's time.
4968
+ 4. Bind it: `yootheme_builder_element_update_settings({ layout_id, element_path, props: { image: "<local_url>" }, merge: true })`.
3579
4969
 
3580
4970
  **Real photo vs honest placeholder:**
3581
4971
  - A real, ON-TOPIC photo beats a placeholder every time; it is the single biggest lift from "template" to "finished". Fill the hero and feature-section image slots with searched photos that match the brief.
@@ -3597,7 +4987,9 @@ The most common way an AI-built page falls short of a real Pro layout: it defaul
3597
4987
  1. Compose from the real Library sections FIRST. The section corpus (`library_browse({kind:"section"})`) is about 2000 individually-designed, already-rich sections across every demo. Selecting well among them for fit, tone, and variety, and preserving their design, is the primary path to a varied Pro-quality page. Do NOT strip their rich features (below).
3598
4988
  2. When you hand-build a section (no corpus section fits, or you build from native elements), reach into the palette below and pick the rich pattern that fits. Never the bare-minimum version. Vary your choices across the page.
3599
4989
 
3600
- **The polish features Pro sections use constantly (these ARE what "rich" means): preserve them when composing, apply them when hand-building. The `panel` props named below are VERIFIED against the LIVE YOOtheme Pro 5.x `panel` schema (`element_type_get_schema("panel")` → 81 fields). `element_type_get_schema` is the source of truth — always confirm the exact key AND its enum values there before writing; a guessed key is hard-rejected and a guessed VALUE silently no-ops.**
4990
+ **The polish features Pro sections use constantly (these ARE what "rich" means): preserve them when composing, apply them when hand-building. The `panel` props named below are VERIFIED against the LIVE YOOtheme Pro 5.x `panel` schema (`element_type_get_schema("panel")` → 81 fields). `element_type_get_schema` is the source of truth — always confirm the exact key AND its enum values there before writing; a guessed key is stored and reported back under `warnings` (`code:
4991
+ "unknown_prop_keys"`) rather than refused, and a guessed VALUE either
4992
+ no-ops silently or, on an enum field, is refused with 422.**
3601
4993
  - **Card styling — THE single biggest "flat vs rich" lever: `panel_style` on a `panel`.** A bare `panel` (or a plain column of text) is what makes a section look flat; a `panel_style` turns it into a real styled CARD. Verified enum: `card-default` / `card-primary` / `card-secondary` / `card-hover` (hover-lift card) / `card-overlay` / `tile-default` / `tile-muted` / `tile-primary` / `tile-secondary`. A feature / team / pricing "card" is a `panel` WITH a `panel_style`, not three bare text columns.
3602
4994
  - **Hover** (on a `panel`; the image-hover props require a `link` + `panel_link`/`image_link`, per the schema `enable` rules): `hover_image` (swap the image on hover), `image_transition: scale-up`|`scale-down` (image zoom on hover), `title_hover_style: reset`|`heading`. `panel_style: card-hover` also gives a hover lift on its own.
3603
4995
  - **Depth**: `image_box_shadow` (`small`…`xlarge`) and `image_box_decoration` (`default`/`primary`/`secondary`/`shadow`/`mask`) — these apply to a panel image only when NO `panel_style` is set (they are mutually gated: use a `panel_style` OR the box-shadow/decoration, not both).
@@ -3631,7 +5023,7 @@ ones are legal depends on `kind`:
3631
5023
  | `target` shape | Accepts `kind: "layout"` | Accepts `kind: "element"` (preset) |
3632
5024
  |---|---|---|
3633
5025
  | `{ new_page: { title, status? } }` | ✓ creates a page, layout's sections become its content | ✗ rejected — a preset is not a page |
3634
- | `{ template_id, element_path? }` | ✓ ONLY the layout ROOT is section-accepting (`element_path: ""` or omitted) | ✓ ONLY a **Column** — every other node type is rejected |
5026
+ | `{ layout_id, element_path? }` | ✓ ONLY the layout ROOT is section-accepting (`element_path: ""` or omitted) | ✓ ONLY a **Column** — every other node type is rejected |
3635
5027
  | `{ sublayout: { template_id, element_path } }` | ✓ the fetched layout's sections are stripped to bare ROWS first (a sublayout/fragment accepts rows, not sections) | ✗ rejected — a sublayout is not a Column |
3636
5028
 
3637
5029
  **A preset (`kind: "element"`) MUST target a Column. There is no
@@ -3646,12 +5038,13 @@ first, then point `element_path` at ONE of the returned `column_paths`:
3646
5038
  yootheme_builder_library_insert({
3647
5039
  id: "8cf55920",
3648
5040
  kind: "element",
3649
- target: { template_id: "home", element_path: "" }
5041
+ target: { template_id: "home", element_path: "" },
5042
+ etag: "<etag>"
3650
5043
  })
3651
5044
  // → 422 target_not_a_column: 'target at "/" is a "layout"'.
3652
5045
 
3653
5046
  // Fix: scaffold a section->row->column hull, then target the new Column.
3654
- yootheme_builder_scaffold_section({ template_id: "home", columns: 1, etag: "<etag>" })
5047
+ yootheme_builder_scaffold_section({ layout_id: "home", columns: 1, etag: "<etag>" })
3655
5048
  // Response includes column_paths: ["/children/4/children/0/children/0"]
3656
5049
 
3657
5050
  yootheme_builder_library_insert({
@@ -3670,6 +5063,63 @@ separate strip step. Point `element_path` at the actual sublayout/
3670
5063
  fragment node itself, not its parent — pointing at the wrong level is a
3671
5064
  `422 target_not_a_sublayout`.
3672
5065
 
5066
+ **The table above is about the TARGET. The ITEM is checked too.** The
5067
+ composed result — the preset, or the layout's sections, or the stripped
5068
+ rows — goes through the SAME layout grammar `page_save` runs, before any
5069
+ image is imported. Two refusals come out of that, and neither is about
5070
+ where you pointed:
5071
+
5072
+ - `422 layout_invalid_hierarchy` — what would be stored is a shape the
5073
+ Page Builder never builds. The common case is a spine node saved as a
5074
+ preset: a Row or a Column put into your own library with
5075
+ `library_save_as` comes back classified as an element, and a Row inside
5076
+ a Column is refused. So is a whole layout spliced into a Column, and so
5077
+ is a **sublayout fragment at a page or layout root** — 67 of the shipped
5078
+ catalog layouts are fragments (the dropdown, search and dialog groups),
5079
+ served under `kind: "layout"` like any other layout. **The one
5080
+ destination that accepts them is a `sublayout` target — never a
5081
+ Column.** The Column branch of the target table only ever opens for
5082
+ `kind: "element"` presets; pointed at a Column, one of these fails
5083
+ before the hierarchy check even runs, with
5084
+ `422 target_not_section_accepting`, and that answer's own hint suggests
5085
+ the layout root or a `new_page` target — both refuse it too, for the
5086
+ same `layout_invalid_hierarchy` reason as the root case above. For this
5087
+ one item type, skip that suggestion and go straight to
5088
+ `target: { sublayout: { template_id, element_path } }`.
5089
+ - `422 library_item_kind_mismatch` — the item carries a different `kind`
5090
+ than the call declared. The declared kind alone decides which branch
5091
+ composes the item, so a layout handed over as `kind: "element"` would go
5092
+ in through the preset branch. The answer names both kinds; retry with
5093
+ the one the item carries. Items from your own library carry their own
5094
+ classification; a catalog id is looked up per kind, so there a wrong
5095
+ `kind` is already a `404 library_item_unavailable`.
5096
+
5097
+ Both are raised BEFORE the write and before any image import, so a
5098
+ refused insert stores nothing and imports nothing.
5099
+
5100
+ ### `insert_mode` — and the confirm gate that guards its default
5101
+
5102
+ An `existing`/`sublayout` target already has children whenever you insert
5103
+ into a page you (or a customer) already built. `insert_mode` decides what
5104
+ happens to them, mirroring the three choices the Library dialog itself
5105
+ offers:
5106
+
5107
+ | `insert_mode` | effect |
5108
+ |---|---|
5109
+ | `"replace"` (default) | DROPS the target's existing children; the composed content becomes the whole list |
5110
+ | `"top"` | prepends the composed content before the existing children |
5111
+ | `"bottom"` | appends the composed content after the existing children |
5112
+
5113
+ **`"replace"` on a non-empty target answers with a PREVIEW instead of
5114
+ writing, unless you pass `confirm: true`.** The preview reports
5115
+ `preview: true`, `applied: false`, `requires_confirmation: true`, and —
5116
+ so you know exactly what would be lost — `would_remove_count` (how many
5117
+ children) and `would_remove_types` (their `type`s, in order). Nothing is
5118
+ written on that call; re-send the SAME request with `confirm: true` to
5119
+ replace anyway, or switch to `insert_mode: "top"` / `"bottom"` to keep
5120
+ the existing content instead. An ALREADY-EMPTY target never needs
5121
+ `confirm`, whatever mode you pick — there is nothing there to lose.
5122
+
3673
5123
  ### Adapting a Library insert is STATIC content editing, not dynamizing
3674
5124
 
3675
5125
  **Every Library layout and preset is authored STATIC** — no source
@@ -3683,7 +5133,7 @@ section.
3683
5133
  an archive-style listing) is a deliberate, separate follow-up call, not
3684
5134
  something insert does for you.** Use the existing dynamize workflow
3685
5135
  documented in "Dynamize an existing section" just above this one
3686
- (`yootheme_builder_page_dynamize`) to re-target a Library-inserted
5136
+ (`yootheme_builder_page_generate_dynamic`) to re-target a Library-inserted
3687
5137
  section's leaf fields onto a Source, or bind a Multi-Items container
3688
5138
  directly with `yootheme_builder_element_bind_source` (Workflow 2) if the
3689
5139
  inserted item already has a grid/list container you want to re-point at
@@ -3705,7 +5155,7 @@ live data instead of its demo items.
3705
5155
  - **Stuffing placeholder copy into a field you have nothing for**, to
3706
5156
  defeat collapsing. Leave it empty; collapsing is the intended behavior.
3707
5157
  - **Expecting `library_insert` to make anything dynamic.** It composes
3708
- STATIC content only — dynamizing is the separate `page_dynamize` /
5158
+ STATIC content only — dynamizing is the separate `page_generate_dynamic` /
3709
5159
  `element_bind_source` call.
3710
5160
 
3711
5161
  **Worked example (browse → get → insert → adapt):**
@@ -3735,7 +5185,7 @@ yootheme_builder_library_insert({
3735
5185
  images: "placeholder",
3736
5186
  etag: "<etag>"
3737
5187
  })
3738
- // Response: { template_id: "home", inserted_path: "/children/3", element_count: 12,
5188
+ // Response: { layout_id: "home", inserted_path: "/children/3", element_count: 12,
3739
5189
  // images_mode: "placeholder", images_resolved: { downloaded: 0, placeholders: 6 },
3740
5190
  // new_etag: "<fresh>", frontend_url: null }
3741
5191
 
@@ -3743,7 +5193,7 @@ yootheme_builder_library_insert({
3743
5193
  // inserted section back first (page_get_layout / element_get) to find the
3744
5194
  // real headline's element_path, then edit CONTENT only — style untouched.
3745
5195
  yootheme_builder_element_update_settings({
3746
- template_id: "home",
5196
+ layout_id: "home",
3747
5197
  element_path: "/children/3/children/0/children/0",
3748
5198
  props: { content: "<customer's real headline>" },
3749
5199
  merge: true,
@@ -3820,62 +5270,69 @@ perform.
3820
5270
 
3821
5271
  <!-- TOOL-CATALOG:BEGIN -->
3822
5272
 
3823
- **53 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **55 callable** end to end, of which **42 are advertised in `tools/list`** (38 L1 + 2 L3 + 2 gateways); the remaining 13 advanced tools are reachable only through the gateways. Generated by `scripts/extract-tools.mjs` from the compiled `buildAllTools()` registry. Do not hand-edit this section; re-run `npm run build && node scripts/extract-tools.mjs` after changing tool definitions.
5273
+ **60 catalogued tools** plus the `yootheme_builder_advanced` (write) + `yootheme_builder_advanced_read` (read-only) gateways = **62 callable** end to end, of which **49 are advertised in `tools/list`** (45 L1 + 2 L3 + 2 gateways); the remaining 13 advanced tools are reachable only through the gateways. Generated by `scripts/extract-tools.mjs` from the compiled `buildAllTools()` registry. Do not hand-edit this section; re-run `npm run build && node scripts/extract-tools.mjs` after changing tool definitions.
3824
5274
 
3825
5275
  | Tool | Kind | Input keys | Description |
3826
5276
  | --- | --- | --- | --- |
3827
- | `yootheme_builder_clean_implode_directives` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Use when a Multi-Items element renders rows as ONE comma-joined string instead of N repeated items — typically stale `implode` directives left by a plugin upgrade (detect via yootheme_builder_inspect_multi_items_binding). Strips `props.source.props.*.implode` directives from an element binding. Returns audit log + new ETag. Idempotent (cleaned_count: 0 when nothing to remove) but destructive: the binding can't be restored without re-mapping. Ask the user to confirm, then call with `confirm: true`. Requires ETag. |
3828
- | `yootheme_builder_diagnose` | read+idempotent | `site_id` | Full diagnostic: /health + authenticated /etag probe. Returns site_url, home_url, plugin reachability, Bearer validity in one call. First call when you need to know where the site lives. For per-template URLs see pages_list. When a write-scoped key resolves to a CMS user with no write capability, the response adds `capability_warning` and `effective_capabilities.can_edit:false` instead of writes failing with a blind 403. |
3829
- | `yootheme_builder_element_add` | mutating | `children`, `element_type`, `etag`, `index`, `name`, `parent_path`, `position`, `props`, `site_id`, `template_id`, `type_name` | Add a new element to a template. Provide `parent_path` (or "" for root), `element_type` (e.g. "headline", "text", "grid"), and optional `props` / `children`. Content/leaf elements may only be added inside a `column`; only `section` is valid at the root — call yootheme_builder_scaffold_section first for a valid column path. Appends by default; pass `index` (0 = first) to insert. Inserting renumbers later siblingsre-read paths via element_list after any structural write. Returns the new element's JSON-Pointer path. Requires ETag. |
3830
- | `yootheme_builder_element_bind_source` | idempotent | `arguments`, `bindingLevel`, `directives`, `element_path`, `etag`, `field`, `field_mappings`, `query_arguments`, `relation`, `site_id`, `source_id`, `source_name`, `template_id` | Binds a Builder source to an element (canonical shape: `source` top-level). Pass `field_mappings` in the SAME call; bindingLevel "item" binds a container's first *_item child. Leaf under an iterating *_item → CONSOLIDATE onto item slots (409 slot_collision if taken/not a slot); leaf under a #parent fragment → INHERIT (`__node_item__`); standalone renders empty. List on a grid_item with children → iterator_is_leaf_renderer. Map: bind `map_item`, map `location` to a "lat,lng" field or no markers. Requires ETag. Call directly. |
3831
- | `yootheme_builder_element_clone` | mutating | `element_path`, `etag`, `site_id`, `template_id` | Clone an element as a sibling (same parent, immediately after the source). Returns the new element's path. Cloning renumbers later siblings re-read paths via element_list after any structural write. Requires ETag. |
3832
- | `yootheme_builder_element_delete` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | PERMANENTLY delete an element + children. Cannot be undone. Pass `confirm: true` (boolean) AND a fresh ETag. Deleting renumbers later siblings re-read paths via element_list after any structural write. If `preview:true` returns despite confirm, check `details.received_confirm` strings/numbers accepted, but boolean is canonical. |
3833
- | `yootheme_builder_element_get` | read+idempotent | `element_path`, `fields`, `max_chars`, `site_id`, `template_id` | Get the full element object at a specific JSON-Pointer path, including props and a `children_count` (NOT the children themselves — use yootheme_builder_element_list with root_path to enumerate children). `fields[]` narrows the returned object; `max_chars` bounds the text output. Address by template_id only resolve a URL/post first via pages_resolve. |
3834
- | `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Read the source binding attached to an element. Always returns {template_id, element_path, binding, has_binding, field_mapping_count, warning?}: `binding` is the raw source binding (bound source name, field-mappings, query args/directives); `has_binding` is false and `field_mapping_count` 0 when the element is not bound. |
3835
- | `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `include_meta`, `limit`, `max_chars`, `root_path`, `site_id`, `template_id` | List elements in a template as a flat array with JSON-Pointer paths + types + `label` (the builder-assigned name; "" when unset — not that the type lacks one). Scope with `root_path`/`depth` for a subtree, paginate with `limit`/`cursor` for large templates. `fields[]` narrows each row; `max_chars` bounds the text output. template_id only resolve a URL/post first via pages_resolve. |
3836
- | `yootheme_builder_element_move` | idempotent | `element_path`, `etag`, `site_id`, `template_id`, `to_index`, `to_parent_path` | Move an element to a new parent + index in the tree. Useful for reordering or reparenting (e.g. moving a card from one grid column to another). Moving renumbers later siblings re-read paths via element_list after any structural write. Requires ETag. |
3837
- | `yootheme_builder_element_set_source` | idempotent | `element_path`, `etag`, `site_id`, `source`, `source_extended`, `template_id` | Surgically SET a precomposed `source` (+ optional `source_extended.props`) on ONE element at a pointer, ETag-locked (blast-radius 0). Use this — not element_bind_source — when you have the exact source shape OR need `source_extended.props`, the native Multi-Items item-mapping carrier bind_source cannot write. E.g. FAQ accordion scoped to term: query.name="categoryFaqs.taxonomyCategoryFaq", field={name:"faq"}, source_extended.props→#node. Element-prop keys the type does not render echo back as `unknown_prop_keys` (non-fatal). ETag req. |
3838
- | `yootheme_builder_element_set_status` | idempotent | `element_path`, `etag`, `site_id`, `status`, `template_id` | Enable or disable an element via its `props.status` field — the SAME field YOOtheme's own native "Disable element" checkbox writes. status:false excludes the element and its descendants from BOTH the live render and live findings; pages_audit and template_summary content_nodes both honour it. status:true re-enables it, restoring the YOOtheme default (no status field means active). Requires ETag. |
3839
- | `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `field_names`, `fields`, `max_chars`, `name_contains`, `site_id`, `type_name` | Prop schema; read before element_add/element_update_settings: unknown keys rejected (error lists valid_prop_keys+did_you_mean). Field: {name,type,label?,text?,description?,enum?,value_hint?,group?,no_effect?} + groups[] + field_count/returned_count + binding_contract (containers/items). value_hint+enum first: some defaults are SEO-fatal (headline.title_element→h1); checkbox = boolean true/false; no_effect = read by nothing, never write it; group:"runtime-accepted" = honoured at render, hidden in UI. Select descriptors: field_names/name_contains. Bound size with fields[]/max_chars. |
3840
- | `yootheme_builder_element_types_list` | read+idempotent | `fields`, `include_meta`, `site_id` | List element types registered on this site (built-ins + YOOessentials/uEssentials extras). Names feed `element_type` of element_add. Pass `fields[]` to narrow each row. Keywords: "slider"/"carousel" → `slideshow`/`panel-slider`/`overlay-slider`. Prop keys differ per element (grid_item `title` vs headline `content`) — use element_type_get_schema before element_add/bind. Container rows carry `requires_child_type` (grid→grid_item); `has_children`/`has_children_support` are aliases (same value). |
3841
- | `yootheme_builder_element_unbind_source` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Remove the source binding from an element. Clears the element's canonical top-level `source` key AND the legacy `props.source`. Destructive in the sense that it may break dynamic-content rendering always ask the user to confirm. Requires ETag. |
3842
- | `yootheme_builder_element_update_settings` | idempotent | `element_path`, `etag`, `merge`, `name`, `props`, `site_id`, `template_id`, `unset` | Update `props` on an element. Default replaces all props; pass `merge:true` for server-side deep-merge (only request keys overwritten, others survive — avoids read-modify-write races). Requires ETag. Pass `unset:["key",…]` to reset props to their YOOtheme default by removing them. |
3843
- | `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current ETag (state revision) for the YOOtheme builder. Returns a sha256+revision string for optimistic locking. This ETag is SITE-WIDE: it covers the ENTIRE builder state, so ANY write to ANY template (by any client) bumps it — a 412 means the site state advanced, not necessarily that YOUR template changed. It is the SAME token the read tools (page_get_layout, element_list) return in their `etag`. Pass it back as `etag` on any write tool; on 412, re-read and retry. |
3844
- | `yootheme_builder_header_transparency_set` | idempotent | `acknowledge_contrast_risk`, `dry_run`, `etag`, `mode`, `pull_content_behind`, `site_id`, `template_id`, `text_color`, `transparent` | Inspect + repair the transparent header (nav over the hero). Always targets the FIRST section YOOtheme reads these props there only; later ones are silently dead. dry_run defaults TRUE: returns current state, the theme-declared --uk-inverse, risk_class, confidence, recommendation, would_change. mode:"auto" writes the theme-declared colour; over a hero image/video it writes NO colour and says to LOOK. mode:"explicit" refuses a colour contradicting the theme (409) unless acknowledge_contrast_risk. transparent:false is always allowed. |
3845
- | `yootheme_builder_heading_outline` | read+idempotent | `mode`, `site_id`, `template_id` | A real, doc-order H1-H6 outline of ONE template: each heading with its TEXT and the source element that carries it (`element_type`/`path`), plus the alt-status of every image (including images inside html/text/code raw markup). Read-only STRUCTURE inspection, not findings; use yootheme_builder_pages_audit for severity-sorted SEO findings across the whole site. Returns {outline, images, h1_count, has_level_skip, images_missing_alt}. Address by `template_id` (from pages_list or pages_resolve). |
3846
- | `yootheme_builder_health` | read+idempotent | `site_id` | Check plugin installed/reachable. Returns plugin version, YT Pro version, REST endpoints. available_endpoints is derived from the live route registration on both platforms, so every path listed is request-ready as printed. Authenticated payload adds site_url + home_url for deep-linking. See yootheme_builder_diagnose for Bearer-validity + connectivity summary. |
3847
- | `yootheme_builder_image_import` | idempotent | `site_id`, `url` | Sideload a photo into the site's own Media Library. Accepts ANY public image URL it does NOT require a Library API key, so it still works when image_search is unavailable (typically pass a `full_url` from yootheme_builder_image_search). Returns `{imported:true, local_url}` where local_url is a SITE-RELATIVE path (e.g. /wp-content/uploads/...). Bind it via element_update_settings({props:{image: local_url}, merge:true}). A caught import failure returns `{imported:false, reason}`, never a 500. |
3848
- | `yootheme_builder_image_search` | read+idempotent | `color`, `include_meta`, `orientation`, `per_page`, `query`, `site_id`, `source` | Search real on-topic photos (Unsplash/Pexels) or YOOtheme Pro Images to fill layout image slots with REAL, on-brand photography instead of bare placeholders. Returns candidate rows with `full_url` (pass straight to yootheme_builder_image_import) plus dimensions/author/orientation. `total:0` usually means no Library API key is configured (the same theme apikey library_browse/insert use). |
3849
- | `yootheme_builder_inspect_multi_items_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Reports Multi-Items binding state: container/item pair (grid↔grid_item, slideshow↔slideshow_item, map↔map_item, …), the binding level (none\|container\|item), and a recommended_fix when it sits on the container. For map/map_item, a bound item without a `location` ("lat,lng") field is flagged as a blocking map_location_warning (renders 0 markers). A List on a grid_item with nested children blocking iterator_is_leaf_renderer (children never render). If rows render as ONE comma-joined string, see clean_implode_directives. |
3850
- | `yootheme_builder_inspect_sublayout` | read+idempotent | `element_path`, `site_id`, `template_id` | Inspects a panel/grid_item `< SUBLAYOUT`: lists the `fragment` subtree bindable leaves (with JSON-Pointer paths to bind on NOT the container) and the render-time inner grid. Flags `ungated_grid_prop` (blocking): grid props set but the gate (title_align=left / image_align=left\|right) unsatisfied silent no-op. For container↔item binding see inspect_multi_items_binding. |
3851
- | `yootheme_builder_layout_query` | read+idempotent | `content_contains`, `cursor`, `fields`, `has_binding`, `include_meta`, `limit`, `max_chars`, `max_depth`, `name_matches`, `site_id`, `template_id`, `type`, `under_section` | Find elements in a template via AND-combined filters: `type` (one type or a list), `name_matches` (case-insensitive substring of the name), `content_contains` (case-insensitive substring of `content`/`title`/`meta` props), `has_binding` (bound vs unbound), `max_depth` (cap descent 0 = top-level), `under_section` (inside a named section). template_id only resolve a URL/post first via pages_resolve. Returns rows {path, element_type, label, has_binding}; paginate `limit`/`cursor`, narrow `fields[]`, bound `max_chars`. |
3852
- | `yootheme_builder_library_browse` | read+idempotent | `cursor`, `element_type`, `has_effects`, `include_meta`, `kind`, `limit`, `purpose`, `query`, `role`, `site_id`, `source`, `topic`, `type`, `website` | Browse the YOOtheme Pro Library catalog: pre-designed page LAYOUTS, element PRESETS, or purpose-classified SECTIONS (hero/features/cta/...). Filter with `kind` ("layout" default \| "element" \| "section"), `topic`, `type`/`role`, `element_type`, `purpose`/`has_effects` (sections only), `website`, `query`. Returns `{items, total, next_cursor}`; a "section" id is the composite "<layoutId>#<sectionIndex>" feeds straight into library_get/library_insert. `total:0` usually means no Library API key configured. |
3853
- | `yootheme_builder_library_delete` | destructive | `confirm`, `id`, `site_id`, `source` | Delete ONE saved entry from your "WootsUp AI Layouts" store (the isolated library yootheme_builder_library_save_as writes into) clean up experimental sections/layouts the agent saved. Pass the `id` from a library_browse(source:"ai") row. `source` defaults to "ai", the ONLY deletable store; the curated library is read-only, so source:"catalog" returns a 403. IRREVERSIBLE requires confirm:true. Returns `{deleted, id, deleted_count, source}`; an unknown id returns a 404. Advanced/gateway-only. |
3854
- | `yootheme_builder_library_get` | read+idempotent | `id`, `include_thumbnail`, `kind`, `site_id`, `source` | Fetch a SINGLE YOOtheme Pro Library layout or element preset by `id` call after yootheme_builder_library_browse (items[].id feeds this). Returns `{id, kind, name, version, tree, sections, images_manifest_count}`: `tree` is the raw JSON; `sections` lists `tree.children` (layouts only, presets return `[]`), each `{index, name, path, element_count}`. `include_thumbnail:true` also returns an image block (costs tokens see its own description). Read-only; does not insert/bind. |
3855
- | `yootheme_builder_library_insert` | mutating | `etag`, `id`, `images`, `kind`, `section_index`, `site_id`, `source`, `target` | Insert a YOOtheme Pro Library item (layout, section, or preset) onto a page — call after yootheme_builder_library_get/browse. `target`: new_page (creates a page), {template_id,element_path?} (splices onto the layout root, or a preset onto a Column), or sublayout (splices rows into a fragment). A preset (kind:"element") MUST target a Column 422 otherwise; scaffold one first with yootheme_builder_scaffold_section. `images`: "placeholder" (default) or "curated" (downloads to Media Library). Optional `etag` locks (412 if stale). |
3856
- | `yootheme_builder_library_save_as` | mutating | `destination`, `name`, `site_id`, `source` | Save a page or subtree into a reusable library the REVERSE of yootheme_builder_library_insert. `source`: {template_id, element_path?} (omit element_path to save the WHOLE page). `destination`: "ai" (default your isolated "WootsUp AI Layouts", reusable via library_browse/insert source:"ai") or "native" (YOOtheme's built-in My Layouts). Returns `{library_id, name, storage, invalid_source_possible}` (the last flags bound sources that may need re-binding after reload). Advanced/gateway-only. |
3857
- | `yootheme_builder_local_content_read` | read+idempotent | `fields`, `include_children`, `include_meta`, `kind`, `limit`, `max_chars`, `offset`, `parent_id`, `search`, `site_id`, `status`, `taxonomy`, `type` | Read native records for one `kind`: entities (posts/pages/CPT/articles), terms (TERMS ids that archive-scope a Grid), users, fields (ACF/com_fields defs), menus, or media. Published-only + paginated: {items,total,next_offset}; pass next_offset back as `offset`. Archive-scoping flow: read {kind:"terms",taxonomy:"category"} pick a term id bind a source, pass query_arguments:{id:<term_id>}. CALL local_content_types FIRST for valid type/taxonomy. E.g. {kind:"entities",type:"product",search:"shoe"}. Slim with `fields[]`. |
3858
- | `yootheme_builder_local_content_types` | read+idempotent | `fields`, `kind`, `max_chars`, `site_id` | Discover the site's NATIVE content landscapepost types/articles, taxonomies and custom-field groups (ACF/com_fields), native to WP/Joomla (no API Mapper). Returns {content_types, taxonomies, field_groups, capabilities}; rows carry `bindable` + `source_name` (null for native types bind those with element_bind_source by the content-type `name`, e.g. "post"). CALL FIRST, then local_content_read for records. Narrow with `kind`, slim with `fields[]`. E.g. {kind:"content_types"}. |
3859
- | `yootheme_builder_navigation` | read+idempotent | `menu`, `site_id` | The site's navigation menus (WP nav menus / Joomla menus) as a parent -> children TREE, not a flat list, for "which page is under which menu item" (product pages usually live in the main menu). Each item carries a `page_role` facet (home\|landing\|product\|legal\|thankyou\|blog\|mockup\|template\|generic, same vocabulary as pages_list) and, when derivable at zero extra cost, a `target_page` (page:<id> / article:<id>) ready for yootheme_builder_page_get_layout. Read-only. Narrow to one menu with `menu`. |
3860
- | `yootheme_builder_page_dynamize` | mutating | `conditions`, `dry_run`, `element_path`, `etag`, `leaf_map`, `list_source_name`, `page`, `remove_leaves`, `section_name`, `site_id`, `source_map`, `template_id` | One-call way to dynamize a designed section: copies the layout byte-identically and swaps only what you map. Use INSTEAD of hand-editing JSON. Give a REFERENCE (section_name from template_summary, or element_path) + list_source_name + a leaf_map (original leaf field-name flat flow field; #parent + tuned props kept). A section with several source blocks dynamizes in ONE call via source_map ({match, source_name, leaf_map?}[]). Sub-query panels: key by "<subquery>.<leaf>". Returns page + `summary`. Preview with dry_run:true. |
3861
- | `yootheme_builder_page_get_layout` | read+idempotent | `cursor`, `fields`, `flat`, `force`, `max_chars`, `mode`, `post_id`, `site_id`, `template_id`, `url` | Get the layout tree for one template address it by `template_id` (from pages_list), `post_id`, or `url`. `mode:"bindings_only"` for dynamize/bind; `mode:"full"` (default) is nested `{layout, etag}`, `flat:true` a depth-first array. `max_chars` bounds the text; a clipped read returns `next_cursor` to page the rest. For a site-wide SEO/structure/content/links audit (h1, image alt, thin pages, dead links) do NOT loop this tool page-by-page call yootheme_builder_pages_audit (one sweep via yootheme_builder_advanced_read). |
3862
- | `yootheme_builder_page_get_schema` | read+idempotent | `fields`, `include_meta`, `max_chars`, `post_id`, `site_id`, `template_id`, `url` | Get the flat schema for a templatenodes with their JSON-Pointer paths and element types. Lighter than page_get_layout, enough to locate elements before editing. Address by `template_id` (pages_list), `post_id`, or `url`. `fields[]` slims each node; `max_chars` bounds output. |
3863
- | `yootheme_builder_page_publish` | idempotent | `etag`, `quick_check`, `site_id`, `template_id` | Publish a template persist state, flush YT + WP caches, snapshot the published-state ETag. YOOtheme templates already publish on save, so this is a cache-flush + state-snapshot op. Neither publish nor save changes the CMS post statusa draft page stays invisible to visitors; set status at creation via pages_create/library_insert. ETag optional when provided, 412 on conflict; when omitted, last-write-wins. |
3864
- | `yootheme_builder_page_save` | idempotent | `dry_run`, `etag`, `layout`, `quick_check`, `site_id`, `template_id` | Re-run save-transforms + flush caches. Optionally REPLACE the template tree by passing the INNER {type:"layout",children:[...]} tree as `layout` — page_get_layout's `.layout` child, NOT the whole response it returns (rejected 422, would blank the page). Validated + ETag-locked; nothing persists on reject. Does NOT change CMS post status — a draft stays invisible to visitors; set status via pages_create. `dry_run:true` preflights; success carries `previous_layout` for rollback. ETag optional (412 on conflict; else last-write-wins). |
3865
- | `yootheme_builder_pages_audit` | read+idempotent | `checks`, `include_meta`, `max_chars`, `mode`, `site_id`, `stale_after_months`, `target`, `thin_content_threshold`, `types` | One-call site-wide SEO/structure/content/links/design auditseverity-sorted findings with fix_hints (a props fragment to merge: supply template_id+path from the locator). Covers h1/headings, thin pages, image alt, duplicate layouts, title collisions, non-pretty URLs, dead anchors. Only active (non-disabled) nodes produce findings. A site sweep caps hits/page, drops content_snippet; `target` scopes to ONE page for the full set (cap 100). Filter with `types`/`checks`. Disclaimer: SEO-plugin meta (title/description/OG) NOT covered. |
3866
- | `yootheme_builder_pages_create` | idempotent | `dry_run`, `layout`, `menu_item`, `published`, `site_id`, `slug`, `status`, `title` | Create a page (WordPress) or article (Joomla 5/6) with a YOOtheme per-page layout. Required: title. Optional: slug, status (default draft), published (default false live on true: WP publish / Joomla state 1), layout (JSON), menu_item (Joomla SEF route), dry_run (preflight, no persist). Returns page_id (WP) / article_id (Joomla), slug, template_id (page:{id} or article:{id}), etag, warnings. Joomla: a published article has NO SEF route until a menu item points at it pass menu_item, else frontend_url may 404. |
3867
- | `yootheme_builder_pages_delete` | destructive | `confirm`, `force`, `post_id`, `site_id` | Delete a page (WordPress) or article (Joomla 5/6) with its YOOtheme per-page layout. Requires confirm:true (gate against accidental deletion). Default: trash (recoverable). force:true: permanent delete. Refuses non-page/article types with 403 (Layer 9 defense). Returns deleted:true, page_id (WP) / article_id (Joomla), force_deleted, bumped etag. |
3868
- | `yootheme_builder_pages_list` | read+idempotent | `cursor`, `fields`, `include_meta`, `label_contains`, `limit`, `max_chars`, `site_id` | List pages, templates, and layouts in the YOOtheme Pro builder (id, label, type, elements_count, frontend_url, builder_url, serves_context_kind, serves_context_ids, page_role per row). CALL THIS FIRST to discover template IDs before page_get_layout / element_list / page_get_schema. Paginates: up to `limit` rows (default 50) plus `total` and a `next_cursor` (pass it back as `cursor`) when more remain. Filter with `label_contains`. Slim rows with `fields:["id","label"]`; bound output with `max_chars`. |
3869
- | `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `target`, `url` | Resolve a public page/article/archive URL to the template_id that renders it paste a URL, get the layout id. Pretty/SEF permalinks resolve on WordPress AND Joomla (incl. deep sub-category paths), not only raw ?option= URLs. A singular page its per-page layout; a category/tag/CPT/author archive the governing THEME TEMPLATE, and for a category/term the SPECIFICALLY-ASSIGNED template (honoring include-child-categories), not a generic one. Or a `target`. Returns {template_id, post_id, type, frontend_url, context, resolved_via}. |
3870
- | `yootheme_builder_rename_source` | idempotent | `dry_run`, `etag`, `field_map`, `from`, `site_id`, `source_map`, `template_id`, `to` | Template-wide RENAME of a source / type / field name across EVERY binding in a template, in ONE ETag-locked write preserving each binding's arguments, props and field_mappings. Use after a taxonomy/CPT rename (faq→help) breaks every bound layout. Pass `from`/`to` for one source, and/or `source_map`/`field_map` to rename a relation + its field tokens together; the dotted "<source>.<relation>" form is rewritten segment-by-segment. Returns {replaced_count, changed_paths, new_etag}. Preview with dry_run:true. ETag req. |
3871
- | `yootheme_builder_render_preview` | read+idempotent | `expect`, `post_id`, `site_id`, `template_id`, `url` | Render-check a saved template — does it actually render, with how many items, or is it blank? Address by `template_id` (from pages_list), `post_id`, or `url` (resolved for you) exactly one. `item_count` = items yielded by the template's dynamic/Multi-Items bindings (rendered repeat-source rows); 0 with empty:true means a binding returned nothing. Returns {rendered, item_count, empty, error?, cache_purge_hint}; with `expect`, also {matched, snippet}. Use AFTER a save/bind to catch a silently-empty page. Read-only. |
3872
- | `yootheme_builder_scaffold_section` | mutating | `column_widths`, `columns`, `etag`, `site_id`, `template_id` | Create a VALID section row column hull in ONE call; returns the column JSON-Pointer path(s) for content. The correct way to start a layout block: only `section` may sit at the layout root; a bare section accepts `row`, a bare row accepts `column`; leaf/content elements are rejected anywhere except inside a `column`. Pass `columns` (1-12, default 1); columns get width tokens so they render side-by-side (hand-built columns default to full width), or `column_widths` (one token per column) for an asymmetric row. Requires ETag. |
3873
- | `yootheme_builder_sites_list` | read+idempotent | `include_meta`, `site_id` | List all sites configured in this multi-site MCP installation. Returns site_id + URL + platform (wordpress\|joomla) + default flag per row. CALL THIS FIRST when working with a fresh MCP connection to discover available site_ids before targeting one with any other tool. Read-only, no REST calls. Keywords: list sites, list connections, list installations, discover site_id, available sites, configured sites, what sites exist, multi-site index. The `default_site_id` is the write target when a call omits site_id. (site_id is accepted for schema-uniformity but ignored by this tool.) |
5277
+ | `yootheme_builder_clean_implode_directives` | write+destructive+idempotent | `confirm`, `element_path`, `etag`, `layout_id`, `site_id` | Use when a Multi-Items element renders rows as ONE comma-joined string instead of N repeated items — stale `implode` directives left by a plugin upgrade (detect via inspect_multi_items_binding). Destructive: the binding can't be restored without re-mapping. The clean requires confirm:true + ETag; the dry run neither. Without `confirm` the call runs a server-side DRY RUN and reports `would_clean_count` while writing nothing. Live at once. |
5278
+ | `yootheme_builder_diagnose` | read+idempotent | `site_id` | Why a call was refused: `auth_bound` (key accepted), `etag_ok` (layout readable), `site_url` (WHICH install; a key valid elsewhere fails here), `capability_ok` + `capability_warning` (the CMS user has no `can_edit`, so no blind 403). Install facts: health. |
5279
+ | `yootheme_builder_element_add` | write+non-idempotent | `children`, `element_type`, `etag`, `index`, `layout_id`, `name`, `parent_path`, `position`, `props`, `site_id`, `type_name` | Add a new element to a template. Provide `parent_path` (or "" for root), `element_type`, and optional `props`/`children`. Content/leaf elements may only be added inside a `column`; at the root only `section` else call scaffold_section for a valid column path. Inserting renumbers later siblings: `renumbered_siblings` names each moved sibling address (absent if none). Requires ETag. Live at once in the stored tree already save-transformed (defaults filled, empty children removed). Stage hidden with element_set_status(status:false). Prop names are snake_case. Common traps: `content` (not `title`) on button_item/headline/list_item/quotation/text; check `element_type_get_schema` first. The answer carries no `props`, only the new address. The stored node also carries the element's defaults read it back with element_get. |
5280
+ | `yootheme_builder_element_bind_source` | write+destructive+idempotent | `arguments`, `bindingLevel`, `confirm`, `directives`, `element_path`, `etag`, `field`, `field_mappings`, `layout_id`, `probe`, `query_arguments`, `relation`, `site_id`, `source_id`, `source_name` | Bind a Builder source to an element (canonical shape: `source` top-level). Pass `field_mappings` in the SAME call; bindingLevel "item" binds a container's first *_item child. Leaf under an iterating *_item → CONSOLIDATE onto item slots (409 slot_collision if taken); leaf under a #parent fragment → INHERIT (`__node_item__`); a standalone query renders empty. Map: bind `map_item`, map `location` to a "lat,lng" field or no markers. Requires ETag. Live at once. CONVENIENCE half of a pair — plain field names, but it REPLACES `source.props` whole, never merges; element_set_source is the CAPABILITY half. A re-bind that would drop anything already bound answers with a preview until you repeat it with `confirm: true`. To remove a binding, call element_unbind_source. |
5281
+ | `yootheme_builder_element_clone` | write+non-idempotent | `element_path`, `etag`, `layout_id`, `site_id`, `to_index`, `to_parent_path` | Clone an element. Default: as a sibling right after the source; pass `to_parent_path` to drop the copy elsewhere in the SAME template instead. Renumbers later siblings: `renumbered_siblings` names each moved sibling address (absent if none). Requires ETag. Live at once — stage hidden with element_set_status(status:false). |
5282
+ | `yootheme_builder_element_delete` | write+destructive+non-idempotent | `confirm`, `element_path`, `etag`, `layout_id`, `site_id` | PERMANENTLY delete an element + children. Cannot be undone. Pass `confirm: true` AND a fresh ETag. Preview: neither. A real delete returns `deleted:true` and NO `requires_confirmation` key; a returned `requires_confirmation:true` means NOTHING was deleted. `renumbered_siblings` names each moved sibling address (absent if none). Live at once. Hide instead: element_set_status(status:false). |
5283
+ | `yootheme_builder_element_get` | read+idempotent | `element_path`, `fields`, `layout_id`, `max_chars`, `site_id` | Get the full element object at a JSON-Pointer path, including props and a `children_count` (not the children — use element_list with root_path). On a `module`/`module_position` element it also returns `resolved` (the referenced widget/module row) plus `resolved_reason` when it could not be resolved. `max_chars` bounds the text output. |
5284
+ | `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `layout_id`, `max_chars`, `site_id` | Read the source binding on an element. Returns {layout_id, element_path, binding, has_binding, bound_via_item, item_binding?}: on a multi-item container (grid/slideshow/accordion/...) the binding sits on the `*_item` CHILD, so `has_binding:false` there does NOT mean static — read `bound_via_item`/`item_binding` first. Site-wide REVERSE question (which elements hang on a source): yootheme_builder_layout_query (gateway-only), OMIT layout_id. To remove what this returns, call element_unbind_source. |
5285
+ | `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `include_meta`, `layout_id`, `limit`, `max_chars`, `root_path`, `site_id` | List elements in a template as a flat array with JSON-Pointer paths + types + `label`. Scope with `root_path`/`depth`, paginate with `limit`/`cursor`. Rows carry BOTH addresses: `path` (absolute) and `rel_path` `rel_path` is the one write calls take as `element_path`. To find WHERE a module/widget is used, call local_content_read (gateway-only), never loop this per page. |
5286
+ | `yootheme_builder_element_move` | write+idempotent | `element_path`, `etag`, `layout_id`, `site_id`, `to_index`, `to_parent_path` | Move an element to a new parent + index in the tree reordering or reparenting. Renumbers siblings at BOTH ends: `renumbered_siblings` names each moved sibling address (absent if none). Requires ETag. Live at once. |
5287
+ | `yootheme_builder_element_set_source` | write+destructive+idempotent | `confirm`, `element_path`, `etag`, `layout_id`, `site_id`, `source`, `source_extended` | Surgically SET a precomposed `source` (+ optional `source_extended.props`) on ONE element at a pointer, ETag-locked. Use this — not element_bind_source — when you need `source_extended.props`, which bind_source cannot write. CAPABILITY half of a pair it also REPLACES `source.props` whole, never merges; element_bind_source is the CONVENIENCE half. Element-prop keys the type does not render echo back as `unknown_prop_keys`. WHOLE-NODE write: whatever you do not send is REMOVED. Omitting `source_extended` clears it exactly like passing `null`, and a `source` without `props` drops every prop mapping. Re-send what you want to keep. A call that would drop existing mappings answers with a preview until you repeat it with `confirm: true`. Live at once. To remove a binding entirely, call element_unbind_source. |
5288
+ | `yootheme_builder_element_set_status` | write+idempotent | `element_path`, `etag`, `layout_id`, `site_id`, `status` | Enable or disable an element via its `props.status` field — the SAME field YOOtheme's own native "Disable element" checkbox writes. status:false excludes the element and its descendants from BOTH the live render and live findings; pages_audit (gateway-only) and template_summary content_nodes both honour it. Requires ETag. Live at once. |
5289
+ | `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `field_names`, `fields`, `include_fields`, `max_chars`, `name_contains`, `site_id`, `type_name` | Prop schema; read before element_add/element_update_settings: undeclared keys are STORED, not refused: warns unknown_prop_keys. NO element_type: lists the types. With one: head only — binding_contract (containers/items) + groups[] + field_count. include_fields:true adds every descriptor {name,type,label?,text?,description?,enum?,value_hint?,group?,no_effect?}; Select descriptors: field_names/name_contains, answered with field_count/returned_count. value_hint+enum first: some defaults are SEO-fatal (headline.title_element→h1); checkbox = boolean true/false; no_effect = read by nothing, never write it; group:"runtime-accepted" = honoured at render, hidden in UI. Bound size with fields[]/max_chars. |
5290
+ | `yootheme_builder_element_types_list` | read+idempotent | `fields`, `include_meta`, `site_id` | List element types registered on this site (built-ins + YOOessentials extras). Names feed `element_type` of element_add. "slider"/"carousel" → `slideshow`/`panel-slider`/`overlay-slider`. Prop keys differ per element — use element_type_get_schema before element_add/bind. |
5291
+ | `yootheme_builder_element_unbind_source` | write+destructive+idempotent | `confirm`, `element_path`, `etag`, `layout_id`, `site_id` | Remove the source binding from an element — the counterpart to element_bind_source. Clears the canonical top-level `source` key AND the legacy `props.source` may break dynamic-content rendering, always ask the user to confirm. ETag on the unbind only. Live at once. |
5292
+ | `yootheme_builder_element_update_settings` | write+idempotent | `element_path`, `etag`, `layout_id`, `merge`, `name`, `props`, `site_id`, `unset` | Update `props` on an element. Default replaces all props; pass `merge:true` for server-side deep-merge (only request keys overwritten). Requires ETag. `unset:["key",…]` resets props to their YOOtheme default and is valid ON ITS OWN: sent without `props`, it removes only those keys and touches nothing else. Send `props`, `unset`, or both — a call with neither is refused. Live at once. |
5293
+ | `yootheme_builder_fetch_public_url` | read+idempotent+openWorld | `expect`, `max_excerpt_chars`, `site_id`, `url` | The independent proof a publish reached visitors: fetches a page of the CONFIGURED site over the public network as an ANONYMOUS visitor no login/cookies/Bearer and reports http_status, page_title, an EXCERPT (never the whole document), html_bytes, and whether a cache served it (cache_verdict; "unknown" means no header said, NOT that nothing cached, and no re-fetch settles it — purge the cache instead). With `expect` it answers expect_found true/false. Refuses any address outside the configured site and never follows a redirect off it an unrestricted fetcher is an open proxy. Get the url from `frontend_url` (pages_list / page_publish). Searches the WHOLE page (header, menu, footer too); a hit is no proof of THIS layout. render_preview renders ONLY that layout tree, never the theme wrapper. |
5294
+ | `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current site-wide ETag for optimistic locking. ANY write to ANY template bumps it a 412 means the site state advanced, not necessarily your template. Same token page_get_layout/element_list return. Pass back as `etag` on writes; on 412, re-read and retry. The ETag is SITE-wide, not per template that is why this tool takes no `layout_id`. |
5295
+ | `yootheme_builder_get_skill` | read+idempotent | `site_id`, `topic` | Read a chapter of the full workflow guide. Without `topic`: the list of available topics, each with the SIZE of what it returns. With `topic`: that chapter. The connect-time `instructions` carry only a short preamble this is how you reach the rest. A long chapter is served in PARTS: its row lists their slugs, and asking for the parent returns the chapter intro plus that list. A row carrying `oversized_note` is served whole and is large — read the note before asking for it. |
5296
+ | `yootheme_builder_header_transparency_set` | write+idempotent | `acknowledge_contrast_risk`, `dry_run`, `etag`, `layout_id`, `mode`, `pull_content_behind`, `site_id`, `text_color`, `transparent` | Inspect + repair the transparent header. Always targets the FIRST section later ones are silently dead. dry_run defaults TRUE: returns current state + the theme-declared --uk-inverse, risk_class, recommendation. mode:"auto" writes the theme-declared colour; over a hero image/video it writes NONE and says LOOK. mode:"explicit" refuses a colour contradicting the theme (409) unless acknowledge_contrast_risk. transparent:false is always allowed. |
5297
+ | `yootheme_builder_heading_outline` | read+idempotent | `layout_id`, `max_chars`, `mode`, `site_id` | A real, doc-order H1-H6 outline of ONE template, each heading with its source element, plus alt-status of every image. Structure inspection, not findings use pages_audit for site-wide SEO findings. One row per CARRIER: a container whose bound item repeats appears once, while template_summary.heading_levels counts it once per rendered row. |
5298
+ | `yootheme_builder_health` | read+idempotent | `site_id` | Check plugin installed/reachable. Returns plugin version, YT Pro version, REST endpoints, site_url, home_url on both platforms; wp_version + yooessentials_version are WordPress-only. available_endpoints is derived from the live route registration on both platforms; every path is request-ready as printed. served_areas lists the tool areas this server serves (was `capabilities` kept one release as an alias). Multi-site: pass `site_id`, else this checks the active/default site. `write_rate_limit` = the write cap this site enforces, per BEARER KEY: everything sharing the key shares it. See diagnose for Bearer-validity + connectivity. |
5299
+ | `yootheme_builder_image_import` | write+idempotent | `full_url`, `site_id`, `url` | Sideload a photo into the site's Media Library. Only Unsplash, Pexels and the YOOtheme library CDN are accepted; other hosts are refused. Returns `{imported:true, local_url}`; shape is platform-specific: WordPress leads with a slash (/wp-content/uploads/...), Joomla does NOT (images/yootheme/...). Bind it VERBATIM via element_update_settings({props:{image: local_url}, merge:true}). Give the address as `url` OR as `full_url`, the field name an image_search row carries; one of the two is required. Identical re-imports reuse the existing file in the current upload folder. Failures return `{imported:false, reason, failure}`; `unavailable` = no url works now. |
5300
+ | `yootheme_builder_image_search` | read+idempotent | `color`, `include_meta`, `offset`, `orientation`, `per_page`, `query`, `site_id`, `source` | Search on-topic photos (Unsplash/Pexels), YOOtheme Pro Images, or (source:"local") this site's own media library. Rows carry `full_url` (feeds image_import) plus dimensions/author. `total:0` is the ONLY shape meaning the provider matched nothing; a search that never happened answers an `error_code` instead. No apikey gives `image_search_api_key_not_configured`; a provider that REFUSES gives `error_code: image_search_unavailable`, which no broader query can fix. A key can be CONFIGURED and still be REFUSED (401): report that the upstream refused it, never why (source:"local" is exempt see its field description). Works on WordPress and Joomla. |
5301
+ | `yootheme_builder_inspect_multi_items_binding` | read+idempotent | `element_path`, `layout_id`, `site_id` | Reports Multi-Items binding state: container/item pair (grid↔grid_item, map↔map_item, ), binding level (none\|container\|item), recommended_fix. For map/map_item, a bound item without a `location` field is flagged as a blocking map_location_warning (0 markers). If rows render as ONE comma-joined string, see clean_implode_directives (gateway-only). |
5302
+ | `yootheme_builder_inspect_sublayout` | read+idempotent | `element_path`, `layout_id`, `max_chars`, `site_id` | Inspects a panel/grid_item `< SUBLAYOUT`: lists the `fragment` subtree bindable leaves and the render-time inner grid. Flags `ungated_grid_prop`: grid props set but the gate unsatisfied silent no-op. For container↔item binding see inspect_multi_items_binding. |
5303
+ | `yootheme_builder_layout_query` | read+idempotent | `content_contains`, `cursor`, `element_type`, `fields`, `has_binding`, `has_implode_directives`, `include_meta`, `layout_id`, `limit`, `max_chars`, `max_depth`, `max_paths`, `name_matches`, `site_id`, `source_name`, `under_section` | TWO scopes. OMIT layout_id SITE-WIDE: binding counts per `source_name` (COUNTED, never resolved a source that no longer exists counts like a live one; pages_audit flags those as `dangling-source`, with paths), `max_paths` for individual paths, `has_implode_directives:true` to locate the comma-joining leftovers. WITH layout_id → ONE layout, AND-combined `element_type`, `name_matches`, `content_contains` (case-insensitive substring of `content`/`title`/`meta` props), `has_binding`, `under_section`; paginate `limit`/`cursor`. Mixing scope arguments is REFUSED, never dropped. To find WHERE a module/widget is used call local_content_read (kind:"modules"). On a module/module_position row `label` is the builder-assigned element name, NOT the module title: read element_get(.resolved). |
5304
+ | `yootheme_builder_library_browse` | read+idempotent | `cursor`, `element_type`, `has_effects`, `include_meta`, `kind`, `limit`, `purpose`, `query`, `role`, `site_id`, `source`, `topic`, `type`, `website` | Browse the YOOtheme Pro Library catalog: pre-designed page LAYOUTS, element PRESETS, or purpose-classified SECTIONS. Filter: `kind`, `topic`, `type`/`role`, `element_type`, `purpose`, `query`. A section id is the composite "<layoutId>#<sectionIndex>" feeds library_get/insert. `source`: "catalog" (default) or "ai" YOUR saved entries from library_save_as, which library_delete removes again — both gateway-only, call via yootheme_builder_advanced({tool, arguments}). `total:0` = the catalog matched nothing; a read with no data answers `library_upstream_unavailable` or `library_api_key_not_configured`, never an empty page. |
5305
+ | `yootheme_builder_library_delete` | write+destructive+non-idempotent | `confirm`, `id`, `site_id`, `source` | Delete ONE saved entry from your "WootsUp AI Layouts" store (library_save_as writes into it) pass the `id` from a library_browse(source:"ai") row. `source` defaults to "ai", the ONLY deletable store; the curated library is read-only, source:"catalog" returns 403. IRREVERSIBLE requires confirm:true. |
5306
+ | `yootheme_builder_library_get` | read+idempotent | `id`, `include_thumbnail`, `kind`, `max_chars`, `site_id`, `source` | Fetch ONE Library layout or element preset by `id`call after library_browse (items[].id feeds this). `include_thumbnail:true` adds an image (costs tokens). This DETAIL endpoint checks the key, browse does not: no key gives 503 library_api_key_not_configured, a refused key 503 library_upstream_unavailable. Report the refusal, not a cause. `source`: "catalog" (default) or "ai" for your own library_save_as (gateway-only) entries. |
5307
+ | `yootheme_builder_library_insert` | write+destructive+non-idempotent | `confirm`, `etag`, `id`, `images`, `index`, `insert_mode`, `kind`, `section_index`, `site_id`, `source`, `target` | Insert a Library item onto a page — call after library_get/browse. `target`: new_page, {template_id,element_path?} (splices onto the layout root, or a preset onto a Column), or sublayout. A preset (kind:"element") MUST target a Column (422 otherwise) scaffold one with scaffold_section. `images`: "placeholder" (default) or "curated" (downloads to Media Library). Placement: `insert_mode` for layouts, `index` for presets. `etag` locks (412 if stale). `source`: "catalog" (default) or "ai" to insert your own saved entry. |
5308
+ | `yootheme_builder_library_save_as` | write+non-idempotent | `destination`, `name`, `site_id`, `source` | Save a page or subtree into a reusable library the REVERSE of library_insert. `source`: {template_id, element_path?} (omit element_path to save the WHOLE page). `destination`: "ai" (defaultyour isolated "WootsUp AI Layouts", reusable via library_browse source:"ai") or "native" (YOOtheme's built-in My Layouts). |
5309
+ | `yootheme_builder_local_content_read` | read+idempotent+openWorld | `fields`, `include_children`, `include_meta`, `include_usage`, `kind`, `limit`, `max_chars`, `offset`, `parent_id`, `search`, `site_id`, `status`, `taxonomy`, `type`, `verify_on_urls` | Read native records for one `kind`: entities, terms (ids that archive-scope a Grid), users, fields, menus, media, or modules (Builder widgets). Paginated {items,total,next_offset}, clamped to what the key may see. A deleted page/article is not in the default list; `status` (kind:"entities" only) gives the exclusive trashed view; the response carries `trashed_total` unasked when one exists. Archive-scoping: read {kind:"terms",taxonomy:"category"}, pick a term id, bind a source with query_arguments:{id:<term_id>}. Call local_content_types first. |
5310
+ | `yootheme_builder_local_content_types` | read+idempotent | `fields`, `kind`, `max_chars`, `site_id` | Discover the site's NATIVE content landscape, native to WP/Joomla (no API Mapper) {content_types, taxonomies, field_groups, capabilities, positions}; taxonomies include ACF/com_fields groups. Content-type and taxonomy rows carry `bindable` + `source_name` (null for native types bind via element_bind_source by content-type name); field-group rows carry neither. `positions` (B23) lists every valid widget-area / theme-position value for modules_create/modules_update's `position` argument read it here BEFORE a write, not from a 400 refusal after one; null when not determinable, never `[]` as a claim that none exist. Call first, then local_content_read for records. |
5311
+ | `yootheme_builder_modules_create` | write+non-idempotent | `base`, `cache`, `cache_time`, `cachemode`, `class_sfx`, `content`, `dry_run`, `endLevel`, `layout`, `menutype`, `moduleclass_sfx`, `placement`, `position`, `showAllChildren`, `showtitle`, `site_id`, `startLevel`, `tag_id`, `text`, `title`, `type`, `window_open` | Create a widget (WordPress: custom_html/text/block) or module (Joomla: mod_custom/mod_menu), exactly five known types. REQUIRES placement (position, module_element or position_element): a call without it creates nothing and returns ways (every way this platform offers) plus notes. `position` is validated server-side against what this site declares and must agree with placement. content/text stored byte-for-byte, never sanitized. dry_run defaults TRUE, creates nothing; a real create returns the full row PLUS next_step, the exact follow-up call for the placement you chose. Gateway-only, not in tools/list call via yootheme_builder_advanced. |
5312
+ | `yootheme_builder_modules_delete` | write+destructive+non-idempotent | `acknowledge_element_refs`, `confirm`, `id`, `intent`, `site_id` | Delete a widget (WordPress) or module (Joomla)the clean-up verb create/update lacked. Needs `intent`: call once without it and the server lists which are possible for this module's CURRENT state, each with its own effect. confirm defaults FALSE: returns the row plus usage.element_refs and changes NOTHING. A DIRECT layout reference blocks the irreversible step (409) until acknowledge_element_refs:true. |
5313
+ | `yootheme_builder_modules_update` | write+idempotent | `access`, `active`, `assignment`, `content`, `dry_run`, `id`, `ordering`, `position`, `showtitle`, `site_id`, `theme_overlay`, `title` | Update a widget/module's writable subset: title, content (byte-identical), position, active, theme_overlay; plus showtitle, ordering, access, assignment on Joomla. dry_run defaults TRUE, returns {before, after, changed, notes}, writes nothing. Refused: title on a WordPress block widget (it has none), a position this site does not declare (either platform, same error_code as modules_create), and active:true while a Joomla publish window leaves it scheduled/expired there active means published AND that window. Joomla: active:true also lifts a module out of the trash, PUBLISHED not as it was. position:"" clears the position on Joomla, refused on WordPress (deactivate with active:false instead). Gateway-only, not in tools/list — call via yootheme_builder_advanced. |
5314
+ | `yootheme_builder_navigation` | read+idempotent | `menu`, `site_id` | The site's navigation menus as a parent→children TREE, not a flat list "which page is under which menu item". Each item carries a `page_role` facet and, when derivable, a `target_page` ready for page_get_layout. |
5315
+ | `yootheme_builder_page_generate_dynamic` | write+non-idempotent | `conditions`, `dry_run`, `element_path`, `etag`, `layout_id`, `leaf_map`, `list_source_name`, `output`, `page`, `remove_leaves`, `section_name`, `site_id`, `source_map` | Copies a designed section byte-identically into a NEW, unpublished, unlinked page and swaps only what you map it never edits the reference. Requires output:"new_page" (today's only value; "in_place" is not yet available). Give a REFERENCE (section_name or element_path) + list_source_name + a leaf_map. A section with several source blocks dynamizes in ONE call via source_map. Preview with dry_run:true (a would_change:false answer carries `reason`). Live at once — stage hidden with element_set_status(status:false). The response carries `cms_visibility.findable_by_visitor`: can a visitor FIND this one page via a navigation path? It reads the menu route AND the access level, which the per-row `visibility` of pages_list does not. Cannot make it the site front page (a CMS menu setting). |
5316
+ | `yootheme_builder_page_get_layout` | read+idempotent | `fields`, `flat`, `force`, `include_modules`, `layout_id`, `max_chars`, `mode`, `site_id`, `url` | Get the layout tree for one layout address by `layout_id` or `url`. `mode:"bindings_only"` for dynamize/bind. Over `max_chars` whole entries DROP (`truncated`+`omitted_count`; outline cuts its text) and NOTHING pages the rest no cursor in any mode. Narrow instead: `mode:"outline"` to map, then element_list(root_path). For a site-wide SEO/structure audit do NOT loop this tool page-by-page call yootheme_builder_pages_audit (gateway-only). For ONE narrow question (e.g. "where is the binding" or "which elements have type X") this whole-template read is heavier than it needs to be prefer yootheme_builder_layout_query (gateway-only; has_binding/element_type/name_matches filters) or element_list with `root_path` to scope the walk. |
5317
+ | `yootheme_builder_page_publish` | write+idempotent | `cms_status`, `etag`, `layout_id`, `menu_item`, `quick_check`, `site_id` | Publish a template's LAYOUT persist state, flush the plugin's own caches (a page-cache/CDN plugin is NOT purged: purge it yourself, then verify with fetch_public_url), snapshot the ETag; "publish" ALSO means the CMS post status, and this tool does not change that, so a draft page stays invisible. Step (2) of the three page_save describes; pages_set_status is step (3). ETag required — 412 on conflict. Answers `cms_visibility.findable_by_visitor`: can a visitor FIND this one page via a navigation path? It reads the menu route AND the access level, which the per-row `visibility` of pages_list does not. Optional `cms_status` folds step (3) into this call for the common build-it-and-make-it-live case; without it nothing about the CMS status changes. |
5318
+ | `yootheme_builder_page_save` | write+idempotent | `dry_run`, `etag`, `layout`, `layout_id`, `quick_check`, `site_id` | Re-run save-transforms + flush the plugin's own caches. Optional whole-tree replace via `layout` — the INNER {type:"layout",children:[...]} tree, NOT page_get_layout's whole response (rejected 422). THREE separate steps, and this is the first: (1) page_save replaces or re-normalises the stored tree — every write, element or whole-tree, runs the same save-transforms; (2) page_publish snapshots the ETag and flushes them; (3) pages_set_status decides whether a visitor may see the page at all. This call does NOT do (2) or (3). Response `previous_layout` is the tree right before THIS call, not before your edits (element writes already landed) identical to what you just wrote is normal when the transforms change nothing. `dry_run:true` preflights, no ETag. ETag on the save (428/412). |
5319
+ | `yootheme_builder_pages_audit` | read+idempotent | `checks`, `include_meta`, `include_modules`, `max_chars`, `mode`, `site_id`, `stale_after_months`, `target`, `thin_content_threshold`, `types` | One-call site-wide SEO / structure / content / links / design auditseverity-sorted findings with fix_hints (a props fragment to merge). Covers heading structure and accessibility (h1/heading-skip), image alt text, thin content, duplicate layouts, title collisions, broken in-page anchor links (same-page `#id` jumps only no external link is ever fetched or resolved), and design consistency (markup outside the UIkit design system, header/navigation contrast). Only active (non-disabled) nodes produce findings. `target` scopes to ONE page for the full set. SEO-plugin meta (title/description/OG) NOT covered. |
5320
+ | `yootheme_builder_pages_create` | write+non-idempotent | `dry_run`, `layout`, `menu_item`, `published`, `site_id`, `slug`, `status`, `title` | Create a page (WordPress) or article (Joomla 5/6) with a per-page layout. Required: title. Optional: status (default draft), layout, menu_item, dry_run (preflight, no persist). Joomla: a published article has NO SEF route until a menu item points at it pass menu_item for a navigable url. Cannot make it the site front page: which page answers / is a CMS menu setting this server never writes. pages_list marks the current one with is_public_homepage. |
5321
+ | `yootheme_builder_pages_delete` | write+destructive+non-idempotent | `confirm`, `intent`, `layout_id`, `site_id` | Delete a page (WordPress) or article (Joomla 5/6). OMIT confirm to preview: returns 200 with label, elements_count, what happens to media, and the intents this page's state allows, and deletes nothing. confirm:true + an intent carries it out. Already-trashed returns 200 {deleted:false, already_trashed:true}, not an error. |
5322
+ | `yootheme_builder_pages_list` | read+idempotent | `cursor`, `fields`, `include_meta`, `label_contains`, `limit`, `max_chars`, `site_id`, `sort`, `visibility` | List pages, templates and layouts. Each row returns `kind` (content \| theme_template) plus serves_context_kind, serves_context_ids and page_role — the template ROLE (WHICH objects it renders for), NOT a data binding; for who-binds-what call layout_query with no layout_id. The envelope returns `totals`. Call first for layout_id (page_get_layout/element_list). `type` = PAGE type, not element type (those filter on `element_type`). Rows with a CMS status carry `visibility`: public \| hidden \| undetermined, never empty. Reads STORED FIELDS only — status, plus WordPress password protection (`hidden`, with the reason). It does NOT read the menu route, the access level, or whether the permalink resolves, so Joomla rows stay `undetermined`; for THAT verdict read `cms_visibility.findable_by_visitor` from pages_set_status / page_publish — a different, per-page question. Page contents (SEO/structure/links/modules): yootheme_builder_pages_audit. `is_public_homepage` marks the one row served at / (the site front page); no tool here moves it. |
5323
+ | `yootheme_builder_pages_resolve` | read+idempotent | `site_id`, `target`, `url` | Resolve a page/article/archive URL to the layout_id that renders it. SEF/pretty permalinks resolve on both WordPress and Joomla. A category/term resolves to the SPECIFICALLY-ASSIGNED template (honoring include-child-categories), not a generic one. Returns {layout_id, post_id, type, frontend_url, context, resolved_via}. |
5324
+ | `yootheme_builder_pages_restore` | write+non-idempotent | `layout_id`, `site_id` | Restore a TRASHED page (WordPress) or article (Joomla) — the one correct way back (pages_set_status refuses a trashed page with 409 naming this tool). WordPress: reinstates the EXACT pre-trash status (core defaults restores to draft since 5.6) and attempts to reclaim the pre-trash slug — a hand-rolled write leaves the "__trashed" suffix instead; a slug another page has since taken comes back different — check the response's frontend_url. Joomla stores no pre-trash status, so this can only set state to unpublished (visible to editors, not visitors); call yootheme_builder_pages_set_status with status:"published" afterwards to make it visible. Not currently trashed → refused (409). Find a trashed id with yootheme_builder_local_content_read({kind:"entities", status:"trash"}). |
5325
+ | `yootheme_builder_pages_set_status` | write+idempotent | `layout_id`, `site_id`, `status` | Change the CMS status of an EXISTING page (WordPress) or article (Joomla) — keeps its id and URL. Step (3) of the three page_save describes. Use YOUR platform's own words: WordPress publish, draft, pending, private, future; Joomla published, unpublished, archived — the other platform's word is refused (422). Status is RE-READ after the write. To trash or delete use yootheme_builder_pages_delete; a page already IN the trash is refused (409) — use yootheme_builder_pages_restore first. The response carries `cms_visibility.findable_by_visitor`: can a visitor FIND this one page via a navigation path? It reads the menu route AND the access level, which the per-row `visibility` of pages_list does not. |
5326
+ | `yootheme_builder_render_preview` | read+idempotent | `expect`, `layout_id`, `site_id`, `url` | Render-check a SAVED layout. Address by `layout_id` or `url`. rendered_item_wrapper_count counts the repeat-item wrappers rendered, BOUND OR NOT, so a 0 is not evidence a binding failed (per-source rows: `source_item_counts`). Distinct from pages_list `elements_count` (all tree nodes). A source the registry cannot resolve is named in `unresolved_sources`, not just a smaller count. Renders ONLY the page's own layout tree, never the theme wrapper: a MISS with `expect` proves nothing beyond the tree (adds {matched, snippet, match_conclusive, match_note, mismatch_hints, stored_but_not_rendered — the last names elements that DO store the text but rendered no rows}). A Joomla module/module_position element sharpens this — EXCLUDED (fallback_used:true) or silently empty (dropped_count:0) — but match_conclusive:false marks EVERY inconclusive miss. Reports CMS status in `cms_status`/`cms_status_note` (absent for a theme template); pages_set_status remains the tool that changes it. |
5327
+ | `yootheme_builder_repair_source_references` | write+idempotent | `allow_unregistered`, `dry_run`, `etag`, `field_map`, `from`, `layout_id`, `site_id`, `source_map`, `to` | Repairs binding REFERENCES to a source/type/field name in ONE template, in one ETag-locked write, after that name changed OUTSIDE the builder (a taxonomy/CPT rename, a re-published flow). Does NOT rename the source itself — sources_list stays unchanged. Pass `from`/`to` for one token, or `source_map`/`field_map` for several. Preview with dry_run:true. |
5328
+ | `yootheme_builder_scaffold_section` | write+non-idempotent | `column_widths`, `columns`, `etag`, `layout_id`, `site_id` | Create a VALID section → row → column hull in ONE call; returns the column path(s) for content. Only `section` may sit at the layout root; leaf/content elements are rejected anywhere except inside a `column`. Pass `columns` (1-12) or `column_widths` for an asymmetric row. Requires ETag. |
5329
+ | `yootheme_builder_sites_list` | read+idempotent | `include_meta`, `site_id` | List all sites configured in this multi-site MCP installation — site_id, URL, platform, DEFAULT + ACTIVE flag per row. Call first to discover site_ids. A call omitting site_id goes to `active_site_id` (set by use_site, process-wide) else to `default_site_id`. Pass `site_id` to FILTER to one site; omit it for all. IDs from any tool (layout_id, element ids, …) are PER-SITE, never global: the same id can exist on a different site naming something else entirely. Always keep an id paired with the site_id it came from. |
5330
+ | `yootheme_builder_sites_overview` | read+idempotent | `include_meta`, `include_pages`, `max_chars`, `site_id` | Every configured site in ONE call: reachability, Bearer validity and page counts per site, probed in parallel. Replaces sites_list plus one sites_test and one pages_list per site. A site that is down becomes a row with an `error`, never a failed call. Pass `site_id` to narrow to one site; `include_pages:false` skips the page read (one HTTP request per site less). The content/theme_template split is absent on hosts that do not report it — never reported as zero. |
3874
5331
  | `yootheme_builder_sites_test` | read+idempotent | `site_id` | Verify connectivity to ONE site: probes /health (no auth) + /etag (auth) in parallel; returns plugin_reachable + bearer_valid. `site_id` is REQUIRED. Use sites_list to find IDs. |
3875
- | `yootheme_builder_source_get_schema` | read+idempotent | `max_chars`, `name`, `site_id` | Introspect ONE data source: its query methods + arguments + RELATIONS (e.g. Term→Posts: categoryFaqs.taxonomyCategoryFaq → field "faq") + a provides_archive_context flag (id:0 = current archive term, auto-scoped). SOME arguments carry `enum_values` (the allowed values); when an arg has none it is a free string — pick from enum_values when present, never invent values. CALL BEFORE binding on an archive/category template. Keywords: source schema, relations, archive context, enum values. Get `name` from yootheme_builder_sources_list. |
3876
- | `yootheme_builder_sources_list` | read+idempotent | `cursor`, `fields`, `group`, `include_meta`, `kind`, `limit`, `max_chars`, `name_contains`, `site_id` | List all data sources, feeds, and dynamic content sources available in the YOOtheme Pro builder. Returns name + label + origin (apimapper / wordpress / joomla / essentials) per source. CALL THIS BEFORE binding any element to a data source. Then bind directly with yootheme_builder_element_bind_source. Keywords: list sources, list feeds, list bindings, dynamic content, what sources exist. Paginates (`limit`/`cursor`/`total`); scope with `group`/`kind`/`name_contains`; slim with `fields[]`; bound output with `max_chars`. |
3877
- | `yootheme_builder_template_set_type` | idempotent | `dry_run`, `etag`, `site_id`, `template_id`, `type` | Reassign which archive/single a YOOtheme THEME template applies to — set its `type` condition (the value pages_list shows) in ONE ETag-locked write. Use after a taxonomy/CPT rename orphans a template (e.g. re-type "taxonomy-category_faq" → "taxonomy-category_help"). Returns {type, previous_type, new_etag}. Site-wide + immediate — preview with dry_run:true, reversible via previous_type. THEME templates only: page:/article: is refused (422). ETag req. |
3878
- | `yootheme_builder_template_summary` | read+idempotent | `cursor`, `include_nodes`, `limit`, `site_id`, `template_id`, `template_ids` | Token-efficient template overview: element counts by type, binding count, max nesting depth, and named landmark sections, computed server-side in one call. Use this to grasp a large template before pulling element_list or page_get_layout. Pass `include_nodes:true` for a `content_nodes` per-node breakdown (status/effective_visible/headings/alts), pageable via `limit`/`cursor`. Pass `template_ids:[...]` for a batch: response carries `summaries` + `not_found`. Example: yootheme_builder_template_summary({ template_id: "default" }). |
3879
- | `yootheme_builder_use_site` | idempotent | `probe`, `site_id` | Switch the ACTIVE site for this multi-site session. After this call, every tool that omits `site_id` targets the site you picked here. NOTE: this default is PROCESS-WIDE — concurrent clients sharing this MCP server inherit it; pass `site_id` per call for isolation between parallel agents. Runs a /health + /etag probe by default; pass `probe: false` to switch offline. A degraded site is a soft warning (the switch still happens). `site_id` is REQUIRED. Keywords: switch/change/set active/target site, select installation, use profile. Use yootheme_builder_sites_list first to find IDs. |
5332
+ | `yootheme_builder_source_get_schema` | read+idempotent | `max_chars`, `name`, `site_id`, `source_name` | Introspect ONE data source: query methods + arguments + RELATIONS (e.g. Term→Posts: categoryFaqs.taxonomyCategoryFaq → field "faq"). `is_archive_context_provider` says whether the source CAN follow the current archive term; it does NOT say whether the source returns rows here only a render answers that (element_bind_source with probe:true, or render_preview). Some arguments carry `enum_values` — pick from those when present, never invent values. Call before binding on an archive/category template. Get `name` from sources_list. |
5333
+ | `yootheme_builder_sources_list` | read+idempotent | `cursor`, `fields`, `group`, `include_meta`, `kind`, `limit`, `max_chars`, `name_contains`, `site_id` | List data sources/feeds available in the builder name + label + origin + `group` per source. Call before binding an element with element_bind_source. `group` is what a source answers FROM: a "Page"-group LIST source serves the CURRENT page/archive context, so a standalone page gets zero rows from it. Each row says so in `context_note` when it applies. A long list narrows with `group` (the ORIGIN bucket: apimapper \| wordpress \| joomla \| essentials — not the per-row `group` column above), `name_contains`, or `kind`. Reach for one before paging. WHERE a source is used, site-wide: yootheme_builder_layout_query (gateway-only), OMIT layout_id. |
5334
+ | `yootheme_builder_template_set_type` | write+idempotent | `dry_run`, `etag`, `layout_id`, `site_id`, `type` | Reassign which archive/single a YOOtheme THEME template applies to — set its `type` condition in ONE ETag-locked write. Use after a taxonomy/CPT rename orphans a template. Site-wide + immediate — preview with dry_run:true, reversible via previous_type. THEME templates only: page:/article: refused (422). |
5335
+ | `yootheme_builder_template_summary` | read+idempotent | `cursor`, `include_nodes`, `layout_id`, `layout_ids`, `limit`, `site_id` | Token-efficient template overview: element counts by type, bindings, max depth, named sections computed server-side in one call. Use before element_list/page_get_layout on a large template. `include_nodes:true` adds a `content_nodes` per-node breakdown (status/effective_visible/headings/alts), pageable via `limit`/`cursor`. `layout_ids:[...]` batches several: response carries `summaries`, `not_found` and `forbidden`. Example: yootheme_builder_template_summary({ layout_id: "default" }). |
5336
+ | `yootheme_builder_use_site` | write+idempotent | `probe`, `site_id` | Switch the ACTIVE site for this session subsequent calls that omit `site_id` target it. NOTE: this default is PROCESS-WIDE — concurrent clients sharing this MCP server inherit it; pass `site_id` per call for isolation. Runs a /health + /etag probe by default; `probe:false` switches offline. `site_id` is REQUIRED. |
3880
5337
 
3881
5338
  <!-- TOOL-CATALOG:END -->