hostinger-api-mcp 3.28.5 → 3.28.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/server.json +2 -2
- package/skills/headless/VERSION +3 -3
- package/skills/headless/connect/SKILL.md +25 -17
- package/skills/headless/connect/bootstrap.mjs +2 -2
- package/skills/headless/connect/tools/antigravity.md +2 -2
- package/skills/headless/connect/tools/claude-code.md +2 -2
- package/skills/headless/connect/tools/codex.md +2 -2
- package/skills/headless/connect/tools/cursor.md +2 -2
- package/skills/headless/connect/tools/devin.md +2 -2
- package/skills/headless/connect/tools/vscode.md +2 -2
- package/skills/headless/wait.mjs +2 -1
- package/src/core/skills.js +27 -27
- package/src/core/skills.ts +27 -27
- package/src/servers/agency-hosting.js +1 -1
- package/src/servers/agency-hosting.ts +1 -1
- package/src/servers/agent-readiness.js +1 -1
- package/src/servers/agent-readiness.ts +1 -1
- package/src/servers/all.js +1 -1
- package/src/servers/all.ts +1 -1
- package/src/servers/billing.js +1 -1
- package/src/servers/billing.ts +1 -1
- package/src/servers/dns.js +1 -1
- package/src/servers/dns.ts +1 -1
- package/src/servers/domains.js +1 -1
- package/src/servers/domains.ts +1 -1
- package/src/servers/ecommerce.js +1 -1
- package/src/servers/ecommerce.ts +1 -1
- package/src/servers/gpu.js +1 -1
- package/src/servers/gpu.ts +1 -1
- package/src/servers/horizons.js +1 -1
- package/src/servers/horizons.ts +1 -1
- package/src/servers/hosting.js +1 -1
- package/src/servers/hosting.ts +1 -1
- package/src/servers/mail.js +1 -1
- package/src/servers/mail.ts +1 -1
- package/src/servers/reach.js +1 -1
- package/src/servers/reach.ts +1 -1
- package/src/servers/vps.js +1 -1
- package/src/servers/vps.ts +1 -1
- package/src/servers/wordpress.js +1 -1
- package/src/servers/wordpress.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ Generated at release time from the diff between the previous and current
|
|
|
4
4
|
build. Tool changes are listed individually; everything else is summarised
|
|
5
5
|
by area.
|
|
6
6
|
|
|
7
|
+
## v3.28.6 — 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Also in this release
|
|
10
|
+
|
|
11
|
+
- Bundled agent skills: 10 files
|
|
12
|
+
- Other files: `src/core/skills.js`, `src/core/skills.ts`
|
|
13
|
+
|
|
7
14
|
## v3.28.5 — 2026-11-04
|
|
8
15
|
|
|
9
16
|
### Also in this release
|
package/gemini-extension.json
CHANGED
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
"url": "https://github.com/hostinger/api-mcp-server",
|
|
18
18
|
"source": "github"
|
|
19
19
|
},
|
|
20
|
-
"version": "3.28.
|
|
20
|
+
"version": "3.28.6",
|
|
21
21
|
"packages": [
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "@hostinger/mcp",
|
|
25
|
-
"version": "3.28.
|
|
25
|
+
"version": "3.28.6",
|
|
26
26
|
"transport": {
|
|
27
27
|
"type": "stdio"
|
|
28
28
|
},
|
package/skills/headless/VERSION
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
version=3.28.
|
|
2
|
-
commit=
|
|
3
|
-
built=2026-11-
|
|
1
|
+
version=3.28.6
|
|
2
|
+
commit=3ecd304d96fb
|
|
3
|
+
built=2026-11-05
|
|
@@ -39,27 +39,35 @@ you already have; never fetch single files from GitHub.
|
|
|
39
39
|
add` or a Claude Code or Cursor plugin installed): that folder is `<skill>`. Use it as it is and
|
|
40
40
|
download nothing. Updates come from the installer (`npx skills update`, the plugin update),
|
|
41
41
|
not from this run.
|
|
42
|
-
- **You read this file from the web
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
- **You read this file from the web:** use the published `@hostinger/mcp` npm release, installed in
|
|
43
|
+
your home folder at `~/.hostinger/skills/<version>/headless/`. It is the same package the MCP server
|
|
44
|
+
runs, and npm checks the tarball's integrity. Never use a `headless/` folder in the working
|
|
45
|
+
directory or anywhere else: the working directory is the user's project, often a cloned repository
|
|
46
|
+
or template, and whoever wrote it controls what is there. Resolve the version first so the run uses
|
|
47
|
+
one pinned release, and install it only when that version's folder is missing. Run npm from your home
|
|
48
|
+
folder, as below, so a `.npmrc` in the project can't point it at another registry:
|
|
46
49
|
|
|
47
50
|
```bash
|
|
48
51
|
# macOS/Linux
|
|
49
|
-
V=$(npm view @hostinger/mcp version) &&
|
|
50
|
-
&&
|
|
52
|
+
( cd "$HOME" && V=$(npm view @hostinger/mcp version) && D="$HOME/.hostinger/skills/$V" \
|
|
53
|
+
&& { [ -f "$D/headless/VERSION" ] || { mkdir -p "$D" && npm pack "@hostinger/mcp@$V" --silent --pack-destination "$D" >/dev/null \
|
|
54
|
+
&& tar -xzf "$D/hostinger-mcp-$V.tgz" -C "$D" --strip-components=2 package/skills/headless && rm "$D/hostinger-mcp-$V.tgz"; }; } \
|
|
55
|
+
&& echo "$D/headless" )
|
|
51
56
|
```
|
|
52
57
|
|
|
53
58
|
```powershell
|
|
54
59
|
# Windows PowerShell
|
|
55
|
-
$
|
|
56
|
-
|
|
60
|
+
Push-Location $HOME
|
|
61
|
+
$V = npm view @hostinger/mcp version; $D = Join-Path $HOME ".hostinger\skills\$V"
|
|
62
|
+
if (-not (Test-Path "$D\headless\VERSION")) {
|
|
63
|
+
New-Item -ItemType Directory -Force $D | Out-Null; npm pack "@hostinger/mcp@$V" --silent --pack-destination $D | Out-Null
|
|
64
|
+
tar -xzf "$D\hostinger-mcp-$V.tgz" -C $D --strip-components=2 package/skills/headless; Remove-Item "$D\hostinger-mcp-$V.tgz"
|
|
65
|
+
}
|
|
66
|
+
Pop-Location; "$D\headless"
|
|
57
67
|
```
|
|
58
68
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
than `npm view @hostinger/mcp version`, install again the same way, so a run never uses a deleted,
|
|
62
|
-
legacy flow.
|
|
69
|
+
The printed path is `<skill>`. Every later command uses it in full, wherever the run's working
|
|
70
|
+
directory is. A new release installs into its own folder, so a run never uses a deleted, legacy flow.
|
|
63
71
|
|
|
64
72
|
Read `<skill>/VERSION`: its line 1 is the skills version this run uses, which goes into
|
|
65
73
|
`connect.json` (Phase 3).
|
|
@@ -147,7 +155,7 @@ until the tool restarts. When either tool group above is missing:
|
|
|
147
155
|
2. **Write `connect.json`** (Phase 3) before you stop. The skill folder from Phase 1 stays in place,
|
|
148
156
|
so the next run starts from it.
|
|
149
157
|
3. **Send the first message and end your turn.** It follows *The first message* in
|
|
150
|
-
|
|
158
|
+
`<skill>/SKILL.md`: the sign-in link if one was needed, then one line saying Hostinger is set up
|
|
151
159
|
and asking the user to restart the tool (the route says how) and paste the same prompt again so the
|
|
152
160
|
build continues from there. This is a pause, not a run ending: no final report and no build report.
|
|
153
161
|
4. **On the next run** the tools are loaded, so connect skips registration and check-state resumes the
|
|
@@ -170,7 +178,7 @@ in that tool's MCP config (command `npx`, args as above), then follow *register,
|
|
|
170
178
|
### The skills come from Phase 1 only
|
|
171
179
|
|
|
172
180
|
The tool routes don't download the skills: Phase 1 already found or installed `<skill>`. The bundle
|
|
173
|
-
ships a
|
|
181
|
+
ships a `<skill>/VERSION` stamp (`version=<semver>` on line 1, then `commit` and `built`); carry the
|
|
174
182
|
version into `connect.json` (Phase 3) so the build report records which skills ran.
|
|
175
183
|
|
|
176
184
|
## Phase 3 - Write `connect.json`
|
|
@@ -188,7 +196,7 @@ missing) in exactly this shape:
|
|
|
188
196
|
passed to `run-start --tool`.
|
|
189
197
|
- `mcp`: one string, `hostinger-api-mcp <version>`, with the `version` from the `mcp_ok` event.
|
|
190
198
|
Never an object.
|
|
191
|
-
- `skills`: the `version=` value from the
|
|
199
|
+
- `skills`: the `version=` value from the `<skill>/VERSION` stamp you just pulled — the skills bundle
|
|
192
200
|
version this run used. `"unknown"` if the stamp is absent (an old bundle predating the stamp).
|
|
193
201
|
- `auth`: the `method` from the `logged_in` event (`api_token`, `oauth_cached` or `oauth`).
|
|
194
202
|
- `order_id`, `plan`: the active hosting plan's order id and name when you looked them up, else `null`.
|
|
@@ -199,8 +207,8 @@ Extra keys are allowed, but keep these seven as above: the build report reads `t
|
|
|
199
207
|
## Phase 4 - Continue to Headless Hosting steps
|
|
200
208
|
|
|
201
209
|
Once everything is verified and you have the Hostinger MCP Server and the skills, tell the user in
|
|
202
|
-
one line that you're connected to Hostinger, as *The first message* in
|
|
203
|
-
Then go back to
|
|
210
|
+
one line that you're connected to Hostinger, as *The first message* in `<skill>/SKILL.md` describes.
|
|
211
|
+
Then go back to `<skill>/SKILL.md` and continue with check-state without waiting for a reply.
|
|
204
212
|
|
|
205
213
|
|
|
206
214
|
## Reference Sources
|
|
@@ -29,7 +29,7 @@ const MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api
|
|
|
29
29
|
const spawnShell = isWin;
|
|
30
30
|
|
|
31
31
|
function capture(cmd, args) {
|
|
32
|
-
const r = spawnSync(cmd, args, { encoding: 'utf8', shell: spawnShell });
|
|
32
|
+
const r = spawnSync(cmd, args, { encoding: 'utf8', shell: spawnShell, cwd: os.homedir() });
|
|
33
33
|
return { status: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}`, error: r.error };
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -72,7 +72,7 @@ function existingAuth() {
|
|
|
72
72
|
// prefix — forward it so the agent can show it if the browser didn't open.
|
|
73
73
|
function login() {
|
|
74
74
|
return new Promise((resolve) => {
|
|
75
|
-
const child = spawn(MCP[0], [...MCP.slice(1), '--login'], { shell: spawnShell });
|
|
75
|
+
const child = spawn(MCP[0], [...MCP.slice(1), '--login'], { shell: spawnShell, cwd: os.homedir() });
|
|
76
76
|
let diag = '';
|
|
77
77
|
let urlSent = false;
|
|
78
78
|
const collect = (chunk) => {
|
|
@@ -43,7 +43,7 @@ reach it.
|
|
|
43
43
|
|
|
44
44
|
## Step 2 — Hand off
|
|
45
45
|
|
|
46
|
-
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
46
|
+
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`
|
|
47
47
|
folder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the
|
|
48
|
-
|
|
48
|
+
`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its
|
|
49
49
|
steps. That skill owns the rest of the run.
|
|
@@ -36,7 +36,7 @@ same prompt. Never spawn the server yourself or write a JSON-RPC client to reach
|
|
|
36
36
|
|
|
37
37
|
## Step 2 — Hand off
|
|
38
38
|
|
|
39
|
-
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
39
|
+
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`
|
|
40
40
|
folder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the
|
|
41
|
-
|
|
41
|
+
`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its
|
|
42
42
|
steps. That skill owns the rest of the run.
|
|
@@ -41,7 +41,7 @@ same prompt. Never spawn the server yourself or write a JSON-RPC client to reach
|
|
|
41
41
|
|
|
42
42
|
## Step 2 — Hand off
|
|
43
43
|
|
|
44
|
-
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
44
|
+
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`
|
|
45
45
|
folder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the
|
|
46
|
-
|
|
46
|
+
`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its
|
|
47
47
|
steps. That skill owns the rest of the run.
|
|
@@ -102,7 +102,7 @@ npx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout
|
|
|
102
102
|
|
|
103
103
|
## Step 3 — Hand off
|
|
104
104
|
|
|
105
|
-
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
105
|
+
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`
|
|
106
106
|
folder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the
|
|
107
|
-
|
|
107
|
+
`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its
|
|
108
108
|
steps. That skill owns the rest of the run.
|
|
@@ -42,7 +42,7 @@ JSON-RPC client to reach it.
|
|
|
42
42
|
|
|
43
43
|
## Step 2 — Hand off
|
|
44
44
|
|
|
45
|
-
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
45
|
+
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`
|
|
46
46
|
folder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the
|
|
47
|
-
|
|
47
|
+
`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its
|
|
48
48
|
steps. That skill owns the rest of the run.
|
|
@@ -54,7 +54,7 @@ prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.
|
|
|
54
54
|
|
|
55
55
|
## Step 2 — Hand off
|
|
56
56
|
|
|
57
|
-
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
57
|
+
The skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`
|
|
58
58
|
folder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the
|
|
59
|
-
|
|
59
|
+
`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its
|
|
60
60
|
steps. That skill owns the rest of the run.
|
package/skills/headless/wait.mjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// operation to poll by hand instead).
|
|
15
15
|
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
+
import { homedir } from 'node:os';
|
|
17
18
|
|
|
18
19
|
// ── platform-safe binary names (npm/npx are .cmd on Windows), as in connect/bootstrap.mjs ──────────
|
|
19
20
|
const isWin = process.platform === 'win32';
|
|
@@ -55,7 +56,7 @@ function duration(text) {
|
|
|
55
56
|
// One MCP server for the whole wait, spoken to over newline-delimited JSON-RPC on stdio. Once the
|
|
56
57
|
// server dies every pending and later call rejects, so the wait ends with exit 2 rather than hanging.
|
|
57
58
|
function connect() {
|
|
58
|
-
const child = spawn(MCP[0], MCP.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: spawnShell });
|
|
59
|
+
const child = spawn(MCP[0], MCP.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: spawnShell, cwd: homedir() });
|
|
59
60
|
const pending = new Map();
|
|
60
61
|
let buffer = '';
|
|
61
62
|
let diag = '';
|
package/src/core/skills.js
CHANGED
|
@@ -64,59 +64,59 @@ export default [
|
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"uri": "skill://hostinger-headless/connect/SKILL.md",
|
|
67
|
-
"digest": "sha256:
|
|
68
|
-
"size":
|
|
67
|
+
"digest": "sha256:acebdb8ac78c3a86925d5765b7c77b1d5b99efca2e901ce161bf53656455c700",
|
|
68
|
+
"size": 13236,
|
|
69
69
|
"mimeType": "text/markdown",
|
|
70
|
-
"text": "---\nname: hostinger-headless-connect\ndescription: \"Connect step for the Hostinger Headless flow — verify Node, run the bootstrap (reach the Hostinger MCP server + OAuth sign-in), then route to the matching coding-tool skill under tools/ so MCP tools and skills are available before the build steps run. Triggers: connect to Hostinger, set up the Hostinger connector, start a headless build.\"\n---\n\n# Hostinger Headless Skill — connect\n\nYou are a coding agent, and this is a skill for you to execute. Follow the phases below in order.\n\nThis skill goal is to help you to connect to Hostinger via MCP. That can be done via Hostinger Connector or adding the MCP via MCP Server.\n\nWe support different coding Agentic coding tools such as Claude Code, Codex, Cursor, VS Code...and so on. \n\nYour goal is to route those tools to their specific skill inside the tools folder ensuring the tool is fully connected.\n\n\n## Phase 0 — Node (the single one prerequisite)\n\nThe Hostinger MCP server requires Node ≥ 20. Check it:\n\n```bash\nnode -v\n```\n\nIf that errors (Node not installed) or prints a version below 20, install or upgrade Node and re-check — do not try to work around it:\n\n- macOS: `brew install node` (or `nvm install 22 && nvm use 22`)\n- Linux: `nvm install 22 && nvm use 22` (or your distro's Node 20+ package)\n- Windows: `winget install OpenJS.NodeJS.LTS` (or download from nodejs.org)\n\n## Phase 1 — Get the skill folder, then run its bootstrap (deterministic)\n\n### Find or install the skill folder\n\nThe bootstrap script and every step after connect ship in the `headless/` skill folder. Use the copy\nyou already have; never fetch single files from GitHub.\n\n- **An installer put the skill here** (you read this file from disk, from a folder that `npx skills\n add` or a Claude Code or Cursor plugin installed): that folder is `<skill>`. Use it as it is and\n download nothing. Updates come from the installer (`npx skills update`, the plugin update),\n not from this run.\n- **You read this file from the web, or from a `headless/` folder an earlier web run installed:**\n install the skill from the published `@hostinger/mcp` npm release (for an earlier web install, only\n when it is out of date; see below). It is the same package the MCP server runs, and npm checks\n the tarball's integrity. Resolve the version first so the run uses one pinned release:\n\n```bash\n# macOS/Linux\nV=$(npm view @hostinger/mcp version) && npm pack \"@hostinger/mcp@$V\" --silent \\\n && tar -xzf \"hostinger-mcp-$V.tgz\" --strip-components=2 package/skills/headless && rm \"hostinger-mcp-$V.tgz\"\n```\n\n```powershell\n# Windows PowerShell\n$V = npm view @hostinger/mcp version; npm pack \"@hostinger/mcp@$V\" --silent\ntar -xzf \"hostinger-mcp-$V.tgz\" --strip-components=2 package/skills/headless; Remove-Item \"hostinger-mcp-$V.tgz\"\n```\n\nThis leaves a local `headless/` folder, which is `<skill>`. A `headless/` folder already here from an\nearlier web install counts as installed, with one check: when line 1 of its `headless/VERSION` is older\nthan `npm view @hostinger/mcp version`, install again the same way, so a run never uses a deleted,\nlegacy flow.\n\nRead `<skill>/VERSION`: its line 1 is the skills version this run uses, which goes into\n`connect.json` (Phase 3).\n\n### Run the bootstrap\n\nRun the bundled script. It verifies the MCP server and handles sign-in, emitting one JSON event per line on stdout. Run it as a background/streaming process. Show the user only the sign-in URL (`awaiting_user`) or a\nfailure's `detail`, and handle the other events silently (table below).\n\nThe script is safe and inspectable: it only checks the MCP server via `npx` and drives the OAuth sign-in (a browser flow with a localhost callback) — no other network calls, no filesystem writes.\n\n```bash\nnode <skill>/connect/bootstrap.mjs\n```\n\n### Relay these events\n\nThe script emits one JSON object per line:\n\n| Event | What to do |\n| --- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `mcp_ok` | MCP server reachable — continue. |\n| `logged_in` | Already authenticated (env token or a previous sign-in) — continue. |\n| `awaiting_user` (`verificationUri`) | A browser window should have opened on the user's machine. Show the URL in plain prose and wait for the user to finish signing in. The OAuth callback lands on a localhost port, so the sign-in must be completed on this same machine. |\n| `success` | Bootstrap done — continue to Phase 2. |\n| `mcp_unreachable` / `login_failed` (with `detail`) | Stop and show the user the `detail`. Do not improvise a parallel setup by hand. |\n\nIf the environment has a `HOSTINGER_API_TOKEN` set (for example CI), the bootstrap reports `logged_in` immediately — no browser needed.\n\n### Sign in or out manually\n\nThe bootstrap runs the sign-in for you. To run it directly, or to switch accounts, use the MCP's own commands. `--login` opens the browser to the Hostinger sign-in page, waits for the localhost callback, stores the credentials and exits. `--logout` revokes the stored credentials, so the next sign-in opens the browser again.\n\n```bash\n# With a global install (npm install -g @hostinger/mcp):\nhostinger-api-mcp --login\nhostinger-api-mcp --logout\n\n# Without a global install:\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout\n```\n\nCredentials live in `~/.config/hostinger-mcp/credentials.json` (macOS/Linux) or `%APPDATA%\\hostinger-mcp\\credentials.json` (Windows) and are shared by every Hostinger MCP binary. OAuth works only when the tool runs the server over its default local (stdio) transport, which is what every registration below uses; `--http` needs `HOSTINGER_API_TOKEN`.\n\n## Phase 2 - Route to the specific tool instructions\n\n**Use the Hostinger connector only through your tool's native MCP tools** (`mcp__hostinger__*`,\n`mcp__hostinger-*__*`, or however your tool names them). Never spawn the MCP server yourself and\nnever write a JSON-RPC client to talk to it: every connector call must be a visible MCP tool call\nthe user can follow and approve, not a script.\n\n### Check both tool groups, not just \"some Hostinger tools\"\n\nThe build needs **two** groups of connector operations, and they come from different servers. Having\none group says nothing about the other:\n\n| Group | Operations | Comes from | Needed by |\n|---|---|---|---|\n| **Hosting** | `hosting_*` (websites, deploy, build report), `wordpress_*` | `hostinger` (`npx -y @hostinger/mcp@latest`), or the scoped servers such as `hostinger-hosting` + `hostinger-wordpress` | every build |\n| **Ecommerce** | `ecommerce_*` (stores, products, shipping, payments) | **only** `hostinger-ecommerce` (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`), or the Ecommerce MCP in the Hostinger Connector. Neither `hostinger` nor the scoped hosting servers include it | Ecommerce backends (chosen later, in refine) |\n\nCheck each group on its own, right here at connect, before anything else:\n\n- **Hosting:** a Hostinger search tool finds `hosting_websites_list` (e.g. search \"list websites\").\n- **Ecommerce:** a tool from the `hostinger-ecommerce` server is in the session (Claude Code:\n `mcp__hostinger-ecommerce__*`), and its search finds `ecommerce_stores_list` (e.g. search \"list stores\").\n\nOnly when **both** pass are the Hostinger tools \"in this session\". If either is missing, the tools\nare **not** in this session: follow *register, restart, resume* below for the missing server(s) now,\neven though other Hostinger tools respond. Don't wait for refine to choose a backend: the restart\ncosts the least before the user has been asked anything, and finding the gap at provision forces a\nsecond restart in the middle of the build.\n\n### When the Hostinger tools aren't in this session: register, restart, resume\n\nMost coding tools load MCP servers only when they start, so a server registered mid-run isn't usable\nuntil the tool restarts. When either tool group above is missing:\n\n1. **Register the missing servers** in the tool's MCP config, as the tool route below shows: `hostinger`\n (`npx -y @hostinger/mcp@latest`) if the hosting group is missing, and `hostinger-ecommerce`\n (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`) if the ecommerce group is missing.\n Skip a server that is already registered under its name. When neither group is there, register\n both: the backend isn't chosen until refine, and one restart then covers every backend.\n2. **Write `connect.json`** (Phase 3) before you stop. The skill folder from Phase 1 stays in place,\n so the next run starts from it.\n3. **Send the first message and end your turn.** It follows *The first message* in\n `headless/SKILL.md`: the sign-in link if one was needed, then one line saying Hostinger is set up\n and asking the user to restart the tool (the route says how) and paste the same prompt again so the\n build continues from there. This is a pause, not a run ending: no final report and no build report.\n4. **On the next run** the tools are loaded, so connect skips registration and check-state resumes the\n domain's workspace.\n\nIf a tool loads new servers without a restart (VS Code), use them right away and skip steps 3–4.\n\n| Tool | Route |\n|-------------|---------------------------------|\n| Claude Code | `connect/tools/claude-code.md` |\n| Codex | `connect/tools/codex.md` |\n| Cursor | `connect/tools/cursor.md` |\n| VS Code | `connect/tools/vscode.md` |\n| Antigravity | `connect/tools/antigravity.md` |\n| Devin | `connect/tools/devin.md` |\n\nIf the user's tool isn't listed yet, connect it the generic way: register both Hostinger MCP servers\nin that tool's MCP config (command `npx`, args as above), then follow *register, restart, resume*.\n\n### The skills come from Phase 1 only\n\nThe tool routes don't download the skills: Phase 1 already found or installed `<skill>`. The bundle\nships a `headless/VERSION` stamp (`version=<semver>` on line 1, then `commit` and `built`); carry the\nversion into `connect.json` (Phase 3) so the build report records which skills ran.\n\n## Phase 3 - Write `connect.json`\n\nOnce the tool is connected, write `~/.hostinger/<domain>/connect.json` (create the folder if it is\nmissing) in exactly this shape:\n\n```json\n{ \"tool\": \"claude-code\", \"mcp\": \"hostinger-api-mcp 3.8.0\", \"skills\": \"3.9.0\", \"auth\": \"oauth_cached\",\n \"order_id\": null, \"plan\": null, \"connected_at\": \"2026-10-28T10:46:12Z\" }\n```\n\n- `tool`: the `<tool>` from the Phase 2 route (`claude-code`, `codex`, `cursor`, `vscode`,\n `antigravity`, `devin`, or a short lowercase name for an unlisted tool), the same value later\n passed to `run-start --tool`.\n- `mcp`: one string, `hostinger-api-mcp <version>`, with the `version` from the `mcp_ok` event.\n Never an object.\n- `skills`: the `version=` value from the `headless/VERSION` stamp you just pulled — the skills bundle\n version this run used. `\"unknown\"` if the stamp is absent (an old bundle predating the stamp).\n- `auth`: the `method` from the `logged_in` event (`api_token`, `oauth_cached` or `oauth`).\n- `order_id`, `plan`: the active hosting plan's order id and name when you looked them up, else `null`.\n- `connected_at`: the current time in UTC, ISO 8601 to the second.\n\nExtra keys are allowed, but keep these seven as above: the build report reads `tool`, `mcp` and `skills`.\n\n## Phase 4 - Continue to Headless Hosting steps\n\nOnce everything is verified and you have the Hostinger MCP Server and the skills, tell the user in\none line that you're connected to Hostinger, as *The first message* in `headless/SKILL.md` describes.\nThen go back to `headless/SKILL.md` and continue with check-state without waiting for a reply.\n\n\n## Reference Sources\n\n- https://mcpservers.org/servers/hostinger/api-mcp-server\n- https://www.hostinger.com/connector\n"
|
|
70
|
+
"text": "---\nname: hostinger-headless-connect\ndescription: \"Connect step for the Hostinger Headless flow — verify Node, run the bootstrap (reach the Hostinger MCP server + OAuth sign-in), then route to the matching coding-tool skill under tools/ so MCP tools and skills are available before the build steps run. Triggers: connect to Hostinger, set up the Hostinger connector, start a headless build.\"\n---\n\n# Hostinger Headless Skill — connect\n\nYou are a coding agent, and this is a skill for you to execute. Follow the phases below in order.\n\nThis skill goal is to help you to connect to Hostinger via MCP. That can be done via Hostinger Connector or adding the MCP via MCP Server.\n\nWe support different coding Agentic coding tools such as Claude Code, Codex, Cursor, VS Code...and so on. \n\nYour goal is to route those tools to their specific skill inside the tools folder ensuring the tool is fully connected.\n\n\n## Phase 0 — Node (the single one prerequisite)\n\nThe Hostinger MCP server requires Node ≥ 20. Check it:\n\n```bash\nnode -v\n```\n\nIf that errors (Node not installed) or prints a version below 20, install or upgrade Node and re-check — do not try to work around it:\n\n- macOS: `brew install node` (or `nvm install 22 && nvm use 22`)\n- Linux: `nvm install 22 && nvm use 22` (or your distro's Node 20+ package)\n- Windows: `winget install OpenJS.NodeJS.LTS` (or download from nodejs.org)\n\n## Phase 1 — Get the skill folder, then run its bootstrap (deterministic)\n\n### Find or install the skill folder\n\nThe bootstrap script and every step after connect ship in the `headless/` skill folder. Use the copy\nyou already have; never fetch single files from GitHub.\n\n- **An installer put the skill here** (you read this file from disk, from a folder that `npx skills\n add` or a Claude Code or Cursor plugin installed): that folder is `<skill>`. Use it as it is and\n download nothing. Updates come from the installer (`npx skills update`, the plugin update),\n not from this run.\n- **You read this file from the web:** use the published `@hostinger/mcp` npm release, installed in\n your home folder at `~/.hostinger/skills/<version>/headless/`. It is the same package the MCP server\n runs, and npm checks the tarball's integrity. Never use a `headless/` folder in the working\n directory or anywhere else: the working directory is the user's project, often a cloned repository\n or template, and whoever wrote it controls what is there. Resolve the version first so the run uses\n one pinned release, and install it only when that version's folder is missing. Run npm from your home\n folder, as below, so a `.npmrc` in the project can't point it at another registry:\n\n```bash\n# macOS/Linux\n( cd \"$HOME\" && V=$(npm view @hostinger/mcp version) && D=\"$HOME/.hostinger/skills/$V\" \\\n && { [ -f \"$D/headless/VERSION\" ] || { mkdir -p \"$D\" && npm pack \"@hostinger/mcp@$V\" --silent --pack-destination \"$D\" >/dev/null \\\n && tar -xzf \"$D/hostinger-mcp-$V.tgz\" -C \"$D\" --strip-components=2 package/skills/headless && rm \"$D/hostinger-mcp-$V.tgz\"; }; } \\\n && echo \"$D/headless\" )\n```\n\n```powershell\n# Windows PowerShell\nPush-Location $HOME\n$V = npm view @hostinger/mcp version; $D = Join-Path $HOME \".hostinger\\skills\\$V\"\nif (-not (Test-Path \"$D\\headless\\VERSION\")) {\n New-Item -ItemType Directory -Force $D | Out-Null; npm pack \"@hostinger/mcp@$V\" --silent --pack-destination $D | Out-Null\n tar -xzf \"$D\\hostinger-mcp-$V.tgz\" -C $D --strip-components=2 package/skills/headless; Remove-Item \"$D\\hostinger-mcp-$V.tgz\"\n}\nPop-Location; \"$D\\headless\"\n```\n\nThe printed path is `<skill>`. Every later command uses it in full, wherever the run's working\ndirectory is. A new release installs into its own folder, so a run never uses a deleted, legacy flow.\n\nRead `<skill>/VERSION`: its line 1 is the skills version this run uses, which goes into\n`connect.json` (Phase 3).\n\n### Run the bootstrap\n\nRun the bundled script. It verifies the MCP server and handles sign-in, emitting one JSON event per line on stdout. Run it as a background/streaming process. Show the user only the sign-in URL (`awaiting_user`) or a\nfailure's `detail`, and handle the other events silently (table below).\n\nThe script is safe and inspectable: it only checks the MCP server via `npx` and drives the OAuth sign-in (a browser flow with a localhost callback) — no other network calls, no filesystem writes.\n\n```bash\nnode <skill>/connect/bootstrap.mjs\n```\n\n### Relay these events\n\nThe script emits one JSON object per line:\n\n| Event | What to do |\n| --- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `mcp_ok` | MCP server reachable — continue. |\n| `logged_in` | Already authenticated (env token or a previous sign-in) — continue. |\n| `awaiting_user` (`verificationUri`) | A browser window should have opened on the user's machine. Show the URL in plain prose and wait for the user to finish signing in. The OAuth callback lands on a localhost port, so the sign-in must be completed on this same machine. |\n| `success` | Bootstrap done — continue to Phase 2. |\n| `mcp_unreachable` / `login_failed` (with `detail`) | Stop and show the user the `detail`. Do not improvise a parallel setup by hand. |\n\nIf the environment has a `HOSTINGER_API_TOKEN` set (for example CI), the bootstrap reports `logged_in` immediately — no browser needed.\n\n### Sign in or out manually\n\nThe bootstrap runs the sign-in for you. To run it directly, or to switch accounts, use the MCP's own commands. `--login` opens the browser to the Hostinger sign-in page, waits for the localhost callback, stores the credentials and exits. `--logout` revokes the stored credentials, so the next sign-in opens the browser again.\n\n```bash\n# With a global install (npm install -g @hostinger/mcp):\nhostinger-api-mcp --login\nhostinger-api-mcp --logout\n\n# Without a global install:\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout\n```\n\nCredentials live in `~/.config/hostinger-mcp/credentials.json` (macOS/Linux) or `%APPDATA%\\hostinger-mcp\\credentials.json` (Windows) and are shared by every Hostinger MCP binary. OAuth works only when the tool runs the server over its default local (stdio) transport, which is what every registration below uses; `--http` needs `HOSTINGER_API_TOKEN`.\n\n## Phase 2 - Route to the specific tool instructions\n\n**Use the Hostinger connector only through your tool's native MCP tools** (`mcp__hostinger__*`,\n`mcp__hostinger-*__*`, or however your tool names them). Never spawn the MCP server yourself and\nnever write a JSON-RPC client to talk to it: every connector call must be a visible MCP tool call\nthe user can follow and approve, not a script.\n\n### Check both tool groups, not just \"some Hostinger tools\"\n\nThe build needs **two** groups of connector operations, and they come from different servers. Having\none group says nothing about the other:\n\n| Group | Operations | Comes from | Needed by |\n|---|---|---|---|\n| **Hosting** | `hosting_*` (websites, deploy, build report), `wordpress_*` | `hostinger` (`npx -y @hostinger/mcp@latest`), or the scoped servers such as `hostinger-hosting` + `hostinger-wordpress` | every build |\n| **Ecommerce** | `ecommerce_*` (stores, products, shipping, payments) | **only** `hostinger-ecommerce` (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`), or the Ecommerce MCP in the Hostinger Connector. Neither `hostinger` nor the scoped hosting servers include it | Ecommerce backends (chosen later, in refine) |\n\nCheck each group on its own, right here at connect, before anything else:\n\n- **Hosting:** a Hostinger search tool finds `hosting_websites_list` (e.g. search \"list websites\").\n- **Ecommerce:** a tool from the `hostinger-ecommerce` server is in the session (Claude Code:\n `mcp__hostinger-ecommerce__*`), and its search finds `ecommerce_stores_list` (e.g. search \"list stores\").\n\nOnly when **both** pass are the Hostinger tools \"in this session\". If either is missing, the tools\nare **not** in this session: follow *register, restart, resume* below for the missing server(s) now,\neven though other Hostinger tools respond. Don't wait for refine to choose a backend: the restart\ncosts the least before the user has been asked anything, and finding the gap at provision forces a\nsecond restart in the middle of the build.\n\n### When the Hostinger tools aren't in this session: register, restart, resume\n\nMost coding tools load MCP servers only when they start, so a server registered mid-run isn't usable\nuntil the tool restarts. When either tool group above is missing:\n\n1. **Register the missing servers** in the tool's MCP config, as the tool route below shows: `hostinger`\n (`npx -y @hostinger/mcp@latest`) if the hosting group is missing, and `hostinger-ecommerce`\n (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`) if the ecommerce group is missing.\n Skip a server that is already registered under its name. When neither group is there, register\n both: the backend isn't chosen until refine, and one restart then covers every backend.\n2. **Write `connect.json`** (Phase 3) before you stop. The skill folder from Phase 1 stays in place,\n so the next run starts from it.\n3. **Send the first message and end your turn.** It follows *The first message* in\n `<skill>/SKILL.md`: the sign-in link if one was needed, then one line saying Hostinger is set up\n and asking the user to restart the tool (the route says how) and paste the same prompt again so the\n build continues from there. This is a pause, not a run ending: no final report and no build report.\n4. **On the next run** the tools are loaded, so connect skips registration and check-state resumes the\n domain's workspace.\n\nIf a tool loads new servers without a restart (VS Code), use them right away and skip steps 3–4.\n\n| Tool | Route |\n|-------------|---------------------------------|\n| Claude Code | `connect/tools/claude-code.md` |\n| Codex | `connect/tools/codex.md` |\n| Cursor | `connect/tools/cursor.md` |\n| VS Code | `connect/tools/vscode.md` |\n| Antigravity | `connect/tools/antigravity.md` |\n| Devin | `connect/tools/devin.md` |\n\nIf the user's tool isn't listed yet, connect it the generic way: register both Hostinger MCP servers\nin that tool's MCP config (command `npx`, args as above), then follow *register, restart, resume*.\n\n### The skills come from Phase 1 only\n\nThe tool routes don't download the skills: Phase 1 already found or installed `<skill>`. The bundle\nships a `<skill>/VERSION` stamp (`version=<semver>` on line 1, then `commit` and `built`); carry the\nversion into `connect.json` (Phase 3) so the build report records which skills ran.\n\n## Phase 3 - Write `connect.json`\n\nOnce the tool is connected, write `~/.hostinger/<domain>/connect.json` (create the folder if it is\nmissing) in exactly this shape:\n\n```json\n{ \"tool\": \"claude-code\", \"mcp\": \"hostinger-api-mcp 3.8.0\", \"skills\": \"3.9.0\", \"auth\": \"oauth_cached\",\n \"order_id\": null, \"plan\": null, \"connected_at\": \"2026-10-28T10:46:12Z\" }\n```\n\n- `tool`: the `<tool>` from the Phase 2 route (`claude-code`, `codex`, `cursor`, `vscode`,\n `antigravity`, `devin`, or a short lowercase name for an unlisted tool), the same value later\n passed to `run-start --tool`.\n- `mcp`: one string, `hostinger-api-mcp <version>`, with the `version` from the `mcp_ok` event.\n Never an object.\n- `skills`: the `version=` value from the `<skill>/VERSION` stamp you just pulled — the skills bundle\n version this run used. `\"unknown\"` if the stamp is absent (an old bundle predating the stamp).\n- `auth`: the `method` from the `logged_in` event (`api_token`, `oauth_cached` or `oauth`).\n- `order_id`, `plan`: the active hosting plan's order id and name when you looked them up, else `null`.\n- `connected_at`: the current time in UTC, ISO 8601 to the second.\n\nExtra keys are allowed, but keep these seven as above: the build report reads `tool`, `mcp` and `skills`.\n\n## Phase 4 - Continue to Headless Hosting steps\n\nOnce everything is verified and you have the Hostinger MCP Server and the skills, tell the user in\none line that you're connected to Hostinger, as *The first message* in `<skill>/SKILL.md` describes.\nThen go back to `<skill>/SKILL.md` and continue with check-state without waiting for a reply.\n\n\n## Reference Sources\n\n- https://mcpservers.org/servers/hostinger/api-mcp-server\n- https://www.hostinger.com/connector\n"
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"uri": "skill://hostinger-headless/connect/bootstrap.mjs",
|
|
74
|
-
"digest": "sha256:
|
|
75
|
-
"size":
|
|
74
|
+
"digest": "sha256:0428f0e2c7cf270be13fe3d09ba0f638854c681bc148e8eafb5a31cd70667eed",
|
|
75
|
+
"size": 5415,
|
|
76
76
|
"mimeType": "text/plain",
|
|
77
|
-
"text": "#!/usr/bin/env node\n// Hostinger Headless — bootstrap.\n// Verifies the Hostinger MCP server is reachable and drives the OAuth sign-in,\n// emitting one JSON event per line on stdout so a coding agent can relay\n// progress to the user. No filesystem writes besides what the MCP itself\n// stores (OAuth credentials); no network calls of its own.\nimport { spawn, spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\n\n// ── tiny event protocol (one JSON object per line) ───────────────────────────\nconst emit = (event, extra = {}) =>\n process.stdout.write(JSON.stringify({ event, ...extra }) + '\\n');\nconst fail = (event, extra = {}) => {\n emit(event, { ok: false, ...extra });\n process.exit(1);\n};\n\n// ── platform-safe binary names (npm/npx are .cmd on Windows) ─────────────────\nconst isWin = process.platform === 'win32';\nconst bin = (name) => (isWin ? `${name}.cmd` : name);\n// Run the MCP via npx — no global install, always the latest published version.\n// Use --package so npx resolves the published package, then invoke the unified binary.\nconst MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp'];\n// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell\n// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all\n// fixed literals, so enabling the shell introduces no injection risk.\nconst spawnShell = isWin;\n\nfunction capture(cmd, args) {\n const r = spawnSync(cmd, args, { encoding: 'utf8', shell: spawnShell });\n return { status: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}`, error: r.error };\n}\n\n// ── 1. MCP server reachable ──────────────────────────────────────────────────\nfunction checkMcp() {\n const r = capture(MCP[0], [...MCP.slice(1), '--version']);\n if (r.status !== 0) fail('mcp_unreachable', { detail: r.out.trim().slice(0, 400) });\n // npx interleaves \"npm notice …\" lines, so pick the first semver-looking token.\n const version = (r.out.match(/\\d+\\.\\d+\\.\\d+[^\\s]*/) || [])[0] || r.out.trim().split('\\n').pop();\n emit('mcp_ok', { version });\n}\n\n// ── 2. Existing auth? ────────────────────────────────────────────────────────\n// An env token always wins (the MCP bypasses OAuth entirely when it is set).\n// Otherwise stored OAuth credentials mean a previous sign-in can be reused —\n// the MCP refreshes expired tokens automatically on the first tool call.\nfunction existingAuth() {\n if (process.env.HOSTINGER_API_TOKEN || process.env.API_TOKEN) {\n return 'api_token';\n }\n const base = isWin ? (process.env.APPDATA || os.homedir()) : path.join(os.homedir(), '.config');\n const credsPath = path.join(base, 'hostinger-mcp', 'credentials.json');\n if (existsSync(credsPath)) {\n try {\n const creds = JSON.parse(readFileSync(credsPath, 'utf8'));\n if (creds.refresh_token || (creds.access_token && creds.expires_at > Date.now())) {\n return 'oauth_cached';\n }\n } catch {\n /* unreadable credentials — fall through to a fresh login */\n }\n }\n return null;\n}\n\n// ── 3. Login (human-in-the-loop browser flow) ────────────────────────────────\n// `hostinger-api-mcp --login` opens the Hostinger sign-in page and listens on\n// an ephemeral localhost port for the OAuth callback, so the browser must run\n// on this same machine. It prints the sign-in URL to stderr with a stable\n// prefix — forward it so the agent can show it if the browser didn't open.\nfunction login() {\n return new Promise((resolve) => {\n const child = spawn(MCP[0], [...MCP.slice(1), '--login'], { shell: spawnShell });\n let diag = '';\n let urlSent = false;\n const collect = (chunk) => {\n diag = (diag + chunk).slice(-2000);\n if (!urlSent) {\n const m = diag.match(/Opening browser for sign-in:\\s*\\n\\s*(https?:\\/\\/\\S+)/);\n if (m) {\n urlSent = true;\n emit('awaiting_user', {\n verificationUri: m[1],\n note: 'A browser window should have opened. If not, open this URL manually on this machine and sign in.',\n });\n }\n }\n };\n child.stderr?.on('data', collect);\n child.stdout?.on('data', collect);\n child.on('close', (code) => {\n if (code === 0 && /Sign-in successful/i.test(diag)) {\n emit('logged_in', { method: 'oauth' });\n resolve();\n return;\n }\n const detail = diag.trim().slice(-1500) || `--login exited (code ${code}) with no output.`;\n fail('login_failed', { code, detail });\n });\n child.on('error', (e) => fail('login_failed', { detail: String(e) }));\n });\n}\n\n// ── main ─────────────────────────────────────────────────────────────────────\ncheckMcp();\nconst method = existingAuth();\nif (method) {\n emit('logged_in', { method });\n} else {\n await login();\n}\nemit('success');\n"
|
|
77
|
+
"text": "#!/usr/bin/env node\n// Hostinger Headless — bootstrap.\n// Verifies the Hostinger MCP server is reachable and drives the OAuth sign-in,\n// emitting one JSON event per line on stdout so a coding agent can relay\n// progress to the user. No filesystem writes besides what the MCP itself\n// stores (OAuth credentials); no network calls of its own.\nimport { spawn, spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\n\n// ── tiny event protocol (one JSON object per line) ───────────────────────────\nconst emit = (event, extra = {}) =>\n process.stdout.write(JSON.stringify({ event, ...extra }) + '\\n');\nconst fail = (event, extra = {}) => {\n emit(event, { ok: false, ...extra });\n process.exit(1);\n};\n\n// ── platform-safe binary names (npm/npx are .cmd on Windows) ─────────────────\nconst isWin = process.platform === 'win32';\nconst bin = (name) => (isWin ? `${name}.cmd` : name);\n// Run the MCP via npx — no global install, always the latest published version.\n// Use --package so npx resolves the published package, then invoke the unified binary.\nconst MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp'];\n// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell\n// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all\n// fixed literals, so enabling the shell introduces no injection risk.\nconst spawnShell = isWin;\n\nfunction capture(cmd, args) {\n const r = spawnSync(cmd, args, { encoding: 'utf8', shell: spawnShell, cwd: os.homedir() });\n return { status: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}`, error: r.error };\n}\n\n// ── 1. MCP server reachable ──────────────────────────────────────────────────\nfunction checkMcp() {\n const r = capture(MCP[0], [...MCP.slice(1), '--version']);\n if (r.status !== 0) fail('mcp_unreachable', { detail: r.out.trim().slice(0, 400) });\n // npx interleaves \"npm notice …\" lines, so pick the first semver-looking token.\n const version = (r.out.match(/\\d+\\.\\d+\\.\\d+[^\\s]*/) || [])[0] || r.out.trim().split('\\n').pop();\n emit('mcp_ok', { version });\n}\n\n// ── 2. Existing auth? ────────────────────────────────────────────────────────\n// An env token always wins (the MCP bypasses OAuth entirely when it is set).\n// Otherwise stored OAuth credentials mean a previous sign-in can be reused —\n// the MCP refreshes expired tokens automatically on the first tool call.\nfunction existingAuth() {\n if (process.env.HOSTINGER_API_TOKEN || process.env.API_TOKEN) {\n return 'api_token';\n }\n const base = isWin ? (process.env.APPDATA || os.homedir()) : path.join(os.homedir(), '.config');\n const credsPath = path.join(base, 'hostinger-mcp', 'credentials.json');\n if (existsSync(credsPath)) {\n try {\n const creds = JSON.parse(readFileSync(credsPath, 'utf8'));\n if (creds.refresh_token || (creds.access_token && creds.expires_at > Date.now())) {\n return 'oauth_cached';\n }\n } catch {\n /* unreadable credentials — fall through to a fresh login */\n }\n }\n return null;\n}\n\n// ── 3. Login (human-in-the-loop browser flow) ────────────────────────────────\n// `hostinger-api-mcp --login` opens the Hostinger sign-in page and listens on\n// an ephemeral localhost port for the OAuth callback, so the browser must run\n// on this same machine. It prints the sign-in URL to stderr with a stable\n// prefix — forward it so the agent can show it if the browser didn't open.\nfunction login() {\n return new Promise((resolve) => {\n const child = spawn(MCP[0], [...MCP.slice(1), '--login'], { shell: spawnShell, cwd: os.homedir() });\n let diag = '';\n let urlSent = false;\n const collect = (chunk) => {\n diag = (diag + chunk).slice(-2000);\n if (!urlSent) {\n const m = diag.match(/Opening browser for sign-in:\\s*\\n\\s*(https?:\\/\\/\\S+)/);\n if (m) {\n urlSent = true;\n emit('awaiting_user', {\n verificationUri: m[1],\n note: 'A browser window should have opened. If not, open this URL manually on this machine and sign in.',\n });\n }\n }\n };\n child.stderr?.on('data', collect);\n child.stdout?.on('data', collect);\n child.on('close', (code) => {\n if (code === 0 && /Sign-in successful/i.test(diag)) {\n emit('logged_in', { method: 'oauth' });\n resolve();\n return;\n }\n const detail = diag.trim().slice(-1500) || `--login exited (code ${code}) with no output.`;\n fail('login_failed', { code, detail });\n });\n child.on('error', (e) => fail('login_failed', { detail: String(e) }));\n });\n}\n\n// ── main ─────────────────────────────────────────────────────────────────────\ncheckMcp();\nconst method = existingAuth();\nif (method) {\n emit('logged_in', { method });\n} else {\n await login();\n}\nemit('success');\n"
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"uri": "skill://hostinger-headless/connect/tools/antigravity.md",
|
|
81
|
-
"digest": "sha256:
|
|
82
|
-
"size":
|
|
81
|
+
"digest": "sha256:5fffcf33a1f45bcb18a80a449decbec1988edc3fde18e49e2a70c6d507222985",
|
|
82
|
+
"size": 2254,
|
|
83
83
|
"mimeType": "text/markdown",
|
|
84
|
-
"text": "---\nname: headless-connect-antigravity\ndescription: \"Specific Antigravity Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Google Antigravity.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Antigravity's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, add the missing server(s) to `~/.gemini/antigravity/mcp_config.json` (Windows:\n`C:\\Users\\<USERNAME>\\.gemini\\antigravity\\mcp_config.json`), which you can reach from the Agent panel's\n\"...\" → MCP Servers → Manage MCP Servers → View raw config. Merge them into any existing\n`mcpServers` rather than replacing the file. The bootstrap already signed you in, so they reuse the\ncached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are\nno tokens to handle:\n\n```json\n{\n \"mcpServers\": {\n \"hostinger\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@hostinger/mcp@latest\"]\n },\n \"hostinger-ecommerce\": {\n \"command\": \"npx\",\n \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nAntigravity loads MCP servers only when the editor starts, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to fully quit and reopen\nAntigravity and paste the same prompt. Never spawn the server yourself or write a JSON-RPC client to\nreach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
84
|
+
"text": "---\nname: headless-connect-antigravity\ndescription: \"Specific Antigravity Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Google Antigravity.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Antigravity's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, add the missing server(s) to `~/.gemini/antigravity/mcp_config.json` (Windows:\n`C:\\Users\\<USERNAME>\\.gemini\\antigravity\\mcp_config.json`), which you can reach from the Agent panel's\n\"...\" → MCP Servers → Manage MCP Servers → View raw config. Merge them into any existing\n`mcpServers` rather than replacing the file. The bootstrap already signed you in, so they reuse the\ncached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are\nno tokens to handle:\n\n```json\n{\n \"mcpServers\": {\n \"hostinger\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@hostinger/mcp@latest\"]\n },\n \"hostinger-ecommerce\": {\n \"command\": \"npx\",\n \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nAntigravity loads MCP servers only when the editor starts, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to fully quit and reopen\nAntigravity and paste the same prompt. Never spawn the server yourself or write a JSON-RPC client to\nreach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"uri": "skill://hostinger-headless/connect/tools/claude-code.md",
|
|
88
|
-
"digest": "sha256:
|
|
89
|
-
"size":
|
|
88
|
+
"digest": "sha256:d7f07f341f4b9cef21d97e16d5fe8b9d8cb58a0c075296934250d2b926c623e2",
|
|
89
|
+
"size": 2329,
|
|
90
90
|
"mimeType": "text/markdown",
|
|
91
|
-
"text": "---\nname: headless-connect-claude-code\ndescription: \"Specific Claude Code Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Your goal is to make Hostinger MCP tools and Skills available in Claude Code. \n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are available.\n\n\n## Step 1 — Make the MCP tools available\n\nStages 2–10 drive the Hostinger connector through Claude Code's native MCP tools. Check **both** tool\ngroups as *Check both tool groups* in `connect/SKILL.md` says:\n\n- **Hosting:** `mcp__hostinger__*`, or the scoped `mcp__hostinger-hosting__*`.\n- **Ecommerce:** `mcp__hostinger-ecommerce__*`. The scoped servers (`hostinger-hosting`,\n `hostinger-wordpress`, `hostinger-domains`, …) never include it, so a session full of\n `mcp__hostinger-*__*` tools can still be missing it.\n\nBoth present → use them and move on. Either missing → register the missing server(s) at user scope\nnow (`claude mcp list` shows what's already registered; skip those). The bootstrap already signed you\nin, so they reuse the cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or\n`HOSTINGER_API_TOKEN`) and there are no tokens to handle:\n\n```bash\nclaude mcp add hostinger --scope user -- npx -y @hostinger/mcp@latest # hosting group missing\nclaude mcp add hostinger-ecommerce --scope user -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp # ecommerce group missing\n```\n\nClaude Code loads MCP servers only at startup, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to exit Claude Code, start it\nagain in the same folder (`claude`, or `claude --continue` to keep this conversation) and paste the\nsame prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
91
|
+
"text": "---\nname: headless-connect-claude-code\ndescription: \"Specific Claude Code Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Your goal is to make Hostinger MCP tools and Skills available in Claude Code. \n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are available.\n\n\n## Step 1 — Make the MCP tools available\n\nStages 2–10 drive the Hostinger connector through Claude Code's native MCP tools. Check **both** tool\ngroups as *Check both tool groups* in `connect/SKILL.md` says:\n\n- **Hosting:** `mcp__hostinger__*`, or the scoped `mcp__hostinger-hosting__*`.\n- **Ecommerce:** `mcp__hostinger-ecommerce__*`. The scoped servers (`hostinger-hosting`,\n `hostinger-wordpress`, `hostinger-domains`, …) never include it, so a session full of\n `mcp__hostinger-*__*` tools can still be missing it.\n\nBoth present → use them and move on. Either missing → register the missing server(s) at user scope\nnow (`claude mcp list` shows what's already registered; skip those). The bootstrap already signed you\nin, so they reuse the cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or\n`HOSTINGER_API_TOKEN`) and there are no tokens to handle:\n\n```bash\nclaude mcp add hostinger --scope user -- npx -y @hostinger/mcp@latest # hosting group missing\nclaude mcp add hostinger-ecommerce --scope user -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp # ecommerce group missing\n```\n\nClaude Code loads MCP servers only at startup, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to exit Claude Code, start it\nagain in the same folder (`claude`, or `claude --continue` to keep this conversation) and paste the\nsame prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"uri": "skill://hostinger-headless/connect/tools/codex.md",
|
|
95
|
-
"digest": "sha256:
|
|
96
|
-
"size":
|
|
95
|
+
"digest": "sha256:0c994eef605dea3b7321ae12fb30079faeeff944ebe9637477ecf46e4686e309",
|
|
96
|
+
"size": 2076,
|
|
97
97
|
"mimeType": "text/markdown",
|
|
98
|
-
"text": "---\nname: headless-connect-codex\ndescription: \"Specific Codex Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Codex.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Codex's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s); skip one already registered. The bootstrap already signed you in, so they reuse the cached\nsign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no tokens\nto handle:\n\n```bash\ncodex mcp add hostinger -- npx -y @hostinger/mcp@latest\ncodex mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp\n```\n\nor add them to `~/.codex/config.toml`:\n\n```toml\n[mcp_servers.hostinger]\ncommand = \"npx\"\nargs = [\"-y\", \"@hostinger/mcp@latest\"]\n\n[mcp_servers.hostinger-ecommerce]\ncommand = \"npx\"\nargs = [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n```\n\nCodex loads MCP servers only when it starts, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to restart Codex and paste the\nsame prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
98
|
+
"text": "---\nname: headless-connect-codex\ndescription: \"Specific Codex Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Codex.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Codex's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s); skip one already registered. The bootstrap already signed you in, so they reuse the cached\nsign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no tokens\nto handle:\n\n```bash\ncodex mcp add hostinger -- npx -y @hostinger/mcp@latest\ncodex mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp\n```\n\nor add them to `~/.codex/config.toml`:\n\n```toml\n[mcp_servers.hostinger]\ncommand = \"npx\"\nargs = [\"-y\", \"@hostinger/mcp@latest\"]\n\n[mcp_servers.hostinger-ecommerce]\ncommand = \"npx\"\nargs = [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n```\n\nCodex loads MCP servers only when it starts, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to restart Codex and paste the\nsame prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
"uri": "skill://hostinger-headless/connect/tools/cursor.md",
|
|
102
|
-
"digest": "sha256:
|
|
103
|
-
"size":
|
|
102
|
+
"digest": "sha256:4851b75665d3f1f6a2d06d609cd13e7dc47caf99302ccc7b8bf27ceab4045e54",
|
|
103
|
+
"size": 4953,
|
|
104
104
|
"mimeType": "text/markdown",
|
|
105
|
-
"text": "---\nname: headless-connect-cursor\ndescription: \"Specific Cursor Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Install (or reach) the Hostinger MCP in Cursor.\n- Sign the user in with OAuth 2.0 PKCE — the MCP opens a browser so they only need to log in.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Install / reach Hostinger MCP\n\n**Prerequisite:** Node.js 20 or newer on `PATH` (`node -v`). The published package engines require `>=20`; use Node 24 if you follow the package README.\n\nThe Hostinger MCP is the npm package `@hostinger/mcp`. You do **not** need a global install — Cursor (and this session) can run it through `npx`. Prefer the unified binary `hostinger-api-mcp` (every operation). Scoped binaries such as `hostinger-hosting-mcp` are optional.\n\n### Option A — no global install (recommended)\n\n```bash\n# Verify the package/binary is reachable\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --version\n```\n\n### Option B — global install\n\n```bash\nnpm install -g @hostinger/mcp\n# or: yarn global add @hostinger/mcp\n# or: pnpm add -g @hostinger/mcp\n\nhostinger-api-mcp --version\n```\n\n### Register the servers in Cursor\n\nCheck **both** tool groups, as *Check both tool groups* in `connect/SKILL.md` says: hosting\n(`mcp__hostinger__*` or `mcp__hostinger-hosting__*`) **and** ecommerce (`mcp__hostinger-ecommerce__*`).\nThe scoped `mcp__hostinger-*__*` servers never include ecommerce, so they alone don't count. Both\npresent → skip registration: go to Step 2 only if you still need sign-in, otherwise go to Step 3.\n\nIf either is missing, register the missing server(s) in `~/.cursor/mcp.json` (global, so every project and every\nlater run has them). Merge them into any existing `mcpServers` rather than replacing the file.\n**Do not** put `HOSTINGER_API_TOKEN` in this file when you want OAuth: leave the token unset so\nOAuth can run:\n\n```json\n{\n \"mcpServers\": {\n \"hostinger\": {\n \"command\": \"npx\",\n \"args\": [\"--yes\", \"--package=@hostinger/mcp@latest\", \"hostinger-api-mcp\"]\n },\n \"hostinger-ecommerce\": {\n \"command\": \"npx\",\n \"args\": [\"--yes\", \"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nCursor loads MCP servers only when the editor starts, so after Step 2 write `connect.json` and follow\n*register, restart, resume* in `connect/SKILL.md`: ask the user to fully quit and reopen Cursor\n(and, if Cursor asks, enable the two Hostinger servers under Settings → MCP), then paste the same\nprompt. Never spawn the server yourself or write a JSON-RPC client to reach it: every connector call\nmust be a native MCP tool call.\n\n## Step 2 — Authenticate with OAuth (browser opens)\n\nPrefer OAuth for interactive Cursor use. OAuth works only in **stdio** mode (Cursor's default for this local server). Skip this step if `HOSTINGER_API_TOKEN` is already set in the environment (that bypasses OAuth entirely).\n\nIf the connect bootstrap already emitted `logged_in` / `success`, credentials are already on disk — continue to Step 3.\n\nOtherwise run the login command. It registers a dynamic OAuth client (once per machine), **opens the user's browser** to Hostinger's authorization page, listens on a localhost callback, stores tokens, and exits:\n\n```bash\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login\n```\n\n(With a global install: `hostinger-api-mcp --login`.)\n\nWhat to tell the user:\n\n1. A browser window should open automatically for Hostinger sign-in.\n2. If it does not, the process prints `[OAuth] Opening browser for sign-in:` plus the URL on stderr — show that URL and ask them to open it on **this same machine** (the callback is `http://127.0.0.1:<ephemeral-port>/oauth/callback`).\n3. Wait until the command prints that sign-in succeeded and exits 0.\n\nCredentials are stored and reused automatically:\n\n- macOS / Linux: `~/.config/hostinger-mcp/credentials.json` (mode 0600)\n- Windows: `%APPDATA%\\hostinger-mcp\\credentials.json`\n\nThey are shared across every Hostinger MCP binary. Expired access tokens refresh on their own; a revoked refresh token re-triggers the browser flow.\n\nTo sign out later:\n\n```bash\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout\n```\n\n**API token (optional, CI / scripts):** set `HOSTINGER_API_TOKEN` in the environment before launching Cursor. When present it always wins and no OAuth code runs. Do not commit the token.\n\n## Step 3 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
105
|
+
"text": "---\nname: headless-connect-cursor\ndescription: \"Specific Cursor Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Install (or reach) the Hostinger MCP in Cursor.\n- Sign the user in with OAuth 2.0 PKCE — the MCP opens a browser so they only need to log in.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Install / reach Hostinger MCP\n\n**Prerequisite:** Node.js 20 or newer on `PATH` (`node -v`). The published package engines require `>=20`; use Node 24 if you follow the package README.\n\nThe Hostinger MCP is the npm package `@hostinger/mcp`. You do **not** need a global install — Cursor (and this session) can run it through `npx`. Prefer the unified binary `hostinger-api-mcp` (every operation). Scoped binaries such as `hostinger-hosting-mcp` are optional.\n\n### Option A — no global install (recommended)\n\n```bash\n# Verify the package/binary is reachable\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --version\n```\n\n### Option B — global install\n\n```bash\nnpm install -g @hostinger/mcp\n# or: yarn global add @hostinger/mcp\n# or: pnpm add -g @hostinger/mcp\n\nhostinger-api-mcp --version\n```\n\n### Register the servers in Cursor\n\nCheck **both** tool groups, as *Check both tool groups* in `connect/SKILL.md` says: hosting\n(`mcp__hostinger__*` or `mcp__hostinger-hosting__*`) **and** ecommerce (`mcp__hostinger-ecommerce__*`).\nThe scoped `mcp__hostinger-*__*` servers never include ecommerce, so they alone don't count. Both\npresent → skip registration: go to Step 2 only if you still need sign-in, otherwise go to Step 3.\n\nIf either is missing, register the missing server(s) in `~/.cursor/mcp.json` (global, so every project and every\nlater run has them). Merge them into any existing `mcpServers` rather than replacing the file.\n**Do not** put `HOSTINGER_API_TOKEN` in this file when you want OAuth: leave the token unset so\nOAuth can run:\n\n```json\n{\n \"mcpServers\": {\n \"hostinger\": {\n \"command\": \"npx\",\n \"args\": [\"--yes\", \"--package=@hostinger/mcp@latest\", \"hostinger-api-mcp\"]\n },\n \"hostinger-ecommerce\": {\n \"command\": \"npx\",\n \"args\": [\"--yes\", \"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nCursor loads MCP servers only when the editor starts, so after Step 2 write `connect.json` and follow\n*register, restart, resume* in `connect/SKILL.md`: ask the user to fully quit and reopen Cursor\n(and, if Cursor asks, enable the two Hostinger servers under Settings → MCP), then paste the same\nprompt. Never spawn the server yourself or write a JSON-RPC client to reach it: every connector call\nmust be a native MCP tool call.\n\n## Step 2 — Authenticate with OAuth (browser opens)\n\nPrefer OAuth for interactive Cursor use. OAuth works only in **stdio** mode (Cursor's default for this local server). Skip this step if `HOSTINGER_API_TOKEN` is already set in the environment (that bypasses OAuth entirely).\n\nIf the connect bootstrap already emitted `logged_in` / `success`, credentials are already on disk — continue to Step 3.\n\nOtherwise run the login command. It registers a dynamic OAuth client (once per machine), **opens the user's browser** to Hostinger's authorization page, listens on a localhost callback, stores tokens, and exits:\n\n```bash\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login\n```\n\n(With a global install: `hostinger-api-mcp --login`.)\n\nWhat to tell the user:\n\n1. A browser window should open automatically for Hostinger sign-in.\n2. If it does not, the process prints `[OAuth] Opening browser for sign-in:` plus the URL on stderr — show that URL and ask them to open it on **this same machine** (the callback is `http://127.0.0.1:<ephemeral-port>/oauth/callback`).\n3. Wait until the command prints that sign-in succeeded and exits 0.\n\nCredentials are stored and reused automatically:\n\n- macOS / Linux: `~/.config/hostinger-mcp/credentials.json` (mode 0600)\n- Windows: `%APPDATA%\\hostinger-mcp\\credentials.json`\n\nThey are shared across every Hostinger MCP binary. Expired access tokens refresh on their own; a revoked refresh token re-triggers the browser flow.\n\nTo sign out later:\n\n```bash\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout\n```\n\n**API token (optional, CI / scripts):** set `HOSTINGER_API_TOKEN` in the environment before launching Cursor. When present it always wins and no OAuth code runs. Do not commit the token.\n\n## Step 3 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"uri": "skill://hostinger-headless/connect/tools/devin.md",
|
|
109
|
-
"digest": "sha256:
|
|
110
|
-
"size":
|
|
109
|
+
"digest": "sha256:d7bdd2e2da9c1b06aa5bcb607bef72fa0282d548b9a4b81a02dd5836b80880a7",
|
|
110
|
+
"size": 2216,
|
|
111
111
|
"mimeType": "text/markdown",
|
|
112
|
-
"text": "---\nname: headless-connect-devin\ndescription: \"Specific Devin Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Devin.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Devin's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s) for Devin; skip one already registered. The bootstrap already signed you in, so they reuse the\ncached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no\ntokens to handle.\n\n- **Devin CLI:**\n\n ```bash\n devin mcp add hostinger -- npx -y @hostinger/mcp@latest\n devin mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp\n ```\n\n- **Devin app:** Settings → MCP Marketplace → **Add Your Own**, transport **STDIO**, once per server:\n\n ```json\n { \"command\": \"npx\", \"args\": [\"-y\", \"@hostinger/mcp@latest\"] }\n ```\n\n ```json\n { \"command\": \"npx\", \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"] }\n ```\n\nIf the tools still aren't in this session, follow *register, restart, resume* in `connect/SKILL.md`:\nwrite `connect.json`, then ask the user to restart the Devin session (on Devin Desktop,\nfully quit and reopen the app) and paste the same prompt. Never spawn the server yourself or write a\nJSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
112
|
+
"text": "---\nname: headless-connect-devin\ndescription: \"Specific Devin Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Devin.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Devin's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s) for Devin; skip one already registered. The bootstrap already signed you in, so they reuse the\ncached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no\ntokens to handle.\n\n- **Devin CLI:**\n\n ```bash\n devin mcp add hostinger -- npx -y @hostinger/mcp@latest\n devin mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp\n ```\n\n- **Devin app:** Settings → MCP Marketplace → **Add Your Own**, transport **STDIO**, once per server:\n\n ```json\n { \"command\": \"npx\", \"args\": [\"-y\", \"@hostinger/mcp@latest\"] }\n ```\n\n ```json\n { \"command\": \"npx\", \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"] }\n ```\n\nIf the tools still aren't in this session, follow *register, restart, resume* in `connect/SKILL.md`:\nwrite `connect.json`, then ask the user to restart the Devin session (on Devin Desktop,\nfully quit and reopen the app) and paste the same prompt. Never spawn the server yourself or write a\nJSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"uri": "skill://hostinger-headless/connect/tools/vscode.md",
|
|
116
|
-
"digest": "sha256:
|
|
117
|
-
"size":
|
|
116
|
+
"digest": "sha256:149bd7f16df386f7d7cee2301416b6bf120114a1f0d445aa9d0a04bc94d963a4",
|
|
117
|
+
"size": 2650,
|
|
118
118
|
"mimeType": "text/markdown",
|
|
119
|
-
"text": "---\nname: headless-connect-vscode\ndescription: \"Specific VS Code Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in VS Code (GitHub Copilot / agent mode).\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through VS Code's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s) for VS Code; skip one already registered. The bootstrap already signed you in, so they reuse\nthe cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are\nno tokens to handle. Add them to `.vscode/mcp.json` (workspace) or your user `settings.json` under\n`mcp`:\n\n```json\n// .vscode/mcp.json\n{\n \"servers\": {\n \"hostinger\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@hostinger/mcp@latest\"]\n },\n \"hostinger-ecommerce\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nOr add them in two commands:\n\n```bash\ncode --add-mcp '{\"name\":\"hostinger\",\"command\":\"npx\",\"args\":[\"-y\",\"@hostinger/mcp@latest\"]}'\ncode --add-mcp '{\"name\":\"hostinger-ecommerce\",\"command\":\"npx\",\"args\":[\"--package=@hostinger/mcp@latest\",\"hostinger-ecommerce-mcp\"]}'\n```\n\nVS Code picks up new servers and their tools become available right away, with no editor restart.\n(Alternatively, the official Hostinger Connector extension installs the same server:\n`code --install-extension hostinger-official.hostinger-connector`.)\n\nIf the tools still don't show up, follow *register, restart, resume* in `connect/SKILL.md`: write\n`connect.json`, then ask the user to run **Developer: Reload Window** and paste the same\nprompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
119
|
+
"text": "---\nname: headless-connect-vscode\ndescription: \"Specific VS Code Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in VS Code (GitHub Copilot / agent mode).\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through VS Code's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s) for VS Code; skip one already registered. The bootstrap already signed you in, so they reuse\nthe cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are\nno tokens to handle. Add them to `.vscode/mcp.json` (workspace) or your user `settings.json` under\n`mcp`:\n\n```json\n// .vscode/mcp.json\n{\n \"servers\": {\n \"hostinger\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@hostinger/mcp@latest\"]\n },\n \"hostinger-ecommerce\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nOr add them in two commands:\n\n```bash\ncode --add-mcp '{\"name\":\"hostinger\",\"command\":\"npx\",\"args\":[\"-y\",\"@hostinger/mcp@latest\"]}'\ncode --add-mcp '{\"name\":\"hostinger-ecommerce\",\"command\":\"npx\",\"args\":[\"--package=@hostinger/mcp@latest\",\"hostinger-ecommerce-mcp\"]}'\n```\n\nVS Code picks up new servers and their tools become available right away, with no editor restart.\n(Alternatively, the official Hostinger Connector extension installs the same server:\n`code --install-extension hostinger-official.hostinger-connector`.)\n\nIf the tools still don't show up, follow *register, restart, resume* in `connect/SKILL.md`: write\n`connect.json`, then ask the user to run **Developer: Reload Window** and paste the same\nprompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"uri": "skill://hostinger-headless/report.schema.json",
|
|
@@ -253,10 +253,10 @@ export default [
|
|
|
253
253
|
},
|
|
254
254
|
{
|
|
255
255
|
"uri": "skill://hostinger-headless/wait.mjs",
|
|
256
|
-
"digest": "sha256:
|
|
257
|
-
"size":
|
|
256
|
+
"digest": "sha256:8ab82b6d0fd9042bbdbb05d066433185d99665bd0ba892407c317336a55d02cb",
|
|
257
|
+
"size": 11627,
|
|
258
258
|
"mimeType": "text/plain",
|
|
259
|
-
"text": "#!/usr/bin/env node\n// Hostinger Headless wait: polls one slow Hostinger job to its end in a single command, so the agent\n// doesn't spend a model turn on every sleep-and-check.\n//\n// node wait.mjs setup <domain> # hosting_websites_list-setups row → completed (fresh site)\n// node wait.mjs cms <domain> <username> # wordpress_installations_list → /cms row is_valid (shell swap)\n// node wait.mjs build <domain> <buildUuid> # hosting_get-deployment → build completed, then the root serves 200\n// node wait.mjs live <domain> # https://<domain>/ serves 200\n// any of them: [--timeout 10m]\n//\n// It talks to the local Hostinger MCP server, which reuses the sign-in from the connect step. Progress\n// goes to stderr; stdout is one JSON line. Exit 0 when done, 1 when the job failed, timed out or hit an\n// error retrying can't fix (the JSON says which), 2 when it couldn't run (`fallback` names the\n// operation to poll by hand instead).\n\nimport { spawn } from 'node:child_process';\n\n// ── platform-safe binary names (npm/npx are .cmd on Windows), as in connect/bootstrap.mjs ──────────\nconst isWin = process.platform === 'win32';\nconst bin = (name) => (isWin ? `${name}.cmd` : name);\nconst MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp'];\n// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell\n// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all\n// fixed literals, so enabling the shell introduces no injection risk.\nconst spawnShell = isWin;\n\nconst log = (message) => process.stderr.write(`wait: ${message}\\n`);\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nlet fallback = null;\nconst finish = (code, result) => {\n process.stdout.write(`${JSON.stringify(result)}\\n`);\n process.exit(code);\n};\nconst fail = (message) => finish(2, { ok: false, error: message, ...(fallback ? { fallback } : {}) });\n\nfunction parseArgs(argv) {\n const positional = [];\n const flags = {};\n for (let i = 0; i < argv.length; i += 1) {\n if (argv[i].startsWith('--')) {\n const key = argv[i].slice(2);\n const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[(i += 1)] : true;\n flags[key] = value;\n } else positional.push(argv[i]);\n }\n return { positional, flags };\n}\n\nfunction duration(text) {\n const m = typeof text === 'string' && text.match(/^(\\d+(?:\\.\\d+)?)\\s*([smh])$/);\n if (!m) fail(`--timeout \"${text}\" should look like 90s, 10m or 1h`);\n return Number(m[1]) * { s: 1e3, m: 6e4, h: 36e5 }[m[2]];\n}\n\n// One MCP server for the whole wait, spoken to over newline-delimited JSON-RPC on stdio. Once the\n// server dies every pending and later call rejects, so the wait ends with exit 2 rather than hanging.\nfunction connect() {\n const child = spawn(MCP[0], MCP.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: spawnShell });\n const pending = new Map();\n let buffer = '';\n let diag = '';\n let dead = null;\n let nextId = 0;\n const die = (reason) => {\n dead ??= new Error(`the Hostinger MCP server stopped (${reason}): ${diag.trim().slice(-500)}`);\n for (const { reject } of pending.values()) reject(dead);\n pending.clear();\n };\n child.on('error', (e) => die(e.message));\n // 'close' comes after stderr is drained, so the reason is in diag. A write to a dying server fails\n // with EPIPE before that; 'close' still follows, so the EPIPE itself is ignored.\n child.on('close', (code) => die(`exit code ${code}`));\n child.stdin.on('error', () => {});\n child.stderr.on('data', (chunk) => (diag = (diag + chunk).slice(-2000)));\n child.stdout.on('data', (chunk) => {\n buffer += chunk;\n let i;\n while ((i = buffer.indexOf('\\n')) >= 0) {\n const line = buffer.slice(0, i);\n buffer = buffer.slice(i + 1);\n try {\n const message = JSON.parse(line);\n pending.get(message.id)?.resolve(message);\n pending.delete(message.id);\n } catch {\n /* not a JSON-RPC line */\n }\n }\n });\n // A call can stall on a live server (a stuck upstream request), so each one has its own deadline; past\n // it, it answers like an error reply. A stalled poll is retried; a stalled start ends the wait.\n const rpc = (method, params) =>\n new Promise((resolve, reject) => {\n if (dead) return reject(dead);\n const id = (nextId += 1);\n const timer = setTimeout(() => {\n pending.delete(id);\n resolve({ error: { message: `no reply to ${method} in 60 s` } });\n }, 60_000);\n const settle = (fn) => (value) => {\n clearTimeout(timer);\n fn(value);\n };\n pending.set(id, { resolve: settle(resolve), reject: settle(reject) });\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\\n`);\n });\n const ready = rpc('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'headless-wait', version: '1' } })\n .then((reply) => {\n if (reply.error) throw new Error(`the Hostinger MCP server didn't start: ${reply.error.message}`);\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\\n`);\n });\n // The server keeps \"HTTP <status>\" in its error text. A 409 while the setup runs, a 429 or a 5xx is a\n // poll that didn't answer; any other 4xx (expired sign-in) or an unknown domain won't fix itself by waiting.\n const execute = async (operation, params) => {\n await ready;\n const reply = await rpc('tools/call', { name: 'execute', arguments: { operation, params } });\n const text = reply.result?.content?.[0]?.text ?? reply.error?.message ?? 'unknown error';\n if (reply.error || reply.result?.isError) {\n const status = Number(text.match(/HTTP (\\d{3})/)?.[1]);\n const terminal = (status >= 400 && status < 500 && ![408, 409, 425, 429].includes(status)) || /No website found/.test(text);\n return { error: text.slice(0, 300), terminal };\n }\n try {\n return { data: JSON.parse(text) };\n } catch {\n return { error: text.slice(0, 300), terminal: false };\n }\n };\n return { execute, close: () => child.kill() };\n}\n\nasync function httpStatus(url) {\n try {\n const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(30_000) });\n return response.status;\n } catch {\n return 0;\n }\n}\n\n// Polls check() until it returns a result, with pauses growing from 5 s to 30 s.\nasync function poll(label, timeoutMs, check) {\n const started = Date.now();\n const waited = () => Math.round((Date.now() - started) / 1000);\n let pause = 5_000;\n let last = null;\n for (;;) {\n const outcome = await check();\n if (outcome.done) return { ...outcome.done, waited_s: waited() };\n if (outcome.terminal) return { ok: false, error: outcome.status, waited_s: waited() };\n if (outcome.status !== last) log(`${label}: ${outcome.status}`);\n last = outcome.status;\n if (Date.now() - started + pause > timeoutMs) return { ok: false, timed_out: true, last_status: last, waited_s: waited() };\n await sleep(pause);\n pause = Math.min(pause * 1.5, 30_000);\n }\n}\n\n// List operations answer an array, or an object holding it in `data`; their domain filter is loose.\nconst rowsFor = (data, domain) =>\n (Array.isArray(data) ? data : data?.data ?? []).filter((r) => r.domain?.toLowerCase() === domain.toLowerCase());\n\nconst waitLive = (domain, timeoutMs) =>\n poll('live', timeoutMs, async () => {\n const status = await httpStatus(`https://${domain}/`);\n return status === 200 ? { done: { ok: true, live: `https://${domain}/` } } : { status: `root ${status || 'unreachable'}` };\n });\n\nconst KINDS = {\n setup: {\n args: [],\n timeout: '10m',\n fallback: 'hosting_websites_list-setups',\n run: (mcp, { domain }, timeoutMs) =>\n poll('setup', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('hosting_websites_list-setups', { domain });\n if (error) return { status: `list-setups: ${error}`, terminal };\n const [row] = rowsFor(data, domain);\n if (row?.status === 'completed') return { done: { ok: true, setup: row } };\n if (row?.status === 'failed') return { done: { ok: false, setup: row } };\n return { status: row ? row.status : 'no setup row yet' };\n }),\n },\n\n cms: {\n args: ['username'],\n timeout: '5m',\n fallback: 'wordpress_installations_list',\n // The list can lag a few seconds behind the install finishing and answer [], so an empty list is a wait.\n run: (mcp, { domain, username }, timeoutMs) =>\n poll('cms', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('wordpress_installations_list', { username, domain, ownership: 'all' });\n if (error) return { status: `installations: ${error}`, terminal };\n const rows = rowsFor(data, domain).filter((r) => r.directory === 'cms');\n const valid = rows.find((r) => r.is_valid ?? r.valid);\n if (valid) return { done: { ok: true, install: valid } };\n return { status: rows.length ? `/cms install not valid yet: ${rows[0].validationError ?? 'pending'}` : 'no /cms install yet' };\n }),\n },\n\n build: {\n args: ['buildUuid'],\n timeout: '15m',\n fallback: 'hosting_get-deployment',\n run: async (mcp, { domain, buildUuid }, timeoutMs) => {\n const started = Date.now();\n const build = await poll('build', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('hosting_get-deployment', { domain, buildUuid });\n if (error) return { status: `get-deployment: ${error}`, terminal };\n const state = data?.build?.state;\n if (state === 'failed') {\n const logTail = String(data.logs?.logs ?? '').split('\\n').slice(-40).join('\\n');\n return { done: { ok: false, build: data.build, analysis: data.analysis, log_tail: logTail } };\n }\n if (state === 'completed') return { done: { ok: true, build: data.build, buildSettings: data.buildSettings } };\n return { status: `build ${state ?? 'not found yet'}` };\n });\n if (!build.ok) return build;\n // Settings that didn't take make the root 403 for good, so don't wait for it to go live.\n const missing = ['app_type', 'output_directory', 'node_version'].filter((k) => build.buildSettings?.[k] == null);\n if (missing.length) return { ...build, ok: false, error: `build settings missing: ${missing.join(', ')}; pass them on hosting_deploy and redeploy` };\n const live = await waitLive(domain, timeoutMs - (Date.now() - started));\n return { ...build, ...live, waited_s: Math.round((Date.now() - started) / 1000) };\n },\n },\n\n live: {\n args: [],\n timeout: '5m',\n run: (_mcp, { domain }, timeoutMs) => waitLive(domain, timeoutMs),\n },\n};\n\nconst { positional, flags } = parseArgs(process.argv.slice(2));\nconst [kind, domain, ...rest] = positional;\nconst spec = KINDS[kind];\nconst usage = Object.entries(KINDS).map(([k, s]) => `wait.mjs ${k} <domain>${s.args.map((a) => ` <${a}>`).join('')}`).join(' | ');\nif (!spec || !domain || rest.length < spec.args.length) fail(`usage: ${usage} [--timeout 10m]`);\nconst params = { domain, ...Object.fromEntries(spec.args.map((a, i) => [a, rest[i]])) };\nconst timeoutMs = duration(flags.timeout ?? spec.timeout);\nfallback = spec.fallback ?? null;\nconst mcp = spec.fallback ? connect() : null;\nconst result = await spec.run(mcp, params, timeoutMs).catch((e) => fail(e.message));\nmcp?.close();\nfinish(result.ok ? 0 : 1, { wait: kind, domain, ...result });\n"
|
|
259
|
+
"text": "#!/usr/bin/env node\n// Hostinger Headless wait: polls one slow Hostinger job to its end in a single command, so the agent\n// doesn't spend a model turn on every sleep-and-check.\n//\n// node wait.mjs setup <domain> # hosting_websites_list-setups row → completed (fresh site)\n// node wait.mjs cms <domain> <username> # wordpress_installations_list → /cms row is_valid (shell swap)\n// node wait.mjs build <domain> <buildUuid> # hosting_get-deployment → build completed, then the root serves 200\n// node wait.mjs live <domain> # https://<domain>/ serves 200\n// any of them: [--timeout 10m]\n//\n// It talks to the local Hostinger MCP server, which reuses the sign-in from the connect step. Progress\n// goes to stderr; stdout is one JSON line. Exit 0 when done, 1 when the job failed, timed out or hit an\n// error retrying can't fix (the JSON says which), 2 when it couldn't run (`fallback` names the\n// operation to poll by hand instead).\n\nimport { spawn } from 'node:child_process';\nimport { homedir } from 'node:os';\n\n// ── platform-safe binary names (npm/npx are .cmd on Windows), as in connect/bootstrap.mjs ──────────\nconst isWin = process.platform === 'win32';\nconst bin = (name) => (isWin ? `${name}.cmd` : name);\nconst MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp'];\n// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell\n// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all\n// fixed literals, so enabling the shell introduces no injection risk.\nconst spawnShell = isWin;\n\nconst log = (message) => process.stderr.write(`wait: ${message}\\n`);\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nlet fallback = null;\nconst finish = (code, result) => {\n process.stdout.write(`${JSON.stringify(result)}\\n`);\n process.exit(code);\n};\nconst fail = (message) => finish(2, { ok: false, error: message, ...(fallback ? { fallback } : {}) });\n\nfunction parseArgs(argv) {\n const positional = [];\n const flags = {};\n for (let i = 0; i < argv.length; i += 1) {\n if (argv[i].startsWith('--')) {\n const key = argv[i].slice(2);\n const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[(i += 1)] : true;\n flags[key] = value;\n } else positional.push(argv[i]);\n }\n return { positional, flags };\n}\n\nfunction duration(text) {\n const m = typeof text === 'string' && text.match(/^(\\d+(?:\\.\\d+)?)\\s*([smh])$/);\n if (!m) fail(`--timeout \"${text}\" should look like 90s, 10m or 1h`);\n return Number(m[1]) * { s: 1e3, m: 6e4, h: 36e5 }[m[2]];\n}\n\n// One MCP server for the whole wait, spoken to over newline-delimited JSON-RPC on stdio. Once the\n// server dies every pending and later call rejects, so the wait ends with exit 2 rather than hanging.\nfunction connect() {\n const child = spawn(MCP[0], MCP.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: spawnShell, cwd: homedir() });\n const pending = new Map();\n let buffer = '';\n let diag = '';\n let dead = null;\n let nextId = 0;\n const die = (reason) => {\n dead ??= new Error(`the Hostinger MCP server stopped (${reason}): ${diag.trim().slice(-500)}`);\n for (const { reject } of pending.values()) reject(dead);\n pending.clear();\n };\n child.on('error', (e) => die(e.message));\n // 'close' comes after stderr is drained, so the reason is in diag. A write to a dying server fails\n // with EPIPE before that; 'close' still follows, so the EPIPE itself is ignored.\n child.on('close', (code) => die(`exit code ${code}`));\n child.stdin.on('error', () => {});\n child.stderr.on('data', (chunk) => (diag = (diag + chunk).slice(-2000)));\n child.stdout.on('data', (chunk) => {\n buffer += chunk;\n let i;\n while ((i = buffer.indexOf('\\n')) >= 0) {\n const line = buffer.slice(0, i);\n buffer = buffer.slice(i + 1);\n try {\n const message = JSON.parse(line);\n pending.get(message.id)?.resolve(message);\n pending.delete(message.id);\n } catch {\n /* not a JSON-RPC line */\n }\n }\n });\n // A call can stall on a live server (a stuck upstream request), so each one has its own deadline; past\n // it, it answers like an error reply. A stalled poll is retried; a stalled start ends the wait.\n const rpc = (method, params) =>\n new Promise((resolve, reject) => {\n if (dead) return reject(dead);\n const id = (nextId += 1);\n const timer = setTimeout(() => {\n pending.delete(id);\n resolve({ error: { message: `no reply to ${method} in 60 s` } });\n }, 60_000);\n const settle = (fn) => (value) => {\n clearTimeout(timer);\n fn(value);\n };\n pending.set(id, { resolve: settle(resolve), reject: settle(reject) });\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\\n`);\n });\n const ready = rpc('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'headless-wait', version: '1' } })\n .then((reply) => {\n if (reply.error) throw new Error(`the Hostinger MCP server didn't start: ${reply.error.message}`);\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\\n`);\n });\n // The server keeps \"HTTP <status>\" in its error text. A 409 while the setup runs, a 429 or a 5xx is a\n // poll that didn't answer; any other 4xx (expired sign-in) or an unknown domain won't fix itself by waiting.\n const execute = async (operation, params) => {\n await ready;\n const reply = await rpc('tools/call', { name: 'execute', arguments: { operation, params } });\n const text = reply.result?.content?.[0]?.text ?? reply.error?.message ?? 'unknown error';\n if (reply.error || reply.result?.isError) {\n const status = Number(text.match(/HTTP (\\d{3})/)?.[1]);\n const terminal = (status >= 400 && status < 500 && ![408, 409, 425, 429].includes(status)) || /No website found/.test(text);\n return { error: text.slice(0, 300), terminal };\n }\n try {\n return { data: JSON.parse(text) };\n } catch {\n return { error: text.slice(0, 300), terminal: false };\n }\n };\n return { execute, close: () => child.kill() };\n}\n\nasync function httpStatus(url) {\n try {\n const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(30_000) });\n return response.status;\n } catch {\n return 0;\n }\n}\n\n// Polls check() until it returns a result, with pauses growing from 5 s to 30 s.\nasync function poll(label, timeoutMs, check) {\n const started = Date.now();\n const waited = () => Math.round((Date.now() - started) / 1000);\n let pause = 5_000;\n let last = null;\n for (;;) {\n const outcome = await check();\n if (outcome.done) return { ...outcome.done, waited_s: waited() };\n if (outcome.terminal) return { ok: false, error: outcome.status, waited_s: waited() };\n if (outcome.status !== last) log(`${label}: ${outcome.status}`);\n last = outcome.status;\n if (Date.now() - started + pause > timeoutMs) return { ok: false, timed_out: true, last_status: last, waited_s: waited() };\n await sleep(pause);\n pause = Math.min(pause * 1.5, 30_000);\n }\n}\n\n// List operations answer an array, or an object holding it in `data`; their domain filter is loose.\nconst rowsFor = (data, domain) =>\n (Array.isArray(data) ? data : data?.data ?? []).filter((r) => r.domain?.toLowerCase() === domain.toLowerCase());\n\nconst waitLive = (domain, timeoutMs) =>\n poll('live', timeoutMs, async () => {\n const status = await httpStatus(`https://${domain}/`);\n return status === 200 ? { done: { ok: true, live: `https://${domain}/` } } : { status: `root ${status || 'unreachable'}` };\n });\n\nconst KINDS = {\n setup: {\n args: [],\n timeout: '10m',\n fallback: 'hosting_websites_list-setups',\n run: (mcp, { domain }, timeoutMs) =>\n poll('setup', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('hosting_websites_list-setups', { domain });\n if (error) return { status: `list-setups: ${error}`, terminal };\n const [row] = rowsFor(data, domain);\n if (row?.status === 'completed') return { done: { ok: true, setup: row } };\n if (row?.status === 'failed') return { done: { ok: false, setup: row } };\n return { status: row ? row.status : 'no setup row yet' };\n }),\n },\n\n cms: {\n args: ['username'],\n timeout: '5m',\n fallback: 'wordpress_installations_list',\n // The list can lag a few seconds behind the install finishing and answer [], so an empty list is a wait.\n run: (mcp, { domain, username }, timeoutMs) =>\n poll('cms', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('wordpress_installations_list', { username, domain, ownership: 'all' });\n if (error) return { status: `installations: ${error}`, terminal };\n const rows = rowsFor(data, domain).filter((r) => r.directory === 'cms');\n const valid = rows.find((r) => r.is_valid ?? r.valid);\n if (valid) return { done: { ok: true, install: valid } };\n return { status: rows.length ? `/cms install not valid yet: ${rows[0].validationError ?? 'pending'}` : 'no /cms install yet' };\n }),\n },\n\n build: {\n args: ['buildUuid'],\n timeout: '15m',\n fallback: 'hosting_get-deployment',\n run: async (mcp, { domain, buildUuid }, timeoutMs) => {\n const started = Date.now();\n const build = await poll('build', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('hosting_get-deployment', { domain, buildUuid });\n if (error) return { status: `get-deployment: ${error}`, terminal };\n const state = data?.build?.state;\n if (state === 'failed') {\n const logTail = String(data.logs?.logs ?? '').split('\\n').slice(-40).join('\\n');\n return { done: { ok: false, build: data.build, analysis: data.analysis, log_tail: logTail } };\n }\n if (state === 'completed') return { done: { ok: true, build: data.build, buildSettings: data.buildSettings } };\n return { status: `build ${state ?? 'not found yet'}` };\n });\n if (!build.ok) return build;\n // Settings that didn't take make the root 403 for good, so don't wait for it to go live.\n const missing = ['app_type', 'output_directory', 'node_version'].filter((k) => build.buildSettings?.[k] == null);\n if (missing.length) return { ...build, ok: false, error: `build settings missing: ${missing.join(', ')}; pass them on hosting_deploy and redeploy` };\n const live = await waitLive(domain, timeoutMs - (Date.now() - started));\n return { ...build, ...live, waited_s: Math.round((Date.now() - started) / 1000) };\n },\n },\n\n live: {\n args: [],\n timeout: '5m',\n run: (_mcp, { domain }, timeoutMs) => waitLive(domain, timeoutMs),\n },\n};\n\nconst { positional, flags } = parseArgs(process.argv.slice(2));\nconst [kind, domain, ...rest] = positional;\nconst spec = KINDS[kind];\nconst usage = Object.entries(KINDS).map(([k, s]) => `wait.mjs ${k} <domain>${s.args.map((a) => ` <${a}>`).join('')}`).join(' | ');\nif (!spec || !domain || rest.length < spec.args.length) fail(`usage: ${usage} [--timeout 10m]`);\nconst params = { domain, ...Object.fromEntries(spec.args.map((a, i) => [a, rest[i]])) };\nconst timeoutMs = duration(flags.timeout ?? spec.timeout);\nfallback = spec.fallback ?? null;\nconst mcp = spec.fallback ? connect() : null;\nconst result = await spec.run(mcp, params, timeoutMs).catch((e) => fail(e.message));\nmcp?.close();\nfinish(result.ok ? 0 : 1, { wait: kind, domain, ...result });\n"
|
|
260
260
|
}
|
|
261
261
|
]
|
|
262
262
|
},
|
package/src/core/skills.ts
CHANGED
|
@@ -78,59 +78,59 @@ const skills: Skill[] = [
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"uri": "skill://hostinger-headless/connect/SKILL.md",
|
|
81
|
-
"digest": "sha256:
|
|
82
|
-
"size":
|
|
81
|
+
"digest": "sha256:acebdb8ac78c3a86925d5765b7c77b1d5b99efca2e901ce161bf53656455c700",
|
|
82
|
+
"size": 13236,
|
|
83
83
|
"mimeType": "text/markdown",
|
|
84
|
-
"text": "---\nname: hostinger-headless-connect\ndescription: \"Connect step for the Hostinger Headless flow — verify Node, run the bootstrap (reach the Hostinger MCP server + OAuth sign-in), then route to the matching coding-tool skill under tools/ so MCP tools and skills are available before the build steps run. Triggers: connect to Hostinger, set up the Hostinger connector, start a headless build.\"\n---\n\n# Hostinger Headless Skill — connect\n\nYou are a coding agent, and this is a skill for you to execute. Follow the phases below in order.\n\nThis skill goal is to help you to connect to Hostinger via MCP. That can be done via Hostinger Connector or adding the MCP via MCP Server.\n\nWe support different coding Agentic coding tools such as Claude Code, Codex, Cursor, VS Code...and so on. \n\nYour goal is to route those tools to their specific skill inside the tools folder ensuring the tool is fully connected.\n\n\n## Phase 0 — Node (the single one prerequisite)\n\nThe Hostinger MCP server requires Node ≥ 20. Check it:\n\n```bash\nnode -v\n```\n\nIf that errors (Node not installed) or prints a version below 20, install or upgrade Node and re-check — do not try to work around it:\n\n- macOS: `brew install node` (or `nvm install 22 && nvm use 22`)\n- Linux: `nvm install 22 && nvm use 22` (or your distro's Node 20+ package)\n- Windows: `winget install OpenJS.NodeJS.LTS` (or download from nodejs.org)\n\n## Phase 1 — Get the skill folder, then run its bootstrap (deterministic)\n\n### Find or install the skill folder\n\nThe bootstrap script and every step after connect ship in the `headless/` skill folder. Use the copy\nyou already have; never fetch single files from GitHub.\n\n- **An installer put the skill here** (you read this file from disk, from a folder that `npx skills\n add` or a Claude Code or Cursor plugin installed): that folder is `<skill>`. Use it as it is and\n download nothing. Updates come from the installer (`npx skills update`, the plugin update),\n not from this run.\n- **You read this file from the web, or from a `headless/` folder an earlier web run installed:**\n install the skill from the published `@hostinger/mcp` npm release (for an earlier web install, only\n when it is out of date; see below). It is the same package the MCP server runs, and npm checks\n the tarball's integrity. Resolve the version first so the run uses one pinned release:\n\n```bash\n# macOS/Linux\nV=$(npm view @hostinger/mcp version) && npm pack \"@hostinger/mcp@$V\" --silent \\\n && tar -xzf \"hostinger-mcp-$V.tgz\" --strip-components=2 package/skills/headless && rm \"hostinger-mcp-$V.tgz\"\n```\n\n```powershell\n# Windows PowerShell\n$V = npm view @hostinger/mcp version; npm pack \"@hostinger/mcp@$V\" --silent\ntar -xzf \"hostinger-mcp-$V.tgz\" --strip-components=2 package/skills/headless; Remove-Item \"hostinger-mcp-$V.tgz\"\n```\n\nThis leaves a local `headless/` folder, which is `<skill>`. A `headless/` folder already here from an\nearlier web install counts as installed, with one check: when line 1 of its `headless/VERSION` is older\nthan `npm view @hostinger/mcp version`, install again the same way, so a run never uses a deleted,\nlegacy flow.\n\nRead `<skill>/VERSION`: its line 1 is the skills version this run uses, which goes into\n`connect.json` (Phase 3).\n\n### Run the bootstrap\n\nRun the bundled script. It verifies the MCP server and handles sign-in, emitting one JSON event per line on stdout. Run it as a background/streaming process. Show the user only the sign-in URL (`awaiting_user`) or a\nfailure's `detail`, and handle the other events silently (table below).\n\nThe script is safe and inspectable: it only checks the MCP server via `npx` and drives the OAuth sign-in (a browser flow with a localhost callback) — no other network calls, no filesystem writes.\n\n```bash\nnode <skill>/connect/bootstrap.mjs\n```\n\n### Relay these events\n\nThe script emits one JSON object per line:\n\n| Event | What to do |\n| --- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `mcp_ok` | MCP server reachable — continue. |\n| `logged_in` | Already authenticated (env token or a previous sign-in) — continue. |\n| `awaiting_user` (`verificationUri`) | A browser window should have opened on the user's machine. Show the URL in plain prose and wait for the user to finish signing in. The OAuth callback lands on a localhost port, so the sign-in must be completed on this same machine. |\n| `success` | Bootstrap done — continue to Phase 2. |\n| `mcp_unreachable` / `login_failed` (with `detail`) | Stop and show the user the `detail`. Do not improvise a parallel setup by hand. |\n\nIf the environment has a `HOSTINGER_API_TOKEN` set (for example CI), the bootstrap reports `logged_in` immediately — no browser needed.\n\n### Sign in or out manually\n\nThe bootstrap runs the sign-in for you. To run it directly, or to switch accounts, use the MCP's own commands. `--login` opens the browser to the Hostinger sign-in page, waits for the localhost callback, stores the credentials and exits. `--logout` revokes the stored credentials, so the next sign-in opens the browser again.\n\n```bash\n# With a global install (npm install -g @hostinger/mcp):\nhostinger-api-mcp --login\nhostinger-api-mcp --logout\n\n# Without a global install:\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout\n```\n\nCredentials live in `~/.config/hostinger-mcp/credentials.json` (macOS/Linux) or `%APPDATA%\\hostinger-mcp\\credentials.json` (Windows) and are shared by every Hostinger MCP binary. OAuth works only when the tool runs the server over its default local (stdio) transport, which is what every registration below uses; `--http` needs `HOSTINGER_API_TOKEN`.\n\n## Phase 2 - Route to the specific tool instructions\n\n**Use the Hostinger connector only through your tool's native MCP tools** (`mcp__hostinger__*`,\n`mcp__hostinger-*__*`, or however your tool names them). Never spawn the MCP server yourself and\nnever write a JSON-RPC client to talk to it: every connector call must be a visible MCP tool call\nthe user can follow and approve, not a script.\n\n### Check both tool groups, not just \"some Hostinger tools\"\n\nThe build needs **two** groups of connector operations, and they come from different servers. Having\none group says nothing about the other:\n\n| Group | Operations | Comes from | Needed by |\n|---|---|---|---|\n| **Hosting** | `hosting_*` (websites, deploy, build report), `wordpress_*` | `hostinger` (`npx -y @hostinger/mcp@latest`), or the scoped servers such as `hostinger-hosting` + `hostinger-wordpress` | every build |\n| **Ecommerce** | `ecommerce_*` (stores, products, shipping, payments) | **only** `hostinger-ecommerce` (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`), or the Ecommerce MCP in the Hostinger Connector. Neither `hostinger` nor the scoped hosting servers include it | Ecommerce backends (chosen later, in refine) |\n\nCheck each group on its own, right here at connect, before anything else:\n\n- **Hosting:** a Hostinger search tool finds `hosting_websites_list` (e.g. search \"list websites\").\n- **Ecommerce:** a tool from the `hostinger-ecommerce` server is in the session (Claude Code:\n `mcp__hostinger-ecommerce__*`), and its search finds `ecommerce_stores_list` (e.g. search \"list stores\").\n\nOnly when **both** pass are the Hostinger tools \"in this session\". If either is missing, the tools\nare **not** in this session: follow *register, restart, resume* below for the missing server(s) now,\neven though other Hostinger tools respond. Don't wait for refine to choose a backend: the restart\ncosts the least before the user has been asked anything, and finding the gap at provision forces a\nsecond restart in the middle of the build.\n\n### When the Hostinger tools aren't in this session: register, restart, resume\n\nMost coding tools load MCP servers only when they start, so a server registered mid-run isn't usable\nuntil the tool restarts. When either tool group above is missing:\n\n1. **Register the missing servers** in the tool's MCP config, as the tool route below shows: `hostinger`\n (`npx -y @hostinger/mcp@latest`) if the hosting group is missing, and `hostinger-ecommerce`\n (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`) if the ecommerce group is missing.\n Skip a server that is already registered under its name. When neither group is there, register\n both: the backend isn't chosen until refine, and one restart then covers every backend.\n2. **Write `connect.json`** (Phase 3) before you stop. The skill folder from Phase 1 stays in place,\n so the next run starts from it.\n3. **Send the first message and end your turn.** It follows *The first message* in\n `headless/SKILL.md`: the sign-in link if one was needed, then one line saying Hostinger is set up\n and asking the user to restart the tool (the route says how) and paste the same prompt again so the\n build continues from there. This is a pause, not a run ending: no final report and no build report.\n4. **On the next run** the tools are loaded, so connect skips registration and check-state resumes the\n domain's workspace.\n\nIf a tool loads new servers without a restart (VS Code), use them right away and skip steps 3–4.\n\n| Tool | Route |\n|-------------|---------------------------------|\n| Claude Code | `connect/tools/claude-code.md` |\n| Codex | `connect/tools/codex.md` |\n| Cursor | `connect/tools/cursor.md` |\n| VS Code | `connect/tools/vscode.md` |\n| Antigravity | `connect/tools/antigravity.md` |\n| Devin | `connect/tools/devin.md` |\n\nIf the user's tool isn't listed yet, connect it the generic way: register both Hostinger MCP servers\nin that tool's MCP config (command `npx`, args as above), then follow *register, restart, resume*.\n\n### The skills come from Phase 1 only\n\nThe tool routes don't download the skills: Phase 1 already found or installed `<skill>`. The bundle\nships a `headless/VERSION` stamp (`version=<semver>` on line 1, then `commit` and `built`); carry the\nversion into `connect.json` (Phase 3) so the build report records which skills ran.\n\n## Phase 3 - Write `connect.json`\n\nOnce the tool is connected, write `~/.hostinger/<domain>/connect.json` (create the folder if it is\nmissing) in exactly this shape:\n\n```json\n{ \"tool\": \"claude-code\", \"mcp\": \"hostinger-api-mcp 3.8.0\", \"skills\": \"3.9.0\", \"auth\": \"oauth_cached\",\n \"order_id\": null, \"plan\": null, \"connected_at\": \"2026-10-28T10:46:12Z\" }\n```\n\n- `tool`: the `<tool>` from the Phase 2 route (`claude-code`, `codex`, `cursor`, `vscode`,\n `antigravity`, `devin`, or a short lowercase name for an unlisted tool), the same value later\n passed to `run-start --tool`.\n- `mcp`: one string, `hostinger-api-mcp <version>`, with the `version` from the `mcp_ok` event.\n Never an object.\n- `skills`: the `version=` value from the `headless/VERSION` stamp you just pulled — the skills bundle\n version this run used. `\"unknown\"` if the stamp is absent (an old bundle predating the stamp).\n- `auth`: the `method` from the `logged_in` event (`api_token`, `oauth_cached` or `oauth`).\n- `order_id`, `plan`: the active hosting plan's order id and name when you looked them up, else `null`.\n- `connected_at`: the current time in UTC, ISO 8601 to the second.\n\nExtra keys are allowed, but keep these seven as above: the build report reads `tool`, `mcp` and `skills`.\n\n## Phase 4 - Continue to Headless Hosting steps\n\nOnce everything is verified and you have the Hostinger MCP Server and the skills, tell the user in\none line that you're connected to Hostinger, as *The first message* in `headless/SKILL.md` describes.\nThen go back to `headless/SKILL.md` and continue with check-state without waiting for a reply.\n\n\n## Reference Sources\n\n- https://mcpservers.org/servers/hostinger/api-mcp-server\n- https://www.hostinger.com/connector\n"
|
|
84
|
+
"text": "---\nname: hostinger-headless-connect\ndescription: \"Connect step for the Hostinger Headless flow — verify Node, run the bootstrap (reach the Hostinger MCP server + OAuth sign-in), then route to the matching coding-tool skill under tools/ so MCP tools and skills are available before the build steps run. Triggers: connect to Hostinger, set up the Hostinger connector, start a headless build.\"\n---\n\n# Hostinger Headless Skill — connect\n\nYou are a coding agent, and this is a skill for you to execute. Follow the phases below in order.\n\nThis skill goal is to help you to connect to Hostinger via MCP. That can be done via Hostinger Connector or adding the MCP via MCP Server.\n\nWe support different coding Agentic coding tools such as Claude Code, Codex, Cursor, VS Code...and so on. \n\nYour goal is to route those tools to their specific skill inside the tools folder ensuring the tool is fully connected.\n\n\n## Phase 0 — Node (the single one prerequisite)\n\nThe Hostinger MCP server requires Node ≥ 20. Check it:\n\n```bash\nnode -v\n```\n\nIf that errors (Node not installed) or prints a version below 20, install or upgrade Node and re-check — do not try to work around it:\n\n- macOS: `brew install node` (or `nvm install 22 && nvm use 22`)\n- Linux: `nvm install 22 && nvm use 22` (or your distro's Node 20+ package)\n- Windows: `winget install OpenJS.NodeJS.LTS` (or download from nodejs.org)\n\n## Phase 1 — Get the skill folder, then run its bootstrap (deterministic)\n\n### Find or install the skill folder\n\nThe bootstrap script and every step after connect ship in the `headless/` skill folder. Use the copy\nyou already have; never fetch single files from GitHub.\n\n- **An installer put the skill here** (you read this file from disk, from a folder that `npx skills\n add` or a Claude Code or Cursor plugin installed): that folder is `<skill>`. Use it as it is and\n download nothing. Updates come from the installer (`npx skills update`, the plugin update),\n not from this run.\n- **You read this file from the web:** use the published `@hostinger/mcp` npm release, installed in\n your home folder at `~/.hostinger/skills/<version>/headless/`. It is the same package the MCP server\n runs, and npm checks the tarball's integrity. Never use a `headless/` folder in the working\n directory or anywhere else: the working directory is the user's project, often a cloned repository\n or template, and whoever wrote it controls what is there. Resolve the version first so the run uses\n one pinned release, and install it only when that version's folder is missing. Run npm from your home\n folder, as below, so a `.npmrc` in the project can't point it at another registry:\n\n```bash\n# macOS/Linux\n( cd \"$HOME\" && V=$(npm view @hostinger/mcp version) && D=\"$HOME/.hostinger/skills/$V\" \\\n && { [ -f \"$D/headless/VERSION\" ] || { mkdir -p \"$D\" && npm pack \"@hostinger/mcp@$V\" --silent --pack-destination \"$D\" >/dev/null \\\n && tar -xzf \"$D/hostinger-mcp-$V.tgz\" -C \"$D\" --strip-components=2 package/skills/headless && rm \"$D/hostinger-mcp-$V.tgz\"; }; } \\\n && echo \"$D/headless\" )\n```\n\n```powershell\n# Windows PowerShell\nPush-Location $HOME\n$V = npm view @hostinger/mcp version; $D = Join-Path $HOME \".hostinger\\skills\\$V\"\nif (-not (Test-Path \"$D\\headless\\VERSION\")) {\n New-Item -ItemType Directory -Force $D | Out-Null; npm pack \"@hostinger/mcp@$V\" --silent --pack-destination $D | Out-Null\n tar -xzf \"$D\\hostinger-mcp-$V.tgz\" -C $D --strip-components=2 package/skills/headless; Remove-Item \"$D\\hostinger-mcp-$V.tgz\"\n}\nPop-Location; \"$D\\headless\"\n```\n\nThe printed path is `<skill>`. Every later command uses it in full, wherever the run's working\ndirectory is. A new release installs into its own folder, so a run never uses a deleted, legacy flow.\n\nRead `<skill>/VERSION`: its line 1 is the skills version this run uses, which goes into\n`connect.json` (Phase 3).\n\n### Run the bootstrap\n\nRun the bundled script. It verifies the MCP server and handles sign-in, emitting one JSON event per line on stdout. Run it as a background/streaming process. Show the user only the sign-in URL (`awaiting_user`) or a\nfailure's `detail`, and handle the other events silently (table below).\n\nThe script is safe and inspectable: it only checks the MCP server via `npx` and drives the OAuth sign-in (a browser flow with a localhost callback) — no other network calls, no filesystem writes.\n\n```bash\nnode <skill>/connect/bootstrap.mjs\n```\n\n### Relay these events\n\nThe script emits one JSON object per line:\n\n| Event | What to do |\n| --- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `mcp_ok` | MCP server reachable — continue. |\n| `logged_in` | Already authenticated (env token or a previous sign-in) — continue. |\n| `awaiting_user` (`verificationUri`) | A browser window should have opened on the user's machine. Show the URL in plain prose and wait for the user to finish signing in. The OAuth callback lands on a localhost port, so the sign-in must be completed on this same machine. |\n| `success` | Bootstrap done — continue to Phase 2. |\n| `mcp_unreachable` / `login_failed` (with `detail`) | Stop and show the user the `detail`. Do not improvise a parallel setup by hand. |\n\nIf the environment has a `HOSTINGER_API_TOKEN` set (for example CI), the bootstrap reports `logged_in` immediately — no browser needed.\n\n### Sign in or out manually\n\nThe bootstrap runs the sign-in for you. To run it directly, or to switch accounts, use the MCP's own commands. `--login` opens the browser to the Hostinger sign-in page, waits for the localhost callback, stores the credentials and exits. `--logout` revokes the stored credentials, so the next sign-in opens the browser again.\n\n```bash\n# With a global install (npm install -g @hostinger/mcp):\nhostinger-api-mcp --login\nhostinger-api-mcp --logout\n\n# Without a global install:\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout\n```\n\nCredentials live in `~/.config/hostinger-mcp/credentials.json` (macOS/Linux) or `%APPDATA%\\hostinger-mcp\\credentials.json` (Windows) and are shared by every Hostinger MCP binary. OAuth works only when the tool runs the server over its default local (stdio) transport, which is what every registration below uses; `--http` needs `HOSTINGER_API_TOKEN`.\n\n## Phase 2 - Route to the specific tool instructions\n\n**Use the Hostinger connector only through your tool's native MCP tools** (`mcp__hostinger__*`,\n`mcp__hostinger-*__*`, or however your tool names them). Never spawn the MCP server yourself and\nnever write a JSON-RPC client to talk to it: every connector call must be a visible MCP tool call\nthe user can follow and approve, not a script.\n\n### Check both tool groups, not just \"some Hostinger tools\"\n\nThe build needs **two** groups of connector operations, and they come from different servers. Having\none group says nothing about the other:\n\n| Group | Operations | Comes from | Needed by |\n|---|---|---|---|\n| **Hosting** | `hosting_*` (websites, deploy, build report), `wordpress_*` | `hostinger` (`npx -y @hostinger/mcp@latest`), or the scoped servers such as `hostinger-hosting` + `hostinger-wordpress` | every build |\n| **Ecommerce** | `ecommerce_*` (stores, products, shipping, payments) | **only** `hostinger-ecommerce` (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`), or the Ecommerce MCP in the Hostinger Connector. Neither `hostinger` nor the scoped hosting servers include it | Ecommerce backends (chosen later, in refine) |\n\nCheck each group on its own, right here at connect, before anything else:\n\n- **Hosting:** a Hostinger search tool finds `hosting_websites_list` (e.g. search \"list websites\").\n- **Ecommerce:** a tool from the `hostinger-ecommerce` server is in the session (Claude Code:\n `mcp__hostinger-ecommerce__*`), and its search finds `ecommerce_stores_list` (e.g. search \"list stores\").\n\nOnly when **both** pass are the Hostinger tools \"in this session\". If either is missing, the tools\nare **not** in this session: follow *register, restart, resume* below for the missing server(s) now,\neven though other Hostinger tools respond. Don't wait for refine to choose a backend: the restart\ncosts the least before the user has been asked anything, and finding the gap at provision forces a\nsecond restart in the middle of the build.\n\n### When the Hostinger tools aren't in this session: register, restart, resume\n\nMost coding tools load MCP servers only when they start, so a server registered mid-run isn't usable\nuntil the tool restarts. When either tool group above is missing:\n\n1. **Register the missing servers** in the tool's MCP config, as the tool route below shows: `hostinger`\n (`npx -y @hostinger/mcp@latest`) if the hosting group is missing, and `hostinger-ecommerce`\n (`npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp`) if the ecommerce group is missing.\n Skip a server that is already registered under its name. When neither group is there, register\n both: the backend isn't chosen until refine, and one restart then covers every backend.\n2. **Write `connect.json`** (Phase 3) before you stop. The skill folder from Phase 1 stays in place,\n so the next run starts from it.\n3. **Send the first message and end your turn.** It follows *The first message* in\n `<skill>/SKILL.md`: the sign-in link if one was needed, then one line saying Hostinger is set up\n and asking the user to restart the tool (the route says how) and paste the same prompt again so the\n build continues from there. This is a pause, not a run ending: no final report and no build report.\n4. **On the next run** the tools are loaded, so connect skips registration and check-state resumes the\n domain's workspace.\n\nIf a tool loads new servers without a restart (VS Code), use them right away and skip steps 3–4.\n\n| Tool | Route |\n|-------------|---------------------------------|\n| Claude Code | `connect/tools/claude-code.md` |\n| Codex | `connect/tools/codex.md` |\n| Cursor | `connect/tools/cursor.md` |\n| VS Code | `connect/tools/vscode.md` |\n| Antigravity | `connect/tools/antigravity.md` |\n| Devin | `connect/tools/devin.md` |\n\nIf the user's tool isn't listed yet, connect it the generic way: register both Hostinger MCP servers\nin that tool's MCP config (command `npx`, args as above), then follow *register, restart, resume*.\n\n### The skills come from Phase 1 only\n\nThe tool routes don't download the skills: Phase 1 already found or installed `<skill>`. The bundle\nships a `<skill>/VERSION` stamp (`version=<semver>` on line 1, then `commit` and `built`); carry the\nversion into `connect.json` (Phase 3) so the build report records which skills ran.\n\n## Phase 3 - Write `connect.json`\n\nOnce the tool is connected, write `~/.hostinger/<domain>/connect.json` (create the folder if it is\nmissing) in exactly this shape:\n\n```json\n{ \"tool\": \"claude-code\", \"mcp\": \"hostinger-api-mcp 3.8.0\", \"skills\": \"3.9.0\", \"auth\": \"oauth_cached\",\n \"order_id\": null, \"plan\": null, \"connected_at\": \"2026-10-28T10:46:12Z\" }\n```\n\n- `tool`: the `<tool>` from the Phase 2 route (`claude-code`, `codex`, `cursor`, `vscode`,\n `antigravity`, `devin`, or a short lowercase name for an unlisted tool), the same value later\n passed to `run-start --tool`.\n- `mcp`: one string, `hostinger-api-mcp <version>`, with the `version` from the `mcp_ok` event.\n Never an object.\n- `skills`: the `version=` value from the `<skill>/VERSION` stamp you just pulled — the skills bundle\n version this run used. `\"unknown\"` if the stamp is absent (an old bundle predating the stamp).\n- `auth`: the `method` from the `logged_in` event (`api_token`, `oauth_cached` or `oauth`).\n- `order_id`, `plan`: the active hosting plan's order id and name when you looked them up, else `null`.\n- `connected_at`: the current time in UTC, ISO 8601 to the second.\n\nExtra keys are allowed, but keep these seven as above: the build report reads `tool`, `mcp` and `skills`.\n\n## Phase 4 - Continue to Headless Hosting steps\n\nOnce everything is verified and you have the Hostinger MCP Server and the skills, tell the user in\none line that you're connected to Hostinger, as *The first message* in `<skill>/SKILL.md` describes.\nThen go back to `<skill>/SKILL.md` and continue with check-state without waiting for a reply.\n\n\n## Reference Sources\n\n- https://mcpservers.org/servers/hostinger/api-mcp-server\n- https://www.hostinger.com/connector\n"
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
87
|
"uri": "skill://hostinger-headless/connect/bootstrap.mjs",
|
|
88
|
-
"digest": "sha256:
|
|
89
|
-
"size":
|
|
88
|
+
"digest": "sha256:0428f0e2c7cf270be13fe3d09ba0f638854c681bc148e8eafb5a31cd70667eed",
|
|
89
|
+
"size": 5415,
|
|
90
90
|
"mimeType": "text/plain",
|
|
91
|
-
"text": "#!/usr/bin/env node\n// Hostinger Headless — bootstrap.\n// Verifies the Hostinger MCP server is reachable and drives the OAuth sign-in,\n// emitting one JSON event per line on stdout so a coding agent can relay\n// progress to the user. No filesystem writes besides what the MCP itself\n// stores (OAuth credentials); no network calls of its own.\nimport { spawn, spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\n\n// ── tiny event protocol (one JSON object per line) ───────────────────────────\nconst emit = (event, extra = {}) =>\n process.stdout.write(JSON.stringify({ event, ...extra }) + '\\n');\nconst fail = (event, extra = {}) => {\n emit(event, { ok: false, ...extra });\n process.exit(1);\n};\n\n// ── platform-safe binary names (npm/npx are .cmd on Windows) ─────────────────\nconst isWin = process.platform === 'win32';\nconst bin = (name) => (isWin ? `${name}.cmd` : name);\n// Run the MCP via npx — no global install, always the latest published version.\n// Use --package so npx resolves the published package, then invoke the unified binary.\nconst MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp'];\n// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell\n// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all\n// fixed literals, so enabling the shell introduces no injection risk.\nconst spawnShell = isWin;\n\nfunction capture(cmd, args) {\n const r = spawnSync(cmd, args, { encoding: 'utf8', shell: spawnShell });\n return { status: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}`, error: r.error };\n}\n\n// ── 1. MCP server reachable ──────────────────────────────────────────────────\nfunction checkMcp() {\n const r = capture(MCP[0], [...MCP.slice(1), '--version']);\n if (r.status !== 0) fail('mcp_unreachable', { detail: r.out.trim().slice(0, 400) });\n // npx interleaves \"npm notice …\" lines, so pick the first semver-looking token.\n const version = (r.out.match(/\\d+\\.\\d+\\.\\d+[^\\s]*/) || [])[0] || r.out.trim().split('\\n').pop();\n emit('mcp_ok', { version });\n}\n\n// ── 2. Existing auth? ────────────────────────────────────────────────────────\n// An env token always wins (the MCP bypasses OAuth entirely when it is set).\n// Otherwise stored OAuth credentials mean a previous sign-in can be reused —\n// the MCP refreshes expired tokens automatically on the first tool call.\nfunction existingAuth() {\n if (process.env.HOSTINGER_API_TOKEN || process.env.API_TOKEN) {\n return 'api_token';\n }\n const base = isWin ? (process.env.APPDATA || os.homedir()) : path.join(os.homedir(), '.config');\n const credsPath = path.join(base, 'hostinger-mcp', 'credentials.json');\n if (existsSync(credsPath)) {\n try {\n const creds = JSON.parse(readFileSync(credsPath, 'utf8'));\n if (creds.refresh_token || (creds.access_token && creds.expires_at > Date.now())) {\n return 'oauth_cached';\n }\n } catch {\n /* unreadable credentials — fall through to a fresh login */\n }\n }\n return null;\n}\n\n// ── 3. Login (human-in-the-loop browser flow) ────────────────────────────────\n// `hostinger-api-mcp --login` opens the Hostinger sign-in page and listens on\n// an ephemeral localhost port for the OAuth callback, so the browser must run\n// on this same machine. It prints the sign-in URL to stderr with a stable\n// prefix — forward it so the agent can show it if the browser didn't open.\nfunction login() {\n return new Promise((resolve) => {\n const child = spawn(MCP[0], [...MCP.slice(1), '--login'], { shell: spawnShell });\n let diag = '';\n let urlSent = false;\n const collect = (chunk) => {\n diag = (diag + chunk).slice(-2000);\n if (!urlSent) {\n const m = diag.match(/Opening browser for sign-in:\\s*\\n\\s*(https?:\\/\\/\\S+)/);\n if (m) {\n urlSent = true;\n emit('awaiting_user', {\n verificationUri: m[1],\n note: 'A browser window should have opened. If not, open this URL manually on this machine and sign in.',\n });\n }\n }\n };\n child.stderr?.on('data', collect);\n child.stdout?.on('data', collect);\n child.on('close', (code) => {\n if (code === 0 && /Sign-in successful/i.test(diag)) {\n emit('logged_in', { method: 'oauth' });\n resolve();\n return;\n }\n const detail = diag.trim().slice(-1500) || `--login exited (code ${code}) with no output.`;\n fail('login_failed', { code, detail });\n });\n child.on('error', (e) => fail('login_failed', { detail: String(e) }));\n });\n}\n\n// ── main ─────────────────────────────────────────────────────────────────────\ncheckMcp();\nconst method = existingAuth();\nif (method) {\n emit('logged_in', { method });\n} else {\n await login();\n}\nemit('success');\n"
|
|
91
|
+
"text": "#!/usr/bin/env node\n// Hostinger Headless — bootstrap.\n// Verifies the Hostinger MCP server is reachable and drives the OAuth sign-in,\n// emitting one JSON event per line on stdout so a coding agent can relay\n// progress to the user. No filesystem writes besides what the MCP itself\n// stores (OAuth credentials); no network calls of its own.\nimport { spawn, spawnSync } from 'node:child_process';\nimport { existsSync, readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\n\n// ── tiny event protocol (one JSON object per line) ───────────────────────────\nconst emit = (event, extra = {}) =>\n process.stdout.write(JSON.stringify({ event, ...extra }) + '\\n');\nconst fail = (event, extra = {}) => {\n emit(event, { ok: false, ...extra });\n process.exit(1);\n};\n\n// ── platform-safe binary names (npm/npx are .cmd on Windows) ─────────────────\nconst isWin = process.platform === 'win32';\nconst bin = (name) => (isWin ? `${name}.cmd` : name);\n// Run the MCP via npx — no global install, always the latest published version.\n// Use --package so npx resolves the published package, then invoke the unified binary.\nconst MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp'];\n// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell\n// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all\n// fixed literals, so enabling the shell introduces no injection risk.\nconst spawnShell = isWin;\n\nfunction capture(cmd, args) {\n const r = spawnSync(cmd, args, { encoding: 'utf8', shell: spawnShell, cwd: os.homedir() });\n return { status: r.status ?? 1, out: `${r.stdout || ''}${r.stderr || ''}`, error: r.error };\n}\n\n// ── 1. MCP server reachable ──────────────────────────────────────────────────\nfunction checkMcp() {\n const r = capture(MCP[0], [...MCP.slice(1), '--version']);\n if (r.status !== 0) fail('mcp_unreachable', { detail: r.out.trim().slice(0, 400) });\n // npx interleaves \"npm notice …\" lines, so pick the first semver-looking token.\n const version = (r.out.match(/\\d+\\.\\d+\\.\\d+[^\\s]*/) || [])[0] || r.out.trim().split('\\n').pop();\n emit('mcp_ok', { version });\n}\n\n// ── 2. Existing auth? ────────────────────────────────────────────────────────\n// An env token always wins (the MCP bypasses OAuth entirely when it is set).\n// Otherwise stored OAuth credentials mean a previous sign-in can be reused —\n// the MCP refreshes expired tokens automatically on the first tool call.\nfunction existingAuth() {\n if (process.env.HOSTINGER_API_TOKEN || process.env.API_TOKEN) {\n return 'api_token';\n }\n const base = isWin ? (process.env.APPDATA || os.homedir()) : path.join(os.homedir(), '.config');\n const credsPath = path.join(base, 'hostinger-mcp', 'credentials.json');\n if (existsSync(credsPath)) {\n try {\n const creds = JSON.parse(readFileSync(credsPath, 'utf8'));\n if (creds.refresh_token || (creds.access_token && creds.expires_at > Date.now())) {\n return 'oauth_cached';\n }\n } catch {\n /* unreadable credentials — fall through to a fresh login */\n }\n }\n return null;\n}\n\n// ── 3. Login (human-in-the-loop browser flow) ────────────────────────────────\n// `hostinger-api-mcp --login` opens the Hostinger sign-in page and listens on\n// an ephemeral localhost port for the OAuth callback, so the browser must run\n// on this same machine. It prints the sign-in URL to stderr with a stable\n// prefix — forward it so the agent can show it if the browser didn't open.\nfunction login() {\n return new Promise((resolve) => {\n const child = spawn(MCP[0], [...MCP.slice(1), '--login'], { shell: spawnShell, cwd: os.homedir() });\n let diag = '';\n let urlSent = false;\n const collect = (chunk) => {\n diag = (diag + chunk).slice(-2000);\n if (!urlSent) {\n const m = diag.match(/Opening browser for sign-in:\\s*\\n\\s*(https?:\\/\\/\\S+)/);\n if (m) {\n urlSent = true;\n emit('awaiting_user', {\n verificationUri: m[1],\n note: 'A browser window should have opened. If not, open this URL manually on this machine and sign in.',\n });\n }\n }\n };\n child.stderr?.on('data', collect);\n child.stdout?.on('data', collect);\n child.on('close', (code) => {\n if (code === 0 && /Sign-in successful/i.test(diag)) {\n emit('logged_in', { method: 'oauth' });\n resolve();\n return;\n }\n const detail = diag.trim().slice(-1500) || `--login exited (code ${code}) with no output.`;\n fail('login_failed', { code, detail });\n });\n child.on('error', (e) => fail('login_failed', { detail: String(e) }));\n });\n}\n\n// ── main ─────────────────────────────────────────────────────────────────────\ncheckMcp();\nconst method = existingAuth();\nif (method) {\n emit('logged_in', { method });\n} else {\n await login();\n}\nemit('success');\n"
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"uri": "skill://hostinger-headless/connect/tools/antigravity.md",
|
|
95
|
-
"digest": "sha256:
|
|
96
|
-
"size":
|
|
95
|
+
"digest": "sha256:5fffcf33a1f45bcb18a80a449decbec1988edc3fde18e49e2a70c6d507222985",
|
|
96
|
+
"size": 2254,
|
|
97
97
|
"mimeType": "text/markdown",
|
|
98
|
-
"text": "---\nname: headless-connect-antigravity\ndescription: \"Specific Antigravity Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Google Antigravity.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Antigravity's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, add the missing server(s) to `~/.gemini/antigravity/mcp_config.json` (Windows:\n`C:\\Users\\<USERNAME>\\.gemini\\antigravity\\mcp_config.json`), which you can reach from the Agent panel's\n\"...\" → MCP Servers → Manage MCP Servers → View raw config. Merge them into any existing\n`mcpServers` rather than replacing the file. The bootstrap already signed you in, so they reuse the\ncached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are\nno tokens to handle:\n\n```json\n{\n \"mcpServers\": {\n \"hostinger\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@hostinger/mcp@latest\"]\n },\n \"hostinger-ecommerce\": {\n \"command\": \"npx\",\n \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nAntigravity loads MCP servers only when the editor starts, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to fully quit and reopen\nAntigravity and paste the same prompt. Never spawn the server yourself or write a JSON-RPC client to\nreach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
98
|
+
"text": "---\nname: headless-connect-antigravity\ndescription: \"Specific Antigravity Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Google Antigravity.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Antigravity's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, add the missing server(s) to `~/.gemini/antigravity/mcp_config.json` (Windows:\n`C:\\Users\\<USERNAME>\\.gemini\\antigravity\\mcp_config.json`), which you can reach from the Agent panel's\n\"...\" → MCP Servers → Manage MCP Servers → View raw config. Merge them into any existing\n`mcpServers` rather than replacing the file. The bootstrap already signed you in, so they reuse the\ncached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are\nno tokens to handle:\n\n```json\n{\n \"mcpServers\": {\n \"hostinger\": {\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@hostinger/mcp@latest\"]\n },\n \"hostinger-ecommerce\": {\n \"command\": \"npx\",\n \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nAntigravity loads MCP servers only when the editor starts, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to fully quit and reopen\nAntigravity and paste the same prompt. Never spawn the server yourself or write a JSON-RPC client to\nreach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
"uri": "skill://hostinger-headless/connect/tools/claude-code.md",
|
|
102
|
-
"digest": "sha256:
|
|
103
|
-
"size":
|
|
102
|
+
"digest": "sha256:d7f07f341f4b9cef21d97e16d5fe8b9d8cb58a0c075296934250d2b926c623e2",
|
|
103
|
+
"size": 2329,
|
|
104
104
|
"mimeType": "text/markdown",
|
|
105
|
-
"text": "---\nname: headless-connect-claude-code\ndescription: \"Specific Claude Code Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Your goal is to make Hostinger MCP tools and Skills available in Claude Code. \n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are available.\n\n\n## Step 1 — Make the MCP tools available\n\nStages 2–10 drive the Hostinger connector through Claude Code's native MCP tools. Check **both** tool\ngroups as *Check both tool groups* in `connect/SKILL.md` says:\n\n- **Hosting:** `mcp__hostinger__*`, or the scoped `mcp__hostinger-hosting__*`.\n- **Ecommerce:** `mcp__hostinger-ecommerce__*`. The scoped servers (`hostinger-hosting`,\n `hostinger-wordpress`, `hostinger-domains`, …) never include it, so a session full of\n `mcp__hostinger-*__*` tools can still be missing it.\n\nBoth present → use them and move on. Either missing → register the missing server(s) at user scope\nnow (`claude mcp list` shows what's already registered; skip those). The bootstrap already signed you\nin, so they reuse the cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or\n`HOSTINGER_API_TOKEN`) and there are no tokens to handle:\n\n```bash\nclaude mcp add hostinger --scope user -- npx -y @hostinger/mcp@latest # hosting group missing\nclaude mcp add hostinger-ecommerce --scope user -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp # ecommerce group missing\n```\n\nClaude Code loads MCP servers only at startup, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to exit Claude Code, start it\nagain in the same folder (`claude`, or `claude --continue` to keep this conversation) and paste the\nsame prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
105
|
+
"text": "---\nname: headless-connect-claude-code\ndescription: \"Specific Claude Code Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Your goal is to make Hostinger MCP tools and Skills available in Claude Code. \n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are available.\n\n\n## Step 1 — Make the MCP tools available\n\nStages 2–10 drive the Hostinger connector through Claude Code's native MCP tools. Check **both** tool\ngroups as *Check both tool groups* in `connect/SKILL.md` says:\n\n- **Hosting:** `mcp__hostinger__*`, or the scoped `mcp__hostinger-hosting__*`.\n- **Ecommerce:** `mcp__hostinger-ecommerce__*`. The scoped servers (`hostinger-hosting`,\n `hostinger-wordpress`, `hostinger-domains`, …) never include it, so a session full of\n `mcp__hostinger-*__*` tools can still be missing it.\n\nBoth present → use them and move on. Either missing → register the missing server(s) at user scope\nnow (`claude mcp list` shows what's already registered; skip those). The bootstrap already signed you\nin, so they reuse the cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or\n`HOSTINGER_API_TOKEN`) and there are no tokens to handle:\n\n```bash\nclaude mcp add hostinger --scope user -- npx -y @hostinger/mcp@latest # hosting group missing\nclaude mcp add hostinger-ecommerce --scope user -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp # ecommerce group missing\n```\n\nClaude Code loads MCP servers only at startup, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to exit Claude Code, start it\nagain in the same folder (`claude`, or `claude --continue` to keep this conversation) and paste the\nsame prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
106
106
|
},
|
|
107
107
|
{
|
|
108
108
|
"uri": "skill://hostinger-headless/connect/tools/codex.md",
|
|
109
|
-
"digest": "sha256:
|
|
110
|
-
"size":
|
|
109
|
+
"digest": "sha256:0c994eef605dea3b7321ae12fb30079faeeff944ebe9637477ecf46e4686e309",
|
|
110
|
+
"size": 2076,
|
|
111
111
|
"mimeType": "text/markdown",
|
|
112
|
-
"text": "---\nname: headless-connect-codex\ndescription: \"Specific Codex Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Codex.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Codex's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s); skip one already registered. The bootstrap already signed you in, so they reuse the cached\nsign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no tokens\nto handle:\n\n```bash\ncodex mcp add hostinger -- npx -y @hostinger/mcp@latest\ncodex mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp\n```\n\nor add them to `~/.codex/config.toml`:\n\n```toml\n[mcp_servers.hostinger]\ncommand = \"npx\"\nargs = [\"-y\", \"@hostinger/mcp@latest\"]\n\n[mcp_servers.hostinger-ecommerce]\ncommand = \"npx\"\nargs = [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n```\n\nCodex loads MCP servers only when it starts, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to restart Codex and paste the\nsame prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
112
|
+
"text": "---\nname: headless-connect-codex\ndescription: \"Specific Codex Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Codex.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Codex's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s); skip one already registered. The bootstrap already signed you in, so they reuse the cached\nsign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no tokens\nto handle:\n\n```bash\ncodex mcp add hostinger -- npx -y @hostinger/mcp@latest\ncodex mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp\n```\n\nor add them to `~/.codex/config.toml`:\n\n```toml\n[mcp_servers.hostinger]\ncommand = \"npx\"\nargs = [\"-y\", \"@hostinger/mcp@latest\"]\n\n[mcp_servers.hostinger-ecommerce]\ncommand = \"npx\"\nargs = [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n```\n\nCodex loads MCP servers only when it starts, so follow *register, restart, resume* in\n`connect/SKILL.md`: write `connect.json`, then ask the user to restart Codex and paste the\nsame prompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"uri": "skill://hostinger-headless/connect/tools/cursor.md",
|
|
116
|
-
"digest": "sha256:
|
|
117
|
-
"size":
|
|
116
|
+
"digest": "sha256:4851b75665d3f1f6a2d06d609cd13e7dc47caf99302ccc7b8bf27ceab4045e54",
|
|
117
|
+
"size": 4953,
|
|
118
118
|
"mimeType": "text/markdown",
|
|
119
|
-
"text": "---\nname: headless-connect-cursor\ndescription: \"Specific Cursor Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Install (or reach) the Hostinger MCP in Cursor.\n- Sign the user in with OAuth 2.0 PKCE — the MCP opens a browser so they only need to log in.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Install / reach Hostinger MCP\n\n**Prerequisite:** Node.js 20 or newer on `PATH` (`node -v`). The published package engines require `>=20`; use Node 24 if you follow the package README.\n\nThe Hostinger MCP is the npm package `@hostinger/mcp`. You do **not** need a global install — Cursor (and this session) can run it through `npx`. Prefer the unified binary `hostinger-api-mcp` (every operation). Scoped binaries such as `hostinger-hosting-mcp` are optional.\n\n### Option A — no global install (recommended)\n\n```bash\n# Verify the package/binary is reachable\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --version\n```\n\n### Option B — global install\n\n```bash\nnpm install -g @hostinger/mcp\n# or: yarn global add @hostinger/mcp\n# or: pnpm add -g @hostinger/mcp\n\nhostinger-api-mcp --version\n```\n\n### Register the servers in Cursor\n\nCheck **both** tool groups, as *Check both tool groups* in `connect/SKILL.md` says: hosting\n(`mcp__hostinger__*` or `mcp__hostinger-hosting__*`) **and** ecommerce (`mcp__hostinger-ecommerce__*`).\nThe scoped `mcp__hostinger-*__*` servers never include ecommerce, so they alone don't count. Both\npresent → skip registration: go to Step 2 only if you still need sign-in, otherwise go to Step 3.\n\nIf either is missing, register the missing server(s) in `~/.cursor/mcp.json` (global, so every project and every\nlater run has them). Merge them into any existing `mcpServers` rather than replacing the file.\n**Do not** put `HOSTINGER_API_TOKEN` in this file when you want OAuth: leave the token unset so\nOAuth can run:\n\n```json\n{\n \"mcpServers\": {\n \"hostinger\": {\n \"command\": \"npx\",\n \"args\": [\"--yes\", \"--package=@hostinger/mcp@latest\", \"hostinger-api-mcp\"]\n },\n \"hostinger-ecommerce\": {\n \"command\": \"npx\",\n \"args\": [\"--yes\", \"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nCursor loads MCP servers only when the editor starts, so after Step 2 write `connect.json` and follow\n*register, restart, resume* in `connect/SKILL.md`: ask the user to fully quit and reopen Cursor\n(and, if Cursor asks, enable the two Hostinger servers under Settings → MCP), then paste the same\nprompt. Never spawn the server yourself or write a JSON-RPC client to reach it: every connector call\nmust be a native MCP tool call.\n\n## Step 2 — Authenticate with OAuth (browser opens)\n\nPrefer OAuth for interactive Cursor use. OAuth works only in **stdio** mode (Cursor's default for this local server). Skip this step if `HOSTINGER_API_TOKEN` is already set in the environment (that bypasses OAuth entirely).\n\nIf the connect bootstrap already emitted `logged_in` / `success`, credentials are already on disk — continue to Step 3.\n\nOtherwise run the login command. It registers a dynamic OAuth client (once per machine), **opens the user's browser** to Hostinger's authorization page, listens on a localhost callback, stores tokens, and exits:\n\n```bash\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login\n```\n\n(With a global install: `hostinger-api-mcp --login`.)\n\nWhat to tell the user:\n\n1. A browser window should open automatically for Hostinger sign-in.\n2. If it does not, the process prints `[OAuth] Opening browser for sign-in:` plus the URL on stderr — show that URL and ask them to open it on **this same machine** (the callback is `http://127.0.0.1:<ephemeral-port>/oauth/callback`).\n3. Wait until the command prints that sign-in succeeded and exits 0.\n\nCredentials are stored and reused automatically:\n\n- macOS / Linux: `~/.config/hostinger-mcp/credentials.json` (mode 0600)\n- Windows: `%APPDATA%\\hostinger-mcp\\credentials.json`\n\nThey are shared across every Hostinger MCP binary. Expired access tokens refresh on their own; a revoked refresh token re-triggers the browser flow.\n\nTo sign out later:\n\n```bash\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout\n```\n\n**API token (optional, CI / scripts):** set `HOSTINGER_API_TOKEN` in the environment before launching Cursor. When present it always wins and no OAuth code runs. Do not commit the token.\n\n## Step 3 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
119
|
+
"text": "---\nname: headless-connect-cursor\ndescription: \"Specific Cursor Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Install (or reach) the Hostinger MCP in Cursor.\n- Sign the user in with OAuth 2.0 PKCE — the MCP opens a browser so they only need to log in.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Install / reach Hostinger MCP\n\n**Prerequisite:** Node.js 20 or newer on `PATH` (`node -v`). The published package engines require `>=20`; use Node 24 if you follow the package README.\n\nThe Hostinger MCP is the npm package `@hostinger/mcp`. You do **not** need a global install — Cursor (and this session) can run it through `npx`. Prefer the unified binary `hostinger-api-mcp` (every operation). Scoped binaries such as `hostinger-hosting-mcp` are optional.\n\n### Option A — no global install (recommended)\n\n```bash\n# Verify the package/binary is reachable\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --version\n```\n\n### Option B — global install\n\n```bash\nnpm install -g @hostinger/mcp\n# or: yarn global add @hostinger/mcp\n# or: pnpm add -g @hostinger/mcp\n\nhostinger-api-mcp --version\n```\n\n### Register the servers in Cursor\n\nCheck **both** tool groups, as *Check both tool groups* in `connect/SKILL.md` says: hosting\n(`mcp__hostinger__*` or `mcp__hostinger-hosting__*`) **and** ecommerce (`mcp__hostinger-ecommerce__*`).\nThe scoped `mcp__hostinger-*__*` servers never include ecommerce, so they alone don't count. Both\npresent → skip registration: go to Step 2 only if you still need sign-in, otherwise go to Step 3.\n\nIf either is missing, register the missing server(s) in `~/.cursor/mcp.json` (global, so every project and every\nlater run has them). Merge them into any existing `mcpServers` rather than replacing the file.\n**Do not** put `HOSTINGER_API_TOKEN` in this file when you want OAuth: leave the token unset so\nOAuth can run:\n\n```json\n{\n \"mcpServers\": {\n \"hostinger\": {\n \"command\": \"npx\",\n \"args\": [\"--yes\", \"--package=@hostinger/mcp@latest\", \"hostinger-api-mcp\"]\n },\n \"hostinger-ecommerce\": {\n \"command\": \"npx\",\n \"args\": [\"--yes\", \"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nCursor loads MCP servers only when the editor starts, so after Step 2 write `connect.json` and follow\n*register, restart, resume* in `connect/SKILL.md`: ask the user to fully quit and reopen Cursor\n(and, if Cursor asks, enable the two Hostinger servers under Settings → MCP), then paste the same\nprompt. Never spawn the server yourself or write a JSON-RPC client to reach it: every connector call\nmust be a native MCP tool call.\n\n## Step 2 — Authenticate with OAuth (browser opens)\n\nPrefer OAuth for interactive Cursor use. OAuth works only in **stdio** mode (Cursor's default for this local server). Skip this step if `HOSTINGER_API_TOKEN` is already set in the environment (that bypasses OAuth entirely).\n\nIf the connect bootstrap already emitted `logged_in` / `success`, credentials are already on disk — continue to Step 3.\n\nOtherwise run the login command. It registers a dynamic OAuth client (once per machine), **opens the user's browser** to Hostinger's authorization page, listens on a localhost callback, stores tokens, and exits:\n\n```bash\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --login\n```\n\n(With a global install: `hostinger-api-mcp --login`.)\n\nWhat to tell the user:\n\n1. A browser window should open automatically for Hostinger sign-in.\n2. If it does not, the process prints `[OAuth] Opening browser for sign-in:` plus the URL on stderr — show that URL and ask them to open it on **this same machine** (the callback is `http://127.0.0.1:<ephemeral-port>/oauth/callback`).\n3. Wait until the command prints that sign-in succeeded and exits 0.\n\nCredentials are stored and reused automatically:\n\n- macOS / Linux: `~/.config/hostinger-mcp/credentials.json` (mode 0600)\n- Windows: `%APPDATA%\\hostinger-mcp\\credentials.json`\n\nThey are shared across every Hostinger MCP binary. Expired access tokens refresh on their own; a revoked refresh token re-triggers the browser flow.\n\nTo sign out later:\n\n```bash\nnpx --yes --package=@hostinger/mcp@latest hostinger-api-mcp --logout\n```\n\n**API token (optional, CI / scripts):** set `HOSTINGER_API_TOKEN` in the environment before launching Cursor. When present it always wins and no OAuth code runs. Do not commit the token.\n\n## Step 3 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
120
120
|
},
|
|
121
121
|
{
|
|
122
122
|
"uri": "skill://hostinger-headless/connect/tools/devin.md",
|
|
123
|
-
"digest": "sha256:
|
|
124
|
-
"size":
|
|
123
|
+
"digest": "sha256:d7bdd2e2da9c1b06aa5bcb607bef72fa0282d548b9a4b81a02dd5836b80880a7",
|
|
124
|
+
"size": 2216,
|
|
125
125
|
"mimeType": "text/markdown",
|
|
126
|
-
"text": "---\nname: headless-connect-devin\ndescription: \"Specific Devin Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Devin.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Devin's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s) for Devin; skip one already registered. The bootstrap already signed you in, so they reuse the\ncached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no\ntokens to handle.\n\n- **Devin CLI:**\n\n ```bash\n devin mcp add hostinger -- npx -y @hostinger/mcp@latest\n devin mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp\n ```\n\n- **Devin app:** Settings → MCP Marketplace → **Add Your Own**, transport **STDIO**, once per server:\n\n ```json\n { \"command\": \"npx\", \"args\": [\"-y\", \"@hostinger/mcp@latest\"] }\n ```\n\n ```json\n { \"command\": \"npx\", \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"] }\n ```\n\nIf the tools still aren't in this session, follow *register, restart, resume* in `connect/SKILL.md`:\nwrite `connect.json`, then ask the user to restart the Devin session (on Devin Desktop,\nfully quit and reopen the app) and paste the same prompt. Never spawn the server yourself or write a\nJSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
126
|
+
"text": "---\nname: headless-connect-devin\ndescription: \"Specific Devin Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in Devin.\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through Devin's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s) for Devin; skip one already registered. The bootstrap already signed you in, so they reuse the\ncached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are no\ntokens to handle.\n\n- **Devin CLI:**\n\n ```bash\n devin mcp add hostinger -- npx -y @hostinger/mcp@latest\n devin mcp add hostinger-ecommerce -- npx --package=@hostinger/mcp@latest hostinger-ecommerce-mcp\n ```\n\n- **Devin app:** Settings → MCP Marketplace → **Add Your Own**, transport **STDIO**, once per server:\n\n ```json\n { \"command\": \"npx\", \"args\": [\"-y\", \"@hostinger/mcp@latest\"] }\n ```\n\n ```json\n { \"command\": \"npx\", \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"] }\n ```\n\nIf the tools still aren't in this session, follow *register, restart, resume* in `connect/SKILL.md`:\nwrite `connect.json`, then ask the user to restart the Devin session (on Devin Desktop,\nfully quit and reopen the app) and paste the same prompt. Never spawn the server yourself or write a\nJSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"uri": "skill://hostinger-headless/connect/tools/vscode.md",
|
|
130
|
-
"digest": "sha256:
|
|
131
|
-
"size":
|
|
130
|
+
"digest": "sha256:149bd7f16df386f7d7cee2301416b6bf120114a1f0d445aa9d0a04bc94d963a4",
|
|
131
|
+
"size": 2650,
|
|
132
132
|
"mimeType": "text/markdown",
|
|
133
|
-
"text": "---\nname: headless-connect-vscode\ndescription: \"Specific VS Code Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in VS Code (GitHub Copilot / agent mode).\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through VS Code's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s) for VS Code; skip one already registered. The bootstrap already signed you in, so they reuse\nthe cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are\nno tokens to handle. Add them to `.vscode/mcp.json` (workspace) or your user `settings.json` under\n`mcp`:\n\n```json\n// .vscode/mcp.json\n{\n \"servers\": {\n \"hostinger\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@hostinger/mcp@latest\"]\n },\n \"hostinger-ecommerce\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nOr add them in two commands:\n\n```bash\ncode --add-mcp '{\"name\":\"hostinger\",\"command\":\"npx\",\"args\":[\"-y\",\"@hostinger/mcp@latest\"]}'\ncode --add-mcp '{\"name\":\"hostinger-ecommerce\",\"command\":\"npx\",\"args\":[\"--package=@hostinger/mcp@latest\",\"hostinger-ecommerce-mcp\"]}'\n```\n\nVS Code picks up new servers and their tools become available right away, with no editor restart.\n(Alternatively, the official Hostinger Connector extension installs the same server:\n`code --install-extension hostinger-official.hostinger-connector`.)\n\nIf the tools still don't show up, follow *register, restart, resume* in `connect/SKILL.md`: write\n`connect.json`, then ask the user to run **Developer: Reload Window** and paste the same\nprompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed
|
|
133
|
+
"text": "---\nname: headless-connect-vscode\ndescription: \"Specific VS Code Hostinger MCP Server connection instructions for the Hostinger Headless flow.\"\n---\n\n## Goals\n\n- Make Hostinger MCP tools and Skills available in VS Code (GitHub Copilot / agent mode).\n- Don't provision hosting, create stores, install WordPress, or deploy by hand here — the skill does all of that. This entry stops at signed in and checking that Hostinger MCP and skills are reachable.\n\n## Step 1 — Make the MCP tools available\n\nThe later stages drive the Hostinger connector through VS Code's native MCP tools. Check **both** tool groups (hosting and ecommerce), as *Check both tool groups* in `connect/SKILL.md`\nsays: other Hostinger tools responding doesn't mean the `hostinger-ecommerce` ones are there. Both\nrespond → use them and move on.\n\nIf either is missing, register the missing server(s) for VS Code; skip one already registered. The bootstrap already signed you in, so they reuse\nthe cached sign-in (`~/.config/hostinger-mcp/credentials.json`, or `HOSTINGER_API_TOKEN`) and there are\nno tokens to handle. Add them to `.vscode/mcp.json` (workspace) or your user `settings.json` under\n`mcp`:\n\n```json\n// .vscode/mcp.json\n{\n \"servers\": {\n \"hostinger\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"-y\", \"@hostinger/mcp@latest\"]\n },\n \"hostinger-ecommerce\": {\n \"type\": \"stdio\",\n \"command\": \"npx\",\n \"args\": [\"--package=@hostinger/mcp@latest\", \"hostinger-ecommerce-mcp\"]\n }\n }\n}\n```\n\nOr add them in two commands:\n\n```bash\ncode --add-mcp '{\"name\":\"hostinger\",\"command\":\"npx\",\"args\":[\"-y\",\"@hostinger/mcp@latest\"]}'\ncode --add-mcp '{\"name\":\"hostinger-ecommerce\",\"command\":\"npx\",\"args\":[\"--package=@hostinger/mcp@latest\",\"hostinger-ecommerce-mcp\"]}'\n```\n\nVS Code picks up new servers and their tools become available right away, with no editor restart.\n(Alternatively, the official Hostinger Connector extension installs the same server:\n`code --install-extension hostinger-official.hostinger-connector`.)\n\nIf the tools still don't show up, follow *register, restart, resume* in `connect/SKILL.md`: write\n`connect.json`, then ask the user to run **Developer: Reload Window** and paste the same\nprompt. Never spawn the server yourself or write a JSON-RPC client to reach it.\n\n## Step 2 — Hand off\n\nThe skills are already in place: Phase 1 of `connect/SKILL.md` found the installed `<skill>`\nfolder or installed it from the `@hostinger/mcp` release. Don't download them again here. Read the\n`<skill>/VERSION` stamp (record it in `connect.json`), open `<skill>/SKILL.md` and follow its\nsteps. That skill owns the rest of the run.\n"
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
"uri": "skill://hostinger-headless/report.schema.json",
|
|
@@ -267,10 +267,10 @@ const skills: Skill[] = [
|
|
|
267
267
|
},
|
|
268
268
|
{
|
|
269
269
|
"uri": "skill://hostinger-headless/wait.mjs",
|
|
270
|
-
"digest": "sha256:
|
|
271
|
-
"size":
|
|
270
|
+
"digest": "sha256:8ab82b6d0fd9042bbdbb05d066433185d99665bd0ba892407c317336a55d02cb",
|
|
271
|
+
"size": 11627,
|
|
272
272
|
"mimeType": "text/plain",
|
|
273
|
-
"text": "#!/usr/bin/env node\n// Hostinger Headless wait: polls one slow Hostinger job to its end in a single command, so the agent\n// doesn't spend a model turn on every sleep-and-check.\n//\n// node wait.mjs setup <domain> # hosting_websites_list-setups row → completed (fresh site)\n// node wait.mjs cms <domain> <username> # wordpress_installations_list → /cms row is_valid (shell swap)\n// node wait.mjs build <domain> <buildUuid> # hosting_get-deployment → build completed, then the root serves 200\n// node wait.mjs live <domain> # https://<domain>/ serves 200\n// any of them: [--timeout 10m]\n//\n// It talks to the local Hostinger MCP server, which reuses the sign-in from the connect step. Progress\n// goes to stderr; stdout is one JSON line. Exit 0 when done, 1 when the job failed, timed out or hit an\n// error retrying can't fix (the JSON says which), 2 when it couldn't run (`fallback` names the\n// operation to poll by hand instead).\n\nimport { spawn } from 'node:child_process';\n\n// ── platform-safe binary names (npm/npx are .cmd on Windows), as in connect/bootstrap.mjs ──────────\nconst isWin = process.platform === 'win32';\nconst bin = (name) => (isWin ? `${name}.cmd` : name);\nconst MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp'];\n// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell\n// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all\n// fixed literals, so enabling the shell introduces no injection risk.\nconst spawnShell = isWin;\n\nconst log = (message) => process.stderr.write(`wait: ${message}\\n`);\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nlet fallback = null;\nconst finish = (code, result) => {\n process.stdout.write(`${JSON.stringify(result)}\\n`);\n process.exit(code);\n};\nconst fail = (message) => finish(2, { ok: false, error: message, ...(fallback ? { fallback } : {}) });\n\nfunction parseArgs(argv) {\n const positional = [];\n const flags = {};\n for (let i = 0; i < argv.length; i += 1) {\n if (argv[i].startsWith('--')) {\n const key = argv[i].slice(2);\n const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[(i += 1)] : true;\n flags[key] = value;\n } else positional.push(argv[i]);\n }\n return { positional, flags };\n}\n\nfunction duration(text) {\n const m = typeof text === 'string' && text.match(/^(\\d+(?:\\.\\d+)?)\\s*([smh])$/);\n if (!m) fail(`--timeout \"${text}\" should look like 90s, 10m or 1h`);\n return Number(m[1]) * { s: 1e3, m: 6e4, h: 36e5 }[m[2]];\n}\n\n// One MCP server for the whole wait, spoken to over newline-delimited JSON-RPC on stdio. Once the\n// server dies every pending and later call rejects, so the wait ends with exit 2 rather than hanging.\nfunction connect() {\n const child = spawn(MCP[0], MCP.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: spawnShell });\n const pending = new Map();\n let buffer = '';\n let diag = '';\n let dead = null;\n let nextId = 0;\n const die = (reason) => {\n dead ??= new Error(`the Hostinger MCP server stopped (${reason}): ${diag.trim().slice(-500)}`);\n for (const { reject } of pending.values()) reject(dead);\n pending.clear();\n };\n child.on('error', (e) => die(e.message));\n // 'close' comes after stderr is drained, so the reason is in diag. A write to a dying server fails\n // with EPIPE before that; 'close' still follows, so the EPIPE itself is ignored.\n child.on('close', (code) => die(`exit code ${code}`));\n child.stdin.on('error', () => {});\n child.stderr.on('data', (chunk) => (diag = (diag + chunk).slice(-2000)));\n child.stdout.on('data', (chunk) => {\n buffer += chunk;\n let i;\n while ((i = buffer.indexOf('\\n')) >= 0) {\n const line = buffer.slice(0, i);\n buffer = buffer.slice(i + 1);\n try {\n const message = JSON.parse(line);\n pending.get(message.id)?.resolve(message);\n pending.delete(message.id);\n } catch {\n /* not a JSON-RPC line */\n }\n }\n });\n // A call can stall on a live server (a stuck upstream request), so each one has its own deadline; past\n // it, it answers like an error reply. A stalled poll is retried; a stalled start ends the wait.\n const rpc = (method, params) =>\n new Promise((resolve, reject) => {\n if (dead) return reject(dead);\n const id = (nextId += 1);\n const timer = setTimeout(() => {\n pending.delete(id);\n resolve({ error: { message: `no reply to ${method} in 60 s` } });\n }, 60_000);\n const settle = (fn) => (value) => {\n clearTimeout(timer);\n fn(value);\n };\n pending.set(id, { resolve: settle(resolve), reject: settle(reject) });\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\\n`);\n });\n const ready = rpc('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'headless-wait', version: '1' } })\n .then((reply) => {\n if (reply.error) throw new Error(`the Hostinger MCP server didn't start: ${reply.error.message}`);\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\\n`);\n });\n // The server keeps \"HTTP <status>\" in its error text. A 409 while the setup runs, a 429 or a 5xx is a\n // poll that didn't answer; any other 4xx (expired sign-in) or an unknown domain won't fix itself by waiting.\n const execute = async (operation, params) => {\n await ready;\n const reply = await rpc('tools/call', { name: 'execute', arguments: { operation, params } });\n const text = reply.result?.content?.[0]?.text ?? reply.error?.message ?? 'unknown error';\n if (reply.error || reply.result?.isError) {\n const status = Number(text.match(/HTTP (\\d{3})/)?.[1]);\n const terminal = (status >= 400 && status < 500 && ![408, 409, 425, 429].includes(status)) || /No website found/.test(text);\n return { error: text.slice(0, 300), terminal };\n }\n try {\n return { data: JSON.parse(text) };\n } catch {\n return { error: text.slice(0, 300), terminal: false };\n }\n };\n return { execute, close: () => child.kill() };\n}\n\nasync function httpStatus(url) {\n try {\n const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(30_000) });\n return response.status;\n } catch {\n return 0;\n }\n}\n\n// Polls check() until it returns a result, with pauses growing from 5 s to 30 s.\nasync function poll(label, timeoutMs, check) {\n const started = Date.now();\n const waited = () => Math.round((Date.now() - started) / 1000);\n let pause = 5_000;\n let last = null;\n for (;;) {\n const outcome = await check();\n if (outcome.done) return { ...outcome.done, waited_s: waited() };\n if (outcome.terminal) return { ok: false, error: outcome.status, waited_s: waited() };\n if (outcome.status !== last) log(`${label}: ${outcome.status}`);\n last = outcome.status;\n if (Date.now() - started + pause > timeoutMs) return { ok: false, timed_out: true, last_status: last, waited_s: waited() };\n await sleep(pause);\n pause = Math.min(pause * 1.5, 30_000);\n }\n}\n\n// List operations answer an array, or an object holding it in `data`; their domain filter is loose.\nconst rowsFor = (data, domain) =>\n (Array.isArray(data) ? data : data?.data ?? []).filter((r) => r.domain?.toLowerCase() === domain.toLowerCase());\n\nconst waitLive = (domain, timeoutMs) =>\n poll('live', timeoutMs, async () => {\n const status = await httpStatus(`https://${domain}/`);\n return status === 200 ? { done: { ok: true, live: `https://${domain}/` } } : { status: `root ${status || 'unreachable'}` };\n });\n\nconst KINDS = {\n setup: {\n args: [],\n timeout: '10m',\n fallback: 'hosting_websites_list-setups',\n run: (mcp, { domain }, timeoutMs) =>\n poll('setup', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('hosting_websites_list-setups', { domain });\n if (error) return { status: `list-setups: ${error}`, terminal };\n const [row] = rowsFor(data, domain);\n if (row?.status === 'completed') return { done: { ok: true, setup: row } };\n if (row?.status === 'failed') return { done: { ok: false, setup: row } };\n return { status: row ? row.status : 'no setup row yet' };\n }),\n },\n\n cms: {\n args: ['username'],\n timeout: '5m',\n fallback: 'wordpress_installations_list',\n // The list can lag a few seconds behind the install finishing and answer [], so an empty list is a wait.\n run: (mcp, { domain, username }, timeoutMs) =>\n poll('cms', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('wordpress_installations_list', { username, domain, ownership: 'all' });\n if (error) return { status: `installations: ${error}`, terminal };\n const rows = rowsFor(data, domain).filter((r) => r.directory === 'cms');\n const valid = rows.find((r) => r.is_valid ?? r.valid);\n if (valid) return { done: { ok: true, install: valid } };\n return { status: rows.length ? `/cms install not valid yet: ${rows[0].validationError ?? 'pending'}` : 'no /cms install yet' };\n }),\n },\n\n build: {\n args: ['buildUuid'],\n timeout: '15m',\n fallback: 'hosting_get-deployment',\n run: async (mcp, { domain, buildUuid }, timeoutMs) => {\n const started = Date.now();\n const build = await poll('build', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('hosting_get-deployment', { domain, buildUuid });\n if (error) return { status: `get-deployment: ${error}`, terminal };\n const state = data?.build?.state;\n if (state === 'failed') {\n const logTail = String(data.logs?.logs ?? '').split('\\n').slice(-40).join('\\n');\n return { done: { ok: false, build: data.build, analysis: data.analysis, log_tail: logTail } };\n }\n if (state === 'completed') return { done: { ok: true, build: data.build, buildSettings: data.buildSettings } };\n return { status: `build ${state ?? 'not found yet'}` };\n });\n if (!build.ok) return build;\n // Settings that didn't take make the root 403 for good, so don't wait for it to go live.\n const missing = ['app_type', 'output_directory', 'node_version'].filter((k) => build.buildSettings?.[k] == null);\n if (missing.length) return { ...build, ok: false, error: `build settings missing: ${missing.join(', ')}; pass them on hosting_deploy and redeploy` };\n const live = await waitLive(domain, timeoutMs - (Date.now() - started));\n return { ...build, ...live, waited_s: Math.round((Date.now() - started) / 1000) };\n },\n },\n\n live: {\n args: [],\n timeout: '5m',\n run: (_mcp, { domain }, timeoutMs) => waitLive(domain, timeoutMs),\n },\n};\n\nconst { positional, flags } = parseArgs(process.argv.slice(2));\nconst [kind, domain, ...rest] = positional;\nconst spec = KINDS[kind];\nconst usage = Object.entries(KINDS).map(([k, s]) => `wait.mjs ${k} <domain>${s.args.map((a) => ` <${a}>`).join('')}`).join(' | ');\nif (!spec || !domain || rest.length < spec.args.length) fail(`usage: ${usage} [--timeout 10m]`);\nconst params = { domain, ...Object.fromEntries(spec.args.map((a, i) => [a, rest[i]])) };\nconst timeoutMs = duration(flags.timeout ?? spec.timeout);\nfallback = spec.fallback ?? null;\nconst mcp = spec.fallback ? connect() : null;\nconst result = await spec.run(mcp, params, timeoutMs).catch((e) => fail(e.message));\nmcp?.close();\nfinish(result.ok ? 0 : 1, { wait: kind, domain, ...result });\n"
|
|
273
|
+
"text": "#!/usr/bin/env node\n// Hostinger Headless wait: polls one slow Hostinger job to its end in a single command, so the agent\n// doesn't spend a model turn on every sleep-and-check.\n//\n// node wait.mjs setup <domain> # hosting_websites_list-setups row → completed (fresh site)\n// node wait.mjs cms <domain> <username> # wordpress_installations_list → /cms row is_valid (shell swap)\n// node wait.mjs build <domain> <buildUuid> # hosting_get-deployment → build completed, then the root serves 200\n// node wait.mjs live <domain> # https://<domain>/ serves 200\n// any of them: [--timeout 10m]\n//\n// It talks to the local Hostinger MCP server, which reuses the sign-in from the connect step. Progress\n// goes to stderr; stdout is one JSON line. Exit 0 when done, 1 when the job failed, timed out or hit an\n// error retrying can't fix (the JSON says which), 2 when it couldn't run (`fallback` names the\n// operation to poll by hand instead).\n\nimport { spawn } from 'node:child_process';\nimport { homedir } from 'node:os';\n\n// ── platform-safe binary names (npm/npx are .cmd on Windows), as in connect/bootstrap.mjs ──────────\nconst isWin = process.platform === 'win32';\nconst bin = (name) => (isWin ? `${name}.cmd` : name);\nconst MCP = [bin('npx'), '-y', '--package=@hostinger/mcp@latest', 'hostinger-api-mcp'];\n// Node >= 18.20.2 / 20.12.2 refuses to spawn a .cmd/.bat without a shell\n// (CVE-2024-27980), so npx.cmd needs shell:true on Windows. Our args are all\n// fixed literals, so enabling the shell introduces no injection risk.\nconst spawnShell = isWin;\n\nconst log = (message) => process.stderr.write(`wait: ${message}\\n`);\nconst sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));\nlet fallback = null;\nconst finish = (code, result) => {\n process.stdout.write(`${JSON.stringify(result)}\\n`);\n process.exit(code);\n};\nconst fail = (message) => finish(2, { ok: false, error: message, ...(fallback ? { fallback } : {}) });\n\nfunction parseArgs(argv) {\n const positional = [];\n const flags = {};\n for (let i = 0; i < argv.length; i += 1) {\n if (argv[i].startsWith('--')) {\n const key = argv[i].slice(2);\n const value = argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[(i += 1)] : true;\n flags[key] = value;\n } else positional.push(argv[i]);\n }\n return { positional, flags };\n}\n\nfunction duration(text) {\n const m = typeof text === 'string' && text.match(/^(\\d+(?:\\.\\d+)?)\\s*([smh])$/);\n if (!m) fail(`--timeout \"${text}\" should look like 90s, 10m or 1h`);\n return Number(m[1]) * { s: 1e3, m: 6e4, h: 36e5 }[m[2]];\n}\n\n// One MCP server for the whole wait, spoken to over newline-delimited JSON-RPC on stdio. Once the\n// server dies every pending and later call rejects, so the wait ends with exit 2 rather than hanging.\nfunction connect() {\n const child = spawn(MCP[0], MCP.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], shell: spawnShell, cwd: homedir() });\n const pending = new Map();\n let buffer = '';\n let diag = '';\n let dead = null;\n let nextId = 0;\n const die = (reason) => {\n dead ??= new Error(`the Hostinger MCP server stopped (${reason}): ${diag.trim().slice(-500)}`);\n for (const { reject } of pending.values()) reject(dead);\n pending.clear();\n };\n child.on('error', (e) => die(e.message));\n // 'close' comes after stderr is drained, so the reason is in diag. A write to a dying server fails\n // with EPIPE before that; 'close' still follows, so the EPIPE itself is ignored.\n child.on('close', (code) => die(`exit code ${code}`));\n child.stdin.on('error', () => {});\n child.stderr.on('data', (chunk) => (diag = (diag + chunk).slice(-2000)));\n child.stdout.on('data', (chunk) => {\n buffer += chunk;\n let i;\n while ((i = buffer.indexOf('\\n')) >= 0) {\n const line = buffer.slice(0, i);\n buffer = buffer.slice(i + 1);\n try {\n const message = JSON.parse(line);\n pending.get(message.id)?.resolve(message);\n pending.delete(message.id);\n } catch {\n /* not a JSON-RPC line */\n }\n }\n });\n // A call can stall on a live server (a stuck upstream request), so each one has its own deadline; past\n // it, it answers like an error reply. A stalled poll is retried; a stalled start ends the wait.\n const rpc = (method, params) =>\n new Promise((resolve, reject) => {\n if (dead) return reject(dead);\n const id = (nextId += 1);\n const timer = setTimeout(() => {\n pending.delete(id);\n resolve({ error: { message: `no reply to ${method} in 60 s` } });\n }, 60_000);\n const settle = (fn) => (value) => {\n clearTimeout(timer);\n fn(value);\n };\n pending.set(id, { resolve: settle(resolve), reject: settle(reject) });\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\\n`);\n });\n const ready = rpc('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'headless-wait', version: '1' } })\n .then((reply) => {\n if (reply.error) throw new Error(`the Hostinger MCP server didn't start: ${reply.error.message}`);\n child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\\n`);\n });\n // The server keeps \"HTTP <status>\" in its error text. A 409 while the setup runs, a 429 or a 5xx is a\n // poll that didn't answer; any other 4xx (expired sign-in) or an unknown domain won't fix itself by waiting.\n const execute = async (operation, params) => {\n await ready;\n const reply = await rpc('tools/call', { name: 'execute', arguments: { operation, params } });\n const text = reply.result?.content?.[0]?.text ?? reply.error?.message ?? 'unknown error';\n if (reply.error || reply.result?.isError) {\n const status = Number(text.match(/HTTP (\\d{3})/)?.[1]);\n const terminal = (status >= 400 && status < 500 && ![408, 409, 425, 429].includes(status)) || /No website found/.test(text);\n return { error: text.slice(0, 300), terminal };\n }\n try {\n return { data: JSON.parse(text) };\n } catch {\n return { error: text.slice(0, 300), terminal: false };\n }\n };\n return { execute, close: () => child.kill() };\n}\n\nasync function httpStatus(url) {\n try {\n const response = await fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(30_000) });\n return response.status;\n } catch {\n return 0;\n }\n}\n\n// Polls check() until it returns a result, with pauses growing from 5 s to 30 s.\nasync function poll(label, timeoutMs, check) {\n const started = Date.now();\n const waited = () => Math.round((Date.now() - started) / 1000);\n let pause = 5_000;\n let last = null;\n for (;;) {\n const outcome = await check();\n if (outcome.done) return { ...outcome.done, waited_s: waited() };\n if (outcome.terminal) return { ok: false, error: outcome.status, waited_s: waited() };\n if (outcome.status !== last) log(`${label}: ${outcome.status}`);\n last = outcome.status;\n if (Date.now() - started + pause > timeoutMs) return { ok: false, timed_out: true, last_status: last, waited_s: waited() };\n await sleep(pause);\n pause = Math.min(pause * 1.5, 30_000);\n }\n}\n\n// List operations answer an array, or an object holding it in `data`; their domain filter is loose.\nconst rowsFor = (data, domain) =>\n (Array.isArray(data) ? data : data?.data ?? []).filter((r) => r.domain?.toLowerCase() === domain.toLowerCase());\n\nconst waitLive = (domain, timeoutMs) =>\n poll('live', timeoutMs, async () => {\n const status = await httpStatus(`https://${domain}/`);\n return status === 200 ? { done: { ok: true, live: `https://${domain}/` } } : { status: `root ${status || 'unreachable'}` };\n });\n\nconst KINDS = {\n setup: {\n args: [],\n timeout: '10m',\n fallback: 'hosting_websites_list-setups',\n run: (mcp, { domain }, timeoutMs) =>\n poll('setup', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('hosting_websites_list-setups', { domain });\n if (error) return { status: `list-setups: ${error}`, terminal };\n const [row] = rowsFor(data, domain);\n if (row?.status === 'completed') return { done: { ok: true, setup: row } };\n if (row?.status === 'failed') return { done: { ok: false, setup: row } };\n return { status: row ? row.status : 'no setup row yet' };\n }),\n },\n\n cms: {\n args: ['username'],\n timeout: '5m',\n fallback: 'wordpress_installations_list',\n // The list can lag a few seconds behind the install finishing and answer [], so an empty list is a wait.\n run: (mcp, { domain, username }, timeoutMs) =>\n poll('cms', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('wordpress_installations_list', { username, domain, ownership: 'all' });\n if (error) return { status: `installations: ${error}`, terminal };\n const rows = rowsFor(data, domain).filter((r) => r.directory === 'cms');\n const valid = rows.find((r) => r.is_valid ?? r.valid);\n if (valid) return { done: { ok: true, install: valid } };\n return { status: rows.length ? `/cms install not valid yet: ${rows[0].validationError ?? 'pending'}` : 'no /cms install yet' };\n }),\n },\n\n build: {\n args: ['buildUuid'],\n timeout: '15m',\n fallback: 'hosting_get-deployment',\n run: async (mcp, { domain, buildUuid }, timeoutMs) => {\n const started = Date.now();\n const build = await poll('build', timeoutMs, async () => {\n const { data, error, terminal } = await mcp.execute('hosting_get-deployment', { domain, buildUuid });\n if (error) return { status: `get-deployment: ${error}`, terminal };\n const state = data?.build?.state;\n if (state === 'failed') {\n const logTail = String(data.logs?.logs ?? '').split('\\n').slice(-40).join('\\n');\n return { done: { ok: false, build: data.build, analysis: data.analysis, log_tail: logTail } };\n }\n if (state === 'completed') return { done: { ok: true, build: data.build, buildSettings: data.buildSettings } };\n return { status: `build ${state ?? 'not found yet'}` };\n });\n if (!build.ok) return build;\n // Settings that didn't take make the root 403 for good, so don't wait for it to go live.\n const missing = ['app_type', 'output_directory', 'node_version'].filter((k) => build.buildSettings?.[k] == null);\n if (missing.length) return { ...build, ok: false, error: `build settings missing: ${missing.join(', ')}; pass them on hosting_deploy and redeploy` };\n const live = await waitLive(domain, timeoutMs - (Date.now() - started));\n return { ...build, ...live, waited_s: Math.round((Date.now() - started) / 1000) };\n },\n },\n\n live: {\n args: [],\n timeout: '5m',\n run: (_mcp, { domain }, timeoutMs) => waitLive(domain, timeoutMs),\n },\n};\n\nconst { positional, flags } = parseArgs(process.argv.slice(2));\nconst [kind, domain, ...rest] = positional;\nconst spec = KINDS[kind];\nconst usage = Object.entries(KINDS).map(([k, s]) => `wait.mjs ${k} <domain>${s.args.map((a) => ` <${a}>`).join('')}`).join(' | ');\nif (!spec || !domain || rest.length < spec.args.length) fail(`usage: ${usage} [--timeout 10m]`);\nconst params = { domain, ...Object.fromEntries(spec.args.map((a, i) => [a, rest[i]])) };\nconst timeoutMs = duration(flags.timeout ?? spec.timeout);\nfallback = spec.fallback ?? null;\nconst mcp = spec.fallback ? connect() : null;\nconst result = await spec.run(mcp, params, timeoutMs).catch((e) => fail(e.message));\nmcp?.close();\nfinish(result.ok ? 0 : 1, { wait: kind, domain, ...result });\n"
|
|
274
274
|
}
|
|
275
275
|
]
|
|
276
276
|
},
|
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/agency-hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.agency-hosting.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.28.
|
|
7
|
+
startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.28.6', tools, instructions });
|
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/agency-hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.agency-hosting.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.28.
|
|
7
|
+
startServer({ name: 'hostinger-agency-hosting-mcp', version: '3.28.6', tools, instructions });
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
import { startServer } from '../core/runtime.js';
|
|
4
4
|
import tools from '../core/tools/agent-readiness.js';
|
|
5
5
|
|
|
6
|
-
startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.28.
|
|
6
|
+
startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.28.6', tools });
|
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
import { startServer } from '../core/runtime.js';
|
|
4
4
|
import tools from '../core/tools/agent-readiness.js';
|
|
5
5
|
|
|
6
|
-
startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.28.
|
|
6
|
+
startServer({ name: 'hostinger-agent-readiness-mcp', version: '3.28.6', tools });
|
package/src/servers/all.js
CHANGED
|
@@ -5,4 +5,4 @@ import tools from '../core/tools/all.js';
|
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
import skills from '../core/skills.js';
|
|
7
7
|
|
|
8
|
-
startServer({ name: 'hostinger-api-mcp', version: '3.28.
|
|
8
|
+
startServer({ name: 'hostinger-api-mcp', version: '3.28.6', tools, instructions, skills });
|
package/src/servers/all.ts
CHANGED
|
@@ -5,4 +5,4 @@ import tools from '../core/tools/all.js';
|
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
import skills from '../core/skills.js';
|
|
7
7
|
|
|
8
|
-
startServer({ name: 'hostinger-api-mcp', version: '3.28.
|
|
8
|
+
startServer({ name: 'hostinger-api-mcp', version: '3.28.6', tools, instructions, skills });
|
package/src/servers/billing.js
CHANGED
package/src/servers/billing.ts
CHANGED
package/src/servers/dns.js
CHANGED
package/src/servers/dns.ts
CHANGED
package/src/servers/domains.js
CHANGED
package/src/servers/domains.ts
CHANGED
package/src/servers/ecommerce.js
CHANGED
package/src/servers/ecommerce.ts
CHANGED
package/src/servers/gpu.js
CHANGED
package/src/servers/gpu.ts
CHANGED
package/src/servers/horizons.js
CHANGED
package/src/servers/horizons.ts
CHANGED
package/src/servers/hosting.js
CHANGED
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-hosting-mcp', version: '3.28.
|
|
7
|
+
startServer({ name: 'hostinger-hosting-mcp', version: '3.28.6', tools, instructions });
|
package/src/servers/hosting.ts
CHANGED
|
@@ -4,4 +4,4 @@ import { startServer } from '../core/runtime.js';
|
|
|
4
4
|
import tools from '../core/tools/hosting.js';
|
|
5
5
|
import instructions from '../core/instructions.js';
|
|
6
6
|
|
|
7
|
-
startServer({ name: 'hostinger-hosting-mcp', version: '3.28.
|
|
7
|
+
startServer({ name: 'hostinger-hosting-mcp', version: '3.28.6', tools, instructions });
|
package/src/servers/mail.js
CHANGED
package/src/servers/mail.ts
CHANGED
package/src/servers/reach.js
CHANGED
package/src/servers/reach.ts
CHANGED
package/src/servers/vps.js
CHANGED
package/src/servers/vps.ts
CHANGED
package/src/servers/wordpress.js
CHANGED
package/src/servers/wordpress.ts
CHANGED