@wootsup/yt-builder-mcp 1.0.1 → 1.1.5

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.
package/README.md CHANGED
@@ -79,7 +79,7 @@ in `tools/list`** (well under Cursor's ~40-tool cap):
79
79
  clean_implode_directives, element_type_get_schema).
80
80
 
81
81
  Total: **23 domain tools + 1 gateway = 24**. Surface: **11 entries
82
- in tools/list**. See `skills/yootheme-builder/SKILL.md` for the
82
+ in tools/list**. See `skills/yt-builder-mcp/SKILL.md` for the
83
83
  full catalog and the 5 canonical workflows.
84
84
 
85
85
  ## Tool catalogue — 23 domain tools
@@ -144,8 +144,10 @@ When launched by an AI client (or directly):
144
144
 
145
145
  | Variable | Required? | Purpose |
146
146
  |----------|-----------|---------|
147
- | `YTB_MCP_WP_URL` | Yes | WordPress base URL (e.g. `https://example.com`). Trailing slash is stripped. |
148
- | `YTB_MCP_BEARER_TOKEN` | Yes | Bearer key from wp-admin. Format-checked client-side: must match `ytb_(live\|test)_<payload>.<sig>`. Do **not** prepend `Bearer ` the MCP server adds it. |
147
+ | `YTB_MCP_SITE_URL` | Yes | Host CMS base URL (e.g. `https://example.com`). Works for BOTH WordPress and Joomla. Trailing slash is stripped. |
148
+ | `YTB_MCP_WP_URL` | No (deprecated) | Legacy alias for `YTB_MCP_SITE_URL`; still honoured for pre-Wave-7 configurations. A non-fatal deprecation notice is written to stderr when this is used without `YTB_MCP_SITE_URL`. |
149
+ | `YTB_MCP_BEARER_TOKEN` | Yes | Bearer key from wp-admin (WordPress: Tools → "YT Builder MCP") or Administrator → Components → "YT Builder MCP" (Joomla). Format-checked client-side: must match `ytb_(live\|test)_<payload>.<sig>`. Do **not** prepend `Bearer ` — the MCP server adds it. |
150
+ | `YTB_MCP_PLATFORM` | No | Explicit platform hint: `wordpress` or `joomla`. Set to `joomla` when `YTB_MCP_SITE_URL` is an origin-only Joomla URL (no `/api/index.php/` in the path). Defaults to URL-shape auto-detection. |
149
151
  | `YTB_MCP_TIMEOUT_MS` | No | REST timeout (default 15000). |
150
152
  | `YTB_MCP_TEST_MODE` | No | `1` skips the stdio loop (smoke tests). |
151
153
 
@@ -157,11 +159,11 @@ client's MCP config file:
157
159
  ```json
158
160
  {
159
161
  "mcpServers": {
160
- "yootheme-builder": {
162
+ "yt-builder-mcp": {
161
163
  "command": "npx",
162
164
  "args": ["-y", "@wootsup/yt-builder-mcp"],
163
165
  "env": {
164
- "YTB_MCP_WP_URL": "https://example.com",
166
+ "YTB_MCP_SITE_URL": "https://example.com",
165
167
  "YTB_MCP_BEARER_TOKEN": "ytb_live_…"
166
168
  }
167
169
  }
@@ -169,6 +171,16 @@ client's MCP config file:
169
171
  }
170
172
  ```
171
173
 
174
+ For a Joomla install at an origin-only URL, also set the platform hint:
175
+
176
+ ```json
177
+ "env": {
178
+ "YTB_MCP_SITE_URL": "https://example.com/joomla",
179
+ "YTB_MCP_BEARER_TOKEN": "ytb_live_…",
180
+ "YTB_MCP_PLATFORM": "joomla"
181
+ }
182
+ ```
183
+
172
184
  ## Non-interactive CI usage
173
185
 
174
186
  For scripted / CI installs, pass the answers as flags and add
@@ -213,7 +225,7 @@ The CLI returns POSIX-style exit codes for scripting / CI:
213
225
 
214
226
  ## Documentation
215
227
 
216
- - [SKILL.md (bundled agent skill)](./skills/yootheme-builder/SKILL.md) — 5 canonical workflows, gateway model, tool catalog
228
+ - [SKILL.md (bundled agent skill)](./skills/yt-builder-mcp/SKILL.md) — 5 canonical workflows, gateway model, tool catalog
217
229
  - [REST API Reference](https://github.com/wootsup/yt-builder-mcp/blob/main/docs/rest-api-reference.md)
218
230
  - [MCP Tool Reference](https://github.com/wootsup/yt-builder-mcp/blob/main/docs/mcp-tool-reference.md)
219
231
 
@@ -25,6 +25,12 @@ const CLI_SUBCOMMANDS = new Set([
25
25
  'setup',
26
26
  'install-skill',
27
27
  'install',
28
+ // W9 multi-site CLI subcommands.
29
+ 'add-site',
30
+ 'list-sites',
31
+ 'remove-site',
32
+ 'set-default',
33
+ 'test-site',
28
34
  'help',
29
35
  '--help',
30
36
  '-h',
package/manifest.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "@wootsup/yt-builder-mcp",
4
4
  "display_name": "YT Builder MCP for YOOtheme Pro (unofficial)",
5
- "version": "1.0.1",
6
- "description": "Drive your page builder programmatically from Claude, Cursor, Codex, Gemini and 5 other MCP-capable AI assistants — read layouts, edit settings, add elements, bind dynamic sources. Built for YOOtheme Pro 4.0+. Independent third-party project, not affiliated with YOOtheme GmbH.",
7
- "long_description": "YT Builder MCP gives AI assistants a typed, scoped, and idempotent surface for your YOOtheme Pro page builder. Tools cover page discovery, layout inspection, element CRUD, source binding, and diagnostics. Bearer auth, optimistic locking via ETag, scope hierarchy (read < write < admin), and structured-error responses keep the agent safe and the workflow correct on first try. Independent third-party project — YOOtheme® is a registered trademark of YOOtheme GmbH (yootheme.com); YT Builder MCP is built by WootsUp (getimo productions) and is not affiliated with, endorsed by, or sponsored by YOOtheme.",
5
+ "version": "1.1.5",
6
+ "description": "Drive your YOOtheme Pro Page Builder programmatically from Claude, Cursor, Codex, Gemini and 5 other MCP-capable AI assistants — read layouts, edit settings, add elements, bind dynamic sources. Works on WordPress and Joomla 5/6. Built for YOOtheme Pro 4.0+. Independent third-party project, not affiliated with YOOtheme GmbH.",
7
+ "long_description": "YT Builder MCP gives AI assistants a typed, scoped, and idempotent surface for your YOOtheme Pro Page Builder, on either WordPress or Joomla 5/6. Tools cover page discovery, layout inspection, element CRUD, source binding, and diagnostics. Bearer auth, optimistic locking via ETag, scope hierarchy (read < write < admin), and structured-error responses keep the agent safe and the workflow correct on first try. Independent third-party project — YOOtheme® is a registered trademark of YOOtheme GmbH (yootheme.com); YT Builder MCP is built by WootsUp (getimo productions) and is not affiliated with, endorsed by, or sponsored by YOOtheme.",
8
8
  "icon": "icon.png",
9
9
  "author": {
10
10
  "name": "WootsUp",
@@ -21,6 +21,8 @@
21
21
  "yootheme",
22
22
  "yootheme-pro",
23
23
  "wordpress",
24
+ "joomla",
25
+ "cms",
24
26
  "page-builder",
25
27
  "mcp",
26
28
  "model-context-protocol",
@@ -37,24 +39,39 @@
37
39
  "${__dirname}/dist/index.js"
38
40
  ],
39
41
  "env": {
40
- "YTB_MCP_WP_URL": "${user_config.YTB_MCP_WP_URL}",
42
+ "YTB_MCP_SITES_FILE": "${user_config.YTB_MCP_SITES_FILE}",
43
+ "YTB_MCP_SITE_URL": "${user_config.YTB_MCP_SITE_URL}",
44
+ "YTB_MCP_PLATFORM": "${user_config.YTB_MCP_PLATFORM}",
41
45
  "YTB_MCP_BEARER_TOKEN": "${user_config.YTB_MCP_BEARER_TOKEN}"
42
46
  }
43
47
  }
44
48
  },
45
49
  "user_config": {
46
- "YTB_MCP_WP_URL": {
50
+ "YTB_MCP_SITE_URL": {
47
51
  "type": "string",
48
- "title": "WordPress site URL",
49
- "description": "Base URL of the WordPress install (e.g. https://example.com). Without trailing slash.",
50
- "required": true
52
+ "title": "Site URL (connect one site)",
53
+ "description": "Fill this if you connect a SINGLE site (most people). The address of your YOOtheme site, e.g. https://example.com, or https://example.com/wordpress if WordPress runs in a subfolder. No trailing slash. Leave empty if you use the Sites file below.",
54
+ "required": false
51
55
  },
52
56
  "YTB_MCP_BEARER_TOKEN": {
53
57
  "type": "string",
54
- "title": "Bearer token",
55
- "description": "Bearer token generated in wp-admin Tools YT Builder MCP Generate Key.",
56
- "required": true,
58
+ "title": "Bearer token (for that one site)",
59
+ "description": "Fill this if you connect a SINGLE site. Paste the key you generated in your CMS admin: WordPress under Tools, YT Builder MCP; Joomla under Components, YT Builder MCP, then Generate Key. The 'write' scope is enough. Leave empty if you use the Sites file below.",
60
+ "required": false,
57
61
  "sensitive": true
62
+ },
63
+ "YTB_MCP_PLATFORM": {
64
+ "type": "string",
65
+ "title": "Platform (one site, optional)",
66
+ "description": "Leave this on 'auto'. The server detects WordPress or Joomla by itself. Only set 'wordpress' or 'joomla' if your host blocks the automatic check.",
67
+ "required": false,
68
+ "default": "auto"
69
+ },
70
+ "YTB_MCP_SITES_FILE": {
71
+ "type": "string",
72
+ "title": "Sites file (connect many sites, advanced)",
73
+ "description": "Only for managing several sites from one install (agencies). Path to a sites.json, e.g. /Users/you/.config/yt-builder-mcp/sites.json. Create and fill it with: npx -y @wootsup/yt-builder-mcp add-site (do not hand-edit). When set, the three single-site fields above are ignored. Most people leave this empty.",
74
+ "required": false
58
75
  }
59
76
  },
60
77
  "compatibility": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wootsup/yt-builder-mcp",
3
- "version": "1.0.1",
3
+ "version": "1.1.5",
4
4
  "type": "module",
5
5
  "description": "YT Builder MCP — drive your page builder from Claude, Cursor, Codex, Gemini and 5 other MCP-capable AI assistants. Built for YOOtheme Pro 4.0+. Independent third-party project, not affiliated with YOOtheme GmbH.",
6
6
  "license": "MIT",
@@ -66,10 +66,12 @@
66
66
  "@modelcontextprotocol/sdk": "^1.27.0",
67
67
  "@clack/prompts": "^0.7.0",
68
68
  "pino": "^10.3.1",
69
+ "proper-lockfile": "^4.1.2",
69
70
  "zod": "^4.3.6"
70
71
  },
71
72
  "devDependencies": {
72
73
  "@types/node": "^20.11.0",
74
+ "@types/proper-lockfile": "^4.1.4",
73
75
  "@typescript-eslint/eslint-plugin": "^8.0.0",
74
76
  "@typescript-eslint/parser": "^8.0.0",
75
77
  "@vitest/coverage-v8": "^1.4.0",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: yt-builder-mcp
3
- description: Drive the YOOtheme Pro page builder programmatically — discover pages, inspect layouts, add/move/clone/delete elements, bind dynamic sources, diagnose 401/403 auth failures. Use when the user wants to build, modify, audit, or troubleshoot a YOOtheme-powered WordPress site through the YT Builder MCP server.
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
4
  ---
5
5
 
6
6
  # YT Builder MCP for YOOtheme Pro (unofficial) — Skill
@@ -8,34 +8,64 @@ description: Drive the YOOtheme Pro page builder programmatically — discover p
8
8
  > Independent third-party project. YOOtheme® is a registered trademark of YOOtheme GmbH
9
9
  > ([yootheme.com](https://yootheme.com)). YT Builder MCP is built by WootsUp (getimo
10
10
  > productions) and is not affiliated with, endorsed by, or sponsored by YOOtheme.
11
- > The integration uses YOOtheme Pro's public extension points.
11
+ > The integration uses YOOtheme Pro's public extension points and works on both
12
+ > WordPress and Joomla 5/6.
12
13
 
13
- This skill helps AI assistants drive the YOOtheme Pro page builder through the
14
- `@wootsup/yt-builder-mcp` server. The server exposes 24 typed,
15
- scoped, idempotent tools behind a 11-entry Gateway-Hub (so it stays
16
- inside the 80-tool Cursor cap even when the catalogue grows).
14
+ This skill helps AI assistants drive the YOOtheme Pro Page Builder through the
15
+ `@wootsup/yt-builder-mcp` server. The server exposes 27 typed, scoped, idempotent
16
+ callable tools. 20 of them advertised as first-class entries in `tools/list`
17
+ (17 essential L1 forwards + 2 direct L3 entries + 1 gateway), and 7 advanced
18
+ tools reachable through the single `yootheme_builder_advanced` gateway. This
19
+ 3-lane split keeps `tools/list` well below the ~40-tool Cursor cap while every
20
+ catalogued tool stays fully reachable.
17
21
 
18
22
  ## How to use this MCP server
19
23
 
20
- The user invokes you through Claude Desktop, Cursor, Zed, Continue, or
21
- any other MCP-aware AI client. Setup looks like this:
22
-
23
- 1. The user installs the WordPress plugin
24
- (`https://wootsup.com/products/yt-builder-mcp`) and generates
25
- a Bearer key in **wp-admin Tools "YT Builder MCP" → Bearer Keys**.
26
- 2. The user runs `npx -y @wootsup/yt-builder-mcp setup` once;
27
- the wizard probes the plugin, validates the key, and writes the
28
- MCP server entry into every selected AI client's config file.
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.)
29
42
  3. The user restarts their AI client. The server is now visible.
30
43
  4. The user asks for a YOOtheme task (build, audit, change, diagnose).
31
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
+
32
60
  When the user asks a YOOtheme-related question, **always start with
33
- `yootheme_builder_health`** — it confirms the plugin is reachable and
34
- returns the plugin/YOOtheme/WordPress/PHP versions you need to know
35
- about before reading or writing layout state.
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`...").
36
66
 
37
- If a tool returns `401 Unauthorized` or `403 Forbidden`, jump straight
38
- to **Workflow 4: Diagnose 401/auth failure**. Do not retry blindly.
67
+ If a tool returns `401 Unauthorized` or `403 Forbidden`, jump straight to
68
+ **Workflow 4: Diagnose 401/auth failure**. Do not retry blindly.
39
69
 
40
70
  ## Gateway routing (so you know what you can call)
41
71
 
@@ -44,20 +74,44 @@ The server exposes:
44
74
  - **2 direct top-level tools** — always callable, always in `tools/list`:
45
75
  `yootheme_builder_health` and `yootheme_builder_diagnose`. These are
46
76
  the "the gateway itself might be broken" escape hatch.
47
- - **8 essential forwarded tools** — common reads + the most-used writes
48
- (page list, get_etag, element list / add / update, sources_list,
49
- element_types_list, inspect_multi_items_binding). Always in
50
- `tools/list` so AI clients see them first-class.
51
- - **13 advanced captured tools** everything else (move, clone, delete,
52
- schema introspection, source binding, clean_implode_directives).
53
- Reachable through one gateway tool:
54
- `yootheme_builder_advanced({ tool: "<name>", input: { ... } })`.
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: { ... } })`.
55
87
  - **1 gateway tool** — `yootheme_builder_advanced`.
56
88
 
57
- If the AI client reports "tool not found", you are almost certainly
58
- calling an advanced tool by its raw name. Wrap it in
89
+ `tools/list` therefore advertises 20 names (17 + 2 + 1) that's 17 L1
90
+ essentials + 2 L3 direct + 1 gateway. The total callable surface is 27
91
+ (20 advertised + 7 advanced reachable through the gateway). If the AI
92
+ client reports "tool not found", you are almost certainly calling an
93
+ advanced tool by its raw name. Wrap it in
59
94
  `yootheme_builder_advanced({ tool, input })` instead.
60
95
 
96
+ ## Site and frontend URLs (for deep-linking and verification)
97
+
98
+ The server surfaces the connected site's URLs in two places so you never have to
99
+ guess where the agent is pointing:
100
+
101
+ - **`yootheme_builder_health` (Bearer-authenticated) and
102
+ `yootheme_builder_diagnose`** return `site_url` and `home_url` for the
103
+ connected install. Call one of them when the user asks "which site are you
104
+ on?" or before deep-linking the user back into wp-admin / Joomla administrator.
105
+ - **`yootheme_builder_pages_list`** returns per-template `frontend_url`,
106
+ `frontend_url_template`, and `frontend_url_description` columns when the
107
+ host plugin can resolve them. Use these when the user asks for a verification
108
+ URL ("show me the 404 page", "give me the front-end URL of the homepage
109
+ template"): find the matching row, return `frontend_url` (resolved) or
110
+ `frontend_url_template` (with placeholders the user fills in).
111
+
112
+ Treat `frontend_url: null` as "host plugin could not resolve a public URL for
113
+ this template" — surface that honestly rather than fabricating one.
114
+
61
115
  ## Scopes (Bearer key permissions)
62
116
 
63
117
  Every Bearer key has a scope, set at key creation time:
@@ -69,8 +123,173 @@ Every Bearer key has a scope, set at key creation time:
69
123
  | `admin` | ✓ | ✓ | ✓ |
70
124
 
71
125
  When a tool returns `{ error: 'insufficient_scope', context: { required: 'write', actual: 'read' } }`,
72
- ask the user to regenerate the key with a higher scope **before**
73
- retrying. Do not loop on auth errors.
126
+ ask the user to regenerate the key with a higher scope **before** retrying.
127
+ Do not loop on auth errors.
128
+
129
+ > **Joomla note.** On the Joomla API surface (`com_api`), the Bearer token's
130
+ > scope is the **sole** authority — the L2 article-write `core.edit` ACL gate
131
+ > 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).
132
+ > Joomla ACL still governs the admin component (`com_ytbmcp`). On WordPress,
133
+ > capabilities like `manage_options` gate the admin settings page only; the
134
+ > REST API surface is Bearer-gated.
135
+
136
+ > **Joomla L2 articles surface.** The Joomla plugin ships `/v1/articles*`
137
+ > REST endpoints for per-article custom layouts, but they are NOT exposed
138
+ > via MCP tools in v1.x. To use that surface, call the REST endpoints
139
+ > directly with the Bearer key. MCP tool coverage for L2 articles is
140
+ > planned for v1.2.0.
141
+
142
+ ---
143
+
144
+ ## Working with multiple sites
145
+
146
+ One MCP install can drive many YOOtheme Pro sites. You configure each site
147
+ once (URL + Bearer key + platform). The agent then targets a specific site
148
+ per tool-call via a `site_id` parameter, or falls back to the default site
149
+ when `site_id` is omitted.
150
+
151
+ ### When and why
152
+
153
+ The typical case is an agency or freelancer running 5, 20, or 100+ YOOtheme
154
+ sites (WordPress and Joomla mixed). Without multi-site support you would need
155
+ one MCP install per site, one set of env vars per site, and one AI-client
156
+ restart per site you want to talk to. With multi-site:
157
+
158
+ - One DXT install in Claude Desktop, one entry in your AI client config.
159
+ - One conversation can edit elements on `acme.com` and `beta.io` back-to-back.
160
+ - Each site keeps its own Bearer key, platform, label, and 1Password reference.
161
+ - Adding a new client site does not require a new MCP install.
162
+
163
+ ### How `site_id` works
164
+
165
+ Every tool accepts an optional `site_id` parameter.
166
+
167
+ - **Omit `site_id`**: the tool runs against the **default site**. This is the
168
+ common case for single-site users (the registry has one site and it is the
169
+ default) and for agency users who picked a "main" site for the session.
170
+ - **Pass `site_id: "wp-acme"`**: the tool runs against that specific site,
171
+ overriding the default for that one call.
172
+
173
+ When the agent does not know which sites are available, it calls
174
+ `yootheme_builder_sites_list` first. The response lists every configured site
175
+ with its `site_id`, URL, platform, default flag, and bearer source (so the
176
+ agent can choose by label and the user can verify by URL).
177
+
178
+ To verify a specific site before doing work on it, call
179
+ `yootheme_builder_sites_test({ site_id: "wp-acme" })`. This probes `/health`
180
+ and `/etag` in parallel and returns `plugin_reachable` + `bearer_valid`
181
+ without mutating anything.
182
+
183
+ ### Default-site mechanics
184
+
185
+ The default site is set automatically on first add:
186
+
187
+ - **First site you add**: becomes the default automatically. No flag needed.
188
+ - **Every subsequent site**: NOT default by default. Use `--default` on
189
+ `add-site` to make it the new default (the old default is demoted).
190
+ - **You remove the current default**: the next site in the registry order
191
+ is promoted to default. The registry is never left without a default
192
+ while ≥1 site exists.
193
+
194
+ ### Plain bearer vs 1Password reference
195
+
196
+ You can store the Bearer key two ways per site:
197
+
198
+ - **Plain field**: `bearer: "ytb_live_..."` in `sites.json`. Easy for dev or
199
+ onboarding, but the secret lives on disk in plaintext.
200
+ - **1Password reference**: `bearer_ref: "op://Vault/Item/credential"` in
201
+ `sites.json`. The plaintext token never touches disk. The MCP server shells
202
+ out to the `op` CLI at first use per site to fetch the live token, then
203
+ caches it in memory for that process lifetime.
204
+
205
+ **Recommendation for production sites**: use `bearer_ref`. You get rotation
206
+ without editing `sites.json`, and your 1Password audit log captures every
207
+ fetch. The `op` CLI must be installed and signed in on the machine running
208
+ the MCP server. If `op` is missing, the resolver returns a structured error
209
+ (`op CLI not found in PATH`) pointing at the install docs.
210
+
211
+ ### `sites.json` location
212
+
213
+ The registry lives at `~/.config/yt-builder-mcp/sites.json` (XDG-conform).
214
+ If `XDG_CONFIG_HOME` is set, the file lives at
215
+ `$XDG_CONFIG_HOME/yt-builder-mcp/sites.json` instead. The file is created
216
+ with mode `0600` so only the current user can read it.
217
+
218
+ The CLI subcommands below are the supported way to edit `sites.json`. Direct
219
+ edits work but skip the schema-validation and atomic-write paths.
220
+
221
+ ### CLI subcommands
222
+
223
+ Run these via `npx -y @wootsup/yt-builder-mcp <subcommand>`:
224
+
225
+ - **`setup`**: interactive wizard for the first site. Probes the host
226
+ plugin, validates the Bearer key, picks the platform, writes `sites.json`,
227
+ and writes the MCP server entry into every selected AI-client config.
228
+ - **`add-site [--url <url>] [--token <bearer> | --token-ref op://...] [--platform auto|wordpress|joomla] [--label "..."] [--default] [--site-id <slug>] [--yes]`**:
229
+ add a new site. Flags can be passed for non-interactive use; missing flags
230
+ trigger prompts. `--default` makes the new site the default (demoting the
231
+ old one). `--yes` skips the confirmation prompt.
232
+ - **`list-sites`**: print every configured site as a table (site_id, URL,
233
+ platform, default flag, bearer source).
234
+ - **`remove-site <site_id> [--yes]`**: delete a site from the registry. If
235
+ the removed site was the default, the next site in registry order is
236
+ auto-promoted. `--yes` skips the confirmation prompt.
237
+ - **`set-default <site_id>`**: switch the default site to `<site_id>`. The
238
+ previous default is demoted.
239
+ - **`test-site <site_id>`**: pre-flight probe (`/health` + `/etag`) for one
240
+ site. Returns `plugin_reachable` + `bearer_valid` and exits non-zero on
241
+ failure. Mutates nothing.
242
+
243
+ ### Restart your AI client after registry changes
244
+
245
+ After `add-site`, `remove-site`, `set-default`, or any direct edit to
246
+ `sites.json`, **restart Claude Desktop** (or your AI client). The MCP
247
+ protocol sends the `instructions` block (which carries the "Currently
248
+ configured sites" appendix) once at `initialize`. The agent will not see new
249
+ sites in its instructions until the next `initialize` cycle.
250
+
251
+ `sites_list` and the per-call `site_id` parameter both keep working
252
+ without a restart (they read the live registry on every call), but the
253
+ agent's narrative awareness of which sites exist lags by one restart.
254
+
255
+ The CLI prints a reminder line after every mutation so you do not forget.
256
+
257
+ ### Site-awareness in every response
258
+
259
+ Every tool reply carries the connected site in two places:
260
+
261
+ - **Text prefix**: every text response starts with `[<label> @ <host>]` so
262
+ the customer can see at a glance which site produced the answer
263
+ (`[ACME Production @ acme.com] 12 templates ...`).
264
+ - **Structured metadata**: `structuredContent._meta.site_id`,
265
+ `structuredContent._meta.site_url`, and `structuredContent._meta.platform`
266
+ carry the same info in a machine-readable shape. Agents can use this for
267
+ routing, logging, or follow-up calls.
268
+
269
+ If the AI-client UI hides `_meta`, the text prefix still tells the user
270
+ which site they are looking at.
271
+
272
+ ### End-to-end example: bulk element update across 5 agency sites
273
+
274
+ Goal: change a hero headline on the `home` template of 5 client sites in
275
+ one conversation.
276
+
277
+ 1. The agent calls `yootheme_builder_sites_list()`. It learns the 5
278
+ site_ids: `wp-acme`, `wp-beta`, `joomla-gamma`, `wp-delta`,
279
+ `joomla-epsilon`.
280
+ 2. For each site, the agent runs the same sequence with `site_id` set:
281
+ - `yootheme_builder_pages_list({ site_id: "wp-acme", fields: ["id", "label"] })`
282
+ - `yootheme_builder_get_etag({ site_id: "wp-acme" })`
283
+ - `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>" })`
284
+ - `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { site_id: "wp-acme", template_id: "home", etag: "<fresh>" } })`
285
+ - `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { site_id: "wp-acme", template_id: "home", etag: "<fresh>" } })`
286
+ 3. The customer sees a stream of replies, each prefixed with the matching
287
+ `[label @ host]`, so it is obvious which site is at which step.
288
+
289
+ If one site fails (auth error, plugin not active, network blip), the agent
290
+ isolates the failure to that one `site_id` and continues with the rest.
291
+ Run `yootheme_builder_sites_test({ site_id: "<id>" })` on the failing site
292
+ for a focused diagnosis without touching the others.
74
293
 
75
294
  ---
76
295
 
@@ -81,8 +300,9 @@ to an existing page.
81
300
 
82
301
  **Canonical tool-call sequence (real parameter names — snake_case):**
83
302
 
84
- 1. `yootheme_builder_health` — confirm plugin reachable; note plugin
85
- version (some element types are version-gated).
303
+ 1. `yootheme_builder_health` — confirm host plugin reachable; note plugin
304
+ version and `site_url` (some element types are version-gated; surface
305
+ the site URL back to the user).
86
306
  2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` — find
87
307
  the target template. Returns `[{ id, label, ... }]`. If the user
88
308
  named a specific page, match on `label` (exact then fuzzy).
@@ -100,10 +320,11 @@ to an existing page.
100
320
  — add a text element.
101
321
  8. `yootheme_builder_element_add({ template_id, parent_path: "<row-path>", element_type: "button", props: { content: "<cta>", link: "<url>" }, etag })`
102
322
  — add the CTA button.
103
- 9. `yootheme_builder_page_save({ template_id, etag })` — persist the
104
- working copy (visible in YOOtheme Customizer preview).
105
- 10. `yootheme_builder_page_publish({ template_id, etag })` make the
106
- changes live on the front-end.
323
+ 9. `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { template_id, etag } })`
324
+ — persist the working copy (visible in YOOtheme Customizer preview).
325
+ `page_save` is an advanced tool; call it through the gateway.
326
+ 10. `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { template_id, etag } })`
327
+ — make the changes live on the front-end. Also an advanced tool.
107
328
 
108
329
  **Common pitfalls:**
109
330
 
@@ -124,6 +345,9 @@ to an existing page.
124
345
  - **Reusing a stale etag across many writes.** Every write returns a
125
346
  fresh etag in the response. Pass THAT etag into the next write —
126
347
  don't hold the one from the original `get_etag` call.
348
+ - **Calling page_save / page_publish by name.** Both are advanced
349
+ (L2) tools — call them through `yootheme_builder_advanced({ tool, input })`,
350
+ not directly. The first sign you forgot is "tool not found".
127
351
 
128
352
  **Worked example (tool-call snippet):**
129
353
 
@@ -138,6 +362,12 @@ yootheme_builder_element_add({
138
362
  })
139
363
  // Response: { path: "/0/children/3", etag: "def456" }
140
364
  // → next call uses etag "def456"
365
+
366
+ // Step 9 — page_save is L2; call via the gateway.
367
+ yootheme_builder_advanced({
368
+ tool: "yootheme_builder_page_save",
369
+ input: { template_id: "home", etag: "<latest>" }
370
+ })
141
371
  ```
142
372
 
143
373
  **Edge case:** YOOtheme allows nested sections (rare) — if the user
@@ -161,7 +391,7 @@ renders dynamic items.
161
391
 
162
392
  **Canonical tool-call sequence (real parameter names — snake_case):**
163
393
 
164
- 1. `yootheme_builder_health` — confirm plugin reachable.
394
+ 1. `yootheme_builder_health` — confirm host plugin reachable.
165
395
  2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` and
166
396
  `yootheme_builder_page_get_layout({ template_id: "<id>", flat: false })`
167
397
  — locate the target Grid. Note its JSON-Pointer `path` (e.g.
@@ -172,20 +402,21 @@ renders dynamic items.
172
402
  4. `yootheme_builder_sources_list()` — enumerate available Sources.
173
403
  Each returns `{ name, label, origin, kind }`. Pick the one the
174
404
  user asked for.
175
- 5. `yootheme_builder_element_get_binding({ template_id, element_path })`
405
+ 5. `yootheme_builder_advanced({ tool: "yootheme_builder_element_get_binding", input: { template_id, element_path } })`
176
406
  — check whether the Grid is already bound (idempotency: skip step
177
407
  7 if `source_name` already matches the user's intent).
408
+ `element_get_binding` is an advanced (L2) tool — call via the gateway.
178
409
  6. `yootheme_builder_get_etag()` — fetch the optimistic-lock etag for
179
410
  the upcoming mutation.
180
- 7. `yootheme_builder_element_bind_source({ template_id, element_path, source_name: "<name>", etag: "<etag>" })`
411
+ 7. `yootheme_builder_advanced({ tool: "yootheme_builder_element_bind_source", input: { template_id, element_path, source_name: "<name>", etag: "<etag>" } })`
181
412
  — apply the binding. Returns `{ path, etag, has_binding: true }`.
182
413
  Pass `source_id: "<origin>:<name>"` as well **only** when two
183
414
  plugins register a source with the same `source_name` (the server
184
415
  surfaces the ambiguity as an elicitation prompt; if the host
185
416
  doesn't support elicitation you'll see a structured error listing
186
- the candidates).
187
- 8. `yootheme_builder_page_save({ template_id, etag: "<fresh>" })`
188
- then `yootheme_builder_page_publish({ template_id, etag: "<fresh>" })`.
417
+ the candidates). Also an advanced tool.
418
+ 8. `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { template_id, etag: "<fresh>" } })`
419
+ then `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { template_id, etag: "<fresh>" } })`.
189
420
 
190
421
  **Common pitfalls:**
191
422
 
@@ -207,23 +438,29 @@ renders dynamic items.
207
438
  - **Forgetting `etag`.** Every write requires the optimistic-lock
208
439
  etag. On `412 Precondition Failed` re-fetch via
209
440
  `yootheme_builder_get_etag` and retry.
441
+ - **Calling bind/unbind directly.** Both `element_bind_source` and
442
+ `element_get_binding` are L2 advanced — wrap in
443
+ `yootheme_builder_advanced({ tool, input })`.
210
444
 
211
445
  **Worked example (tool-call snippet):**
212
446
 
213
447
  ```jsonc
214
- // Step 7 — bind a Posts source onto a Grid element.
215
- yootheme_builder_element_bind_source({
216
- template_id: "home",
217
- element_path: "/0/children/2/children/0",
218
- source_name: "wp_posts",
219
- etag: "abc123"
220
- // source_id: "wordpress:wp_posts" // pass ONLY when name collides
448
+ // Step 7 — bind a Posts source onto a Grid element via the gateway.
449
+ yootheme_builder_advanced({
450
+ tool: "yootheme_builder_element_bind_source",
451
+ input: {
452
+ template_id: "home",
453
+ element_path: "/0/children/2/children/0",
454
+ source_name: "wp_posts",
455
+ etag: "abc123"
456
+ // source_id: "wordpress:wp_posts" // pass ONLY when name collides
457
+ }
221
458
  })
222
459
  // Response: { path: "/0/children/2/children/0", etag: "def456", has_binding: true }
223
- // Verify:
224
- yootheme_builder_element_get_binding({
225
- template_id: "home",
226
- element_path: "/0/children/2/children/0"
460
+ // Verify (via gateway):
461
+ yootheme_builder_advanced({
462
+ tool: "yootheme_builder_element_get_binding",
463
+ input: { template_id: "home", element_path: "/0/children/2/children/0" }
227
464
  })
228
465
  // → { source_name: "wp_posts", source_config: { ... }, ... }
229
466
  ```
@@ -232,12 +469,12 @@ yootheme_builder_element_get_binding({
232
469
  search filter). The bind call still succeeds; the front-end Grid just
233
470
  shows the YOOtheme "no items" placeholder. Don't treat empty render
234
471
  as a binding failure — verify by re-reading
235
- `yootheme_builder_element_get_binding`.
472
+ `yootheme_builder_element_get_binding` through the gateway.
236
473
 
237
474
  **Success criterion:** After publish, the Grid on the front-end shows
238
475
  items from the Source (verify by item count and at least one
239
476
  field-value spot-check). `yootheme_builder_element_get_binding`
240
- returns the new `source_name`.
477
+ (via gateway) returns the new `source_name`.
241
478
 
242
479
  ---
243
480
 
@@ -256,16 +493,17 @@ right after the source. To move the clone elsewhere in the SAME
256
493
  template, call `yootheme_builder_element_move` afterwards. To
257
494
  duplicate into a DIFFERENT template, flag to the user that
258
495
  cross-template clone is not currently supported and suggest a
259
- WordPress-level template duplication.
496
+ CMS-level template duplication (in wp-admin or Joomla administrator).
260
497
 
261
498
  **Canonical tool-call sequence (real parameter names — snake_case):**
262
499
 
263
- 1. `yootheme_builder_health` — confirm plugin reachable.
500
+ 1. `yootheme_builder_health` — confirm host plugin reachable.
264
501
  2. `yootheme_builder_pages_list({ fields: ["id", "label"] })` —
265
502
  locate the template by `label`. Note its `id`.
266
- 3. `yootheme_builder_page_get_schema({ template_id })` — flat schema
267
- view (lighter than `page_get_layout`) showing every element path
268
- + type. Pick the JSON-Pointer path of the section to clone.
503
+ 3. `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", input: { template_id } })`
504
+ — flat schema view (lighter than `page_get_layout`) showing every
505
+ element path + type. Pick the JSON-Pointer path of the section
506
+ to clone. `page_get_schema` is L2; call via the gateway.
269
507
  4. `yootheme_builder_get_etag()` — fetch the optimistic-lock etag.
270
508
  5. `yootheme_builder_element_clone({ template_id, element_path: "<src-path>", etag: "<etag>" })`
271
509
  — clone as sibling. Returns `{ path: "<new-path>", etag: "<fresh>" }`.
@@ -274,30 +512,31 @@ WordPress-level template duplication.
274
512
  — re-parent the clone within the same template if needed.
275
513
  7. `yootheme_builder_element_update_settings({ template_id, element_path: "<final-path>", props: { ... }, etag: "<fresh>" })`
276
514
  — replace the `props` on the clone. **Existing props NOT in the
277
- request are removed** (update_settings is a full replace, not a
278
- merge). Read the current props first via
279
- `yootheme_builder_element_get` if you only want to tweak a subset.
280
- 8. `yootheme_builder_page_save({ template_id, etag })` then
281
- `yootheme_builder_page_publish({ template_id, etag })`.
515
+ request are removed** (update_settings is a full replace by default;
516
+ pass `merge: true` to apply a server-side deep-merge instead).
517
+ Read the current props first via `yootheme_builder_element_get`
518
+ if you only want to tweak a subset.
519
+ 8. `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { template_id, etag } })`
520
+ then `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { template_id, etag } })`.
282
521
 
283
522
  **Common pitfalls:**
284
523
 
285
524
  - **Inventing destination parameters.** `element_clone` does NOT
286
525
  accept `destPageId`, `destParentPath`, or any cross-template
287
526
  argument. It's sibling-only within ONE template.
288
- - **Treating `element_update_settings` as a merge.** The handler
289
- REPLACES the entire `props` object on the element; any key you
290
- don't include is removed. Use `element_get` first if you need to
291
- preserve siblings of the field you're changing.
527
+ - **Treating `element_update_settings` as a merge by default.** The handler
528
+ REPLACES the entire `props` object on the element unless you pass
529
+ `merge: true`. Read the existing shape via `yootheme_builder_element_get`
530
+ first if you only want to tweak a subset and prefer not to use merge.
292
531
  - **Clone-then-update path drift.** The clone returns a path that's
293
532
  correct at the moment of the call. If you fire off many ops in
294
533
  parallel, a concurrent edit may shift indices — refresh via
295
- `get_etag` + `page_get_schema` between independent batches.
534
+ `get_etag` + `page_get_schema` (via gateway) between independent batches.
296
535
  - **Cloning a bound element keeps the binding.** `element_clone`
297
536
  copies the entire element including `props.source`. If the user
298
537
  wanted a "data-free" copy, call
299
- `yootheme_builder_element_unbind_source` on the new path
300
- afterwards.
538
+ `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", input: { ... } })`
539
+ on the new path afterwards.
301
540
  - **Wrong parameter names.** Use `template_id`, `element_path`,
302
541
  `etag` (NOT `pageId`, `srcPath`, `ifMatch`).
303
542
 
@@ -312,8 +551,7 @@ yootheme_builder_element_clone({
312
551
  })
313
552
  // Response: { path: "/0/children/3", etag: "def456" }
314
553
 
315
- // Step 7 — tweak the clone (replace props entirely).
316
- // First read the current shape so you can preserve siblings:
554
+ // Step 7 — tweak the clone (replace props entirely, or pass merge: true).
317
555
  const current = yootheme_builder_element_get({
318
556
  template_id: "home",
319
557
  element_path: "/0/children/3",
@@ -328,12 +566,13 @@ yootheme_builder_element_update_settings({
328
566
 
329
567
  **Edge case:** When cloning a Grid with a source binding, the binding
330
568
  is preserved (same `source_name`). If the user wants a "data-free"
331
- copy, follow up with `yootheme_builder_element_unbind_source` on the
332
- new path. Verify with `yootheme_builder_element_get_binding`.
569
+ copy, follow up with
570
+ `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", input: { ... } })`
571
+ on the new path. Verify with the gateway `element_get_binding` call.
333
572
 
334
573
  **Success criterion:** After publish,
335
- `yootheme_builder_page_get_schema({ template_id })` shows the new
336
- section at the cloned path with the user's tweaks reflected in
574
+ `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", input: { template_id } })`
575
+ shows the new section at the cloned path with the user's tweaks reflected in
337
576
  `element_get` on that path.
338
577
 
339
578
  ---
@@ -348,32 +587,39 @@ guessing — and without rotating the user's key unnecessarily.
348
587
  1. `yootheme_builder_diagnose` — single probe that hits `/health` (no
349
588
  auth) and then `/etag` (Bearer auth). Returns
350
589
  `{ plugin_reachable, plugin_version, yootheme_loaded, yootheme_version,
351
- endpoint_count, bearer_valid, bearer_error?, summary? }`. Call this
352
- **before** any other tool when you see auth errors. (Takes no
353
- arguments — the schema is `{}`.)
590
+ endpoint_count, bearer_valid, bearer_error?, site_url?, home_url?,
591
+ summary? }`. Call this **before** any other tool when you see
592
+ auth errors. (Takes no arguments — the schema is `{}`.)
354
593
  2. **Interpret the result:**
355
- - `plugin_reachable: false` → the WordPress install is down OR the
356
- plugin is deactivated. Send the user to wp-admin → Plugins →
357
- activate "YT Builder MCP". Do not retry until they confirm.
594
+ - `plugin_reachable: false` → the WordPress / Joomla install is down
595
+ OR the host plugin is deactivated. Send the user to **wp-admin →
596
+ Plugins → activate "YT Builder MCP"** (WordPress) or **Joomla
597
+ administrator → Extensions → Plugins → enable "System - YT Builder
598
+ MCP" and the matching webservices + component entries** (Joomla).
599
+ Do not retry until they confirm.
358
600
  - `plugin_reachable: true, bearer_valid: false` → the Bearer key is
359
601
  wrong (typo, revoked, or wrong key for this install). The
360
602
  `bearer_error` field carries the upstream HTTP status. Send the
361
- user to wp-admin → Tools → "YT Builder MCP" → Bearer Keys → either
362
- copy the existing key into their MCP client config, or generate
363
- a new one. Then they must restart the AI client.
603
+ user to:
604
+ - **WordPress:** wp-admin Tools "YT Builder MCP" Bearer Keys
605
+ copy the existing key into their MCP client config, or
606
+ generate a new one.
607
+ - **Joomla:** Components → YT Builder MCP → Bearer Keys → same.
364
608
  - `plugin_reachable: true, bearer_valid: true` but the original
365
609
  tool returned a 403 → the key works but the scope is too low for
366
610
  the tool's required scope (`write` for mutations, `admin` for
367
611
  destructive operations). Ask the user to regenerate the key with
368
612
  a higher scope and restart the AI client.
369
613
  3. **Walk the user through key rotation if needed:**
370
- - "Go to wp-admin → Tools → YT Builder MCP → Bearer Keys."
614
+ - WordPress: "wp-admin → Tools → YT Builder MCP → Bearer Keys."
615
+ Joomla: "Components → YT Builder MCP → Bearer Keys."
371
616
  - "Click 'Generate New Key', pick the scope (admin for full access)."
372
617
  - "Copy the key — it's shown ONCE; you cannot recover it later."
373
618
  - "Update your AI client config: replace `YTB_MCP_BEARER_TOKEN`
374
619
  with the new key. The fastest way is to re-run
375
620
  `npx -y @wootsup/yt-builder-mcp setup`."
376
- - "Restart Claude / Cursor / Zed / Continue / Cline / Roo Code."
621
+ - "Restart Claude / Cursor / Zed / Continue / Cline / Roo Code /
622
+ Claude Code / Codex CLI."
377
623
  - "Confirm with `yootheme_builder_diagnose` that
378
624
  `bearer_valid: true` before retrying the original task."
379
625
 
@@ -403,22 +649,25 @@ yootheme_builder_diagnose({})
403
649
  // Response shape:
404
650
  // {
405
651
  // plugin_reachable: true,
406
- // plugin_version: "0.1.0-alpha.1",
652
+ // plugin_version: "1.1.0",
407
653
  // yootheme_loaded: true,
408
654
  // yootheme_version: "5.0.22",
409
655
  // endpoint_count: 16,
410
656
  // bearer_valid: false, // ← key is bad
411
- // bearer_error: "HTTP 401: invalid_token"
657
+ // bearer_error: "HTTP 401: invalid_token",
658
+ // site_url: "https://example.com",
659
+ // home_url: "https://example.com"
412
660
  // }
413
- // → diagnosis: rotate the key. Send user to wp-adminSettings.
661
+ // → diagnosis: rotate the key. Send user to Tools/ComponentsYT Builder MCP.
414
662
  ```
415
663
 
416
664
  **Edge case:** `plugin_reachable: true` but `yootheme_loaded: false`
417
- — the user installed the MCP plugin but YOOtheme itself isn't
418
- active. The MCP server still answers, but every tool that touches
419
- the YOOtheme layout returns an empty/error response. Surface the
420
- mismatch ("YOOtheme is not active on this install") instead of
421
- retrying.
665
+ — the user installed the MCP host plugin but YOOtheme Pro itself
666
+ isn't active. The MCP server still answers, but every tool that
667
+ touches the YOOtheme layout returns an empty/error response. Surface
668
+ the mismatch ("YOOtheme Pro is not active on this install") instead
669
+ of retrying. On Joomla this can also surface as a "YOOtheme Pro
670
+ required" admin notice in the component dashboard.
422
671
 
423
672
  **Success criterion:** A subsequent `yootheme_builder_diagnose`
424
673
  returns `plugin_reachable: true` AND `bearer_valid: true`. The
@@ -454,10 +703,11 @@ an instance with a sensible default props payload.
454
703
  returns a structured `validation` error with a per-field issue
455
704
  list if anything is missing or malformed.
456
705
  7. (Optional) `yootheme_builder_element_update_settings({ template_id, element_path: "<new-path>", props: { ... }, etag })`
457
- — iterate on the props. **Note: this REPLACES `props` entirely**
458
- include every key you want to keep.
459
- 8. `yootheme_builder_page_save({ template_id, etag })` then
460
- `yootheme_builder_page_publish({ template_id, etag })`.
706
+ — iterate on the props. **Note: this REPLACES `props` entirely by
707
+ default; pass `merge: true` for a server-side deep-merge.** When
708
+ replacing, include every key you want to keep.
709
+ 8. `yootheme_builder_advanced({ tool: "yootheme_builder_page_save", input: { template_id, etag } })`
710
+ then `yootheme_builder_advanced({ tool: "yootheme_builder_page_publish", input: { template_id, etag } })`.
461
711
 
462
712
  **Common pitfalls:**
463
713
 
@@ -468,10 +718,11 @@ an instance with a sensible default props payload.
468
718
  (not `pageId`), `parent_path` (not `parentPath`), `element_type`
469
719
  (not `type` / `name`), `props` (not `settings`), `etag` (not
470
720
  `ifMatch`).
471
- - **`element_update_settings` is a full replace, not a merge.** Any
472
- key NOT in the request is REMOVED from `props`. Read the existing
473
- shape via `yootheme_builder_element_get` first if you only want
474
- to tweak a subset.
721
+ - **`element_update_settings` is a full replace by default.** Any
722
+ key NOT in the request is REMOVED from `props` unless you set
723
+ `merge: true`. Read the existing shape via
724
+ `yootheme_builder_element_get` first if you only want to tweak a
725
+ subset.
475
726
  - **Custom elements without a schema.** A poorly-built third-party
476
727
  element may not register a prop schema. In that case
477
728
  `yootheme_builder_element_type_get_schema` returns an empty/sparse
@@ -535,53 +786,69 @@ and the props payload you passed.
535
786
  destroying state when `confirm` is omitted. On hosts without
536
787
  elicitation, it returns a preview-with-confirm-required response;
537
788
  call again with `confirm: true`.
538
- - **Unbind a source**: use `yootheme_builder_element_unbind_source({
539
- template_id, element_path, etag, confirm: true })`. Same
540
- elicitation flow as delete.
789
+ - **Unbind a source**: call through the gateway:
790
+ `yootheme_builder_advanced({ tool: "yootheme_builder_element_unbind_source", input: { template_id, element_path, etag, confirm: true } })`.
791
+ Same elicitation flow as delete.
541
792
  - **Flat schema inspection** (e.g. enumerate every element path +
542
- type without fetching the whole nested tree): use
543
- `yootheme_builder_page_get_schema({ template_id })`.
793
+ type without fetching the whole nested tree): call through the
794
+ gateway: `yootheme_builder_advanced({ tool: "yootheme_builder_page_get_schema", input: { template_id } })`.
544
795
  - **Etag-only fetch** (e.g. polling for concurrent edits): use
545
796
  `yootheme_builder_get_etag()` (takes no arguments) — cheaper than
546
797
  fetching the full layout.
798
+ - **Find a public/front-end URL for a template** (404 test page,
799
+ homepage URL, etc.): call
800
+ `yootheme_builder_pages_list({ fields: ["id", "label", "frontend_url", "frontend_url_template", "frontend_url_description"] })`,
801
+ match by label, and read `frontend_url` (resolved) or
802
+ `frontend_url_template` (with placeholders the user fills in).
803
+ - **Find out which site / install you are connected to**: call
804
+ `yootheme_builder_health` (Bearer-authenticated payload includes
805
+ `site_url` + `home_url`) or `yootheme_builder_diagnose`.
806
+ - **Strip legacy `implode` directives** from an element binding (audit-clean
807
+ source props that pre-date the wrapper-source refactor): call through the
808
+ gateway:
809
+ `yootheme_builder_advanced({ tool: "yootheme_builder_clean_implode_directives", input: { template_id, element_path, etag } })`.
810
+ Returns the audit log + a fresh ETag; idempotent (`cleaned_count: 0` when
811
+ there is nothing to remove).
547
812
 
548
813
  If the user asks for something none of the above covers (e.g. global
549
814
  theme settings, menu management, media library), tell them clearly:
550
815
  "This MCP server only covers the YOOtheme Page Builder surface. For
551
- <X> you'll need <YOOtheme MCP / WP REST / direct wp-admin>." Don't
552
- fabricate tool calls.
816
+ <X> you'll need the relevant CMS REST API or direct wp-admin /
817
+ Joomla administrator access." Don't fabricate tool calls.
553
818
 
554
819
  ## Appendix: Tool Catalog (auto-generated)
555
820
 
556
821
  <!-- TOOL-CATALOG:BEGIN -->
557
822
 
558
- **24 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.
823
+ **26 catalogued tools** plus the `yootheme_builder_advanced` gateway = **27 reachable via `tools/list`** (17 L1 + 2 L3 + 1 gateway = 20 advertised; the gateway routes to 7 additional advanced tools, bringing the total to 27 callable). 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.
559
824
 
560
825
  | Tool | Kind | Input keys | Description |
561
826
  | --- | --- | --- | --- |
562
- | `yootheme_builder_clean_implode_directives` | idempotent | `element_path`, `etag`, `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. |
563
- | `yootheme_builder_diagnose` | read+idempotent | _(none)_ | Run a full diagnostic: hit /health (no auth), then attempt an authenticated call (/etag) to confirm the Bearer key is valid. Use when health passes but tools return 401/403. |
564
- | `yootheme_builder_element_add` | mutating | `children`, `element_type`, `etag`, `parent_path`, `props`, `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. |
565
- | `yootheme_builder_element_bind_source` | idempotent | `bindingLevel`, `element_path`, `etag`, `field_mappings`, `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. |
566
- | `yootheme_builder_element_clone` | mutating | `element_path`, `etag`, `template_id` | Clone an element as a sibling (same parent, immediately after the source). Returns the new element's path. Requires ETag. |
567
- | `yootheme_builder_element_delete` | destructive | `confirm`, `element_path`, `etag`, `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. |
568
- | `yootheme_builder_element_get` | read+idempotent | `element_path`, `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. |
569
- | `yootheme_builder_element_get_binding` | read+idempotent | `element_path`, `template_id` | Read the source binding (and source_config/source_args/etc.) attached to an element. Returns the empty object if the element is not bound. |
570
- | `yootheme_builder_element_list` | read+idempotent | `fields`, `template_id` | List all elements in a template as a flat array with JSON-Pointer paths + element types. Best starting-point for "find the element I want to edit". Pass `fields:["path","element_type"]` to narrow each row. |
571
- | `yootheme_builder_element_move` | idempotent | `element_path`, `etag`, `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. |
572
- | `yootheme_builder_element_type_get_schema` | read+idempotent | `type_name` | Get the prop/field schema for a single element type. Use the result to discover valid keys for `props` when calling yootheme_builder_element_add or _update_settings. |
573
- | `yootheme_builder_element_types_list` | read+idempotent | `fields` | 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. |
574
- | `yootheme_builder_element_unbind_source` | destructive | `confirm`, `element_path`, `etag`, `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. |
575
- | `yootheme_builder_element_update_settings` | idempotent | `element_path`, `etag`, `merge`, `props`, `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. |
576
- | `yootheme_builder_get_etag` | read+idempotent | _(none)_ | Get the current top-level state ETag. Pass this back via the `etag` parameter on any write tool to prevent overwriting concurrent edits. |
577
- | `yootheme_builder_health` | read+idempotent | _(none)_ | Check that the YT Builder MCP plugin is installed and reachable. Returns plugin version, YOOtheme Pro version (if loaded), and the list of available REST endpoints. Unauthenticated probe call this first when troubleshooting connectivity. |
578
- | `yootheme_builder_inspect_multi_items_binding` | read+idempotent | `element_path`, `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. |
579
- | `yootheme_builder_page_get_layout` | read+idempotent | `fields`, `flat`, `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. |
580
- | `yootheme_builder_page_get_schema` | read+idempotent | `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. |
581
- | `yootheme_builder_page_publish` | idempotent | `etag`, `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. |
582
- | `yootheme_builder_page_save` | idempotent | `etag`, `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). |
583
- | `yootheme_builder_pages_list` | read+idempotent | `fields` | List all YOOtheme templates ("pages") on the site. Returns id, label and usage metadata for each. Use this first to discover template IDs. Pass `fields:["id","label"]` to project per-item to a smaller shape. |
584
- | `yootheme_builder_sources_list` | read+idempotent | `fields` | 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. |
585
- | `yootheme_builder_template_summary` | read+idempotent | `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. |
827
+ | `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. |
828
+ | `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. |
829
+ | `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. |
830
+ | `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. |
831
+ | `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. |
832
+ | `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. |
833
+ | `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. |
834
+ | `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. |
835
+ | `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. |
836
+ | `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. |
837
+ | `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. |
838
+ | `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. |
839
+ | `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. |
840
+ | `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. |
841
+ | `yootheme_builder_get_etag` | read+idempotent | `site_id` | Get the current ETag (state revision) for the YOOtheme builder. Returns sha256+revision string used for optimistic locking on writes. Pass the returned value back as `etag` on any write tool (page_save, page_publish, element_add, element_update_settings, element_clone, element_move, element_delete). The server returns HTTP 412 if the ETag has changed since you read it. Keywords: get etag, current etag, state revision, optimistic lock, version stamp. Operates on the default site unless site_id is provided. |
842
+ | `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. |
843
+ | `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. |
844
+ | `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. |
845
+ | `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. |
846
+ | `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. |
847
+ | `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. |
848
+ | `yootheme_builder_pages_list` | read+idempotent | `fields`, `site_id` | List all pages, templates, and layouts in the YOOtheme Pro builder. Returns template_id, label, type, element count, and frontend_url per row. CALL THIS FIRST to discover available template IDs before any tool that needs a template_id (page_get_layout, element_list, page_get_schema, etc.). Keywords: list pages, list templates, list layouts, discover template_id, index, available templates, what pages exist. Pass `fields:["id","label"]` to slim. Returns ALL pages in one call (no pagination, typically <50 templates per site). Operates on the default site unless site_id is provided. |
849
+ | `yootheme_builder_sites_list` | read+idempotent | `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. (site_id is accepted for schema-uniformity but ignored by this tool.) |
850
+ | `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. |
851
+ | `yootheme_builder_sources_list` | read+idempotent | `fields`, `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. Pick a source name from the list and pass it to `element_bind_source`. Keywords: list sources, list data sources, list feeds, list bindings, dynamic content, available data, what sources exist. Pass `fields[]` to narrow each row. Operates on the default site unless site_id is provided. |
852
+ | `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. |
586
853
 
587
854
  <!-- TOOL-CATALOG:END -->