@wootsup/yt-builder-mcp 1.0.0 → 1.1.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 (187) hide show
  1. package/README.md +18 -6
  2. package/bin/yt-builder-mcp.js +6 -0
  3. package/dist/auth.d.ts +86 -6
  4. package/dist/auth.d.ts.map +1 -1
  5. package/dist/auth.js +141 -8
  6. package/dist/auth.js.map +1 -1
  7. package/dist/errors/sanitize.d.ts.map +1 -1
  8. package/dist/errors/sanitize.js +10 -0
  9. package/dist/errors/sanitize.js.map +1 -1
  10. package/dist/gateway/essentials.d.ts +1 -1
  11. package/dist/gateway/essentials.d.ts.map +1 -1
  12. package/dist/gateway/essentials.js +15 -2
  13. package/dist/gateway/essentials.js.map +1 -1
  14. package/dist/index.d.ts +5 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +52 -16
  17. package/dist/index.js.map +1 -1
  18. package/dist/install-skill.d.ts +10 -2
  19. package/dist/install-skill.d.ts.map +1 -1
  20. package/dist/install-skill.js +18 -7
  21. package/dist/install-skill.js.map +1 -1
  22. package/dist/platform/detect.d.ts +64 -0
  23. package/dist/platform/detect.d.ts.map +1 -0
  24. package/dist/platform/detect.js +98 -0
  25. package/dist/platform/detect.js.map +1 -0
  26. package/dist/platform/index.d.ts +72 -0
  27. package/dist/platform/index.d.ts.map +1 -1
  28. package/dist/platform/index.js +138 -0
  29. package/dist/platform/index.js.map +1 -1
  30. package/dist/platform/joomla.d.ts +44 -0
  31. package/dist/platform/joomla.d.ts.map +1 -0
  32. package/dist/platform/joomla.js +46 -0
  33. package/dist/platform/joomla.js.map +1 -0
  34. package/dist/server.d.ts +61 -2
  35. package/dist/server.d.ts.map +1 -1
  36. package/dist/server.js +86 -2
  37. package/dist/server.js.map +1 -1
  38. package/dist/setup-cli-confirm.d.ts +36 -0
  39. package/dist/setup-cli-confirm.d.ts.map +1 -0
  40. package/dist/setup-cli-confirm.js +59 -0
  41. package/dist/setup-cli-confirm.js.map +1 -0
  42. package/dist/setup-cli.d.ts +53 -1
  43. package/dist/setup-cli.d.ts.map +1 -1
  44. package/dist/setup-cli.js +525 -10
  45. package/dist/setup-cli.js.map +1 -1
  46. package/dist/setup-prompts.d.ts.map +1 -1
  47. package/dist/setup-prompts.js +12 -8
  48. package/dist/setup-prompts.js.map +1 -1
  49. package/dist/setup-wizard-types.d.ts +19 -3
  50. package/dist/setup-wizard-types.d.ts.map +1 -1
  51. package/dist/setup-wizard.d.ts.map +1 -1
  52. package/dist/setup-wizard.js +34 -1
  53. package/dist/setup-wizard.js.map +1 -1
  54. package/dist/sites/cli/add-site.d.ts +106 -0
  55. package/dist/sites/cli/add-site.d.ts.map +1 -0
  56. package/dist/sites/cli/add-site.js +185 -0
  57. package/dist/sites/cli/add-site.js.map +1 -0
  58. package/dist/sites/cli/list-sites.d.ts +40 -0
  59. package/dist/sites/cli/list-sites.d.ts.map +1 -0
  60. package/dist/sites/cli/list-sites.js +117 -0
  61. package/dist/sites/cli/list-sites.js.map +1 -0
  62. package/dist/sites/cli/remove-site.d.ts +60 -0
  63. package/dist/sites/cli/remove-site.d.ts.map +1 -0
  64. package/dist/sites/cli/remove-site.js +120 -0
  65. package/dist/sites/cli/remove-site.js.map +1 -0
  66. package/dist/sites/cli/set-default.d.ts +47 -0
  67. package/dist/sites/cli/set-default.d.ts.map +1 -0
  68. package/dist/sites/cli/set-default.js +69 -0
  69. package/dist/sites/cli/set-default.js.map +1 -0
  70. package/dist/sites/cli/test-site.d.ts +77 -0
  71. package/dist/sites/cli/test-site.d.ts.map +1 -0
  72. package/dist/sites/cli/test-site.js +146 -0
  73. package/dist/sites/cli/test-site.js.map +1 -0
  74. package/dist/sites/client-pool.d.ts +124 -0
  75. package/dist/sites/client-pool.d.ts.map +1 -0
  76. package/dist/sites/client-pool.js +172 -0
  77. package/dist/sites/client-pool.js.map +1 -0
  78. package/dist/sites/env-bridge.d.ts +55 -0
  79. package/dist/sites/env-bridge.d.ts.map +1 -0
  80. package/dist/sites/env-bridge.js +94 -0
  81. package/dist/sites/env-bridge.js.map +1 -0
  82. package/dist/sites/paths.d.ts +21 -0
  83. package/dist/sites/paths.d.ts.map +1 -0
  84. package/dist/sites/paths.js +37 -0
  85. package/dist/sites/paths.js.map +1 -0
  86. package/dist/sites/probe.d.ts +60 -0
  87. package/dist/sites/probe.d.ts.map +1 -0
  88. package/dist/sites/probe.js +112 -0
  89. package/dist/sites/probe.js.map +1 -0
  90. package/dist/sites/registry.d.ts +142 -0
  91. package/dist/sites/registry.d.ts.map +1 -0
  92. package/dist/sites/registry.js +163 -0
  93. package/dist/sites/registry.js.map +1 -0
  94. package/dist/sites/schema.d.ts +90 -0
  95. package/dist/sites/schema.d.ts.map +1 -0
  96. package/dist/sites/schema.js +71 -0
  97. package/dist/sites/schema.js.map +1 -0
  98. package/dist/sites/secret-resolver.d.ts +109 -0
  99. package/dist/sites/secret-resolver.d.ts.map +1 -0
  100. package/dist/sites/secret-resolver.js +189 -0
  101. package/dist/sites/secret-resolver.js.map +1 -0
  102. package/dist/sites/store.d.ts +82 -0
  103. package/dist/sites/store.d.ts.map +1 -0
  104. package/dist/sites/store.js +198 -0
  105. package/dist/sites/store.js.map +1 -0
  106. package/dist/sites/tools/index.d.ts +28 -0
  107. package/dist/sites/tools/index.d.ts.map +1 -0
  108. package/dist/sites/tools/index.js +30 -0
  109. package/dist/sites/tools/index.js.map +1 -0
  110. package/dist/sites/tools/sites-list.d.ts +48 -0
  111. package/dist/sites/tools/sites-list.d.ts.map +1 -0
  112. package/dist/sites/tools/sites-list.js +135 -0
  113. package/dist/sites/tools/sites-list.js.map +1 -0
  114. package/dist/sites/tools/sites-test.d.ts +47 -0
  115. package/dist/sites/tools/sites-test.d.ts.map +1 -0
  116. package/dist/sites/tools/sites-test.js +148 -0
  117. package/dist/sites/tools/sites-test.js.map +1 -0
  118. package/dist/skill-loader.d.ts +63 -3
  119. package/dist/skill-loader.d.ts.map +1 -1
  120. package/dist/skill-loader.js +123 -10
  121. package/dist/skill-loader.js.map +1 -1
  122. package/dist/tools/elements/builders.d.ts +2 -2
  123. package/dist/tools/elements/builders.d.ts.map +1 -1
  124. package/dist/tools/elements/builders.js +45 -23
  125. package/dist/tools/elements/builders.js.map +1 -1
  126. package/dist/tools/elements/handlers.d.ts +8 -1
  127. package/dist/tools/elements/handlers.d.ts.map +1 -1
  128. package/dist/tools/elements/handlers.js +37 -1
  129. package/dist/tools/elements/handlers.js.map +1 -1
  130. package/dist/tools/format/health-format.d.ts +12 -6
  131. package/dist/tools/format/health-format.d.ts.map +1 -1
  132. package/dist/tools/format/health-format.js +14 -11
  133. package/dist/tools/format/health-format.js.map +1 -1
  134. package/dist/tools/format/pages-format.d.ts +7 -1
  135. package/dist/tools/format/pages-format.d.ts.map +1 -1
  136. package/dist/tools/format/pages-format.js +25 -1
  137. package/dist/tools/format/pages-format.js.map +1 -1
  138. package/dist/tools/health.d.ts +2 -2
  139. package/dist/tools/health.d.ts.map +1 -1
  140. package/dist/tools/health.js +120 -47
  141. package/dist/tools/health.js.map +1 -1
  142. package/dist/tools/index.d.ts +9 -2
  143. package/dist/tools/index.d.ts.map +1 -1
  144. package/dist/tools/index.js +21 -7
  145. package/dist/tools/index.js.map +1 -1
  146. package/dist/tools/inspection.d.ts +2 -2
  147. package/dist/tools/inspection.d.ts.map +1 -1
  148. package/dist/tools/inspection.js +67 -22
  149. package/dist/tools/inspection.js.map +1 -1
  150. package/dist/tools/multi-items/builders.d.ts +2 -2
  151. package/dist/tools/multi-items/builders.d.ts.map +1 -1
  152. package/dist/tools/multi-items/builders.js +21 -8
  153. package/dist/tools/multi-items/builders.js.map +1 -1
  154. package/dist/tools/pages/builders.d.ts +2 -2
  155. package/dist/tools/pages/builders.d.ts.map +1 -1
  156. package/dist/tools/pages/builders.js +36 -24
  157. package/dist/tools/pages/builders.js.map +1 -1
  158. package/dist/tools/pages/handlers-read.d.ts.map +1 -1
  159. package/dist/tools/pages/handlers-read.js +10 -0
  160. package/dist/tools/pages/handlers-read.js.map +1 -1
  161. package/dist/tools/pages/schemas.d.ts.map +1 -1
  162. package/dist/tools/pages/schemas.js +18 -0
  163. package/dist/tools/pages/schemas.js.map +1 -1
  164. package/dist/tools/pool-resolve-helper.d.ts +83 -0
  165. package/dist/tools/pool-resolve-helper.d.ts.map +1 -0
  166. package/dist/tools/pool-resolve-helper.js +148 -0
  167. package/dist/tools/pool-resolve-helper.js.map +1 -0
  168. package/dist/tools/shared-schemas.d.ts +10 -0
  169. package/dist/tools/shared-schemas.d.ts.map +1 -1
  170. package/dist/tools/shared-schemas.js +20 -2
  171. package/dist/tools/shared-schemas.js.map +1 -1
  172. package/dist/tools/sources/builders.d.ts +2 -2
  173. package/dist/tools/sources/builders.d.ts.map +1 -1
  174. package/dist/tools/sources/builders.js +27 -15
  175. package/dist/tools/sources/builders.js.map +1 -1
  176. package/dist/tools/tool-builder/index.d.ts +1 -1
  177. package/dist/tools/tool-builder/index.d.ts.map +1 -1
  178. package/dist/tools/tool-builder/index.js +1 -1
  179. package/dist/tools/tool-builder/index.js.map +1 -1
  180. package/dist/tools/tool-builder/results.d.ts +34 -0
  181. package/dist/tools/tool-builder/results.d.ts.map +1 -1
  182. package/dist/tools/tool-builder/results.js +86 -0
  183. package/dist/tools/tool-builder/results.js.map +1 -1
  184. package/manifest.json +27 -10
  185. package/package.json +3 -1
  186. package/skills/yt-builder-mcp/SKILL.md +846 -0
  187. package/skills/yootheme-builder/SKILL.md +0 -587
@@ -0,0 +1,846 @@
1
+ ---
2
+ name: yt-builder-mcp
3
+ description: Drive the YOOtheme Pro Page Builder on WordPress or Joomla 5/6 — discover pages, inspect layouts, add/move/clone/delete elements, bind dynamic sources, diagnose 401/403 auth failures. Use when the user works with a YOOtheme Pro site via the YT Builder MCP server.
4
+ ---
5
+
6
+ # YT Builder MCP for YOOtheme Pro (unofficial) — Skill
7
+
8
+ > Independent third-party project. YOOtheme® is a registered trademark of YOOtheme GmbH
9
+ > ([yootheme.com](https://yootheme.com)). YT Builder MCP is built by WootsUp (getimo
10
+ > productions) and is not affiliated with, endorsed by, or sponsored by YOOtheme.
11
+ > The integration uses YOOtheme Pro's public extension points and works on both
12
+ > WordPress and Joomla 5/6.
13
+
14
+ This skill helps AI assistants drive the YOOtheme Pro Page Builder through the
15
+ `@wootsup/yt-builder-mcp` server. The server exposes 26 typed, scoped, idempotent
16
+ tools. 20 of them advertised as first-class entries in `tools/list` (17 essential
17
+ L1 forwards + 2 direct L3 entries + 1 gateway), and 7 advanced tools reachable
18
+ through the single `yootheme_builder_advanced` gateway. This 3-lane split keeps
19
+ `tools/list` well below the ~40-tool Cursor cap while every catalogued tool stays
20
+ fully reachable.
21
+
22
+ ## How to use this MCP server
23
+
24
+ The user invokes you through Claude Desktop, Cursor, Zed, Continue, Cline,
25
+ Roo Code, Claude Code, Codex CLI or any other MCP-aware AI client. Setup is
26
+ **cross-platform** — the same MCP server speaks to the same WordPress *or*
27
+ Joomla 5/6 host plugin:
28
+
29
+ 1. The user installs the host plugin for their CMS:
30
+ - **WordPress** — install the `yt-builder-mcp` plugin (downloadable from the
31
+ [GitHub repository](https://github.com/wootsup/yt-builder-mcp)) and generate
32
+ a Bearer key in **wp-admin → Tools → "YT Builder MCP" → Bearer Keys**.
33
+ - **Joomla 5/6** — install the `yt-builder-mcp` package (downloadable from the
34
+ [GitHub repository](https://github.com/wootsup/yt-builder-mcp)) and generate
35
+ a Bearer key in **Components → YT Builder MCP → Bearer Keys**. The package
36
+ installs three sub-extensions (system plugin, webservices plugin, component).
37
+ 2. The user runs `npx -y @wootsup/yt-builder-mcp setup` once; the wizard probes
38
+ the host plugin, validates the key, and writes the MCP server entry into every
39
+ selected AI client's config file. (Wizard prompts include a platform hint —
40
+ `auto` works for most cases; set it to `joomla` explicitly when the site URL
41
+ has no `/joomla` segment.)
42
+ 3. The user restarts their AI client. The server is now visible.
43
+ 4. The user asks for a YOOtheme task (build, audit, change, diagnose).
44
+
45
+ ### Two picker entries — activate both
46
+
47
+ Some clients (notably Claude Desktop with the `.dxt` bundle) expose **two**
48
+ entries when the user types "YT Builder MCP" into the picker:
49
+
50
+ - **`YT Builder MCP for YOOtheme Pro (unofficial)`** — the MCP **server**.
51
+ Provides the 20 first-class tools (17 essential, 2 direct, 1 gateway).
52
+ The `yootheme_builder_advanced` gateway routes 7 additional tools.
53
+ - **`Von YT Builder MCP for YOOtheme Pro`** — the bundled **skill** (this
54
+ document). Gives the agent the workflow knowledge needed to drive those
55
+ tools correctly on first try.
56
+
57
+ **Activate both for the full experience.** The MCP server alone gives the agent
58
+ typed tools but no narrative guidance; the skill alone has no tools to call.
59
+
60
+ When the user asks a YOOtheme-related question, **always start with
61
+ `yootheme_builder_health`** — it confirms the host plugin is reachable and (when
62
+ the Bearer key is valid) returns the **plugin version, YOOtheme version, WordPress
63
+ or Joomla version, PHP version, and the site_url + home_url of the connected
64
+ site**. The site URL is how you know *which* site the agent is currently driving;
65
+ surface it back to the user when relevant ("Working on `https://example.com`...").
66
+
67
+ If a tool returns `401 Unauthorized` or `403 Forbidden`, jump straight to
68
+ **Workflow 4: Diagnose 401/auth failure**. Do not retry blindly.
69
+
70
+ ## Gateway routing (so you know what you can call)
71
+
72
+ The server exposes:
73
+
74
+ - **2 direct top-level tools** — always callable, always in `tools/list`:
75
+ `yootheme_builder_health` and `yootheme_builder_diagnose`. These are
76
+ the "the gateway itself might be broken" escape hatch.
77
+ - **17 essential forwarded tools** — common reads + the most-used writes
78
+ (pages_list, get_etag, element_list / add / update_settings / get / move /
79
+ clone / delete, page_get_layout, sources_list, element_types_list,
80
+ element_type_get_schema, template_summary, inspect_multi_items_binding,
81
+ sites_list, sites_test). Always advertised in `tools/list` so AI clients
82
+ see them first-class.
83
+ - **7 advanced captured tools** — everything else (page_save, page_publish,
84
+ page_get_schema, element_get_binding, element_bind_source,
85
+ element_unbind_source, clean_implode_directives). Reachable through one
86
+ gateway tool: `yootheme_builder_advanced({ tool: "<name>", input: { ... } })`.
87
+ - **1 gateway tool** — `yootheme_builder_advanced`.
88
+
89
+ `tools/list` therefore advertises 20 names (17 + 2 + 1) for a total catalogue
90
+ of 26. If the AI client reports "tool not found", you are almost certainly
91
+ calling an advanced tool by its raw name. Wrap it in
92
+ `yootheme_builder_advanced({ tool, input })` instead.
93
+
94
+ ## Site and frontend URLs (for deep-linking and verification)
95
+
96
+ The server surfaces the connected site's URLs in two places so you never have to
97
+ guess where the agent is pointing:
98
+
99
+ - **`yootheme_builder_health` (Bearer-authenticated) and
100
+ `yootheme_builder_diagnose`** return `site_url` and `home_url` for the
101
+ connected install. Call one of them when the user asks "which site are you
102
+ on?" or before deep-linking the user back into wp-admin / Joomla administrator.
103
+ - **`yootheme_builder_pages_list`** returns per-template `frontend_url`,
104
+ `frontend_url_template`, and `frontend_url_description` columns when the
105
+ host plugin can resolve them. Use these when the user asks for a verification
106
+ URL ("show me the 404 page", "give me the front-end URL of the homepage
107
+ template"): find the matching row, return `frontend_url` (resolved) or
108
+ `frontend_url_template` (with placeholders the user fills in).
109
+
110
+ Treat `frontend_url: null` as "host plugin could not resolve a public URL for
111
+ this template" — surface that honestly rather than fabricating one.
112
+
113
+ ## Scopes (Bearer key permissions)
114
+
115
+ Every Bearer key has a scope, set at key creation time:
116
+
117
+ | Scope | Reads | Writes | Destructive |
118
+ |----------|-------|--------|-------------|
119
+ | `read` | ✓ | ✗ | ✗ |
120
+ | `write` | ✓ | ✓ | ✗ |
121
+ | `admin` | ✓ | ✓ | ✓ |
122
+
123
+ When a tool returns `{ error: 'insufficient_scope', context: { required: 'write', actual: 'read' } }`,
124
+ ask the user to regenerate the key with a higher scope **before** retrying.
125
+ Do not loop on auth errors.
126
+
127
+ > **Joomla note.** On the Joomla API surface (`com_api`), the Bearer token's
128
+ > scope is the **sole** authority — the L2 article-write `core.edit` ACL gate
129
+ > was intentionally removed (see ADR at https://github.com/wootsup/yt-builder-mcp/blob/main/docs/adr/2026-05-24-l2-bearer-as-authority.md).
130
+ > Joomla ACL still governs the admin component (`com_ytbmcp`). On WordPress,
131
+ > capabilities like `manage_options` gate the admin settings page only; the
132
+ > REST API surface is Bearer-gated.
133
+
134
+ ---
135
+
136
+ ## Working with multiple sites
137
+
138
+ One MCP install can drive many YOOtheme Pro sites. You configure each site
139
+ once (URL + Bearer key + platform). The agent then targets a specific site
140
+ per tool-call via a `site_id` parameter, or falls back to the default site
141
+ when `site_id` is omitted.
142
+
143
+ ### When and why
144
+
145
+ The typical case is an agency or freelancer running 5, 20, or 100+ YOOtheme
146
+ sites (WordPress and Joomla mixed). Without multi-site support you would need
147
+ one MCP install per site, one set of env vars per site, and one AI-client
148
+ restart per site you want to talk to. With multi-site:
149
+
150
+ - One DXT install in Claude Desktop, one entry in your AI client config.
151
+ - One conversation can edit elements on `acme.com` and `beta.io` back-to-back.
152
+ - Each site keeps its own Bearer key, platform, label, and 1Password reference.
153
+ - Adding a new client site does not require a new MCP install.
154
+
155
+ ### How `site_id` works
156
+
157
+ Every tool accepts an optional `site_id` parameter.
158
+
159
+ - **Omit `site_id`**: the tool runs against the **default site**. This is the
160
+ common case for single-site users (the registry has one site and it is the
161
+ default) and for agency users who picked a "main" site for the session.
162
+ - **Pass `site_id: "wp-acme"`**: the tool runs against that specific site,
163
+ overriding the default for that one call.
164
+
165
+ When the agent does not know which sites are available, it calls
166
+ `yootheme_builder_sites_list` first. The response lists every configured site
167
+ with its `site_id`, URL, platform, default flag, and bearer source (so the
168
+ agent can choose by label and the user can verify by URL).
169
+
170
+ To verify a specific site before doing work on it, call
171
+ `yootheme_builder_sites_test({ site_id: "wp-acme" })`. This probes `/health`
172
+ and `/etag` in parallel and returns `plugin_reachable` + `bearer_valid`
173
+ without mutating anything.
174
+
175
+ ### Default-site mechanics
176
+
177
+ The default site is set automatically on first add:
178
+
179
+ - **First site you add**: becomes the default automatically. No flag needed.
180
+ - **Every subsequent site**: NOT default by default. Use `--default` on
181
+ `add-site` to make it the new default (the old default is demoted).
182
+ - **You remove the current default**: the next site in the registry order
183
+ is promoted to default. The registry is never left without a default
184
+ while ≥1 site exists.
185
+
186
+ ### Plain bearer vs 1Password reference
187
+
188
+ You can store the Bearer key two ways per site:
189
+
190
+ - **Plain field**: `bearer: "ytb_live_..."` in `sites.json`. Easy for dev or
191
+ onboarding, but the secret lives on disk in plaintext.
192
+ - **1Password reference**: `bearer_ref: "op://Vault/Item/credential"` in
193
+ `sites.json`. The plaintext token never touches disk. The MCP server shells
194
+ out to the `op` CLI at first use per site to fetch the live token, then
195
+ caches it in memory for that process lifetime.
196
+
197
+ **Recommendation for production sites**: use `bearer_ref`. You get rotation
198
+ without editing `sites.json`, and your 1Password audit log captures every
199
+ fetch. The `op` CLI must be installed and signed in on the machine running
200
+ the MCP server. If `op` is missing, the resolver returns a structured error
201
+ (`op CLI not found in PATH`) pointing at the install docs.
202
+
203
+ ### `sites.json` location
204
+
205
+ The registry lives at `~/.config/yt-builder-mcp/sites.json` (XDG-conform).
206
+ If `XDG_CONFIG_HOME` is set, the file lives at
207
+ `$XDG_CONFIG_HOME/yt-builder-mcp/sites.json` instead. The file is created
208
+ with mode `0600` so only the current user can read it.
209
+
210
+ The CLI subcommands below are the supported way to edit `sites.json`. Direct
211
+ edits work but skip the schema-validation and atomic-write paths.
212
+
213
+ ### CLI subcommands
214
+
215
+ Run these via `npx -y @wootsup/yt-builder-mcp <subcommand>`:
216
+
217
+ - **`setup`**: interactive wizard for the first site. Probes the host
218
+ plugin, validates the Bearer key, picks the platform, writes `sites.json`,
219
+ and writes the MCP server entry into every selected AI-client config.
220
+ - **`add-site [--url <url>] [--token <bearer> | --token-ref op://...] [--platform auto|wordpress|joomla] [--label "..."] [--default] [--site-id <slug>] [--yes]`**:
221
+ add a new site. Flags can be passed for non-interactive use; missing flags
222
+ trigger prompts. `--default` makes the new site the default (demoting the
223
+ old one). `--yes` skips the confirmation prompt.
224
+ - **`list-sites`**: print every configured site as a table (site_id, URL,
225
+ platform, default flag, bearer source).
226
+ - **`remove-site <site_id> [--yes]`**: delete a site from the registry. If
227
+ the removed site was the default, the next site in registry order is
228
+ auto-promoted. `--yes` skips the confirmation prompt.
229
+ - **`set-default <site_id>`**: switch the default site to `<site_id>`. The
230
+ previous default is demoted.
231
+ - **`test-site <site_id>`**: pre-flight probe (`/health` + `/etag`) for one
232
+ site. Returns `plugin_reachable` + `bearer_valid` and exits non-zero on
233
+ failure. Mutates nothing.
234
+
235
+ ### Restart your AI client after registry changes
236
+
237
+ After `add-site`, `remove-site`, `set-default`, or any direct edit to
238
+ `sites.json`, **restart Claude Desktop** (or your AI client). The MCP
239
+ protocol sends the `instructions` block (which carries the "Currently
240
+ configured sites" appendix) once at `initialize`. The agent will not see new
241
+ sites in its instructions until the next `initialize` cycle.
242
+
243
+ `sites_list` and the per-call `site_id` parameter both keep working
244
+ without a restart (they read the live registry on every call), but the
245
+ agent's narrative awareness of which sites exist lags by one restart.
246
+
247
+ The CLI prints a reminder line after every mutation so you do not forget.
248
+
249
+ ### Site-awareness in every response
250
+
251
+ Every tool reply carries the connected site in two places:
252
+
253
+ - **Text prefix**: every text response starts with `[<label> @ <host>]` so
254
+ the customer can see at a glance which site produced the answer
255
+ (`[ACME Production @ acme.com] 12 templates ...`).
256
+ - **Structured metadata**: `structuredContent._meta.site_id`,
257
+ `structuredContent._meta.site_url`, and `structuredContent._meta.platform`
258
+ carry the same info in a machine-readable shape. Agents can use this for
259
+ routing, logging, or follow-up calls.
260
+
261
+ If the AI-client UI hides `_meta`, the text prefix still tells the user
262
+ which site they are looking at.
263
+
264
+ ### End-to-end example: bulk element update across 5 agency sites
265
+
266
+ Goal: change a hero headline on the `home` template of 5 client sites in
267
+ one conversation.
268
+
269
+ 1. The agent calls `yootheme_builder_sites_list()`. It learns the 5
270
+ site_ids: `wp-acme`, `wp-beta`, `joomla-gamma`, `wp-delta`,
271
+ `joomla-epsilon`.
272
+ 2. For each site, the agent runs the same sequence with `site_id` set:
273
+ - `yootheme_builder_pages_list({ site_id: "wp-acme", fields: ["id", "label"] })`
274
+ - `yootheme_builder_get_etag({ site_id: "wp-acme" })`
275
+ - `yootheme_builder_element_update_settings({ site_id: "wp-acme", template_id: "home", element_path: "/0/children/0/children/0/children/0", props: { content: "New headline" }, merge: true, etag: "<etag>" })`
276
+ - `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { site_id: "wp-acme", template_id: "home", etag: "<fresh>" } })`
277
+ - `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { site_id: "wp-acme", template_id: "home", etag: "<fresh>" } })`
278
+ 3. The customer sees a stream of replies, each prefixed with the matching
279
+ `[label @ host]`, so it is obvious which site is at which step.
280
+
281
+ If one site fails (auth error, plugin not active, network blip), the agent
282
+ isolates the failure to that one `site_id` and continues with the rest.
283
+ Run `yootheme_builder_sites_test({ site_id: "<id>" })` on the failing site
284
+ for a focused diagnosis without touching the others.
285
+
286
+ ---
287
+
288
+ ## Workflow 1: Build a hero section
289
+
290
+ **Goal:** Add a fresh hero section (heading + sub-heading + CTA button)
291
+ to an existing page.
292
+
293
+ **Canonical tool-call sequence (real parameter names — snake_case):**
294
+
295
+ 1. `yootheme_builder_health` — confirm host plugin reachable; note plugin
296
+ version and `site_url` (some element types are version-gated; surface
297
+ the site URL back to the user).
298
+ 2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` — find
299
+ the target template. Returns `[{ id, label, ... }]`. If the user
300
+ named a specific page, match on `label` (exact then fuzzy).
301
+ 3. `yootheme_builder_get_etag()` — fetch the current top-level
302
+ optimistic-lock ETag. Every write tool requires it via `etag`.
303
+ 4. `yootheme_builder_element_add({ template_id: "<id>", parent_path: "", element_type: "section", props: { background: "primary" }, etag: "<etag>" })`
304
+ — append a new section at the template root (`parent_path: ""`).
305
+ Returns `{ path: "/0/children/N", etag: "<fresh>" }`.
306
+ 5. `yootheme_builder_element_add({ template_id, parent_path: "<section-path>", element_type: "row", etag: "<fresh-etag>" })`
307
+ — add a row inside the section. Use the etag returned by the
308
+ previous write — etags rotate every mutation.
309
+ 6. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "headline", props: { content: "<h1 text>" }, etag })`
310
+ — add a headline.
311
+ 7. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "text", props: { content: "<sub text>" }, etag })`
312
+ — add a text element.
313
+ 8. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "button", props: { content: "<cta>", link: "<url>" }, etag })`
314
+ — add the CTA button.
315
+ 9. `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { template_id, etag } })`
316
+ — persist the working copy (visible in YOOtheme Customizer preview).
317
+ `page_save` is an advanced tool; call it through the gateway.
318
+ 10. `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { template_id, etag } })`
319
+ — make the changes live on the front-end. Also an advanced tool.
320
+
321
+ **Common pitfalls:**
322
+
323
+ - **Wrong parameter names.** Every tool uses snake_case. Use
324
+ `template_id` (not `pageId`), `parent_path` (not `parentPath`),
325
+ `element_type` (not `type`), `props` (not `settings`), `etag`
326
+ (not `ifMatch`). The MCP server rejects unknown keys with a
327
+ Zod-validation error.
328
+ - **Forgetting `etag`.** Every write tool needs the latest etag. The
329
+ shared schema marks it required (min length 1). On `412 Precondition
330
+ Failed` re-fetch via `yootheme_builder_get_etag` and retry.
331
+ - **Adding non-row elements directly to a section.** Sections expect a
332
+ row in between. The server returns a structured error with a
333
+ human-readable hint when you skip the row.
334
+ - **Saving without publishing.** `page_save` is the equivalent of the
335
+ YOOtheme Customizer "Save" button — content lives in the staging
336
+ copy. Visitors see nothing until `page_publish`.
337
+ - **Reusing a stale etag across many writes.** Every write returns a
338
+ fresh etag in the response. Pass THAT etag into the next write —
339
+ don't hold the one from the original `get_etag` call.
340
+ - **Calling page_save / page_publish by name.** Both are advanced
341
+ (L2) tools — call them through `yootheme_builder_advanced({ tool, input })`,
342
+ not directly. The first sign you forgot is "tool not found".
343
+
344
+ **Worked example (tool-call snippet):**
345
+
346
+ ```jsonc
347
+ // Step 4 — add the section. parent_path: "" means template root.
348
+ yootheme_builder_element_add({
349
+ template_id: "home",
350
+ parent_path: "",
351
+ element_type: "section",
352
+ props: { background: "primary" },
353
+ etag: "abc123" // from yootheme_builder_get_etag
354
+ })
355
+ // Response: { path: "/0/children/3", etag: "def456" }
356
+ // → next call uses etag "def456"
357
+
358
+ // Step 9 — page_save is L2; call via the gateway.
359
+ yootheme_builder_advanced({
360
+ tool: "yootheme_builder_page_save",
361
+ input: { template_id: "home", etag: "<latest>" }
362
+ })
363
+ ```
364
+
365
+ **Edge case:** YOOtheme allows nested sections (rare) — if the user
366
+ asks for a "card grid inside a hero", you still need the
367
+ `section → row → column → grid` hierarchy, even when the parent
368
+ section sits inside another section.
369
+
370
+ **Success criterion:** After `page_publish`, navigating to the page
371
+ URL on the front-end shows the new hero section above the previous
372
+ content. Re-reading the layout via
373
+ `yootheme_builder_page_get_layout({ template_id })` shows the new
374
+ section as the last child of the template root.
375
+
376
+ ---
377
+
378
+ ## Workflow 2: Bind a dynamic source to a grid
379
+
380
+ **Goal:** Wire an existing Grid (or other multi-item element) to a
381
+ Source from API Mapper or the built-in YOOtheme Sources system so it
382
+ renders dynamic items.
383
+
384
+ **Canonical tool-call sequence (real parameter names — snake_case):**
385
+
386
+ 1. `yootheme_builder_health` — confirm host plugin reachable.
387
+ 2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
388
+ `yootheme_builder_page_get_layout({ template_id: "<id>", flat: false })`
389
+ — locate the target Grid. Note its JSON-Pointer `path` (e.g.
390
+ `/0/children/2/children/0`).
391
+ 3. `yootheme_builder_element_get({ template_id, element_path })` —
392
+ fetch the Grid's current props so you can preserve them; binding
393
+ sets `props.source` and leaves the rest alone.
394
+ 4. `yootheme_builder_sources_list()` — enumerate available Sources.
395
+ Each returns `{ name, label, origin, kind }`. Pick the one the
396
+ user asked for.
397
+ 5. `yootheme_builder_advanced({ tool: "yootheme_builder_element_get_binding", input: { template_id, element_path } })`
398
+ — check whether the Grid is already bound (idempotency: skip step
399
+ 7 if `source_name` already matches the user's intent).
400
+ `element_get_binding` is an advanced (L2) tool — call via the gateway.
401
+ 6. `yootheme_builder_get_etag()` — fetch the optimistic-lock etag for
402
+ the upcoming mutation.
403
+ 7. `yootheme_builder_advanced({ tool: "yootheme_builder_element_bind_source", input: { template_id, element_path, source_name: "<name>", etag: "<etag>" } })`
404
+ — apply the binding. Returns `{ path, etag, has_binding: true }`.
405
+ Pass `source_id: "<origin>:<name>"` as well **only** when two
406
+ plugins register a source with the same `source_name` (the server
407
+ surfaces the ambiguity as an elicitation prompt; if the host
408
+ doesn't support elicitation you'll see a structured error listing
409
+ the candidates). Also an advanced tool.
410
+ 8. `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { template_id, etag: "<fresh>" } })`
411
+ then `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { template_id, etag: "<fresh>" } })`.
412
+
413
+ **Common pitfalls:**
414
+
415
+ - **Inventing `fieldMap`.** The bind tool's schema is just
416
+ `template_id`, `element_path`, `source_name`, optional `source_id`,
417
+ `etag`. Field mapping happens inside YOOtheme at render time based
418
+ on the element's own field bindings — not via an MCP parameter.
419
+ - **Wrong parameter names.** Use `template_id` (not `pageId`),
420
+ `element_path` (not `path`), `source_name` (not `sourceName`),
421
+ `etag` (not `ifMatch`).
422
+ - **Source not in the list.** API Mapper sources only appear once
423
+ they're PUBLISHED in API Mapper (not just saved). If
424
+ `yootheme_builder_sources_list` returns no match for the name the
425
+ user typed, send the user to API Mapper to publish it.
426
+ - **Binding non-list elements.** Only multi-item element types (Grid,
427
+ List, Switcher, Slider, Slideshow, Carousel, Map) accept a source
428
+ binding. Binding a single-item element like Headline returns a
429
+ structured `validation` error.
430
+ - **Forgetting `etag`.** Every write requires the optimistic-lock
431
+ etag. On `412 Precondition Failed` re-fetch via
432
+ `yootheme_builder_get_etag` and retry.
433
+ - **Calling bind/unbind directly.** Both `element_bind_source` and
434
+ `element_get_binding` are L2 advanced — wrap in
435
+ `yootheme_builder_advanced({ tool, input })`.
436
+
437
+ **Worked example (tool-call snippet):**
438
+
439
+ ```jsonc
440
+ // Step 7 — bind a Posts source onto a Grid element via the gateway.
441
+ yootheme_builder_advanced({
442
+ tool: "yootheme_builder_element_bind_source",
443
+ input: {
444
+ template_id: "home",
445
+ element_path: "/0/children/2/children/0",
446
+ source_name: "wp_posts",
447
+ etag: "abc123"
448
+ // source_id: "wordpress:wp_posts" // pass ONLY when name collides
449
+ }
450
+ })
451
+ // Response: { path: "/0/children/2/children/0", etag: "def456", has_binding: true }
452
+ // Verify (via gateway):
453
+ yootheme_builder_advanced({
454
+ tool: "yootheme_builder_element_get_binding",
455
+ input: { template_id: "home", element_path: "/0/children/2/children/0" }
456
+ })
457
+ // → { source_name: "wp_posts", source_config: { ... }, ... }
458
+ ```
459
+
460
+ **Edge case:** A Source can render zero items at runtime (e.g. empty
461
+ search filter). The bind call still succeeds; the front-end Grid just
462
+ shows the YOOtheme "no items" placeholder. Don't treat empty render
463
+ as a binding failure — verify by re-reading
464
+ `yootheme_builder_element_get_binding` through the gateway.
465
+
466
+ **Success criterion:** After publish, the Grid on the front-end shows
467
+ items from the Source (verify by item count and at least one
468
+ field-value spot-check). `yootheme_builder_element_get_binding`
469
+ (via gateway) returns the new `source_name`.
470
+
471
+ ---
472
+
473
+ ## Workflow 3: Clone & modify a section within a template
474
+
475
+ **Goal:** Duplicate a section inside the SAME template and tweak the
476
+ copy. Common variants: A/B-style hero, repeated CTA blocks, mirroring
477
+ a row layout. (Cross-template duplication is **not** supported by
478
+ `element_clone` — see "Important scope note" below.)
479
+
480
+ **Important scope note:** `yootheme_builder_element_clone` is
481
+ **sibling-only and intra-template**. Its real schema is
482
+ `{ template_id, element_path, etag }` — there is **no** `destPageId`
483
+ or `destParentPath`. The cloned element lands at the same parent,
484
+ right after the source. To move the clone elsewhere in the SAME
485
+ template, call `yootheme_builder_element_move` afterwards. To
486
+ duplicate into a DIFFERENT template, flag to the user that
487
+ cross-template clone is not currently supported and suggest a
488
+ CMS-level template duplication (in wp-admin or Joomla administrator).
489
+
490
+ **Canonical tool-call sequence (real parameter names — snake_case):**
491
+
492
+ 1. `yootheme_builder_health` — confirm host plugin reachable.
493
+ 2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` —
494
+ locate the template by `label`. Note its `id`.
495
+ 3. `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", input: { template_id } })`
496
+ — flat schema view (lighter than `page_get_layout`) showing every
497
+ element path + type. Pick the JSON-Pointer path of the section
498
+ to clone. `page_get_schema` is L2; call via the gateway.
499
+ 4. `yootheme_builder_get_etag()` — fetch the optimistic-lock etag.
500
+ 5. `yootheme_builder_element_clone({ template_id, element_path: "<src-path>", etag: "<etag>" })`
501
+ — clone as sibling. Returns `{ path: "<new-path>", etag: "<fresh>" }`.
502
+ The new path is at the same parent, immediately after the source.
503
+ 6. (Optional) `yootheme_builder_element_move({ template_id, element_path: "<new-path>", to_parent_path: "<other-parent>", to_index: 0, etag: "<fresh>" })`
504
+ — re-parent the clone within the same template if needed.
505
+ 7. `yootheme_builder_element_update_settings({ template_id, element_path: "<final-path>", props: { ... }, etag: "<fresh>" })`
506
+ — replace the `props` on the clone. **Existing props NOT in the
507
+ request are removed** (update_settings is a full replace by default;
508
+ pass `merge: true` to apply a server-side deep-merge instead).
509
+ Read the current props first via `yootheme_builder_element_get`
510
+ if you only want to tweak a subset.
511
+ 8. `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { template_id, etag } })`
512
+ then `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { template_id, etag } })`.
513
+
514
+ **Common pitfalls:**
515
+
516
+ - **Inventing destination parameters.** `element_clone` does NOT
517
+ accept `destPageId`, `destParentPath`, or any cross-template
518
+ argument. It's sibling-only within ONE template.
519
+ - **Treating `element_update_settings` as a merge by default.** The handler
520
+ REPLACES the entire `props` object on the element unless you pass
521
+ `merge: true`. Read the existing shape via `yootheme_builder_element_get`
522
+ first if you only want to tweak a subset and prefer not to use merge.
523
+ - **Clone-then-update path drift.** The clone returns a path that's
524
+ correct at the moment of the call. If you fire off many ops in
525
+ parallel, a concurrent edit may shift indices — refresh via
526
+ `get_etag` + `page_get_schema` (via gateway) between independent batches.
527
+ - **Cloning a bound element keeps the binding.** `element_clone`
528
+ copies the entire element including `props.source`. If the user
529
+ wanted a "data-free" copy, call
530
+ `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", input: { ... } })`
531
+ on the new path afterwards.
532
+ - **Wrong parameter names.** Use `template_id`, `element_path`,
533
+ `etag` (NOT `pageId`, `srcPath`, `ifMatch`).
534
+
535
+ **Worked example (tool-call snippet):**
536
+
537
+ ```jsonc
538
+ // Step 5 — clone the section element as a sibling.
539
+ yootheme_builder_element_clone({
540
+ template_id: "home",
541
+ element_path: "/0/children/2", // the hero section to duplicate
542
+ etag: "abc123"
543
+ })
544
+ // Response: { path: "/0/children/3", etag: "def456" }
545
+
546
+ // Step 7 — tweak the clone (replace props entirely, or pass merge: true).
547
+ const current = yootheme_builder_element_get({
548
+ template_id: "home",
549
+ element_path: "/0/children/3",
550
+ });
551
+ yootheme_builder_element_update_settings({
552
+ template_id: "home",
553
+ element_path: "/0/children/3",
554
+ props: { ...current.props, background: "secondary" },
555
+ etag: "def456"
556
+ })
557
+ ```
558
+
559
+ **Edge case:** When cloning a Grid with a source binding, the binding
560
+ is preserved (same `source_name`). If the user wants a "data-free"
561
+ copy, follow up with
562
+ `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", input: { ... } })`
563
+ on the new path. Verify with the gateway `element_get_binding` call.
564
+
565
+ **Success criterion:** After publish,
566
+ `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", input: { template_id } })`
567
+ shows the new section at the cloned path with the user's tweaks reflected in
568
+ `element_get` on that path.
569
+
570
+ ---
571
+
572
+ ## Workflow 4: Diagnose a 401 / 403 / auth failure
573
+
574
+ **Goal:** Recover from `401 Unauthorized` or `403 Forbidden` without
575
+ guessing — and without rotating the user's key unnecessarily.
576
+
577
+ **Canonical tool-call sequence:**
578
+
579
+ 1. `yootheme_builder_diagnose` — single probe that hits `/health` (no
580
+ auth) and then `/etag` (Bearer auth). Returns
581
+ `{ plugin_reachable, plugin_version, yootheme_loaded, yootheme_version,
582
+ endpoint_count, bearer_valid, bearer_error?, site_url?, home_url?,
583
+ summary? }`. Call this **before** any other tool when you see
584
+ auth errors. (Takes no arguments — the schema is `{}`.)
585
+ 2. **Interpret the result:**
586
+ - `plugin_reachable: false` → the WordPress / Joomla install is down
587
+ OR the host plugin is deactivated. Send the user to **wp-admin →
588
+ Plugins → activate "YT Builder MCP"** (WordPress) or **Joomla
589
+ administrator → Extensions → Plugins → enable "System - YT Builder
590
+ MCP" and the matching webservices + component entries** (Joomla).
591
+ Do not retry until they confirm.
592
+ - `plugin_reachable: true, bearer_valid: false` → the Bearer key is
593
+ wrong (typo, revoked, or wrong key for this install). The
594
+ `bearer_error` field carries the upstream HTTP status. Send the
595
+ user to:
596
+ - **WordPress:** wp-admin → Tools → "YT Builder MCP" → Bearer Keys
597
+ → copy the existing key into their MCP client config, or
598
+ generate a new one.
599
+ - **Joomla:** Components → YT Builder MCP → Bearer Keys → same.
600
+ - `plugin_reachable: true, bearer_valid: true` but the original
601
+ tool returned a 403 → the key works but the scope is too low for
602
+ the tool's required scope (`write` for mutations, `admin` for
603
+ destructive operations). Ask the user to regenerate the key with
604
+ a higher scope and restart the AI client.
605
+ 3. **Walk the user through key rotation if needed:**
606
+ - WordPress: "wp-admin → Tools → YT Builder MCP → Bearer Keys."
607
+ Joomla: "Components → YT Builder MCP → Bearer Keys."
608
+ - "Click 'Generate New Key', pick the scope (admin for full access)."
609
+ - "Copy the key — it's shown ONCE; you cannot recover it later."
610
+ - "Update your AI client config: replace `YTB_MCP_BEARER_TOKEN`
611
+ with the new key. The fastest way is to re-run
612
+ `npx -y @wootsup/yt-builder-mcp setup`."
613
+ - "Restart Claude / Cursor / Zed / Continue / Cline / Roo Code /
614
+ Claude Code / Codex CLI."
615
+ - "Confirm with `yootheme_builder_diagnose` that
616
+ `bearer_valid: true` before retrying the original task."
617
+
618
+ **Common pitfalls:**
619
+
620
+ - **Treating 401 as a network error.** A network error has no HTTP
621
+ status — it's a TCP/TLS / DNS failure. 401 means the server
622
+ responded "I do not accept this key", which is fundamentally a
623
+ config problem.
624
+ - **Stripping the `Bearer ` prefix.** The MCP server adds it
625
+ automatically when reading `YTB_MCP_BEARER_TOKEN`. If the user
626
+ pasted the prefix into the env var, the value sent will be
627
+ `Bearer Bearer ytb_live_…` and every request 401s.
628
+ - **Trying every tool to "see which ones work".** Don't. One
629
+ `yootheme_builder_diagnose` call tells you whether the failure is
630
+ reachability, auth, or scope.
631
+ - **Confusing 401 with 403.** 401 = "I don't recognise this key"
632
+ (rotate). 403 = "I recognise the key but it lacks the required
633
+ scope" (regenerate with higher scope). Different error codes,
634
+ different recovery — never collapse them into one branch.
635
+
636
+ **Worked example (tool-call snippet):**
637
+
638
+ ```jsonc
639
+ // First — never retry blindly. Call diagnose (no args).
640
+ yootheme_builder_diagnose({})
641
+ // Response shape:
642
+ // {
643
+ // plugin_reachable: true,
644
+ // plugin_version: "1.1.0",
645
+ // yootheme_loaded: true,
646
+ // yootheme_version: "5.0.22",
647
+ // endpoint_count: 16,
648
+ // bearer_valid: false, // ← key is bad
649
+ // bearer_error: "HTTP 401: invalid_token",
650
+ // site_url: "https://example.com",
651
+ // home_url: "https://example.com"
652
+ // }
653
+ // → diagnosis: rotate the key. Send user to Tools/Components → YT Builder MCP.
654
+ ```
655
+
656
+ **Edge case:** `plugin_reachable: true` but `yootheme_loaded: false`
657
+ — the user installed the MCP host plugin but YOOtheme Pro itself
658
+ isn't active. The MCP server still answers, but every tool that
659
+ touches the YOOtheme layout returns an empty/error response. Surface
660
+ the mismatch ("YOOtheme Pro is not active on this install") instead
661
+ of retrying. On Joomla this can also surface as a "YOOtheme Pro
662
+ required" admin notice in the component dashboard.
663
+
664
+ **Success criterion:** A subsequent `yootheme_builder_diagnose`
665
+ returns `plugin_reachable: true` AND `bearer_valid: true`. The
666
+ original tool now returns a non-auth response.
667
+
668
+ ---
669
+
670
+ ## Workflow 5: Add a custom element type to a page
671
+
672
+ **Goal:** Inspect what element types are installed on the user's
673
+ YOOtheme install (built-ins + YOOtheme Pro + YOOessentials + child
674
+ theme + plugin-contributed elements), pick the right one, and place
675
+ an instance with a sensible default props payload.
676
+
677
+ **Canonical tool-call sequence (real parameter names — snake_case):**
678
+
679
+ 1. `yootheme_builder_health` — note the YOOtheme version; custom
680
+ elements often require a minimum YOOtheme major.
681
+ 2. `yootheme_builder_element_types_list({ fields: ["name", "label", "origin"] })`
682
+ — narrow the catalogue with sparse-fields. Returns rows like
683
+ `{ name: "headline", label: "Headline", origin: "core", ... }`.
684
+ 3. `yootheme_builder_element_type_get_schema({ type_name: "<picked>" })`
685
+ — fetch the prop schema for the chosen type. Returns the field
686
+ definitions you can pass via `props`. Note: the parameter is
687
+ `type_name`, not `name`.
688
+ 4. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
689
+ `yootheme_builder_page_get_layout({ template_id, flat: false })`
690
+ — locate the `parent_path` (JSON-Pointer) where the new element
691
+ should land.
692
+ 5. `yootheme_builder_get_etag()` — fetch the optimistic-lock etag.
693
+ 6. `yootheme_builder_element_add({ template_id, parent_path: "<path>", element_type: "<picked-name>", props: { ... }, etag })`
694
+ — the server validates `props` against the type schema and
695
+ returns a structured `validation` error with a per-field issue
696
+ list if anything is missing or malformed.
697
+ 7. (Optional) `yootheme_builder_element_update_settings({ template_id, element_path: "<new-path>", props: { ... }, etag })`
698
+ — iterate on the props. **Note: this REPLACES `props` entirely by
699
+ default; pass `merge: true` for a server-side deep-merge.** When
700
+ replacing, include every key you want to keep.
701
+ 8. `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { template_id, etag } })`
702
+ then `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { template_id, etag } })`.
703
+
704
+ **Common pitfalls:**
705
+
706
+ - **Wrong parameter name on the type-schema tool.** It's
707
+ `type_name`, not `name`. The server's Zod schema rejects unknown
708
+ keys.
709
+ - **Wrong parameter names on `element_add`.** Use `template_id`
710
+ (not `pageId`), `parent_path` (not `parentPath`), `element_type`
711
+ (not `type` / `name`), `props` (not `settings`), `etag` (not
712
+ `ifMatch`).
713
+ - **`element_update_settings` is a full replace by default.** Any
714
+ key NOT in the request is REMOVED from `props` unless you set
715
+ `merge: true`. Read the existing shape via
716
+ `yootheme_builder_element_get` first if you only want to tweak a
717
+ subset.
718
+ - **Custom elements without a schema.** A poorly-built third-party
719
+ element may not register a prop schema. In that case
720
+ `yootheme_builder_element_type_get_schema` returns an empty/sparse
721
+ schema and the server accepts arbitrary `props`. Don't assume "no
722
+ schema = no required fields" — read the third-party element's
723
+ docs.
724
+ - **Type name vs. label confusion.** The `name` field on the
725
+ catalogue row is the machine identifier (e.g. `pro_slider`); the
726
+ `label` is the human display string ("Pro Slider"). Always pass
727
+ the `name` (as `element_type` / `type_name`).
728
+ - **Pro-only types on a Free install.** YOOtheme Free does not
729
+ register `pro_*` element types. Surface that to the user instead
730
+ of retrying with a different filter.
731
+
732
+ **Worked example (tool-call snippet):**
733
+
734
+ ```jsonc
735
+ // Step 2 — narrow the catalogue with sparse-fields to save tokens.
736
+ yootheme_builder_element_types_list({
737
+ fields: ["name", "label", "origin"]
738
+ })
739
+ // Rows: [{ name: "headline", label: "Headline", origin: "core" }, ...]
740
+
741
+ // Step 3 — fetch the schema (note: type_name, not name).
742
+ yootheme_builder_element_type_get_schema({ type_name: "headline" })
743
+ // Returns the field definitions for the headline's `props`.
744
+
745
+ // Step 6 — place the element.
746
+ yootheme_builder_element_add({
747
+ template_id: "home",
748
+ parent_path: "/0/children/2", // row inside section
749
+ element_type: "headline",
750
+ props: { content: "Welcome", tag: "h1" },
751
+ etag: "abc123"
752
+ })
753
+ // Response: { path: "/0/children/2/children/0", etag: "def456" }
754
+ ```
755
+
756
+ **Edge case:** A child theme can override a built-in element's
757
+ schema in PHP. The `origin` field will read `child_theme` instead
758
+ of `core`. If you see surprising required keys, that's the override
759
+ talking — surface this to the user so they know their theme is
760
+ customising element defaults.
761
+
762
+ **Success criterion:** After publish, the front-end shows the new
763
+ element rendered with its default props. A re-read of the layout
764
+ shows the element under `parent_path` with the chosen `element_type`
765
+ and the props payload you passed.
766
+
767
+ ---
768
+
769
+ ## When something doesn't fit one of these 5 workflows
770
+
771
+ - **Move an element** (intra-template reorder/reparent): use
772
+ `yootheme_builder_element_move({ template_id, element_path,
773
+ to_parent_path, to_index, etag })`. Reorders or reparents without
774
+ re-creating.
775
+ - **Delete an element**: use `yootheme_builder_element_delete({
776
+ template_id, element_path, etag, confirm: true })`.
777
+ Elicitation-aware — confirms via the AI client prompt before
778
+ destroying state when `confirm` is omitted. On hosts without
779
+ elicitation, it returns a preview-with-confirm-required response;
780
+ call again with `confirm: true`.
781
+ - **Unbind a source**: call through the gateway:
782
+ `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", input: { template_id, element_path, etag, confirm: true } })`.
783
+ Same elicitation flow as delete.
784
+ - **Flat schema inspection** (e.g. enumerate every element path +
785
+ type without fetching the whole nested tree): call through the
786
+ gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", input: { template_id } })`.
787
+ - **Etag-only fetch** (e.g. polling for concurrent edits): use
788
+ `yootheme_builder_get_etag()` (takes no arguments) — cheaper than
789
+ fetching the full layout.
790
+ - **Find a public/front-end URL for a template** (404 test page,
791
+ homepage URL, etc.): call
792
+ `yootheme_builder_pages_list({ fields: ["id", "label", "frontend_url", "frontend_url_template", "frontend_url_description"] })`,
793
+ match by label, and read `frontend_url` (resolved) or
794
+ `frontend_url_template` (with placeholders the user fills in).
795
+ - **Find out which site / install you are connected to**: call
796
+ `yootheme_builder_health` (Bearer-authenticated payload includes
797
+ `site_url` + `home_url`) or `yootheme_builder_diagnose`.
798
+ - **Strip legacy `implode` directives** from an element binding (audit-clean
799
+ source props that pre-date the wrapper-source refactor): call through the
800
+ gateway:
801
+ `yootheme_builder_advanced({ tool: "yootheme_builder_clean_implode_directives", input: { template_id, element_path, etag } })`.
802
+ Returns the audit log + a fresh ETag; idempotent (`cleaned_count: 0` when
803
+ there is nothing to remove).
804
+
805
+ If the user asks for something none of the above covers (e.g. global
806
+ theme settings, menu management, media library), tell them clearly:
807
+ "This MCP server only covers the YOOtheme Page Builder surface. For
808
+ <X> you'll need the relevant CMS REST API or direct wp-admin /
809
+ Joomla administrator access." Don't fabricate tool calls.
810
+
811
+ ## Appendix: Tool Catalog (auto-generated)
812
+
813
+ <!-- TOOL-CATALOG:BEGIN -->
814
+
815
+ **26 tools** — 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.
816
+
817
+ | Tool | Kind | Input keys | Description |
818
+ | --- | --- | --- | --- |
819
+ | `yootheme_builder_clean_implode_directives` | idempotent | `element_path`, `etag`, `site_id`, `template_id` | Strips `props.source.props.*.implode` directives from an element binding. Returns audit log + new ETag. Idempotent (cleaned_count: 0 when nothing to remove). Requires ETag. Operates on the default site unless site_id is provided. |
820
+ | `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. Operates on the default site unless site_id is provided. |
821
+ | `yootheme_builder_element_add` | mutating | `children`, `element_type`, `etag`, `parent_path`, `props`, `site_id`, `template_id` | Add a new element to a template. Provide `parent_path` (or "" for root), `element_type` (e.g. "headline", "text", "grid"), and optional `props` / `children`. Returns the new element's JSON-Pointer path. Requires ETag. Operates on the default site unless site_id is provided. |
822
+ | `yootheme_builder_element_bind_source` | idempotent | `bindingLevel`, `element_path`, `etag`, `field_mappings`, `site_id`, `source_id`, `source_name`, `template_id` | Binds a Builder source to an element (sets `props.source`). Use bindingLevel "item" on Multi-Items containers (grid/slideshow/switcher/…) to bind on the first *_item child instead of the container itself. Requires ETag. Operates on the default site unless site_id is provided. |
823
+ | `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. Requires ETag. Operates on the default site unless site_id is provided. |
824
+ | `yootheme_builder_element_delete` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | PERMANENTLY delete an element and all its children. Cannot be undone. Always ask the user to confirm first, then call again with `confirm: true`. Requires ETag. Operates on the default site unless site_id is provided. |
825
+ | `yootheme_builder_element_get` | read+idempotent | `element_path`, `site_id`, `template_id` | Get the full element object at a specific JSON-Pointer path, including props and children. Use yootheme_builder_element_list to discover paths. Operates on the default site unless site_id is provided. |
826
+ | `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `site_id`, `template_id` | Read the source binding attached to an element — the bound source name, the field-mappings (which source field feeds which element prop) and the query arguments/directives. Returns the empty object if the element is not bound. Operates on the default site unless site_id is provided. |
827
+ | `yootheme_builder_element_list` | read+idempotent | `cursor`, `depth`, `fields`, `limit`, `root_path`, `site_id`, `template_id` | List elements in a template as a flat array with JSON-Pointer paths + types. Scope with `root_path`/`depth` for a subtree, paginate with `limit`/`cursor` for large templates. `fields[]` narrows each row. Operates on the default site unless site_id is provided. |
828
+ | `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). Requires ETag. Operates on the default site unless site_id is provided. |
829
+ | `yootheme_builder_element_type_get_schema` | read+idempotent | `element_type`, `site_id`, `type_name` | **Call before every `element_add` / `_update_settings`** — unknown prop keys are silently dropped server-side, so guessing fails quietly. Returns `{name,type,label?}` field descriptors. Use `element_type`; `type_name` is DEPRECATED. Operates on the default site unless site_id is provided. |
830
+ | `yootheme_builder_element_types_list` | read+idempotent | `fields`, `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. Operates on the default site unless site_id is provided. |
831
+ | `yootheme_builder_element_unbind_source` | destructive | `confirm`, `element_path`, `etag`, `site_id`, `template_id` | Remove the source binding from an element. Clears `props.source`. Destructive in the sense that it may break dynamic-content rendering — always ask the user to confirm. Requires ETag. Operates on the default site unless site_id is provided. |
832
+ | `yootheme_builder_element_update_settings` | idempotent | `element_path`, `etag`, `merge`, `props`, `site_id`, `template_id` | 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. Operates on the default site unless site_id is provided. |
833
+ | `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current top-level state ETag. Pass it back via `etag` on any write tool (page_save/page_publish/element_set/binding_set) to enforce optimistic-locking — the server returns 412 when the ETag has changed since you read it. Operates on the default site unless site_id is provided. |
834
+ | `yootheme_builder_health` | read+idempotent | `site_id` | Check plugin installed/reachable. Returns plugin version, YT Pro version, REST endpoints. Authenticated payload adds site_url + home_url for deep-linking. See yootheme_builder_diagnose for Bearer-validity + connectivity summary. Operates on the default site unless site_id is provided. |
835
+ | `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, …), current binding level (none\|container\|item), and a recommended_fix when the binding sits on the container instead of the child. Operates on the default site unless site_id is provided. |
836
+ | `yootheme_builder_page_get_layout` | read+idempotent | `fields`, `flat`, `site_id`, `template_id` | Get full layout tree for one template. Default nested `{layout, etag}`. Set `flat:true` for depth-first array `{elements:[...], etag}`; combine with `fields[]` to project per-element. Operates on the default site unless site_id is provided. |
837
+ | `yootheme_builder_page_get_schema` | read+idempotent | `site_id`, `template_id` | Get the flat schema for a template — a list of nodes with their JSON-Pointer paths and element types. Best entry-point for navigation: lighter than page_get_layout, sufficient to locate elements before editing. Operates on the default site unless site_id is provided. |
838
+ | `yootheme_builder_page_publish` | idempotent | `etag`, `site_id`, `template_id` | Publish a template — persist state, flush YT + WP caches, snapshot the published-state ETag. ETag optional — when provided, 412 on conflict; when omitted, last-write-wins. Recommended for collaborative edits. Operates on the default site unless site_id is provided. |
839
+ | `yootheme_builder_page_save` | idempotent | `etag`, `site_id`, `template_id` | Re-run save-transforms and persist. ETag optional — when provided, 412 on conflict; when omitted, last-write-wins. Recommended for collaborative edits. No-op when state is byte-identical (returns `no_changes:true`, ETag unchanged). Operates on the default site unless site_id is provided. |
840
+ | `yootheme_builder_pages_list` | read+idempotent | `fields`, `site_id` | List YT templates. Returns id, label, usage meta + per-row `frontend_url` (permalink when public) and `frontend_url_template` (pattern for search / 404 / ID-based templates). Pass `fields:["id","label"]` to slim. Site URL: diagnose. Operates on the default site unless site_id is provided. |
841
+ | `yootheme_builder_sites_list` | read+idempotent | `site_id` | List every site configured in this MCP server (site_id, URL, platform, default flag, bearer source). Platform-agnostic. Never calls a REST endpoint, never resolves a bearer. Useful when the agent needs to discover available site_ids before targeting one with `site_id: "<id>"`. (`site_id` is accepted for schema-uniformity but ignored by this tool.) |
842
+ | `yootheme_builder_sites_test` | read+idempotent | `site_id` | Verify connectivity to ONE configured site: probes `/health` (unauthenticated) and `/etag` (authenticated) in parallel and returns plugin_reachable + bearer_valid in a single call. `site_id` is REQUIRED (this tool exists to test a specific id, never the default). Use `yootheme_builder_sites_list` to discover available IDs first. |
843
+ | `yootheme_builder_sources_list` | read+idempotent | `fields`, `site_id` | List Builder sources grouped by origin (apimapper/wordpress/essentials). Returns name+label per source — pick one for `element_bind_source`. Pass `fields[]` to narrow each row. Operates on the default site unless site_id is provided. |
844
+ | `yootheme_builder_template_summary` | read+idempotent | `site_id`, `template_id` | 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. Operates on the default site unless site_id is provided. |
845
+
846
+ <!-- TOOL-CATALOG:END -->