From d460ab2c8ed5e611866c92a56e640b9946d890b4 Mon Sep 17 00:00:00 2001 From: Arnas Donauskas Date: Thu, 6 Aug 2026 14:34:01 +0300 Subject: [PATCH] fix: sync plugin with the real MCP server and extension parity The plugin was last touched on 2026-05-27 and had drifted from both hostinger-api-mcp (now 1.29.0) and the VS Code extension (1.3.2). Several parts were not merely stale but non-functional. Every Hostinger tool name in the rules, skills, agent, and command was invented. The plugin told the agent to call list_hosting_plans, create_nodejs_deployment, list_dns_records, create_dns_snapshot, query_logs and others, none of which the MCP server has ever exposed. All 104 references now use the real OpenAPI operation IDs. The token-leak hook never fired: hooks.json used a "pre-commit" trigger, which is not a Cursor hook event, plus an array shape and a "script" key the v1 schema does not read. It is now a beforeShellExecution hook that matches git commit and speaks the hook JSON protocol, failing open so a guard that cannot read the staged diff does not block every commit. Deployment guidance described an API that does not exist. There is no framework preset parameter; the real app_type override accepts only create-react-app, vite, angular, react, vue, parcel, express, fastify and nest, with everything else auto-detected. DNS guidance assumed per-record tools, but the API is zone-oriented with an overwrite flag and no on-demand snapshot creation. query-hosting-logs promised access and error logs the API does not expose, so it is now query-deployment-logs, scoped to build, deployment and cron logs. mcp.json now registers eight per-product servers mirroring the extension's groups instead of one monolith loading all 289 tools, drops the fragile ${HOSTINGER_API_TOKEN} interpolation that shadowed the OAuth fallback, and sets USER_AGENT so plugin traffic is attributable. To stop the tool-name class of bug recurring, check-tool-names.mjs validates every tool-shaped identifier in the docs against a checked-in catalog. It default-denies unknown snake_case names rather than checking known prefixes, because the fabricated names had no prefix at all. CI runs it alongside the manifest validator, JSON parsing and shellcheck. --- .cursor-plugin/plugin.json | 9 +- .github/workflows/ci.yml | 63 +++++ CHANGELOG.md | 35 +++ LICENSE | 10 + README.md | 130 +++++++--- agents/hostinger-deployment-reviewer.md | 33 +-- commands/hostinger-status.md | 41 +-- hooks/hooks.json | 19 +- mcp.json | 43 +++- rules/confirm-destructive-actions.mdc | 58 ++++- rules/framework-presets.mdc | 36 --- rules/nodejs-deployments.mdc | 58 +++++ rules/prefer-mcp-tools.mdc | 43 +++- scripts/check-no-token-leak.sh | 51 ++-- scripts/check-tool-names.mjs | 194 +++++++++++++++ scripts/mcp-tools.json | 318 ++++++++++++++++++++++++ scripts/sync-mcp-tools.mjs | 64 +++++ skills/deploy-nodejs-app/SKILL.md | 58 +++-- skills/diagnose-build-failure/SKILL.md | 53 ++-- skills/manage-dns-records/SKILL.md | 79 +++--- skills/query-deployment-logs/SKILL.md | 53 ++++ skills/query-hosting-logs/SKILL.md | 50 ---- skills/troubleshoot-wordpress/SKILL.md | 65 +++-- 23 files changed, 1250 insertions(+), 313 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 LICENSE delete mode 100644 rules/framework-presets.mdc create mode 100644 rules/nodejs-deployments.mdc create mode 100644 scripts/check-tool-names.mjs create mode 100644 scripts/mcp-tools.json create mode 100644 scripts/sync-mcp-tools.mjs create mode 100644 skills/query-deployment-logs/SKILL.md delete mode 100644 skills/query-hosting-logs/SKILL.md diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index d425714..dbda907 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,14 +1,14 @@ { "name": "hostinger-connector", "displayName": "Hostinger Connector", - "version": "0.1.0", - "description": "MCP plugin to deploy and manage Hostinger hosting, Node.js apps, deployments, and DNS from inside Cursor.", + "version": "0.2.0", + "description": "MCP plugin to deploy and manage Hostinger websites, WordPress, domains, DNS, VPS, and subscriptions from inside Cursor.", "author": "Hostinger", "license": "MIT", "homepage": "https://hostinger.com/", "repository": { "type": "git", - "url": "https://github.com/hostinger/api-mcp-server" + "url": "https://github.com/hostinger/hostinger-cursor-plugin" }, "keywords": [ "hosting", @@ -17,7 +17,8 @@ "mcp", "hostinger", "dns", - "wordpress" + "wordpress", + "vps" ], "logo": "assets/logo.svg" } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4ff3c96 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +on: + pull_request: + push: + branches: [main, master] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Validate plugin manifest and component frontmatter + run: node scripts/validate-template.mjs + + - name: Check every referenced MCP tool exists + run: node scripts/check-tool-names.mjs + + - name: Check JSON files parse + run: | + for f in mcp.json hooks/hooks.json .cursor-plugin/plugin.json scripts/mcp-tools.json; do + node --input-type=module -e " + import { readFileSync } from 'node:fs'; + JSON.parse(readFileSync('$f', 'utf8')); + console.log('ok $f'); + " + done + + - name: Lint hook script + run: shellcheck scripts/check-no-token-leak.sh + + - name: Check hook script is executable + run: test -x scripts/check-no-token-leak.sh + + # The MCP server ships new tools regularly. This is advisory: it tells us the + # checked-in catalog has fallen behind, without failing PRs that didn't cause it. + catalog-drift: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Regenerate the catalog from the latest published server + run: node scripts/sync-mcp-tools.mjs + + - name: Report drift + run: | + if git diff --quiet -- scripts/mcp-tools.json; then + echo "Catalog is up to date with hostinger-api-mcp@latest." + else + echo "::warning::scripts/mcp-tools.json is behind hostinger-api-mcp@latest." + echo "Run 'node scripts/sync-mcp-tools.mjs' and commit the result." + git --no-pager diff --stat -- scripts/mcp-tools.json + fi diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..db94af1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# Changelog + +## [0.2.0] - 2026-08-06 + +Brings the plugin back in line with `hostinger-api-mcp` (now 1.29.0) and with the product surface of the Hostinger VS Code extension (1.3.2). The plugin was last touched on 2026-05-27, two days before OAuth shipped. + +### Fixed + +- **Every Hostinger tool name in the rules, skills, agent, and command was invented.** The plugin instructed the agent to call `list_hosting_plans`, `create_nodejs_deployment`, `list_dns_records`, `create_dns_snapshot`, `query_logs`, `restore_backup`, and others — none of which the MCP server has ever exposed. All 104 tool references now use the real OpenAPI operation IDs (`hosting_listWebsitesV1`, `DNS_getDNSRecordsV1`, `billing_getSubscriptionListV1`, …) and are verified in CI. +- **The token-leak hook never ran.** `hooks/hooks.json` used a `pre-commit` trigger, which is not a Cursor hook event, along with an array shape and a `script` key that Cursor's v1 schema doesn't read. It is now a `beforeShellExecution` hook matching `git commit`, and the script speaks the hook JSON protocol on stdin/stdout. It fails open, so a guard that can't read the staged diff won't block every commit. +- **The deployment guidance described an API that doesn't exist.** There is no framework "preset" parameter. `rules/framework-presets.mdc` invented seven preset names; the real `app_type` override accepts only `create-react-app`, `vite`, `angular`, `react`, `vue`, `parcel`, `express`, `fastify`, and `nest`, with everything else auto-detected from `package.json`. Replaced by `rules/nodejs-deployments.mdc`, documenting the real tools and the accepted `node_version`, `package_manager`, `root_directory`, `output_directory`, `build_script`, and `entry_file` overrides. +- **DNS guidance assumed per-record operations.** Hostinger's DNS API is zone-oriented: `DNS_updateDNSRecordsV1` takes a whole zone array plus an `overwrite` flag, and there is no tool to create a snapshot on demand. The skill now explains the `overwrite` semantics, uses `DNS_validateDNSRecordsV1` as a dry run, and cites an existing snapshot ID for rollback instead of promising a fresh backup. +- **`query-hosting-logs` promised access and error logs the API does not expose.** Renamed to `query-deployment-logs` and scoped to what exists: JS deployment logs, Node.js build logs, and cron output. The plugin now states plainly that HTTP access logs, PHP error logs, Node.js environment variables, and shared-hosting backups are hPanel-only. +- `repository.url` in `plugin.json` pointed at `hostinger/api-mcp-server` instead of this repository. +- Added the `LICENSE` file that `plugin.json` has always declared. + +### Changed + +- **One MCP server per product area instead of a single monolith.** `mcp.json` previously started `hostinger-api-mcp`, loading all 289 tools into context at once. It now registers eight servers — hosting, wordpress, domains, dns, billing, reach, ecommerce, vps — mirroring the VS Code extension's groups, so users can disable areas they don't use from Cursor's MCP settings. +- **OAuth is the default auth path.** The README claimed OAuth was "not currently supported by the Hostinger backend"; it shipped in the MCP server and in extension 1.1.0 on 2026-05-29. The server now signs in through the browser on the first authenticated tool call, and `mcp.json` no longer hardcodes `HOSTINGER_API_TOKEN` — the fragile `${HOSTINGER_API_TOKEN}` interpolation would have shadowed the OAuth fallback. An exported token still takes precedence. +- **Product naming matches the extension.** "Web Hosting" → Websites, "Reach" → Email Marketing, "Billing" → Subscriptions & Payments, and DNS is documented alongside Domains. +- The README now documents the overlap with the VS Code extension, which writes these same servers into `~/.cursor/mcp.json` — running both in Cursor duplicates every server. + +### Added + +- `USER_AGENT` on every MCP server (`plugin;cursor;`), matching the extension's attribution so plugin traffic is no longer invisible to Hostinger. +- `scripts/check-tool-names.mjs` — asserts every tool-shaped identifier in the docs resolves to a real tool. It default-denies unknown snake_case identifiers rather than checking known prefixes, because the original fabricated names had no prefix at all and a prefix check would have missed them entirely. +- `scripts/sync-mcp-tools.mjs` and `scripts/mcp-tools.json` — a checked-in snapshot of the published tool catalog (289 tools, 11 groups) so the check runs offline. +- `.github/workflows/ci.yml` — runs both validators, parses every JSON file, shellchecks the hook, and confirms it's executable. A second advisory job flags when the catalog snapshot falls behind `hostinger-api-mcp@latest`. + +## [0.1.0] - 2026-05-27 + +### Initial release + +- Hostinger MCP server wiring, five skills, three rules, one agent, one command. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..348f7f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +The MIT License (MIT) Copyright (c) 2026 Hostinger International Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +By using this plugin you agree to Hostinger's Terms of Service: +https://www.hostinger.com/legal/universal-terms-of-service-agreement diff --git a/README.md b/README.md index 2124239..6c09cb5 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,20 @@ # Hostinger Connector for Cursor -Official Cursor plugin for [Hostinger](https://hostinger.com/) — deploy and manage Hostinger hosting, Node.js apps, deployments, and DNS without leaving Cursor. +Official Cursor plugin for [Hostinger](https://hostinger.com/) — deploy and manage Hostinger websites, WordPress, domains, DNS, VPS, and subscriptions without leaving Cursor. -This plugin wires the official `hostinger-api-mcp` server, plus a set of skills and rules, into Cursor so the agent can take real actions on your Hostinger account. +The plugin wires the official [`hostinger-api-mcp`](https://www.npmjs.com/package/hostinger-api-mcp) servers into Cursor, plus a set of skills, rules, an agent, and a command so the agent can take real actions on your Hostinger account. --- ## What it does -- Deploy Node.js, SvelteKit, Hono, Remix, Fastify, Astro, and Next.js apps to Hostinger Managed Node.js Hosting. +- Deploy Node.js and static sites to Hostinger, then follow the server-side build to completion. - Diagnose failed builds from real Hostinger build logs. -- Read and write DNS records on Hostinger-managed domains. -- Troubleshoot Hostinger-hosted WordPress sites (logs, plugins, PHP version). -- Query hosting, access, and runtime logs for a domain. -- Inspect VPS state, billing, and subscriptions. - -Backed by 100+ MCP tools spanning hosting, domains, DNS, VPS, and billing. +- Read, validate, and update DNS zone records, with snapshot-based rollback. +- Manage Hostinger-hosted WordPress: installations, plugins, themes, core, caches, PHP settings. +- Manage domains — availability, registration, transfers, locks, forwarding, WHOIS. +- Inspect VPS state, firewalls, snapshots, and metrics. +- Review subscriptions, renewals, and payment methods. --- @@ -25,7 +24,7 @@ Backed by 100+ MCP tools spanning hosting, domains, DNS, VPS, and billing. [Install Hostinger Connector in Cursor](cursor://anysphere.cursor-deeplink/plugin/install?repo=hostinger/hostinger-cursor-plugin) -### From a `/add-plugin` URL +### From an `/add-plugin` URL In Cursor chat, run: @@ -33,59 +32,98 @@ In Cursor chat, run: /add-plugin https://github.com/hostinger/hostinger-cursor-plugin ``` +### Requirements + +Node.js 20 or newer must be on your `PATH`, since the MCP servers run via `npx`. Check with `node --version`. + --- ## Auth setup -The plugin uses the published [`hostinger-api-mcp`](https://www.npmjs.com/package/hostinger-api-mcp) server. Authentication is via a Hostinger API token: +**No token needed.** On the first tool call that touches your account, the MCP server registers an OAuth 2.0 client, opens your browser to sign in to Hostinger, and stores the credentials locally: + +- macOS / Linux: `~/.config/hostinger-mcp/credentials.json` (mode 0600) +- Windows: `%APPDATA%\hostinger-mcp\credentials.json` + +Access tokens refresh automatically, and the credentials are shared across every Hostinger MCP server, so you sign in once. + +To sign in ahead of time, or to sign out: + +```bash +npx --package=hostinger-api-mcp@latest hostinger-hosting-mcp --login +npx --package=hostinger-api-mcp@latest hostinger-hosting-mcp --logout +``` + +### API token (optional) + +For CI, scripting, or shared machines, an API token bypasses OAuth entirely: 1. Log in to [hpanel.hostinger.com](https://hpanel.hostinger.com) → **Profile & settings** → **API Tokens** → **Generate new token**. -2. Set the token in your shell environment before launching Cursor: +2. Export it before launching Cursor, so the editor's child processes inherit it: ```bash export HOSTINGER_API_TOKEN=hst_xxx... ``` -3. Restart Cursor so the new process inherits the variable. +3. Restart Cursor. -Never commit a token to source — the plugin ships a pre-commit hook that blocks obvious leaks. +`HOSTINGER_API_TOKEN` always takes precedence when set — no OAuth code runs. The plugin's `mcp.json` deliberately does **not** hardcode the variable, so the server can fall back to OAuth when it's absent. -> The `hostinger-api-mcp` CLI exposes `--login` / `--logout` flags for a future OAuth flow, but OAuth is **not currently supported** by the Hostinger backend. Use the API-token path until that ships. +Never commit a token. The plugin ships a `beforeShellExecution` hook that inspects the staged diff and blocks a `git commit` carrying what looks like a literal token. The hook fails open: if it can't read the diff, it allows the commit rather than blocking all of your commits. --- -## Available skills +## MCP servers -| Skill | What it does | -|---|---| -| [`deploy-nodejs-app`](skills/deploy-nodejs-app/SKILL.md) | Guide a Node.js deploy end-to-end with the right framework preset. | -| [`diagnose-build-failure`](skills/diagnose-build-failure/SKILL.md) | Pull build logs, identify the failure mode, propose a fix. | -| [`manage-dns-records`](skills/manage-dns-records/SKILL.md) | Read, create, update, delete DNS records — with snapshot + diff. | -| [`troubleshoot-wordpress`](skills/troubleshoot-wordpress/SKILL.md) | Diagnose WP issues: PHP version, error logs, plugin/theme conflicts. | -| [`query-hosting-logs`](skills/query-hosting-logs/SKILL.md) | Pull and summarize access / error / runtime logs for a domain. | +Each product area runs as its own MCP server. This keeps the tool count per server small instead of loading all 289 tools into context at once, and it lets you disable areas you don't use from Cursor's MCP settings. ---- +| Server | Binary | Tools | Covers | +|---|---|---:|---| +| `hostinger-hosting` | `hostinger-hosting-mcp` | 48 | Websites, Node.js builds and deployments, databases, cron, PHP, subdomains | +| `hostinger-wordpress` | `hostinger-wordpress-mcp` | 35 | WordPress installations, plugins, themes, core, LiteSpeed cache, maintenance mode | +| `hostinger-domains` | `hostinger-domains-mcp` | 36 | Availability, registration, transfers, locks, forwarding, WHOIS | +| `hostinger-dns` | `hostinger-dns-mcp` | 8 | Zone records, snapshots, validation | +| `hostinger-billing` | `hostinger-billing-mcp` | 9 | Subscriptions, auto-renewal, payment methods, catalog, orders | +| `hostinger-reach` | `hostinger-reach-mcp` | 12 | Contacts, segments, email marketing profiles | +| `hostinger-ecommerce` | `hostinger-ecommerce-mcp` | 12 | Stores, products, sales channels, shipping | +| `hostinger-vps` | `hostinger-vps-mcp` | 62 | Virtual machines, firewalls, snapshots, backups, SSH keys, metrics | -## Available MCP tools +These eight mirror the product groups in the [Hostinger VS Code extension](https://open-vsx.org/extension/hostinger/hostinger-connector). `hostinger-api-mcp` also publishes `hostinger-mail-mcp`, `hostinger-agency-hosting-mcp`, and `hostinger-horizons-mcp`, which neither the plugin nor the extension wires up yet. -The `hostinger` MCP server exposes 100+ tools across these areas: +Tool names are Hostinger's OpenAPI operation IDs — `hosting_listWebsitesV1`, `DNS_getDNSRecordsV1`, `billing_getSubscriptionListV1` — and the prefix tells you which server owns the call. WordPress tools share the `hosting_` prefix despite living on their own server. For the full catalog, see [`scripts/mcp-tools.json`](scripts/mcp-tools.json) or [hostinger/api-mcp-server](https://github.com/hostinger/api-mcp-server). -- **Hosting** — list plans, deploy Node.js / static / WordPress, retrieve build and runtime logs, manage SSH keys. -- **Domains** — search, register, transfer, lock/unlock, forwarding, WHOIS, verification. -- **DNS** — list/create/update/delete records, snapshots, restore, validation. -- **VPS** — list, power state, firewalls, OS reinstall, SSH keys, metrics, action history. -- **Billing** — subscriptions, auto-renewal, payment methods, catalog, orders. -- **WordPress** — list/install/update/disable plugins, themes, PHP settings, backups. +### Already using the VS Code extension? -For the complete tool catalog, see [hostinger/api-mcp-server](https://github.com/hostinger/api-mcp-server). +The extension writes these same servers into `~/.cursor/mcp.json` for whichever IDE it detects. If you run both the extension and this plugin in Cursor, you'll get two copies of every server and roughly 200 duplicate tools. Pick one: keep the plugin for Cursor, or disconnect the extension from Cursor's config. --- -## Rules (always-on guidance) +## What the API can't do + +Worth knowing up front, because the agent will tell you rather than inventing a tool: + +- **No raw access or PHP error logs.** Build, deployment, and cron logs are available; HTTP access logs and PHP error logs live in hPanel only. +- **No Node.js environment variables.** Set them in hPanel — there is no API for it. +- **No shared-hosting backups.** VPS backups and snapshots are exposed; shared-hosting backups are not. +- **No on-demand DNS snapshots.** Hostinger creates them automatically. You can list, read, and restore them, but not trigger one. + +--- + +## Available skills + +| Skill | What it does | +|---|---| +| [`deploy-nodejs-app`](skills/deploy-nodejs-app/SKILL.md) | Pick the right deploy tool, build a clean archive, follow the build to completion. | +| [`diagnose-build-failure`](skills/diagnose-build-failure/SKILL.md) | Pull build logs, match the failure signature, propose a concrete fix. | +| [`manage-dns-records`](skills/manage-dns-records/SKILL.md) | Read, validate, and update zone records; roll back via snapshots. | +| [`troubleshoot-wordpress`](skills/troubleshoot-wordpress/SKILL.md) | Diagnose WP issues: PHP settings, plugin/theme conflicts, caches, maintenance mode. | +| [`query-deployment-logs`](skills/query-deployment-logs/SKILL.md) | Pull and summarize build, deployment, and cron logs. | + +## Rules -- [`prefer-mcp-tools`](rules/prefer-mcp-tools.mdc) — use MCP tools instead of `curl` / `ssh` / one-off scripts. -- [`confirm-destructive-actions`](rules/confirm-destructive-actions.mdc) — require explicit confirmation for deletes, restores, redeploys. -- [`framework-presets`](rules/framework-presets.mdc) — pick the right Hostinger framework preset for Node.js deploys. +- [`prefer-mcp-tools`](rules/prefer-mcp-tools.mdc) — use MCP tools instead of `curl` / `ssh` / one-off scripts, and which server owns what. Always on. +- [`confirm-destructive-actions`](rules/confirm-destructive-actions.mdc) — require explicit confirmation before any mutating tool call. Always on. +- [`nodejs-deployments`](rules/nodejs-deployments.mdc) — which deploy tool to use, how to build the archive, and the accepted build override values. ## Agents @@ -93,25 +131,35 @@ For the complete tool catalog, see [hostinger/api-mcp-server](https://github.com ## Commands -- [`/hostinger-status`](commands/hostinger-status.md) — one-screen snapshot of plans, deployments, VPS, subscriptions. +- [`/hostinger-status`](commands/hostinger-status.md) — one-screen snapshot of websites, deployments, domains, VPS, and subscriptions. --- -## Validate +## Development ```bash +# Validate the plugin manifest and component frontmatter node scripts/validate-template.mjs + +# Assert every MCP tool named in the docs actually exists +node scripts/check-tool-names.mjs + +# Refresh the tool catalog after the MCP server ships new tools +node scripts/sync-mcp-tools.mjs ``` +`scripts/mcp-tools.json` is a checked-in snapshot of the published server's tool catalog, so `check-tool-names.mjs` runs offline in CI. Regenerate and commit it whenever the server adds tools. CI also runs an advisory job that flags when the snapshot has fallen behind `hostinger-api-mcp@latest`. + --- ## Links -- Hostinger Connector on Open VSX: https://open-vsx.org/extension/hostinger/hostinger-connector - MCP server source: https://github.com/hostinger/api-mcp-server +- MCP server on npm: https://www.npmjs.com/package/hostinger-api-mcp +- Hostinger VS Code extension: https://open-vsx.org/extension/hostinger/hostinger-connector - Hostinger API docs: https://developers.hostinger.com - Hostinger: https://hostinger.com/ ## License -MIT — see [`hostinger-api-mcp`](https://www.npmjs.com/package/hostinger-api-mcp). +MIT — see [`LICENSE`](LICENSE). diff --git a/agents/hostinger-deployment-reviewer.md b/agents/hostinger-deployment-reviewer.md index 20ebeab..969c7fa 100644 --- a/agents/hostinger-deployment-reviewer.md +++ b/agents/hostinger-deployment-reviewer.md @@ -7,33 +7,37 @@ description: Pre-flight review of a planned Hostinger deployment. Reads the proj ## When to invoke -- Before calling `create_nodejs_deployment` for a non-trivial deploy. +- Before calling `hosting_deployJsApplication` or `hosting_createNodeJSBuildFromArchiveV1` for a non-trivial deploy. - When the user asks "is this ready to ship to Hostinger?". - After a build failure, before retrying. ## What to check -1. **Framework preset** — does the detected framework match a Hostinger preset (see `rules/framework-presets.mdc`)? -2. **Node version** — does `engines.node` in `package.json` match a Hostinger-supported runtime? -3. **Start command** — does the `start` script bind to `process.env.PORT`? Hardcoded ports break on Hostinger. -4. **Env vars** — does the code reference any `process.env.X` that isn't set on the deployment? List the missing ones. -5. **Secrets** — is `.env` accidentally being uploaded? Is anything that looks like a token committed in source? -6. **Static assets** — are `dist/` / `build/` paths consistent between the build output and the preset's expected output dir? -7. **Domain state** — does the target domain resolve to Hostinger? Is SSL provisioned? -8. **Memory / size** — is the bundled output near the plan's size limit? +1. **Deploy tool** — is the right one selected? Anything with a `package.json` and a build script needs `hosting_deployJsApplication` or `hosting_createNodeJSBuildFromArchiveV1`, never `hosting_deployStaticWebsite`. +2. **Node version** — does `engines.node` resolve to `18`, `20`, `22`, or `24`? Anything else needs an explicit `node_version` override. +3. **Overrides** — if `app_type` is being set, is the value in the accepted enum (`create-react-app`, `vite`, `angular`, `react`, `vue`, `parcel`, `express`, `fastify`, `nest`)? For any other framework, `app_type` must be omitted and auto-detection allowed to run. +4. **Output directory** — does the build's real output path match `output_directory`? A mismatch builds cleanly and then serves a 404. +5. **Root directory** — in a monorepo, does `root_directory` point at the folder holding `package.json`? +6. **Package manager** — does the committed lockfile match `package_manager`? +7. **Start command / PORT** — does the entry point bind to `process.env.PORT`? A hardcoded port receives no traffic. +8. **Env vars** — list every `process.env.X` the code reads. These cannot be set through the API, so flag them for the user to add in hPanel before the first request. +9. **Archive hygiene** — is `node_modules/`, build output, `.git/`, and `.env` excluded? Is the archive under 50 MB? +10. **Secrets** — is anything that looks like a token committed in source? +11. **Domain state** — does `hosting_listWebsitesV1` show the target domain, and does `DNS_getDNSRecordsV1` point it at Hostinger? ## Output Reply with a checklist: ``` -- [x] Framework preset: -- [x] Node version: (supported) +- [x] Deploy tool: hosting_deployJsApplication (build required) +- [x] Node version: 22 (supported) +- [x] app_type: omitted — SvelteKit isn't in the enum, auto-detect will run - [ ] PORT: hardcoded in server.js:42 — change to process.env.PORT -- [ ] Env vars: missing DATABASE_URL, STRIPE_SECRET_KEY +- [ ] Env vars: DATABASE_URL, STRIPE_SECRET_KEY must be set in hPanel (not settable via API) +- [x] Archive: node_modules, dist, .git, .env excluded — 3.1 MB - [x] Secrets: clean -- [x] Build output: ./dist matches preset -- [x] Domain: example.com points to Hostinger, SSL active +- [x] Domain: example.com listed, apex A record points to Hostinger ``` End with a one-line verdict: "Ready to deploy" or "Block: issues to fix first". @@ -42,3 +46,4 @@ End with a one-line verdict: "Ready to deploy" or "Block: issues to fix firs - Do not actually deploy — this agent only reviews. - Do not modify files without user confirmation. +- Do not recommend a "framework preset" — Hostinger has no preset parameter. Recommend specific overrides instead. diff --git a/commands/hostinger-status.md b/commands/hostinger-status.md index 6dc32b3..b6544f9 100644 --- a/commands/hostinger-status.md +++ b/commands/hostinger-status.md @@ -1,38 +1,46 @@ --- name: hostinger-status -description: Print a one-screen summary of the user's Hostinger account — active hosting plans, recent deployments, VPS power state, and any subscriptions expiring soon. +description: Print a one-screen summary of the user's Hostinger account — websites, recent deployments, domains, VPS power state, and any subscriptions expiring soon. --- # /hostinger-status -Run a fast snapshot of the user's Hostinger account. +Run a fast, read-only snapshot of the user's Hostinger account. ## Steps -1. Call the `hostinger` MCP server: - - `list_hosting_plans` → name, type, status. - - `list_recent_deployments` (last 5 across all domains) → domain, status, timestamp. - - `list_vps` → hostname, power state. - - `list_subscriptions` → next renewal date, auto-renewal flag. -2. Render as four compact sections — do not paginate. +1. Call these tools in parallel where possible: + - `hosting_listWebsitesV1` → domain, username, enabled state. + - `hosting_listJsDeployments` per domain that has deployments → state (`pending`, `running`, `completed`, `failed`) and timestamp. Skip domains with none rather than erroring. + - `domains_getDomainListV1` → registered domains and expiry. + - `VPS_getVirtualMachinesV1` → hostname and power state. + - `billing_getSubscriptionListV1` → next renewal date and auto-renewal flag. +2. Render as compact sections — do not paginate. +3. Flag anything that needs attention with `[!]`: a failed deployment, a disabled website, a stopped VPS, or a subscription renewing within 30 days without auto-renewal. + +If a server isn't enabled in the user's Cursor MCP settings, its section will error. Note the section as unavailable and carry on — don't abort the whole snapshot. ## Output format ``` -Hosting (N plans) -- example.com — Premium, active -- shop.example — Business, suspended +Websites (N) +- example.com — enabled +- shop.example — disabled [!] + +Recent deployments +- example.com — completed, 2h ago +- api.example — failed, 6h ago [!] -Recent deployments (last 5) -- example.com — success, 2h ago -- api.example — failed, 6h ago +Domains (N) +- example.com — expires 2027-03-14 +- shop.example — expires 2026-09-02 VPS (N) - srv-1 — running -- srv-2 — stopped +- srv-2 — stopped [!] Subscriptions -- Premium hosting — renews 2026-07-01 (auto) +- Premium hosting — renews 2027-01-01 (auto) - Domain example.com — renews 2026-08-15 (manual) [!] ``` @@ -40,3 +48,4 @@ Subscriptions - Do not perform any write operations from this command. - Do not dump raw API responses — summarize. +- Do not invent a section for data the enabled servers didn't return. diff --git a/hooks/hooks.json b/hooks/hooks.json index 4b0346a..23381f8 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,10 +1,13 @@ { - "hooks": [ - { - "name": "block-hostinger-token-commit", - "trigger": "pre-commit", - "description": "Block committing files that look like a leaked Hostinger API token.", - "script": "scripts/check-no-token-leak.sh" - } - ] + "version": 1, + "hooks": { + "beforeShellExecution": [ + { + "command": "scripts/check-no-token-leak.sh", + "matcher": "git\\s.*commit", + "timeout": 10, + "failClosed": false + } + ] + } } diff --git a/mcp.json b/mcp.json index abc6065..651a87d 100644 --- a/mcp.json +++ b/mcp.json @@ -1,11 +1,44 @@ { "mcpServers": { - "hostinger": { + "hostinger-hosting": { "command": "npx", - "args": ["-y", "hostinger-api-mcp@latest"], - "env": { - "HOSTINGER_API_TOKEN": "${HOSTINGER_API_TOKEN}" - } + "args": ["--yes", "--package=hostinger-api-mcp@latest", "hostinger-hosting-mcp"], + "env": { "USER_AGENT": "plugin;cursor;0.2.0" } + }, + "hostinger-wordpress": { + "command": "npx", + "args": ["--yes", "--package=hostinger-api-mcp@latest", "hostinger-wordpress-mcp"], + "env": { "USER_AGENT": "plugin;cursor;0.2.0" } + }, + "hostinger-domains": { + "command": "npx", + "args": ["--yes", "--package=hostinger-api-mcp@latest", "hostinger-domains-mcp"], + "env": { "USER_AGENT": "plugin;cursor;0.2.0" } + }, + "hostinger-dns": { + "command": "npx", + "args": ["--yes", "--package=hostinger-api-mcp@latest", "hostinger-dns-mcp"], + "env": { "USER_AGENT": "plugin;cursor;0.2.0" } + }, + "hostinger-billing": { + "command": "npx", + "args": ["--yes", "--package=hostinger-api-mcp@latest", "hostinger-billing-mcp"], + "env": { "USER_AGENT": "plugin;cursor;0.2.0" } + }, + "hostinger-reach": { + "command": "npx", + "args": ["--yes", "--package=hostinger-api-mcp@latest", "hostinger-reach-mcp"], + "env": { "USER_AGENT": "plugin;cursor;0.2.0" } + }, + "hostinger-ecommerce": { + "command": "npx", + "args": ["--yes", "--package=hostinger-api-mcp@latest", "hostinger-ecommerce-mcp"], + "env": { "USER_AGENT": "plugin;cursor;0.2.0" } + }, + "hostinger-vps": { + "command": "npx", + "args": ["--yes", "--package=hostinger-api-mcp@latest", "hostinger-vps-mcp"], + "env": { "USER_AGENT": "plugin;cursor;0.2.0" } } } } diff --git a/rules/confirm-destructive-actions.mdc b/rules/confirm-destructive-actions.mdc index 0f9bb9e..ceaf33d 100644 --- a/rules/confirm-destructive-actions.mdc +++ b/rules/confirm-destructive-actions.mdc @@ -9,13 +9,53 @@ Before invoking any Hostinger MCP tool that **mutates or destroys** state, show ## Always confirm -- **Domains**: `delete_domain`, `transfer_domain`, `unlock_domain`, changing forwarding, changing nameservers. -- **DNS**: `delete_dns_record`, bulk record replacements, NS changes, apex A/AAAA changes, MX record replacement. -- **Hosting**: `delete_website`, `restore_backup`, `update_php_version`, `disable_wordpress_plugin` on production, plan downgrades. -- **VPS**: `restart_vps`, `stop_vps`, `reinstall_os`, firewall rule deletions, snapshot deletions. -- **Databases**: `drop_database`, `delete_database_user`, restoring a DB backup (overwrites current). -- **Deployments**: redeploying production, rolling back, deleting a deployment. -- **Billing**: cancelling a subscription, disabling auto-renewal on an active service. +**Websites & hosting** + +- `hosting_deleteWebsiteV1`, `hosting_deleteWebsiteSubdomainV1`, `hosting_deleteWebsiteParkedDomainV1` +- `hosting_deployJsApplication`, `hosting_deployStaticWebsite`, `hosting_createNodeJSBuildFromArchiveV1` — these overwrite what is currently live +- `hosting_restartNode_jsApplicationV1`, `hosting_patchNode_jsVulnerabilitiesV1` +- `hosting_updatePHPVersionV1`, `hosting_updatePHPOptionsV1`, `hosting_updatePHPExtensionsV1`, `hosting_resetPHPExtensionsV1` +- `hosting_deleteAccountDatabaseV1`, `hosting_changeDatabasePasswordV1`, `hosting_repairDatabaseV1`, `hosting_deleteDatabaseRemoteConnectionV1` +- `hosting_deleteAccountCronJobV1` + +**WordPress** + +- `hosting_deleteWordPressInstallationV1`, `hosting_updateWordPressCoreV1` +- `hosting_uninstallWordPressPluginsV1`, `hosting_deactivateWordPressPluginV1`, `hosting_updateWordPressPluginsV1` +- `hosting_uninstallWordPressThemesV1`, `hosting_activateWordPressThemeV1`, `hosting_updateWordPressThemesV1` +- `hosting_toggleMaintenanceModeV1` on a production site + +**Domains** + +- `domains_purchaseNewDomainV1` (spends money), `domains_disableDomainLockV1`, `domains_updateDomainNameserversV1` +- `domains_createDomainForwardingV1`, `domains_updateDomainForwardingV1`, `domains_deleteDomainForwardingV1` +- `domains_startOutgoingDomainMoveV1`, `domains_acceptIncomingDomainMoveV1`, `domains_rejectIncomingDomainMoveV1` +- `domains_deleteWHOISProfileV1`, `domains_disablePrivacyProtectionV1` + +**DNS** + +- `DNS_updateDNSRecordsV1` — always, and especially with `overwrite: true`, which replaces matching records +- `DNS_deleteDNSRecordsV1`, `DNS_resetDNSRecordsV1` (resets the whole zone to defaults), `DNS_restoreDNSSnapshotV1` +- Any change to apex `A`/`AAAA`, `NS`, or `MX` records — these break live traffic or mail delivery + +**VPS** + +- `VPS_stopVirtualMachineV1`, `VPS_restartVirtualMachineV1`, `VPS_recreateVirtualMachineV1` (destroys all data), `VPS_purchaseNewVirtualMachineV1` +- `VPS_restoreSnapshotV1`, `VPS_deleteSnapshotV1`, `VPS_restoreBackupV1` +- `VPS_setRootPasswordV1`, `VPS_setPanelPasswordV1`, `VPS_deletePublicKeyV1` +- `VPS_deleteFirewallV1`, `VPS_deleteFirewallRuleV1`, `VPS_deactivateFirewallV1`, `VPS_syncFirewallV1` +- `VPS_deleteProjectV1`, `VPS_stopProjectV1`, `VPS_restartProjectV1` +- `VPS_startRecoveryModeV1`, `VPS_deletePTRRecordV1`, `VPS_uninstallMonarxV1` + +**Subscriptions & payments** + +- `billing_createPurchaseOrderV1`, `billing_renewSubscriptionV1` (both spend money) +- `billing_disableAutoRenewalV1` on an active service, `billing_deletePaymentMethodV1` + +**Ecommerce & email marketing** + +- `ecommerce_deleteStoreV1`, `ecommerce_updateSalesChannelV1`, `ecommerce_setStoreShippingV1` +- `reach_deleteAContactV1` ## Confirmation format @@ -23,11 +63,11 @@ Reply with: 1. **Action** — one sentence ("I'm about to delete the `mail` A record on `example.com`."). 2. **Impact** — one or two sentences on what breaks if this is wrong. -3. **Recovery** — how to undo, if it's reversible (e.g. DNS snapshot ID, backup file, etc.). +3. **Recovery** — how to undo, if it's reversible (e.g. the DNS snapshot ID from `DNS_getDNSSnapshotListV1`, a VPS snapshot ID, etc.). 4. End with a clear question: "Proceed? (yes / no)". Do not chain destructive operations — confirm each one separately, even if the user already approved a related action earlier in the conversation. ## Read-only is safe -`list_*`, `get_*`, `describe_*`, `check_*` tools never need confirmation. +Tools whose names read as `list*`, `get*`, `show*`, `check*`, or `validate*` never need confirmation. `DNS_validateDNSRecordsV1` is safe by design — it is the dry run for `DNS_updateDNSRecordsV1`, so prefer calling it first rather than asking the user to approve an unvalidated change. diff --git a/rules/framework-presets.mdc b/rules/framework-presets.mdc deleted file mode 100644 index 33d5811..0000000 --- a/rules/framework-presets.mdc +++ /dev/null @@ -1,36 +0,0 @@ ---- -description: Use Hostinger's framework presets when generating deployment configs for Node.js apps -alwaysApply: false ---- - -# Use Hostinger framework presets - -When generating a deployment config or guiding a `deploy-nodejs-app` flow, **detect the framework first and pick the matching Hostinger preset**. Do not default to "generic Node" if a specific preset exists — presets bake in correct build commands, output dirs, and runtime expectations. - -## Recognized presets - -| Framework | Detect by | Preset name | Default build / start | -|---|---|---|---| -| **SvelteKit** | `@sveltejs/kit` in `dependencies` | `sveltekit` | `npm run build` → `node build` | -| **Hono** | `hono` in `dependencies` | `hono` | `npm run build` (if present) → `node dist/index.js` | -| **Remix** | `@remix-run/*` in `dependencies` | `remix` | `npm run build` → `npm start` (`remix-serve`) | -| **Fastify** | `fastify` in `dependencies`, no Remix/Hono | `fastify` | `npm run build` (optional) → `node server.js` | -| **Astro** | `astro` in `dependencies` | `astro` | `npm run build` → `node ./dist/server/entry.mjs` (SSR) or static | -| **Next.js** | `next` in `dependencies` | `nextjs` | `npm run build` → `npm start` | -| **Express / generic Node** | none of the above | `nodejs` | `npm start` | - -## Picking the preset - -1. Read `package.json` first. -2. If multiple framework deps are present (unusual), ask the user which is the primary runtime. -3. If `package.json` is unavailable, ask the user to name the framework rather than guessing. - -## When the preset doesn't fit - -- If the user has a custom build command that doesn't match the preset's defaults, override `build` and `start` in the deployment config but keep the preset (so static / SSR routing still works). -- If no preset fits, fall back to `nodejs` and explicitly call out that no specific preset was used. - -## Do not - -- Do not silently fall back to `nodejs` when a more specific preset applies — the build cache and runtime config will be wrong. -- Do not invent preset names — only use the ones in the table above. diff --git a/rules/nodejs-deployments.mdc b/rules/nodejs-deployments.mdc new file mode 100644 index 0000000..b85fc8a --- /dev/null +++ b/rules/nodejs-deployments.mdc @@ -0,0 +1,58 @@ +--- +description: Pick the right Hostinger deployment tool and build overrides when shipping Node.js or static sites +alwaysApply: false +--- + +# Deploying to Hostinger + +Hostinger deploys from an **uploaded archive**, and the build runs server-side. There is no framework "preset" parameter — build settings are auto-detected from `package.json`, and you may override individual fields. Pick the tool that matches the job. + +## Which tool + +| Situation | Tool | +|---|---| +| Node.js / JS app, let Hostinger auto-detect everything | `hosting_deployJsApplication` | +| Node.js app where you must override Node version, entry file, build script, or output dir | `hosting_createNodeJSBuildFromArchiveV1` | +| Pre-built static files, no build step at all | `hosting_deployStaticWebsite` | + +`hosting_deployJsApplication` is the default choice — it takes just `domain` and `archivePath` and resolves the hosting username itself. Reach for `hosting_createNodeJSBuildFromArchiveV1` only when an override is actually needed; it additionally requires `username`, which you can get from `hosting_listWebsitesV1`. + +Do not use `hosting_deployStaticWebsite` for anything with a `package.json` or a build command — it extracts the archive verbatim and serves it, with no install or build step. + +## Building the archive + +Applies to all three tools: + +- Include **source only**. Exclude `node_modules/` and build output (`dist/`, `build/`, `.next/`, `.svelte-kit/`, `.output/`) — the server runs the install and build itself, and shipping them just bloats the upload. +- Also exclude anything matched by `.gitignore` when that file exists. +- Never include `.env` or any file holding credentials. +- Supported formats: `zip`, `tar`, `tar.gz`, `tgz`, `7z`, `gz`. `hosting_createNodeJSBuildFromArchiveV1` accepts `.zip`, `.tar.gz`, `.tgz` only, and caps the archive at **50 MB**. +- For `hosting_deployStaticWebsite`, name the archive exactly `_YYYYMMDD_HHMMSS.zip` (e.g. `mystaticwebsite_20260806_143022.zip`). + +```bash +zip -r myapp.zip . --exclude "node_modules/*" --exclude "dist/*" --exclude ".git/*" --exclude ".env" +``` + +## Valid override values + +Only these values are accepted — do not invent others. + +- `node_version`: `18`, `20`, `22`, `24`. Omit to auto-detect from `engines.node`. +- `app_type`: `create-react-app`, `vite`, `angular`, `react`, `vue`, `parcel`, `express`, `fastify`, `nest`. Omit to auto-detect. +- `package_manager`: `npm`, `yarn`, `pnpm`. Omit to auto-detect from the lockfile. +- `root_directory` — where `package.json` lives, relative to `public_html`. Needed for monorepos. +- `output_directory` — build output, relative to the root directory. +- `build_script`, `entry_file` — free-form string overrides. + +If the project's framework isn't in the `app_type` list (SvelteKit, Remix, Astro, Hono, Nuxt, and so on), **omit `app_type` entirely** and let auto-detection handle it, overriding `build_script`, `entry_file`, and `output_directory` if the detected values are wrong. Do not force an unrelated `app_type` value as a stand-in. + +## After submitting + +1. `hosting_listJsDeployments` (or `hosting_listNodeJSBuildsV1`) to get the build `uuid` and state — `pending`, `running`, `completed`, or `failed`. +2. Poll logs while the state is `running`: `hosting_showJsDeploymentLogs` for `deployJsApplication` builds, or `hosting_getNodeJSBuildLogsV1` for archive builds. Pass the previously returned line count as `fromLine` / `from_line` so you only fetch new output. +3. On `failed`, hand off to the `diagnose-build-failure` skill. + +## Runtime expectations + +- The app must bind to `process.env.PORT`. A hardcoded port will not receive traffic. +- Node.js application environment variables cannot be set through the API. If the app needs them, tell the user to add them in hPanel before the first request — don't pretend an MCP tool can do it. diff --git a/rules/prefer-mcp-tools.mdc b/rules/prefer-mcp-tools.mdc index c418e38..c942d8f 100644 --- a/rules/prefer-mcp-tools.mdc +++ b/rules/prefer-mcp-tools.mdc @@ -3,9 +3,9 @@ description: Prefer Hostinger MCP tools over generic shell, curl, or hand-rolled alwaysApply: true --- -# Use the Hostinger MCP server, not shell hacks +# Use the Hostinger MCP servers, not shell hacks -When the user asks about anything on Hostinger — hosting, domains, DNS, VPS, billing, deployments, WordPress, Node.js apps — call a tool on the `hostinger` MCP server (this plugin's MCP) instead of: +When the user asks about anything on Hostinger — websites, WordPress, domains, DNS, VPS, subscriptions, email marketing, ecommerce — call a tool on one of this plugin's Hostinger MCP servers instead of: - `curl` / `wget` against `https://developers.hostinger.com` - `ssh` into a host to read files or run commands @@ -14,17 +14,42 @@ When the user asks about anything on Hostinger — hosting, domains, DNS, VPS, b ## Why -- The MCP tools are typed and validated. -- They handle authentication consistently via `HOSTINGER_API_TOKEN`. +- The MCP tools are typed and validated against Hostinger's OpenAPI spec. +- They handle authentication consistently (OAuth by default, `HOSTINGER_API_TOKEN` when set). - They surface structured errors that the agent can act on. - They don't leak tokens into shell history. -## Exceptions +## Which server to use -- Read-only `dig` / `nslookup` against public DNS is fine for *verification* after an MCP-driven change, never as the primary read path. -- If a needed capability genuinely isn't covered by any MCP tool, tell the user that explicitly before falling back to the raw API — don't quietly skip the MCP layer. +The plugin registers one MCP server per product area. Tool names are prefixed by area, so the prefix tells you which server owns the call: + +| Server | Tool prefix | Covers | +|---|---|---| +| `hostinger-hosting` | `hosting_` | Websites, Node.js builds & deployments, databases, cron, PHP, subdomains | +| `hostinger-wordpress` | `hosting_` | WordPress installations, plugins, themes, core, LiteSpeed cache, maintenance mode | +| `hostinger-domains` | `domains_` | Availability, registration, transfers, locks, forwarding, WHOIS | +| `hostinger-dns` | `DNS_` | Zone records, snapshots, validation | +| `hostinger-billing` | `billing_` | Subscriptions, auto-renewal, payment methods, catalog, orders | +| `hostinger-reach` | `reach_` | Contacts, segments, email marketing profiles | +| `hostinger-ecommerce` | `ecommerce_` | Stores, products, sales channels, shipping | +| `hostinger-vps` | `VPS_` | Virtual machines, firewalls, snapshots, backups, SSH keys, metrics | + +Note that WordPress tools also use the `hosting_` prefix — they live on a separate server but share the namespace. ## Tool selection -- Prefer the most specific tool for the task (e.g. `update_dns_record` over a generic batch tool). -- Fall back to broader `list_*` / `get_*` tools only when no specific tool exists. +- Discover before you guess. Tool names follow Hostinger's OpenAPI operation IDs — `hosting_listWebsitesV1`, `DNS_getDNSRecordsV1`, `billing_getSubscriptionListV1` — not generic verbs of the *list_websites* or *create_deployment* shape. If you are unsure of an exact name, list the server's tools rather than inventing one. +- Several Hostinger endpoints are deliberately **batch-oriented**. Do not look for a per-item tool that does not exist — for example DNS has no single-record create/update tool; `DNS_updateDNSRecordsV1` takes a whole zone array and an `overwrite` flag. +- Read before you write: pair a `get`/`list`/`show` call with every mutation so you can report the before/after state. + +## Not covered by the API + +Some things users ask for have no MCP tool. Say so plainly instead of substituting a plausible-sounding call: + +- Raw HTTP access logs and PHP error logs are not exposed by the API — point the user to hPanel. Build and deployment logs *are* available (see the `query-deployment-logs` skill). +- Node.js application environment variables cannot be managed through the API; they are set in hPanel. + +## Exceptions + +- Read-only `dig` / `nslookup` against public DNS is fine for *verification* after an MCP-driven change, never as the primary read path. +- If a needed capability genuinely isn't covered by any MCP tool, tell the user explicitly before falling back to the raw API — don't quietly skip the MCP layer. diff --git a/scripts/check-no-token-leak.sh b/scripts/check-no-token-leak.sh index 496f7be..9241559 100755 --- a/scripts/check-no-token-leak.sh +++ b/scripts/check-no-token-leak.sh @@ -1,19 +1,42 @@ #!/usr/bin/env bash -# Refuse the commit if any staged file looks like a leaked Hostinger API token. -# Hostinger personal access tokens are long opaque strings; the safest signal is -# a `HOSTINGER_API_TOKEN=` assignment with a non-empty literal value committed -# to a tracked file. +# Cursor `beforeShellExecution` hook: deny a `git commit` that would record +# something looking like a Hostinger API token. +# +# Contract (Cursor hooks v1): hook input arrives as JSON on stdin, the decision +# is JSON on stdout. We don't need any field from the input — the staged diff is +# the whole source of truth — so stdin is drained and discarded. Draining +# matters: exiting without reading can hand the caller an EPIPE. +# +# Fails open. The hook is registered with `failClosed: false` and every +# unexpected condition below returns `allow`, because a guard that cannot read +# the staged diff must not block every commit the user makes. -set -euo pipefail +set -uo pipefail -leaked=$(git diff --cached -U0 -- ':!*.example' ':!*.md' \ - | grep -E '^\+[^+].*HOSTINGER_API_TOKEN\s*=\s*["'\'']?[A-Za-z0-9_\-]{16,}' \ - || true) +cat >/dev/null 2>&1 || true -if [[ -n "$leaked" ]]; then - echo "Refusing commit: looks like a Hostinger API token is being committed." >&2 - echo "Use environment variables or a secret store instead." >&2 - echo "" >&2 - echo "$leaked" >&2 - exit 1 +allow() { + printf '{"permission":"allow"}\n' + exit 0 +} + +command -v git >/dev/null 2>&1 || allow +git rev-parse --is-inside-work-tree >/dev/null 2>&1 || allow + +# Added lines only, skipping docs and .example files where a placeholder +# assignment is legitimate. +leaked=$( + git diff --cached -U0 -- ':!*.example' ':!*.md' 2>/dev/null \ + | grep -E '^\+[^+].*(HOSTINGER_API_TOKEN|API_TOKEN)[[:space:]]*[=:][[:space:]]*["'\'']?[A-Za-z0-9_-]{16,}' \ + || true +) + +if [ -n "$leaked" ]; then + # Report which files are implicated rather than echoing the matched lines — + # writing the token into the transcript is the thing we're preventing. + files=$(git diff --cached --name-only -- ':!*.example' ':!*.md' 2>/dev/null | tr '\n' ' ') + printf '{"permission":"deny","user_message":"Blocked: a staged change looks like it contains a Hostinger API token.","agent_message":"A staged change appears to assign a literal Hostinger API token. Do not commit it. Unstage the value, replace it with a placeholder or an environment variable reference, and set the real token via the HOSTINGER_API_TOKEN environment variable instead. Staged files: %s"}\n' "$files" + exit 0 fi + +allow diff --git a/scripts/check-tool-names.mjs b/scripts/check-tool-names.mjs new file mode 100644 index 0000000..5c99f37 --- /dev/null +++ b/scripts/check-tool-names.mjs @@ -0,0 +1,194 @@ +#!/usr/bin/env node +/** + * Assert that every Hostinger MCP tool named in this plugin's rules, skills, + * agents, commands, and README actually exists. + * + * This exists because the plugin originally shipped guidance built entirely on + * invented tool names (`list_hosting_plans`, `create_dns_snapshot`, + * `query_logs`, ...). The agent would then hunt for tools the server has never + * exposed. Checking only known prefixes would not have caught that, since the + * invented names had no prefix at all — so this default-denies: any backticked + * snake_case-looking identifier must either resolve to a real tool or be listed + * in NON_TOOL_IDENTIFIERS below. + * + * Catalog source: scripts/mcp-tools.json (regenerate with sync-mcp-tools.mjs). + */ + +import { readFileSync, readdirSync, statSync } from "node:fs"; +import path from "node:path"; +import process from "node:process"; + +const repoRoot = path.resolve(import.meta.dirname, ".."); +const catalog = JSON.parse(readFileSync(path.join(repoRoot, "scripts", "mcp-tools.json"), "utf8")); + +const knownTools = new Set(Object.values(catalog.groups).flat()); + +/** + * Identifiers that look like tool names but aren't: API request/response fields, + * npm and filesystem names, WordPress internals, shell and code fragments. + * Additions here should be things a reader could otherwise mistake for a tool. + */ +const NON_TOOL_IDENTIFIERS = new Set([ + // Deployment / build request fields + "app_type", + "build_script", + "entry_file", + "from_line", + "node_version", + "output_directory", + "package_manager", + "root_directory", + "public_html", + // Pagination and filter fields + "is_enabled", + "order_id", + "per_page", + "snapshot_id", + // package.json / npm / filesystem + "create-react-app", + "engines.node", + "legacy-peer-deps", + "node_modules", + "package.json", + "package-lock.json", + // Env vars and code fragments + "HOSTINGER_API_TOKEN", + "API_TOKEN", + "USER_AGENT", + "process.env.PORT", + "max_execution_time", + "memory_limit", + // WordPress internals + "wp-config.php", + "wp_options", + "wp-admin", + "siteurl", +]); + +/** Ignore fenced code blocks: JSON examples and shell snippets aren't guidance. */ +function stripFencedBlocks(text) { + return text.replace(/^```[\s\S]*?^```/gm, (block) => block.replace(/[^\n]/g, " ")); +} + +/** + * A backticked token is "tool-shaped" if it could plausibly be read as an MCP + * tool name: a bare identifier containing an underscore, no whitespace, and no + * path or call syntax. Hostinger's own names are mixed-case with an underscore + * separator (hosting_listWebsitesV1), and the invented ones were snake_case — + * both land here. + */ +function isToolShaped(token) { + if (!token.includes("_")) return false; + if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(token)) return false; + // A trailing underscore is a prefix reference (`hosting_`, `DNS_`), not a name. + if (token.endsWith("_")) return false; + return true; +} + +function walk(target) { + const found = []; + const stack = [target]; + while (stack.length > 0) { + const current = stack.pop(); + let info; + try { + info = statSync(current); + } catch { + continue; + } + if (info.isDirectory()) { + for (const entry of readdirSync(current)) stack.push(path.join(current, entry)); + } else if (/\.(md|mdc|markdown)$/i.test(current)) { + found.push(current); + } + } + return found; +} + +// Everything the agent reads as instructions, plus the user-facing README. +// CHANGELOG.md is deliberately out of scope: it has to name the tools that were +// wrong in order to describe having fixed them. +const targets = ["rules", "skills", "agents", "commands", "README.md"].flatMap((t) => + walk(path.join(repoRoot, t)), +); + +const problems = []; +const seen = new Set(); + +for (const file of targets) { + const relative = path.relative(repoRoot, file); + const lines = stripFencedBlocks(readFileSync(file, "utf8")).split("\n"); + + lines.forEach((line, index) => { + for (const [, token] of line.matchAll(/`([^`\n]+)`/g)) { + if (!isToolShaped(token)) continue; + if (NON_TOOL_IDENTIFIERS.has(token)) continue; + if (knownTools.has(token)) { + seen.add(token); + continue; + } + problems.push({ file: relative, line: index + 1, token }); + } + }); +} + +// The mcp.json binaries must map onto real tool groups, otherwise a server +// starts with zero tools and nothing obviously fails. +const mcpConfig = JSON.parse(readFileSync(path.join(repoRoot, "mcp.json"), "utf8")); +const manifest = JSON.parse( + readFileSync(path.join(repoRoot, ".cursor-plugin", "plugin.json"), "utf8"), +); +const binaryProblems = []; + +for (const [key, server] of Object.entries(mcpConfig.mcpServers ?? {})) { + const binary = (server.args ?? []).at(-1); + const group = String(binary).replace(/^hostinger-/, "").replace(/-mcp$/, ""); + if (!Object.hasOwn(catalog.groups, group)) { + binaryProblems.push(`${key}: "${binary}" does not map to a tool group in the catalog`); + } + + // USER_AGENT carries the plugin version for Hostinger-side attribution, so a + // version bump that misses mcp.json would silently report the old one. + const expectedUserAgent = `plugin;cursor;${manifest.version}`; + const actualUserAgent = server.env?.USER_AGENT; + if (actualUserAgent !== expectedUserAgent) { + binaryProblems.push( + `${key}: USER_AGENT is "${actualUserAgent}", expected "${expectedUserAgent}" to match plugin.json`, + ); + } +} + +if (problems.length > 0 || binaryProblems.length > 0) { + console.error(`Check failed against ${catalog.package}@${catalog.version}.\n`); + + for (const { file, line, token } of problems) { + console.error(` ${file}:${line} unknown tool \`${token}\``); + } + for (const message of binaryProblems) { + console.error(` mcp.json ${message}`); + } + + if (problems.length > 0) { + console.error( + [ + "", + "Every backticked identifier that reads like an MCP tool must exist in", + "scripts/mcp-tools.json. If the catalog is stale, regenerate it:", + "", + " node scripts/sync-mcp-tools.mjs", + "", + "If the identifier is a request field or some other non-tool name, add it", + "to NON_TOOL_IDENTIFIERS in scripts/check-tool-names.mjs.", + ].join("\n"), + ); + } + + process.exit(1); +} + +console.log( + `Tool-name check passed: ${seen.size} distinct tools referenced, all present in ${catalog.package}@${catalog.version}.`, +); +console.log( + `MCP binary check passed: ${Object.keys(mcpConfig.mcpServers ?? {}).length} servers map to real tool groups.`, +); diff --git a/scripts/mcp-tools.json b/scripts/mcp-tools.json new file mode 100644 index 0000000..b88f8bc --- /dev/null +++ b/scripts/mcp-tools.json @@ -0,0 +1,318 @@ +{ + "package": "hostinger-api-mcp", + "version": "1.29.0", + "total": 289, + "groups": { + "agency-hosting": [ + "agency-hosting_buildAgencyPlanWebsiteNodeJSAssetsV1", + "agency-hosting_changeAgencyPlanWebsiteDomainV1", + "agency-hosting_changeAgencyPlanWebsiteWordPressCoreVersionV1", + "agency-hosting_clearAgencyPlanWebsiteCacheV1", + "agency-hosting_createAgencyPlanWebsiteCronJobV1", + "agency-hosting_createAgencyPlanWebsiteDatabaseUserV1", + "agency-hosting_createAgencyPlanWebsiteDatabaseV1", + "agency-hosting_deleteAgencyPlanWebsiteCronJobV1", + "agency-hosting_deleteAgencyPlanWebsiteDatabaseUserV1", + "agency-hosting_deleteAgencyPlanWebsiteDatabaseV1", + "agency-hosting_deleteAgencyPlanWebsiteV1", + "agency-hosting_getAgencyPlanWebsiteDetailsV1", + "agency-hosting_getAgencyPlanWebsiteSetupStatusV1", + "agency-hosting_getAgencyPlanWebsiteWordPressSettingsV1", + "agency-hosting_importAgencyPlanWebsiteFromArchiveV1", + "agency-hosting_linkDomainToAgencyPlanWebsiteV1", + "agency-hosting_listAgencyPlanDomainsV1", + "agency-hosting_listAgencyPlanOrdersV1", + "agency-hosting_listAgencyPlanWebsiteCronJobsV1", + "agency-hosting_listAgencyPlanWebsiteDatabasesV1", + "agency-hosting_listAvailableDatacentersForAnAgencyPlanOrderV1", + "agency-hosting_listAvailableWordPressVersionsForAnAgencyPlanWebsiteV1", + "agency-hosting_listRunningAgencyPlanWebsiteProcessesV1", + "agency-hosting_provisionANewAgencyPlanWebsiteV1", + "agency-hosting_unlinkDomainFromAgencyPlanWebsiteV1", + "agencyHosting_deployNodeStaticWebsite", + "agencyHosting_deployPhpApplication" + ], + "billing": [ + "billing_createPurchaseOrderV1", + "billing_deletePaymentMethodV1", + "billing_disableAutoRenewalV1", + "billing_enableAutoRenewalV1", + "billing_getCatalogItemListV1", + "billing_getPaymentMethodListV1", + "billing_getSubscriptionListV1", + "billing_renewSubscriptionV1", + "billing_setDefaultPaymentMethodV1" + ], + "dns": [ + "DNS_deleteDNSRecordsV1", + "DNS_getDNSRecordsV1", + "DNS_getDNSSnapshotListV1", + "DNS_getDNSSnapshotV1", + "DNS_resetDNSRecordsV1", + "DNS_restoreDNSSnapshotV1", + "DNS_updateDNSRecordsV1", + "DNS_validateDNSRecordsV1" + ], + "domains": [ + "domains_acceptIncomingDomainMoveV1", + "domains_cancelOutgoingDomainMoveV1", + "domains_cancelPendingIRTPVerificationV1", + "domains_changeWHOISProfileForDomainV1", + "domains_checkDomainAvailabilityV1", + "domains_createDomainForwardingV1", + "domains_createWHOISProfileV1", + "domains_deleteDomainForwardingV1", + "domains_deleteWHOISProfileV1", + "domains_disableDomainLockV1", + "domains_disablePrivacyProtectionV1", + "domains_enableDomainLockV1", + "domains_enablePrivacyProtectionV1", + "domains_getDomainAuthorizationCodeV1", + "domains_getDomainDetailsV1", + "domains_getDomainForwardingV1", + "domains_getDomainListV1", + "domains_getDomainRenewalInformationV1", + "domains_getIncomingDomainMoveListV1", + "domains_getIncomingDomainMoveV1", + "domains_getOutgoingDomainMoveListV1", + "domains_getOutgoingDomainMoveV1", + "domains_getPendingIRTPVerificationV1", + "domains_getTransferListV1", + "domains_getTransferV1", + "domains_getWHOISProfileListV1", + "domains_getWHOISProfileUsageV1", + "domains_getWHOISProfileV1", + "domains_purchaseNewDomainV1", + "domains_rejectIncomingDomainMoveV1", + "domains_setWHOISProfileAsDefaultV1", + "domains_startOutgoingDomainMoveV1", + "domains_unsetDefaultWHOISProfileV1", + "domains_updateDomainForwardingV1", + "domains_updateDomainNameserversV1", + "v2_getDomainVerificationsDIRECT" + ], + "ecommerce": [ + "ecommerce_createCustomSalesChannelV1", + "ecommerce_createDigitalProductV1", + "ecommerce_createPhysicalProductV1", + "ecommerce_createStoreV1", + "ecommerce_deleteStoreV1", + "ecommerce_enableManualPaymentMethodV1", + "ecommerce_getCustomStorefrontSetupInstructionsV1", + "ecommerce_getStoreMetadataV1", + "ecommerce_getStoresV1", + "ecommerce_listSalesChannelsV1", + "ecommerce_setStoreShippingV1", + "ecommerce_updateSalesChannelV1" + ], + "horizons": [ + "horizons_createWebsiteV1", + "horizons_getWebsiteV1" + ], + "hosting": [ + "hosting_changeDatabasePasswordV1", + "hosting_clearWebsiteCacheV1", + "hosting_createAccountCronJobV1", + "hosting_createAccountDatabaseV1", + "hosting_createDatabaseRemoteConnectionV1", + "hosting_createNodeJSBuildFromArchiveV1", + "hosting_createWebsiteParkedDomainV1", + "hosting_createWebsiteSubdomainV1", + "hosting_createWebsiteV1", + "hosting_deleteAccountCronJobV1", + "hosting_deleteAccountDatabaseV1", + "hosting_deleteDatabaseRemoteConnectionV1", + "hosting_deleteWebsiteParkedDomainV1", + "hosting_deleteWebsiteSubdomainV1", + "hosting_deleteWebsiteV1", + "hosting_deployJsApplication", + "hosting_deployStaticWebsite", + "hosting_deployWordpressPlugin", + "hosting_deployWordpressTheme", + "hosting_generateAFreeSubdomainV1", + "hosting_getCronJobOutputV1", + "hosting_getNodeJSBuildLogsV1", + "hosting_getPHPDetailsV1", + "hosting_getPHPInfoV1", + "hosting_getPhpMyAdminLinkV1", + "hosting_importWordpressWebsite", + "hosting_listAccountCronJobsV1", + "hosting_listAccountDatabasesV1", + "hosting_listAvailableDatacentersV1", + "hosting_listDatabaseRemoteConnectionsV1", + "hosting_listJsDeployments", + "hosting_listNodeJSBuildsV1", + "hosting_listNode_jsVulnerabilitiesV1", + "hosting_listOrdersV1", + "hosting_listWebsiteParkedDomainsV1", + "hosting_listWebsiteSubdomainsV1", + "hosting_listWebsitesV1", + "hosting_patchNode_jsVulnerabilitiesV1", + "hosting_repairDatabaseV1", + "hosting_resetPHPExtensionsV1", + "hosting_restartNode_jsApplicationV1", + "hosting_showJsDeploymentLogs", + "hosting_toggleCachelessModeV1", + "hosting_toggleWebsiteCacheV1", + "hosting_updatePHPExtensionsV1", + "hosting_updatePHPOptionsV1", + "hosting_updatePHPVersionV1", + "hosting_verifyDomainOwnershipV1" + ], + "mail": [ + "mail_changeMailboxPasswordV1", + "mail_createAPITokenV1", + "mail_createAliasV1", + "mail_createAutoreplyV1", + "mail_createCatchAllV1", + "mail_createForwarderV1", + "mail_createMailboxV1", + "mail_createWebhookV1", + "mail_deleteAliasV1", + "mail_deleteAutoreplyV1", + "mail_deleteCatchAllV1", + "mail_deleteForwarderV1", + "mail_deleteMailboxV1", + "mail_deleteWebhookV1", + "mail_getOrderPlanV1", + "mail_getWebhookV1", + "mail_listAPITokensV1", + "mail_listAccessLogsV1", + "mail_listActionLogsV1", + "mail_listAliasesV1", + "mail_listAutorepliesV1", + "mail_listCatchAllsV1", + "mail_listForwardersV1", + "mail_listInboundLogsV1", + "mail_listMailboxActionLogsV1", + "mail_listMailboxesV1", + "mail_listOrdersV1", + "mail_listOutboundLogsV1", + "mail_listWebhookDeliveryLogsV1", + "mail_listWebhooksV1", + "mail_regenerateWebhookSecretV1", + "mail_resendCatchAllConfirmationV1", + "mail_resendForwarderConfirmationV1", + "mail_revokeAPITokenV1", + "mail_testWebhookV1", + "mail_updateAutoreplyV1", + "mail_updateForwarderKeepCopySettingV1", + "mail_updateWebhookV1" + ], + "reach": [ + "reach_createANewContactSegmentV1", + "reach_createANewContactV1", + "reach_createNewContactsV1", + "reach_deleteAContactV1", + "reach_getProfileDomainDNSStatusV1", + "reach_getSegmentDetailsV1", + "reach_listContactGroupsV1", + "reach_listContactsV1", + "reach_listProfileSegmentContactsV1", + "reach_listProfilesV1", + "reach_listSegmentContactsV1", + "reach_listSegmentsV1" + ], + "vps": [ + "VPS_activateFirewallV1", + "VPS_attachPublicKeyV1", + "VPS_createFirewallRuleV1", + "VPS_createNewFirewallV1", + "VPS_createNewProjectV1", + "VPS_createPTRRecordV1", + "VPS_createPostInstallScriptV1", + "VPS_createPublicKeyV1", + "VPS_createSnapshotV1", + "VPS_deactivateFirewallV1", + "VPS_deleteFirewallRuleV1", + "VPS_deleteFirewallV1", + "VPS_deletePTRRecordV1", + "VPS_deletePostInstallScriptV1", + "VPS_deleteProjectV1", + "VPS_deletePublicKeyV1", + "VPS_deleteSnapshotV1", + "VPS_getActionDetailsV1", + "VPS_getActionsV1", + "VPS_getAttachedPublicKeysV1", + "VPS_getBackupsV1", + "VPS_getDataCenterListV1", + "VPS_getFirewallDetailsV1", + "VPS_getFirewallListV1", + "VPS_getMetricsV1", + "VPS_getPostInstallScriptV1", + "VPS_getPostInstallScriptsV1", + "VPS_getProjectContainersV1", + "VPS_getProjectContentsV1", + "VPS_getProjectListV1", + "VPS_getProjectLogsV1", + "VPS_getPublicKeysV1", + "VPS_getScanMetricsV1", + "VPS_getSnapshotV1", + "VPS_getTemplateDetailsV1", + "VPS_getTemplatesV1", + "VPS_getVirtualMachineDetailsV1", + "VPS_getVirtualMachinesV1", + "VPS_installMonarxV1", + "VPS_purchaseNewVirtualMachineV1", + "VPS_recreateVirtualMachineV1", + "VPS_resetHostnameV1", + "VPS_restartProjectV1", + "VPS_restartVirtualMachineV1", + "VPS_restoreBackupV1", + "VPS_restoreSnapshotV1", + "VPS_setHostnameV1", + "VPS_setNameserversV1", + "VPS_setPanelPasswordV1", + "VPS_setRootPasswordV1", + "VPS_setupPurchasedVirtualMachineV1", + "VPS_startProjectV1", + "VPS_startRecoveryModeV1", + "VPS_startVirtualMachineV1", + "VPS_stopProjectV1", + "VPS_stopRecoveryModeV1", + "VPS_stopVirtualMachineV1", + "VPS_syncFirewallV1", + "VPS_uninstallMonarxV1", + "VPS_updateFirewallRuleV1", + "VPS_updatePostInstallScriptV1", + "VPS_updateProjectV1" + ], + "wordpress": [ + "hosting_activateWordPressPluginV1", + "hosting_activateWordPressThemeV1", + "hosting_checkIfWooCommerceIsInstalledV1", + "hosting_checkIfWordPressInstallationsAreValidV1", + "hosting_createLoginLinksV1", + "hosting_deactivateWordPressPluginV1", + "hosting_deleteWordPressInstallationV1", + "hosting_detectWordPressInstallationsV1", + "hosting_getInstallationJWTTokenV1", + "hosting_installWordPressPluginsV1", + "hosting_installWordPressThemeV1", + "hosting_installWordPressV1", + "hosting_listAvailableWordPressCoreUpdatesV1", + "hosting_listAvailableWordPressPluginsV1", + "hosting_listInstalledWordPressPluginsV1", + "hosting_listInstalledWordPressThemesV1", + "hosting_listSuggestedWordPressPluginsV1", + "hosting_listWordPressInstallationsV1", + "hosting_listWordPressThemesV1", + "hosting_purgeLiteSpeedCacheV1", + "hosting_searchWordPressPluginsV1", + "hosting_setAIOptionStatusV1", + "hosting_showAIOptionStatusV1", + "hosting_showLiteSpeedCacheStatusV1", + "hosting_showMaintenanceStatusV1", + "hosting_showMemcachedObjectCacheStatusV1", + "hosting_showWordPressCoreVersionV1", + "hosting_toggleMaintenanceModeV1", + "hosting_toggleMemcachedObjectCacheV1", + "hosting_uninstallWordPressPluginsV1", + "hosting_uninstallWordPressThemesV1", + "hosting_updateHostingerWordPressPluginV1", + "hosting_updateWordPressCoreV1", + "hosting_updateWordPressPluginsV1", + "hosting_updateWordPressThemesV1" + ] + } +} diff --git a/scripts/sync-mcp-tools.mjs b/scripts/sync-mcp-tools.mjs new file mode 100644 index 0000000..20bfef9 --- /dev/null +++ b/scripts/sync-mcp-tools.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +/** + * Regenerate scripts/mcp-tools.json from a published hostinger-api-mcp tarball. + * + * node scripts/sync-mcp-tools.mjs # latest + * node scripts/sync-mcp-tools.mjs 1.29.0 # a specific version + * + * The catalog is checked in so scripts/check-tool-names.mjs can run offline in + * CI. Re-run this whenever the MCP server ships new tools, then commit the diff. + */ + +import { execFileSync } from "node:child_process"; +import { mkdtempSync, readFileSync, readdirSync, writeFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import process from "node:process"; + +const version = process.argv[2] ?? "latest"; +const spec = `hostinger-api-mcp@${version}`; +const outFile = path.join(import.meta.dirname, "mcp-tools.json"); + +const work = mkdtempSync(path.join(tmpdir(), "hostinger-mcp-tools-")); +try { + const packed = execFileSync("npm", ["pack", spec, "--silent", "--pack-destination", work], { + encoding: "utf8", + }) + .trim() + .split("\n") + .pop(); + + execFileSync("tar", ["xzf", path.join(work, packed), "-C", work]); + + const pkgRoot = path.join(work, "package"); + const { version: resolvedVersion } = JSON.parse( + readFileSync(path.join(pkgRoot, "package.json"), "utf8"), + ); + + const toolsDir = path.join(pkgRoot, "src", "core", "tools"); + const groups = {}; + + for (const file of readdirSync(toolsDir).sort()) { + // all.js is the union of every group — indexing it would double-count. + if (!file.endsWith(".js") || file === "all.js") continue; + const group = file.replace(/\.js$/, ""); + const source = readFileSync(path.join(toolsDir, file), "utf8"); + // Tool objects are emitted as pretty-printed JSON at a fixed indent, so the + // 4-space `"name"` key is the tool itself rather than a nested schema field. + const names = [...source.matchAll(/^ {4}"name":\s*"([^"]+)"/gm)].map((m) => m[1]); + if (names.length === 0) throw new Error(`No tools parsed from ${file}`); + groups[group] = names.sort(); + } + + const total = Object.values(groups).reduce((n, g) => n + g.length, 0); + writeFileSync( + outFile, + `${JSON.stringify({ package: "hostinger-api-mcp", version: resolvedVersion, total, groups }, null, 2)}\n`, + ); + + console.log(`Wrote ${path.relative(process.cwd(), outFile)}`); + console.log(` ${spec} resolved to ${resolvedVersion}`); + console.log(` ${total} tools across ${Object.keys(groups).length} groups`); +} finally { + rmSync(work, { recursive: true, force: true }); +} diff --git a/skills/deploy-nodejs-app/SKILL.md b/skills/deploy-nodejs-app/SKILL.md index 48889bd..f0412ef 100644 --- a/skills/deploy-nodejs-app/SKILL.md +++ b/skills/deploy-nodejs-app/SKILL.md @@ -1,45 +1,51 @@ --- name: deploy-nodejs-app -description: Guide the agent through deploying a Node.js project to Hostinger Managed Node.js Hosting. Use when the user asks to deploy, publish, or push a Node.js app to Hostinger. -when-to-use: User mentions deploying a Node.js / SvelteKit / Hono / Remix / Fastify / Astro / Next.js app to Hostinger, or asks how to put a Node project live on a Hostinger domain. +description: Guide the agent through deploying a Node.js or static project to Hostinger. Use when the user asks to deploy, publish, or push an app to Hostinger. +when-to-use: User mentions deploying a Node.js / React / Vue / Vite / Express / Nest / Fastify / SvelteKit / Next.js app to Hostinger, or asks how to put a project live on a Hostinger domain. --- -# Deploy a Node.js app to Hostinger +# Deploy an app to Hostinger ## When to use -- User says "deploy this to Hostinger", "ship this Node app", or names a framework (SvelteKit, Hono, Remix, Fastify, Astro, Next.js). -- User wants to redeploy a project that already lives on Hostinger Managed Node.js Hosting. +- User says "deploy this to Hostinger", "ship this app", or "put this live on ". +- User wants to redeploy a project that already lives on Hostinger. ## Inputs to gather first -1. Which Hostinger account / Node.js hosting plan to use (call the MCP `list_hosting_plans` tool if ambiguous). -2. The target domain or subdomain. -3. Framework / preset — SvelteKit, Hono, Remix, Fastify, Astro, Next.js, or "generic Node". -4. Node.js version (default to the LTS supported by the chosen plan). -5. Entry point / start command (`npm start`, `node server.js`, etc.). -6. Required environment variables (read from a `.env` file if present, but never commit it). -7. Source: local directory, Git repository, or zip bundle. +1. Target domain. Call `hosting_listWebsitesV1` if it's ambiguous — filter with the `domain` parameter, or page through with `page` / `per_page`. +2. Whether the project needs a build. A `package.json` with a `build` script means yes; a folder of finished HTML/CSS/JS means no. +3. Project root, if it isn't the repo root (monorepos). +4. Any build overrides the user already knows they need — Node version, package manager, entry file, output directory. + +See `rules/nodejs-deployments.mdc` for the full list of accepted override values. Do not invent `app_type` values. ## Steps -1. Confirm the inputs above with the user before any write operation. -2. Detect the framework from `package.json` and dependency list when the user hasn't specified one. Match to a Hostinger framework preset. -3. Call the Hostinger MCP `hostinger` server: - - `list_hosting_plans` → pick the Node-capable plan. - - `check_domain_status` → verify the domain is connected and DNS is healthy. - - `create_nodejs_deployment` (or update if one exists) → submit the source, framework preset, Node version, and env vars. -4. Poll deployment status / stream build logs and surface failures back to the user. -5. After success, return the public URL, the build duration, and any post-deploy follow-ups (cache purge, SSL provisioning status). +1. Confirm the target domain and that deploying will overwrite what is currently live. Wait for explicit approval. +2. Build the archive: source only, excluding `node_modules/`, build output, `.git/`, and `.env`. Keep it under 50 MB. +3. Deploy: + - **Default** — `hosting_deployJsApplication` with `domain` and `archivePath`. Hostinger auto-detects build settings and resolves the username. + - **Needs overrides** — `hosting_createNodeJSBuildFromArchiveV1`. This one also needs `username`, from `hosting_listWebsitesV1`. + - **No build step** — `hosting_deployStaticWebsite`, with the archive named `_YYYYMMDD_HHMMSS.zip`. +4. Track the build: `hosting_listJsDeployments` (or `hosting_listNodeJSBuildsV1`) for state and the build `uuid`. +5. Stream logs while the state is `running` — `hosting_showJsDeploymentLogs` or `hosting_getNodeJSBuildLogsV1`, passing the last line count back as `fromLine` / `from_line`. +6. On success, report the live URL and the build duration. On failure, hand off to `diagnose-build-failure`. + +## Optional follow-ups + +- `hosting_clearWebsiteCacheV1` if the user is seeing stale content. +- `hosting_restartNode_jsApplicationV1` if the process needs a bounce after a config change. +- `hosting_listNode_jsVulnerabilitiesV1` to audit dependencies post-deploy. +- `DNS_getDNSRecordsV1` to confirm the domain actually points at Hostinger before promising the user a working URL. ## Failure handling -- Surface raw Hostinger API errors verbatim — do not fabricate causes. -- If the build fails, hand off to the `diagnose-build-failure` skill. -- If the token is missing or unauthorized, instruct the user to generate a token at hpanel.hostinger.com → Profile & settings → API Tokens, then set `HOSTINGER_API_TOKEN` and restart Cursor. +- Surface Hostinger API errors verbatim — do not fabricate causes. +- If authentication fails, the MCP server opens a browser sign-in on the next tool call. Only if the user needs a non-interactive setup should they generate an API token in hPanel (**Profile & settings → API Tokens**) and export `HOSTINGER_API_TOKEN` before launching Cursor. ## Do not -- Do not commit `.env`, credentials, or tokens to the source bundle. -- Do not auto-deploy to a production domain without explicit confirmation. -- Do not change the framework preset between deploys without telling the user — it can reset build caches. +- Do not include `.env`, credentials, or tokens in the archive. +- Do not deploy to a production domain without explicit confirmation. +- Do not promise to set application environment variables — the API cannot. Direct the user to hPanel. diff --git a/skills/diagnose-build-failure/SKILL.md b/skills/diagnose-build-failure/SKILL.md index 3c07f17..c2d2e45 100644 --- a/skills/diagnose-build-failure/SKILL.md +++ b/skills/diagnose-build-failure/SKILL.md @@ -1,6 +1,6 @@ --- name: diagnose-build-failure -description: Pull build logs from a failed Hostinger deployment and identify common failure modes (missing env vars, framework preset mismatch, dep install errors, Node version mismatch). +description: Pull build logs from a failed Hostinger deployment and identify the failure mode (missing dependency, Node version mismatch, wrong output directory, dependency resolution conflict). when-to-use: A Hostinger deployment failed, the build is stuck, or the user reports "my site won't deploy" / "the build broke". --- @@ -8,43 +8,48 @@ when-to-use: A Hostinger deployment failed, the build is stuck, or the user repo ## When to use -- A `create_nodejs_deployment` or static deploy returned a failed status. -- User reports the live site is showing a build/runtime error. -- User asks "why did my deploy fail?". +- A deploy returned state `failed`. +- The user reports the live site showing a build or runtime error. +- The user asks "why did my deploy fail?". ## Inputs to gather first -1. Domain and deployment ID (if user knows it). Otherwise call `list_deployments` for the domain. -2. Approximate time of the failure (for log ranging). +1. Domain. +2. The build `uuid`. If the user doesn't have it, call `hosting_listJsDeployments` (or `hosting_listNodeJSBuildsV1`) for the domain and filter `states` to `failed`. +3. For `hosting_getNodeJSBuildLogsV1` you also need `username` — get it from `hosting_listWebsitesV1`. ## Steps -1. Call the Hostinger MCP `hostinger` server: - - `get_deployment` for the deployment ID → status, framework preset, Node version. - - `get_deployment_logs` → full build + runtime logs. -2. Scan logs for the failure signatures below and report the most likely cause + concrete fix. +1. Fetch the logs: + - `hosting_showJsDeploymentLogs` with `domain` + `buildUuid` for `hosting_deployJsApplication` builds. + - `hosting_getNodeJSBuildLogsV1` with `username` + `domain` + `uuid` for archive builds. + - Both accept a start line (`fromLine` / `from_line`) — page through rather than requesting one enormous response. +2. Log content may contain ANSI escape sequences. Strip them before quoting. +3. Match against the signatures below, then report the most likely cause and a concrete fix. ## Common failure modes | Signature in logs | Likely cause | Fix | |---|---|---| -| `Error: Cannot find module 'X'` | Missing dependency | Add to `package.json`; redeploy. | -| `ENV_VAR is not defined`, `undefined is not a function` referring to `process.env.X` | Missing env var | Add via `update_nodejs_env_vars` MCP tool. | -| `npm ERR! peer dep`, `ERESOLVE` | Dep resolution conflict | Pin versions or use `npm install --legacy-peer-deps`. | -| `engine "node" is incompatible` | Node version mismatch | Match `engines.node` in `package.json` to the plan's supported Node version. | -| `Build command failed: ` but framework preset is "generic" | Preset mismatch | Re-run deploy with the correct framework preset (SvelteKit, Hono, Remix, Fastify, Astro, Next.js). | -| `out of memory`, `JavaScript heap out of memory` | Build memory cap | Reduce build memory footprint or upgrade plan. | -| `permission denied`, `EACCES` writing to `/` | Wrong start command | Ensure the start command writes only inside the project workdir. | -| `Address already in use`, port collision | Port hardcoded | Use `process.env.PORT`. | +| `Error: Cannot find module 'X'` | Dependency missing from `package.json`, or it was only in `devDependencies` | Add it to `dependencies`, rebuild | +| `npm ERR! ERESOLVE`, `peer dep` | Dependency resolution conflict | Pin versions, or commit a lockfile so the server installs deterministically | +| `engine "node" is incompatible`, syntax errors in dependency code | Node version mismatch | Set `node_version` to `18`, `20`, `22`, or `24` on `hosting_createNodeJSBuildFromArchiveV1`, or fix `engines.node` | +| `sh: vite: not found`, `build script not found` | Wrong `build_script`, or the build tool is in `devDependencies` and wasn't installed | Override `build_script`; verify the script name in `package.json` | +| `no such file or directory` for `package.json` | Wrong project root in a monorepo | Set `root_directory` relative to `public_html` | +| Build succeeds but the site 404s | Wrong `output_directory` | Point `output_directory` at the real build output, relative to the root directory | +| `JavaScript heap out of memory` | Build exceeded the memory cap | Reduce the build footprint, or upgrade the plan | +| Wrong package manager resolving deps | Lockfile/manager mismatch | Set `package_manager` to `npm`, `yarn`, or `pnpm` | +| App builds and starts but gets no traffic | Port hardcoded | Bind to `process.env.PORT` | +| `undefined` config values at runtime | Missing environment variables | Environment variables are not settable via the API — the user must add them in hPanel | ## Report format -Reply with: -1. **Cause** — one sentence, citing the exact log line. -2. **Fix** — concrete diff / MCP tool to call. -3. **Next action** — offer to apply the fix (with explicit user confirmation for any write). +1. **Cause** — one sentence, quoting the exact log line. +2. **Fix** — the concrete code change or the tool call with the specific override. +3. **Next action** — offer to apply it, and wait for confirmation. ## Do not -- Do not guess the cause without quoting a log line. -- Do not redeploy automatically — propose the fix and wait for confirmation. +- Do not guess a cause without quoting a log line. +- Do not redeploy automatically — a redeploy overwrites what is live. Propose and wait. +- Do not blame a "framework preset" — Hostinger has no preset parameter. The overrides in `rules/nodejs-deployments.mdc` are the real knobs. diff --git a/skills/manage-dns-records/SKILL.md b/skills/manage-dns-records/SKILL.md index 5ced834..ffa6f8b 100644 --- a/skills/manage-dns-records/SKILL.md +++ b/skills/manage-dns-records/SKILL.md @@ -1,7 +1,7 @@ --- name: manage-dns-records -description: Read, create, update, and delete DNS records on a Hostinger-managed domain via the Hostinger MCP server. Use for any DNS change, lookup, or troubleshooting. -when-to-use: User wants to add/update/delete an A, AAAA, CNAME, MX, TXT, NS, or SRV record on a Hostinger domain; or asks "what are my DNS records for X?". +description: Read, update, and delete DNS zone records on a Hostinger-managed domain via the Hostinger DNS MCP server. Use for any DNS change, lookup, or troubleshooting. +when-to-use: User wants to add/update/delete an A, AAAA, CNAME, ALIAS, MX, TXT, NS, SRV, or CAA record on a Hostinger domain; or asks "what are my DNS records for X?". --- # Manage Hostinger DNS records @@ -13,45 +13,62 @@ when-to-use: User wants to add/update/delete an A, AAAA, CNAME, MX, TXT, NS, or - "Remove the old MX records and set up Google Workspace." - "Why isn't my DNS resolving?" -## Inputs to gather first +## The zone model matters -1. Domain (must be on Hostinger — confirm with `list_domains` if uncertain). -2. Record type (A, AAAA, CNAME, MX, TXT, NS, SRV). -3. Name / subdomain (e.g. `@`, `www`, `mail`). -4. Value(s) and TTL. -5. For MX: priority. For SRV: priority, weight, port, target. +Hostinger's DNS API is **zone-oriented, not record-oriented**. There is no `create record` or `update record` tool. `DNS_updateDNSRecordsV1` takes a `zone` array where each entry groups all records sharing one name and type: + +```json +{ + "domain": "example.com", + "overwrite": true, + "zone": [ + { "name": "@", "type": "A", "ttl": 300, "records": [{ "content": "192.0.2.10" }] }, + { "name": "www", "type": "CNAME", "ttl": 300, "records": [{ "content": "example.com" }] } + ] +} +``` + +`name`, `type`, and `records` are required per entry; `ttl` is optional. Use `@` for the apex. + +The `overwrite` flag is the critical decision: + +- **`overwrite: true`** — every existing record matching that name+type is deleted and replaced by exactly what you send. Use this when you want the listed records to be the complete set (e.g. replacing all MX records). +- **`overwrite: false`** (or omitted) — TTLs are updated and your records are *appended* alongside the existing ones. Use this when adding a record without touching siblings (e.g. adding one more TXT verification record). + +Getting this backwards either silently duplicates records or silently deletes the ones you meant to keep. State which mode you're using, and why, in your confirmation message. + +Valid `type` values: `A`, `AAAA`, `CNAME`, `ALIAS`, `MX`, `TXT`, `NS`, `SOA`, `SRV`, `CAA`. ## Steps -1. Read current state first: call `list_dns_records` for the domain. -2. **Snapshot before changing.** Call `create_dns_snapshot` so the change is reversible. -3. Show the user the exact set of records that will be created / changed / deleted (diff format). -4. Wait for explicit confirmation. -5. Apply the change via `create_dns_record`, `update_dns_record`, or `delete_dns_record`. -6. Re-read the records and confirm the resulting state. +1. **Read current state** — `DNS_getDNSRecordsV1` for the domain. +2. **Note the rollback point** — `DNS_getDNSSnapshotListV1` and record the most recent snapshot ID. Hostinger creates snapshots automatically; there is no tool to create one on demand, so capture the existing ID rather than promising a fresh backup. +3. **Dry run** — `DNS_validateDNSRecordsV1` with the exact `zone` and `overwrite` values you intend to send. It returns `200` when valid and `422` with details when not. Always do this before mutating. +4. **Show the diff** — list what will be created, changed, and removed, and name the `overwrite` mode. +5. **Wait for explicit confirmation.** +6. **Apply** — `DNS_updateDNSRecordsV1`. +7. **Verify** — re-read with `DNS_getDNSRecordsV1` and confirm the resulting zone. Optionally corroborate with `dig` against public DNS, noting that propagation lags the API. + +## Deleting -## Validation +`DNS_deleteDNSRecordsV1` filters by name and type, and removes *all* records matching each filter. To drop only some of several records sharing a name and type, use `DNS_updateDNSRecordsV1` with `overwrite: true` and the records you want to keep. -- Reject syntactically invalid records before calling the API: - - A → IPv4 only. - - AAAA → IPv6 only. - - CNAME → cannot coexist with other records on the same name. - - MX → must reference a hostname, not an IP. - - TXT → quote-escape inner double quotes. -- Warn if TTL is below 300s (propagation thrash) or above 86400s (slow recovery). +`DNS_resetDNSRecordsV1` returns the entire zone to Hostinger defaults. It is not a targeted delete — treat it as a last resort and confirm explicitly. -## Confirm before +## Rolling back -- Deleting any record (always). -- Replacing all MX records (mail delivery risk). -- Changing NS records (delegation change — can break the domain). -- Changing the A/AAAA record of the apex (`@`) — site downtime risk. +`DNS_restoreDNSSnapshotV1` with the domain and a snapshot ID from `DNS_getDNSSnapshotListV1`. Inspect a snapshot's contents first with `DNS_getDNSSnapshotV1` so the user knows what state they're reverting to. -## Recovery +## Validation before calling -If a change goes wrong, call `restore_dns_snapshot` with the snapshot ID from step 2. +- `A` → IPv4 only; `AAAA` → IPv6 only. +- `CNAME` cannot coexist with other record types on the same name, and cannot be used on the apex — use `ALIAS` there. +- `MX` must point at a hostname, never an IP. +- `TXT` → escape inner double quotes. +- Changing apex `A`/`AAAA`, `NS`, or `MX` breaks live traffic or mail. Call these out prominently. ## Do not -- Do not delete or mutate records without showing the user the diff first. -- Do not infer the intended record type — ask if ambiguous. +- Do not call `DNS_updateDNSRecordsV1` without a preceding `DNS_validateDNSRecordsV1`. +- Do not claim a backup was created — snapshots are automatic, so cite an existing snapshot ID instead. +- Do not use `dig` as the primary read path; it reflects cached data, not the zone. diff --git a/skills/query-deployment-logs/SKILL.md b/skills/query-deployment-logs/SKILL.md new file mode 100644 index 0000000..64d5f66 --- /dev/null +++ b/skills/query-deployment-logs/SKILL.md @@ -0,0 +1,53 @@ +--- +name: query-deployment-logs +description: Pull and summarize the logs Hostinger's API exposes — Node.js build logs, JS deployment logs, and cron job output — for a domain. Use to investigate a failed or slow build, or a cron job that isn't doing what the user expects. +when-to-use: User asks to "see the logs", investigates why a build or deployment behaved oddly, or wants to know whether a scheduled job ran. +--- + +# Query Hostinger logs + +## What is actually available + +Hostinger's API exposes three kinds of log output: + +| Log | Tool | Needs | +|---|---|---| +| JS deployment logs | `hosting_showJsDeploymentLogs` | `domain`, `buildUuid`, optional `fromLine` | +| Node.js build logs | `hosting_getNodeJSBuildLogsV1` | `username`, `domain`, `uuid`, optional `from_line` | +| Cron job output | `hosting_getCronJobOutputV1` | cron job identifiers from `hosting_listAccountCronJobsV1` | + +**Raw HTTP access logs and PHP error logs are not exposed by the API.** If the user asks for 4xx/5xx breakdowns, traffic spikes, per-request timings, or PHP fatals, tell them directly that these live in hPanel and cannot be fetched here. Do not substitute a plausible-sounding tool name, and do not present build logs as if they were access logs. + +## Inputs to gather first + +1. Domain. +2. Which log the user actually needs — a build/deployment, or a cron job. +3. For builds: the build `uuid`. Get it from `hosting_listJsDeployments` or `hosting_listNodeJSBuildsV1`, filtering `states` when the user only cares about failures. +4. For `hosting_getNodeJSBuildLogsV1`: the `username`, from `hosting_listWebsitesV1`. + +## Steps + +1. Resolve the build or cron job identifier first — every log tool requires one. +2. Fetch the log, starting at line `0`. +3. To follow a running build, poll while the state is `running`, passing the previously returned line count as `fromLine` / `from_line` so each call returns only new output. +4. Strip ANSI escape sequences before quoting — build output is colorized. +5. Summarize rather than dumping. Report: + - The final state and, for a failure, the first error and the last 20 lines verbatim. + - Which step failed (install, build, start). + - Total duration, if the timestamps allow it. +6. Offer drill-down: "Want the full log?", "Want me to compare this against the last successful build?". + +## Handing off + +- A failed build with an identified error signature → `diagnose-build-failure`. +- A cron job producing no output → check the schedule and command with `hosting_listAccountCronJobsV1` before assuming the script is broken. + +## Privacy + +- Build logs can echo environment values and tokens. Redact anything that looks like a credential before quoting, and never paste a full environment dump into chat. + +## Do not + +- Do not dump thousands of log lines into the conversation — page through and summarize. +- Do not infer a cause from a single line; corroborate with the surrounding context. +- Do not claim access-log or error-log capability the API doesn't have. diff --git a/skills/query-hosting-logs/SKILL.md b/skills/query-hosting-logs/SKILL.md deleted file mode 100644 index 4ed8448..0000000 --- a/skills/query-hosting-logs/SKILL.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -name: query-hosting-logs -description: Pull runtime, access, and error logs for a domain on Hostinger and summarize what's happening. Use to investigate traffic spikes, 4xx/5xx bursts, slow requests, or runtime errors. -when-to-use: User asks to "see the logs", investigates a traffic anomaly, debugs a runtime issue, or wants to know why requests are failing. ---- - -# Query Hostinger hosting logs - -## When to use - -- "Show me the access log for example.com." -- "Why am I getting 500s?" -- "Did anything spike traffic last night?" -- "Are there errors in my Node.js app?" - -## Inputs to gather first - -1. Domain. -2. Log type: `access`, `error`, `runtime` (Node.js / app stdout), `build`. -3. Time range (default to last 1 hour if user doesn't specify). -4. Optional filters: status code (e.g. 5xx), path prefix, IP, request method. - -## Steps - -1. Call the Hostinger MCP `hostinger` server: - - `list_log_streams` for the domain → confirm which log types are available for the plan. - - `query_logs` with `{ domain, type, since, until, filter }`. -2. Summarize the result — do **not** dump 10k log lines into chat. Limit to: - - Top 5 status codes with counts. - - Top 5 endpoints by request volume. - - Top 5 error messages (for error/runtime logs). - - Up to 20 most recent lines verbatim. -3. Offer drill-down: "Want me to filter by 5xx?", "Want the full raw output for path X?". - -## Anomaly checks to run automatically - -- Spike: compare request count vs the previous matching window — flag a >3× jump. -- Error rate: flag if 5xx > 1% of total requests. -- New user agent: flag a single UA suddenly responsible for >20% of traffic (potential bot/scraper). -- Slow requests: flag the p95 response time if >2s. - -## Privacy - -- Strip or redact IPs in summary output unless the user explicitly asks for them. -- Never paste session cookies or `Authorization` headers from logs into chat. - -## Do not - -- Do not dump raw logs without summarization. -- Do not infer a cause from a single log line — corroborate with at least one other signal. diff --git a/skills/troubleshoot-wordpress/SKILL.md b/skills/troubleshoot-wordpress/SKILL.md index 67c0e20..5ccc440 100644 --- a/skills/troubleshoot-wordpress/SKILL.md +++ b/skills/troubleshoot-wordpress/SKILL.md @@ -1,6 +1,6 @@ --- name: troubleshoot-wordpress -description: Diagnose common Hostinger WordPress issues — PHP version mismatches, error logs, plugin/theme conflicts, white screen of death, slow admin, broken updates. +description: Diagnose common Hostinger WordPress issues — PHP version and extension mismatches, plugin/theme conflicts, white screen of death, slow admin, stale cache, failed core updates. when-to-use: User reports their Hostinger-hosted WordPress site is broken, slow, throwing errors, or behaving unexpectedly after a change. --- @@ -11,46 +11,59 @@ when-to-use: User reports their Hostinger-hosted WordPress site is broken, slow, - "My WordPress site is down / blank / showing a 500." - "Admin is really slow." - "I updated a plugin and now nothing works." -- "How do I see WP error logs?" +- "My changes aren't showing up." + +## What the API can and can't see + +The WordPress and hosting MCP servers expose installations, plugins, themes, core version, caches, maintenance mode, and PHP configuration. They do **not** expose PHP error logs or shared-hosting backups. When the diagnosis genuinely needs an error log or a restore, say so and point the user to hPanel rather than calling a tool that doesn't exist. ## Inputs to gather first 1. Domain. -2. Symptom: front-end blank, 500 error, 502/504, admin slow, login loop, white screen, broken updates, etc. -3. What changed recently (plugin install/update, theme switch, PHP version bump, manual file edit). +2. Symptom: blank front-end, 500, 502/504, slow admin, login loop, stale content, failed update. +3. What changed recently — plugin install or update, theme switch, PHP version bump, core update. ## Steps -1. Call the Hostinger MCP `hostinger` server: - - `get_wordpress_site` for the domain → installed version, active theme, PHP version, plan tier. - - `list_wordpress_plugins` → installed plugins + versions + active status. - - `get_php_error_log` → last error log entries. - - `get_php_settings` → PHP version, memory limit, max_execution_time. - -2. Map the symptom to the most likely cause from the table below. - -3. Propose **one** fix at a time, confirm with the user, then apply. +1. Establish the baseline: + - `hosting_listWordPressInstallationsV1` → which installations exist on the account. + - `hosting_checkIfWordPressInstallationsAreValidV1` → whether Hostinger considers the install healthy. Run `hosting_detectWordPressInstallationsV1` first if the site isn't listed. + - `hosting_showWordPressCoreVersionV1` and `hosting_listAvailableWordPressCoreUpdatesV1` → core version and pending updates. + - `hosting_listInstalledWordPressPluginsV1` and `hosting_listInstalledWordPressThemesV1` → versions and active state. + - `hosting_getPHPDetailsV1` (and `hosting_getPHPInfoV1` for the full dump) → PHP version, extensions, limits. +2. Map the symptom using the table below. +3. Propose **one** fix at a time, confirm, then apply. ## Common issues | Symptom | Likely cause | First check / fix | |---|---|---| -| Blank front-end ("white screen of death") | Fatal PHP error, often after plugin update | Read `get_php_error_log` for the latest fatal. Disable the suspect plugin via `disable_wordpress_plugin`. | -| 500 error site-wide | `.htaccess` corruption or PHP fatal | Check error log. Regenerate permalinks. | -| Admin extremely slow | Slow plugin (e.g. analytics, security scan), low memory limit | Bulk-disable non-essential plugins; raise PHP memory_limit. | -| Login loop / can't log in | Cookies / `wp_options` `siteurl` mismatch, plugin conflict | Verify `siteurl` and `home` match the actual domain. Disable all plugins. | -| "PHP version" warning | Plugin requires newer PHP than the site runs | Bump PHP via `update_php_version` after confirming compatibility. | -| Auto-update fails | File permission, plan limit, conflicting plugin | Check error log; trigger manual update via `update_wordpress`. | -| Mixed content / SSL warnings | `siteurl` is `http://` while site is `https://` | Update `siteurl`/`home` to `https://`. | -| Site hacked / spam pages | Compromised plugin or stolen admin creds | Restore from `list_backups` → `restore_backup`. Rotate admin password. | +| Blank front-end ("white screen of death") | Fatal PHP error, usually from a plugin or theme update | Compare `hosting_listInstalledWordPressPluginsV1` against what the user just changed, then `hosting_deactivateWordPressPluginV1` on the suspect. The fatal itself is only visible in the hPanel error log. | +| 500 site-wide | PHP fatal, or a PHP version/extension mismatch | `hosting_getPHPDetailsV1` to confirm the version and loaded extensions; `hosting_updatePHPExtensionsV1` if something required is missing | +| Admin extremely slow | Heavy plugin, or a low PHP memory limit / execution time | `hosting_updatePHPOptionsV1` to raise the limits; deactivate non-essential plugins one at a time | +| "Requires PHP x.y" warning | Plugin needs a newer PHP than the site runs | Confirm theme and plugin compatibility, then `hosting_updatePHPVersionV1` | +| Changes not appearing | LiteSpeed or website cache serving stale content | `hosting_showLiteSpeedCacheStatusV1`, then `hosting_purgeLiteSpeedCacheV1`; also `hosting_clearWebsiteCacheV1`, and `hosting_toggleCachelessModeV1` while actively debugging | +| Object cache errors after a plugin change | Memcached object cache out of sync | `hosting_showMemcachedObjectCacheStatusV1`, then `hosting_toggleMemcachedObjectCacheV1` | +| Site stuck showing "briefly unavailable" | Maintenance mode left on | `hosting_showMaintenanceStatusV1`, then `hosting_toggleMaintenanceModeV1` | +| Core auto-update failed | Version conflict or a blocking plugin | `hosting_listAvailableWordPressCoreUpdatesV1`, then `hosting_updateWordPressCoreV1` | +| Can't reach wp-admin to verify a fix | Lost or broken admin session | `hosting_createLoginLinksV1` for a one-time admin login link | +| Broken checkout on a shop | WooCommerce missing or inactive | `hosting_checkIfWooCommerceIsInstalledV1` | +| Hostinger plugin features misbehaving | Outdated Hostinger plugin | `hosting_updateHostingerWordPressPluginV1` | + +## Isolating a plugin conflict + +Deactivate one plugin at a time with `hosting_deactivateWordPressPluginV1`, re-test, and reactivate with `hosting_activateWordPressPluginV1` before moving to the next. Say which plugin you're about to disable and what user-facing feature might break. Do not bulk-deactivate a production site without explicit approval. -## Destructive operations — confirm before +## Confirm before -- `restore_backup` (overwrites current site). -- `disable_wordpress_plugin` on production (could break a customer-facing feature). -- `update_php_version` (can break themes/plugins that don't support the new version). +- `hosting_deleteWordPressInstallationV1` — destroys the site. +- `hosting_uninstallWordPressPluginsV1`, `hosting_uninstallWordPressThemesV1` — may drop plugin data. +- `hosting_updateWordPressCoreV1`, `hosting_updateWordPressPluginsV1`, `hosting_updateWordPressThemesV1` — can introduce new breakage. +- `hosting_updatePHPVersionV1` — can break themes and plugins that don't support the new version. +- `hosting_toggleMaintenanceModeV1` on production — takes the site offline for visitors. ## Do not -- Do not guess at fixes without inspecting the error log. +- Do not claim to have read an error log. Direct the user to hPanel for PHP error logs. +- Do not offer to restore a backup — shared-hosting backups aren't exposed by the API. - Do not edit `wp-config.php` or `.htaccess` directly when a Hostinger MCP tool covers the same change.