Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "hostinger",
"description": "Deploy, manage and monitor Hostinger services — Websites, Domains & DNS, Ecommerce, Email, Email Marketing, WordPress, Subscriptions & Payments, and VPS. Runs as a remote MCP server with browser (OAuth) sign-in — nothing to install locally.",
"version": "1.2.0",
"version": "1.3.0",
"author": {
"name": "Hostinger",
"url": "https://www.hostinger.com"
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/skills-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: skills-drift

on:
pull_request:
push:
branches: [main]
schedule:
- cron: "0 6 * * 1"

jobs:
skills-drift:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 22

- name: Regenerate skills from the latest published server
run: node scripts/sync-skills.mjs

- name: Report drift
run: |
if [ -z "$(git status --porcelain -- skills)" ]; then
echo "Skills are up to date with hostinger-api-mcp@latest."
else
echo "::warning::skills/ is behind hostinger-api-mcp@latest."
echo "Run 'node scripts/sync-skills.mjs' and commit the result."
git status --short -- skills
fi
36 changes: 20 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,22 @@ A single remote Hostinger MCP server at `https://mcp.hostinger.com`, covering:
| Subscriptions & Payments | Subscriptions, payment methods, catalog, orders |
| VPS | Virtual servers, firewalls, snapshots, monitoring |

Plus seven **deployment skills** that let the agent push your project files to
Hostinger and deploy them:
Plus seven **web hosting skills** for Shared, Cloud and Agency plans:

| Skill | Use for |
|---|---|
| `hosting-deploy-static-site` | Pre-built static site (no build step) |
| `hosting-deploy-nodejs-app` | Node.js app — built on Hostinger |
| `hosting-deploy-wordpress-site` | Import a WordPress site (archive + SQL dump) |
| `hosting-deploy-wordpress-plugin` | Deploy a WordPress plugin |
| `hosting-deploy-wordpress-theme` | Deploy a WordPress theme |
| `agency-hosting-deploy-static-site` | Agency Plan static / node-static site |
| `agency-hosting-deploy-php-site` | Agency Plan PHP app, deployed as-is |

The agent picks the right one from your request — you don't invoke them by name.
| `troubleshoot-website` | A site that is down, slow, erroring, insecure or failing to build — cause and fix |
| `connect-domain` | Attach a domain, point DNS without losing email records, install SSL |
| `deploy-to-hosting` | Deploy static sites, Node.js apps, PHP apps, WordPress plugins and themes; Git auto-deploy, environment variables, databases |
| `maintain-wordpress` | Updates and vulnerability checks across one or all WordPress sites |
| `audit-hosting` | Read-only review of the whole hosting account with a prioritised to-do list |
| `migrate-to-hosting` | Move a site from another host, tested before DNS moves |
| `hostinger-headless` | Build a new site from a prompt, optionally with a store or a WordPress backend |

The agent picks the right one from your request; you can also name a skill. The
skills come from [hostinger/api-mcp-server](https://github.com/hostinger/api-mcp-server)
and are synced into `skills/` with `node scripts/sync-skills.mjs` — change them
upstream, not here.

## Installation

Expand Down Expand Up @@ -68,16 +70,18 @@ export HOSTINGER_API_TOKEN="your-token-here"
The remote server can't read files off your machine, so deploys run in three
stages, all driven by the agent:

1. **Get a short-lived upload URL** — `hosting_generateUploadURLV1` (or the
`agency-hosting` equivalent) returns a URL plus `auth_key` / `rest_auth_key`.
1. **Get a short-lived upload URL** — `hosting_files_generate-upload-url` (or
`agency-hosting_files_generate-upload-url`) returns a URL plus `auth_key` /
`rest_auth_key`.
2. **Upload the archive over TUS** — plain `curl`, authenticated with those keys.
This is the one step with no tool wrapper, because it talks to the file-storage
host directly.
3. **Trigger the deploy or build** — an MCP tool call referencing the uploaded
filename.
file, e.g. `hosting_websites_deploy-static-site-archive` or
`hosting_nodejs_start-build`.

The skills document each variant of this flow, including the destructive steps
that overwrite a site's contents.
The `deploy-to-hosting` skill documents each variant of this flow, including the
destructive steps that overwrite a site's contents.

> **Deployment requires an agent with shell access**, since the upload step runs
> `curl`. Everything else — domains, DNS, VPS, WordPress management, email —
Expand Down
69 changes: 69 additions & 0 deletions scripts/sync-skills.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import { cpSync, existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import process from "node:process";

const source = process.argv[2] ?? "latest";
const outDir = path.join(import.meta.dirname, "..", "skills");
const work = mkdtempSync(path.join(tmpdir(), "hostinger-skills-"));

function resolveSkillsRoot() {
if (existsSync(source) && statSync(source).isDirectory()) {
const root = path.resolve(source);
return { root, label: root };
}

const spec = `hostinger-api-mcp@${source}`;
const packed = execFileSync("npm", ["pack", spec, "--silent", "--pack-destination", work], {
encoding: "utf8",
})
.trim()
.split("\n")
.pop();
execFileSync("tar", ["xzf", path.join(work, packed), "-C", work]);

const pkgRoot = path.join(work, "package");
const { version } = JSON.parse(readFileSync(path.join(pkgRoot, "package.json"), "utf8"));
return { root: path.join(pkgRoot, "skills"), label: `hostinger-api-mcp@${version}` };
}

function skillName(skillFile) {
const text = readFileSync(skillFile, "utf8").replace(/\r\n/g, "\n");
const end = text.indexOf("\n---\n", 4);
const frontmatter = text.startsWith("---\n") && end !== -1 ? text.slice(4, end) : "";
const match = frontmatter.match(/^name:\s*["']?([a-z0-9]+(?:-[a-z0-9]+)*)["']?\s*$/m);
if (!match) {
throw new Error(`No valid name in the frontmatter of ${skillFile}`);
}
return match[1];
}

try {
const { root, label } = resolveSkillsRoot();
const folders = readdirSync(root)
.filter((folder) => existsSync(path.join(root, folder, "SKILL.md")))
.sort();
if (folders.length === 0) {
throw new Error(`No skills found in ${root}`);
}

rmSync(outDir, { recursive: true, force: true });

const names = [];
for (const folder of folders) {
const from = path.join(root, folder);
const name = skillName(path.join(from, "SKILL.md"));
cpSync(from, path.join(outDir, name), {
recursive: true,
filter: (src) => path.relative(from, src).split(path.sep)[0] !== "entry",
});
names.push(name);
}

console.log(`Synced ${names.length} skills from ${label}: ${names.join(", ")}`);
} finally {
rmSync(work, { recursive: true, force: true });
}
74 changes: 0 additions & 74 deletions skills/agency-hosting-deploy-php-site/SKILL.md

This file was deleted.

87 changes: 0 additions & 87 deletions skills/agency-hosting-deploy-static-site/SKILL.md

This file was deleted.

Loading
Loading