hostinger-api-mcp 1.63.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/README.md +433 -425
  3. package/gemini-extension.json +1 -1
  4. package/package.json +1 -1
  5. package/server.json +2 -2
  6. package/skills/headless/SKILL.md +5 -5
  7. package/skills/headless/references/DATABASE.md +15 -15
  8. package/skills/headless/references/DEPLOYMENT.md +5 -5
  9. package/skills/headless/references/SETUP.md +10 -10
  10. package/skills/headless/references/STORE.md +5 -5
  11. package/skills/headless/references/WORDPRESS.md +6 -6
  12. package/src/core/catalog.js +183 -0
  13. package/src/core/catalog.ts +207 -0
  14. package/src/core/instructions.js +1 -1
  15. package/src/core/instructions.ts +1 -1
  16. package/src/core/runtime.js +70 -94
  17. package/src/core/runtime.ts +61 -96
  18. package/src/core/tools/agency-hosting.js +44 -44
  19. package/src/core/tools/agency-hosting.ts +44 -44
  20. package/src/core/tools/all.js +405 -405
  21. package/src/core/tools/all.ts +405 -405
  22. package/src/core/tools/billing.js +9 -9
  23. package/src/core/tools/billing.ts +9 -9
  24. package/src/core/tools/dns.js +8 -8
  25. package/src/core/tools/dns.ts +8 -8
  26. package/src/core/tools/domains.js +41 -41
  27. package/src/core/tools/domains.ts +41 -41
  28. package/src/core/tools/ecommerce.js +29 -29
  29. package/src/core/tools/ecommerce.ts +29 -29
  30. package/src/core/tools/horizons.js +6 -6
  31. package/src/core/tools/horizons.ts +6 -6
  32. package/src/core/tools/hosting.js +76 -76
  33. package/src/core/tools/hosting.ts +76 -76
  34. package/src/core/tools/mail.js +38 -38
  35. package/src/core/tools/mail.ts +38 -38
  36. package/src/core/tools/reach.js +52 -52
  37. package/src/core/tools/reach.ts +52 -52
  38. package/src/core/tools/vps.js +64 -64
  39. package/src/core/tools/vps.ts +64 -64
  40. package/src/core/tools/wordpress.js +38 -38
  41. package/src/core/tools/wordpress.ts +38 -38
  42. package/src/servers/agency-hosting.js +1 -1
  43. package/src/servers/agency-hosting.ts +1 -1
  44. package/src/servers/all.js +1 -1
  45. package/src/servers/all.ts +1 -1
  46. package/src/servers/billing.js +1 -1
  47. package/src/servers/billing.ts +1 -1
  48. package/src/servers/dns.js +1 -1
  49. package/src/servers/dns.ts +1 -1
  50. package/src/servers/domains.js +1 -1
  51. package/src/servers/domains.ts +1 -1
  52. package/src/servers/ecommerce.js +1 -1
  53. package/src/servers/ecommerce.ts +1 -1
  54. package/src/servers/horizons.js +1 -1
  55. package/src/servers/horizons.ts +1 -1
  56. package/src/servers/hosting.js +1 -1
  57. package/src/servers/hosting.ts +1 -1
  58. package/src/servers/mail.js +1 -1
  59. package/src/servers/mail.ts +1 -1
  60. package/src/servers/reach.js +1 -1
  61. package/src/servers/reach.ts +1 -1
  62. package/src/servers/vps.js +1 -1
  63. package/src/servers/vps.ts +1 -1
  64. package/src/servers/wordpress.js +1 -1
  65. package/src/servers/wordpress.ts +1 -1
  66. package/types.d.ts +405 -405
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostinger-api-mcp",
3
- "version": "1.63.4",
3
+ "version": "2.1.0",
4
4
  "description": "Hostinger API — manage domains, DNS, hosting, VPS, billing and more from Gemini CLI.",
5
5
  "mcpServers": {
6
6
  "hostinger": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostinger-api-mcp",
3
- "version": "1.63.4",
3
+ "version": "2.1.0",
4
4
  "mcpName": "io.github.hostinger/hostinger-api-mcp",
5
5
  "description": "MCP server for Hostinger API",
6
6
  "repository": {
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/hostinger/api-mcp-server",
7
7
  "source": "github"
8
8
  },
9
- "version": "1.63.4",
9
+ "version": "2.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@hostinger/mcp",
14
- "version": "1.63.4",
14
+ "version": "2.1.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -12,7 +12,7 @@ The frontend is built ad-hoc to the user's intent — there is no template libra
12
12
  ## Preconditions
13
13
 
14
14
  1. An authenticated Hostinger MCP session — the entry skill's bootstrap handled this. If MCP tools fail with auth errors, send the user back through `entry/skill.md`.
15
- 2. The Hostinger MCP tools for **hosting** (plus **ecommerce** when the run involves a store, and **wordpress** when it involves a content backend). Tool availability varies per user — product groups can be toggled in the Hostinger Connector, and some clients cap the number of exposed tools. When a needed tool is missing, ask the user to enable that product group (or configure the scoped binary, e.g. `hostinger-hosting-mcp`) rather than improvising around it.
15
+ 2. The Hostinger MCP operations for **hosting** (plus **ecommerce** when the run involves a store, and **wordpress** when it involves a content backend). Operation availability varies per user — product groups can be toggled in the Hostinger Connector. When search cannot find a needed operation, ask the user to enable that product group (or configure the scoped binary, e.g. `hostinger-hosting-mcp`) rather than improvising around it.
16
16
  3. An active hosting plan (checked in Setup §1). Hostinger hosting is a paid product: if the account has no usable plan, inform the user plainly that a subscription is required, link them to https://www.hostinger.com/web-hosting, and pause until they confirm the purchase. Do not treat this as an error — it is a normal step for new accounts. Everything that doesn't need hosting (planning, building the frontend locally) can proceed while they decide.
17
17
 
18
18
  ## Resolving the operation
@@ -32,7 +32,7 @@ If signals conflict, ask the user — don't guess.
32
32
  3. **Store** (`references/STORE.md`, only when commerce is needed) — resolve or create the store and its `custom` sales channel, seed products/shipping/payment, and note the `sales_channel_id` for the frontend.
33
33
  4. **Content backend** (`references/WORDPRESS.md`, only when owner-managed content is needed) — install WordPress on a dedicated subdomain, wait until it's ready, and hand the owner a wp-admin login link. The frontend reads it through the public WP REST API.
34
34
  5. **Build** — create or wire the frontend. For store runs, follow the frontend contract in `references/STORE.md` (catalog fetched at runtime from the public Storefront API, cart in `localStorage`, client-side checkout — never embed an API token in the site). For content runs, follow the frontend contract in `references/WORDPRESS.md` (posts fetched at runtime from the WP REST API, rendered HTML bodies, graceful empty states). When the app needs MySQL, follow `references/DATABASE.md` (which host each runtime connects to, credentials via env vars or a server-side config file, never in the frontend).
35
- 6. **Deploy** (`references/DEPLOYMENT.md`) — archive and deploy via the matching hosting tool, polling build logs for Node.js runs.
35
+ 6. **Deploy** (`references/DEPLOYMENT.md`) — archive and deploy via the matching hosting operation, polling build logs for Node.js runs.
36
36
  7. **Verify** — curl the live URL for a 200 and a piece of real page copy; for store runs, confirm a checkout POST returns a redirect URL; for content runs, confirm the WP REST API returns 200 and the frontend renders posts. Show the user the live URL and where to manage things (hPanel: https://hpanel.hostinger.com, the store dashboard for commerce, wp-admin for content).
37
37
  8. **Record** — write `.hostinger/site.json` into the project: `{ "domain", "username", "type": "static"|"nodejs", "sales_channel_id"?, "store_id"?, "cms_domain"? }`. This is what makes future runs resolve as `iterate`.
38
38
 
@@ -50,7 +50,7 @@ Run non-interactively wherever possible. The exceptions that must involve the us
50
50
 
51
51
  ## Where the how comes from
52
52
 
53
- The MCP tool descriptions are authoritative for request shapes — read them before calling. Two live sources supersede anything written here when they disagree:
53
+ The inputSchema returned by search is authoritative for request shapes — read it before calling. Two live sources supersede anything written here when they disagree:
54
54
 
55
- - The ecommerce MCP tool `ecommerce_getCustomStorefrontSetupInstructionsV1` returns the current storefront integration guide from the server — call it at the start of any store run.
56
- - The Hostinger API reference at https://developers.hostinger.com describes every endpoint behind the tools.
55
+ - The ecommerce operation `ecommerce_miscellaneous_custom-storefront-setup-instructions` returns the current storefront integration guide from the server — execute it at the start of any store run.
56
+ - The Hostinger API reference at https://developers.hostinger.com describes every endpoint behind the operations.
@@ -1,11 +1,11 @@
1
1
  # Database and runtime — MySQL hosts, env vars, and what to poll
2
2
 
3
- Use this when the app needs MySQL, or when a deploy "succeeded" but the app cannot reach its database. Everything here uses the **hosting** MCP tools; Agency Plan differences are at the end.
3
+ Use this when the app needs MySQL, or when a deploy "succeeded" but the app cannot reach its database. Everything here uses the **hosting** MCP operations; Agency Plan differences are at the end.
4
4
 
5
5
  ## 1. Create the database
6
6
 
7
- 1. `hosting_createAccountDatabaseV1` on the site's `username` with a database name, a user and a password you generate — name and user are prefixed with the account username automatically.
8
- 2. Read the full, prefixed `name` and `user` back from `hosting_listAccountDatabasesV1`; every other database tool wants the full name.
7
+ 1. `hosting_databases_create` on the site's `username` with a database name, a user and a password you generate — name and user are prefixed with the account username automatically.
8
+ 2. Read the full, prefixed `name` and `user` back from `hosting_databases_list`; every other database operation wants the full name.
9
9
  3. Do not print the password to the user once it is stored, and never commit it.
10
10
 
11
11
  ## 2. Which host the app connects to
@@ -14,13 +14,13 @@ Use this when the app needs MySQL, or when a deploy "succeeded" but the app cann
14
14
  | --- | --- | --- | --- |
15
15
  | PHP on the website | `localhost` or `127.0.0.1` | 3306 | `localhost` uses the MySQL socket; both are granted |
16
16
  | Node.js on the website | `127.0.0.1` | 3306 | `localhost` may resolve to IPv6 `::1`, and not every database user has that grant |
17
- | Outside Hostinger (a laptop, another server) | the `host` from `hosting_listAccountDatabasesV1` (`srvNNNN.hstgr.io`) | 3306 | needs `hosting_createDatabaseRemoteConnectionV1` for that client's IP first — never `%`, it opens the database to every address on the internet |
17
+ | Outside Hostinger (a laptop, another server) | the `host` from `hosting_databases_list` (`srvNNNN.hstgr.io`) | 3306 | needs `hosting_databases_create-remote-connection` for that client's IP first — never `%`, it opens the database to every address on the internet |
18
18
 
19
19
  Do not put the remote `host` into the deployed app — it has no grant for connections that come from the server itself.
20
20
 
21
21
  ## 3. Hand credentials to the app
22
22
 
23
- - **Node.js:** environment variables via `hosting_replaceNode_jsEnvironmentVariablesV1`. Conventional keys: `DB_HOST=127.0.0.1`, `DB_PORT=3306`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, plus `DATABASE_URL=mysql://USER:PASSWORD@127.0.0.1:3306/NAME` for ORMs that want one string — URL-encode `USER` and `PASSWORD` there (`encodeURIComponent`), or a password containing `@`, `:`, `/` or `#` breaks the URL. The call is a **full replace**: list the current keys with `hosting_listNode_jsEnvironmentVariablesV1` (values come back masked as `********` — never copy those), then send the complete set with real values. Saving restarts the process; frameworks that bake variables into the build (Next.js, `NEXT_PUBLIC_*`) need a new `hosting_startNode_jsBuildV1` afterwards.
23
+ - **Node.js:** environment variables via `hosting_nodejs_replace-environment-variables`. Conventional keys: `DB_HOST=127.0.0.1`, `DB_PORT=3306`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, plus `DATABASE_URL=mysql://USER:PASSWORD@127.0.0.1:3306/NAME` for ORMs that want one string — URL-encode `USER` and `PASSWORD` there (`encodeURIComponent`), or a password containing `@`, `:`, `/` or `#` breaks the URL. The call is a **full replace**: list the current keys with `hosting_nodejs_list-environment-variables` (values come back masked as `********` — never copy those), then send the complete set with real values. Saving restarts the process; frameworks that bake variables into the build (Next.js, `NEXT_PUBLIC_*`) need a new `hosting_nodejs_start-build` afterwards.
24
24
  - **PHP:** a config file on the server that the app reads (its own `config.php`, WordPress's `wp-config.php`). Ship it with the deploy, keep it out of git.
25
25
  - **Never** in client-side JavaScript, in a committed `.env`, or in chat replies.
26
26
 
@@ -28,22 +28,22 @@ Do not put the remote `host` into the deployed app — it has no grant for conne
28
28
 
29
29
  | You called | Poll this until | Typical wait |
30
30
  | --- | --- | --- |
31
- | `hosting_createWebsiteV1` | `hosting_listWebsitesV1` lists the domain | up to a few minutes |
32
- | `hosting_deployJsApplication` | `hosting_listJsDeployments` shows the build finished | minutes |
33
- | `hosting_startNode_jsBuildV1` | `hosting_getNode_jsBuildDetailsV1` state is `completed` or `failed` | minutes |
34
- | `hosting_installWordPressV1`, plugin/theme/core jobs | `hosting_listWordPressInstallationsV1` lists the install | 1–2 minutes |
35
- | `hosting_repairDatabaseV1`, `hosting_deleteWebsiteV1` | nothing to poll — runs in the background | minutes |
36
- | `agency-hosting_createANewWebsiteV1` | `agency-hosting_getWebsiteSetupStatusV1` with `setup_uuid` is `completed` | minutes |
31
+ | `hosting_websites_create` | `hosting_websites_list` lists the domain | up to a few minutes |
32
+ | `hosting_deploy-js-application` | `hosting_list-js-deployments` shows the build finished | minutes |
33
+ | `hosting_nodejs_start-build` | `hosting_nodejs_build` state is `completed` or `failed` | minutes |
34
+ | `wordpress_installations_install`, plugin/theme/core jobs | `wordpress_installations_list` lists the install | 1–2 minutes |
35
+ | `hosting_databases_repair`, `hosting_websites_delete` | nothing to poll — runs in the background | minutes |
36
+ | `agency-hosting_website-setups_create` | `agency-hosting_website-setups_status` with `setup_uuid` is `completed` | minutes |
37
37
 
38
38
  Poll with backoff (a few seconds, then tens of seconds). A queued response is not a failure, and re-sending the write does not speed it up.
39
39
 
40
40
  ## 5. When the app is up but broken
41
41
 
42
- - Build failed: `hosting_getNodeJSBuildLogsV1` for the raw log, `hosting_analyseFailedNode_jsBuildV1` for a diagnosis.
43
- - Build passed, app errors: `hosting_getNode_jsRuntimeLogsV1` with a `period` such as `1h`; entries before `last_deployed_at` belong to the previous deploy.
42
+ - Build failed: `hosting_nodejs_build-logs` for the raw log, `hosting_nodejs_analyse-failed-build` for a diagnosis.
43
+ - Build passed, app errors: `hosting_nodejs_runtime-logs` with a `period` such as `1h`; entries before `last_deployed_at` belong to the previous deploy.
44
44
  - `Access denied for user 'x'@'::1'`: the app used `localhost` from Node.js — set `DB_HOST` to `127.0.0.1`.
45
- - PHP limits (memory, upload size): `hosting_getPHPDetailsV1`, then `hosting_updatePHPOptionsV1`.
45
+ - PHP limits (memory, upload size): `hosting_php_get`, then `hosting_php_update-options`.
46
46
 
47
47
  ## Agency Plan websites (`agency-hosting_*`)
48
48
 
49
- `agency-hosting_createWebsiteDatabaseV1` creates the database and its single user; the user is granted on `localhost`, so the PHP app connects to `localhost`. Deploys are synchronous there — `agency-hosting_deployPhpApplication` returns when the site is live. Other jobs (SSL, backups) show up in `agency-hosting_listWebsiteProcessesV1`.
49
+ `agency-hosting_databases_create-website` creates the database and its single user; the user is granted on `localhost`, so the PHP app connects to `localhost`. Deploys are synchronous there — `agency-hosting_deploy-php-application` returns when the site is live. Other jobs (SSL, backups) show up in `agency-hosting_websites_list-processes`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Match the deploy method to what the project actually is — this is the single most common failure point.
4
4
 
5
- ## Static site → `hosting_deployStaticWebsite`
5
+ ## Static site → `hosting_deploy-static-website`
6
6
 
7
7
  For pre-built files only: plain HTML/CSS/JS, or the **build output** of a framework (run the build locally first).
8
8
 
@@ -10,15 +10,15 @@ For pre-built files only: plain HTML/CSS/JS, or the **build output** of a framew
10
10
  - Name it `name_YYYYMMDD_HHMMSS.zip`; pass `removeArchive: true` to clean up after upload.
11
11
  - Deployment is effectively immediate — verify right after.
12
12
 
13
- ## Node.js app → `hosting_deployJsApplication`
13
+ ## Node.js app → `hosting_deploy-js-application`
14
14
 
15
15
  For anything that needs a server or a server-side build (Express, Next.js, NestJS, SSR frameworks).
16
16
 
17
17
  - Archive the **source**, not the output: exclude `node_modules/`, `dist/`, `.next/`, `build/`, and everything matched by `.gitignore`. The install and build run on Hostinger.
18
18
  - Hard cap: **50 MB** archive.
19
- - The upload starts a build. Track it with `hosting_listJsDeployments`; on failure pull `hosting_showJsDeploymentLogs`, fix, and redeploy. Poll with backoff — builds take minutes, not seconds.
19
+ - The upload starts a build. Track it with `hosting_list-js-deployments`; on failure pull `hosting_show-js-deployment-logs`, fix, and redeploy. Poll with backoff — builds take minutes, not seconds.
20
20
 
21
- ## WordPress → `hosting_importWordpressWebsite` / plugin & theme tools
21
+ ## WordPress → `hosting_import-wordpress-website` / plugin & theme operations
22
22
 
23
23
  Only when the user explicitly wants a WordPress *site* migrated or themed. Site imports take an archive plus a `.sql` dump and can run for several minutes. (WordPress as a headless content backend behind an agent-built frontend is a different flow — that's `WORDPRESS.md`, installed fresh via the API, not imported here.)
24
24
 
@@ -29,6 +29,6 @@ curl -s -o /dev/null -w "%{http_code}\n" https://YOURDOMAIN/
29
29
  curl -s https://YOURDOMAIN/ | grep -o "SOME_REAL_HEADLINE_TEXT"
30
30
  ```
31
31
 
32
- Replace the grep target with copy you actually rendered — a 200 alone can be a placeholder page. A freshly created site may serve a default page for a short while; if content doesn't match, clear the cache (`hosting_clearWebsiteCacheV1`) and retry before assuming the deploy failed. For store runs, also run the checkout verification in `STORE.md`.
32
+ Replace the grep target with copy you actually rendered — a 200 alone can be a placeholder page. A freshly created site may serve a default page for a short while; if content doesn't match, clear the cache (`hosting_cache_clear-website`) and retry before assuming the deploy failed. For store runs, also run the checkout verification in `STORE.md`.
33
33
 
34
34
  When the site is live, report the URL and remind the user the site is managed from hPanel (https://hpanel.hostinger.com).
@@ -1,34 +1,34 @@
1
1
  # Setup — plan check and website provisioning
2
2
 
3
- Everything here uses the **hosting** MCP tools. If they're missing, ask the user to enable the Websites product group in the Hostinger Connector (or use the `hostinger-hosting-mcp` scoped binary).
3
+ Everything here uses the **hosting** MCP operations. If they're missing, ask the user to enable the Websites product group in the Hostinger Connector (or use the `hostinger-hosting-mcp` scoped binary).
4
4
 
5
5
  ## 1. Plan check (gate — run this first)
6
6
 
7
7
  A website can only be created on an active hosting plan.
8
8
 
9
- 1. `hosting_listWebsitesV1` — if it returns websites, the account has a working plan; note any existing `order_id` and `username` for step 2.
10
- 2. Otherwise `hosting_listOrdersV1` — look for an order in a usable state. A fresh order that has never hosted a website still works; note its `order_id`.
9
+ 1. `hosting_websites_list` — if it returns websites, the account has a working plan; note any existing `order_id` and `username` for step 2.
10
+ 2. Otherwise `hosting_orders_list` — look for an order in a usable state. A fresh order that has never hosted a website still works; note its `order_id`.
11
11
  3. If there is no usable order: **stop and tell the user** that an active Hostinger hosting plan is required to deploy, link https://www.hostinger.com/web-hosting, and offer to continue building the site locally in the meantime. When they confirm the purchase, re-run this check.
12
12
 
13
13
  Never purchase a plan, domain, or any paid item without the user explicitly approving that specific purchase.
14
14
 
15
15
  ## 2. Choose the domain
16
16
 
17
- - **Default: free subdomain.** `hosting_generateAFreeSubdomainV1` returns a `*.hostingersite.com` domain. No verification needed. Tell the user a custom domain can be connected later.
18
- - **User-owned domain:** `hosting_verifyDomainOwnershipV1` first. If not accessible, relay the TXT record it returns, remind them propagation can take ~10 minutes, and re-verify before continuing.
19
- - **New domain purchase:** only on explicit request — check availability with `domains_checkDomainAvailabilityV1`, state the price, and get an explicit yes before `domains_purchaseNewDomainV1` (needs the domains product group).
17
+ - **Default: free subdomain.** `hosting_domains_generate-free-subdomain` returns a `*.hostingersite.com` domain. No verification needed. Tell the user a custom domain can be connected later.
18
+ - **User-owned domain:** `hosting_domains_verify-ownership` first. If not accessible, relay the TXT record it returns, remind them propagation can take ~10 minutes, and re-verify before continuing.
19
+ - **New domain purchase:** only on explicit request — check availability with `domains_availability_check`, state the price, and get an explicit yes before `domains_portfolio_purchase` (needs the domains product group).
20
20
 
21
21
  ## 3. Create the website and wait for it
22
22
 
23
23
  Generating a subdomain does **not** create a website — deploying straight to it fails with `No website found for domain`. The working sequence:
24
24
 
25
- 1. `hosting_createWebsiteV1 { domain, order_id }` — `datacenter_code` is required only for the first website on a brand-new plan (pick the first entry from `hosting_listAvailableDatacentersV1`).
26
- 2. **Poll** `hosting_listWebsitesV1` filtered by the domain until the site appears. Creation takes up to a few minutes — poll with backoff, don't fail fast.
27
- 3. Note the site's `username` — deployment and database tools are keyed on it.
25
+ 1. `hosting_websites_create { domain, order_id }` — `datacenter_code` is required only for the first website on a brand-new plan (pick the first entry from `hosting_datacenters_list`).
26
+ 2. **Poll** `hosting_websites_list` filtered by the domain until the site appears. Creation takes up to a few minutes — poll with backoff, don't fail fast.
27
+ 3. Note the site's `username` — deployment and database operations are keyed on it.
28
28
 
29
29
  If the domain already has a website (an `iterate` run, or the user pointed at an existing site), skip creation entirely.
30
30
 
31
31
  ## 4. Optional extras (only when the run needs them)
32
32
 
33
33
  - **Database:** when the app needs MySQL, follow `DATABASE.md` — creating it, which host each runtime connects to (Node.js must use `127.0.0.1`), and handing the credentials to the app without hard-coding them.
34
- - **DNS records:** the DNS tools (`DNS_updateDNSRecordsV1` etc.) for custom-domain records; take a snapshot (`DNS_getDNSSnapshotListV1` context) before destructive changes.
34
+ - **DNS records:** the DNS operations (`dns_records_update` etc.) for custom-domain records; take a snapshot (`dns_snapshots_list` context) before destructive changes.
@@ -1,6 +1,6 @@
1
1
  # Store — Hostinger Ecommerce with a custom storefront
2
2
 
3
- **Primary source:** call `ecommerce_getCustomStorefrontSetupInstructionsV1` at the start of every store run — it returns the current, server-maintained integration guide, and it supersedes this file wherever they disagree. This file carries the essentials so the run can be planned before that call, and a fallback when the ecommerce tools aren't enabled.
3
+ **Primary source:** execute `ecommerce_miscellaneous_custom-storefront-setup-instructions` at the start of every store run — it returns the current, server-maintained integration guide, and it supersedes this file wherever they disagree. This file carries the essentials so the run can be planned before that call, and a fallback when the ecommerce operations aren't enabled.
4
4
 
5
5
  ## Mental model
6
6
 
@@ -17,11 +17,11 @@ Two API surfaces — don't mix them:
17
17
 
18
18
  ## Backend setup (management tools)
19
19
 
20
- 1. Resolve the `custom` sales channel: `ecommerce_getStoresV1` → `ecommerce_listSalesChannelsV1`. If a `custom` channel already exists, confirm with the user that it's the one to use; otherwise add one to the existing store with `ecommerce_createCustomSalesChannelV1`, or create a store with `ecommerce_createStoreV1` (with a `custom` sales channel) when no suitable store exists. Set the channel `url` to the storefront's public domain at creation when it's already known.
21
- 2. For checkout to work the store needs all three: ≥ 1 product, ≥ 1 payment method, ≥ 1 shipping zone. Payment, shipping, and currency are store-level — check `ecommerce_getStoreMetadataV1` (`has_payment_methods` and `has_shipping` must be `true`; the same response carries `default_currency` with `code`, `decimal_digits`, `template` — read it here rather than guessing). Products are **per sales channel** — verify by listing products for the resolved channel on the Storefront API and confirming the list is non-empty. Seed what's missing with `ecommerce_createPhysicalProductV1` / `ecommerce_createDigitalProductV1`, `ecommerce_enableManualPaymentMethodV1`, and `ecommerce_setStoreShippingV1` (price `0` = free shipping) — confirming with the user before writing to a store that already has real data.
22
- 3. After deploy, point the channel at the live site: `ecommerce_updateSalesChannelV1 { store_id, sales_channel_id, url }`.
20
+ 1. Resolve the `custom` sales channel: `ecommerce_stores_list` → `ecommerce_sales-channels_list`. If a `custom` channel already exists, confirm with the user that it's the one to use; otherwise add one to the existing store with `ecommerce_sales-channels_create`, or create a store with `ecommerce_stores_create` (with a `custom` sales channel) when no suitable store exists. Set the channel `url` to the storefront's public domain at creation when it's already known.
21
+ 2. For checkout to work the store needs all three: ≥ 1 product, ≥ 1 payment method, ≥ 1 shipping zone. Payment, shipping, and currency are store-level — check `ecommerce_stores_metadata` (`has_payment_methods` and `has_shipping` must be `true`; the same response carries `default_currency` with `code`, `decimal_digits`, `template` — read it here rather than guessing). Products are **per sales channel** — verify by listing products for the resolved channel on the Storefront API and confirming the list is non-empty. Seed what's missing with `ecommerce_products_create-physical` / `ecommerce_products_create-digital`, `ecommerce_payments_enable-manual-method`, and `ecommerce_shipping_set-store` (price `0` = free shipping) — confirming with the user before writing to a store that already has real data.
22
+ 3. After deploy, point the channel at the live site: `ecommerce_sales-channels_update { store_id, sales_channel_id, url }`.
23
23
 
24
- If the ecommerce tools aren't available, ask the user to enable the Ecommerce product group in the Hostinger Connector — or to provide the `sales_channel_id` directly, after which the public endpoints suffice for the frontend work.
24
+ If the ecommerce operations aren't available, ask the user to enable the Ecommerce product group in the Hostinger Connector — or to provide the `sales_channel_id` directly, after which the public endpoints suffice for the frontend work.
25
25
 
26
26
  ## Frontend contract
27
27
 
@@ -4,7 +4,7 @@ Use WordPress when the site has **owner-managed content**: a blog, news, article
4
4
 
5
5
  **When NOT to use this:** static copy that rarely changes (an about page, a services list, a brochure site) — bake that into the frontend. Installing WordPress for content the owner will never edit adds a moving part for nothing. Any buy/sell intent is `STORE.md`, not this file.
6
6
 
7
- Everything in Setup uses the **hosting** and **wordpress** MCP tool groups; ask the user to enable them in the Hostinger Connector if tools are missing.
7
+ Everything in Setup uses the **hosting** and **wordpress** MCP operation groups; ask the user to enable them in the Hostinger Connector if operations are missing.
8
8
 
9
9
  ## Mental model
10
10
 
@@ -20,11 +20,11 @@ Two surfaces, like the store recipe:
20
20
 
21
21
  ## Setup (management tools)
22
22
 
23
- 1. **Check for an existing installation first**: `hosting_listWordPressInstallationsV1` filtered by the site's username/domain. Reuse a valid install when the user agrees — never overwrite one silently.
24
- 2. **Choose where WordPress lives.** Default: a dedicated subdomain of the site, e.g. `cms.<domain>` — create it with `hosting_createWebsiteSubdomainV1` so the main domain stays free for the frontend. A separate free-subdomain website (per `SETUP.md`) also works when the plan allows another website.
25
- 3. **Install**: `hosting_installWordPressV1` on that domain. The call only queues the job — **poll** `hosting_listWordPressInstallationsV1` until the installation appears (typically 1–2 minutes). Don't proceed on the queued response alone.
26
- 4. **Hand the owner their editor**: mint a one-click wp-admin link with `hosting_createLoginLinksV1` and show it to the user. Content authoring happens in wp-admin — the skill does not seed posts (there is no anonymous write path, by design). The fresh install ships with a sample post, which is enough to build and verify the frontend against.
27
- 5. **Caching** (recommended before finishing): enable the object cache with `hosting_toggleMemcachedObjectCacheV1`; after config changes, purge with `hosting_purgeLiteSpeedCacheV1`.
23
+ 1. **Check for an existing installation first**: `wordpress_installations_list` filtered by the site's username/domain. Reuse a valid install when the user agrees — never overwrite one silently.
24
+ 2. **Choose where WordPress lives.** Default: a dedicated subdomain of the site, e.g. `cms.<domain>` — create it with `hosting_domains_create-website-subdomain` so the main domain stays free for the frontend. A separate free-subdomain website (per `SETUP.md`) also works when the plan allows another website.
25
+ 3. **Install**: `wordpress_installations_install` on that domain. The call only queues the job — **poll** `wordpress_installations_list` until the installation appears (typically 1–2 minutes). Don't proceed on the queued response alone.
26
+ 4. **Hand the owner their editor**: mint a one-click wp-admin link with `wordpress_login_create-links` and show it to the user. Content authoring happens in wp-admin — the skill does not seed posts (there is no anonymous write path, by design). The fresh install ships with a sample post, which is enough to build and verify the frontend against.
27
+ 5. **Caching** (recommended before finishing): enable the object cache with `wordpress_object-cache_toggle-memcached`; after config changes, purge with `wordpress_litespeed-cache_purge-lite-speed`.
28
28
 
29
29
  ## Frontend contract
30
30
 
@@ -0,0 +1,183 @@
1
+ // Meta-tool logic for the consolidated MCP surface: search, execute and
2
+ // multi-execute over a catalog of API operations. Pure functions only; the
3
+ // runtime supplies the catalog and the function that performs a call.
4
+ // Keep in sync with catalog.ts and with api-remote-mcp's PHP implementation.
5
+
6
+ export const DEFAULT_SEARCH_LIMIT = 5;
7
+ export const MAX_SEARCH_LIMIT = 20;
8
+ export const MAX_STEPS = 20;
9
+
10
+ const STEP_REF = /^\$steps\.(\d+)((?:\.[^.]+)*)$/;
11
+
12
+ /** Bad input from the model: reported as an isError result, never a protocol error. */
13
+ export class ToolInputError extends Error {
14
+ constructor(message) {
15
+ super(message);
16
+ this.name = 'ToolInputError';
17
+ }
18
+ }
19
+
20
+ const meta = (name, title, description, inputSchema, readOnly) => ({
21
+ name,
22
+ title,
23
+ description,
24
+ inputSchema,
25
+ annotations: { title, readOnlyHint: readOnly, destructiveHint: !readOnly },
26
+ });
27
+
28
+ export const META_TOOLS = [
29
+ meta('search', 'Search API operations',
30
+ "Find API operations by keyword. Returns the best matches, each with its operation name, hints and inputSchema. Pass a result's operation and params matching its inputSchema to execute or multi-execute.",
31
+ {
32
+ type: 'object',
33
+ properties: {
34
+ query: { type: 'string', description: 'Keywords, e.g. "list vps" or "dns records".' },
35
+ limit: { type: 'integer', minimum: 1, maximum: MAX_SEARCH_LIMIT, default: DEFAULT_SEARCH_LIMIT, description: 'Maximum number of results.' },
36
+ },
37
+ required: ['query'],
38
+ },
39
+ true),
40
+ meta('execute', 'Execute an API operation',
41
+ "Run one API operation found with search. params is a flat object holding the operation's path, query and body parameters as described by its inputSchema.",
42
+ {
43
+ type: 'object',
44
+ properties: {
45
+ operation: { type: 'string', description: 'Operation name returned by search.' },
46
+ params: { type: 'object', description: 'Parameters matching the operation inputSchema.' },
47
+ },
48
+ required: ['operation'],
49
+ },
50
+ false),
51
+ meta('multi-execute', 'Execute API operations in sequence',
52
+ 'Run up to 20 API operations in order, stopping at the first failure. A params value that is exactly "$steps.<i>.<path>" is replaced by that value from the result of earlier step <i> (dot-separated keys; numbers index arrays), e.g. "$steps.0.0.id".',
53
+ {
54
+ type: 'object',
55
+ properties: {
56
+ steps: {
57
+ type: 'array',
58
+ minItems: 1,
59
+ maxItems: MAX_STEPS,
60
+ items: {
61
+ type: 'object',
62
+ properties: {
63
+ operation: { type: 'string', description: 'Operation name returned by search.' },
64
+ params: { type: 'object', description: 'Parameters; may contain "$steps.<i>.<path>" references.' },
65
+ },
66
+ required: ['operation'],
67
+ },
68
+ },
69
+ },
70
+ required: ['steps'],
71
+ },
72
+ false),
73
+ ];
74
+
75
+ const byteOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
76
+
77
+ function toSummary(op) {
78
+ return {
79
+ operation: op.name,
80
+ title: op.title ?? op.name,
81
+ description: op.description ?? '',
82
+ group: op.group ?? null,
83
+ readOnly: op.annotations?.readOnlyHint === true,
84
+ destructive: op.annotations?.destructiveHint === true,
85
+ inputSchema: op.inputSchema,
86
+ };
87
+ }
88
+
89
+ export function searchOperations(operations, query, limit = DEFAULT_SEARCH_LIMIT) {
90
+ if (typeof query !== 'string') throw new ToolInputError('query must be a string');
91
+ const tokens = query.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
92
+ if (tokens.length === 0) throw new ToolInputError('query must contain at least one letter or digit');
93
+ if (!Number.isInteger(limit) || limit < 1 || limit > MAX_SEARCH_LIMIT) {
94
+ throw new ToolInputError(`limit must be an integer from 1 to ${MAX_SEARCH_LIMIT}`);
95
+ }
96
+
97
+ return operations
98
+ .map(op => {
99
+ const name = op.name.toLowerCase();
100
+ const group = (op.group ?? '').toLowerCase();
101
+ const title = (op.title ?? '').toLowerCase();
102
+ const description = (op.description ?? '').toLowerCase();
103
+ let score = 0;
104
+ for (const t of tokens) {
105
+ if (name.includes(t)) score += 3;
106
+ if (group.includes(t)) score += 2;
107
+ if (title.includes(t)) score += 2;
108
+ if (description.includes(t)) score += 1;
109
+ }
110
+ return { op, score };
111
+ })
112
+ .filter(s => s.score > 0)
113
+ .sort((a, b) => b.score - a.score || byteOrder(a.op.name, b.op.name))
114
+ .slice(0, limit)
115
+ .map(s => toSummary(s.op));
116
+ }
117
+
118
+ export function renderSearch(query, results) {
119
+ return results.length > 0
120
+ ? JSON.stringify(results)
121
+ : `No operations matched "${query}". Try broader or different terms.`;
122
+ }
123
+
124
+ export function findOperation(operations, name, params) {
125
+ if (typeof name !== 'string' || !operations.has(name)) {
126
+ throw new ToolInputError(`Unknown operation "${name}". Use search to find operations.`);
127
+ }
128
+ if (params === null || typeof params !== 'object' || Array.isArray(params)) {
129
+ throw new ToolInputError('params must be an object');
130
+ }
131
+ const op = operations.get(name);
132
+ const missing = (op.inputSchema?.required ?? []).filter(k => params[k] === undefined || params[k] === null);
133
+ if (missing.length > 0) {
134
+ throw new ToolInputError(`Missing required parameter(s) for ${name}: ${missing.join(', ')}`);
135
+ }
136
+ return op;
137
+ }
138
+
139
+ export function resolveRefs(value, results, stepIndex) {
140
+ if (typeof value === 'string') {
141
+ const m = STEP_REF.exec(value);
142
+ if (!m) return value;
143
+ const i = Number(m[1]);
144
+ if (i >= stepIndex) {
145
+ throw new ToolInputError(`${value}: references must point to an earlier step`);
146
+ }
147
+ let current = results[i];
148
+ for (const segment of m[2].split('.').slice(1)) {
149
+ if (current === null || typeof current !== 'object' || !Object.hasOwn(current, segment)) {
150
+ throw new ToolInputError(`${value}: path not found in the result of step ${i}`);
151
+ }
152
+ current = current[segment];
153
+ }
154
+ return current;
155
+ }
156
+ if (Array.isArray(value)) return value.map(v => resolveRefs(v, results, stepIndex));
157
+ if (value !== null && typeof value === 'object') {
158
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolveRefs(v, results, stepIndex)]));
159
+ }
160
+ return value;
161
+ }
162
+
163
+ export async function runSteps(steps, runOne, formatError) {
164
+ if (!Array.isArray(steps) || steps.length === 0 || steps.length > MAX_STEPS) {
165
+ throw new ToolInputError(`steps must be an array of 1 to ${MAX_STEPS} items`);
166
+ }
167
+ const raw = [];
168
+ const results = [];
169
+ for (const [i, step] of steps.entries()) {
170
+ try {
171
+ if (step === null || typeof step !== 'object' || typeof step.operation !== 'string') {
172
+ throw new ToolInputError(`step ${i}: operation must be a string`);
173
+ }
174
+ const params = resolveRefs(step.params ?? {}, raw, i);
175
+ const result = await runOne(step.operation, params);
176
+ raw.push(result);
177
+ results.push({ step: i, operation: step.operation, result });
178
+ } catch (error) {
179
+ return { results, error: { step: i, message: formatError(error) } };
180
+ }
181
+ }
182
+ return { results };
183
+ }