@hostinger/mcp 3.2.2 → 3.3.1

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 (65) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/gemini-extension.json +1 -1
  3. package/package.json +1 -1
  4. package/server.json +2 -2
  5. package/skills/headless/SKILL.md +245 -36
  6. package/skills/headless/connect/SKILL.md +98 -0
  7. package/skills/headless/{entry → connect}/bootstrap.mjs +8 -3
  8. package/skills/headless/connect/tools/antigravity.md +54 -0
  9. package/skills/headless/connect/tools/claude-code.md +46 -0
  10. package/skills/headless/connect/tools/codex.md +48 -0
  11. package/skills/headless/connect/tools/cursor.md +109 -0
  12. package/skills/headless/connect/tools/devin.md +50 -0
  13. package/skills/headless/connect/tools/vscode.md +57 -0
  14. package/skills/headless/report.schema.json +149 -0
  15. package/skills/headless/run-log.mjs +662 -0
  16. package/skills/headless/steps/01-check-state/SKILL.md +209 -0
  17. package/skills/headless/steps/02-refine/SKILL.md +273 -0
  18. package/skills/headless/steps/03-provision/SKILL.md +117 -0
  19. package/skills/headless/steps/03-provision/ecommerce/SKILL.md +142 -0
  20. package/skills/headless/steps/03-provision/webapp/SKILL.md +154 -0
  21. package/skills/headless/steps/03-provision/wordpress/SKILL.md +249 -0
  22. package/skills/headless/steps/04-content-model/SKILL.md +228 -0
  23. package/skills/headless/steps/04-content-model/ecommerce/SKILL.md +75 -0
  24. package/skills/headless/steps/04-content-model/wordpress/SKILL.md +223 -0
  25. package/skills/headless/steps/04-content-model/wordpress/woocommerce.md +153 -0
  26. package/skills/headless/steps/05-design/SKILL.md +464 -0
  27. package/skills/headless/steps/05-design/logo-kit.mjs +201 -0
  28. package/skills/headless/steps/06-frontend/SKILL.md +160 -0
  29. package/skills/headless/steps/07-validate/SKILL.md +56 -0
  30. package/skills/headless/steps/08-deploy/SKILL.md +179 -0
  31. package/skills/headless/steps/09-done/SKILL.md +157 -0
  32. package/src/core/skills.js +171 -24
  33. package/src/core/skills.ts +171 -24
  34. package/src/servers/agency-hosting.js +1 -1
  35. package/src/servers/agency-hosting.ts +1 -1
  36. package/src/servers/agent-readiness.js +1 -1
  37. package/src/servers/agent-readiness.ts +1 -1
  38. package/src/servers/all.js +1 -1
  39. package/src/servers/all.ts +1 -1
  40. package/src/servers/billing.js +1 -1
  41. package/src/servers/billing.ts +1 -1
  42. package/src/servers/dns.js +1 -1
  43. package/src/servers/dns.ts +1 -1
  44. package/src/servers/domains.js +1 -1
  45. package/src/servers/domains.ts +1 -1
  46. package/src/servers/ecommerce.js +1 -1
  47. package/src/servers/ecommerce.ts +1 -1
  48. package/src/servers/horizons.js +1 -1
  49. package/src/servers/horizons.ts +1 -1
  50. package/src/servers/hosting.js +1 -1
  51. package/src/servers/hosting.ts +1 -1
  52. package/src/servers/mail.js +1 -1
  53. package/src/servers/mail.ts +1 -1
  54. package/src/servers/reach.js +1 -1
  55. package/src/servers/reach.ts +1 -1
  56. package/src/servers/vps.js +1 -1
  57. package/src/servers/vps.ts +1 -1
  58. package/src/servers/wordpress.js +1 -1
  59. package/src/servers/wordpress.ts +1 -1
  60. package/skills/headless/entry/skill.md +0 -79
  61. package/skills/headless/references/DATABASE.md +0 -49
  62. package/skills/headless/references/DEPLOYMENT.md +0 -34
  63. package/skills/headless/references/SETUP.md +0 -35
  64. package/skills/headless/references/STORE.md +0 -52
  65. package/skills/headless/references/WORDPRESS.md +0 -51
@@ -0,0 +1,142 @@
1
+ ---
2
+ name: headless-step-provision-ecommerce
3
+ description: "Provision a Hostinger Ecommerce backend for the Hostinger Headless flow: ensure the hostinger-ecommerce MCP is connected, create the headless website (frontend host) and a Hostinger Ecommerce STORE with a custom (headless) sales channel, set shipping + payment, and record it all in site.json."
4
+ ---
5
+
6
+ # headless-step-provision-ecommerce
7
+
8
+ Provision a **Hostinger Ecommerce** backend for `<domain>` and prove it is ready. Two distinct things:
9
+
10
+ 1. A **headless website** (`type: "headless_ecommerce"`) — this is only the **hosting slot for the
11
+ storefront you'll build**. It installs **no store engine** (no `/cms`, no API on the domain).
12
+ 2. A **Hostinger Ecommerce store** — a separate object created through the **Ecommerce API**, with a
13
+ **custom (headless) sales channel**. The storefront reads this store's **public Storefront Core V2
14
+ API**, keyed on the sales channel. This is the real backend.
15
+
16
+ **Out:** `site.json` (store id, custom sales channel id, URLs).
17
+
18
+ > ## ⚠️ Prerequisite: the `hostinger-ecommerce` MCP server must be connected
19
+ > Store management is **not** in the `hosting`/`wordpress` MCP groups — it's the separate
20
+ > **`hostinger-ecommerce`** group (`ecommerce_*` ops, OAuth-backed, no API token needed). If those
21
+ > tools aren't available, add the server and **reconnect the session** before continuing:
22
+ > ```bash
23
+ > claude mcp add-json hostinger-ecommerce '{"command":"npx","args":["--package=@hostinger/mcp@latest","hostinger-ecommerce-mcp"],"env":{"USER_AGENT":"extension;claude-code;via-cursor;1.4.2"}}' --scope user
24
+ > ```
25
+ > (For other tools, enable the **Ecommerce** MCP in the Hostinger Connector.) A newly added MCP server
26
+ > only loads on a session restart/reconnect.
27
+ >
28
+ > **Can't restart your own session? Don't block — drive the server directly.** A coding agent usually
29
+ > can't reconnect itself, so instead of stopping, run the stdio server yourself and speak MCP to it:
30
+ > spawn `npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp` (same `USER_AGENT` as above) and
31
+ > send newline-delimited JSON-RPC over its stdin/stdout — `initialize` → the `notifications/initialized`
32
+ > notification → `tools/list` → `tools/call`, where the tools are `search` / `execute` / `multi-execute`
33
+ > (call ops exactly as in-session: `{ "name": "execute", "arguments": { "operation": "…", "params": {…} } }`).
34
+ > It self-authenticates with the same stored Hostinger OAuth creds as the other connected servers, so
35
+ > there is **no token to handle** (keep any secret off the command line). Your sandbox may ask the user
36
+ > to approve spawning it — ask once, then proceed. Only fall back to asking the user to restart if
37
+ > driving the server directly isn't permitted.
38
+ >
39
+ > Verify the tools work (in-session or via the direct drive) with a search — e.g. "list stores" →
40
+ > `ecommerce_stores_list` — before proceeding.
41
+ > The authoritative, always-current build guide is `ecommerce_miscellaneous_custom-storefront-setup-instructions` — read it once per build.
42
+
43
+ All website-lifecycle calls go through the `hosting` connector (`execute`); all store calls through the
44
+ `hostinger-ecommerce` MCP.
45
+
46
+ ## Step 1 — The headless website (frontend host)
47
+
48
+ Same lifecycle as `../wordpress/SKILL.md` Steps 1–2, but `type: "headless_ecommerce"`:
49
+
50
+ 1. Detect: `hosting_websites_list` / `hosting_websites_list-setups` (exact-match the domain). `completed`
51
+ → adopt. `running` → poll. `failed` → stop, point at hPanel. If `website_type` is not `headless`
52
+ (plain WordPress, Node.js, …), stop and tell the user — don't overwrite another kind of site.
53
+ 2. Create (only if **no** site exists): `hosting_websites_start-setup { order_id, type: "headless_ecommerce", domain }`
54
+ — **omit `domain`** for a temporary subdomain: the setup generates one itself (don't call a separate
55
+ subdomain op first — passing a generated free subdomain back as `domain` 422s). The response carries
56
+ the generated domain; if it differs from the workspace dir, move `~/.hostinger/<old>/` → `<new>/` and
57
+ update `brief.md`. Poll `list-setups` to `completed`. `website_type` will be `headless`.
58
+
59
+ > **Onboarding handoff — an empty headless shell needs no new website.** If the domain already has an
60
+ > empty headless shell (`website_type: headless`, setup `type: headless_empty`, nothing installed —
61
+ > typically created in onboarding), **adopt it as the storefront host and skip the create.** A
62
+ > `headless_ecommerce` website installs no store engine either, so an empty headless shell *is* the host
63
+ > you need — there is no backend to "swap" on the website side. Record its `username`/`order_id` and go
64
+ > straight to Step 2 (store + custom sales channel). Never call `start-setup` on a domain that already
65
+ > has a site.
66
+
67
+ > Don't expect `/cms` or any API on this domain — it serves the storefront you build (Step 6) and
68
+ > deploy (Step 8).
69
+
70
+ ## Step 2 — Resolve or create the store + custom sales channel
71
+
72
+ `ecommerce_stores_list` → reuse an existing store if this build already made one; otherwise create:
73
+
74
+ ```json
75
+ { "operation": "ecommerce_stores_create",
76
+ "params": { "name": "<store name>", "country_code": "US", "company_email": "<email>",
77
+ "company_name": "<store name>", "language": "en",
78
+ "sales_channel": { "type": "custom", "external_id": "<slug>-headless" } } }
79
+ ```
80
+
81
+ This returns `store.id` (`store_*`) **and** a custom `sales_channel.id` (`scha_*`) in one call — the
82
+ custom channel is what the headless storefront is keyed on. (You can also add one later with
83
+ `ecommerce_sales-channels_create { store_id, type:"custom", name }`.) Currency comes from
84
+ `country_code` (`default_currency_code`); all prices are **minor units** (cents).
85
+
86
+ ## Step 3 — Store readiness: shipping + payment
87
+
88
+ A store needs ≥1 shipping zone and ≥1 payment method to check out.
89
+
90
+ - **Shipping:** `ecommerce_shipping_set-store { store_id, price }` (minor units; 0 = free).
91
+ - **Payment:** `ecommerce_payments_enable-manual-method { store_id, title }`.
92
+
93
+ > **Known gate — free accounts have no payment method, and therefore no checkout.** On a **free
94
+ > account** (and any account/store without a paid Hostinger Ecommerce plan), `enable-manual-method`
95
+ > returns **HTTP 402**, and checkout later fails with `400 payment_provider_not_configured`. This is
96
+ > **expected, not an error** — payment, and therefore checkout, is simply unavailable on the plan. The
97
+ > catalog and the storefront still work fully. **Never block, stop, loop or retry the build on this:**
98
+ > - Record `payment_enabled: false` in `site.json` (set `shipping_enabled` as usual).
99
+ > - Tell the user in one line that checkout needs a paid Ecommerce plan, and carry straight on.
100
+ > - Treat the 402 as a normal outcome — don't surface it as a failure or re-attempt the call.
101
+ > - Never ask the user for payment credentials.
102
+ >
103
+ > Every downstream stage reads `site.json → ecommerce.payment_enabled` and, when it is `false`, skips
104
+ > all checkout wiring and verification (see content-model Step 5, frontend, deploy). A store with
105
+ > `payment_enabled: false` is a **complete, valid build** — a browsable catalog without checkout.
106
+
107
+ ## Step 4 — Verify
108
+
109
+ - `ecommerce_stores_metadata { store_id }` → `has_shipping` true; `has_payment_methods` (may be false,
110
+ see the gate); read `default_currency` (`decimal_digits`, `template`) here, don't guess.
111
+ - Public Storefront V2 answers for the channel (no auth):
112
+ `GET https://api-ecommerce.hostinger.com/v2/channels/<scha_*>/products?limit=100` → 200 (empty until
113
+ content-model seeds products — fine here).
114
+ - `GET https://<domain>/` returns the Hostinger default page until the storefront is deployed — expected.
115
+
116
+ ## Step 5 — Record `site.json`
117
+
118
+ ```json
119
+ {
120
+ "backend": "ecommerce",
121
+ "website_type": "headless",
122
+ "order_id": 1007223428,
123
+ "username": "u…",
124
+ "ecommerce": {
125
+ "store_id": "store_…",
126
+ "sales_channel": { "id": "scha_…", "type": "custom", "external_id": "…-headless" },
127
+ "currency": "usd",
128
+ "payment_enabled": false,
129
+ "shipping_enabled": true
130
+ },
131
+ "urls": {
132
+ "site": "https://<domain>",
133
+ "storefront_api": "https://api-ecommerce.hostinger.com/v2/channels/<scha_*>",
134
+ "store_admin": "https://ecommerce.hostinger.com/store/<store_*>"
135
+ },
136
+ "provisioned_at": "…",
137
+ "verified_at": "…"
138
+ }
139
+ ```
140
+
141
+ No secrets in `site.json`. After deploy, link the channel to the live domain with
142
+ `ecommerce_sales-channels_update { store_id, sales_channel_id, url }` (done in the deploy stage).
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: headless-step-provision-webapp
3
+ description: "Provision a plain Node.js app host for the Hostinger Headless flow: create an empty website (no WordPress, no store engine, no /cms), optionally a MySQL database, then record it in site.json. For any webapp / SPA / app that brings its own code and data."
4
+ ---
5
+
6
+ # headless-step-provision-webapp
7
+
8
+ Provision a bare **Node.js app host** for `<domain>` and prove it is ready. This is the backend for
9
+ **any webapp, SPA or app** that brings its own code (and, if it needs one, its own database) — there is
10
+ no CMS and no store engine here. The site is an **empty website**: just the hosting slot that the
11
+ frontend step builds into and the deploy step ships to as a Node.js application. **Out:** `site.json`.
12
+
13
+ > **Not WordPress, not Ecommerce.** Never provision `headless_wordpress` / `headless_ecommerce` for a
14
+ > webapp, and never install WordPress at `/cms`. A webapp owns its own routes, data and rendering.
15
+
16
+ All website/database operations below go through the Hostinger connector (`execute`).
17
+
18
+ > **Fire vs join (background provision).** Steps 1–2 up to `hosting_websites_start-setup` are the
19
+ > **fire** half: once the setup is queued, record `provision.in_progress` and return to do prepare work
20
+ > (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2, plus Steps 3–5, are the
21
+ > **join** half — run them when an instance-barrier step needs the backend (or immediately, if the
22
+ > agent can't interleave). An already-`completed` site detected in Step 1 means provision is done:
23
+ > record `provision.completed` and adopt it.
24
+
25
+ ## Step 1 — Detect before you create (never provision twice)
26
+
27
+ 1. `hosting_websites_list { "domain": "<domain>" }` — substring filter, so **exact-match** the row's
28
+ `domain`. If a row exists, note `username`, `order_id`, `website_type`.
29
+ 2. `hosting_websites_list-setups { "domain": "<domain>" }` — exact-match the row:
30
+ - `completed` → already provisioned → skip to Step 3 (adopt it).
31
+ - `running` → skip to Step 2 (poll).
32
+ - `failed` → tell the user and point them at hPanel; do **not** call start-setup again on your own.
33
+ 3. If the site exists but is a different kind (`website_type` is `headless`, or a WordPress/store site),
34
+ stop and tell the user — this domain already hosts something else.
35
+
36
+ ## Step 2 — Create (only when nothing exists)
37
+
38
+ 1. Pick the plan: `hosting_orders_list { "statuses": ["active"] }`. One active order → use it; several
39
+ → ask the user which.
40
+ 2. Start the setup as an **empty website** — **omit `type`** (no WordPress, no store):
41
+
42
+ ```json
43
+ { "operation": "hosting_websites_start-setup",
44
+ "params": { "order_id": <order_id>, "domain": "<domain>" } }
45
+ ```
46
+
47
+ `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,
48
+ **omit `domain`** — the setup generates one itself. Don't call a separate subdomain op to make one
49
+ first: there's no need, and passing a generated free subdomain back as `domain` fails with
50
+ `422 … Free subdomains cannot be provided`. The response then carries the generated domain: if it
51
+ differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,
52
+ update the domain in `brief.md`, and tell the user the new domain.
53
+ 3. Poll `hosting_websites_list-setups { "domain": "<domain>" }` every ~15 s until `completed`
54
+ (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.
55
+
56
+ ## Step 3 — Database (only if the app needs one)
57
+
58
+ Skip this step entirely for an app with no database (static SPA, an app that talks to an external API).
59
+ This is **join-half** work — it needs the live website, so run it once `provision.status == "completed"`.
60
+
61
+ **Preferred — let the platform create the database and inject the credentials:**
62
+ `hosting_databases_setup-website { username, domain }` creates a MySQL database, **generates the
63
+ password, writes `DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USER` / `DB_PASSWORD` / `DATABASE_URL` into the
64
+ website's environment variables, and restarts the app — in one call.** The password is never returned,
65
+ so there is **nothing to persist and nothing to lose across sessions** — the app reads it from the
66
+ environment. (It 422s if any of those env vars already exist; clear them first with
67
+ `hosting_nodejs_replace-environment-variables`. Frameworks that bake vars into the build — Next.js,
68
+ `NEXT_PUBLIC_*` — only pick up the values on the deploy build.)
69
+
70
+ > **⚠️ A later `hosting_nodejs_replace-environment-variables` wipes these credentials.** That call is a
71
+ > **full replace**, and when you list the current vars first the `DB_PASSWORD` (and `DATABASE_URL`) read
72
+ > back **masked** (`********`) — the generated password is unknown, so you cannot resend it and the DB
73
+ > env is lost. One rule, pick one:
74
+ > - **(a) Set every other app env var first, and call `setup-website` last** — then nothing has to
75
+ > replace the env again.
76
+ > - **(b) If a later replace is unavoidable**, reset the password with
77
+ > `hosting_databases_change-password { username, name, password }` and resend **all** of `DB_HOST`,
78
+ > `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD` and `DATABASE_URL` with the new password in that same
79
+ > full replace.
80
+
81
+ **Manual alternative** — only when you need a specific name/user or finer control:
82
+
83
+ 1. `hosting_databases_create` with a name, a user, and a password you generate — name and user are
84
+ prefixed with the account username automatically. Read the full, prefixed `name`/`user` back from
85
+ `hosting_databases_list`.
86
+ 2. **Which host the app connects to:** the app runs **on the website**, so Node.js connects to
87
+ **`127.0.0.1:3306`** — never `localhost` (it can resolve to IPv6 `::1`, and not every database user
88
+ has that grant), and never the external `host` from `hosting_databases_list` (`srvNNNN.hstgr.io`,
89
+ which has no grant for connections from the server itself).
90
+ 3. **Set the env vars now, while you still hold the generated password.** The password is **not persisted
91
+ anywhere** (never in `site.json`), so set `DB_HOST=127.0.0.1`, `DB_PORT=3306`, `DB_NAME`, `DB_USER`,
92
+ `DB_PASSWORD`, plus `DATABASE_URL=mysql://USER:PASSWORD@127.0.0.1:3306/NAME` (URL-encode USER/PASSWORD)
93
+ right here with `hosting_nodejs_replace-environment-variables` (a **full replace** — list current keys
94
+ first; values come back masked; send the complete set with real values). Saving restarts the process;
95
+ build-time vars need a fresh deploy (`hosting_deploy`) afterwards. **Don't defer this to deploy:** on
96
+ a **cross-session resume** the generated password is gone, so you must **reset it** with
97
+ `hosting_databases_change-password { username, name, password }` and set the env vars from the new one.
98
+ 4. **Never** put the password in source, a committed `.env`, client-side JavaScript, or a chat reply;
99
+ never print it once stored.
100
+
101
+ ## Step 4 — Verify
102
+
103
+ | Check | How | Expect |
104
+ |---|---|---|
105
+ | Website exists | `hosting_websites_list` exact row | the row is present, `username` known |
106
+ | Setup finished | `hosting_websites_list-setups` exact row | `status: completed` |
107
+ | Database reachable *(only if created)* | `hosting_databases_list { username }` | the prefixed db + user are listed |
108
+
109
+ `GET https://<domain>/` returns the Hostinger default page (or 403) until the frontend is deployed —
110
+ expected, not a failure. There is **no `/cms` and no API** on this domain; the app itself serves every
111
+ route once it is deployed (Step 8).
112
+
113
+ If a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.
114
+
115
+ ## Step 5 — Record `site.json`
116
+
117
+ ```json
118
+ {
119
+ "backend": "webapp",
120
+ "website_type": "empty",
121
+ "order_id": 1007223428,
122
+ "username": "u…",
123
+ "runtime": "nodejs",
124
+ "database": {
125
+ "enabled": false,
126
+ "engine": "mysql",
127
+ "name": "u…_appdb",
128
+ "user": "u…_app",
129
+ "host": "127.0.0.1",
130
+ "port": 3306
131
+ },
132
+ "urls": {
133
+ "site": "https://<domain>"
134
+ },
135
+ "provisioned_at": "…",
136
+ "verified_at": "…"
137
+ }
138
+ ```
139
+
140
+ No passwords or tokens in `site.json` — only the non-secret connection shape (host/port/name/user). Set
141
+ `"database": { "enabled": false }` (and drop the rest) when the app needs no database.
142
+
143
+ ## Deploy & content model (what happens next)
144
+
145
+ - **Content model is skipped for a webapp** — there is no CMS or store to seed; the app owns its own
146
+ data. The content-model router (`../../04-content-model/SKILL.md`) passes straight through to the
147
+ frontend step for this backend.
148
+ - **Deploy** is a standard Node.js application deploy, handled by `../../08-deploy/SKILL.md`: it archives
149
+ the **source** (not the build output; exclude `node_modules/`, `.next/`, `dist/`, build output and
150
+ `.git/`), then deploys with `hosting_deploy` (`type: "nodejs"`, upload + server-side `npm install` +
151
+ build, one shot) passing the framework's `appType` + `outputDirectory` inline, and polls
152
+ `hosting_get-deployment` until the build completes. Any supported `appType` works (Next.js, Nuxt,
153
+ Express, NestJS, Fastify, Vite/React/Vue SPA, …). There is no `/cms` to preserve here, so the whole
154
+ root is the app.
@@ -0,0 +1,249 @@
1
+ ---
2
+ name: headless-step-provision-wordpress
3
+ description: "Provision a headless WordPress backend for the Hostinger Headless flow: create the headless website with WordPress at /cms, verify and self-heal, then record it in site.json."
4
+ ---
5
+
6
+ # headless-step-provision-wordpress
7
+
8
+ Create the headless WordPress backend for `<domain>` and prove it is ready. One platform call lays
9
+ every headless site out identically: WordPress in `public_html/cms`, Hostinger AI Assistant + MCP
10
+ adapter, site marked `headless`. **Out:** `site.json`.
11
+
12
+ > **WordPress + WooCommerce?** This same recipe serves it. When the brief's **Suggested backend** is
13
+ > `**WordPress + WooCommerce**`, provision the WordPress site exactly as below, then run the **optional
14
+ > WooCommerce tail in Step 5** (install + activate the `woocommerce` plugin and verify the store
15
+ > endpoints; the catalog is seeded in content-model via the per-site WP MCP).
16
+ > WooCommerce is a plugin on this WordPress install, **not** a separate backend: `site.json → backend`
17
+ > stays `"wordpress"` and the store is recorded under `site.json → woocommerce`. For a plain WordPress
18
+ > brief, skip Step 5 entirely.
19
+
20
+ All operations below go through the Hostinger connector (`execute`).
21
+
22
+ > **Fire vs join (background provision).** Step 1, plus the queuing call — `hosting_websites_start-setup`
23
+ > for a fresh site (Step 2) **or** `wordpress_installations_install` to swap an empty headless shell
24
+ > (Step 2b) — are the **fire** half: once the job is queued, record `provision.in_progress` and return
25
+ > to do prepare work (see the router `../SKILL.md`). The readiness **poll** at the end of Step 2 / 2b,
26
+ > plus Steps 3–6, are the **join** half — run them when an instance-barrier step needs the backend (or
27
+ > immediately, if the agent can't interleave). An already-`completed` site with WordPress at `/cms`
28
+ > detected in Step 1 means provision is already done: record `provision.completed` and adopt it.
29
+
30
+ ## Step 1 — Detect before you create (never provision twice)
31
+
32
+ 1. `hosting_websites_list { "domain": "<domain>" }` — substring filter, so **exact-match** the row's
33
+ `domain`. If a row exists, note `username`, `order_id`, `website_type`.
34
+ 2. **No exact row** → nothing exists on this domain → **create fresh** (Step 2).
35
+ 3. **A row exists.** If `website_type` is **not** `headless`, stop and tell the user — this domain
36
+ already hosts a different kind of site (plain WordPress, Node.js, builder, …); never overwrite it.
37
+ If it **is** `headless`, work out whether WordPress is already installed or the shell is still empty
38
+ (see the router's *Onboarding handoff*): check `hosting_websites_list-setups { "domain": "<domain>" }`
39
+ (exact row) and `wordpress_installations_list { username, domain, ownership: "all" }` (exact row):
40
+ - Setup `running` → provisioning is underway → skip to Step 2's poll.
41
+ - Setup `failed` → tell the user and point them at hPanel; do **not** call start-setup again yourself.
42
+ - A WordPress install with `directory: "cms"` already present (or setup `type: headless_wordpress`)
43
+ → **already provisioned** → skip to Step 3 (adopt it).
44
+ - A `headless` site with **no** `/cms` install (an **empty headless shell** — setup
45
+ `type: headless_empty`, typically from onboarding) → **swap it to WordPress** (Step 2b). Do **not**
46
+ call `start-setup` again — the website already exists; you only install WordPress into it.
47
+
48
+ ## Step 2 — Create a fresh headless WordPress site (only when no site exists)
49
+
50
+ 1. Pick the plan: `hosting_orders_list { "statuses": ["active"] }`. One active order → use it; several
51
+ → ask the user which.
52
+ 2. Start the setup:
53
+
54
+ ```json
55
+ { "operation": "hosting_websites_start-setup",
56
+ "params": { "order_id": <order_id>, "type": "headless_wordpress", "domain": "<domain>" } }
57
+ ```
58
+
59
+ `domain` must be a **customer-owned** domain. For a temporary `*.hostingersite.com` subdomain,
60
+ **omit `domain`** — the setup generates one itself. Don't call a separate subdomain op to make one
61
+ first: there's no need, and passing a generated free subdomain back as `domain` fails with
62
+ `422 … Free subdomains cannot be provided`. The response then carries the generated domain:
63
+ if it differs from the workspace directory name, move `~/.hostinger/<old>/` to `~/.hostinger/<new>/`,
64
+ update the domain in `brief.md`, and tell the user the new domain.
65
+ Admin credentials are generated by the platform; never ask the user for a password.
66
+ 3. Poll `hosting_websites_list-setups { "domain": "<domain>" }` every ~15 s until `completed`
67
+ (typically under a minute; allow a few minutes). `failed` → stop, point at hPanel.
68
+
69
+ ## Step 2b — Swap an empty headless shell to WordPress (only when onboarding already made the shell)
70
+
71
+ The website already exists as an empty `headless` shell, so there is **no `start-setup`** here — you
72
+ install WordPress straight into its `/cms` directory. This is the onboarding handoff path.
73
+
74
+ 1. Use the shell's `username` and `order_id` from Step 1's row — no new order pick, no new website.
75
+ 2. Install WordPress into `/cms`, flagged headless:
76
+
77
+ ```json
78
+ { "operation": "wordpress_installations_install",
79
+ "params": { "username": "<username>", "domain": "<domain>", "type": "headless",
80
+ "site_title": "<site title from brief.md>",
81
+ "credentials": { "login": "<admin login>", "email": "<account email>",
82
+ "password": "<strong generated password>" } } }
83
+ ```
84
+
85
+ - `type: "headless"` installs into `/cms` with the Hostinger headless MU plugin and marks the site
86
+ headless-WordPress; **`directory` is not allowed with it** (it is always `cms`). The target must
87
+ already be `headless` — the headless install rewrites the root `.htaccess`, so the API rejects it
88
+ with `422` on any non-headless site (which is why Step 1 confirms `website_type: headless` first).
89
+ - **You generate the admin password** (strong, 8–50 chars, mixed case + a digit) — never ask the user
90
+ for one and never print it. The user signs in later through a one-click login link (Step 6), so the
91
+ password only has to exist, not be remembered.
92
+ - A new database is created for the install unless you pass `database.name` of an existing one; the
93
+ generated database counts against the plan's limit.
94
+ 3. The call is **asynchronous** — a success response only means the install job was **queued**. This is
95
+ the *fire*: record `provision.in_progress` and return to prepare work. Poll
96
+ `wordpress_installations_list { username, domain, ownership: "all" }` (exact row) at the join; when a
97
+ row with `directory: "cms"` and `is_valid: true` appears, WordPress is ready. Allow 1–2 minutes;
98
+ `wordpress_installations_list` can lag a few seconds behind the job finishing and return `[]` — retry
99
+ briefly before failing.
100
+
101
+ ## Step 3 — Verify (all must pass)
102
+
103
+ | Check | How | Expect |
104
+ |---|---|---|
105
+ | Site is headless | `hosting_websites_list` exact row | `website_type: "headless"` |
106
+ | WordPress at `/cms` | `wordpress_installations_list { "username": "<username>", "ownership": "all" }`, exact-match `domain` | row with `directory: "cms"`, `is_valid: true`; note its `id` |
107
+ | REST API answers | `GET https://<domain>/cms/wp-json/wp/v2/posts?per_page=1` | 200, JSON array |
108
+ | AI Assistant + MCP loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `hostinger-ai-assistant/v1` and `mcp` |
109
+ | JWT mintable | `wordpress_installations_jwt-token { username, software: <id> }` | returns `token`, `mcp_url` |
110
+
111
+ Notes:
112
+
113
+ - `wordpress_installations_list` can lag a few seconds behind `completed` and return `[]` — retry
114
+ briefly before failing. Its `domain` filter is a substring match, so exact-match the row.
115
+ - The install record's `url` may say `http://`; always use `https://` URLs.
116
+ - `GET https://<domain>/` returns 403 until the frontend is deployed — expected, not a failure.
117
+ - **Never print, log or store the JWT.** It is minted on demand (24 h lifetime) by whichever stage
118
+ needs the per-site WP MCP; only `mcp_url` is recorded.
119
+
120
+ If a check fails: retry once after ~30 s; if it still fails, stop and point the user at hPanel.
121
+
122
+ ## Step 4 — SEO plugin (Yoast) *(every WordPress site — on by default)*
123
+
124
+ Every headless WordPress site gets **Yoast SEO** (`wordpress-seo`) installed and activated by default,
125
+ so per-page title/description/Open Graph/JSON-LD come from a maintained plugin's `yoast_head_json`
126
+ rather than being hand-generated from each item's content by the frontend. This is **join-half** work
127
+ (needs the live `/cms` install), so run it once `provision.status == "completed"`, the same way as the
128
+ WooCommerce tail. Tell the user in one line that SEO (Yoast) is being set up; if they explicitly don't
129
+ want it, skip this step and record `seo.enabled: false` — but the default is to install it.
130
+
131
+ 1. **Install + activate Yoast** (management MCP, async — poll, never blind-retry):
132
+ - `wordpress_plugins_install { username, software: <install_id>, plugins: ["wordpress-seo"] }`
133
+ - Poll `wordpress_plugins_list-installed { username, software }` until `wordpress-seo` shows
134
+ `active` (install + activate can complete together), or activate it with
135
+ `wordpress_plugins_activate { username, software, plugin: "wordpress-seo" }`.
136
+ - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job
137
+ succeeded**. Treat a 500 as "verify, don't assume": poll `list-installed` before retrying, and
138
+ never fire a second install blindly.
139
+ 2. **Verify** `GET https://<domain>/cms/wp-json/` → `namespaces` once more, and that a sample post/page
140
+ response carries `yoast_head_json`. The per-item Yoast meta (focus title/description per post) is set
141
+ later in the **content-model push** — provision only installs + activates the plugin here. Don't fail
142
+ provision over SEO: if the install can't be confirmed after one retry, record `seo.enabled: false`,
143
+ tell the user, and let the frontend fall back to generating tags from content.
144
+
145
+ ## Step 5 — WooCommerce store *(only for a `WordPress + WooCommerce` brief — skip otherwise)*
146
+
147
+ Run this only when the brief's **Suggested backend** is `**WordPress + WooCommerce**`. It turns the
148
+ headless WordPress site into a headless **store**: WooCommerce is the `woocommerce` plugin on this same
149
+ `/cms` install — **not a separate backend**. This is **join-half** work (needs the live instance), so run
150
+ it once `provision.status == "completed"`. The authoritative data-layer reference is
151
+ [`../../04-content-model/wordpress/woocommerce.md`](../../04-content-model/wordpress/woocommerce.md).
152
+
153
+ > **No custom plugin — the catalog is driven through the per-site WP MCP.** A custom plugin can't be
154
+ > installed into a headless `/cms` from a coding agent, so there is **no seed/config plugin and no REST
155
+ > consumer key in this flow.** Instead, the Hostinger AI Assistant (the per-site WP MCP, same JWT session
156
+ > content-model uses) exposes **native WooCommerce abilities** — `wc-products-*`, `wc-product-categories-*`,
157
+ > `wc-coupons-*`, `wc-orders-*`, `media-upload-from-url`, `wp-settings-*`. Provision just **installs
158
+ > WooCommerce and verifies the store endpoints**; the catalog + coupons are seeded in the content-model
159
+ > push through those abilities (see the cookbook).
160
+
161
+ 1. **Install + activate WooCommerce** (management MCP, async — poll, never blind-retry):
162
+ - `wordpress_plugins_install { username, software: <install_id>, plugins: ["woocommerce"] }`
163
+ - Poll `wordpress_plugins_list-installed { username, software }` until `woocommerce` shows `active`
164
+ (install + activate can complete together), or activate it with
165
+ `wordpress_plugins_activate { username, software, plugin: "woocommerce" }`.
166
+ - Confirm with `wordpress_plugins_check-if-woo-commerce-is-installed { domain: "<domain>" }`. Allow a
167
+ moment for WooCommerce's installer to create its tables and the Shop/Cart/Checkout pages.
168
+ - **Known quirk:** the `hostinger-wordpress` MCP sometimes returns **HTTP 500 even though the job
169
+ succeeded** (seen on both `plugins_install` and `jwt-token`). Treat a 500 as "verify, don't assume":
170
+ poll `list-installed` / re-check before retrying, and never fire a second install blindly.
171
+ 2. **Verify the store endpoints (public, no auth):**
172
+
173
+ | Check | How | Expect |
174
+ |---|---|---|
175
+ | WooCommerce active | `wordpress_plugins_check-if-woo-commerce-is-installed { domain }` | installed + active |
176
+ | Store API answers | `GET https://<domain>/cms/wp-json/wc/store/v1/products?per_page=1` | 200 (empty until content-model seeds — fine) |
177
+ | Both namespaces loaded | `GET https://<domain>/cms/wp-json/` → `namespaces` | contains `wc/store/v1` **and** `wc/v3` |
178
+
179
+ Pretty permalinks are on by default on a headless install, so the Store API resolves immediately; if a
180
+ store route ever 404s, the permalink structure is the thing to check.
181
+
182
+ > **Checkout needs owner setup — treat it like the Ecommerce payment gate.** A fresh WooCommerce install
183
+ > has **no payment gateway and no shipping zone enabled**, and that configuration is **not reachable from
184
+ > the headless tooling** (there is no payment/shipping MCP ability, and `wp-settings-update` only writes
185
+ > standard WP settings). So **checkout is unavailable until the owner enables a payment method + shipping
186
+ > in wp-admin** (hand them the one-click login in Step 6). Record `woocommerce.payment_enabled: false`
187
+ > accordingly. This is **not a build failure**: the catalog, product pages, client-side cart and coupon
188
+ > application all work headlessly — the downstream frontend renders a graceful disabled-checkout state and
189
+ > never calls `/checkout` while `payment_enabled` is `false` (see content-model + frontend). Never ask the
190
+ > user for payment credentials. Store currency and address (`woocommerce_*` options) likewise aren't
191
+ > settable headlessly — they follow the install default and the owner can change them in wp-admin.
192
+
193
+ ## Step 6 — Record `site.json`
194
+
195
+ ```json
196
+ {
197
+ "backend": "wordpress",
198
+ "website_type": "headless",
199
+ "order_id": 1007223428,
200
+ "username": "u…",
201
+ "wordpress": {
202
+ "install_id": "30…",
203
+ "directory": "cms",
204
+ "admin_login": "…"
205
+ },
206
+ "seo": {
207
+ "plugin": "wordpress-seo",
208
+ "enabled": true
209
+ },
210
+ "urls": {
211
+ "site": "https://<domain>",
212
+ "cms": "https://<domain>/cms",
213
+ "wp_admin": "https://<domain>/cms/wp-admin",
214
+ "rest_api": "https://<domain>/cms/wp-json/wp/v2",
215
+ "wp_mcp": "<mcp_url from the JWT call>"
216
+ },
217
+ "provisioned_at": "…",
218
+ "verified_at": "…"
219
+ }
220
+ ```
221
+
222
+ No passwords or tokens in `site.json`. When the user needs wp-admin, mint a one-click link with
223
+ `wordpress_login_create-links { username, software: <install_id> }` — never hand over a password.
224
+
225
+ **For a `WordPress + WooCommerce` brief, also add a `woocommerce` block and the store URLs** (the
226
+ `backend` field still stays `"wordpress"`):
227
+
228
+ ```json
229
+ {
230
+ "woocommerce": {
231
+ "enabled": true,
232
+ "seeded_via": "per-site-wp-mcp",
233
+ "currency": "<currency_code from GET {store_api}/products or the install default>",
234
+ "payment_enabled": false,
235
+ "checkout_setup": "owner enables a payment method + shipping in wp-admin"
236
+ },
237
+ "urls": {
238
+ "store_api": "https://<domain>/cms/wp-json/wc/store/v1",
239
+ "wc_rest_api": "https://<domain>/cms/wp-json/wc/v3"
240
+ }
241
+ }
242
+ ```
243
+
244
+ `rest_api` stays `wp/v2` (the WordPress content contract, for the blog/pages on the same site);
245
+ `store_api` is the public storefront surface (browse/cart/coupons/checkout, no auth). `wc_rest_api`
246
+ (`wc/v3`) is recorded only as the surface the **owner's own** integrations use with a consumer key they
247
+ generate in wp-admin — **this flow never creates or stores a key** (the catalog is seeded through the
248
+ per-site WP MCP's `wc-*` abilities; the storefront uses the public Store API). `payment_enabled` starts
249
+ `false` and flips to `true` only once a gateway + shipping are configured — no secrets in `site.json`.